---
title: Streaming
description: Consume eve client stream events live, reconnect by line position, and aggregate turn results.
url: "https://eve.dev/docs/guides/client/streaming"
docs_index: /llms.txt
---

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

A `Delivery` is the receipt `ClientSession.send()` returns once the server accepts a message. It carries a `sessionId` and `deliveryId` and doesn't wait for a reply. Call `result()` for its summary or `stream()` to watch it live. Each read is independent, and you can repeat either one. Import `Delivery` and its read types from `eve/sessions`.

Session controls return a `Delivery` too. `session.cancel({ turnId })` targets a turn you saw on the stream, so a late cancellation can't stop unrelated later work. Omit `turnId` to cancel the current work. For example, cancel as soon as this message reaches a turn:

```ts
const { session, delivery } = await client.sessions.create({ message: "Run the long operation." });

for await (const event of delivery.stream()) {
  if (
    event.type === "delivery.consumed" &&
    event.data.deliveryId === delivery.deliveryId &&
    event.data.turnId !== undefined
  ) {
    const cancellation = await session.cancel({ turnId: event.data.turnId });
    await cancellation.result();
  }
}
```

The cancellation's `result()` resolves once eve applies the cancel, even as a no-op for a turn that already ended. An idle session that hasn't ended also accepts cancellation instead of returning `no_active_turn`. Keep reading the original message's `Delivery` to see its outcome.

Between turns, `session.compact()` queues context compaction without sending model input. `session.clear()` cancels a running turn and every working task, then removes the model's message history. It keeps the session and its other saved resources. Client `messages` and `transcriptOf()` then leave out everything before the clear. Await their results before you send another turn:

```ts
const compaction = await session.compact();
for await (const event of compaction.stream()) {
  if (event.type === "context.settled") console.log(event.data.outcome);
}
await compaction.result();

const cleared = await session.clear();
await cleared.result();
```

`context.settled` reports whether summarization succeeded; a failed or empty summary keeps the previous history. `session.end()` instead retires the session, and its `result()` resolves after the session is retired.

## Aggregate a turn

Use `result()` when you need only 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.reply);
console.log(result.outcome);
if (result.status === "failed") console.error(result.error.code, result.error.message);
```

`result()` reads until the `delivery.settled` event for the message's `deliveryId`. If the stream
ends first, the read rejects, even when the session has ended. That event arrives when the turn
finishes (`handled`) or pauses for an approval, question, or sign-in (`awaiting-input`). See
[authorization pauses](#authorization-pauses).

A turn that waits on its [tasks](/docs/tools/workflows#run-calls-as-tasks-task) stays open, and
`result()` keeps reading to its final reply; text written before the wait is narration. When a
call asks a question, such as a workflow tool's `ctx.ask()`, `result()` returns
`status: "awaiting-input"` with the question in `interactions`. Answer it with `respond()`, which
keeps reading the same turn. A question stays open until its `interaction.settled` event. The
session you answer on emits that event, even for a subagent's or workflow tool's question.

`result().status` is `awaiting-input`, `completed`, `failed`, or `cancelled`. A completed
request can leave its session open, so read `result().sessionEnded` to check whether the session
ended. Read `outcome` to tell an applied control from an ignored one. To inspect live progress, use
`delivery(view, deliveryId)` and, when it has a `turnId`, `turn(view, turnId)` from `eve/events`.

Results don't collect an `events` array. To keep a request log, append events from
`response.stream()` to your own collection. Reads don't share a cursor, so a later
`response.result()` still reads the whole delivery.

`result()` closes its HTTP stream, even when fetch instrumentation clones the response. The
session stays available for follow-up turns.

## Stream events live

Iterate `stream()` with `for await...of` to render progress:

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

for await (const event of response.stream()) {
  if (event.type === "content.delta") {
    process.stdout.write(event.data.delta);
  }

  if (event.type === "content.completed" && event.data.phase === "reply") {
    console.log("\nreply:", event.data.value);
  }
}
```

`content.delta` and `call.input` carry only their new text. eve may merge adjacent deltas for the same part or call without changing the text or order.

The default message reducer accumulates them for you. A raw consumer appends them by part:

```ts
const parts = new Map<string, string>();

for await (const event of response.stream()) {
  if (event.type !== "content.delta") continue;
  parts.set(event.data.partId, (parts.get(event.data.partId) ?? "") + event.data.delta);
}
```

After reconnecting without local state, don't append a later delta to an empty value. Replay the earlier lines, or wait for the part's `content.completed`, which carries its whole value.

The client checks the stream version on every connection and accepts only version 27; a missing or different version fails the read.

## Handle event types

Import event types from `eve/client`, or the full catalog, the shared fold, and selectors from `eve/events`. Events read from a stream are `SessionStreamEvent`: a recorded event or a progress update, with its `meta` envelope.

```ts
import type { SessionStreamEvent } from "eve/client";
import { endsTurn } from "eve/client";

function handleEvent(event: SessionStreamEvent) {
  console.log(event.meta.position, event.meta.at);

  if (endsTurn(event)) {
    console.log("turn ended or paused on an interaction:", event.type);
  }
}
```

