# Eve sessions and streaming

> Start sessions, stream NDJSON events, send follow-ups with `continuationToken`, and handle HITL, subagent, and compaction events.

- Repository: withastro/flue-with-vercel-eve
- GitHub: https://github.com/withastro/flue
- Human docs: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6
- Complete Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/llms-full.txt

## Source Files

- `vercel-eve:docs/concepts/sessions-runs-and-streaming.md`
- `vercel-eve:docs/concepts/execution-model-and-durability.md`
- `vercel-eve:docs/concepts/default-harness.md`
- `vercel-eve:docs/guides/session-context.md`
- `vercel-eve:docs/guides/auth-and-route-protection.md`
- `vercel-eve:packages/eve/src/public/index.ts`

---

---
title: "Eve sessions and streaming"
description: "Start sessions, stream NDJSON events, send follow-ups with `continuationToken`, and handle HITL, subagent, and compaction events."
---

Eve exposes a stable HTTP API at `/eve/v1` through the default `eveChannel` in `agent/channels/eve.ts`. A `POST` creates or continues a durable session; a `GET` on `/stream` returns a replayable NDJSON event feed. Clients hold two distinct handles — `continuationToken` for follow-up delivery and `sessionId` for stream attachment — and must not interchange them.

## Session handles

| Handle | Role | Owner |
| --- | --- | --- |
| `continuationToken` | Resume handle for the next user turn or HITL response | Channel |
| `sessionId` | Stream-and-inspect handle for event history | Runtime |

A session has one active continuation at a time. Each follow-up must use the current `continuationToken`; a stale token is rejected. For deterministic ordering, send one user turn at a time and wait for `session.waiting` before posting the next message to the same session.

<Note>
The TypeScript client (`eve/client`) tracks `continuationToken`, `sessionId`, and `streamIndex` as a `SessionState` cursor. Serialize `session.state` after each turn to persist and resume later.
</Note>

## HTTP routes

All three session routes run the channel's ordered `auth` walk before dispatch. `GET /eve/v1/health` is always public.

| Method | Path | Purpose | Success status |
| --- | --- | --- | --- |
| `POST` | `/eve/v1/session` | Create a new durable session | `202` |
| `POST` | `/eve/v1/session/:sessionId` | Continue with `continuationToken` | `200` |
| `GET` | `/eve/v1/session/:sessionId/stream` | Stream NDJSON events | `200` |

:::endpoint POST /eve/v1/session
Create a durable session and start the first turn.
:::

<ParamField body="message" type="string | UserContent" required>
User message. A string or an array of `text` and `file` parts (base64, data URL, or URL).
</ParamField>

<ParamField body="mode" type="'conversation' | 'task'">
Optional run mode for the first turn.
</ParamField>

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

<ParamField body="clientContext" type="string | string[] | object">
One-turn ephemeral context prepended as user-role messages. Not persisted to durable history.
</ParamField>

<ParamField body="callback" type="object">
Optional terminal session callback configuration.
</ParamField>

<ResponseField name="continuationToken" type="string">
Resume handle for the next delivery to this session.
</ResponseField>

<ResponseField name="sessionId" type="string">
Stream-and-inspect handle. Also returned in the `x-eve-session-id` response header.
</ResponseField>

<RequestExample>

```bash title="Create session"
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 title="202 response"
{
  "ok": true,
  "sessionId": "wrun_01ARYZ6S41TSV4RRFFQ69G5FAV",
  "continuationToken": "eve:6c8b1f2e-3d4a-4b9c-8e21-9f0a1b2c3d4e"
}
```

</ResponseExample>

:::

:::endpoint POST /eve/v1/session/:sessionId
Deliver a follow-up message or HITL response to a waiting session.
:::

<ParamField body="continuationToken" type="string" required>
Current resume handle from the previous turn.
</ParamField>

<ParamField body="message" type="string | UserContent">
Optional follow-up user message.
</ParamField>

<ParamField body="inputResponses" type="InputResponse[]">
HITL responses resolving pending `input.requested` events. Each entry requires `requestId` and either `optionId` or `text`.
</ParamField>

