Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RPC Protocol Reference

RPC mode runs the coding agent as a newline-delimited JSON protocol over stdio.

  • stdin: commands (RpcCommand), extension UI responses, and host-tool updates/results
  • stdout: a ready frame, command responses (RpcResponse), session/agent events, extension UI requests, host-tool requests/cancellations

Primary implementation:

  • src/modes/rpc/rpc-mode.ts
  • src/modes/rpc/rpc-commands.ts
  • src/modes/rpc/rpc-types.ts
  • src/session/agent-session.ts
  • packages/agent/src/agent.ts
  • packages/agent/src/agent-loop.ts

Startup

veyyon --mode rpc [regular CLI options]
veyyon --mode rpc-ui [regular CLI options]

rpc-ui is rpc for a host that answers UI requests. Extensions use the Extension UI Sub-Protocol in both modes; rpc-ui additionally hands that same request/response channel to the tool layer, so hasUI is true for tools and the ask tool is registered. Under plain rpc the ask tool is absent, the ui member of a tool’s context is undefined, and bash runs in a PTY; rpc-ui disables the PTY.

Behavior notes:

  • @file CLI arguments are rejected in RPC mode.
  • RPC mode disables automatic session title generation by default to avoid an extra model call.
  • RPC mode host-defaults a small set of settings so embedders inherit Veyyon’s neutral defaults: agent.isolation.mode/merge/commits, agent.delegation, agent.batch, agent.maxConcurrency, agent.maxNestedSpawnDepth, agent.agents, and memory.backend, plus async.enabled, async.maxJobs, bash.autoBackground.enabled, and bash.autoBackground.thresholdMs. The default is only applied when the path is unset: any explicit configuration (caller overrides, --config overlays, or the profile config.yml) is preserved. todo.* settings are always caller-controlled in protocol modes.
  • The process reads stdin as JSONL (readJsonl(Bun.stdin.stream())).
  • At startup it writes { "type": "ready" } before processing commands.
  • When stdin closes, pending host-tool calls and host-URI requests are rejected and the process exits with code 0.
  • Responses/events are written as one JSON object per line.

Transport and Framing

Each frame is a single JSON object followed by \n.

There is no envelope beyond the object shape itself.

Outbound frame categories (stdout)

  1. Ready frame ({ type: "ready" })
  2. RpcResponse ({ type: "response", ... })
  3. AgentSessionEvent objects (agent_start, message_update, etc.)
  4. RpcExtensionUIRequest ({ type: "extension_ui_request", ... })
  5. Host tool requests/cancellations (host_tool_call, host_tool_cancel)
  6. Host URI requests/cancellations (host_uri_request, host_uri_cancel)
  7. Extension errors ({ type: "extension_error", extensionPath, event, error })
  8. Available-commands updates ({ type: "available_commands_update", commands }), emitted at startup and whenever command metadata changes
  9. Prompt lifecycle hints ({ type: "prompt_result", id?, agentInvoked }) for scheduled prompts that later resolve without invoking the agent
  10. Agent frames (subagent_lifecycle, subagent_progress, subagent_event), enabled by set_subagent_subscription
  11. Builtin slash-command side channels (command_output, session_info_update, config_update)

Inbound frame categories (stdin)

  1. RpcCommand
  2. RpcExtensionUIResponse ({ type: "extension_ui_response", ... })
  3. Host tool updates/results (host_tool_update, host_tool_result)
  4. Host URI results (host_uri_result)

Request/Response Correlation

All commands accept optional id?: string.

  • If provided, normal command responses echo the same id.
  • RpcClient relies on this for pending-request resolution.

Edge behavior from runtime:

  • Unknown command responses are emitted with id: undefined (even if the request had an id).
  • Parse/handler exceptions in the input loop emit command: "parse" with id: undefined.
  • prompt and abort_and_prompt return immediate success, then may emit a later error response with the same id if async prompt scheduling fails.
  • prompt success responses may include data.agentInvoked. false means the prompt completed locally without an agent turn; true means the prompt produced agent lifecycle events; omitted means the host must rely on session events for completion.
  • abort_and_prompt does not currently emit data.agentInvoked or prompt_result; hosts should treat it as the legacy abort-then-schedule path and rely on session events or same-id scheduling errors.

