# Runtime models

> Compare Flue sessions, workflow runs, and dispatch receipts with Eve sessions, turns, `continuationToken`, and `sessionId` contracts.

- 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

- `withastro-flue:AGENTS.md`
- `withastro-flue:packages/runtime/src/runtime/flue-app.ts`
- `withastro-flue:packages/runtime/src/runtime/handle-agent.ts`
- `vercel-eve:docs/concepts/execution-model-and-durability.md`
- `vercel-eve:docs/concepts/sessions-runs-and-streaming.md`
- `vercel-eve:README.md`

---

---
title: "Runtime models"
description: "Compare Flue sessions, workflow runs, and dispatch receipts with Eve sessions, turns, `continuationToken`, and `sessionId` contracts."
---

Flue and Eve both model long-lived agent work, but they partition identifiers and persistence differently. Flue keeps **workflow runs** (`runId`, `/runs/:runId`) separate from **continuing agent sessions** (`harness.session()`, `instanceId`, `submissionId`, `dispatchId`). Eve unifies durable conversation around a single **`sessionId`** for streaming and a channel-owned **`continuationToken`** for follow-up delivery; each user message runs as a durable **turn** checkpointed at **steps**.

## Side-by-side contracts

| Concept | Flue | Eve |
| --- | --- | --- |
| Long-lived conversation | `AgentInstance` → `Harness` → named `Session` (default `"default"`) | Durable `session` spanning days; `sessionId` is the stream handle |
| One user input unit | `Operation` (`prompt` / `skill` / `task` / `shell`) containing one or more `Turn`s (LLM round-trips) | `turn`: one user message and all model/tool work until the agent responds |
| Durable checkpoint inside a unit | Turn journal inside session history (submission reconciliation) | `step`: one model call and its tool calls; Workflow SDK checkpoints |
| Finite orchestration | `Workflow` → `run()` with unique `ctx.id === runId` | Not authored directly; workflow primitives are runtime-internal |
| Async ingress receipt | `DispatchReceipt` with `dispatchId` (not a `runId`) | N/A — channels deliver via `continuationToken` |
| HTTP prompt admission | `202` with `streamUrl`, `offset`, `submissionId` | `200`/`JSON` with `sessionId`, `continuationToken` |
| Stream protocol | Durable Streams on agent URL or `/runs/:runId` | NDJSON at `/eve/v1/session/:sessionId/stream` |
| Follow-up handle | Same `instanceId` + session name; no token rotation | Current `continuationToken`; stale tokens rejected |
| Terminal “ready for input” | Session returns to idle; no dedicated event name | `session.waiting` on the NDJSON stream |

<Warning>
In Flue, **`runId` is workflow-only**. Direct HTTP prompts, WebSocket traffic, channel dispatch, and `dispatch(...)` operate inside persistent agent sessions and must not be described as runs. `/runs` and `flue logs` inspect workflow runs only.
</Warning>

## Flue runtime model

Flue nests work from reusable profiles down to LLM turns:

```text
Agent profile (defineAgentProfile)
└─ Created agent (createAgent)
   └─ AgentInstance (URL :id)
      └─ Harness (init(); default name "default")
         └─ Session (harness.session(name?); default "default")
            └─ Operation (session.prompt | skill | task | shell)
               └─ Turn (one LLM round-trip inside pi-agent-core)
Workflow (workflows/<name>.ts, exports run)
└─ Workflow run / invocation (unique ctx.id === runId)
```

### Agent sessions and operations

After `ctx.init(agent)` returns a harness, `harness.session()` get-or-creates named conversation state. Each `session.prompt()`, `skill()`, `task()`, or `shell()` call is one **operation** with a generated `operationId`. Operations may emit multiple **turn** events (`turn_start`, `turn`, tool events) before completing.

Sessions persist history so later operations continue with prior context. With a `db.ts` `PersistenceAdapter`, session rows and submission journals survive process restarts; without one, Node keeps state in process memory.

### Workflow runs

`POST /workflows/:name` admits a new run. The handler receives `FlueContext` where `ctx.id` and `runId` identify the invocation. Admission returns:

<ResponseField name="runId" type="string">
Unique workflow run identifier. On Cloudflare, one workflow Durable Object instance per run — `instanceId` equals `runId`.
</ResponseField>

<ResponseField name="streamUrl" type="string">
Absolute Durable Streams URL under the mount prefix at `/runs/:runId`.
</ResponseField>

<ResponseField name="offset" type="string">
Stream offset captured at admission; reading from it replays the full run.
</ResponseField>

Run lifecycle events are `run_start`, optional `run_resume` (recovery), workflow body events, then terminal `run_end`. `RunStore` records status (`active` | `completed` | `errored`). `GET /runs/:runId?meta` returns `RunRecord` JSON; `GET /runs/:runId` reads the Durable Streams event log.

### Direct HTTP agent prompts

`POST /agents/:name/:id` accepts a JSON body `{ "message": string, "images"?: image[] }`. Default mode returns `202` with stream coordinates on the same URL:

<ResponseField name="streamUrl" type="string">
Agent event stream at the request URL (query stripped).
</ResponseField>

<ResponseField name="offset" type="string">
Current Durable Streams offset; `-1` before the first accepted prompt creates the stream.
</ResponseField>

<ResponseField name="submissionId" type="string">
Durable submission identifier for this admitted prompt.
</ResponseField>

`?wait=result` blocks until the operation completes and returns `{ result, streamUrl, offset, submissionId }`. `GET`/`HEAD` on the same path reads the agent Durable Streams log. Stream reads return `404` until the first prompt is accepted.

### `dispatch(...)` receipts

Application code calls `dispatch(agent, { id, input })` or `dispatch({ agent, id, input })` to queue asynchronous input to a continuing instance:

```ts
interface DispatchReceipt {
  dispatchId: string;
  acceptedAt: string;
}
```

<ParamField body="id" type="string" required>
Target agent instance id.
</ParamField>

<ParamField body="input" type="unknown" required>
JSON-serializable payload snapshotted at admission. Use `null` for an intentional empty payload.
</ParamField>

`await dispatch(...)` resolves when the runtime **admits and queues** the input — not when the model replies. The returned `dispatchId` identifies delivery; it is **not** a workflow `runId` and does not create run history. Dispatched events may carry `dispatchId` on the agent stream; correlate external side effects with this id for idempotency.

On Node, the default queue is process-lifetime memory. With a durable `db.ts` adapter, dispatches share the same ordered submission lifecycle as direct HTTP prompts. On Cloudflare, admission is durable to the agent Durable Object with at-least-once processing — design external effects to be idempotent.

### Flue event correlation

| Field | Applies to | Meaning |
| --- | --- | --- |
| `runId` | Workflow runs only | Persisted workflow event identity with `eventIndex` |
| `instanceId` | Agent activity | Agent instance URL segment |
| `submissionId` | Durable agent submissions | HTTP prompt or dispatch admission row |
| `dispatchId` | Dispatched input | Delivery identifier (may equal `submissionId` on replay) |
| `session` | Agent activity | Harness session name |
| `harness` | Agent activity | Harness name |
| `operationId` / `turnId` | Agent activity | Generated per operation / LLM turn |

Agent streams and `observe()` deliver live activity; workflow streams persist immutable `runId` + `eventIndex` pairs.

```mermaid
flowchart TB
  subgraph flue_agent["Flue — continuing agent"]
    HTTP["POST /agents/:name/:id"]
    DISP["dispatch(...)"]
    Q["Per-instance submission queue"]
    SESS["Harness → Session history"]
    ASTR["DS stream at /agents/:name/:id"]
    HTTP --> Q
    DISP --> Q
    Q --> SESS
    SESS --> ASTR
  end

  subgraph flue_wf["Flue — workflow run"]
    WHTTP["POST /workflows/:name"]
    RUN["run(ctx) handler"]
    RSTORE["RunStore + run_start/run_end"]
    RSTR["DS stream at /runs/:runId"]
    WHTTP --> RUN
    RUN --> RSTORE
    RUN --> RSTR
  end
```

## Eve runtime model

Eve treats every conversation as a **durable session** backed by the Workflow SDK. Work nests in three levels:

- **session** — whole durable conversation; survives restarts and redeploys
- **turn** — one user message and all triggered model/tool work until the agent responds
- **step** — durable checkpoint inside a turn (one model call and its tool calls)

Channels normalize transport and own delivery policy. The harness executes one unit of AI work. The runtime persists state, follows workflow `next` hooks, and streams NDJSON events.

```text
Channel (owns continuationToken, auth, delivery)
  → send(message, { continuationToken, auth })
    → Runtime (owns sessionId, event stream, workflow lifecycle)
      → Harness (one turn's tool loop)
        → Step checkpoints (Workflow SDK)
```

### Two handles

Eve deliberately splits resume from inspection:

| Handle | Owner | Job |
| --- | --- | --- |
| `continuationToken` | Channel | Resume handle for the next user message. Namespaced as `<channel>:<rawToken>` (e.g. `eve:7f3c...`). One active continuation per session; stale tokens are rejected. |
| `sessionId` | Runtime | Stream-and-inspect handle. Attach to `/eve/v1/session/:sessionId/stream`. Also referred to as `runId` in docs when describing the active workflow run. |

<Note>
Mixing these handles is the most common integration mistake. POST follow-ups with `continuationToken`; stream and reconnect with `sessionId`.
</Note>

### HTTP session routes

| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/eve/v1/session` | Start a session |
| `POST` | `/eve/v1/session/:sessionId` | Send follow-up (requires current `continuationToken`) |
| `GET` | `/eve/v1/session/:sessionId/stream` | NDJSON event stream (`?startIndex=` for reconnect) |

<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
{
  "ok": true,
  "sessionId": "ses_01h...",
  "continuationToken": "eve:7f3c..."
}
```
</ResponseExample>

