This document defines the long-lived SDK API shape for A3S Code. The goal is to keep the public SDK stable while the runtime, host integrations, and tool implementations continue to evolve.
- Keep the kernel small: sessions run prompts, stream events, execute selected tools, record evidence, and expose state.
- Keep extension policy outside the kernel: host hooks, UIs, and callers own background advice, context supplements, and proposed programmatic actions.
- Prefer object-shaped APIs for anything that can grow.
- Keep public method names short. Prefer one-word verbs (
send,stream,task,tasks,git) when the receiver already provides the domain. - Preserve compatibility by adding new stable entry points before deprecating old shortcuts.
- Keep raw JSON at system boundaries only; expose typed options and typed snapshots everywhere SDK users build product logic.
Agent owns configuration and creates sessions.
Stable calls:
Agent.create(config_source)
Rust:
Agent.session_builder(workspace).options(options).build().await
Agent.session_async(workspace, options?)
Agent.resume_session_async(session_id, options)
Agent.session_for_agent_async(workspace, definition, options?)
Agent.session_for_worker_async(workspace, worker_spec, options?)
Node / Python facades:
Agent.session(workspace, options?)
Agent.resume_session(session_id, options)
Agent.session_for_agent(workspace, agent_name, agent_dirs?, options?)
Agent.session_for_worker(workspace, worker_spec, options?)
Rules:
- Rust construction is async-first.
SessionBuilder::buildand the async factories resolve configuration, initialize resources, and call one session construction kernel. - The synchronous Rust
session(...)factory is compatibility-only. It never starts or blocks an async runtime, requires an explicitly pre-initialized memory store, accepts only other already-ready resources, and returnsAsyncSessionBuildRequiredwhen configuration still requires asynchronous initialization. A manager supplied throughSessionOptions::with_mcpalways requires the async path for capability discovery; only already-cached agent-global MCP tools can be inherited synchronously. - Node and Python keep their established factory names and delegate to the async core construction path inside their native bindings.
- Worker/subagent setup is data-driven with
WorkerAgentSpec; callers should not create temporary agent files for ephemeral workers. - Model, memory, session store, security, queue, MCP, and worker choices are
typed objects in
SessionOptions, not magic backend strings.
AgentSession / Session is the primary runtime handle.
Stable calls:
send(prompt | { prompt, history?, attachments? }, history?)
run(prompt | { prompt, history?, attachments? }, history?)
stream(prompt | { prompt, history?, attachments? }, history?)
history()
close()/cancel()
Rules:
sendandstreamremain prompt-first because prompt is the dominant input.- Any future optional argument must be added through the request object, not a new positional parameter.
send_request,stream_request, and attachment-specific methods remain compatibility helpers; new examples should usesend({ ... })andstream({ ... }).- Supplying explicit history keeps the operation isolated from session history.
- Conversation operations are fail-fast single-flight per session.
send,stream, attachment variants, slash commands, and run resumption returnSessionBusywhen another operation is active; they are not queued. - Session closure is a gateway-wide terminal state. Conversation calls, direct
tools (including event-streaming tool calls), child execution, and live
capability mutation must return
SessionClosedbefore starting new work. - Immediate local capability changes (dynamic tools, workers/agents, hooks,
slash commands, and runtime budget guards) share a close admission gate.
Async lane/MCP mutation uses the async extension gate. Each mutation either
linearizes before close or reports
SessionClosed; it cannot report a cross-boundary capability update as successful. - A stream owns admission until its producer finishes, not merely until the public stream handle is dropped.
Direct calls bypass model tool selection and are for deterministic product workflows. A selected tool may still use the configured LLM internally.
Stable calls:
tool(name, args)
program(options)
task(options)
tasks(tasks)
git(options)
web_search(options)
read_file(path, options?)
bash(command)
grep(pattern)
glob(pattern)
Rules:
tool(name, args)is the raw escape hatch.program,task,tasks,git,web_search, and MCP APIs are object-shaped because their schemas can grow.- Simple scalar helpers (
bash,grep,glob) may stay scalar as convenience wrappers; richer variants should be added as object-shaped methods instead of widening the scalar signatures. read_filestays path-first for convenience but must expose the underlyingreadtool'soffset/limitline window so large files can be paged without falling back totool("read", ...).- Direct calls carry the explicit trusted host-control-plane origin. They skip model-facing permission/HITL decisions but still pass through pre/post hooks, budget, queue/timeout handling, cancellation, recursion protection, and output sanitization. The embedding application owns end-user authorization.
- When output sanitization is enabled, delta events are sanitized over the complete consumer-visible concatenation domain rather than independently per provider chunk. Text/reasoning may therefore arrive at the run boundary, and tool input/output deltas at their corresponding tool boundary. This is an intentional security/latency tradeoff: chunk boundaries must not be usable to reconstruct a secret. An invocation that buffers more than 8 MiB of delta data drops those deltas fail-closed.
- Programmatic Tool Calling scripts are never auto-executed from runtime
suggestions. Agent-selected programs use the full model policy; direct
program(...)calls and their nested tools inherit the trusted host policy.
Product UIs should build from replayable state rather than parsing text.
Stable calls:
runs()
run_snapshot(run_id)
run_events(run_id)
run_event_page(run_id, after_sequence?, limit?)
current_run()
active_tools()
trace_events()
verification_reports()
verification_summary()
verification_summary_text()
pending_confirmations()
confirm_tool_use(tool_id, approved, reason?)
Rules:
- Run snapshots and event records are durable data contracts.
- SDK stream events use
EventEnvelopeV1 { version, type, payload, metadata }.typeis an open string, and unknown future events must retain their complete payload and metadata. SDK convenience fields are derived from one core projection rather than independent language-specific event matches. - Active tool snapshots are live state for UX only; run events remain the audit source of truth.
- Cursor pages report
retention_gapwhen the requested sequence predates the retained FIFO window. Clients must not interpret such a page as complete history. This flag describes retention only; transport-loss gaps require a future sequenced event transport. - Verification reports are explicit evidence and should not be inferred from the final answer text.
Live extensions mutate session capabilities while keeping one execution path.
Stable calls:
add_mcp(config)
remove_mcp(name)
mcps()
register_agent_dir(path)
register_worker_agent(worker_spec)
register_worker_agents(worker_specs)
register_dynamic_workflow_runtime()
unregister_dynamic_tool(name)
register_hook(name, event_type, handler, matcher?, config?)
unregister_hook(hook_id)
set_budget_guard(guard?)
Rules:
- Prefer
add_mcp(config)over positional MCP overloads. - MCP tools join the normal tool registry and go through the same selection, permission, confirmation, trace, and hook paths as built-in tools.
- Agent-global and host-supplied MCP managers are inherited capability sources, never mutation targets for a session. Every session owns a private live manager; add/remove affects only that session, and delegated children inherit the ordered sources without taking ownership.
- Visible workers are listed by canonical name and purpose in the live
task/parallel_tasktool definitions. Registration, replacement, and removal are reflected on the next run; hidden workers remain callable only when the caller already knows their name. - Dynamic workflow registration exposes the Rust core's A3S Flow-backed
dynamic_workflowtool without requiring SDK callers to construct Rust trait objects. Arbitrary host-native dynamic tools remain Rust-only unless a typed SDK-safe provider shape is added. - Runtime budget guards can be installed after session creation because JS/Python
callbacks cannot always live inside value-typed
SessionOptions. - Hooks supervise the existing agent loop; they do not create a second in-core advisor runtime.
Session persistence publishes one versioned aggregate generation.
Stable core types and calls:
SessionSnapshotV1
SessionStore.save_snapshot(snapshot)
SessionStore.load_snapshot(session_id)
SessionStore.capabilities()
Rules:
- A snapshot contains
SessionData, artifacts, traces, run records, verification reports, and delegated-task snapshots from one save operation. - File and memory stores publish the aggregate atomically. Historical bare session data and fragments remain readable for migration.
- Custom stores must implement aggregate save explicitly. The default method errors; it must never acknowledge a fragmented or no-op save as successful.
- Schema validation happens before any persisted state is restored.
FileSessionStorecaps each JSON document at 256 MiB on both read and write; oversized files are rejected before their contents are allocated.- Session run/event retention is re-applied during restore. Per-run event count
and serialized-byte caps share the same FIFO policy; the default byte budget
is 8 MiB. Trimming never rewrites cumulative
event_countor the surviving event sequence values.
- Rust: snake_case methods and Rust structs.
- Python: snake_case methods, Python dictionaries for extension payloads, typed classes for durable option providers.
- Node.js: camelCase methods and typed option objects.
- Event
typevalues stay snake_case because they are serialized runtime data. - Public methods should be as short as clarity allows. If the receiver already
names the domain, avoid repeating it (
session.git(...), notsession.git_command(...);session.add_mcp(...), notsession.add_mcp_server_config(...)).
- Add object-shaped replacements before removing positional overloads.
- Keep old overloads documented as compatibility paths only.
- Do not reuse a method name with incompatible semantics.
- Do not expose a background runtime concept that duplicates caller-owned control-plane behavior.
- Do not add model-visible shortcuts that bypass
task,parallel_task,program, permissions, confirmations, or trace recording.
Prefer this shape for new public calls:
await session.send({ prompt: 'Explain auth', history, attachments })
await session.git({ command: 'worktree', subcommand: 'list' })
await session.addMcp({
name: 'github',
transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] },
timeoutMs: 30000,
})
await session.program({ source, inputs, allowedTools, limits })
await session.task({ agent: 'explore', description: 'Find auth', prompt: 'Summarize auth files.' })Avoid this shape for new calls:
await session.sendWithAttachments('Explain auth', attachments, history)
await session.git('worktree', 'list', null, null, true, null, false, null, null, false)
await session.addMcpServer('github', 'stdio', 'npx', ['-y', '@modelcontextprotocol/server-github'])The positional forms may remain for compatibility, but new SDK examples should use object-shaped methods.