Command Schema (canonical)

RpcCommand is defined in src/modes/rpc/rpc-types.ts:

Prompting

  • { id?, type: "prompt", message: string, images?: ImageContent[], streamingBehavior?: "steer" | "followUp" }
  • { id?, type: "steer", message: string, images?: ImageContent[] }
  • { id?, type: "follow_up", message: string, images?: ImageContent[] }
  • { id?, type: "abort" }
  • { id?, type: "abort_and_prompt", message: string, images?: ImageContent[] }
  • { id?, type: "new_session", parentSession?: string, background?: boolean }

State

  • { id?, type: "get_state" }
  • { id?, type: "get_available_commands" }
  • { id?, type: "set_todos", phases: TodoPhase[] }
  • { id?, type: "set_host_tools", tools: RpcHostToolDefinition[] }
  • { id?, type: "set_host_uri_schemes", schemes: RpcHostUriSchemeDefinition[] }
  • { id?, type: "set_subagent_subscription", level: "off" | "progress" | "events" }
  • { id?, type: "get_subagents" }
  • { id?, type: "get_subagent_messages", agentId?: string, sessionFile?: string, fromByte?: number }

Model

  • { id?, type: "set_model", provider: string, modelId: string }
  • { id?, type: "cycle_model" }
  • { id?, type: "get_available_models" }

Thinking

  • { id?, type: "set_thinking_level", level: ThinkingLevel }
  • { id?, type: "cycle_thinking_level" }

Queue modes

  • { id?, type: "set_steering_mode", mode: "all" | "one-at-a-time" }
  • { id?, type: "set_follow_up_mode", mode: "all" | "one-at-a-time" }
  • { id?, type: "set_interrupt_mode", mode: "immediate" | "wait" }

Compaction

  • { id?, type: "compact", customInstructions?: string }
  • { id?, type: "set_auto_compaction", enabled: boolean }

Retry

  • { id?, type: "set_auto_retry", enabled: boolean }
  • { id?, type: "abort_retry" }

Bash

  • { id?, type: "bash", command: string }
  • { id?, type: "abort_bash" }

bash is dispatched concurrently: the RPC server continues reading commands while the shell command runs, so abort_bash (or any other command) sent during a long-running bash is handled without waiting for it to finish on its own. The bash response is emitted when the command completes; hosts correlate it via id. Ordering across concurrent commands is not guaranteed , clients MUST match responses on id, not on emission order.

Session

  • { id?, type: "get_session_stats" }
  • { id?, type: "export_html", outputPath?: string }
  • { id?, type: "switch_session", sessionPath: string }
  • { id?, type: "branch", entryId: string }
  • { id?, type: "get_branch_messages" }
  • { id?, type: "get_last_assistant_text" }
  • { id?, type: "set_session_name", name: string }
  • { id?, type: "handoff", customInstructions?: string }

Background conversations

  • { id?, type: "get_background_sessions" }
  • { id?, type: "cancel_background_session", sessionId: string }

new_session with background: true while a turn is streaming attaches the client to a new session; the streaming one finishes its turn in the background and its events are no longer sent. With no streaming turn the session resets in place, as without background. background and parentSession cannot be combined.

switch_session to the transcript of a background conversation re-attaches the live session with its turn still streaming, and the session the client was driving moves to the background. If that session had no turn streaming, it stays only until its background jobs finish, and is disposed at once when it has none.

When a session moved, the response has data.background: { sessionId, sessionFile, streaming, displaced, message }. streaming is false when the session that moved had no turn streaming. displaced lists the session ids of older conversations stopped to stay within session.backgroundLimit.

get_background_sessions returns { sessions }, each { sessionId, sessionFile, title, detachedAt, streaming, stopping }. cancel_background_session aborts the conversation’s turn, waits until it is disposed, and fails when no background conversation has that id. At exit, a background conversation still running after 5 seconds is stopped.

Messages

  • { id?, type: "get_messages" }