The most common UI events are:

| Event                 | Use                                                                                                                      |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `delivery.consumed`   | Confirm the user message landed; `data.parts` includes text and file metadata, and `data.history` any imported history.  |
| `content.delta`       | Render text and reasoning deltas as they stream.                                                                         |
| `content.completed`   | A finished part. `phase` tells a `"reply"` from `"narration"`; `kind: "result"` is [structured output](./output-schema). |
| `call.input`          | Accumulate raw tool-input deltas before validation completes.                                                            |
| `call.requested`      | Show tool calls as the model requests them, before execution.                                                            |
| `call.progress`       | Update a generator tool's provisional output snapshot.                                                                   |
| `call.settled`        | Show a call's result, error, or rejection.                                                                               |
| `task.started`        | Show a task; `kind: "agent"` marks agents. Its calls carry its `taskId` on `call.started`.                               |
| `interaction.opened`  | Pause the UI for an approval, question, budget prompt, or sign-in.                                                       |
| `interaction.settled` | Record how a request was decided.                                                                                        |
| `turn.settled`        | The turn ended: enable the composer.                                                                                     |
| `session.ended`       | Mark the conversation terminal or failed.                                                                                |

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

During `call.input`, the conversation's `tool` part has `state: "streaming"` and accumulates raw, possibly incomplete JSON in `inputText`. `call.requested` replaces it with the validated `input`.

