---
title: Streaming
description: Consume eve client stream events live, reconnect by event index, and aggregate turn results.
---

# Streaming



Every `ClientSession.send()` call posts the turn, then reads the session's NDJSON (newline-delimited JSON) event stream. `MessageResponse` gives you two ways to consume that stream, aggregating it with `result()` or iterating it live.

Once `send()` is accepted, the session exposes its assigned `sessionId` and can request cooperative cancellation with `session.cancel()`, even while the response stream is still running. The result status is `accepted` when the active turn accepted the signal or `no_active_turn` when it already settled:

```ts
const session = client.session();
const response = await session.send("Run the long operation.");

const cancellation = await session.cancel();
console.log(cancellation.status);

const result = await response.result();
```

Cancellation does not replace stream consumption. Continue reading the response to observe its terminal `turn.cancelled` and `session.waiting` boundary and to advance the client session cursor normally.

## Aggregate a turn

Use `result()` when you only need the final turn summary:

```ts
const response = await session.send("Summarize the latest forecast.");
const result = await response.result();

console.log(result.status);
console.log(result.message);
console.log(result.events.length);
```

This consumes the stream until the current turn boundary:

* `session.waiting`
* `session.completed`
* `session.failed`

## Stream events live

Use `for await...of` when you want to render progress:

```ts
const response = await session.send("Draft a plan and show your work.");

for await (const event of response) {
  if (event.type === "message.appended") {
    process.stdout.write(event.data.messageDelta);
  }

  if (event.type === "message.completed" && event.data.finishReason !== "tool-calls") {
    console.log("\nfinal:", event.data.message);
  }
}
```

`message.appended` and `reasoning.appended` are incremental delta events. eve may combine adjacent deltas of the same type while a durable stream write is in flight, but preserves their text and event ordering; any different event is a barrier. Their completed forms, `message.completed` and `reasoning.completed`, are the compatibility path for clients that don't render deltas.

## Handle event types

Import event types from `eve/client` when you want exhaustiveness or helpers. Events read from a stream are `MessageStreamEvent`: the same union, with the `meta` envelope guaranteed present.

`HandleMessageStreamEvent` remains available as a deprecated alias, so existing type imports continue to compile.

```ts
import type { MessageStreamEvent } from "eve/client";
import { isCurrentTurnBoundaryEvent } from "eve/client";

function handleEvent(event: MessageStreamEvent) {
  console.log(event.meta.id, event.meta.at);

  if (isCurrentTurnBoundaryEvent(event)) {
    console.log("turn settled:", event.type);
  }
}
```

The most common UI events are:

| Event                | Use                                                                            |
| -------------------- | ------------------------------------------------------------------------------ |
| `message.received`   | Confirm the user message landed; `data.parts` includes text and file metadata. |
| `reasoning.appended` | Render reasoning deltas when the model provides them.                          |
| `message.appended`   | Render assistant text deltas.                                                  |
| `actions.requested`  | Show tool calls as the model requests them, before execution.                  |
| `action.result`      | Show tool call results.                                                        |
| `input.requested`    | Pause the UI for approval or a question answer.                                |
| `result.completed`   | Read structured output from an [output schema](./output-schema).               |
| `session.waiting`    | Enable the composer and read the current `data.continuationToken`.             |
| `session.completed`  | Mark the conversation terminal.                                                |
| `session.failed`     | Mark the conversation failed.                                                  |

For the complete event table, see [Sessions, runs & streaming](../../concepts/sessions-runs-and-streaming).

When a submitted message includes attachments, `message.received.data.message` stays the
flattened compatibility summary, while `message.received.data.parts` carries renderable text and
file metadata. File parts never include raw bytes or internal sandbox paths; `url` appears only for
client-resolvable `http(s)` and `data:` URLs.

## Authorization pauses

`authorization.required` is different from the normal `session.waiting` boundary. It means a connection needs OAuth or another authorization challenge before the parked turn can continue. Chat UIs should render the authorization prompt, disable ordinary text input for that session, and persist the event with the rest of the chat history.

If you support refresh while an authorization prompt is pending, keep the session cursor from the started session and rehydrate the saved events on load. Do not treat a missing `session.waiting` after `authorization.required` as a finished conversation; the callback or a structured decline should resume the same eve session.

## Reconnection

HTTP connections can end before a run does. The client reconnects from the number of events already consumed, so long turns continue without replaying events. It stops at a turn boundary, when aborted, or when the stream can no longer make progress.

