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

## 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. 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.                                 |
| `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 for the next turn.                           |
| `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).

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

The client reconnects after transient stream disconnects. It resumes from the number of events already consumed in the current session:

```ts
const client = new Client({
  host: "https://agent.example.com",
  maxReconnectAttempts: 5,
});
```

`maxReconnectAttempts` is per turn. The default is `3`.

## 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);
}
```

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