FeaturesMessages

Receiving a stream

Receive session events over SSE, and resume with Last-Event-ID after a disconnect

sessions.events.stream (GET /v1/sessions/{id}/events) receives the session's events as Server-Sent Events. It first replays the events already stored from the beginning, then continues with new events as they are created. The scope is sessions:read. Use the session from Send (sessionId in the SDK, SID in the HTTP API).

Receive

The stream sends a keepalive every 15 seconds, so it does not close even when everything is normal. The example below cuts it off after 5 seconds. The SDK cuts it off with signal or stream.close(), and curl with --max-time.

import { AbortedError, type SessionEvent } from "@werft-ai/sdk";

const seen: SessionEvent[] = [];
const stream = werft.sessions.events.stream(sessionId, {
  signal: AbortSignal.timeout(5_000),
});
try {
  for await (const event of stream) seen.push(event);
} catch (error) {
  if (!(error instanceof AbortedError)) throw error;
}
const counts = new Map<string, number>();
for (const event of seen) counts.set(event.kind, (counts.get(event.kind) ?? 0) + 1);
console.log(counts);
Map(6) {
  'status' => 5,
  'system' => 8,
  'assistant' => 2,
  'result' => 2,
  'tool_use' => 1,
  'tool_result' => 1
}

The SDK's event has id, turn_id, attempt_id, occurred_at, kind (the event in the table below), and payload (the inner data).

All events of this session's two turns (the preparation message and ls) arrived. One frame is three lines, event:, data:, and id:, plus a blank line.

event: tool_use
data: {"schema_version":1,"session_id":"…","turn_id":"2","attempt_id":"att_…","occurred_at":"…","data":{…}}
id: ev_3m

data is JSON. It identifies the session, turn, and attempt the event belongs to, includes a timestamp, and holds the event content in the nested data field.

eventInner data
systemStart and configuration information from the engine
assistantA model message. The text is in text, the thinking in thinking
tool_useThe tool the model called and its input (id, name and input of tool_call)
tool_resultThe tool's result (tool_call_id and text of tool_result). is_error: true on failure
questionA permission request or question was raised. How to answer: Waiting for approval and resuming
resultThe engine finished the turn. This is not a completion verdict
statusThe session state changed (phase: running, idle, and so on)
errorAn execution error

Inner fields not in this table (message, session_id and so on) differ by engine. They can go away or change shape with another engine, so do not read them.

Extract text and tool results

for (const event of seen) {
  if (event.kind === "assistant")
    console.log((event.payload as { text?: string }).text);
  if (event.kind === "tool_result") {
    const result = (event.payload as { tool_result?: { is_error?: boolean } })
      .tool_result;
    console.log({ is_error: result?.is_error });
  }
}
ready
{ is_error: false }
listed

Check whether a turn has finished with the turn's status, not with events (Send). Judge whether the work was done from the contents of assistant and tool_result.

Resume after a disconnect

Pass the last id you received to receive from the next event on. The SDK passes stream.lastEventId as the lastEventId option, and the HTTP API passes it in the Last-Event-ID header. When the connection drops, the SDK reconnects on its own with this value. Events you already received do not come again.

const more = werft.sessions.events.stream(sessionId, {
  lastEventId: stream.lastEventId,
  signal: AbortSignal.timeout(3_000),
});
let count = 0;
try {
  for await (const _event of more) count++;
} catch (error) {
  if (!(error instanceof AbortedError)) throw error;
}
console.log(count);
0

No new events came after that, so nothing arrived. If you sent the next message in the meantime, that turn's events would arrive.

ResponseMeaning
410 CURSOR_EXPIREDThat id is outside the retention range. Re-read the session and turns over HTTP and connect anew. The server currently does not delete events and so does not send this response, but clients should handle it
429Too many streams are open at the same time

Next steps

On this page