API 契约

本页只记录 crates/code/scripts/docs_api_contract_smoke.mjs 覆盖过的 A3S Code Node SDK 行为。该脚本会启动一个临时的 OpenAI-compatible 本地测试服务,创建真实 SDK session,调用 native binding, 并断言返回值。不需要 docs build。

在 crates/code 下运行:

SHELLSCRIPT
node scripts/docs_api_contract_smoke.mjs

Agent

已验证入口:

TypeScript
const agent = await Agent.create(aclSource);
await agent.refreshMcpTools();
const session = agent.session(workspace, options);
const named = agent.sessionForAgent(workspace, 'explore', [], options);

Agent.create() 接受 ACL source text 或 .acl 文件路径。JSON config 不在 已验证契约内。集成检查覆盖 apiKey/baseUrl 和 api_key/base_url 两组 provider alias。sessionForAgent() 已用内置 explore agent 验证。

Node factory 在 JavaScript surface 保持同步命名,但 native implementation 会把 resource resolution 委托给 core 的异步 session construction path。Rust embedding 应使用 SessionBuilder::build().await 或异步 factory;同步 Rust compatibility factory 要求显式传入预初始化 memory store。

集成检查也覆盖会创建文件型 session store 的 ACL 字段:

Text
storage_backend = "file"
sessions_dir = "/tmp/a3s-doc-stores/acl-storage"

本契约不覆盖 storage_url。它不是本地文件型 session persistence 路径; ACL 中使用 sessions_dir,或在 SDK options 中传 sessionStore。

Session Options

已验证的 option 形状:

model 是 per-session override。检查已覆盖使用 model: 'openai/docs-alt' 创建的 session 会把 docs-alt 发送给本地 provider。

基础 session accessor 已验证:

TypeScript
console.log(session.sessionId);
console.log(session.workspace);
console.log(session.initWarning);
console.log(session.history());
console.log(session.cancel());

workspace 返回 SDK canonical workspace path。

planningMode 使用显式三态:'auto'、'enabled'、'disabled'。

检查也覆盖 session 创建时接受这个 permissionPolicy 形状:

TypeScript
agent.session(workspace, {
permissionPolicy: {
deny: ['write(**/.env*)', 'bash(rm -rf*)'],
ask: ['bash(git push*)', 'bash(npm publish*)'],
allow: ['read(*)', 'grep(*)', 'glob(*)', 'bash(npm run build*)'],
defaultDecision: 'ask',
enabled: true,
},
});

Prompt slot options 是字符串:

TypeScript
agent.session(workspace, {
role: 'release-readiness reviewer',
guidelines:
'Find blockers before improvements. Require command evidence for done claims.',
responseStyle: 'concise, findings first',
goalTracking: true,
});

Result Shape

session.send() 返回的 AgentResult 字段在 result 对象自身:

TypeScript
const result = await session.send('Return a short answer');
console.log(result.text);
console.log(result.toolCallsCount);
console.log(result.promptTokens);
console.log(result.completionTokens);
console.log(result.totalTokens);
console.log(result.verificationStatus);
console.log(result.pendingVerificationCount);
console.log(result.failedVerificationCount);
console.log(result.verificationReportCount);
console.log(result.verificationSummaryJson);
console.log(result.verificationSummaryText);

trace events 和 verification reports 是 session API,不是 AgentResult 字段。

Streaming

session.stream() 返回 EventStream。已验证的读取方式是 .next():

TypeScript
const stream = await session.stream('Stream one sentence');
while (true) {
const { value: event, done } = await stream.next();
if (done) break;
if (!event) continue;
if (event.text) process.stdout.write(event.text);
}

Smoke check 会在循环结束后立即发起下一次 send()。因此这里验证的不只是 event delivery,还包括 stream single-flight admission lease 已释放;不需要人为增加重试 延时。

每个 SDK 事件都是 envelope v1 投影:version === 1、开放的 type 字符串、 完整的 payload 和可选 metadata。消费者必须为未来事件类型保留默认分支。 Node 提供 payloadJson / metadataJson 字符串视图;Python 提供 payload_json / metadata_json,并保留 event_type 作为 type 的别名。

不要依赖 for await,除非你安装的 SDK 版本已经单独验证支持 async iteration。

