Skip to main content
agent.send(text) is single-turn: every call starts from an empty history. A session is a multi-turn conversation: it keeps the transcript and passes it to the model on every send().

The session object

Concurrent send() calls on one session are queued and run one after another in call order, so the transcript never interleaves. agent.session({ id, turnPolicy }) chooses what a send() or stream() does while a turn is running or waiting to start:
  • 'wait' (the default): it waits and runs as its own turn, as above.
  • 'queue': its input joins that turn, like run.enqueue(): it is added after the current step’s tool results and the turn’s next model call sees it. The call resolves with that turn’s result (its own signal does not apply to the turn), and the transcript saved when the turn ends holds the queued user message in order. A joining stream() yields only the turn’s run.done; the turn’s events, input.queued and input.applied included, stream on the run that started it. If the turn ends before it could take the input (it finished, paused or was aborted), the call runs as the next turn after all. If the turn fails, the call rejects with the same error; in a durable session the input stays in the turn’s checkpoint and resume() applies it.
  • 'steer': like 'queue', but the input joins through run.steer(): if the turn’s model call has not emitted anything yet, it is aborted and made again with the new message, and tool calls of the turn that have not started are not run. Otherwise it waits for the next safe point, as with 'queue'. The call resolves with the turn’s result, and the same fallbacks apply.
send() and stream() take a string, content parts ([{ type: 'text', ... }, { type: 'image', ... }], one user message) or a Message[]; the stores keep the parts, see Multimodal input. A send() that throws (a provider error, or a tool that throws a PropagatingToolError) or is aborted with signal leaves the transcript exactly as it was before that call. An aborted send() resolves with finishReason: 'aborted', as agent.send() does. The stored transcript never contains an assistant tool-call turn without the matching tool results. A send() that pauses on a needsApproval tool resolves with finishReason: 'awaiting-approval'. agent.approvals.resolve({ id, approved }) then continues the run as the session’s next turn, so the tool call, its result and the final answer join the transcript (see Approvals). agent.session({ id, limits }) sets budgets across all of the session’s turns (for example { maxCostUsd: 1 }), counted from the usage saved with the transcript; createAgent({ limits }) limits each turn on its own. See Budgets.

Streaming a session turn

session.stream(input, { signal }) is agent.stream() for a conversation: it returns the same AgentRun (typed events plus a result promise, see Streaming), but the turn sees the transcript so far and joins it.
  • It builds the same message list as send() and queues behind earlier calls on the session (send() and stream() can be mixed).
  • When the run ends, the new user message and the run’s output are saved exactly as send() saves them, and only then is run.done delivered. When the for await loop ends, or run.result resolves, the transcript is complete. The events carry the runId of the returned handle.
  • An aborted run (signal, or breaking out of the loop early) or a failed one leaves the transcript as it was before the call, like send(). A run that was already finished when the loop was left is saved. A failed run ends the stream with error and run.done (finishReason: 'error'), and run.result rejects. This includes a store that fails to load or save: the turn then has no run.start.
  • A run that pauses on a needsApproval tool ends with approval.requested and run.done ('awaiting-approval'), exactly as send() resolves, and the transcript holds the turn up to the pause. agent.approvals.resolve() continues it as the session’s next turn. An approve callback does not apply to streams, as with agent.stream().
Sessions are a thin layer: the session owns the transcript and hands it to the executor on each turn. Without a checkpoint store, a turn is saved only when it ends, so a crash in the middle of a turn loses it; see the next section.

Durable sessions

Give the agent a store with checkpoints and every session turn is checkpointed after each model response and tool result, using the durable execution mechanism. A turn interrupted by a crash, a failed checkpoint write or a PropagatingToolError can then be finished later, in another process:
  • createAgent({ store }) takes any AgentStore ({ sessions?, checkpoints?, approvals? }, see Choosing a store): agent.session({ id }) keeps its transcript in store.sessions and checkpoints in store.checkpoints, and store.approvals holds approval pauses. A session’s own store (a SessionStore, or a { sessions, checkpoints } object) and checkpointStore win over the agent’s, part by part.
  • agent.resume(id) is agent.session({ id }).resume(), except that it first finishes a run started with agent.send(message, { sessionId: id }) (see Durable execution).
  • Each turn runs with sessionId: '<session id>.turn-<n>' (n is the length of the transcript when the turn started), so a new process finds the interrupted turn without any extra bookkeeping. A finished turn joins the transcript, exactly as a plain send() does, and its checkpoint is deleted.
  • resume() continues the turn through the executor’s resume path (input: []): tool calls whose results were recorded do not run again, and a recorded model response is not requested again. A tool that was running when the process died does run again (at-least-once, see Durable execution).
  • send() and stream() resume a pending turn first, then send the new message, so the model sees the finished turn (the resumed turn’s events are not streamed). Use pending() to check first, or discardPending() to drop the unfinished turn instead.
  • A turn that pauses on a needsApproval tool stays in its checkpoint, not in the transcript, until it finishes. While it waits, resume(), send() and stream() throw SessionAwaitingApprovalError (with its approvalId), and agent.approvals.resolve({ id, approved }) continues the turn in this session. After a restart, open the session and call resume() (or send()) once before resolving, so the agent knows which session the approval belongs to; give both agents the same durable store (or approvalStore).
  • An aborted turn is dropped (its checkpoint is deleted), as without a checkpoint store. clear() deletes a pending turn too.

Stores

A SessionStore keeps transcripts between calls:
  • MemorySessionStore (the default, one new store per session) lives as long as the process. Share one instance between sessions to look them up by id.
  • FileSessionStore(dir) writes one JSON file per session (<dir>/<id>.json), atomically (temp file, then rename), creating dir on first save.
Session ids must match ^[A-Za-z0-9_-]{1,128}$ (they become file names, so ../x and a/b are refused with an error that says so). Implement SessionStore yourself to keep transcripts in a database or Redis.

Choosing a store

Sessions, durable-execution checkpoints and approvals each have a store interface (SessionStore, CheckpointStore, ApprovalStore). Pick the implementation by where the process runs: Each one is an AgentStore part: pass them together as createAgent({ store: { sessions, checkpoints, approvals } }). memoryStore() and SqliteStore are ready-made AgentStores; for plain files, combine the file stores:
Any object with the three methods of a part works there too: a Redis SessionStore, or a KV-backed CheckpointStore on Cloudflare Workers (the generated Worker uses KVCheckpointStore, see Deployment). SqliteStore keeps all three in one database file, using Node’s built-in node:sqlite (no native dependency; needs Node 22.13 or newer, and it is not re-exported from the root entry, so importing the SDK never loads it):
  • The directory is created if missing. The schema is versioned with PRAGMA user_version and migrated on open; a database written by a newer release is refused.
  • WAL mode and a 5 s busy timeout let two processes share the file. An approval can be resolved by only one of them.
  • Checkpoints and approval snapshots are stored as opaque JSON, so new fields round-trip unchanged.
  • prune() removes sessions and checkpoints not updated within olderThanMs, and approvals resolved that long ago; unresolved approvals are kept.
  • A file that is not a SQLite database fails with an error naming the path; using the store after close() throws a clear error.

Project instructions

Not part of sessions, but often wanted together: see “Project instructions” in Configuration to give an agent your repository’s AGENTS.md.