<ParamField body="clientContext" type="string | string[] | object">
One-turn ephemeral context for the resumed turn.
</ParamField>

<ParamField body="outputSchema" type="object">
Optional structured-output schema for this turn.
</ParamField>

At least one of `message` or `inputResponses` must be non-empty. Both may be sent together when a resumed turn needs a human answer and follow-up text.

<RequestExample>

```bash title="Follow-up message"
curl -X POST http://127.0.0.1:3000/eve/v1/session/wrun_01ARYZ6S41TSV4RRFFQ69G5FAV \
  -H 'content-type: application/json' \
  -d '{
    "continuationToken": "eve:6c8b1f2e-3d4a-4b9c-8e21-9f0a1b2c3d4e",
    "message": "Now send the short version."
  }'
```

</RequestExample>

:::

:::endpoint GET /eve/v1/session/:sessionId/stream
Attach to the durable NDJSON event stream for one session.
:::

<ParamField query="startIndex" type="integer">
Non-negative event count to skip before replaying. Omit to start from the beginning; pass the number of events already consumed to reconnect without duplicates.
</ParamField>

Response headers:

| Header | Value |
| --- | --- |
| `content-type` | `application/x-ndjson; charset=utf-8` |
| `x-eve-stream-format` | `ndjson` |
| `x-eve-stream-version` | `15` |
| `x-eve-session-id` | Session ID |
| `cache-control` | `no-store, no-transform` |

<RequestExample>

```bash title="Stream from start"
curl http://127.0.0.1:3000/eve/v1/session/wrun_01ARYZ6S41TSV4RRFFQ69G5FAV/stream
```

```bash title="Reconnect after 42 events"
curl "http://127.0.0.1:3000/eve/v1/session/wrun_01ARYZ6S41TSV4RRFFQ69G5FAV/stream?startIndex=42"
```

</RequestExample>

:::

## Stream lifecycle

```mermaid
sequenceDiagram
  participant Client
  participant EveChannel as eveChannel /eve/v1
  participant Runtime as Durable runtime
  participant Stream as Event stream

  Client->>EveChannel: POST /session {message}
  EveChannel->>Runtime: send() + continuationToken
  EveChannel-->>Client: 202 {sessionId, continuationToken}
  Client->>EveChannel: GET /session/:id/stream
  EveChannel->>Stream: getEventStream()
  Stream-->>Client: NDJSON events (one per line)
  Stream-->>Client: session.waiting | session.completed | session.failed
  Client->>EveChannel: POST /session/:id {continuationToken, message}
  EveChannel->>Runtime: resume parked workflow
```

Every event is recorded before a step completes, so the full stream is replayable after disconnects, process restarts, or redeploys. Turn boundaries are `session.waiting`, `session.completed`, and `session.failed`.

## NDJSON event types

Each line is one JSON object with a `type` field. Events carry `data` with `sequence`, `turnId`, and step-scoped fields where applicable.

### Session and turn boundaries

| Event | Meaning |
| --- | --- |
| `session.started` | Durable session created |
| `turn.started` | New turn began |
| `message.received` | Inbound user message accepted |
| `turn.completed` | Turn finished successfully |
| `turn.failed` | Turn failed; carries `{ code, message, details? }` |
| `session.waiting` | Session parked for next input (`data.wait: "next-user-message"`) |
| `session.completed` | Terminal successful end |
| `session.failed` | Terminal session failure; carries `{ code, message, details? }` |

### Model steps and assistant output

| Event | Meaning |
| --- | --- |
| `step.started` | Model step began |
| `step.completed` | Step finished; carries `finishReason` and optional `usage` |
| `step.failed` | Step failed; carries `{ code, message, details? }` |
| `reasoning.appended` | Reasoning delta with `reasoningDelta` and cumulative `reasoningSoFar` |
| `reasoning.completed` | Finalized reasoning block |
| `message.appended` | Assistant text delta with `messageDelta` and cumulative `messageSoFar` |
| `message.completed` | Finalized assistant text; check `data.finishReason` |
| `result.completed` | Structured output when turn requested `outputSchema`; carries `data.result` |

`message.completed` can fire more than once per turn when the model emits interim text before a tool call. Treat `finishReason: "tool-calls"` as non-terminal narration; terminal replies use other finish reasons.

### Tool execution

| Event | Meaning |
| --- | --- |
| `actions.requested` | Model requested tool calls (`data.actions`) |
| `action.result` | Tool call returned with `status` and `result` |

### Human-in-the-loop (HITL)

When a tool requires approval or the harness calls `ask_question`, the turn parks and the stream emits `input.requested` with one or more `InputRequest` objects in `data.requests`.

Each `InputRequest` includes:

| Field | Purpose |
| --- | --- |
| `requestId` | Stable ID echoed in the response |
| `prompt` | Text shown to the user |
| `display` | UX hint: `"confirmation"`, `"select"`, or `"text"` |
| `options` | Selectable choices with `id`, `label`, optional `description` and `style` |
| `allowFreeform` | Whether freeform text is accepted |
| `action` | Underlying tool-call metadata |

Resume the parked turn by posting `inputResponses` to the continue route:

```ts title="HITL approval via eve/client"
const resumed = await session.send({
  inputResponses: pendingRequests.map((request) => ({
    requestId: request.requestId,
    optionId: "approve",
  })),
});
await resumed.result();
```

<Warning>
Answer HITL against the current `continuationToken`. A stale token is rejected, preventing double-resume of the same parked turn.
</Warning>

### Subagent delegation

| Event | Meaning |
| --- | --- |
| `subagent.called` | Child workflow started; carries `childSessionId` for stream attachment |
| `subagent.started` | Inline subagent execution began |
| `subagent.event` | Wraps a child stream event under `data.event` |
| `subagent.completed` | Inline subagent finished with `data.output` |

A delegated subagent publishes progress on its own child-session stream. The parent emits only `subagent.called` with `childSessionId` — attach to `GET /eve/v1/session/<childSessionId>/stream` to observe child progress. Child sessions expose `ctx.session.parent` with parent lineage metadata.

### Context compaction

When conversation history crosses the configured threshold (default `thresholdPercent: 0.9`), the harness summarizes older turns:

| Event | Meaning |
| --- | --- |
| `compaction.requested` | Compaction began; carries `modelId`, `sessionId`, `turnId`, `usageInputTokens` |
| `compaction.completed` | Compaction checkpoint written to durable history |

Tune compaction in `agent/agent.ts` under the `compaction` key. Compaction preserves framework tool state (read-before-write tracking, active todo list) automatically.

### Connection authorization

| Event | Meaning |
| --- | --- |
| `authorization.required` | Connection needs OAuth; carries `name`, `description`, and optional `authorization` challenge (`url`, `userCode`, `expiresAt`, `instructions`) |
| `authorization.completed` | Authorization resolved; `data.outcome` is `"authorized"`, `"declined"`, `"failed"`, or `"timed-out"` |

These events surface when a tool or connection with interactive auth suspends the turn. The runtime resumes after the OAuth callback completes.

## Follow-up delivery contract

Eve does not maintain a durable FIFO message queue per session. The `continuationToken` is a resume handle for the session's current workflow hook, not a general queue address.

<Steps>
<Step title="Start or resume">

`POST /eve/v1/session` for the first message, or `POST /eve/v1/session/:sessionId` with the stored `continuationToken` for follow-ups.

</Step>
<Step title="Stream the turn">

`GET /eve/v1/session/:sessionId/stream` (optionally with `?startIndex=N`). Consume events until a turn boundary arrives.

</Step>
<Step title="Wait for session.waiting">

Enable the composer or queue the next user message only after `session.waiting`. Sessions ending in `session.completed` or `session.failed` cannot accept further user input on the same continuation.

</Step>
<Step title="Send the next turn">

Post the next message with the updated `continuationToken` from the most recent successful delivery.

</Step>
</Steps>

If your channel receives bursts while the agent is working, keep a per-session queue in the channel or app layer and deliver after the session parks again. Separate sessions run independently.

## TypeScript client

`eve/client` wraps the HTTP routes with typed session management, automatic stream reconnection, and cursor advancement.

