Subscriptions
SubscribeAgents streams every agent's state as it changes, with a snapshot, numbered events, resume and resync, heartbeats and filters.
SubscribeAgents keeps its connection open and writes one JSON line per message, so a client sees every agent open, change state and close without polling. A "Client" caller may subscribe, so programs outside the app can use it through api.json.
{"api_version":1,"token":"…","from":"Client","request":{"SubscribeAgents":{"filter":{"group":"api"}}}}Fields
SubscribeAgents takes three optional fields:
| Field | Meaning |
|---|---|
filter | Any of environment, group, harness (a display name such as "Claude Code", or acp:<id>) and terminals (a list of ids). Every field given must match; an empty filter, or none, admits every agent of every environment. |
since_seq | The last seq the client saw, to resume |
stream | The stream that seq came from |
Lines
The app writes, one per line:
| Line | Content |
|---|---|
AgentSnapshot | {"stream","seq","at","resync","agents":[...]}: the open agents the filter admits, in the shape List uses |
AgentEvent | One change of one agent |
Heartbeat | {"stream","seq","at"}, written when nothing else was written for 15 seconds |
Error | The last line before the app closes the stream |
{"AgentEvent":{"seq":42,"at":"2026-11-10T09:15:02.481Z","kind":"state_changed","environment":"main","terminal":4,"title":"Claude Code · 4","group":"api","harness":"Claude Code","previous":"working","state":"waiting-for-input","coarse_state":"waiting-for-input","reason":"permission_pending"}}| Field | Meaning |
|---|---|
seq | Grows by one per event within a stream. A snapshot and a heartbeat carry the seq of the last event before them. |
stream | Identifies one run of the app; seq starts again in a new stream |
at | RFC 3339 in UTC with milliseconds |
kind | opened (previous is null), state_changed, or closed (state is closed) |
previous, state, coarse_state | The states before and after. See Agent states. |
reason | Present when known. See Reasons. |
Only changes of state are events: a new title alone does not make one.
The first line, resume and resync
A new subscription
Without since_seq, the first line is an AgentSnapshot with resync: false. Keep its stream and seq.
Resuming
With since_seq and the stream it came from, when that stream is the app's current one and the seq is still within the last 1024 events, the first line is a Heartbeat, followed by the missed events the filter admits.
Resync
Otherwise (another stream, a seq too old or in the future, or since_seq without stream) the first line is an AgentSnapshot with resync: true. Replace what you knew with it.
{"api_version":1,"token":"…","from":"Client","request":{"SubscribeAgents":{"filter":{"group":"api"},"since_seq":44,"stream":"9d3b5f0a7c214e6b8a90c1d2e3f4a5b6"}}}Slow subscribers
Each subscriber has a queue of 256 lines. A subscriber that lets it fill is dropped: it receives the lines already queued, then {"Error":{"code":"slow_subscriber","message":"..."}}, and the connection closes. Subscribe again with since_seq and stream.
When the app stops publishing, the last line is an Error with code unavailable.
Liveness
A client that hears nothing, not even a heartbeat, for three intervals (45 seconds) should assume the app is gone. krad agents watch exits with 2 then.
Example session
One line per message; the first is the client's.
{"api_version":1,"token":"6f1c…","from":"Client","request":{"SubscribeAgents":{"filter":{"group":"api"}}}}
{"AgentSnapshot":{"stream":"9d3b5f0a7c214e6b8a90c1d2e3f4a5b6","seq":41,"at":"2026-11-10T09:15:00.120Z","resync":false,"agents":[{"environment":"main","terminal":4,"title":"Claude Code · 4","group":"api","state":"working","harness":"Claude Code","is_self":false}]}}
{"AgentEvent":{"seq":42,"at":"2026-11-10T09:15:02.481Z","kind":"state_changed","environment":"main","terminal":4,"title":"Claude Code · 4","group":"api","harness":"Claude Code","previous":"working","state":"waiting-for-input","coarse_state":"waiting-for-input","reason":"permission_pending"}}
{"AgentEvent":{"seq":43,"at":"2026-11-10T09:15:09.007Z","kind":"state_changed","environment":"main","terminal":4,"title":"Claude Code · 4","group":"api","harness":"Claude Code","previous":"waiting-for-input","state":"working","coarse_state":"working","reason":"turn_started"}}
{"AgentEvent":{"seq":44,"at":"2026-11-10T09:16:30.550Z","kind":"opened","environment":"main","terminal":7,"title":"Codex · 7","group":"api","harness":"Codex","previous":null,"state":"idle","coarse_state":"idle"}}
{"Heartbeat":{"stream":"9d3b5f0a7c214e6b8a90c1d2e3f4a5b6","seq":44,"at":"2026-11-10T09:16:45.550Z"}}
{"AgentEvent":{"seq":45,"at":"2026-11-10T09:18:12.904Z","kind":"state_changed","environment":"main","terminal":4,"title":"Claude Code · 4","group":"api","harness":"Claude Code","previous":"working","state":"done","coarse_state":"idle","reason":"turn_finished"}}
{"AgentEvent":{"seq":46,"at":"2026-11-10T09:18:40.033Z","kind":"closed","environment":"main","terminal":4,"title":"Claude Code · 4","group":"api","harness":"Claude Code","previous":"done","state":"closed","coarse_state":"closed","reason":"user_closed"}}From the CLI
krad agents watch --group api
krad agents watch --harness claude --since 44 --stream 9d3b5f0a7c214e6b8a90c1d2e3f4a5b6krad agents watch prints the snapshot, whose first line names its seq and stream, then one line per event until interrupted. With --json it prints the raw lines. See krad CLI.