Direct Tools

完整指南:工具。

已验证的宿主侧直接工具调用:

TypeScript
await session.readFile('README.md');
await session.glob('src/*.rs');
await session.grep('PermissionPolicy');
await session.bash('printf docs-bash');
await session.tool('read', { file_path: 'README.md' });
await session.git('status');
await session.git('diff');
await session.git(
'log',
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
5,
);
await session.tool('search_skills', { query: 'release blockers', limit: 5 });
session.toolNames();
session.toolDefinitions();
session.registerAgentDir(path.join(workspace, 'agents'));

已验证的 toolNames() 集合包含 read、write、edit、patch、grep、 glob、ls、bash、task、parallel_task、search_skills、Skill、 program、git、batch、web_fetch 和 web_search。

直接工具调用是宿主侧特权能力。把它暴露给最终用户之前,应在宿主应用内做权限判断。

AGENTS.md

脚本会在 workspace 写入一个 AGENTS.md,并断言其中的 instruction token 出现在本地 provider request body 中:

Markdown
# Project Instructions
Always mention docs-contract-agents-md-token when asked for project instructions.

项目指令应保持可操作,并且不要包含 secret。

Programmatic Tool Calling

session.program() 在内嵌 QuickJS runtime 中运行有边界的 JavaScript:

TypeScript
const result = await session.program({
source: `
export default async function run(ctx, inputs) {
const text = await ctx.readFile('README.md');
const hits = await ctx.grep(inputs.q, { glob: '*.md' });
return { summary: 'ok', hasHits: text.includes(inputs.q) && hits.includes(inputs.q) };
}
`,
inputs: { q: 'planningMode' },
allowedTools: ['read', 'grep'],
limits: { timeoutMs: 30000, maxToolCalls: 12, maxOutputBytes: 65536 },
});
const meta = JSON.parse(result.metadataJson);
console.log(meta.script_result);
console.log(meta.program.tool_calls);

已验证的 ctx helper:readFile、read、grep、glob、ls、bash、 git 和通用 tool(name, args)。allowedTools 限制脚本能调用的已注册 tool。program 不会出现在它自己的默认 tool set 里。

Verification

完整指南:验证。

验证信息是 session 级能力:

TypeScript
const report = await session.verifyCommands('docs api check', [
{
id: 'echo',
kind: 'command',
description: 'echo works',
command: 'printf verify',
required: true,
},
]);
console.log(report.subject);
console.log(session.verificationReports());
console.log(session.verificationSummary());
console.log(session.verificationSummaryText());
console.log(session.verificationPresets());
console.log(formatVerificationSummary(session.verificationSummary()));

Memory

完整指南:记忆。

Node memory 已用 FileMemoryStore 验证:

TypeScript
const session = agent.session(workspace, {
memoryStore: new FileMemoryStore(memoryDir),
});
console.log(session.hasMemory);
await session.rememberSuccess('docs memory success', ['grep'], 'remembered');
await session.rememberFailure('docs memory failure', 'expected failure', [
'bash',
]);
await session.memoryRecent(10);
await session.recallSimilar('docs memory', 5);
await session.recallByTags(['grep'], 10);

当前 Node SDK 已验证的 recent-memory 方法是 memoryRecent()。recallRecent() 不存在于当前 Node SDK surface。

Skills

完整指南:Skills。

文件型和 inline skills 已通过 search_skills 验证:

TypeScript
const session = agent.session(workspace, {
skillDirs: [path.join(workspace, 'skills')],
inlineSkills: [
{
name: 'strict-release-review',
kind: 'instruction',
content: 'Always separate blockers from nice-to-have improvements.',
},
],
});
await session.tool('search_skills', { query: 'release blockers', limit: 5 });
await session.tool('search_skills', {
query: 'strict release review',
limit: 5,
});

skill-file 检查使用带 YAML frontmatter 的 Markdown,并覆盖了 allowed-tools key。

Side Questions

完整指南:会话。

当前 SDK surface 没有专用的临时提问 helper。需要不改变 session transcript 时,使用显式 history:

TypeScript
const snapshot = session.history();
const side = await session.send('What is this test?', snapshot);
console.log(side.text);
console.log(session.history().length === snapshot.length);

Runs And Cancellation

完整指南:会话。

