# Sessions and streaming

> continuationToken vs sessionId contracts, POST /eve/v1/session and follow-up routes, NDJSON stream events, reconnect behavior, and subagent child-session attachment.

- Repository: vercel/eve
- GitHub: https://github.com/vercel/eve
- Human docs: https://www.grok-wiki.com/public/docs/vercel-eve-759e1d74a10f
- Complete Markdown: https://www.grok-wiki.com/public/docs/vercel-eve-759e1d74a10f/llms-full.txt

## Source Files

- `docs/concepts/sessions-runs-and-streaming.md`
- `packages/eve/src/protocol/routes.ts`
- `packages/eve/src/client/open-stream.ts`
- `packages/eve/src/client/ndjson.ts`
- `packages/eve/src/client/session.ts`
- `packages/eve/src/internal/nitro/routes/index.ts`

---

---
title: "Sessions and streaming"
description: "continuationToken vs sessionId contracts, POST /eve/v1/session and follow-up routes, NDJSON stream events, reconnect behavior, and subagent child-session attachment."
---

Eve exposes session lifecycle over stable `/eve/v1` HTTP routes registered by `eveChannel`. A POST acknowledges work immediately and returns handles; a GET on `/stream` replays durable NDJSON events from the workflow-backed session run. The `continuationToken` resumes parked work; `sessionId` keys the event stream.

## Two handles

| Handle | Role | Used for |
| --- | --- | --- |
| `continuationToken` | Resume handle for the session's current workflow hook | POST follow-ups (`message`, `inputResponses`) while the session is parked or active |
| `sessionId` | Stream-and-inspect handle (workflow run id) | `GET /eve/v1/session/:sessionId/stream`, `x-eve-session-id` response header |

<Warning>
Do not interchange these handles. `continuationToken` is owned by the channel and namespaced internally as `<channelName>:<rawToken>` (for example `eve:<uuid>` on the default HTTP channel). `sessionId` is runtime-owned and stable for the durable run.
</Warning>

A session has one active continuation at a time. Channels may re-key the token mid-session (for example Slack anchoring a thread `ts` via `ctx.session.setContinuationToken`). Deliveries to a superseded token after re-key are silently dropped. A stale token with no matching parked hook yields `RuntimeNoActiveSessionError`; the HTTP `send` path may fall back to starting a new session for plain messages, but rejects `inputResponses` when the target session cannot be found.

## Session routes

The default `eveChannel` registers three routes under `/eve/v1`. All run the channel's `auth` chain via `routeAuth` before dispatch.

| Method | Path | Status | Purpose |
| --- | --- | --- | --- |
| `POST` | `/eve/v1/session` | `202` | Create a session and start the first turn |
| `POST` | `/eve/v1/session/:sessionId` | `200` | Continue a session with `continuationToken` |
| `GET` | `/eve/v1/session/:sessionId/stream` | `200` | Stream NDJSON lifecycle events |

```mermaid
sequenceDiagram
  participant Client
  participant EveChannel as eveChannel routes
  participant Runtime as Workflow runtime
  participant Stream as Durable event stream

  Client->>EveChannel: POST /eve/v1/session { message }
  EveChannel->>Runtime: runtime.run (new session)
  Runtime-->>EveChannel: sessionId, continuationToken
  EveChannel-->>Client: 202 JSON + x-eve-session-id

  Client->>EveChannel: GET /eve/v1/session/:sessionId/stream
  EveChannel->>Stream: getEventStream(sessionId)
  Stream-->>Client: application/x-ndjson events

  Note over Client,Runtime: Turn completes → session.waiting
  Client->>EveChannel: POST /eve/v1/session/:sessionId { continuationToken, message }
  EveChannel->>Runtime: runtime.deliver
  Runtime-->>Client: 200 { sessionId }
```

:::endpoint POST /eve/v1/session
Create a session and enqueue the first user turn. Returns immediately; progress arrives on the stream route.

<ParamField body="message" type="string | UserContent" required>
Plain text or an AI SDK `UserContent` array (`text` and `file` parts). Required on create.
</ParamField>

<ParamField body="clientContext" type="string | string[] | object">
One-turn client/page context. Strings become user-role context messages prefixed with `Client context:\n`. Not persisted to durable history.
</ParamField>

<ParamField body="outputSchema" type="object">
JSON Schema the turn result must satisfy before the turn terminates.
</ParamField>

<ParamField body="mode" type="'conversation' | 'task'">
Run mode. `conversation` enables recoverable input requests; `task` fails when input would be required.
</ParamField>

<ParamField body="callback" type="object">
Optional terminal callback metadata. The runtime posts once when the session completes or fails.
</ParamField>

<ResponseField name="continuationToken" type="string">
Channel-local resume token (raw form, without the `eve:` prefix in the JSON body).
</ResponseField>

<ResponseField name="sessionId" type="string">
Durable session id for streaming.
</ResponseField>

<ResponseField name="x-eve-session-id" type="header">
Same value as `sessionId`. Present on create and continue responses.
</ResponseField>

