Sessions

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.

session.tsTypeScript
import { Agent } from '@a3s-lab/code';
const agent = await Agent.create('agent.acl');
const session = agent.session('/repo', {
model: 'provider/model-id',
planningMode: 'enabled',
goalTracking: true,
autoDelegation: { enabled: true, maxTasks: 4 },
autoParallel: false,
});

Rust Construction

Rust session construction is async-first because default memory, file-backed stores, queues, trajectory recording, and MCP discovery can require I/O:

Rust
let session = 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.

Single-Flight Operations

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.

Send

Rust
Node.js
Python
Go
Rust
let result = session
.send(
"Review this repository and list release blockers",
None,
)
.await?;
println!("{}", result.text);
println!("{}", result.usage.total_tokens);
println!("{:?}", result.verification_summary().status);

Stream

Rust
Node.js
Python
Go
Rust
use a3s_code_core::{AgentEvent, CodeError};
let (mut events, lifecycle) = session
.stream("Run the focused tests and explain failures", None)
.await?;
while let Some(event) = events.recv().await {
match event {
AgentEvent::TextDelta { text } => print!("{text}"),
AgentEvent::ToolStart { name, .. } => println!("\ntool: {name}"),
AgentEvent::End { .. } => break,
AgentEvent::Error { message } => return Err(CodeError::Llm(message)),
_ => {}
}
}
lifecycle
.await
.map_err(|error| CodeError::Internal(error.into()))??;

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.

Side Questions

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.

TypeScript
const snapshot = session.history();
const answer = await session.send(
'What files has this session already inspected?',
snapshot,
);
console.log(answer.text);
console.log(session.history().length === snapshot.length);

Resume

Rust
Node.js
Python
Go
Rust
use a3s_code_core::{Agent, SessionOptions};
let session = agent
.resume_session_async(
"release-review",
SessionOptions::new().with_file_session_store("./.a3s/sessions"),
)
.await?;

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.

Lifecycle and Close

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()
if session.is_closed():
# send/stream now reject with CodeError::SessionClosed
pass
Go
if err := session.Close(ctx); err != nil {
return err
}
closed, err := session.IsClosed(ctx)

Cancellation token

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().

Agent-side registry

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
const ids = await agent.listSessions();
await agent.closeSession(ids[0]);
await agent.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)
if err == nil && len(ids) > 0 {
_, err = agent.CloseSession(ctx, ids[0])
}
err = agent.Close(ctx)

See CHANGELOG [3.3.0] — "Session / Agent lifecycle control".

Host Identity Labels

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)GoMeaning
tenantIdtenant_idTenantIDMulti-tenant label
principalprincipalPrincipalUser / service that triggered the session
agentTemplateIdagent_template_idAgentTemplateIDAgent template / definition the session was instantiated from
correlationIdcorrelation_idCorrelationIDDistributed-trace correlation id
TypeScript
const session = agent.session('/repo', {
tenantId: 'tenant-example',
principal: 'principal-example',
agentTemplateId: 'agent-template-example',
correlationId: 'trace-example',
});
Python
opts = SessionOptions()
opts.tenant_id = 'tenant-example'
opts.principal = 'principal-example'
opts.agent_template_id = 'agent-template-example'
opts.correlation_id = 'trace-example'
session = agent.session('/repo', opts)
print(session.tenant_id, session.principal)
Go
session, err := agent.Session(ctx, "/repo", &code.SessionOptions{
TenantID: "tenant-example",
Principal: "principal-example",
AgentTemplateID: "agent-template-example",
CorrelationID: "trace-example",
})

See CHANGELOG [3.3.0] — "Host-provided identity labels".

Run Replay

Each send() or stream() creates a run record. Applications can inspect those records for UI state, audit, replay, cancellation, and tests:

TypeScript
const runs = await session.runs();
const latest = runs.at(-1);
if (latest) {
console.log(await session.runSnapshot(latest.id));
console.log(await session.runEvents(latest.id));
}
Go
runs, err := session.Runs(ctx)
if err == nil && len(runs) > 0 {
latest := runs[len(runs)-1]
snapshot, snapshotErr := session.RunSnapshot(ctx, latest.ID)
events, eventsErr := session.RunEvents(ctx, latest.ID)
_, _, _ = snapshot, snapshotErr, eventsErr
_ = events
}

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:

TypeScript
const current = await session.currentRun();
if (current?.id && current.status === 'running') {
await session.cancelRun(current.id);
}

Agent Definitions

sessionForAgent() applies a named agent definition from built-in agents, .a3s/agents, or configured agentDirs.

TypeScript
const session = agent.sessionForAgent('/repo', 'explore', ['./agents'], {
planningMode: 'auto',
});
Go
session, err := agent.SessionForAgent(
ctx,
"/repo",
"explore",
[]string{"./agents"},
&code.SessionOptions{PlanningMode: code.PlanningAuto},
)

For disposable value-defined workers, use SessionForWorker; both methods return the regular Go Session API.