SSE Events
Agent-service streaming endpoints respond with Content-Type: text/event-stream. This page is the contract for parsing those streams.
Wire format
- Each event is a frame ending in a blank line (
\n\n). - Each frame is a single line:
data:followed by a JSON object. - The terminal frame is always
{"type":"done", ...}.
data: {"type":"run_started","runId":"clx..."}
data: {"type":"message_chunk","content":"Hi","runId":"clx...","stepId":"stp..."}
data: {"type":"done","runId":"clx..."}Event object
interface AgentSseEvent {
type: AgentEventType
runId?: string // present on every emitted event
stepId?: string // present on every emitted event
sequence?: number
content?: string // text delta (message_chunk)
payload?: Record<string, unknown>
data?: unknown // structured result (e.g. resume_update)
error?: string // present on error frames
}Event catalog
type | When | Key fields |
|---|---|---|
run_started | Run created | runId |
agent_plan | Agent emits a plan | payload |
plan_update | Plan updated (DeepAgents) | payload |
skill_loaded | A skill was loaded (skills-as-tools) | payload |
subagent_started / subagent_completed | A subagent begins / ends | payload, data |
step_started / step_completed | A step begins / ends | stepId |
llm_started / llm_usage | LLM call lifecycle / token usage | payload |
tool_started / tool_result / tool_completed | Tool-call lifecycle | payload, data |
message_chunk | Incremental assistant text | content |
resume_patch | Partial resume change | payload, data |
resume_update | Full revised resume | data, payload.resume |
resume_analysis | Analysis result | payload, data |
translation_result | Translation output | data |
interview_question | Next interview question | payload |
critique / suggestion | Review feedback | payload, data |
run_completed | Run finished OK | runId |
run_failed | Run failed mid-stream | error |
done | Terminal frame — stop reading | runId |
error | Stream-level error | error |
Not every run emits every type — the set depends on the endpoint and what the agent does. Switch on type and ignore the ones you don’t handle.
Robust parser
async function* sse(res: Response) {
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
for (;;) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const frames = buffer.split('\n\n')
buffer = frames.pop() ?? '' // keep partial frame for next read
for (const frame of frames) {
const line = frame.split('\n').find((l) => l.startsWith('data: '))
if (line) yield JSON.parse(line.slice(6))
}
}
}
// usage
for await (const ev of sse(res)) {
if (ev.type === 'message_chunk') append(ev.content)
if (ev.type === 'resume_update') save(ev.data)
if (ev.type === 'error') throw new Error(ev.error)
if (ev.type === 'done') break
}Always buffer across reads. A single TCP chunk can split a frame, and a frame can span multiple chunks. Parsing line-by-line without buffering will throw on partial JSON.
Stop on done. After run_completed you may still get the done frame; done is the definitive end-of-stream signal and carries the final runId you can use to replay the run.
Last updated on