<RequestExample>

```bash
curl -X POST http://127.0.0.1:3000/eve/v1/session \
  -H 'content-type: application/json' \
  -d '{"message":"Summarize the latest forecast."}'
```

</RequestExample>

<ResponseExample>

```json
{
  "continuationToken": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "ok": true,
  "sessionId": "wrun_abc123"
}
```

</ResponseExample>
:::

:::endpoint POST /eve/v1/session/:sessionId
Deliver a follow-up to a parked or active session. Requires a valid `continuationToken` from the current session state.

<ParamField path="sessionId" type="string" required>
Existing session id. Must resolve via `getSession`; otherwise `404`.
</ParamField>

<ParamField body="continuationToken" type="string" required>
Current resume token. Missing or empty → `400`.
</ParamField>

<ParamField body="message" type="string | UserContent">
Follow-up user message. At least one of `message` or `inputResponses` is required.
</ParamField>

<ParamField body="inputResponses" type="InputResponse[]">
HITL responses resolving pending `input.requested` batches (tool approvals, `ask_question` answers).
</ParamField>

<ParamField body="clientContext" type="string | string[] | object">
Same semantics as create; applies to this delivery only.
</ParamField>

<ParamField body="outputSchema" type="object">
Per-turn structured output schema override.
</ParamField>

<RequestExample>

```bash
curl -X POST http://127.0.0.1:3000/eve/v1/session/wrun_abc123 \
  -H 'content-type: application/json' \
  -d '{"continuationToken":"f47ac10b-58cc-4372-a567-0e02b2c3d479","message":"Now send the short version."}'
```

</RequestExample>

<ResponseExample>

```json
{
  "ok": true,
  "sessionId": "wrun_abc123"
}
```

</ResponseExample>

<Note>
Continue responses do not rotate `continuationToken` in the JSON body. Keep the token from create (or from channel re-keying via stream-side state) until the channel issues a new one.
</Note>
:::

:::endpoint GET /eve/v1/session/:sessionId/stream
Replay or tail the durable NDJSON event stream for one session.

<ParamField path="sessionId" type="string" required>
Session to stream. Unknown id → `404`.
</ParamField>

<ParamField query="startIndex" type="integer">
Zero-based event index to start from. Omit to replay from the beginning; set to the number of events already consumed to reconnect without duplication. Negative or non-integer → `400`.
</ParamField>

<ResponseField name="content-type" type="header">
`application/x-ndjson; charset=utf-8`
</ResponseField>

<ResponseField name="x-eve-stream-format" type="header">
`ndjson`
</ResponseField>

<ResponseField name="x-eve-stream-version" type="header">
`15` (current stream schema version)
</ResponseField>

<ResponseField name="x-eve-session-id" type="header">
Echoes the requested `sessionId`.
</ResponseField>

<RequestExample>

```bash
curl "http://127.0.0.1:3000/eve/v1/session/wrun_abc123/stream?startIndex=42"
```

</RequestExample>
:::

## NDJSON event stream

Each line is one JSON object: a `HandleMessageStreamEvent` optionally stamped with `meta.at` (ISO timestamp) at write time. The stream is durable — events are recorded before a step completes, so the full history is replayable.

### Turn and session boundaries

Clients treat these as turn/session boundaries:

| Event | Meaning |
| --- | --- |
| `session.waiting` | Session parked; safe to send the next follow-up with the current `continuationToken` |
| `session.completed` | Terminal success |
| `session.failed` | Terminal failure; carries `{ code, message, details? }` |

`ClientSession` and `openStreamIterable` stop a per-turn consumer at the first `session.completed`, `session.failed`, or `session.waiting` event (`isCurrentTurnBoundaryEvent`).

### Lifecycle events

| Event | Meaning |
| --- | --- |
| `session.started` | Durable session created; child runs include `data.invocation` (`kind: "subagent"`, parent ids) |
| `turn.started` | New turn; `data.turnId`, `data.sequence` |
| `message.received` | Inbound user message accepted |
| `step.started` | Model step began |
| `actions.requested` | Tool call batch requested (`data.actions`) |
| `action.result` | Tool result projected (`data.status`: `completed` \| `failed`) |
| `input.requested` | HITL pause; `data.requests` |
| `step.completed` | Model step finished; `data.finishReason`, optional `data.usage` |
| `step.failed` | Step failure; `{ code, message, details? }` |
| `turn.completed` | Turn succeeded |
| `turn.failed` | Turn failure; `{ code, message, details? }` |
| `result.completed` | Structured output for schema turns; `data.result` |
| `compaction.requested` / `compaction.completed` | Context compaction checkpoint |
| `authorization.required` / `authorization.completed` | Connection OAuth challenge and outcome |

### Streaming text and reasoning

| Event | Meaning |
| --- | --- |
| `message.appended` | Assistant text delta; `messageDelta` + cumulative `messageSoFar` |
| `message.completed` | Finalized assistant text block; `data.finishReason` |
| `reasoning.appended` | Reasoning delta; `reasoningDelta` + `reasoningSoFar` |
| `reasoning.completed` | Finalized reasoning block |