每次 send() 或 stream() 都会记录可回放的 run state:

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));
}
const current = await session.currentRun();
if (current?.id && current.status === 'running') {
await session.cancelRun(current.id);
}
console.log(session.traceEvents());

currentRun() 用于读取当前操作。空闲时,它可能返回 null,也可能 因前序控制流保留一个 snapshot。已完成历史请使用 runs()。

同一个 session 的 transcript-affecting operation 使用 single-flight。重叠的 send、 stream、attachment call、slash command 或 resumeRun 会立即返回 SessionBusy, 不会排队。即使公开 handle 被丢弃,stream 也会持有 admission,直到 producer 停止。

Persistence

完整指南:持久化与会话。

文件型 session persistence 已验证稳定 sessionId、autoSave、显式 save() 和 resumeSession():

TypeScript
const session = agent.session(workspace, {
sessionStore: new FileSessionStore(sessionDir),
sessionId: 'docs-contract',
autoSave: true,
});
await session.save();
const resumed = agent.resumeSession('docs-contract', {
sessionStore: new FileSessionStore(sessionDir),
});
console.log(resumed.history());

Core persistence 会把 conversation、artifact、trace、run record、verification report 与 subagent task snapshot 一起提交为一个有版本的 SessionSnapshotV1。 File/memory store 原子发布这个 aggregate。Legacy fragmented record 仍可加载; 自定义 store 必须显式实现 aggregate save。

Node 进程需要及时释放 session 级后台资源时,调用 session.close()。close() 是完整的优雅停止入口:把 session.isClosed 翻成 true(之后 send / stream 会以 Session closed 错误立即返回),fire session 级 CancellationToken 让所有 in-flight run、委派子代理任务和 HITL 待确认全部中止。重复调用 close() 是 no-op。

控制面只持有 session ID 时,可以从 Agent 侧触发同样的清理:

TypeScript
await agent.listSessions(); // ['session-a', 'session-b']
await agent.closeSession('session-a'); // 若原本是 open,返回 true
await agent.close(); // 关闭所有活 session + 断开全局 MCP

agent.close() 之后,再调 agent.session(...) / agent.resumeSession(...) 会立即抛 Session closed。幂等。建议在进程退出 handler 中调用,保证没有 session 级 worker 比 agent 活得更久。

Delegation

完整指南:任务与编排。

已验证核心委派工具的直接 helper:

TypeScript
await session.task({
agent: 'general',
description: 'docs delegated check',
prompt: 'Return a short response.',
maxSteps: 1,
});
await session.tasks([
{
agent: 'general',
description: 'one',
prompt: 'Return one response.',
maxSteps: 1,
},
{
agent: 'general',
description: 'two',
prompt: 'Return another response.',
maxSteps: 1,
},
]);

它们返回来自 task 和 parallel_task 的 ToolResult。

Hooks

完整指南:Hooks。

已验证的 hook 管理面:

TypeScript
session.registerHook(
'docs-observer',
'pre_tool_use',
{ tool: 'bash' },
{ priority: 1, timeoutMs: 1000 },
() => ({ action: 'continue' }),
);
console.log(session.hookCount());
session.unregisterHook('docs-observer');

把 hook 行为作为生产关卡前,需要对你依赖的具体 event path 做集成测试。

Slash Commands

完整指南:命令。

自定义 slash command 通过 session.send() 触发:

TypeScript
session.registerCommand(
'docs_status',
'Return docs command status',
(args, ctx) => {
return `status args=${args}; session=${ctx.sessionId}; workspace=${ctx.workspace}`;
},
);
console.log(session.listCommands());
const result = await session.send('/docs_status check');
console.log(result.text);

Lane Queue

完整指南:Lane 队列。

Queue infrastructure 是显式 opt-in:

TypeScript
const queued = agent.session(workspace, {
queueConfig: { enableDlq: true, enableMetrics: true },
});
console.log(queued.hasQueue());
await queued.setLaneHandler('execute', { mode: 'external', timeoutMs: 1000 });
await queued.pendingExternalTasks();
await queued.completeExternalTask('missing', {
success: true,
result: { ok: true },
});
await queued.queueStats();
await queued.queueMetrics();
await queued.deadLetters();

没有传入 queueConfig 的普通 session 不会启用 queue。

