API Contract
The Agent, session, tool, memory, skill, run, queue, and MCP sections below
describe the Node SDK behavior asserted by scripts/docs_api_contract_smoke.mjs.
The script starts a temporary OpenAI-compatible local test server, creates real
SDK sessions, calls the native binding, and asserts the returned values. It
does not require a docs build. The cluster-grade extension points at the end of
the page are described from the core and SDK sources; the smoke script does not
exercise them.
Run the contract from the repository root:
Agent
Verified entry points:
Agent.create() accepts ACL source text or an .acl file path; it does not
parse JSON. Hosts that already hold a validated config object can call
Agent.createFromConfig(config) with the typed JSON form of CodeConfig. The
integration check covers both apiKey/baseUrl and api_key/base_url
provider aliases against the local OpenAI-compatible test server.
sessionForAgent() was verified with the built-in explore agent.
session(), resumeSession(), and sessionForAgent() are synchronous at the
JavaScript surface and block the event loop while the native side runs the core
async session construction path. They are marked deprecated; new code should
use sessionAsync(), resumeSessionAsync(), and sessionForAgentAsync(),
which take the same arguments and return a Promise<Session>. Rust embedders
should use SessionBuilder::build().await or the async factories; the
synchronous Rust compatibility factory requires an explicit pre-initialized
memory store.
The integration check also covers the ACL fields that create a file-backed session store:
storage_url is only connection metadata for storage_backend = "custom",
where the host supplies its own SessionStore. It is not the local
file-session persistence path; use sessions_dir in ACL or pass sessionStore
in SDK options.
Session Options
The integration check covers this option shape:
model is a per-session override. The check verifies that a session created
with model: 'openai/docs-alt' sends docs-alt to the local provider.
With confirmationPolicy.enabled, a model tool call that needs approval parks
the turn until the host answers it with confirmToolUse(). On the fact-log
coding path the parked confirmation waits for that answer; no timer settles it.
Basic session accessors are verified:
workspace is returned as the SDK's canonical workspace path.
planningMode accepts 'auto', 'enabled', and 'disabled'.
The check also verifies that this permissionPolicy shape is accepted at
session creation:
Prompt slot options are strings:
Result Shape
session.send() returns AgentResult fields on the result object itself:
send() and stream() also accept a SessionRequestOptions object
({ prompt, history?, attachments? }) in place of the prompt string. Trace
events and verification reports are session APIs, not fields on AgentResult.
Streaming
session.stream() returns an EventStream. The integration check consumes it
with .next():
The package entry point also installs Symbol.asyncIterator on EventStream,
so for await (const event of stream) iterates the same events.
The smoke check starts another send() immediately after this loop. Exhaustion
therefore verifies both event delivery and release of the stream's single-flight
admission lease; no retry delay is required.
Each SDK event is an envelope-v1 projection with version === 1, an open
type string, a complete payload, and optional metadata. Consumers must
retain a default branch for future event types. Node exposes payloadJson and
metadataJson string views; Python exposes payload_json and metadata_json
and retains event_type as an alias for type.
Direct Tools
Full guide: Tools.
The integration check covers these host-driven direct calls:
readFile() options are a 0-indexed line offset and a line limit.
git() also accepts a GitCommandOptions object, which avoids the positional
form: session.git({ command: 'log', maxCount: 5 }).
The verified local-workspace toolNames() set includes read, write, edit,
patch, search, ls, bash, task, search_skills, Skill, program,
git, batch, web_fetch, and web_search. The check also asserts that
parallel_task is absent from both toolNames() and toolDefinitions(), and
that the session has no parallelTask helper.
Direct host calls are trusted host operations: they skip the permission and
HITL gates that apply to model tool calls, although a pre_tool_use hook can
still deny them. Gate them in the host application
before exposing them to end users, or use session.governedTool(name, args),
which runs a tool without an LLM while keeping the session's permission and
HITL gates.
download is registered only for writable local workspaces and is invoked
through the generic direct-tool API:
Only url is required. connections is limited to 1–4, max_bytes defaults
to 512 MiB with an 8 GiB maximum, timeout defaults to 300 seconds with a
3600-second maximum, and expected_sha256 must contain exactly 64 hexadecimal
characters. file_path is workspace-relative and may be omitted for safe
filename inference; overwrite defaults to false.
Model-driven calls remain permission- and HITL-governed workspace mutations. The transfer validates redirects and DNS targets against SSRF, verifies strict Range responses, retries only within fixed bounds, falls back to a sequential transfer when needed, and promotes a temporary file only after completion and optional digest verification.
AGENTS.md
The script writes an AGENTS.md file in the workspace and asserts that its
instruction token appears in the local provider request body:
Keep project instructions operational and free of secrets.
Programmatic Tool Calling
session.program() runs bounded JavaScript in the embedded QuickJS runtime:
The check exercises the ctx helpers readFile, read, search, ls,
bash, git, and the generic tool(name, args). The runtime also defines
tools, grep, glob, bm25, webSearch, and verify; grep and glob
call the search tool. ctx.git() takes an argument object such as
{ command: 'status' }. fetch, WebSocket, and Worker are not available
inside the script.
allowedTools limits which registered tools the script may call and defaults
to every registered tool. A search call is also allowed when its mode
(grep or glob) is listed. program, dynamic_workflow, and
parallel_task are always removed from the script's tool set, and a task
call with more than one entry in tasks is refused. Default limits are a
30-second timeout (600 seconds when task is allowed), 20 tool calls, and
64 KiB of output.
Verification
Full guide: Verification.
Verification is session-scoped:
Memory
Full guide: Memory.
Node memory was verified with FileMemoryStore:
The recent-memory method is memoryRecent(). The check asserts that
recallRecent() is not present on the Node SDK surface.
Skills
Full guide: Skills.
File-backed and inline skills are verified through search_skills:
The skill-file check uses Markdown with YAML frontmatter and the
allowed-tools key.
Side Questions
Full guide: Sessions.
The SDK surface has no dedicated ephemeral-question helper. Pass explicit history when the call must not mutate the session transcript:
Runs And Cancellation
Full guide: Sessions.
Each send() or stream() records replayable run state:
cancelRun() returns false for an id that is not the active run.
Headless hosts can admit exact immutable run identities and receive the authoritative snapshot without waiting for completion:
Repeating compatible immutable input returns replayed: true without starting
duplicate work. Conflicting input fails with the RUN_IDENTITY_CONFLICT error
code; closing the session cancels detached workers.
currentRun() is for the current operation. When idle, it may return
null or a retained snapshot depending on the preceding control flow. Use
runs() for completed history.
Transcript-affecting operations are single-flight per session. An overlapping
send, stream, attachment call, slash command, or resumeRun fails immediately
with SessionBusy (SESSION_BUSY); it is not queued. A stream retains
admission until its producer has stopped, even when its public handle is
dropped.
Persistence
Full guide: Persistence and Sessions.
File-backed session persistence was verified with stable sessionId,
autoSave, explicit save(), and resumeSession():
Core persistence commits the conversation and its artifacts, traces, run
records, verification reports, and subagent task snapshots together as one
versioned SessionSnapshotV1. File and memory stores publish that aggregate
atomically. Older fragmented records remain loadable, while custom stores must
implement aggregate save explicitly. The fact log under .a3s/effect-log/ in
the workspace is the control source for coding turns and is not part of the
snapshot.
Close a session when the process should release its background resources.
session.close() blocks until cleanup finishes and is deprecated in favor of
await session.closeAsync(). Closing marks the session closed
(session.isClosed() returns true, and further send / stream calls fail
with Session '<id>' is closed), cancels the active run, running subagent
tasks, and pending HITL confirmations, drains the lane queue when one is
configured, and disconnects MCP servers the session added itself. Repeated
calls are no-ops.
For control-plane callers that only know the session ID, the same cleanup is reachable from the agent:
After agent.close(), agent.session(...) and agent.resumeSession(...)
fail with a session-closed error. agent.close() is idempotent. Use it in
process-shutdown handlers so no session-scoped workers outlive the agent.
Delegation
Full guide: Tasks and Orchestration.
The direct helpers for the task tool were verified:
Both helpers return a ToolResult named task. tasks() runs the entries
concurrently through the same tool. delegateTask() is an alias of task(),
and DelegateTaskOptions also accepts background.
Hooks
Full guide: Hooks.
The verified hook management surface is:
The matcher also accepts pathPattern, sessionId, and skill. The config
defaults are priority 100 (lower runs first) and a 30000 ms timeout. The check
covers registration and removal only; validate the specific event path you
depend on before using hook behavior as a production enforcement gate.
Slash Commands
Full guide: Commands.
Custom slash commands are invoked through session.send():
The handler context also carries model, historyLen, totalTokens,
totalCost, and toolNames.
Lane Queue
Full guide: Lane Queue.
Queue infrastructure is opt-in:
completeExternalTask() returns false for an unknown task id. Ordinary
sessions are queue-free unless queueConfig (or an agent-level queue ACL
block) is provided. The queue schedules host direct tool calls; model tool
calls on the fact-log coding path run directly and do not enter it.
MCP
Full guide: MCP. Idle disconnect: Cluster Extension Points.
The integration check covers a live stdio MCP server:
Tools from the server are named mcp__<server>__<tool>. addMcp(), mcps(),
and removeMcp() are the compact forms of addMcpServerConfig(),
mcpStatus(), and removeMcpServer(); the positional addMcpServer(...)
overload also remains.
Live add/remove operations target a private manager owned by this session. Adding a server name that is already registered in the session fails. Agent-global and host-supplied managers are inherited read-only capability sources, so a session cannot mutate a sibling or global MCP configuration.
Cluster-grade extension points
Full guide: Cluster Extension Points (identity labels, budget guard, cluster events, deterministic IDs/replay, loop checkpoints, retention caps).
These contracts let a cluster control plane wire multi-tenancy, cost governance, and crash-tolerant runs without forking the framework. The framework defines decision points and emits structured events; the host supplies the policy implementations.
Identity labels
Four optional SessionOptions slots are propagated through hooks,
traces, and SessionData but never interpreted by the framework:
On resume, persisted labels are restored for any slot the caller leaves unset; labels passed in the resume options take precedence, so a host can relabel a session.
Budget / cost guard
BudgetGuard has three methods, each defaulting to allow or no-op:
check_before_llm, record_after_llm, and check_before_tool. A Deny
decision refuses the call with the message
Budget exhausted on '<resource>': <reason>: a denied model request fails with
CodeError::BudgetExhausted (BUDGET_EXHAUSTED), and a denied tool call
returns a denied tool result carrying that message. A SoftLimit decision lets
the call proceed.
Where the guard is consulted depends on the execution path:
- Fact-log coding turns call
check_before_llmbefore each model request, with an estimated token count of 0. They do not callrecord_after_llm, do not emit budget events, and do not callcheck_before_toolfor model tool calls. - Agent-loop paths (child runs such as
taskandSkill, host direct tools, and session verification) callcheck_before_llmbefore andrecord_after_llmafter each provider call, andcheck_before_toolbefore each tool call. On these paths aSoftLimitemitsAgentEvent::BudgetThresholdHit { kind: "soft", .. }and aDenyemits one withkind: "hard"before failing.
Rust hosts inject the trait directly. Node.js, Python, and Go expose callback bridges later in this section:
Cluster event vocabulary
AgentEvent (non-exhaustive) carries platform-level events:
BudgetThresholdHit { resource, kind, consumed, limit, message? }PassivationRequested { reason, deadline_ms? }PeerInvocation { from_session_id, from_tenant_id?, correlation_id? }
The host emits these through HookExecutor. The agent loop itself produces
only BudgetThresholdHit, from the budget checks described above. In-session
hooks subscribe to these events to react uniformly regardless of how the
host's transport delivers them.
Deterministic IDs / time
HostEnv { id_generator, clock } replaces the default
uuid::Uuid::new_v4() + wall-clock pair. Replay tooling configures
SequentialIdGenerator + FixedClock to recreate a run bit-identical
on another node.
Loop checkpoints + run resumption
When a SessionStore is configured, the session persists a LoopCheckpoint
after each tool result that lands on the fact log, keyed by run_id, and
deletes it when the run reaches a terminal state in-process. Any node holding
the same store can recover a crashed run: the checkpoint seeds a fresh
fact-log thread, and folding that log chooses the next step. The checkpoint
never chooses the next model call itself.
The resumed work is recorded as a new run; the checkpoint's run is not
modified. Without a checkpoint, resumeRun folds the session's existing fact
log when it has facts (a quiescent log takes no step). The two errors below
fire only when the log is also empty:
"resume_run requires a session_store on this session": the host should fall back to a fresh session."no loop checkpoint found for run 'X'": the run never wrote a checkpoint, or the checkpoint was deleted when the run ended.
A tool call whose result is missing from the log runs once on resume, so
hosts should keep non-idempotent tools behind confirmation or idempotency
keys. For an exact, replay-safe recovery id use
spawnRecoveryWithRunId(checkpointRunId, runId).
Retention caps for long-running sessions
SessionRetentionLimits caps the in-memory stores that grow with session age:
run records, per-run event buffers (by count and by serialized bytes), trace
events, and terminal subagent task snapshots. The finite defaults are 64 runs,
2,048 events and 8 MiB of events per run, 8,192 trace events, and 512 terminal
subagent tasks. SessionRetentionLimits::unbounded() (or unbounded: true in
the SDKs) deliberately restores unlimited retention. Eviction is FIFO and never
returns an error; running subagent tasks are never dropped, and a run's
cumulative event count is not decremented.
Pick caps from the same observability budget that caps the rest of the host's
in-memory state. Node exposes this as retentionLimits
(maxRunsRetained, maxEventsPerRun, maxEventBytesPerRun,
maxTraceEvents, maxTerminalSubagentTasks, unbounded); Python as
opts.retention_limits; Go as SessionOptions.RetentionLimits.
MCP idle disconnect
Agent::disconnect_idle_mcp(threshold_ms) disconnects agent-global MCP servers
whose last activity is older than now - threshold_ms and returns their names.
Servers with no recorded activity count as idle. Session-local servers added
with addMcp() are not affected.
The server's registered config stays, but the connection does not come back on
its own: a tool call on a disconnected server fails with
MCP server not connected: <name>. Reconnect by calling
agent.syncGlobalMcpServers(configs), which connects every enabled server that
is not connected, then session.republishInheritedMcpTools() on live sessions.
Activity is stamped on connect and at the start of every tool call. Rust hosts
that route tool traffic through a side channel can call
McpManager::touch(name) to keep a server warm.
BudgetGuard SDK bridges
All callback-capable SDKs accept the same decision shape:
Missing methods on the guard object are treated as allow or no-op. In all three
SDKs, a check callback that throws, returns a malformed decision, or does not
return within the timeout (default 5 seconds) is treated as a deny.
recordAfterLlm failures are ignored.
Node callbacks receive a single context object ({ sessionId, estimatedTokens },
{ sessionId, usage }, or { sessionId, toolName }) and accept an optional
timeoutMs. Python guard methods take positional arguments and can also be
installed later with session.set_budget_guard(guard, timeout_ms). Go handlers
receive typed contexts, and BudgetGuardHandlers.Timeout sets the timeout.
To clear, pass null to session.setBudgetGuard (Node), call
session.set_budget_guard(None) (Python), or call
session.SetBudgetGuard(ctx, nil) (Go).