v0.45.0 cb619f7

rpc.md RawHistoryFolder

zut RPC

zut 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 zut rpc; talk to it from a shell
( echo '{"id":"1","type":"prompt","message":"hello"}'; sleep 5 ) \
  | zut 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 zut 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

zut 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.

RPC agents use the same resolved system-prompt policy as other modes. Ponytail coding mode is enabled when ponytail_enabled is missing or true in $ZUT_HOME/config.json, and disabled when it is false. A custom --system-prompt replaces the built-in identity and docs guidance, while enabled append addenda remain present.

Auth

If the environment variable ZUTCORE_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 ZUTCORE_RPC_TOKEN set, no auth is required (the spawning process is implicitly trusted; if it can spawn zut it can also read your auth.json directly).

Wire format

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 ZUTCORE_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": "/path/to/zut",
  "message_count": 12,
  "busy": false,
  "usage": {"input": 1234, "output": 567, "reasoning": 123, "cache_read": 890, "cache_write": 0, "cache_measured_prompt": 1234, "cache_measured_read": 890, "cost_usd": 0.0123}
}

get_messages

Full transcript.

{"id":"5","type":"get_messages"}

Response data: {"messages": [<message>, ...]}. See message shape below.

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 zut 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)
request_started provider, model, scope, attempt, max_attempts A provider (scope=provider) or agent (scope=agent) request attempt started
cache_diagnostics eligible, mode, transport, continuation Sanitized cache-routing state. eligible is a conservative local prompt-length estimate; it never includes prompts, IDs, request bodies, or credentials
retry_scheduled scope, attempt, max_attempts, delay_ms The upcoming attempt is delayed; delay_ms is a non-negative integer
user_message content, time The submitted prompt as it was added to the transcript
assistant_start (none) Provider connection opened; waiting for assistant streaming
text_delta delta Partial assistant text. Concatenate to build the full reply
tool_use_start id, name Provider started streaming a tool call
tool_use_args id, delta Partial tool-call argument JSON
tool_use_end id Provider completed the tool-call argument stream
tool_call id, name, args The model proposed a tool call
tool_execution_started id, name Guard and confirmation checks passed; the tool is about to execute
tool_progress id, text Optional progress line from the tool while it runs
plan_update optional id, nullable explanation, plan The built-in plan tool changed the session checklist. Each plan item has step and status. id is the tool-call id that produced the update; it is omitted when empty.
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, cache_measured_prompt, cache_measured_read, cost_usd, cumulative Per-turn + cumulative tokens / cost. Cache ratios use only the measured cache fields; reasoning is null when unavailable.
turn_end stop, optional error One model call finished. stop is end, continue, tool_use, length, error, or aborted. continue ends one inference, not the logical task: the agent issues another model call in the same turn
turn_recovery reason, attempt The turn ended without a final answer, so the agent appended one bounded continuation. reason is missing_answer or commentary_only; attempt is always 1
done (none) The whole prompt/compact completed (success or error)
error message Non-fatal error message
compact_done summary Compaction finished, summary text included
ext_alert extension, kind, optional reason An extension requested a host alert; RPC clients can map it to their own notification UI.
ext_widget extension, id, position, title, lines Persistent extension widget update. Interactive zut renders widgets above the input; RPC clients choose their own layout.
ext_widget_clear extension, id Remove one persistent extension widget.
ext_chrome_clear extension The extension exited; discard all persistent status/widget chrome owned by it.

A compact request emits request-lifecycle, assistant_start, and summary text_delta events before compact_done. It does not emit model-loop turn or tool events.

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. Within a major version the schema only grows: new events, commands, and optional event fields may appear — such as the id field on plan_update — so consumers must ignore fields they do not recognize.