MCP

完整指南:MCP。闲置断开见集群扩展点。

集成检查覆盖一个真实 stdio MCP server:

TypeScript
const count = await session.addMcp({
name: 'echo',
transport: {
type: 'stdio',
command: process.execPath,
args: ['tools/mcp_echo_server.mjs', 'example-value'],
},
timeoutMs: 30000,
});
console.log(count);
console.log(await session.mcpStatus());
console.log(
session.toolNames().filter((name) => name.startsWith('mcp__echo__')),
);
await session.tool('mcp__echo__echo', { message: 'docs mcp ok' });
await session.removeMcpServer('echo');

server 注册出的 tool 名称格式是 mcp__<server>__<tool>。 addMcpServer(...) 和 addMcpServerConfig(...) 仍是兼容别名;新示例使用更紧凑的 object-shaped addMcp(...) API。

Live add/remove 只作用于当前 session 私有 manager。Agent-global 和 host-supplied manager 是继承的只读 capability source,因此一个 session 不能修改 sibling 或 global MCP 配置。

集群级扩展点

完整指南:集群扩展点(身份标签、预算守卫、集群事件、确定性 ID/回放、loop checkpoint、保留上限)。

这些契约让集群控制面在不 fork 框架的前提下接入多租户、成本管控和容错运行。框架定义"决策点"和"结构化事件",策略实现由 host 提供。

身份标签

SessionOptions 上四个可选 slot,会透传到 hooks / traces / SessionData,框架本身不解释:

TypeScript
const session = agent.session(workspace, {
tenantId: 'tenant-example',
principal: 'principal-example',
agentTemplateId: 'agent-template-example',
correlationId: 'trace-example',
sessionStore: new FileSessionStore('./sessions'),
});
session.tenantId; // -> 'tenant-example'
session.correlationId; // -> 'trace-example'

resume 时 apply_persisted_runtime_options 会从持久化快照里还原标签;但调用方在 resume_session 时传的 opts 优先,可以借此 relabel。

预算 / 成本守卫

BudgetGuard 会在每个属于 run 的 provider call 前检查,并在成功响应后记录 usage; 每个 governed tool call(包括 nested 与 trusted host-direct call)也会在执行前检查。 Deny 返回 CodeError::BudgetExhausted { resource, reason };SoftLimit 发射 AgentEvent::BudgetThresholdHit { kind: "soft", .. } 后继续执行。

目前仅 Rust 层接入(Node/Python wrapper 后续补):

Rust
let guard: Arc<dyn BudgetGuard> = /* host-supplied impl */;
let opts = SessionOptions::new().with_budget_guard(guard);

集群事件词汇

