v0.50.1 a637010

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. A custom --system-prompt replaces the built-in identity and docs guidance, while shared harness guidance and append addenda remain present. Shared guidance asks agents to verify changes with checks appropriate to their scope and report which checks ran and what remains unverified.

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. A custom `--system-prompt` replaces the built-in identity and docs guidance, while shared harness guidance and append addenda remain present. Shared guidance asks agents to verify changes with checks appropriate to their scope and report which checks ran and what remains unverified.
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