If your consumer persists events, key on `event.meta.id`. It is stable across reconnects and rewinds, so an overlapping replay is safe to ingest twice. See [the event envelope](../../concepts/sessions-runs-and-streaming#the-event-envelope).

Set `streamReconnectPolicy: { reconnect: false }` when a relay or proxy owns the cursor and reconnection policy. This makes a single stream GET attempt and returns when that connection ends; it does not stop the server-side turn:

```ts
const response = await session.send({
  message: "Run the long operation.",
  streamReconnectPolicy: { reconnect: false },
});

for await (const event of response) {
  console.log(event.type);
}
```

The same option is available on manual attachments as `session.stream({ streamReconnectPolicy: { reconnect: false } })`.

## Open a stream manually

Use `session.stream()` when you already have a session cursor and only need to attach to the existing stream:

```ts
const session = client.session({
  continuationToken: "eve:6c8b1f2e-3d4a-4b9c-8e21-9f0a1b2c3d4e",
  sessionId: "wrun_01ARYZ6S41TSV4RRFFQ69G5FAV",
  streamIndex: 10,
});

for await (const event of session.stream()) {
  console.log(event.type);
}
```

Pass `startIndex` to override the stored cursor:

```ts
for await (const event of session.stream({ startIndex: 0 })) {
  console.log(event.type);
}
```

Nonnegative values are absolute event indexes. Negative values read relative to the stream's current tail, so `-1` reads the latest event:

```ts
for await (const event of session.stream({ startIndex: -1 })) {
  if (event.type === "session.waiting") {
    console.log(event.data.continuationToken);
  }
  break;
}
```

Tail-relative attachments do not automatically reconnect or advance the session's stored absolute `streamIndex`. Break after the event you need when using one as a tail lookup.

## Bounded catch-up reads

Pass `follow: false` to read from the cursor to the durable tail and then stop, instead of following the live stream (the default):

```ts
for await (const event of session.stream({ follow: false })) {
  console.log(event.type);
}
// Returns once every event recorded before the stream opened is consumed.
```

The first connection pins the bound to the tail the server reports at open time; events recorded afterward are not part of the read. Reconnects during the read keep that original bound, and the session's stored `streamIndex` still advances past the consumed events, so a follow-up `stream()` or `send()` picks up exactly where the bounded read ended. When the cursor is already at or past the tail, the iterator returns immediately without yielding.

Because a tail-relative cursor cannot be bounded, `follow: false` throws when combined with a negative `startIndex`. It also fails if the server does not report the durable tail (an agent running an older eve version).

`stream()` throws if the session has no `sessionId`, because there's no stream to attach to before the first send.

## Snapshot a session

Use `snapshot()` when you need the complete event prefix and its matching cursor
as one value, such as when hydrating a server-rendered chat:

```ts
const session = client.session({ sessionId, streamIndex: 0 });
const snapshot = await session.snapshot();

// snapshot.events contains event indexes 0 through snapshot.session.streamIndex - 1.
```

The read pins the durable tail when it opens, just like
`stream({ follow: false })`. Events written afterward are not included, and
`snapshot.session.streamIndex` is the exact index from which a live consumer can
continue. `snapshot()` always reads from index `0`; use a bounded stream directly
when you only need the unread suffix from an existing cursor.

Unlike `stream()`, `snapshot()` does not advance or reset the originating
`ClientSession`. Its returned session includes a continuation token only when
the captured prefix ends in `session.waiting`, or when completed sessions are
explicitly preserved by the client. Active and terminal snapshots remain
inspectable at their returned stream cursor but cannot accept another turn.

## Abort a request

Pass an `AbortSignal` to cancel the POST or stream. Aborting is local transport cancellation: turns are resumable across disconnects, so detaching never stops the server-side turn — it keeps running and remains attachable by index. To stop the turn itself, POST the session's [cancel route](../../channels/eve#routes) and watch the stream settle with `turn.cancelled` followed by `session.waiting`.

Arm the timeout before awaiting `send()` so it covers the POST as well as the stream:

```ts
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);

const response = await session.send({
  message: "Run a long analysis.",
  signal: controller.signal,
});

for await (const event of response) {
  console.log(event.type);
}

clearTimeout(timeout);
```

Once a response is aborted, create a new send for the next turn. Don't reuse the same `MessageResponse`.

## What to read next

* [Messages](./messages): the send APIs that create streams
* [Continuations](./continuations): how stream cursors are persisted
* [Output schema](./output-schema): consume `result.completed`


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)