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

ShapeDetectionFrontend handling
Structured eventValid JSON with a typeMapped to a typed object and dispatched
Plain text chunkNot JSONAppended as plainText to the body — content is never dropped
Error frameStarts 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

EventKey fieldsPurpose
contenttextBody delta
thinkingtextCollapsible reasoning trace
tool_callid / name / arguments / hiddenA tool call starts
tool_resultid / name / content / isError / collapsed / hiddenTool output (collapsible, hideable)
approval_requestid / name / argumentsTool awaiting approval (interactive card)
questiontoolCallId / dataAsking the user for information
todo / plandata / toolCallId / dataPlan and todo list
usagepromptTokens / completionTokens / cachedTokens / totalTokens / latencyMsUsage badge
file_changepath / actionFiles changed in this session
checkpointid / label / fileCountCheckpoint recorded
subagentid / description / stage / tool / steps / messageSub-agent progress (code sessions persist it for replay)
search_results / knowledge_results / memory_resultsresults[]Web / knowledge / memory hit lists
noticekind / textSystem notices (context compacted, worktree creation failed …)
steeringtextA steer was accepted
debugtextDebug 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

AspectChatCode session
Error framesRendered directly[ERROR] prefix is re-applied before the shared parser
subagentNot special-casedAlso persisted for replay
Retrieval resultsRendered as hit listsNot shown on the timeline today (no-op in the dispatcher)
doneAppended by the callerExplicitly appended after the loop

Implementation notes

  • Field names and casing must stay stable: the frontend maps the exact JSON keys the backend writes.
  • approval_request and question both carry ids but mean different things (approve a tool vs. ask a person) — never interchange them.
  • Missing usage fields fall back to 0; in code sessions latencyMs is computed client-side.
  • The plainText fallback is deliberate: even if only bare text arrives (proxies, intermediaries), the body is preserved.
  • Adding an event: backend adds the type and fields, the parser adds a mapping, each reducer decides rendering — unmapped events are never lost.