"type": "module" and the tsx runner.
Use Agent.stream() or Session.stream() for normal application integrations. This page explains the lower-level protocol those APIs coordinate; it is not necessary to implement SSE manually to use hooks or tools.
Start with a typed SDK stream
With the quickstart setup andOPENAI_API_KEY, save this as events.ts and run node --env-file=.env --import tsx events.ts:
message.result. Full assistant messages and partial deltas are different views of the output; do not concatenate both into the same text buffer. The example selects text deltas rather than printing every partial field indiscriminately.
Raw SSE envelopes
GET /event sends JSON in SSE data: records. The first event is server.connected; heartbeat events keep the connection active. An example framing is:
GET /global/event uses a different envelope with the event under payload, and can cover multiple instances. Do not decode it as the bare /event schema. Global and instance streams include events unrelated to any one session.
For a custom client, connect in the correct directory/workspace context, wait for readiness, then submit the prompt. Filter events by their session identifiers and track message/part IDs; subscribing after submission can miss an early callback or completion. A heartbeat says the connection is alive, not that a model turn succeeded.
Reconnection is not a guarantee that every missed event is replayed. Reconcile persisted messages and pending control requests after a disconnect, and avoid emitting duplicate application effects when reconciling.
Callback requests
Hooks, permission callbacks and direct SDK MCP tools are functions in the client process. The runtime emitscontrol.request with fields including id, sessionID, subtype, payload, createdAt and timeoutMs. The subtype identifies the operation: can_use_tool, hook_callback, mcp_message or elicitation.
A registered dispatcher executes the corresponding local callback and posts a correlated response to POST /session/{sessionID}/control-response. For illustration, a denial response for an existing permission request has this shape; replace the request ID with the actual received UUID:
resolved boolean. Do not interpret HTTP success alone as proof that a stale request was resolved. Match session, request ID and subtype; do not apply a late response to a later operation.
GET /session/{sessionID}/control-requests lists pending requests. POST /session/{sessionID}/control-cancel accepts the request ID and subtype to cancel one. Consult the complete OpenAPI document for each exact payload; the live /doc route in v2 contains only a limited control-plane schema.
Cancellation and callback ownership
The runtime emitscontrol.cancelled when a request times out, is cancelled or is shut down. The SDK maps this to callback cancellation signals. Honor that signal in network calls and long-running operations; arbitrary JavaScript and completed external effects are not rolled back automatically.
The process that registers callbacks must keep their dispatcher alive. Child tasks can route requests through the owning root session; preserve the root/active-session identity carried by the payload instead of assuming all requests belong to a single flat session. Functions are not persisted with transcript data. Reattach real callbacks after a process restart or remove the registrations.
A missing or cancelled permission callback must not be treated as approval. Depending on the policy, an unresolved operation may still wait for a user decision; headless integrations should use deterministic rules and explicit cancellation deadlines. See permissions.
Direct SDK tools use this control channel. HTTP MCP tools use MCP transport instead. Both feed the runtime’s tool loop, but their connection and shutdown responsibilities differ. Avoid running competing custom dispatchers for registrations already owned by an SDK session handle.