Example

Drive a live Pi subprocess over JSONL

Send commands with an id, correlate responses by that id, wait for agent_settled not agent_end, and answer extension UI dialogs.

RPC keeps Pi in a separate process and talks to it as newline-delimited JSON, so you decide the lifetime of the child — across a process you wait for an event, not a promise. The price is a protocol: command and response records, a stream of session events that carry no command id, and a fourth family, extension UI records, that travels in both directions. Everything below is observed against the shipped binary, on Pi 1.0.4, by a hand-written client that splits on LF only.

Run it

Start pi --mode rpc --no-session and send one JSON object per line. The canonical exercise is examples/ch35-rpc/rpc.test.ts: it drives the shipped pi binary with a minimal JSONL client, on the example repository’s test runner. No model key is needed because the provider is scripted.

A command round trip

{"id":"req-1","type":"prompt","message":"Review this repository"}
{"id":"req-1","type":"response","command":"prompt","success":true,"data":{"disposition":"started"}}

The response repeats the command id, which is how two outstanding commands are matched — by id, never by order. And a successful prompt means accepted, queued, or handled, not finished: here disposition is "started", and it arrived before the run’s first event. Everything interesting afterwards arrives as session events, which carry no command id; the completion a client waits for is agent_settled, which follows agent_end.

Protocol walkthrough

Direction Record Carries
stdin command a prompt, a state inspection, a config change, an abort
stdout response one per command: success plus data, repeating the id
stdout session event run, tool, queue, compaction and retry activity; no command id (except bash_execution_update, which repeats an originating bash id)
both extension UI record dialogs and notifications, correlated by request id
rpc.test.ts assertion What the process did
Two outstanding commands Matched by id (a→get_state, b→get_available_models), not order
prompt success disposition: "started", before agent_start
Streaming prompt Without streamingBehavior: an error response; with "steer": "queued"
Malformed JSON A parse response with success: false and no id
Unknown command An error response; the process keeps serving
Closing stdin Orderly shutdown; the process exits 0
@file argument Rejected in RPC mode
get_commands Listed the extension command ask, not /settings or /hotkeys
Extension dialog confirm() emitted an extension_ui_request and blocked; the response with the same id resolved it; cancelled: true delivered false; no normal response was produced
extension_error A throwing handler was reported and the run still completed
get_entries since = an entry id returned only later entries; leafId matched the cursor; an unknown id was an error

Mechanism and limitations

Documented: rpc.md, rpc-commands.md and rpc-extension-ui.md (1.0.4) define the four record families, correlation by id (“correlate by ID rather than response order”), the disposition contract, closed-stdin shutdown, and the extension UI subprotocol with its timeout default. Observed: rpc.test.ts asserts every row above against the shipped binary. Not run: clear_queue then abort, the UI timeout default, and the less common dialog methods.

RPC is not a sandbox: the child has whatever permissions it was started with and loads extensions from your working folder. An extension_ui_request made without a timeout can wait indefinitely for a controller that never answers, and stderr must never be parsed as protocol data. A success response covers only command handling — provider failures and aborts after a prompt is accepted appear in the message and event stream.

Full source and test .

Understand this example

Copy this prompt into your AI tool. No code runs here.

Apply this example

Copy this prompt into your AI tool. No code runs here.