MusePi

RPC Protocol Reference

注意:本文件只覆盖 CLI stdio RPC mode(musepi --mode rpc,新行分隔 JSON)。 daemon 的 WebSocket RPC(GUI/桌面会话:session.pause*daemon.pause*system.pingextensions.* 等,经 ws://127.0.0.1:<port>/ws)契约见 docs/gui-implementation.md(§1c 暂停、§2 扩展;其余散落各节,实现为准)。

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

Primary implementation:

Startup

musepi --mode rpc [regular CLI options]

Behavior notes:

Transport and Framing

Protocol v1 frames are a single JSON object followed by \n. Every physical JSONL frame is limited to 1 MiB.

The initial ready frame uses protocol v1 and advertises the opt-in lossless transport:

{
  "type": "ready",
  "protocolVersion": 1,
  "supportedProtocolVersions": [1, 2],
  "maxFrameBytes": 1048576,
  "maxReassembledFrameBytes": 67108864
}

Clients that support protocol v2 SHOULD immediately send:

{ "id": "protocol-1", "type": "negotiate_protocol", "protocolVersion": 2 }

After the success response, oversized stdout objects are emitted losslessly as an uninterrupted sequence of rpc_chunk frames. Each chunk carries a base64 segment of the original UTF-8 JSON object:

{
  "type": "rpc_chunk",
  "chunkId": "rpc-1",
  "index": 0,
  "count": 7,
  "byteLength": 1600042,
  "data": "eyJ0eXBlIjoicmVzcG9uc2UiLC4uLn0="
}

Clients MUST validate chunkId, index, count, and byteLength, reject interleaved or interrupted sequences, enforce the advertised reassembly limit, concatenate decoded bytes in index order, decode them as strict UTF-8, and parse the result as one JSON object. The exported TypeScript RpcFrameDecoder implements this validation. The bundled TypeScript and Python RpcClient implementations negotiate v2 automatically when the ready frame advertises it.

Legacy clients may ignore the added ready fields and remain on v1. V1 retains its bounded fallback behavior for oversized output. Frames above the v2 reassembly ceiling still fail explicitly; large history APIs should use pagination rather than depending on arbitrarily large logical frames.

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. Subagent frames (subagent_lifecycle, subagent_progress, subagent_event), gated 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.

Important edge behavior from runtime:

Command Schema (canonical)

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

Prompting

Protocol

State

Model

Thinking

Queue modes

Compaction

Retry

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

Messages

get_messages_page returns a stable chronological page with messages, totalMessages, and an opaque nextCursor when more messages remain. Cursors are bound to the session ID, durable leaf, and message count. The server rejects stale cursors if the session changes between requests, and refuses to start a paging walk while the session is streaming or compacting. Failed page requests carry a machine-readable code on the error response — session_busy (session is streaming or compacting) or stale_cursor (the snapshot behind the cursor changed, e.g. a background bash appended a message between pages) — so clients can react without matching error-message text. Pages contain at most 256 messages and normally stay below the v1 physical-frame ceiling. A v1 caller can page ordinary histories, but an individual message whose response exceeds that ceiling produces an overflow error; retrieving it losslessly requires negotiated v2 framing.

The bundled TypeScript RpcClient.getMessages() and Python RpcClient.get_messages() drain this paged endpoint automatically after negotiating v2. They retain the legacy monolithic command when connected to a v1 server, and on either session_busy or stale_cursor they discard partial pages and fall back to the legacy best-effort snapshot. Direct getMessagesPage() and get_messages_page() calls remain strict so incremental hosts never mix snapshots silently.

Login

Response Schema

All command results use RpcResponse:

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

tokensPerSecond is a number when output throughput is available and null otherwise. fastModeEnabled reports the session setting, while fastModeActive reports the actual computed active state. For Fireworks, providers.fireworksTier: priority is a provider-level setting independent of the /fast family setting, so fastModeActive may remain true for an unsupported Fireworks model.

For direct Anthropic, a provider rejection of speed: "fast" uses a sticky fallback scoped by the resolved endpoint and exact model: fastModeEnabled may remain true while fastModeActive is false. An explicit set_fast_mode enable expresses retry intent and clears that fallback so the provider attempt is re-armed.

{
  "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": "...",
  "fastModeEnabled": false,
  "tokensPerSecond": null,
  "fastModeActive": false,
  "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_fast_mode payload

set_fast_mode changes whether fast mode is enabled for the session. The request is:

{ "id": "req_fast_on", "type": "set_fast_mode", "enabled": true }

On success, data always contains both enabled and active. These are the actual computed values: enabled reports the session setting, and active reports the resulting active state, including any provider-level Fireworks priority setting:

For direct Anthropic, an explicit enable also re-arms a provider attempt after the sticky rejection fallback, even when fast mode was already enabled.

{
  "id": "req_fast_on",
  "type": "response",
  "command": "set_fast_mode",
  "success": true,
  "data": { "enabled": true, "active": true }
}

Enabling fast mode on a model without a service-tier family fails with the exact error below:

{
  "id": "req_fast_on",
  "type": "response",
  "command": "set_fast_mode",
  "success": false,
  "error": "Fast mode is unavailable for the current model."
}

Disabling fast mode is idempotent, including on an unsupported model. It succeeds as an off/no-op result, but disabling /fast does not override provider-level settings, so a successful disable does not guarantee active: false. For example, with an unsupported fireworks/deepseek-v4-flash model and providers.fireworksTier: priority, the response reports the session setting as disabled while the provider priority keeps the computed active state true:

{
  "id": "req_fast_off",
  "type": "response",
  "command": "set_fast_mode",
  "success": true,
  "data": { "enabled": false, "active": true }
}

The corresponding get_state result reports the same computed state:

{
  "fastModeEnabled": false,
  "fastModeActive": true
}

set_todos payload

Replaces the in-memory todo state for the current session and returns the normalized phase list:

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

This is useful for hosts that want 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.

security:// is reserved for OMP’s producer-neutral software-security resource store. RPC hosts cannot register or shadow that scheme.

Event Stream Schema

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

Common event types:

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

This is the most important operational behavior.

Immediate ack vs completion

prompt and abort_and_prompt are acknowledged immediately:

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

That means:

While streaming

AgentSession.prompt() requires streamingBehavior during active streaming:

If omitted during streaming, prompt fails.

Queue defaults

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

Mode semantics

Extension UI Sub-Protocol

Extensions in RPC mode use request/response UI frames.

Outbound request

RpcExtensionUIRequest (type: "extension_ui_request") methods:

Runtime note:

Example:

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

Inbound response

RpcExtensionUIResponse (type: "extension_ui_response"):

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 wants the host to 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 surface 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 carrying 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 surfacing:

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

Constraints

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

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:

Use raw protocol frames if you need complete surface coverage.