Continuations and Output Schema
Purpose and Scope
This page explains two client-side patterns that often appear together in production eve applications: resumable conversations and typed turn results. A continuation is the client workflow for saving the handles needed to resume an existing durable session later. An output schema is a per-turn request that asks the agent runtime to return structured data, not only assistant text. Together, these features let an application reload a conversation, send the next turn, and safely consume a typed result for UI state, automation, or downstream persistence.
Use this page when you are building a frontend, backend integration, or multi-chat product on top of eve/client and need more than fire-and-forget text. Continuations protect the conversational lifecycle: they preserve the resume token, stream identity, and stream cursor needed to avoid replaying events incorrectly. Output schemas protect the result contract: they tell the runtime the exact shape the caller expects for a specific turn and expose that payload through the final client result. Sources: docs/guides/client/output-schema.mdx
Relevant Source Files
docs/guides/client/output-schema.mdx— Documents theoutputSchematurn option, JSON Schema and Standard Schema usage,MessageResult.data, manualresult.completedstream consumption, per-turn scoping, and HITL follow-up examples.
Core Primitives
The main client primitive is a ClientSession, created from an eve Client. A session sends turns, consumes stream events, and keeps local cursor state. The official continuation model distinguishes between the resume handle and the stream handle: the continuation token is used to send the next user turn, while the session ID is used to attach to event history. The session also tracks a stream index so a reconnect can continue from the number of events already consumed rather than replaying everything to the UI.
A SessionState should be treated as a cursor, not as a transcript. Store it alongside your own chat record, but do not use it as the message history itself. If the UI needs to show old assistant text, tool events, or structured results after reload, persist those rendered events separately under your own thread or conversation ID. On reload, pass the saved state back to the client session and pass the saved events back to your renderer so the UI starts with historical content and the client starts at the correct stream cursor.
The output-schema primitive is the outputSchema property on a client turn. The source guide describes it as a per-turn option for callers that need structured data instead of only assistant text. The runtime makes the model satisfy the schema before the turn settles and then emits the final payload in a result.completed event. The convenience response.result() method surfaces the most recent completed structured payload as result.data. Sources: docs/guides/client/output-schema.mdx
Continuation Workflow
A typical continuation flow starts with a normal send. Create a session, send the first message, wait for the stream to settle with response.result(), and persist the session state after the turn finishes. Persisting after the streamed turn matters because the state is updated as stream events are consumed. Saving too early can leave the application with an incomplete cursor, which may cause the next page load or reconnect to consume from the wrong position.
When the user returns, hydrate the client session with the saved state before sending the next turn. If the app controls persistence, prefer storing the full state object rather than only the continuation token. A token alone can resume a follow-up, but it does not carry the previous stream cursor, so it is weaker for reconnecting UIs that need precise event delivery. For multi-chat interfaces, create a separate ClientSession per conversation and store each conversation's state independently.
Session status also affects whether a continuation should be reused. The official client model treats waiting sessions as resumable: if a turn ends waiting for more user input or human-in-the-loop responses, the next send continues the same durable session. Completed or failed sessions reset local state, so the next send starts fresh. This distinction is important for applications that mix task-like one-shot turns with long-lived conversational threads.
Output Schema Workflow
Use outputSchema when the caller needs a typed result for a single turn. The guide shows raw JSON Schema working directly with session.send<Summary>({ message, outputSchema }), where the TypeScript generic describes the expected result shape. After await response.result(), the structured payload is available as result.data; it can be undefined if the turn did not produce structured output. Sources: docs/guides/client/output-schema.mdx
import { Client } from "eve/client";
interface Summary {
title: string;
count: number;
}
const outputSchema = {
type: "object",
properties: {
title: { type: "string" },
count: { type: "integer" },
},
required: ["title", "count"],
} as const;
const client = new Client({ host: "http://127.0.0.1:3000" });
const session = client.session();
const response = await session.send<Summary>({
message: "Summarize this turn.",
outputSchema,
});
const result = await response.result();
console.log(result.data?.title);The same workflow supports Standard Schema implementations such as Zod, Valibot, and ArkType. In that mode, the schema object is lowered to JSON Schema before the request is sent. The client uses your generic and schema to type MessageResult.data, but the server remains authoritative for validation and the streamed payload is not revalidated client-side. This is a useful boundary: application code gets ergonomic TypeScript types, while the runtime owns the final model-validation contract. Sources: docs/guides/client/output-schema.mdx
If you consume the stream manually, read the structured payload from result.completed. The guide's event-loop example checks event.type === "result.completed" and casts event.data.result to the expected type. This is the lower-level path for custom renderers, observability code, or UIs that react as events arrive instead of waiting for response.result(). If more than one result.completed event appears in the consumed list, result() returns the most recent structured result as data. Sources: docs/guides/client/output-schema.mdx
Combining Resumability with Typed Results
Continuations and output schemas compose cleanly because they solve different parts of the turn lifecycle. Continuation state answers, “Which durable session and stream cursor should this client use next?” Output schema answers, “What structured payload should this turn produce before it settles?” In a production workflow, you can hydrate a saved session, send a typed follow-up, await the result, persist the updated session state, and store the returned data in your own application tables.
A common pattern is a multi-step assistant that alternates between conversation and structured extraction. The user may first ask a free-form question, then later click a UI action that requests a typed summary, approved action, or report metadata. Because outputSchema is scoped to the turn that sends it, the schema does not become a permanent setting for the whole conversation. A later shorthand text send returns normal assistant output unless it also supplies an output schema. Sources: docs/guides/client/output-schema.mdx
const response = await session.send<Summary>({
message: "Return a structured summary for this saved conversation.",
outputSchema,
});
const result = await response.result();
await saveSessionState(session.state);
await saveSummary(result.data);
const followUpResponse = await session.send("Now answer normally.");
const followUp = await followUpResponse.result();
console.log(followUp.data); // undefined unless this turn also requested a schemaCompact Reference
| Concept | Client-facing contract | Notes |
|---|---|---|
continuationToken | Resume handle for the next user turn | Use it to continue a waiting durable session. |
sessionId | Stream-and-inspect handle | Use with stream history and reconnect behavior. |
streamIndex | Count of consumed stream events | Store with the session cursor to avoid replaying consumed events. |
SessionState | { continuationToken?: string; sessionId?: string; streamIndex: number } | Persist as a cursor, not as the chat transcript. |
outputSchema | Turn option accepted by object-form send() | Requests structured output for that one turn. |
MessageResult.data | Final structured payload from response.result() | undefined when no structured result was produced. |
result.completed | Stream event containing the final structured result | Use when manually iterating response events. |
Object-form sends are the most flexible way to request structured data because the same turn can carry message, clientContext, headers, abort signals, attachments, or human-in-the-loop responses. The source guide specifically shows outputSchema with additional client context and with inputResponses for follow-up HITL turns. That means a resumed approval flow can ask the user to approve an option and also require the agent to return the approved action in a structured shape. Sources: docs/guides/client/output-schema.mdx
Next Steps
Use full SessionState persistence whenever your application owns chat storage, and keep the event log separate from that cursor. Add outputSchema only to the turns that need typed data, and make your UI handle the undefined case for result.data. For adjacent client behavior, read the client messages guide for send payloads, the streaming guide for event rendering, and the agent configuration reference when the desired structured output belongs to an agent or subagent definition rather than a single client turn.