`message.completed` may fire multiple times per turn. `finishReason: "tool-calls"` marks interim narration before a tool call; other values mark a terminal assistant message for that step. Pair deltas (`*.appended`) with finalized blocks (`*.completed`) when building UIs.

### Subagent events on the parent stream

| Event | Meaning |
| --- | --- |
| `subagent.called` | Workflow subagent delegated; includes `data.childSessionId` for child stream attachment |
| `subagent.started` | Inline subagent execution began |
| `subagent.event` | Wraps one child stream event under `data.event` (inline path) |
| `subagent.completed` | Inline subagent finished; `data.output` |

## Message delivery constraints

`continuationToken` names the session's workflow hook, not a durable FIFO message queue. When the session is waiting, one delivery wakes the next turn. Concurrent sends while a turn is active are best-effort at workflow boundaries and may be folded together.

<Tip>
For deterministic ordering, send one user turn at a time and wait for `session.waiting` before the next POST to the same session. Burst-prone channels should queue locally and deliver after each park.
</Tip>

HITL deliveries (`inputResponses` without a new `message`) retry up to 10 times on `500` responses whose body matches `target session was not found` — covering the race where a client posts before the park hook is registered.

## Reconnect and rewind

**Server replay:** Pass `?startIndex=<count>` where `count` is the number of events already consumed. The runtime calls `getRun(sessionId).getReadable({ startIndex })` and drops earlier events.

**Client reconnection:** `eve/client` tracks `streamIndex` in `SessionState`. `openStreamIterable` reconnects on transient socket disconnects (matching `isStreamDisconnectError`), incrementing `startIndex` as events arrive, up to `maxReconnectAttempts` (default `3`). Clean EOF and caller aborts do not reconnect.

**Stream open retries:** `openStreamBody` retries up to 12 times (250 ms delay) on status `404`, `409`, `425`, `500`, `502`, `503`, `504` while a just-created session propagates to the stream route.

After a turn, `advanceSession` updates `continuationToken` and `streamIndex` on `session.waiting`; on `session.completed` or `session.failed` it resets client state unless `preserveCompletedSessions: true`.

## Subagent child-session attachment

Workflow subagents run as independent durable child sessions. The parent stream emits `subagent.called` with:

- `data.childSessionId` — attach here
- `data.callId`, `data.toolName`, `data.name`
- `data.sessionId`, `data.turnId`, `data.workflowId`
- `data.remote.url` when delegating to a `defineRemoteAgent` target

Child progress is **not** mirrored on the parent stream (except the inline `subagent.event` path). Open a second stream:

```bash
curl "http://127.0.0.1:3000/eve/v1/session/<childSessionId>/stream"
```

Child `session.started` events carry `data.invocation` with `kind: "subagent"` and parent lineage (`parentSessionId`, `parentTurnId`, `parentCallId`, `name`).

When a child needs HITL, `SUBAGENT_ADAPTER` forwards `input.requested` batches to the parent continuation token via `resumeHook`. The parent channel renders prompts; responses route back down using the child's continuation token recorded on the parent session.

## TypeScript client

`eve/client` wraps the HTTP contract:

```typescript
import { Client } from "eve/client";

const client = new Client({ host: "http://127.0.0.1:3000" });
const session = client.session();

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

for await (const event of response) {
  if (event.type === "subagent.called") {
    const child = client.session({ sessionId: event.data.childSessionId });
    for await (const childEvent of child.stream()) {
      // render child progress
    }
  }
}

const result = await response.result();
// result.status: "waiting" | "completed" | "failed"
// result.message: terminal assistant text
// result.inputRequests: pending HITL from this turn
```

`ClientSession.send` picks `POST /eve/v1/session` or `POST /eve/v1/session/:sessionId` from stored `sessionId`. Serialize `session.state` (`continuationToken`, `sessionId`, `streamIndex`) to resume later.

## Stream event dispatch order

Inside the harness step loop, each emitted event runs:

1. Channel adapter handler (`adapter[event.type]`) — side effects only; event is not transformed
2. Adapter state persisted to context
3. Event written to the durable stream (with `meta.at`)
4. Authored hooks (`dispatchStreamEventHooks`)
5. Dynamic tool, skill, and instruction resolvers

Channel metadata projected from adapter state is current before hooks and resolvers read `ctx.channel`.

## Related pages

<CardGroup>
<Card title="Execution model and durability" href="/execution-model">
Session/turn/step nesting, parked work, and continuationToken delivery constraints.
</Card>
<Card title="HTTP API reference" href="/http-api">
Full route inventory, error shapes, and NDJSON vocabulary.
</Card>
<Card title="Client integration" href="/client-integration">
`Client`, `ClientSession`, streaming reducers, and framework hooks.
</Card>
<Card title="Subagents and schedules" href="/subagents-and-schedules">
Local subagents, remote agents, and delegation boundaries.
</Card>
</CardGroup>
