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

# Streaming

> The SSE event contract, exactly-once completion, and what a client should do on disconnect.

Set `"stream": true` on any execution endpoint and the body is
`text/event-stream` instead of JSON. Nothing else about the request changes.

One contract covers all three — `/responses`, `/conversations/{id}/messages` and
`/agents/{id}/runs` — so an event means the same thing wherever it arrives. A
client that can read a stream from one can read a stream from the others without
a second code path, because there is only one.

Provider-specific event shapes are never exposed. What you read is normalised.

## The event vocabulary

Contract version **`2026-09-07`**, sent on `response.started` as
`stream_version` so you can pin without a separate discovery call. It is a date
rather than an integer, so "which contract is this client on" and "when did it
change" are the same question.

| Event                        | Carries                                                                                      |
| ---------------------------- | -------------------------------------------------------------------------------------------- |
| `response.started`           | `response_id`, `status`, `stream_version`, `agent`, `conversation`, `request_id`, `trace_id` |
| `response.output_text.delta` | `delta` — a fragment of prose                                                                |
| `response.output_json.delta` | `delta` — a fragment of structured output                                                    |
| `response.tool.started`      | `name`                                                                                       |
| `response.tool.completed`    | `name`                                                                                       |
| `response.tool.failed`       | `name`                                                                                       |
| `response.completed`         | The whole response object                                                                    |
| `response.failed`            | `{"error": {code, message, request_id}}`, plus `id`, `status`, `trace_id`                    |
| `response.cancelled`         | The response object, with no output                                                          |

`cancelled` is its own terminal event rather than a flavour of success or
failure. Folding it into either would misreport a run that did exactly what it
was told.

## Exactly one terminal event, ever

`response.completed`, `response.failed` and `response.cancelled` are terminal.
**Exactly one is emitted per stream.** A stream that ends without one is a bug on
our side, not a state for you to handle.

This is structural rather than a rule someone follows. The object the execution
writes into has no method that can emit a terminal event — progress is the whole
of its API — and the terminal event is produced once, by the single consumer,
after the worker has finished.

## Deltas are advisory; the completion event is authoritative

<Warning>
  **Do not assemble your final answer by concatenating deltas.** Read it off
  `response.completed`.
</Warning>

The internal queue is bounded and drops fragments rather than blocking when a
client stops reading, because a worker blocked behind a slow consumer is a wedged
thread holding a database connection. What you lose in that case is a fragment
you would have rendered. What you keep is `response.completed`, which carries the
whole output.

Render deltas for the feel of it. Trust the completion event for the content.

## Comments are not events

A line beginning `:` is an SSE comment, sent about every **15 seconds** so that
proxies and load balancers do not close an idle connection.

```
: ping
```

Skip them. Do not parse them. They are not in the contract and carry no data.

## Failures before the first event are not streamed

No credential, no credits, an unpublished agent, a schema this runtime cannot
enforce: each is the ordinary JSON error with its own status code, because
nothing has been written to the wire yet and a failed execution must not answer
200\.

```
POST /responses  {"stream": true, ...}
  → 402 application/json
    {"error": {"code": "usage_blocked", ...}}
```

Once `response.started` is on the wire the response is already a 200, and a later
failure arrives as `response.failed` carrying the same error payload inside it.

So a streaming client needs both paths:

<Steps>
  <Step title="Check the response status and content type first">
    A non-2xx, or a `Content-Type` that is not `text/event-stream`, is an
    ordinary JSON error. Handle it exactly as you handle a non-streaming one.
  </Step>

  <Step title="Then read events until a terminal one">
    Skip `:` comments. Render deltas if you want them. Stop at
    `response.completed`, `response.failed` or `response.cancelled`, and take
    your result from that event.
  </Step>
</Steps>

## Disconnecting

A client that vanishes mid-stream does **not** leave a run burning tokens
forever, and is **not** billed differently.

The execution is told to stop and settles once, like any other run: it still
closes its run row, still settles its usage, and notices the cancellation at its
next checkpoint. The worker is never killed — a design that killed it on
disconnect would be the one that orphans spend, because the half-finished
execution would never write its ledger line.

Usage is metered exactly once per execution, in the execution itself. Streaming
adds no settlement path at all. A disconnect does not settle, a cancel does not
settle, and a timeout does not settle — the one execution that already owns the
ledger write does it, exactly as it does for a non-streaming request.

### Reconnecting

There is no resume. Events carry an `id:` that numbers them within one stream,
but a reconnect starts a new stream and `Last-Event-ID` is not honoured.

If you lost the connection and need the outcome, poll the run instead:

```bash theme={null}
curl "$IMPEL_API/logs/run_…" -H "Authorization: Bearer $IMPEL_KEY"
```

That needs `logs:read`. The run row is the authoritative record of what happened,
whatever the stream managed to deliver.

## Timeouts

The stream stops waiting shortly after your own `timeout_ms` — the caller's
deadline plus about ten seconds of slack — and then gives the execution a few
seconds to notice. If it does not, the stream ends with `response.failed` rather
than reporting a completion nobody has.

The run row still settles. A daemon thread that outlives its stream still closes
its own row, and that row stays the authoritative record.

## Reading it

```bash theme={null}
curl -N "$IMPEL_API/responses" \
  -H "Authorization: Bearer $IMPEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent": "agt_support", "input": "…", "stream": true}'
```

```python theme={null}
import json, httpx

with httpx.stream("POST", f"{API}/responses", headers=HEADERS,
                  json={"agent": AGENT, "input": text, "stream": True}) as r:
    if r.headers.get("content-type", "").split(";")[0] != "text/event-stream":
        raise RuntimeError(r.read().decode())   # an ordinary JSON error

    event, data = "", ""
    for line in r.iter_lines():
        if line.startswith(":"):                 # a heartbeat, not an event
            continue
        if line.startswith("event: "):
            event = line[7:]
        elif line.startswith("data: "):
            data = line[6:]
        elif line == "":
            if event in ("response.completed", "response.failed",
                         "response.cancelled"):
                return json.loads(data)          # the authoritative result
            if event == "response.output_text.delta":
                render(json.loads(data)["delta"])
            event, data = "", ""
```

<Note>
  `-N` on curl, and no buffering proxy in front of your client. A reverse proxy
  that buffers the response body will hold every event until the run finishes,
  which looks exactly like streaming being broken.
</Note>
