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, likerun.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 ownsignaldoes not apply to the turn), and the transcript saved when the turn ends holds the queued user message in order. A joiningstream()yields only the turn’srun.done; the turn’s events,input.queuedandinput.appliedincluded, 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 andresume()applies it.'steer': like'queue', but the input joins throughrun.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()andstream()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 isrun.donedelivered. When thefor awaitloop ends, orrun.resultresolves, the transcript is complete. The events carry therunIdof 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, likesend(). A run that was already finished when the loop was left is saved. A failed run ends the stream witherrorandrun.done(finishReason: 'error'), andrun.resultrejects. This includes a store that fails to load or save: the turn then has norun.start. - A run that pauses on a
needsApprovaltool ends withapproval.requestedandrun.done('awaiting-approval'), exactly assend()resolves, and the transcript holds the turn up to the pause.agent.approvals.resolve()continues it as the session’s next turn. Anapprovecallback does not apply to streams, as withagent.stream().
Durable sessions
Give the agent astore 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 anyAgentStore({ sessions?, checkpoints?, approvals? }, see Choosing a store):agent.session({ id })keeps its transcript instore.sessionsand checkpoints instore.checkpoints, andstore.approvalsholds approval pauses. A session’s ownstore(aSessionStore, or a{ sessions, checkpoints }object) andcheckpointStorewin over the agent’s, part by part.agent.resume(id)isagent.session({ id }).resume(), except that it first finishes a run started withagent.send(message, { sessionId: id })(see Durable execution).- Each turn runs with
sessionId: '<session id>.turn-<n>'(nis 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 plainsend()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()andstream()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). Usepending()to check first, ordiscardPending()to drop the unfinished turn instead.- A turn that pauses on a
needsApprovaltool stays in its checkpoint, not in the transcript, until it finishes. While it waits,resume(),send()andstream()throwSessionAwaitingApprovalError(with itsapprovalId), andagent.approvals.resolve({ id, approved })continues the turn in this session. After a restart, open the session and callresume()(orsend()) once before resolving, so the agent knows which session the approval belongs to; give both agents the same durablestore(orapprovalStore). - An aborted turn is dropped (its checkpoint is deleted), as without a
checkpoint store.
clear()deletes a pending turn too.
Stores
ASessionStore 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), creatingdiron first save.
^[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:
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_versionand 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 withinolderThanMs, 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’sAGENTS.md.