Login

  • { id?, type: "get_login_providers" }
  • { id?, type: "login", providerId: string }

Response Schema

All command results use RpcResponse:

  • Success: { id?, type: "response", command: <command>, success: true, data?: ... }
  • Failure: { id?, type: "response", command: string, success: false, error: string }

Data payloads are command-specific and defined in rpc-types.ts.

prompt payload

prompt is acknowledged after the command is accepted, not after a model turn finishes:

{
  "id": "req_1",
  "type": "response",
  "command": "prompt",
  "success": true,
  "data": { "agentInvoked": false }
}

data.agentInvoked: false is a completion signal for local-only prompts, including slash commands that produce output without starting an agent turn. data.agentInvoked: true means the prompt produced agent lifecycle events; those events can be emitted before or after the prompt response depending on the command path. Older runtimes may omit data; hosts should then rely on agent_end, custom message completion, or prompt_result.

prompt_result is emitted when a prompt was accepted immediately but later resolves as local-only:

{ "type": "prompt_result", "id": "req_1", "agentInvoked": false }

Local-only slash commands may emit command_output frames before completing via data.agentInvoked: false or a later prompt_result. They do not emit agent_end.

get_state payload

{
  "model": { "provider": "...", "id": "..." },
  "thinkingLevel": "off|minimal|low|medium|high|xhigh|max",
  "isStreaming": false,
  "isCompacting": false,
  "steeringMode": "all|one-at-a-time",
  "followUpMode": "all|one-at-a-time",
  "interruptMode": "immediate|wait",
  "sessionFile": "...",
  "sessionId": "...",
  "sessionName": "...",
  "autoCompactionEnabled": true,
  "messageCount": 0,
  "queuedMessageCount": 0,
  "todoPhases": [
    {
      "id": "phase-1",
      "name": "Todos",
      "tasks": [
        {
          "id": "task-1",
          "content": "Map the tool surface",
          "status": "in_progress"
        }
      ]
    }
  ],
  "systemPrompt": ["..."],
  "dumpTools": [
    {
      "name": "read",
      "description": "Read files and URLs",
      "parameters": {}
    }
  ],
  "contextUsage": {
    "tokens": 1100,
    "contextWindow": 200000,
    "percent": 0.55
  }
}

set_todos payload

Replaces the in-memory todo state for the current session and returns the normalized phase list. A phase is { name, tasks } and a task is { content, status }; status is one of pending, in_progress, completed, abandoned. Neither has an id: a task is addressed by its content string, and any other field in a phase or task is dropped, not echoed back.

{
  "id": "req_2",
  "type": "set_todos",
  "phases": [
    {
      "name": "Evaluation",
      "tasks": [
        { "content": "Map the read tool surface", "status": "in_progress" },
        { "content": "Exercise edit operations", "status": "pending" }
      ]
    }
  ]
}

Use it to pre-seed a plan before the first prompt.

set_host_tools payload

Replaces the current set of host-owned tools that the RPC server may call back into over stdio:

{
  "id": "req_3",
  "type": "set_host_tools",
  "tools": [
    {
      "name": "echo_host",
      "label": "Echo Host",
      "description": "Echo a value from the embedding host",
      "parameters": {
        "type": "object",
        "properties": {
          "message": { "type": "string" }
        },
        "required": ["message"],
        "additionalProperties": false
      }
    }
  ]
}

The response payload is:

{
  "toolNames": ["echo_host"]
}

These tools are added to the active session tool registry before the next model call. Re-sending set_host_tools replaces the previous host-owned set.

set_host_uri_schemes payload

Replaces the current set of host-owned URL schemes the RPC server should dispatch reads/writes through:

{
  "id": "req_4",
  "type": "set_host_uri_schemes",
  "schemes": [
    {
      "scheme": "db",
      "description": "Virtual db row files",
      "writable": true,
      "immutable": false
    }
  ]
}

The response payload is:

{
  "schemes": ["db"]
}

Schemes are case-insensitive on the wire and normalized to lowercase before the response is sent. Re-sending set_host_uri_schemes replaces the entire previous set, schemes missing from the new list are unregistered.

Event Stream Schema