```ts title="Basic turn"
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.");

// Metadata available immediately after POST
console.log(response.sessionId, response.continuationToken);

const result = await response.result();
console.log(result.status, result.message);
```

### Aggregate vs live streaming

<CodeGroup>

```ts title="Aggregate with result()"
const result = await response.result();
console.log(result.status);       // "waiting" | "completed" | "failed"
console.log(result.message);      // final assistant text
console.log(result.inputRequests); // pending HITL from this turn
```

```ts title="Live iteration"
for await (const event of response) {
  if (event.type === "message.appended") {
    process.stdout.write(event.data.messageDelta);
  }
  if (event.type === "input.requested") {
    // pause UI for approval or question
  }
}
```

</CodeGroup>

`MessageResponse` is single-use — call either `result()` or `for await`, not both on the same response.

### Reconnection and cursor

| Client option | Default | Behavior |
| --- | --- | --- |
| `maxReconnectAttempts` | `3` | Per-turn reconnects after transient stream disconnects |
| `preserveCompletedSessions` | `false` | Keep `continuationToken` after `session.completed` |

After a turn ending in `session.waiting`, `session.state` preserves `continuationToken`, `sessionId`, and `streamIndex`. After `session.completed` or `session.failed`, the client resets to `{ streamIndex: 0 }` unless `preserveCompletedSessions` is enabled.

```ts title="Resume saved state"
import type { SessionState } from "eve/client";

const saved = await loadSessionState() as SessionState;
const session = client.session(saved);
const response = await session.send("Continue where we left off.");
await response.result();
```

HITL deliveries retry up to 10 times on transient `500` responses matching "target session was not found".

### Manual stream attachment

When you already have a `sessionId` and only need to read events without sending:

```ts
const session = client.session({
  continuationToken: "eve:…",
  sessionId: "wrun_…",
  streamIndex: 10,
});

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

Pass `{ startIndex: 0 }` to rewind. `stream()` throws if no `sessionId` exists (no message sent yet).

## Error cases

| Condition | Status | Response shape |
| --- | --- | --- |
| Invalid JSON body | `400` | `{ ok: false, error: "…" }` |
| Missing `message` on create | `400` | `{ ok: false, error: "Missing or empty 'message' field." }` |
| Missing `continuationToken` on continue | `400` | `{ ok: false, error: "Missing or empty 'continuationToken' field." }` |
| Neither `message` nor `inputResponses` on continue | `400` | `{ ok: false, error: "Expected a non-empty 'message'…" }` |
| Session not found | `404` | `{ ok: false, error: "Session not found." }` |
| Attachment policy violation | `413` / `415` | `{ ok: false, error, violations }` |
| Route auth failure | `401` / `403` | Framework-shaped unauthorized envelope |
| `onMessage` returns `null` | `204` | Message accepted but not dispatched |

Transport and route errors from `eve/client` throw `ClientError`. A stream ending in `session.failed` returns `status: "failed"` from `result()` without throwing.

## Event dispatch order

Every stream event runs four steps in order:

1. Channel handler — the channel's event handler mutates adapter state
2. Metadata projection — `metadata(state)` re-evaluated and stored
3. Hooks — authored hooks subscribed to the event fire
4. Dynamic resolvers — dynamic tool, skill, and instruction resolvers fire with fresh `ctx.channel.metadata`

By the time a resolver or hook reads channel metadata, the channel has already updated its state.

## Related pages

<CardGroup>
<Card title="Eve HTTP protocol reference" href="/eve-http-protocol-reference">
Full route inventory, event schemas, reconnect rules, and client integration entry points.
</Card>
<Card title="Runtime models" href="/runtime-models">
Compare Eve sessions, turns, and `continuationToken` with Flue sessions and workflow runs.
</Card>
<Card title="Eve quickstart" href="/eve-quickstart">
Run `eve dev`, send the first session message, and verify the HTTP API locally.
</Card>
<Card title="Auth and route protection" href="/page-eve-authoring-surfaces">
Configure `agent/channels/eve.ts` auth before exposing session routes in production.
</Card>
</CardGroup>
