POST /api/agent can return a Server-Sent Events body when the client asks for it.
Default remains a single JSON { text, toolTrace?, cwd? } response for tests and simple clients.
| Client | Server |
|---|---|
Header Accept: text/event-stream |
Content-Type: text/event-stream; charset=utf-8 + SSE events |
| Other / missing Accept | JSON { text, toolTrace?, cwd? } or { error } |
Early failures (auth, grants, bad body, BYOK) always use JSON status responses — even if Accept requested a stream. Typical statuses are 401 (unauthenticated), 403 (no usable sandbox grant and no alternate tools / denied), and other 4xx for bad body or bad data. There is no sandbox-not-configured 503 → chat fallback; host stream clients must parse the JSON error and must not special-case a chat-fallback string (the route never emits it).
Response hints: Cache-Control: no-cache, no-transform, X-Accel-Buffering: no.
| Field | Required | Notes |
|---|---|---|
prompt |
yes | Same limits as chat |
modelId |
no | Gateway model id |
cwd |
no | Logical workspace-root-relative directory for this turn |
Omitted / null cwd: server uses SANDBOX_DEFAULT_CWD when set and valid, else ".".
Present but invalid (host-absolute, control chars, non-string): 400 JSON error — not a stream.
Response cwd: included on JSON success and SSE done only when FS tools ran this turn; always a normalized workspace-relative path. Host session should update stored cwd only on success (never on abort/error).
Each SSE block is one data: <json>\n\n line:
type |
Fields | Host use |
|---|---|---|
tool_start |
name, optional id |
Live-paint one display-only tool_run card (protocol v11 / kind 6) on this event |
tool_result |
name, ok, summary, optional preview |
Grow the open tool_run card in place (total increments) while the last ring row is a tool-run; else open a NEW card at 1. preview is a bounded, redacted level-2 detail body (TOOL_RUN_PREVIEW_MAX_CHARS = 100k, head+tail … (N more lines)) built from flattened+redacted tool output — not raw MCP envelopes |
reasoning_delta |
text (chunk) |
Grow a Thinking bubble (protocol v8) |
text_delta |
text (chunk) |
Grow Assistant bubble(s) |
done |
text, optional toolTrace, optional cwd |
Collapse open thinking; finalize session; apply cwd on success only; Ready |
error |
error, optional status |
Collapse open thinking; Error message; Ready |
Unknown types are ignored (forward-compatible). String fields are redacted server-side with the same secret list as JSON responses.
The stream tool_result event carries an optional preview field: a bounded,
redacted level-2 detail body for the harness expander (phase 3 #353). It is built
from the already flattened + redacted tool output (never a raw MCP JSON
envelope), keeps the real first 40 / last 10 lines of the full output
joined by … (M more lines) — head/tail are taken before the char cap, so
the L2 "tail" is genuinely the end-of-output — and is capped at
TOOL_RUN_PREVIEW_MAX_CHARS (100k) per tool. Short single-line results omit
preview, so the host paints a static label instead of a duplicate-of-L1 blank
expander. The host feeds preview into the level-2 detail of the aggregated
tool_run message; because a whole tool_run row encodes into ONE message, a
group-level encode budget + a hard clamp to the 262 144-byte/msg cap keep a
multi-preview streak from overflowing it (never a silent mid-payload clip).
JSON fallback (Accept other than text/event-stream) keeps a one-line
level-2 detail from summary — documented parity for the tests/simple-clients
path, not silently diverging from the stream.
Assistant and thinking growth use protocol v8 inv_update_last_message so streaming does not create one ring message per token. Each uninterrupted tool streak is aggregated by the host into ONE tool_run message (protocol v11 / bridge kind 6) that the Wasm paints as a default-collapsed expandable control — not System lines. The tool card is painted live: each tool_start/tool_result opens (or grows) that ONE kind-6 row immediately — 1 tool called → 2… — via update_last, growing iff the last ring row is a tool-run; otherwise a new card at 1. A thinking row that lands last (and any assistant/user/error row) is a physical separator and opens a fresh card (see harness-limits.md — commit-once is removed). Thinking uses MessageKind.Thinking (muted warm chrome + same GFM paint as assistant). No dual DOM transcript. In-place stream growth relies on harness stick-to-bottom (content height) so the viewport follows when the user is near the bottom — not on new SSE event types.
While a Thinking segment is open, the host grows the full monologue (≤ Wasm MAX_MSG_LEN, currently 256 KiB). While the harness is Busy, every thinking row that belongs to the current turn stays fully visible (including the live streaming one and any segments the turn has already closed). Once the turn completes (done, error, or cancel → Ready/err), every committed thinking row from completed turns auto-collapses to a compact expandable control: a Thinking expander + a one-click re-expand to the full markdown monologue (muted one-line preview while collapsed, Copy on the header). This collapses accumulated reasoning so scrolled-away thinking stops being laid out and repainted every frame (composing with the O(dirty) slot-keyed parse + cache_layout paint for the rich transcript). Expand state is in-memory only — thinking is ephemeral and never survives refresh. There is no per-turn thinking-segment or live-tool line product cap — use Stop to cancel.
While a turn is busy, the harness shows Stop. That sets a protocol v9
pending cancel; the host polls it, aborts the in-flight fetch (AbortSignal),
and the existing cancel path surfaces Request cancelled. (no new SSE event type,
no chat fallback). Stopping before the host starts the turn discards a pending
submit so no ghost request is sent.
| Kind | SessionStore | History fold |
|---|---|---|
| User / Assistant | Yes | Included |
tool_run (live tools, protocol v11) |
Yes (live-paint path: open card appended / grown in place) | Not folded (display-only, plan #345) — continue may re-run tools |
| Error | Yes | Included as Error: (stall/cancel context) |
| Thinking | No (display-only) | Never |
| Control | Effect |
|---|---|
| Model id (harness picker) | Choose a reasoning-capable granted model when desired |
AGENT_REASONING |
Optional SDK effort: provider-default | none | low | medium | high |
When AGENT_REASONING is unset, the server enables reasoning: provider-default only if the model id looks reasoning-capable (reasoning / thinking in the id, but not non-reasoning). Other models omit the option.
Every harness turn paints a final line:
| Outcome | Line |
|---|---|
| Model finished | Turn ended · model finished (System) |
| User Stop | Turn ended · you stopped (System) |
| Error / timeout / empty | Turn ended · error · … / timed out / empty (Error) |
| Chat fallback | Turn ended · chat finished (System) |
These markers are not folded as tools into the next prompt.
Product philosophy: no live-tool / thinking-segment UX walls — cancel with Stop.
| Cap | Value | Behavior |
|---|---|---|
Live tool_run groups |
one per uninterrupted streak (painted live, grows per event) | Consecutive tools paint ONE growing card; a thinking/assistant/user/error row that lands last opens a NEW card |
| Thinking segments | none | Every reasoning segment paints |
| Thinking / line chars | 256 KiB | Wasm MAX_MSG_LEN only (bridge hard edge) |
| Ring slots | 2048 | Wasm MAX_MSG; older drop when full; Load earlier for SessionStore |
| Tool summary length | salient ≤320 | salientToolBits — path/counts/status only; not full read_file/exec/http bodies |
Tool level-2 preview |
≤ 100k per tool (TOOL_RUN_PREVIEW_MAX_CHARS), real head 40 / tail 10 lines + … (M more lines) |
Bounded + redacted server-side; short single-line results omit it (static label). Whole-group encoded-detail budget + hard clamp keep any multi-preview tool_run row ≤ 262 144 B/msg |
tool_run group payload |
≤ 262 144 B (TOOL_RUN_MSG_HARD_MAX) |
Host clips/omits memorized previews (explicit … or static label) rather than overflowing the ring/cloud per-msg cap |
| JSON end-of-turn toolTrace lines | none | All entries shown; level-2 detail stays the one-line summary (parity) |
stepevents / step status stripPOST /api/chatSSE (agent stream is the product path)
| Concern | Path |
|---|---|
| Event map / tool summary | lib/agent/agentStream.ts |
| streamText + reasoning option | lib/agent/runAgent.ts, lib/agent/reasoningConfig.ts |
| Route SSE vs JSON | app/api/agent/route.ts |
| Logical cwd parse / default env | lib/agent/agentBody.ts, lib/sandbox/config.ts (SANDBOX_DEFAULT_CWD), lib/agent/workPath.ts |
| Host consumer + collapse/caps | lib/harnessChat.ts, lib/agentApi.ts |
| Thinking paint | native/harness/src/ui.zig (protocol v8 kind) |
| Feature divide | feature-divide.md |