External turn protocol
The versioned protocol Lowkey speaks to a plugin-hosted external agent runtime
(runtime: external:<pluginId> in the agent block).
It is the public stability boundary for Tier-B agents: it intentionally does not
expose Lowkey’s internal AgentEvent type, so internal refactors cannot break your
plugin. Treat this schema — not Lowkey’s source — as the contract.
This is a declarative seam: the
agentblock names anendpointyou host, and Lowkey POSTs each turn to it directly (no jail code involved). Because it needs nomain.ts, a pure external-agent plugin is installable by operator sideload only — the web installer requires an entry module. A sandboxed package that also wants an external agent can declare this block alongside its capabilities.
- Version:
1(EXTERNAL_AGENT_TURN_PROTOCOL_VERSION). - Published JSON Schema (draft 2020-12):
$idhttps://schemas.lowkeyagents.com/plugins/external-turn-protocol.schema.json(served at/schemas/plugins/external-turn-protocol.schema.json; in-repopackages/shared/src/plugins/external-turn-protocol.schema.json). The schema’s root is theExternalAgentTurnRequest; the stream event shape is published under$defs/ExternalAgentTurnEvent. Validate against it — do not read Lowkey’s source. - In-repo types (source of truth):
packages/shared/src/plugins/external-runtime.ts.
Transport
Section titled “Transport”For each turn, Lowkey POSTs an ExternalAgentTurnRequest to your endpoint.
Your service responds with a newline-delimited stream of
ExternalAgentTurnEvent objects (one JSON object per line). Lowkey reads events as
they arrive and streams them into the session.
External runtimes have no Lowkey provider-session to resume, so the request carries the context you need: a stable session handle plus a recent transcript window.
Request — ExternalAgentTurnRequest
Section titled “Request — ExternalAgentTurnRequest”{ "protocolVersion": 1, "pluginId": "muse", "sessionHandle": { "id": "…", // stable key for this session+agent pairing; use as your resume/cache key "projectPath": "/abs/project", "sessionId": "…", "agentId": "muse" }, "turn": { "turnId": "…", "message": "…the user's message…", "headless": false, "userName": "alvin" // optional }, "transcriptWindow": [ { "role": "user" | "agent" | "system", "content": "…", "timestamp": 1719000000000, // optional "agentId": "…", // optional "agentName": "…", // optional "turnId": "…" // optional } ], "limits": { "transcriptWindowMessages": 40 }}sessionHandle.id— use this as your resume/cache key. Do not expect a provider-specific session id from Lowkey.transcriptWindow— the recent history, oldest-first, capped bylimits.transcriptWindowMessages. Roles are normalized touser/agent/system.
Response — newline-delimited ExternalAgentTurnEvent
Section titled “Response — newline-delimited ExternalAgentTurnEvent”Each line is one event, and each event echoes protocolVersion:
{ "protocolVersion": 1, "type": "status", "status": "started" }{ "protocolVersion": 1, "type": "text", "content": "Partial output…" }{ "protocolVersion": 1, "type": "text", "content": "…more output." }{ "protocolVersion": 1, "type": "status", "status": "complete" }type |
Fields | Meaning |
|---|---|---|
status |
status?: started|working|complete|error|cancelled; content? |
Lifecycle signal. Emit complete to end the turn cleanly. |
text |
content (required) |
A chunk of agent output, streamed into the session. |
error |
content (required) |
A terminal error; ends the turn. |
- Version every payload. Lowkey sends
protocolVersion: 1and expects it back on each event. A future protocol version is additive and announced; pin to the version you implement. - Endpoint URL must be absolute
http/httpsand is validated at manifest load. In v1 the external endpoint runs under sideload trust (no standing outbound auth from Lowkey to your endpoint); production hardening (outbound auth, https-only enforcement) is tracked separately. - This protocol is the LSP-style boundary: the seam stays stable even as Lowkey’s internal turn machinery changes.