Skip to content

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 agent block names an endpoint you host, and Lowkey POSTs each turn to it directly (no jail code involved). Because it needs no main.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): $id https://schemas.lowkeyagents.com/plugins/external-turn-protocol.schema.json (served at /schemas/plugins/external-turn-protocol.schema.json; in-repo packages/shared/src/plugins/external-turn-protocol.schema.json). The schema’s root is the ExternalAgentTurnRequest; 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.

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.

{
"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 by limits.transcriptWindowMessages. Roles are normalized to user/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: 1 and 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/https and 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.