SSE event protocol
Chat and code sessions both stream "what the model is doing" over the same SSE channel. The protocol carries no business semantics: the backend writes structured frames, the frontend dispatches on type.
Three frame shapes
| Shape | Detection | Frontend handling |
|---|---|---|
| Structured event | Valid JSON with a type | Mapped to a typed object and dispatched |
| Plain text chunk | Not JSON | Appended as plainText to the body — content is never dropped |
| Error frame | Starts with [ERROR] | Rendered as an error state |
Unknown
type values are kept as unknown with the raw JSON, so shipping a backend event before its frontend mapping never loses frames.Event list
| Event | Key fields | Purpose |
|---|---|---|
content | text | Body delta |
thinking | text | Collapsible reasoning trace |
tool_call | id / name / arguments / hidden | A tool call starts |
tool_result | id / name / content / isError / collapsed / hidden | Tool output (collapsible, hideable) |
approval_request | id / name / arguments | Tool awaiting approval (interactive card) |
question | toolCallId / data | Asking the user for information |
todo / plan | data / toolCallId / data | Plan and todo list |
usage | promptTokens / completionTokens / cachedTokens / totalTokens / latencyMs | Usage badge |
file_change | path / action | Files changed in this session |
checkpoint | id / label / fileCount | Checkpoint recorded |
subagent | id / description / stage / tool / steps / message | Sub-agent progress (code sessions persist it for replay) |
search_results / knowledge_results / memory_results | results[] | Web / knowledge / memory hit lists |
notice | kind / text | System notices (context compacted, worktree creation failed …) |
steering | text | A steer was accepted |
debug | text | Debug output |
done | — | Turn finished (appended by the caller) |
Workflow events are separate
Workflow runs use their own names over the same transport: run_started / node_started / node_completed / node_failed / human_approval_required / agent_tool_call / run_completed / run_failed.
Differences between the two streams
| Aspect | Chat | Code session |
|---|---|---|
| Error frames | Rendered directly | [ERROR] prefix is re-applied before the shared parser |
subagent | Not special-cased | Also persisted for replay |
| Retrieval results | Rendered as hit lists | Not shown on the timeline today (no-op in the dispatcher) |
done | Appended by the caller | Explicitly appended after the loop |
Implementation notes
- Field names and casing must stay stable: the frontend maps the exact JSON keys the backend writes.
approval_requestandquestionboth carry ids but mean different things (approve a tool vs. ask a person) — never interchange them.- Missing
usagefields fall back to 0; in code sessionslatencyMsis computed client-side. - The
plainTextfallback is deliberate: even if only bare text arrives (proxies, intermediaries), the body is preserved. - Adding an event: backend adds the
typeand fields, the parser adds a mapping, each reducer decides rendering — unmapped events are never lost.