zot RPC
zot rpc runs the agent runtime as a subprocess that speaks newline-delimited JSON on stdin and stdout. Use it from any language that can spawn a process and read/write its pipes — Go, TypeScript, Python, Rust, shell, anything.
For a Go program embedding the runtime in-process, use the packages/agent/sdk SDK instead. The wire format below mirrors the SDK's types one-for-one so consumers can share parsing code.
Quick start
# spawn zot rpc; talk to it from a shell
( echo '{"id":"1","type":"prompt","message":"hello"}'; sleep 5 ) \
| zot rpc --provider anthropic
You'll see one JSON object per line on stdout: a response acknowledging the prompt, a stream of events (text_delta, tool_call, tool_result, usage), then done.
Process model
- One
zot rpc process serves one cwd, one model, one session.
- For multiple projects, spawn multiple processes.
- Concurrency: at most one prompt or compact in flight at a time. A second one queues until the first finishes; aborting fires immediately.
- The process exits when stdin closes.
Flags
zot rpc accepts the same flags as the other modes: --provider, --model, --cwd, --api-key, --base-url, --system-prompt, --append-system-prompt, --reasoning, --max-steps, --no-tools, --tools, --no-context-files. Sessions are disabled by default in RPC mode; the embedding application owns persistence.
Extension system prompts
Extensions can subscribe to before_agent_start to inspect and replace the
complete system prompt before the first agent model call. This uses the
extension subprocess protocol, not a new RPC client command. The result is
reused for ordinary follow-up prompts and tool-loop steps; clear and a model
change invalidate it for the next prompt. Failures keep the current prompt and
surface as ext_notify warnings. See System prompt replacement
for chaining, deadlines, size limits, and privacy considerations.
Auth
If the environment variable ZOTCORE_RPC_TOKEN is set on the spawned process, the first line on stdin must be a hello command containing the matching token:
{"id":"0","type":"hello","token":"shared-secret"}
If absent or wrong, the response carries success:false and the process exits. Without ZOTCORE_RPC_TOKEN set, no auth is required (the spawning process is implicitly trusted; if it can spawn zot it can also read your auth.json directly).
Every line in either direction is one JSON object terminated by \n. Object boundaries follow newline boundaries — no multi-line JSON.
Frame types
type |
Direction |
Description |
any command (prompt, abort, ...) |
client → server |
Request |
response |
server → client |
Reply to one command, correlated by id |
any event (text_delta, tool_call, ...) |
server → client |
Stream notification (no id) |
Commands
All commands share an optional id field; if present, the matching response echoes it. Use id to correlate replies with requests, especially when several requests are in flight.
hello
{"id":"0","type":"hello","token":"shared-secret"}
Response:
{"type":"response","id":"0","command":"hello","success":true,
"data":{"protocol_version":1,"version":"0.0.4","provider":"anthropic","model":"claude-opus-4-5"}}
Required as the first message when ZOTCORE_RPC_TOKEN is set; optional otherwise.
prompt
{"id":"1","type":"prompt","message":"fix the failing test","images":[]}
Optional images is [{"mime_type":"image/png","data":"<base64>"}].
Response is immediate (the turn is starting):
{"type":"response","id":"1","command":"prompt","success":true,"data":{"started":true}}
Then a stream of event objects (see below) until the turn ends with {"type":"done"}.
abort
Cancel the active prompt or compact.
{"id":"2","type":"abort"}
Response: {"type":"response","id":"2","command":"abort","success":true}.
If the turn was streaming, the next events you see will be a turn_end with stop:"aborted" then done.
compact
Summarise the current transcript into one synthetic user message. Same lifecycle as prompt (immediate response, then events).
{"id":"3","type":"compact"}
Final event: {"type":"compact_done","summary":"<text>"}.
get_state
Snapshot of the runtime.
{"id":"4","type":"get_state"}
Response data:
{
"provider": "anthropic",
"model": "claude-opus-4-5",
"cwd": "/Users/pat/Developer/zot",
"message_count": 12,
"busy": false,
"usage": {"input": 1234, "output": 567, "reasoning": 123, "cache_read": 890, "cache_write": 0, "cost_usd": 0.0123}
}
get_messages
Full transcript.
{"id":"5","type":"get_messages"}
Response data: {"messages": [<message>, ...]}. See message shape below.
Runtime mutations while busy
clear, set_model, and set_reasoning require an idle agent. During prompt
preparation (including extension hooks), model/tool execution, or compaction,
they return success:false with an agent is busy error and leave state
unchanged. They are not queued. Wait for the active operation to finish, or
send abort, wait for completion, and retry. The RPC read loop stays responsive
to abort while an operation is busy.
clear
Drop the entire transcript. Equivalent to the /clear slash command.
{"id":"6","type":"clear"}
set_model
Switch model within the same provider.
{"id":"7","type":"set_model","model":"claude-sonnet-4-5"}
Cross-provider swaps require relaunching zot rpc with the new --provider.
set_reasoning
Set the reasoning level for subsequent prompts without restarting the process. Accepted values are off, minimum, low, medium, high, xhigh, and max.
{"id":"8","type":"set_reasoning","reasoning":"max"}
The max tier is sent natively to supported models and clamped for other backends.
get_models
List models known for the current provider.
{"id":"8","type":"get_models"}
Response data: {"models":[{"id":"...","provider":"...","context_window":200000,"max_output":8192,"reasoning":true}, ...]}.
ping
Health check.
{"id":"9","type":"ping"}
Response: {"type":"response","id":"9","command":"ping","success":true,"data":{"pong":true}}.
Events
Stream notifications during a prompt or compact. None carry an id.
type |
Fields |
Meaning |
turn_start |
step |
Beginning of one model call (max-steps loop iteration) |
user_message |
content, time |
The submitted prompt as it was added to the transcript |
assistant_start |
(none) |
About to receive assistant streaming |
text_delta |
delta |
Partial assistant text. Concatenate to build the full reply |
tool_call |
id, name, args |
The model wants to call a tool |
tool_progress |
id, text |
Optional progress line from the tool while it runs |
tool_result |
id, is_error, content |
Tool finished |
assistant_message |
content, time |
Final assistant message after the model turn ends |
usage |
input, output, reasoning, cache_read, cache_write, cost_usd, cumulative |
Per-turn + cumulative tokens / cost. reasoning is null when unavailable. |
turn_end |
stop, optional error |
One model call finished. stop is end_turn, tool_use, length, error, or aborted |
done |
(none) |
The whole prompt/compact completed (success or error) |
error |
message |
Non-fatal error message |
compact_done |
summary |
Compaction finished, summary text included |
Message shape
Used by get_messages and inside user_message / assistant_message events.
{
"role": "user",
"content": [<content_block>...],
"time": "2026-04-19T11:30:00Z"
}
Content block types
{"type": "text", "text": "..."}
{"type": "image", "mime_type": "image/png", "bytes": 12345}
{"type": "tool_call", "id": "toolu_xyz", "name": "read", "args": {"path": "..."}}
{"type": "tool_result", "call_id": "toolu_xyz", "is_error": false, "content": [<content_block>...]}
Image bytes are reported as a count rather than embedded base64 to keep transcript dumps small. Tool results may nest text and image blocks.
Reference clients
See examples/rpc/ for working implementations in:
shell — bash + jq one-liner
python — subprocess.Popen + json.loads per line
node — child_process.spawn + readline
go — direct subprocess wrapper (the packages/agent/sdk SDK is the in-process Go API)
Versioning
The protocol_version field in the hello response is the major version of this schema. Backwards-incompatible changes bump it. The set of supported events and commands within a major version only grows.
source
1# zot RPC
2
3`zot rpc` runs the agent runtime as a subprocess that speaks newline-delimited JSON on stdin and stdout. Use it from any language that can spawn a process and read/write its pipes — Go, TypeScript, Python, Rust, shell, anything.
4
5For a Go program embedding the runtime in-process, use the `packages/agent/sdk` SDK instead. The wire format below mirrors the SDK's types one-for-one so consumers can share parsing code.
6
7## Quick start
8
9```bash
10# spawn zot rpc; talk to it from a shell
11( echo '{"id":"1","type":"prompt","message":"hello"}'; sleep 5 ) \
12 | zot rpc --provider anthropic
13```
14
15You'll see one JSON object per line on stdout: a response acknowledging the prompt, a stream of events (`text_delta`, `tool_call`, `tool_result`, `usage`), then `done`.
16
17## Process model
18
19- One `zot rpc` process serves **one cwd, one model, one session**.
20- For multiple projects, spawn multiple processes.
21- Concurrency: at most one prompt or compact in flight at a time. A second one queues until the first finishes; aborting fires immediately.
22- The process exits when stdin closes.
23
24## Flags
25
26`zot rpc` accepts the same flags as the other modes: `--provider`, `--model`, `--cwd`, `--api-key`, `--base-url`, `--system-prompt`, `--append-system-prompt`, `--reasoning`, `--max-steps`, `--no-tools`, `--tools`, `--no-context-files`. Sessions are disabled by default in RPC mode; the embedding application owns persistence.
27
28## Extension system prompts
29
30Extensions can subscribe to `before_agent_start` to inspect and replace the
31complete system prompt before the first agent model call. This uses the
32extension subprocess protocol, not a new RPC client command. The result is
33reused for ordinary follow-up prompts and tool-loop steps; `clear` and a model
34change invalidate it for the next prompt. Failures keep the current prompt and
35surface as `ext_notify` warnings. See [System prompt replacement](extensions.md#system-prompt-replacement)
36for chaining, deadlines, size limits, and privacy considerations.
37
38## Auth
39
40If the environment variable `ZOTCORE_RPC_TOKEN` is set on the spawned process, the first line on stdin **must** be a `hello` command containing the matching token:
41
42```json
43{"id":"0","type":"hello","token":"shared-secret"}
44```
45
46If absent or wrong, the response carries `success:false` and the process exits. Without `ZOTCORE_RPC_TOKEN` set, no auth is required (the spawning process is implicitly trusted; if it can spawn `zot` it can also read your `auth.json` directly).
47
48## Wire format
49
50Every line in either direction is one JSON object terminated by `\n`. Object boundaries follow newline boundaries — no multi-line JSON.
51
52### Frame types
53
54| `type` | Direction | Description |
55|---|---|---|
56| any command (`prompt`, `abort`, ...) | client → server | Request |
57| `response` | server → client | Reply to one command, correlated by `id` |
58| any event (`text_delta`, `tool_call`, ...) | server → client | Stream notification (no `id`) |
59
60## Commands
61
62All commands share an optional `id` field; if present, the matching `response` echoes it. Use `id` to correlate replies with requests, especially when several requests are in flight.
63
64### `hello`
65
66```json
67{"id":"0","type":"hello","token":"shared-secret"}
68```
69
70Response:
71
72```json
73{"type":"response","id":"0","command":"hello","success":true,
74 "data":{"protocol_version":1,"version":"0.0.4","provider":"anthropic","model":"claude-opus-4-5"}}
75```
76
77Required as the first message when `ZOTCORE_RPC_TOKEN` is set; optional otherwise.
78
79### `prompt`
80
81```json
82{"id":"1","type":"prompt","message":"fix the failing test","images":[]}
83```
84
85Optional `images` is `[{"mime_type":"image/png","data":"<base64>"}]`.
86
87Response is immediate (the turn is starting):
88
89```json
90{"type":"response","id":"1","command":"prompt","success":true,"data":{"started":true}}
91```
92
93Then a stream of event objects (see below) until the turn ends with `{"type":"done"}`.
94
95### `abort`
96
97Cancel the active prompt or compact.
98
99```json
100{"id":"2","type":"abort"}
101```
102
103Response: `{"type":"response","id":"2","command":"abort","success":true}`.
104
105If the turn was streaming, the next events you see will be a `turn_end` with `stop:"aborted"` then `done`.
106
107### `compact`
108
109Summarise the current transcript into one synthetic user message. Same lifecycle as `prompt` (immediate response, then events).
110
111```json
112{"id":"3","type":"compact"}
113```
114
115Final event: `{"type":"compact_done","summary":"<text>"}`.
116
117### `get_state`
118
119Snapshot of the runtime.
120
121```json
122{"id":"4","type":"get_state"}
123```
124
125Response data:
126
127```json
128{
129 "provider": "anthropic",
130 "model": "claude-opus-4-5",
131 "cwd": "/Users/pat/Developer/zot",
132 "message_count": 12,
133 "busy": false,
134 "usage": {"input": 1234, "output": 567, "reasoning": 123, "cache_read": 890, "cache_write": 0, "cost_usd": 0.0123}
135}
136```
137
138### `get_messages`
139
140Full transcript.
141
142```json
143{"id":"5","type":"get_messages"}
144```
145
146Response data: `{"messages": [<message>, ...]}`. See **message shape** below.
147
148### Runtime mutations while busy
149
150`clear`, `set_model`, and `set_reasoning` require an idle agent. During prompt
151preparation (including extension hooks), model/tool execution, or compaction,
152they return `success:false` with an `agent is busy` error and leave state
153unchanged. They are not queued. Wait for the active operation to finish, or
154send `abort`, wait for completion, and retry. The RPC read loop stays responsive
155to `abort` while an operation is busy.
156
157### `clear`
158
159Drop the entire transcript. Equivalent to the `/clear` slash command.
160
161```json
162{"id":"6","type":"clear"}
163```
164
165### `set_model`
166
167Switch model within the same provider.
168
169```json
170{"id":"7","type":"set_model","model":"claude-sonnet-4-5"}
171```
172
173Cross-provider swaps require relaunching `zot rpc` with the new `--provider`.
174
175### `set_reasoning`
176
177Set the reasoning level for subsequent prompts without restarting the process. Accepted values are `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, and `max`.
178
179```json
180{"id":"8","type":"set_reasoning","reasoning":"max"}
181```
182
183The `max` tier is sent natively to supported models and clamped for other backends.
184
185### `get_models`
186
187List models known for the current provider.
188
189```json
190{"id":"8","type":"get_models"}
191```
192
193Response data: `{"models":[{"id":"...","provider":"...","context_window":200000,"max_output":8192,"reasoning":true}, ...]}`.
194
195### `ping`
196
197Health check.
198
199```json
200{"id":"9","type":"ping"}
201```
202
203Response: `{"type":"response","id":"9","command":"ping","success":true,"data":{"pong":true}}`.
204
205## Events
206
207Stream notifications during a `prompt` or `compact`. None carry an `id`.
208
209| `type` | Fields | Meaning |
210|---|---|---|
211| `turn_start` | `step` | Beginning of one model call (max-steps loop iteration) |
212| `user_message` | `content`, `time` | The submitted prompt as it was added to the transcript |
213| `assistant_start` | (none) | About to receive assistant streaming |
214| `text_delta` | `delta` | Partial assistant text. Concatenate to build the full reply |
215| `tool_call` | `id`, `name`, `args` | The model wants to call a tool |
216| `tool_progress` | `id`, `text` | Optional progress line from the tool while it runs |
217| `tool_result` | `id`, `is_error`, `content` | Tool finished |
218| `assistant_message` | `content`, `time` | Final assistant message after the model turn ends |
219| `usage` | `input`, `output`, `reasoning`, `cache_read`, `cache_write`, `cost_usd`, `cumulative` | Per-turn + cumulative tokens / cost. `reasoning` is null when unavailable. |
220| `turn_end` | `stop`, optional `error` | One model call finished. `stop` is `end_turn`, `tool_use`, `length`, `error`, or `aborted` |
221| `done` | (none) | The whole prompt/compact completed (success or error) |
222| `error` | `message` | Non-fatal error message |
223| `compact_done` | `summary` | Compaction finished, summary text included |
224
225## Message shape
226
227Used by `get_messages` and inside `user_message` / `assistant_message` events.
228
229```json
230{
231 "role": "user",
232 "content": [<content_block>...],
233 "time": "2026-04-19T11:30:00Z"
234}
235```
236
237### Content block types
238
239```json
240{"type": "text", "text": "..."}
241{"type": "image", "mime_type": "image/png", "bytes": 12345}
242{"type": "tool_call", "id": "toolu_xyz", "name": "read", "args": {"path": "..."}}
243{"type": "tool_result", "call_id": "toolu_xyz", "is_error": false, "content": [<content_block>...]}
244```
245
246Image bytes are reported as a count rather than embedded base64 to keep transcript dumps small. Tool results may nest text and image blocks.
247
248## Reference clients
249
250See `examples/rpc/` for working implementations in:
251
252- `shell` — `bash` + `jq` one-liner
253- `python` — `subprocess.Popen` + `json.loads` per line
254- `node` — `child_process.spawn` + `readline`
255- `go` — direct subprocess wrapper (the `packages/agent/sdk` SDK is the in-process Go API)
256
257## Versioning
258
259The `protocol_version` field in the `hello` response is the major version of this schema. Backwards-incompatible changes bump it. The set of supported events and commands within a major version only grows.