AgentEvent(#[non_exhaustive])新增三类平台级事件,host 通过 HookExecutor 注入:

  • BudgetThresholdHit { resource, kind, consumed, limit, message? }
  • PassivationRequested { reason, deadline_ms? }
  • PeerInvocation { from_session_id, from_tenant_id?, correlation_id? }

session 内部 hook 可统一订阅,不必关心 host 用什么传输发过来。

确定性 ID / 时钟

HostEnv { id_generator, clock } 替换默认的 uuid::Uuid::new_v4() + 墙上时钟。Replay 工具传入 SequentialIdGenerator + FixedClock 即可在另一台机器上 bit-identical 重放一个 run。

Loop checkpoint + run 恢复

配置了 SessionStore 后,agent loop 每次 tool round 结束会持久化一个 LoopCheckpoint(按 run_id 索引)。任何拥有同一个 store 的节点都能从最近的边界 rehydrate:

TypeScript
// Node — host 探测到 A 节点死掉;在 B 节点上:
const session = agentB.session(workspace, {
sessionStore: new FileSessionStore('./sessions'),
sessionId: 'session-from-node-a',
});
const result = await session.resumeRun('run-id-from-node-a');
Python
# Python 等价
opts = SessionOptions()
opts.session_store = FileSessionStore('./sessions')
opts.session_id = 'session-from-node-a'
session = agent_b.session(workspace, opts)
result = session.resume_run('run-id-from-node-a')

resume 出来的会分配一个全新的 run id — 框架不假装旧 run 还在继续,新旧 run 的关系是 host 的元数据。两个可区分的错误路径方便 host 端调度分支:

  • "resume_run requires a session_store" — host 应该回退到新建 session。
  • "no loop checkpoint found for run 'X'" — host 可以稍等重试(checkpoint 写入竞态),或当 run 已丢失。

边界策略:checkpoint 只在 tool round 之间取,不在工具执行中途取。进程在工具执行中途死掉时,这一轮的工作会丢失,LLM 从前一个边界重新思考。这是用"重试成本"换"正确性" — 把非幂等工具(write、bash)在边界两侧重跑比让 LLM 重想要糟得多。

长跑 session 的保留上限

SessionRetentionLimits 让 host 给四种 in-memory 存储设上限:run 记录、每 run 的事件、trace 事件、终态的 subagent 任务快照。每个字段都是可选的(None 保持原本无上限的默认 — 短 session 没问题,小时/天级的就漏内存)。FIFO 严格按插入序丢;Running 状态的 subagent 任务永不被丢。

Rust
use a3s_code_core::retention::SessionRetentionLimits;
let limits = SessionRetentionLimits::new()
.with_max_runs(100)
.with_max_events_per_run(5_000)
.with_max_trace_events(10_000)
.with_max_terminal_subagent_tasks(1_000);
let opts = SessionOptions::new().with_retention_limits(limits);

上限建议跟 host 自己 Prometheus / 观测系统的内存预算保持一致。Node 暴露为 retentionLimits;Python 暴露为 opts.retention_limits。

MCP 闲置断开

Agent::disconnect_idle_mcp(threshold_ms) 扫描所有已连接的 MCP server,把"最后活跃时间"早于 now - threshold_ms 的全部断开。注册的配置保留 — 后续 tool 调用会按需重连。返回被断开的 server 名称列表。

TypeScript
// Node — 周期回收闲置 MCP 子进程
setInterval(async () => {
const dropped = await agent.disconnectIdleMcp(5 * 60 * 1000); // 5min
if (dropped.length) {
console.log('reaped idle MCP servers:', dropped);
}
}, 60_000);
Python
# Python — 等价
dropped = agent.disconnect_idle_mcp(5 * 60 * 1000)

每次 connect 和成功的 call_tool 都会刷新活跃时间。Host 走旁路通道路由 tool 时,可以手动 McpManager.touch(name) 把 server 保温。

BudgetGuard 的 SDK 桥接

两个 SDK 共用同一个决策返回形状:

返回值效果
None / null / {decision:'allow'}静默放行
{decision:'soft', resource, consumed, limit, message?}发射 BudgetThresholdHit('soft') 事件,继续执行
{decision:'deny', resource, reason}中止调用,Python 抛 RuntimeError("Budget exhausted...")/Node reject 同样的错误

guard 对象上缺失的方法 = 宽松默认(Allow / no-op);callback 抛错 → fallback 到 Allow,异常的 guard 不会拖垮 live session。

Python
# Python — 通过 SessionOptions 在 session 构造前挂上
class MyGuard:
def check_before_llm(self, session_id, estimated_tokens):
return {"decision": "deny", "resource": "llm_tokens", "reason": "cap"}
def record_after_llm(self, session_id, usage):
track(session_id, usage["total_tokens"])
opts = SessionOptions()
opts.budget_guard = MyGuard()
session = agent.session(workspace, opts)
TypeScript
// Node — session 构造后通过 setBudgetGuard 挂上。
// JsFunction 不能塞进值类型的 SessionOptions,所以 guard 在 Session 上注册,
// 下一次 send/stream 生效。
session.setBudgetGuard({
checkBeforeLlm: (ctx) => {
if (overBudget(ctx.sessionId)) {
return { decision: 'deny', resource: 'llm_tokens', reason: 'cap' };
}
return null;
},
recordAfterLlm: (ctx) => {
track(ctx.sessionId, ctx.usage.totalTokens);
},
});

Node 回调接收单个 ctx 对象,且绝不能 throw;请用 try/catch 包裹并返回显式决策。 卡住或无法解析的 check* 回调会 fail-closed 为 deny。Python 回调使用位置参数,异常会被捕获并视为 Allow。

Node 用 setBudgetGuard(null) 清除;Python 把 opts.budget_guard 设回 None 后重建 session。