RPC protocol
You are building an application or a long-running process that must drive Zi and observe it: send prompts, follow events, page messages and outcomes, select a model, invoke an extension command. One-shot invocations stop being enough once your process needs to stay attached and correlate its own requests against what Zi actually admitted. See JSON event stream for the one-shot alternative when a finite invocation is still enough.
Zi RPC is a long-lived process protocol over the same authoritative AgentSession used by interactive and print modes. Start with the copyable one-shot client in examples/rpc/client.ts.
zi --mode rpc --no-sessionThe client writes strict UTF-8 JSONL to stdin and reads JSONL from stdout. Positional prompts are rejected. Diagnostics use stderr and never contaminate protocol stdout.
Framing and ordering
Every request carries protocol version 1 and a required correlation ID:
{ "version": 1, "id": "state-1", "method": "session.get_state" }Every server frame carries version 1 and a connection-local monotonic sequence. The first frame is always ready:
{
"version": 1,
"sequence": 1,
"type": "ready",
"state": {
"sessionId": "…",
"activity": { "type": "idle" },
"model": { "type": "unselected" },
"thinkingLevel": "off",
"supportedThinkingLevels": ["off"],
"steeringMode": "one-at-a-time",
"followUpMode": "one-at-a-time",
"queuedInputs": { "steering": [], "followUp": [] },
"workPlan": { "revision": 0, "steps": [] },
"messageCount": 0,
"compaction": { "type": "idle" },
"retry": { "type": "idle" },
"contextUsage": { "type": "unavailable", "reason": "no_model" }
}
}A successful request receives a correlated response:
{
"version": 1,
"sequence": 2,
"type": "response",
"id": "state-1",
"method": "session.get_state",
"ok": true,
"result": { "sessionId": "…" }
}Operation failures use ok: false with capacity, not_found, or operation_failed. Invalid JSON and rejected request shapes produce protocol_error frames and do not close the connection. Invalid UTF-8 and oversized framing are fatal after one invalid_framing frame.
A request ID is connection-local and may identify only one admitted, unsettled operation. Reusing an in-flight ID produces a recoverable protocol_error with code invalid_request and the matching ID; the duplicate record is not launched, including when it requests the reserved interruption slot. An ID may be reused only after its prior correlated response. Reuse always denotes a new request rather than a replay of the prior result.
session_event frames contain source-ordered AgentSessionEvent values. Model-change events use the public model projection described below instead of exposing provider configuration or credentials. entry_appended is the raw all-journal event and includes custom, custom_message, work_plan, operation_outcome, and substrate subagent entries.
Appending an outcome also emits the semantic operation_outcome event, so clients that subscribe to all events receive both. The semantic event and session.get_outcomes items use the same open outcome envelope: journal id, parentId, and timestamp, plus operationId, capability, operation, result, durationMs, and bounded producer-owned evidence. Delivery messages and tool results are not additional outcomes.
Work plan replacements also emit work_plan_changed; see Work plans for the replacement and persistence rules. Message pages include displayed custom messages and omit hidden ones; a hidden custom message's committed entry event remains observable to the trusted process client.
Methods
| Method | Parameters | Result |
|---|---|---|
session.get_state | none | Current session snapshot |
session.get_messages | start defaults to 0; limit defaults to and maxes 100 | Indexed message page, total count, and next start |
session.get_outcomes | start defaults to 0; limit defaults to and maxes 100 | Indexed outcome page, total count, and next start |
session.prompt | delivery, text; optional completionId | Admitted delivery and optional completion revision |
session.interrupt | none | Empty object after interruption settles |
session.await_idle | optional completionId | Idle settlement and optional bounded completion |
connection.set_events | mode: all (default) or none | Active event mode |
command.list | none | Admitted extension-command descriptors |
command.invoke | name, bounded raw arguments string | Optional local feedback after command settlement |
model.list | none | Bounded model descriptors with authentication status |
model.select | provider, id | Selected public model descriptor |
thinking.list | none | Levels supported by the selected model |
thinking.select | level; optional scope: global or project | Requested, effective, and persisted scope |
session.get_outcomes returns { start, total, nextStart, outcomes } in journal order. Follow nextStart until it is null. Each item has exactly the same shape as the outcome carried by a live operation_outcome event, so clients can use one decoder and producer-specific evidence policy for history and streaming updates.
A direct prompt response means the input was admitted, not that provider work completed. Use ordered session events or session.await_idle for completion. session.await_idle follows the current session operation and is not an ordinary short request-response deadline; clients that need a work budget must own that policy separately and interrupt the session when it expires. Steering and follow-up input retain AgentSession queue semantics and may be queued before the next direct prompt.
A client that needs atomic terminal evidence may attach one stable completionId to every prompt admitted into the same logical work cycle, then pass that ID to session.await_idle. Each prompt joined to active work advances completionRevision; steer or follow_up input admitted while already idle remains queued for a future turn and keeps the settled revision.
The idle response returns the observed revision, final message count, and the latest assistant message_end captured for that cycle. Completion text is clipped to 50 KiB and provider error text to 8 KiB.
If input was admitted after an earlier idle observation, its newer admission revision tells the client to issue another idle watch; no transcript paging is needed to recover completion evidence. Omitting completionId preserves the generic empty idle response. A no-ID prompt that starts or joins work invalidates any earlier completion watch so unrelated assistant output cannot be attributed to it.
delivery: "continue" is decided inside the child AgentSession: idle starts a direct run; running queues follow-up into the active run. Aborting, compacting, reloading, failed, and disposed states reject the operation. The response reports only that the continue input was admitted, not that its resulting work settled. Clients using completion watches keep the same completionId when a continue belongs to the current logical cycle and choose a new ID when starting a new cycle.
command.invoke addresses an admitted extension command directly; it does not parse slash text. Commands are idle-only AgentSession operations. Their optional feedback is returned as { "message": "…" } (or {}), remains outside the journal and provider context, and may be interrupted with session.interrupt. Unknown names return not_found. The copyable client exposes runRpcCommand(...), which checks command.list before invoking the exact name.
Application and retry semantics
Rejection before launch is definite non-application. This includes malformed requests, duplicate in-flight IDs, and capacity responses. A command.invoke response with not_found also means no extension command was invoked. These outcomes do not affect an already admitted operation that happens to use the duplicated ID.
Once a valid request is admitted, failure is not an idempotency guarantee. If the connection closes before its correlated response, the client cannot tell whether the operation was applied. An operation_failed response says that the operation did not complete normally; it does not promise rollback of effects that occurred before the failure.
In particular, a session.prompt may have started work or queued input before its response is lost, and an extension behind command.invoke may perform external side effects before it throws or before its response is lost. Retrying either operation can therefore apply it again.
RPC has no response cache or persisted idempotency storage. Clients must use domain evidence such as completion watches, session events, messages, or extension-owned operation keys when they need reconciliation. Reusing the same request ID after any correlated response, including a failure response, starts a new operation and does not retrieve the old response.
connection.set_events is owned by the RPC connection, not AgentSession. Admission applies the mode synchronously in input order before its response is emitted. Mode none suppresses only session_event frames; ready, response, and protocol_error are never suppressed. The compatibility default remains all.
RPC remains a transport for one parent session. Profile-driven standard delegation tools and optional extension-defined orchestration appear as ordinary tools; their durable substrate evidence may appear as journal-entry events. RPC deliberately adds no agent.* topology methods or direct external subagent control plane.
Public model descriptors contain only provider, id, name, reasoning, input, contextWindow, and maxTokens. Catalog results add configured. Base URLs, headers, compatibility settings, prices, and credentials do not cross RPC.
Message pages are bounded by both count and encoded bytes. A client should retain the total from each response, follow nextStart, and restart paging if intervening event sequences indicate the session changed.
Bounds and lifecycle
- input or output record: 16 MiB;
- prompt text: 8 MiB;
- completion ID: 256 bytes;
- completion text/error projection: 50 KiB / 8 KiB;
- message page: 100 messages and 8 MiB;
- outcome page: 100 outcomes; evidence is at most 8 KiB per outcome;
- request ID, model provider, or model ID: 256 bytes;
- command name: 64 bytes; command arguments: 256 KiB;
- ordinary in-flight operations: 32;
- reserved concurrent interruption: 1;
- pending output: 1,024 records and 32 MiB;
- connection settlement: 5 seconds.
Closing stdin means the client has disconnected. Zi stops admitting requests, discards queued input, interrupts active work, waits boundedly, restores protocol resources, and then lets the CLI dispose the session it created. SIGHUP, SIGINT, and SIGTERM follow the same cancellation path and retain the CLI's established exit codes.