A call to a tool that runs as a [task](/docs/tools/workflows#run-calls-as-tasks-task) starts with a `call.started` that carries its `taskId`. It ends later with its own `call.settled`, and the model receives that result in its history.

File parts in `delivery.consumed.data.parts` never include raw bytes or internal sandbox paths. They include `url` only for `http(s)` URLs; an inline file reports its `size` instead.

## Authorization pauses

A sign-in is an `interaction.opened` whose `request.kind` is `"sign-in"`: a connection needs OAuth or another authorization challenge before the turn can continue. Render the prompt from `request.signIn`, disable ordinary text input for that session, and persist the event with the chat history.

While the sign-in is pending, the stream emits `turn.paused`, and the current message's result returns `status: "awaiting-input"`. Completing the sign-in, or declining it, resumes the same session. Follow it with `session.stream()` to see the resumed work. To keep the prompt across a page refresh, save the session cursor and events, and restore them on load.

## Reconnection

When an HTTP connection ends before the run does, the client reconnects from the next unread line without replaying events. Omit a policy to keep reconnecting until the message's work finishes or you abort the read. Use a session stream to follow work past that point. A stream ends for good on `{"$eve":"stream.ended"}`, which the server sends once the session has ended.

Browser network failures also reconnect, both when opening a stream (`TypeError: Failed to fetch`, `TypeError: Load failed`, or `TypeError: NetworkError when attempting to fetch resource.`) and while reading the body (any `TypeError`, such as `TypeError: network error`). Invalid stream events still fail the read.

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

Set `reconnect: false` when a relay or proxy owns the cursor and reconnection. The client makes one stream GET and returns when that connection ends, without stopping the server-side turn:

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

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

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

To tune connection-open retries, pass a `ReconnectOptions` object from `eve/client`:

```ts
const response = await session.send("Run the long operation.", {
  reconnect: {
    maxAttempts: 6,
    baseDelayMs: 250,
    maxDelayMs: 5_000,
    retryableStatuses: [404, 409, 425, 500, 502, 503, 504],
  },
});
```

`maxAttempts` defaults to `12` for each connection open. It also bounds consecutive transport failures during a bounded read (`follow: false`). Following reads continue reconnecting until stopped. `baseDelayMs` and `maxDelayMs` tune retries when opening a connection; reconnects after an interrupted response body retain their default backoff.

`retryableStatuses` replaces the default list (`404`, `409`, `425`, `500`, `502`, `503`, `504`) rather than extending it, so include any defaults you still want retried.

## Open a stream manually

Use `session.stream()` to attach to an existing stream from a saved cursor:

```ts
const session = client.sessions.attach("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 line positions. Negative values read relative to the stream's current tail, so `-1` reads the latest line:

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

Tail-relative reads don't reconnect or advance the session's stored `streamIndex`. Break after the event you need.

## Bounded catch-up reads

Pass `follow: false` to read from the cursor to the stream's current end 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 line recorded before the stream opened is consumed.
```

The server reports that end when the first connection opens, and reconnects keep it. The stored `streamIndex` still advances, so a later `stream()` or `send()` picks up where the bounded read ended. When the cursor is already at or past that end, the iterator returns without yielding.

`follow: false` throws with a negative `startIndex`, because a tail-relative cursor can't be bounded. It also fails when the server doesn't report the stream's end, as an agent on an older eve version doesn't.

## 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.sessions.attach(sessionId);
const snapshot = await session.snapshot();

// snapshot.events holds lines 0 through snapshot.session.streamIndex - 1.
```

Like `stream({ follow: false })`, the read fixes the stream's end when it opens,
and `snapshot.session.streamIndex` is the line a live reader continues from.
`snapshot()` always reads from line `0` and doesn't advance the originating
`ClientSession`; use a bounded stream when you need only the unread suffix.
The returned `session` is the handle's full `ClientSessionState`, including the
opaque `inbox` value. Save it as is to restore the conversation later.

## Follow a subagent

A delegated subagent reports progress on its own child session. The parent stream announces each one with `child.opened`, whether a subagent tool or a [workflow tool](/docs/tools/workflows#delegate-work-ctxagent)'s `ctx.agent` opened it. Pass the event to `agent()` on the parent's handle to get the child's handle, then follow it with `stream()`:

```ts
import { endsTurn } from "eve/client";

const session = client.sessions.attach(parentSessionId);
const { events } = await session.snapshot();
const opened = events.find((event) => event.type === "child.opened");

if (opened?.type === "child.opened") {
  const child = session.agent(opened);
  for await (const event of child.stream()) {
    console.log(child.name, event.type);
    if (endsTurn(event)) break;
  }
}
```

Pass the event as you received it, including its position, because the parent's server checks that line before it returns child history. Cached events and their `stream` strings grant no access to another session or destination.

- Local children are read only after verifying the recorded relationship on the same deployment.
- Remote children use the parent deployment's proxy and the remote agent's authored `auth` and `headers`. The browser never needs the remote URL or its credentials. Foreign redirects are not followed.

To read a deeper child, pass its `child.opened` event to `child.agent(opened)`. Each hop verifies its own relationship, including through remote proxies, and fails instead of attaching by ID when the relationship is unsupported or unavailable. Paths are limited to eight hops.

The child has its own cursor, starting at `0`, and reading it never advances the parent's `streamIndex`. Pass `startIndex` to resume, `follow: false` for a bounded read, and `signal` to detach. A child waits for more input after it answers. To read only the current turn, stop at the turn's end with `endsTurn`. A remote child on an earlier agent protocol still returns its result, but its descendant reader reports unsupported history.

## Observe nested traces without a framework

Use `SessionStore` when you want eve to track nested conversations for you instead of managing readers yourself:

```ts
import { SessionStore } from "eve/client";

const store = new SessionStore({
  baseUrl: "https://agent.example.com",
  initialSession: { sessionId, streamIndex: 0 },
  followAgents: true,
  followWhile: () => false,
});
const stop = store.observe();
const unsubscribe = store.subscribe(() => {
  for (const child of Object.values(store.snapshot.agents)) {
    if (child.observation.status === "not-followed") continue;
    console.log(child.observation.key, child.observation.position, child.observation.caughtUp);
  }
});

try {
  await store.resume(); // Root catch-up only; descendants progress through subscriptions.
  await new Promise<void>((resolve) => setTimeout(resolve, 5_000)); // Bounded capture window.
} finally {
  unsubscribe();
  stop(); // Abort observation, not accepted execution.
}
```

Creating a store doesn't start any reads. `observe()` starts observation and returns a cleanup function that is safe to call more than once. Observation is reference-counted, so it stops after every caller cleans up. Streaming the root also starts following children, and framework bindings start observation on mount. While observation is active, admitted children keep refreshing even after `followWhile` closes the root stream. No call waits for the whole tree to catch up.

`followAgents: true` recursively follows all eligible recorded children within fixed depth, admission, and request limits. A predicate such as `({ depth }) => depth === 1` selects only immediate children. See [child agents](../frontend/use-eve-agent#child-agents) for limits, states, and cursor semantics. Each child conversation shows the child's whole session. Use the parent's call results, not the child's messages, as the outcome of a call.

When the signed-in user or tenant changes, stop the old observation and create a new store. Token callbacks run again for each request without changing the observation key. For a cross-origin browser connection, expose `x-eve-observation-key` in the channel's CORS configuration alongside the stream version and tail headers.

To archive nested conversations, save their messages and each child's position in your own versioned format. `SessionView` is opaque and isn't an archive format. Children can advance while the root position stays the same. A failed observation keeps its last recorded data, so don't replace an existing archive with an empty failed read.

## Abort a request

Pass an `AbortSignal` to `send()` to abort submission, and to `stream()` or `result()` to abort a read. Aborting only detaches the client and never stops server-side work. To stop the active turn, call `session.cancel()` and watch for `turn.settled` with `outcome: "cancelled"`.

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("Run a long analysis.", {
  signal: controller.signal,
});

for await (const event of response.stream({ signal: controller.signal })) {
  console.log(event.type);
}

clearTimeout(timeout);
```

An interrupted read doesn't consume the `Delivery`, and other concurrent readers keep running.
Call `response.stream()` or `response.result()` again without the aborted signal instead of
sending the message again.

## What to read next

- [Messages](./messages): the send APIs that create streams
- [Continuations](./continuations): how stream cursors are persisted
- [Output schema](./output-schema): read a `content.completed` with `kind: "result"`

---

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)