eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
138 lines (92 loc) • 5.32 kB
text/mdx
---
title: "Continuations"
description: "Persist and resume eve client sessions with continuation tokens, session IDs, and stream cursors."
---
Every eve client turn returns two handles, and mixing them up is a common mistake. The TypeScript client tracks both for you:
- `continuationToken`: the resume handle. Use it to send the next user turn.
- `sessionId`: the stream-and-inspect handle. Use it to attach to event history.
`ClientSession` also tracks `streamIndex`, the count of events already consumed. Together these three fields make a `SessionState`.
## Read and persist state
After a streamed turn finishes, read `session.state`:
```ts
const session = client.session();
const response = await session.send("Create a launch checklist.");
await response.result();
await saveSessionState(session.state);
```
As soon as `send()` returns, `session.state.sessionId` identifies the accepted in-flight turn for cancellation or stream reconnection. Consume the response before saving a fully advanced cursor: the continuation token and `streamIndex` update as events reach the turn boundary.
Store the full state object:
```ts
interface SessionState {
continuationToken?: string;
sessionId?: string;
streamIndex: number;
}
```
The continuation token resumes the conversation. The session ID and stream index let the client reconnect to the right stream position without replaying events it already consumed.
`SessionState` is a cursor, not a chat transcript. If your app shows historical messages, persist the stream events separately and store them under your own chat or thread ID. On reload, pass the saved `SessionState` back to `client.session(savedState)` or `useEveAgent({ initialSession })`, and pass the saved events to your renderer or `useEveAgent({ initialEvents })`.
## Resume a saved session
Pass the saved state back into `client.session()`:
```ts
import type { SessionState } from "eve/client";
const saved = (await loadSessionState()) as SessionState;
const session = client.session(saved);
const response = await session.send("Now shorten it.");
const result = await response.result();
console.log(result.message);
```
If all you have is a continuation token, pass it as shorthand:
```ts
const session = client.session(continuationToken);
const response = await session.send("Continue where we left off.");
await response.result();
```
The shorthand can send a follow-up, but it doesn't know the previous stream cursor. Prefer full `SessionState` when you control persistence.
## Waiting, completed, and failed sessions
When a turn ends with `session.waiting`, the event carries the current channel-owned `data.continuationToken` and the client preserves the state so the next send continues the conversation.
When a turn ends with `session.completed` or `session.failed`, the client resets its local state. The next send starts a fresh durable session:
```ts
const response = await session.send("Do this one-shot task.");
const result = await response.result();
if (result.status === "completed") {
// session.state is now a fresh cursor: { streamIndex: 0 }
}
```
This matches the runtime contract, where only waiting sessions can accept the next user input.
## Multiple sessions
Create a separate `ClientSession` per conversation:
```ts
const research = client.session();
const support = client.session();
const researchResponse = await research.send("Research competitors.");
await researchResponse.result();
const supportResponse = await support.send("Draft a support reply.");
await supportResponse.result();
await save("research", research.state);
await save("support", support.state);
```
The shared `Client` only owns host, auth, headers, and reconnect settings. Conversation state lives on each `ClientSession`.
For a multi-chat UI, this usually means one application row per chat with two eve fields: the latest `SessionState` and the ordered event log for rendering. Do not reuse one `ClientSession` across sidebar conversations, and do not treat `streamIndex` as your database row index; it is only the remote stream cursor for that eve session.
## Reconnect an existing stream
When a session already has a `sessionId`, `session.stream()` reattaches to its stream from the saved cursor. Resuming a saved `SessionState` after a restart is the common reason to do this:
```ts
const session = client.session(savedState);
for await (const event of session.stream()) {
console.log(event.type);
}
```
`stream()` attaches to an existing run; to send new user input, use `send()`. For overriding the cursor with `startIndex` and the full reconnection model, see [Streaming](./streaming#open-a-stream-manually).
If you only persisted `sessionId`, read the latest durable event to recover the current token from a waiting session:
```ts
const session = client.session({ sessionId, streamIndex: 0 });
for await (const event of session.stream({ startIndex: -1 })) {
if (event.type === "session.waiting") {
await saveContinuationToken(event.data.continuationToken);
}
break;
}
```
## What to read next
- [Streaming](./streaming): stream events and reconnect by index
- [Sessions, runs & streaming](../../concepts/sessions-runs-and-streaming): the raw HTTP contract
- [eve channel](../../channels/eve): where continuation tokens come from