RPC mode forwards AgentSessionEvent objects from AgentSession.subscribe(...).

Common event types:

  • agent_start, agent_end
  • turn_start, turn_end
  • message_start, message_update, message_end
  • tool_execution_start, tool_execution_update, tool_execution_end
  • auto_compaction_start, auto_compaction_end
  • auto_retry_start, auto_retry_end
  • ttsr_triggered
  • todo_reminder
  • todo_auto_clear

Extension runner errors are emitted separately as:

{
  "type": "extension_error",
  "extensionPath": "...",
  "event": "...",
  "error": "..."
}

message_update includes streaming deltas in assistantMessageEvent (text/thinking/toolcall deltas).

Prompt/Queue Concurrency and Ordering

Immediate ack vs completion

prompt and abort_and_prompt are acknowledged immediately:

{ "id": "req_1", "type": "response", "command": "prompt", "success": true }

That means:

  • command acceptance != run completion
  • agent turns complete via agent_end
  • local-only prompts complete via data.agentInvoked: false on the response or via a later prompt_result

While streaming

AgentSession.prompt() requires streamingBehavior during active streaming:

  • "steer" => queued steering message (interrupt path)
  • "followUp" => queued follow-up message (post-turn path)

If omitted during streaming, prompt fails.

Queue defaults

From packages/agent/src/agent.ts defaults:

  • steeringMode: "one-at-a-time"
  • followUpMode: "one-at-a-time"
  • interruptMode: "immediate"

Mode semantics

  • set_steering_mode / set_follow_up_mode
    • "one-at-a-time": dequeue one queued message per turn
    • "all": dequeue entire queue at once
  • set_interrupt_mode
    • "immediate": tool execution checks steering between tool calls; pending steering can abort remaining tool calls in the turn
    • "wait": defer steering until turn completion

Extension UI Sub-Protocol

Extensions in RPC mode use request/response UI frames.

Outbound request

RpcExtensionUIRequest (type: "extension_ui_request") methods:

  • select, confirm, input, editor, cancel
  • notify, setStatus, setWidget, setTitle, set_editor_text
  • open_url (emitted by RPC login flows). Contains url, optional launchUrl and instructions, and credential: "oauth" when the flow waits on the URL and the host opens it in a browser, "api-key" when the URL is where a key is obtained and the flow is about to prompt for a paste, so the host shows it without opening it.

Runtime note:

  • Automatic session title generation is disabled in RPC mode, and setTitle UI requests are also suppressed by default because most hosts do not have a meaningful terminal-title surface. Set VEYYON_RPC_EMIT_TITLE=1 to opt back in to the UI event only.

Example:

{
  "type": "extension_ui_request",
  "id": "123",
  "method": "confirm",
  "title": "Confirm",
  "message": "Continue?",
  "timeout": 30000
}

Inbound response

RpcExtensionUIResponse (type: "extension_ui_response"):

  • { type: "extension_ui_response", id: string, value: string }
  • { type: "extension_ui_response", id: string, confirmed: boolean }
  • { type: "extension_ui_response", id: string, cancelled: true, timedOut?: boolean }

If a dialog has a timeout, RPC mode resolves to a default value when timeout/abort fires.

Host Tool Sub-Protocol

RPC hosts can expose custom tools to the agent by sending set_host_tools, then serving execution requests over the same transport.

Outbound request

When the agent requests that the host execute one of those tools, RPC mode emits:

{
  "type": "host_tool_call",
  "id": "host_1",
  "toolCallId": "toolu_123",
  "toolName": "echo_host",
  "arguments": { "message": "hello" }
}

If the tool execution is later aborted, RPC mode emits:

{
  "type": "host_tool_cancel",
  "id": "host_cancel_1",
  "targetId": "host_1"
}

Inbound updates and completion

Hosts can optionally stream progress:

{
  "type": "host_tool_update",
  "id": "host_1",
  "partialResult": {
    "content": [{ "type": "text", "text": "working" }]
  }
}

Completion uses:

{
  "type": "host_tool_result",
  "id": "host_1",
  "result": {
    "content": [{ "type": "text", "text": "done" }]
  }
}

