Are you an LLM? View https://a3s-lab.github.io/Code/llms.txt for optimized Markdown documentation, or https://a3s-lab.github.io/Code/llms-full.txt for full documentation bundle. This page is also available as Markdown at https://a3s-lab.github.io/Code/v8.3.0/en/guide/sessions.md
Agent owns configuration and provider state. Session binds that agent to one workspace and one conversation lifecycle.
This is where UI integration usually begins: subscribe to AgentEvent values
from the session and map them to progress, tool calls, confirmations, and results.
Rust session construction is async-first because default memory, file-backed
stores, queues, trajectory recording, and MCP discovery can require I/O:
Rust
letsession=agent
.session_builder("/repo")
.options(options)
.build()
.await?;
session_async, resume_session_async, session_for_agent_async, and
session_for_worker_async are direct async alternatives. Node and Python keep
their existing factory names and delegate to this async construction kernel
inside the native binding.
The synchronous Rust Agent::session factory is a strict compatibility path.
It accepts explicitly pre-initialized resources and never starts or blocks an
async runtime. A default/file memory store, file session store, queue,
trajectory recorder, or any host MCP manager supplied in SessionOptions
returns CodeError::AsyncSessionBuildRequired; session-option MCP capability
discovery is always async. Use the builder instead of catching that error and
silently changing backends. The sync path can only inherit agent-global MCP
tools already cached during agent initialization.
planningMode is explicit: use 'auto' for the default structured pre-analysis
path, 'enabled' to force planning, and 'disabled' to turn planning off for
latency-sensitive calls. The legacy boolean planning option remains available
for compatibility.
Planning emits run-scoped state. Hosts can render that state as a TaskList and
update each item as the runtime records progress instead of trying to infer
progress from text tokens.
A session admits only one transcript-affecting operation at a time. send,
stream, their attachment variants, slash commands, and resumeRun share the
same fail-fast admission gate. An overlapping call returns
CodeError::SessionBusy before reading history or dispatching a command; it is
not queued behind the active operation.
Wait for the active result, consume the stream to completion, or cancel it
before starting another conversation operation. The runtime retains a stream's
lease until its producer has actually stopped, even if the public stream handle
is dropped or aborted. Direct host tool helpers do not mutate the transcript and
do not claim this lease.
Node and Python stream iterators wait for that lifecycle cleanup at the terminal
boundary. Once a fully consumed iterator reports completion, an immediate next
conversation operation will not inherit a stale busy state from that stream.
foreventinsession.stream("Run the focused tests and explain failures"):
ifevent.type=="text_delta"andevent.text:
print(event.text,end="",flush=True)
elifevent.type=="tool_start":
print(f"\ntool:{event.tool_nameor'unknown'}")
elifevent.type=="error":
raiseRuntimeError(event.erroror"stream error")
Go
stream,err:=session.Stream(
ctx,
"Run the focused tests and explain failures",
nil,
)
iferr!=nil{
returnerr
}
forevent:=rangestream.Events{
ifevent.Type!=code.EventTextDelta{
continue
}
varpayloadstruct{
Textstring`json:"text"`
}
iferr:=event.DecodePayload(&payload);err!=nil{
returnerr
}
fmt.Print(payload.Text)
}
iferr:= <-stream.Done;err!=nil{
returnerr
}
Every SDK event is an EventEnvelopeV1 projection with version === 1, an open
type string, complete payload, and optional metadata. Convenience fields
such as text and toolName are derived from that envelope. Keep a default
branch and retain the payload for future event types.
Run control changes an operation already in progress; it does not start a
second transcript turn. steer queues a newer user direction for the next
safe point. interrupt cancels current provider and Tool work cooperatively,
then lets supervised cleanup settle before the Run becomes cancelled.
Requests are idempotent when a caller reuses the same request ID with the same
payload. Optional Run and expected-Turn fields make stale UI actions fail
closed. Accepted control is not yet applied control: observe
run_control_applied or inspect persisted Run events when the distinction
matters. Neither operation changes the selected model, permissions, sandbox,
budgets, or approval policy.
The SDK does not expose a dedicated ephemeral-question helper. Snapshot the
current history and pass it explicitly to send or stream. Explicit history
is used for that call only; it does not append the answer back into the session
history.
Set autoSave: true or call await session.save() when using a session store.
This resumes a saved session snapshot; interrupted run checkpoints use
session.resumeRun(runId) instead. Go uses Save, ResumeSession, and
ResumeRun(ctx, runID) for the same two persistence levels.
See Persistence.
session.close() is a full graceful stop. The first call flips the session
into the closed state — further send/stream calls fast-fail with
CodeError::SessionClosed instead of starting a new run — then cancels the
active run, every in-flight delegated subagent task, and all pending
human-in-the-loop confirmations. Subsequent calls are no-ops and never panic.
Check the closed state with session.isClosed() (Node) / session.is_closed()
(Python) / session.IsClosed(ctx) (Go).
TypeScript
session.close();
if(session.isClosed()) {
// send/stream now reject with CodeError::SessionClosed
}
Python
session.close()
ifsession.is_closed():
# send/stream now reject with CodeError::SessionClosed
Every run derives its per-operation cancellation token from a single
session-level parent via child_token(), so close() cascades to all
in-flight work in one shot. Embedders that need the raw token — for example to
wire it into a host-side select! or to abort the session without the
run-store and hook side effects of close() — can clone it through
AgentSession::session_cancel_token().
The owning Agent tracks its live sessions by Weak reference (pruned
lazily) so a control plane can drive lifecycle without holding a session
handle:
Agent::list_sessions() returns the live session IDs (sorted, stable).
Agent::close_session(id) closes one session by ID — the same cleanup as
AgentSession::close(), invoked out-of-band.
Agent::close() closes every live session and tears down agent-owned
background resources (it also disconnects the global MCP connections).
After it returns, new session / resumeSession calls fail fast with
CodeError::SessionClosed.
Agent::is_closed() reports whether the agent itself has been closed.
TypeScript
constids=awaitagent.listSessions();
awaitagent.closeSession(ids[0]);
awaitagent.close();// closes all remaining sessions + global MCP
console.log(agent.isClosed());
Python
ids=agent.list_sessions()
agent.close_session(ids[0])
agent.close()# closes all remaining sessions + global MCP
print(agent.is_closed())
Go
ids,err:=agent.ListSessions(ctx)
iferr==nil&&len(ids)>0{
_,err=agent.CloseSession(ctx,ids[0])
}
err=agent.Close(ctx)
See CHANGELOG [3.3.0] — "Session / Agent lifecycle control".
SessionOptions carries four opaque identity fields that the host can attach
at session creation. The framework only transports them — it never interprets
or enforces them. They are propagated into SessionData, hooks, and traces,
and restored on resume, so the host can drive multi-tenant aggregation,
billing, and distributed tracing:
Node (camelCase)
Python (snake_case)
Go
Meaning
tenantId
tenant_id
TenantID
Multi-tenant label
principal
principal
Principal
User / service that triggered the session
agentTemplateId
agent_template_id
AgentTemplateID
Agent template / definition the session was instantiated from
currentRun() is for the operation that is current at the time of the call.
During an active send() or stream(), pass its id to cancelRun(id) to
request cancellation. When idle, currentRun() may return null or a retained run
snapshot depending on the preceding control flow, so use runs() for completed
history and inspect status before treating a snapshot as cancellable: