> ## Documentation Index
> Fetch the complete documentation index at: https://www.agent37.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Streaming

> Stream a reply as named Server-Sent Events from your instance URL, and reconnect without losing the answer.

Send `stream: true` on [a message](/docs/agents-api/chat) and the reply comes back as Server-Sent Events. Each event is named, so you can render text, reasoning, and tool activity live. Events arrive in order, and the terminal `response.completed` event carries the final `output_text`, `usage`, and `context`.

The base URL is your instance URL: `https://{instanceId}.agent37.app`, with the same `sk_live_` key sent as the `X-Agent37-Key` header on every request. This page documents the gateway's streaming contract, the API every instance serves.

## Start a stream

```bash curl theme={null}
curl -N https://ab12cd34ef.agent37.app/v1/responses \
  -H "X-Agent37-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Research the top 3 EV makers, write a memo.",
    "stream": true
  }'
```

If the instance is sleeping, the request wakes it first and no response headers arrive until the wake completes, so the first frame is subject to the same wait as any other request to the instance URL: give the request a first-byte timeout of at least three minutes, and past its wake budget the edge answers `503 wake_timeout` instead. The connection stays open and frames arrive as `event:` plus `data:` pairs separated by a blank line:

```text stream theme={null}
event: response.created
data: {"id":"c91d2a7e84f04b6f9a3d5e1c0b87f4a2","session_id":"7f3e0b6c52a949d2b1c4a8e9d0f31726"}

event: response.reasoning.delta
data: {"text":"Comparing deliveries and margins across the big three..."}

event: response.tool_call.generating
data: {"tool":"web_search"}

event: response.tool_call.started
data: {"tool":"web_search","label":"EV deliveries 2025","arguments":{"query":"EV deliveries 2025"}}

event: response.tool_call.completed
data: {"tool":"web_search","duration_ms":1840}

event: response.output_text.delta
data: {"text":"## EV market memo\n\n"}

:keepalive

event: response.completed
data: {"output_text":"## EV market memo\n\n...","usage":{"input_tokens":1840,"output_tokens":920,"cost_usd":0.0137},"context":{"used_tokens":22600,"window_tokens":256000}}
```

## Events

There are exactly nine event types:

| Event                           | Payload                                                                                                                                                                              |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `response.created`              | `{ id, session_id }`, always first; the response id and the session it runs in                                                                                                       |
| `response.reasoning.delta`      | `{ text }`, a chunk of the agent's thinking                                                                                                                                          |
| `response.output_text.delta`    | `{ text }`, a chunk of the visible answer                                                                                                                                            |
| `response.tool_call.generating` | `{ tool }`, the model has begun a call to `tool` and is still writing its arguments; show the tool name so a long file write does not read as silence                                |
| `response.tool_call.started`    | `{ tool, label?, arguments? }`, the call is running; `label` is the harness's one-line summary and `arguments` the tool's actual input as an object, with long string values clipped |
| `response.tool_call.completed`  | `{ tool, duration_ms? }`                                                                                                                                                             |
| `response.tool_call.failed`     | `{ tool, error? }`, the run continues                                                                                                                                                |
| `response.completed`            | `{ output_text, usage, context }`, terminal; `usage` and `context` can be `null`, and `cost_usd` inside `usage` is absent or `null` when the provider reports no cost                |
| `response.failed`               | `{ error: { code, message, param?, hint? } }`, terminal                                                                                                                              |

<Info>
  These event names and payloads are the gateway's streaming contract, not a per-agent detail. Hermes, OpenClaw, Claude Code, Codex, Grok, and OpenCode emit them today, and the agents that follow will emit the same nine, so your client code does not change when you switch templates. Two pieces depend on what the harness reports: `response.tool_call.generating` and the `arguments` field arrive from Hermes today and are absent on the others, so treat both as extra detail, never as something to wait for.
</Info>

Rules the stream always follows:

* `response.created` is always first, and exactly one terminal event (`response.completed` or `response.failed`) ends every live stream.
* Every 30 seconds the gateway writes the comment line `:keepalive`, whether or not events are flowing. Comments are not events: ignore any line starting with `:`. There is no time limit on a turn; the stream stays open until the agent is done.
* There is no `[DONE]` sentinel. The server closes the connection right after the terminal event; terminal event plus close is end of stream.
* Once streaming starts, failures arrive as a `response.failed` event with the standard error body, never as an HTTP error status.

