Skip to Content
ReferenceSSE Events

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

typeWhenKey fields
run_startedRun createdrunId
agent_planAgent emits a planpayload
plan_updatePlan updated (DeepAgents)payload
skill_loadedA skill was loaded (skills-as-tools)payload
subagent_started / subagent_completedA subagent begins / endspayload, data
step_started / step_completedA step begins / endsstepId
llm_started / llm_usageLLM call lifecycle / token usagepayload
tool_started / tool_result / tool_completedTool-call lifecyclepayload, data
message_chunkIncremental assistant textcontent
resume_patchPartial resume changepayload, data
resume_updateFull revised resumedata, payload.resume
resume_analysisAnalysis resultpayload, data
translation_resultTranslation outputdata
interview_questionNext interview questionpayload
critique / suggestionReview feedbackpayload, data
run_completedRun finished OKrunId
run_failedRun failed mid-streamerror
doneTerminal frame — stop readingrunId
errorStream-level errorerror

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