Set top-level isError: true on host_tool_result to reject the pending host tool call and report the returned text content as a tool error.

Host URI Sub-Protocol

RPC hosts can also own custom URL schemes (virtual files). After set_host_uri_schemes, every read of <scheme>://… and write of <scheme>://… (when registered as writable) is bounced back to the host over the same transport.

Outbound request

When a session tool resolves a host-owned URL, RPC mode emits:

{
  "type": "host_uri_request",
  "id": "uri_1",
  "operation": "read",
  "url": "db://users/42"
}

Writes look the same with "operation": "write" and an additional "content": "..." field containing the full replacement bytes.

If the request is later aborted (caller cancels, session ends), RPC mode emits:

{
  "type": "host_uri_cancel",
  "id": "uri_cancel_1",
  "targetId": "uri_1"
}

Inbound result

For successful reads:

{
  "type": "host_uri_result",
  "id": "uri_1",
  "content": "id=42\nname=Alice\n",
  "contentType": "text/plain",
  "notes": ["fresh from cache"],
  "immutable": false
}

For successful writes, omit content:

{ "type": "host_uri_result", "id": "uri_1" }

To reject the request, set isError: true and either populate error with a message or fall back to content for textual error reporting:

{
  "type": "host_uri_result",
  "id": "uri_1",
  "isError": true,
  "error": "row 42 not found"
}

Constraints

  • The agent’s edit tool does not target host URIs. Hosts that want to mutate virtual files expose write and let the model use the write tool with replacement content.
  • Schemes are global to the process; set_host_uri_schemes replaces the previous set, unregistering anything not in the new list.
  • Schemes are normalized to lowercase before registration.

Error Model and Recoverability

Command-level failures

Failures are success: false with string error.

{
  "id": "req_2",
  "type": "response",
  "command": "set_model",
  "success": false,
  "error": "Model not found: provider/model"
}

Recoverability expectations

  • Most command failures are recoverable; process remains alive.
  • Malformed JSONL / parse-loop exceptions emit a parse error response and continue reading subsequent lines.
  • Empty set_session_name is rejected (Session name cannot be empty).
  • Extension UI responses with unknown id are ignored.
  • Process termination conditions are stdin close or explicit extension-triggered shutdown after the current command.

Compact Command Flows

1) Prompt and stream

stdin:

{ "id": "req_1", "type": "prompt", "message": "Summarize this repo" }

stdout sequence (typical):

{ "id": "req_1", "type": "response", "command": "prompt", "success": true }
{ "type": "agent_start" }
{ "type": "message_update", "assistantMessageEvent": { "type": "text_delta", "delta": "..." }, "message": { "role": "assistant", "content": [] } }
{ "type": "agent_end", "messages": [] }

2) Prompt during streaming with explicit queue policy

stdin:

{
  "id": "req_2",
  "type": "prompt",
  "message": "Also include risks",
  "streamingBehavior": "followUp"
}

3) Inspect and tune queue behavior

stdin:

{ "id": "q1", "type": "get_state" }
{ "id": "q2", "type": "set_steering_mode", "mode": "all" }
{ "id": "q3", "type": "set_interrupt_mode", "mode": "wait" }

4) Extension UI round trip

stdout:

{
  "type": "extension_ui_request",
  "id": "ui_7",
  "method": "input",
  "title": "Branch name",
  "placeholder": "feature/..."
}

stdin:

{ "type": "extension_ui_response", "id": "ui_7", "value": "feature/rpc-host" }

Notes on RpcClient helper

src/modes/rpc/rpc-client.ts is a convenience wrapper, not the protocol definition.

Current helper characteristics:

  • Spawns bun <cliPath> --mode rpc
  • Correlates responses by generated req_<n> ids
  • Dispatches recognized core AgentEvent types to listeners
  • Supports host-owned custom tools via setCustomTools() and automatic handling of host_tool_call / host_tool_cancel
  • Wraps common protocol commands including OAuth getLoginProviders() / login(...); use raw protocol frames for any surface not wrapped by the helper.

Use raw protocol frames if you need complete surface coverage.