<Note>
  A cancelled turn (`POST /v1/responses/{id}/cancel`) still ends with `response.completed`, carrying whatever `output_text` accumulated before the cancel. Only failures emit `response.failed`. The stored response's `status` is `cancelled` and its `context` is `null` (a cancelled turn reports no measurement; the session's `context` keeps the last reported value). On OpenClaw, input that OpenClaw treats as one of its own stop commands (for example `stop` or `exit`) never starts a run, so that turn also ends with `response.completed` (empty `output_text`, `usage: null`) and the status `cancelled`.
</Note>

## Reconnect after a drop

```text theme={null}
GET /v1/responses/{id}/stream
```

If your connection drops mid-turn, reconnect with the response id from `response.created`. Lost the id too (page reload, new device)? [`GET /v1/sessions/{id}`](/docs/agents-api/sessions#retrieve-a-session-with-history) returns the running response as `active_response_id`; `null` means no turn is in flight (see that field's timing notes on the Sessions page).

```bash curl theme={null}
curl -N https://ab12cd34ef.agent37.app/v1/responses/c91d2a7e84f04b6f9a3d5e1c0b87f4a2/stream \
  -H "X-Agent37-Key: sk_live_..."
```

While the run is live, the gateway replays the entire ordered event buffer from `response.created` onward, then stays attached for the rest of the run. If the run just finished, it replays the buffer and ends. The buffer holds up to 100,000 events per run; the rare run that exceeds it stops buffering, so a reconnect replays the first 100,000 events and may end without the terminal event. When that happens, wait for `active_response_id` on [`GET /v1/sessions/{id}`](/docs/agents-api/sessions#retrieve-a-session-with-history) to read `null`, then recover the final answer from the same call's `history`.

<Note>
  **Reconnect and the answer is still there.** Reconnect within about 30 minutes of a turn finishing and `/stream` still replays the final `output_text`. One caveat: about 60 seconds after a turn finishes, the in-memory event buffer expires and the replay is synthesized from the retained in-memory response record as `response.created`, one `response.output_text.delta` carrying the full text (omitted when the turn produced none), then the terminal event. Reasoning and tool-call events from the original run are not preserved in that synthesized replay. After the record expires (about 30 minutes, on a gateway restart, or sooner on a busy instance: the gateway keeps at most 1,000 response records and drops the oldest when a new turn starts past that cap) `/stream` returns `404 response_not_found`; recover the answer from the [session transcript](/docs/agents-api/sessions) instead, which holds every finished turn. (The transcript holds finished turns only: the harness writes a turn's messages at turn end, so a still-running turn is never in it. To find a running turn, read `active_response_id` from `GET /v1/sessions/{id}`.)
</Note>

## Parse the stream

No SSE library needed. Read the response body, split on the blank-line frame boundary, skip comment lines, and branch on each frame's `event:` line. Stop when the connection closes after a terminal event.

```javascript node theme={null}
const res = await fetch("https://ab12cd34ef.agent37.app/v1/responses", {
  method: "POST",
  headers: {
    "X-Agent37-Key": "sk_live_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    input: "Research the top 3 EV makers, write a memo.",
    stream: true,
  }),
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  // SSE frames are separated by a blank line
  const frames = buffer.split("\n\n");
  buffer = frames.pop(); // keep the trailing partial frame

  for (const frame of frames) {
    if (frame.startsWith(":")) continue; // comment line, e.g. :keepalive

    const event = frame.match(/^event: (.+)$/m)?.[1];
    const data = JSON.parse(frame.match(/^data: (.+)$/m)?.[1] ?? "{}");

    switch (event) {
      case "response.output_text.delta":
        process.stdout.write(data.text); // stream the answer
        break;
      case "response.reasoning.delta":
        // show the agent thinking, if you want
        break;
      case "response.tool_call.generating":
        console.log(`\n[${data.tool}] writing the call...`);
        break;
      case "response.tool_call.started":
        console.log(`\n[${data.tool}] ${data.label ?? ""}`);
        break;
      case "response.completed":
        console.log("\nusage:", data.usage);
        break;
      case "response.failed":
        console.error("\nerror:", data.error);
        break;
    }
  }
}
```

<Note>
  The browser's built-in `EventSource` cannot send a POST body or custom headers like `X-Agent37-Key`, so it cannot start a stream here. Use `fetch` as above, in the browser and in Node. The [hermes-chat example](https://github.com/agent37-platform/examples/tree/main/hermes-chat) runs a browser version of this parser in a real chat UI (`public/chat.js`), with reconnect and cancel wired in.
</Note>

<Tip>
  Prefer not to stream? Send `stream: false` (the default) and the call returns the finished response as one JSON body, with the agent's reply in `output_text`.
</Tip>