source
1# zut RPC
2
3`zut 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 zut rpc; talk to it from a shell
11( echo '{"id":"1","type":"prompt","message":"hello"}'; sleep 5 ) \
12  | zut 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 `zut 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`zut 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
28RPC agents use the same resolved system-prompt policy as other modes. Ponytail coding mode is enabled when `ponytail_enabled` is missing or `true` in `$ZUT_HOME/config.json`, and disabled when it is `false`. A custom `--system-prompt` replaces the built-in identity and docs guidance, while enabled append addenda remain present.
29
30## Auth
31
32If the environment variable `ZUTCORE_RPC_TOKEN` is set on the spawned process, the first line on stdin **must** be a `hello` command containing the matching token:
33
34```json
35{"id":"0","type":"hello","token":"shared-secret"}
36```
37
38If absent or wrong, the response carries `success:false` and the process exits. Without `ZUTCORE_RPC_TOKEN` set, no auth is required (the spawning process is implicitly trusted; if it can spawn `zut` it can also read your `auth.json` directly).
39
40## Wire format
41
42Every line in either direction is one JSON object terminated by `\n`. Object boundaries follow newline boundaries — no multi-line JSON.
43
44### Frame types
45
46| `type` | Direction | Description |
47|---|---|---|
48| any command (`prompt`, `abort`, ...) | client → server | Request |
49| `response` | server → client | Reply to one command, correlated by `id` |
50| any event (`text_delta`, `tool_call`, ...) | server → client | Stream notification (no `id`) |
51
52## Commands
53
54All 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.
55
56### `hello`
57
58```json
59{"id":"0","type":"hello","token":"shared-secret"}
60```
61
62Response:
63
64```json
65{"type":"response","id":"0","command":"hello","success":true,
66 "data":{"protocol_version":1,"version":"0.0.4","provider":"anthropic","model":"claude-opus-4-5"}}
67```
68
69Required as the first message when `ZUTCORE_RPC_TOKEN` is set; optional otherwise.
70
71### `prompt`
72
73```json
74{"id":"1","type":"prompt","message":"fix the failing test","images":[]}
75```
76
77Optional `images` is `[{"mime_type":"image/png","data":"<base64>"}]`.
78
79Response is immediate (the turn is starting):
80
81```json
82{"type":"response","id":"1","command":"prompt","success":true,"data":{"started":true}}
83```
84
85Then a stream of event objects (see below) until the turn ends with `{"type":"done"}`.
86
87### `abort`
88
89Cancel the active prompt or compact.
90
91```json
92{"id":"2","type":"abort"}
93```
94
95Response: `{"type":"response","id":"2","command":"abort","success":true}`.
96
97If the turn was streaming, the next events you see will be a `turn_end` with `stop:"aborted"` then `done`.
98
99### `compact`
100
101Summarise the current transcript into one synthetic user message. Same lifecycle as `prompt` (immediate response, then events).
102
103```json
104{"id":"3","type":"compact"}
105```
106
107Final event: `{"type":"compact_done","summary":"<text>"}`.
108
109### `get_state`
110
111Snapshot of the runtime.
112
113```json
114{"id":"4","type":"get_state"}
115```
116
117Response data:
118
119```json
120{
121  "provider": "anthropic",
122  "model": "claude-opus-4-5",
123  "cwd": "/path/to/zut",
124  "message_count": 12,
125  "busy": false,
126  "usage": {"input": 1234, "output": 567, "reasoning": 123, "cache_read": 890, "cache_write": 0, "cache_measured_prompt": 1234, "cache_measured_read": 890, "cost_usd": 0.0123}
127}
128```
129
130### `get_messages`
131
132Full transcript.
133
134```json
135{"id":"5","type":"get_messages"}
136```
137
138Response data: `{"messages": [<message>, ...]}`. See **message shape** below.
139
140### `clear`
141
142Drop the entire transcript. Equivalent to the `/clear` slash command.
143
144```json
145{"id":"6","type":"clear"}
146```
147
148### `set_model`
149
150Switch model within the same provider.
151
152```json
153{"id":"7","type":"set_model","model":"claude-sonnet-4-5"}
154```
155
156Cross-provider swaps require relaunching `zut rpc` with the new `--provider`.
157
158### `set_reasoning`
159
160Set the reasoning level for subsequent prompts without restarting the process. Accepted values are `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, and `max`.
161
162```json
163{"id":"8","type":"set_reasoning","reasoning":"max"}
164```
165
166The `max` tier is sent natively to supported models and clamped for other backends.
167
168### `get_models`
169
170List models known for the current provider.
171
172```json
173{"id":"8","type":"get_models"}
174```
175
176Response data: `{"models":[{"id":"...","provider":"...","context_window":200000,"max_output":8192,"reasoning":true}, ...]}`.
177
178### `ping`
179
180Health check.
181
182```json
183{"id":"9","type":"ping"}
184```
185
186Response: `{"type":"response","id":"9","command":"ping","success":true,"data":{"pong":true}}`.
187
188## Events
189
190Stream notifications during a `prompt` or `compact`. None carry an `id`.
191
192| `type` | Fields | Meaning |
193|---|---|---|
194| `turn_start` | `step` | Beginning of one model call (max-steps loop iteration) |
195| `request_started` | `provider`, `model`, `scope`, `attempt`, `max_attempts` | A provider (`scope=provider`) or agent (`scope=agent`) request attempt started |
196| `cache_diagnostics` | `eligible`, `mode`, `transport`, `continuation` | Sanitized cache-routing state. `eligible` is a conservative local prompt-length estimate; it never includes prompts, IDs, request bodies, or credentials |
197| `retry_scheduled` | `scope`, `attempt`, `max_attempts`, `delay_ms` | The upcoming attempt is delayed; `delay_ms` is a non-negative integer |
198| `user_message` | `content`, `time` | The submitted prompt as it was added to the transcript |
199| `assistant_start` | (none) | Provider connection opened; waiting for assistant streaming |
200| `text_delta` | `delta` | Partial assistant text. Concatenate to build the full reply |
201| `tool_use_start` | `id`, `name` | Provider started streaming a tool call |
202| `tool_use_args` | `id`, `delta` | Partial tool-call argument JSON |
203| `tool_use_end` | `id` | Provider completed the tool-call argument stream |
204| `tool_call` | `id`, `name`, `args` | The model proposed a tool call |
205| `tool_execution_started` | `id`, `name` | Guard and confirmation checks passed; the tool is about to execute |
206| `tool_progress` | `id`, `text` | Optional progress line from the tool while it runs |
207| `plan_update` | optional `id`, nullable `explanation`, `plan` | The built-in `plan` tool changed the session checklist. Each plan item has `step` and `status`. `id` is the tool-call id that produced the update; it is omitted when empty. |
208| `tool_result` | `id`, `is_error`, `content` | Tool finished |
209| `assistant_message` | `content`, `time` | Final assistant message after the model turn ends |
210| `usage` | `input`, `output`, `reasoning`, `cache_read`, `cache_write`, `cache_measured_prompt`, `cache_measured_read`, `cost_usd`, `cumulative` | Per-turn + cumulative tokens / cost. Cache ratios use only the measured cache fields; `reasoning` is null when unavailable. |
211| `turn_end` | `stop`, optional `error` | One model call finished. `stop` is `end`, `continue`, `tool_use`, `length`, `error`, or `aborted`. `continue` ends one inference, not the logical task: the agent issues another model call in the same turn |
212| `turn_recovery` | `reason`, `attempt` | The turn ended without a final answer, so the agent appended one bounded continuation. `reason` is `missing_answer` or `commentary_only`; `attempt` is always 1 |
213| `done` | (none) | The whole prompt/compact completed (success or error) |
214| `error` | `message` | Non-fatal error message |
215| `compact_done` | `summary` | Compaction finished, summary text included |
216| `ext_alert` | `extension`, `kind`, optional `reason` | An extension requested a host alert; RPC clients can map it to their own notification UI. |
217| `ext_widget` | `extension`, `id`, `position`, `title`, `lines` | Persistent extension widget update. Interactive zut renders widgets above the input; RPC clients choose their own layout. |
218| `ext_widget_clear` | `extension`, `id` | Remove one persistent extension widget. |
219| `ext_chrome_clear` | `extension` | The extension exited; discard all persistent status/widget chrome owned by it. |
220
221A `compact` request emits request-lifecycle, `assistant_start`, and summary
222`text_delta` events before `compact_done`. It does not emit model-loop turn or
223tool events.
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. Within a major version the schema only grows: new events, commands, and optional event fields may appear — such as the `id` field on `plan_update` — so consumers must ignore fields they do not recognize.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close