The response also sets `x-eve-session-id`. After `session.waiting` appears on the stream, send the next message:

<RequestExample>
```bash
curl -X POST http://127.0.0.1:3000/eve/v1/session/ses_01h... \
  -H 'content-type: application/json' \
  -d '{"continuationToken":"eve:7f3c...","message":"Now send the short version."}'
```
</RequestExample>

### Turns, steps, and parked work

Each turn runs as a durable workflow. Completed steps never re-run; interrupted steps re-run, so non-idempotent side effects need gating or idempotency keys.

When a turn waits on human approval, OAuth, or a subagent, the workflow **parks**. The stream emits `session.waiting`. Delivery to the current `continuationToken` wakes the session and starts the next turn.

<Warning>
Eve does not maintain a durable FIFO message queue per session. `continuationToken` is a workflow resume hook, not a general queue address. For deterministic ordering, send one message at a time and wait for `session.waiting` before the next delivery to the same session.
</Warning>

### Eve stream events (selected)

| Event | Boundary |
| --- | --- |
| `session.started` | Session created |
| `turn.started` / `turn.completed` / `turn.failed` | Turn boundaries |
| `step.started` / `step.completed` / `step.failed` | Step checkpoints |
| `message.received` | Inbound user message accepted |
| `input.requested` | HITL pause |
| `subagent.called` / `subagent.completed` | Delegation (`childSessionId` for child stream) |
| `session.waiting` | Parked, ready for next input |
| `session.completed` / `session.failed` | Terminal session states |

Subagents receive their own durable `sessionId` and stream; the parent emits `subagent.called` with `childSessionId`.

```mermaid
sequenceDiagram
  participant Client
  participant Channel as Eve channel
  participant Runtime
  participant Stream as NDJSON stream

  Client->>Channel: POST /eve/v1/session {message}
  Channel->>Runtime: send(message, {continuationToken})
  Runtime-->>Client: sessionId + continuationToken
  Client->>Stream: GET /session/:sessionId/stream
  Stream-->>Client: turn.started … step.* … session.waiting
  Client->>Channel: POST /session/:sessionId {continuationToken, message}
  Channel->>Runtime: resume hook
  Stream-->>Client: turn.started … session.waiting
```

## Mapping concepts across frameworks

| Integration question | Flue | Eve |
| --- | --- | --- |
| “Where is my conversation?” | `agents/:name/:id` + session name | `sessionId` |
| “How do I send the next message?” | `POST /agents/:name/:id` or `dispatch(...)` to same `id` | `POST /eve/v1/session/:sessionId` with current `continuationToken` |
| “How do I watch progress?” | Durable Streams on agent URL or `/runs/:runId` | `GET /eve/v1/session/:sessionId/stream` |
| “How do I run a one-shot job?” | `POST /workflows/:name` → `runId` | Sessions are conversational; schedules dispatch new `sessionId`s |
| “How do I correlate async ingress?” | `dispatchId` on receipt and events | Channel-local token → namespaced `continuationToken` |
| “When is it safe to send again?” | After prior operation/submission settles (stream idle or `submission_settled`) | After `session.waiting` |

Both frameworks separate **transport admission** from **durable execution**, but the handles differ. Flue exposes `submissionId`/`dispatchId` for agent submissions and reserves `runId` for workflows. Eve rotates `continuationToken` per channel policy while keeping a stable `sessionId` for the event log.

## Durability defaults

| Target | Flue agent sessions | Flue workflow runs | Eve sessions |
| --- | --- | --- | --- |
| Node default | In-memory; optional `db.ts` adapter | `RunStore` + DS stream in-process | Workflow SDK durability when deployed with supported backend |
| Cloudflare | Agent DO SQLite + durable submission queue | Per-run workflow DO | Platform workflow persistence |

Flue agent recovery reconciles submissions conservatively: completed work is recognized, uncertain tool outcomes are not replayed blindly, and `submission_settled` notifies detached stream readers. Eve replays from the last completed step after crash, timeout, or redeploy.

## Related pages

<CardGroup>
<Card title="Flue HTTP API reference" href="/flue-http-api-reference">
Mounted routes, admission envelopes, Durable Streams reads, and error shapes for agents, workflows, and runs.
</Card>
<Card title="Flue persistence reference" href="/flue-persistence-reference">
`PersistenceAdapter`, session stores, and `db.ts` discovery for durable agent execution.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Author `run()`, invoke via CLI or HTTP, and inspect events with `flue logs`.
</Card>
<Card title="Eve sessions and streaming" href="/eve-sessions-streaming">
NDJSON events, `continuationToken` follow-ups, reconnect rules, and client integration.
</Card>
<Card title="Eve HTTP protocol reference" href="/eve-http-protocol-reference">
Session routes, event types, and stream parameters in full reference form.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
`createAgent`, harnesses, sessions, tools, and HTTP route handlers.
</Card>
</CardGroup>
