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

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:

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

function handleEvent(event: HandleMessageStreamEvent) {
  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.

## 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.

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

## 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`
