架构

A3S Code 把配置解析、逐次运行、稳定的线协议契约与持久化分成清晰边界。 SDK 和 9.0.0 终端客户端(a3s-code-tui)使用同一个运行时内核,不存在另一套执行路径。

Text
CodeConfig + SessionOptions
-> 校验并异步解析资源
-> ResolvedSessionConfig
-> AgentSession
-> single-flight run admission
-> FactRun(每次运行一个)
├─ fact log .a3s/effect-log/<thread>.jsonl
├─ fold -> view + next transition
├─ kernel -> permission projection, completion gate
├─ LLM invoker -> provider calls
├─ tool invoker -> model、nested、delegated 与 host-direct calls
└─ events -> EventEnvelopeV1 -> Rust / Node / Python / Go consumers
-> SessionSnapshotV1 -> atomic store generation

事实日志控制

编码循环是对一份只追加事实日志的折叠。除此之外没有任何东西决定下一步: 不是循环检查点,不是内存标志,也不是计时器。每次运行为会话打开一个 FactRun, 通过追加事实并重新折叠日志推进。折叠由 a3s-effect 完成:新的提示经 ingest_coding 进入,resume_coding 则在已有日志上继续 (core/src/fact_control.rs)。

事实日志

会话日志位于 <workspace>/.a3s/effect-log/<thread>.jsonl。当会话 ID 不超过 128 个字符,且只包含 A-Z、a-z、0-9、_、.、: 或 - 时,thread 就是 会话 ID。其他会话 ID 映射为稳定的 s-<hex> thread。

事实类型何时追加
user.message收到提示词或已应用的 steer
model.turn模型返回文本或工具调用
tool.result工具调用完成、被拒绝或被拦截
confirmation.answered宿主批准或拒绝一个停靠的工具调用
question.answered宿主回答一个停靠的 ask_user 问题
compaction.donecompact 部件对较早的事实做了摘要
budget.deniedbudget 部件拒绝再次调用工具

折叠与下一步转移

折叠日志得到一个视图(system 行、消息、工具规格、待确认或待回答的问题、助手文本) 和一个阶段:idle、infer、compact、confirm、question、tool、deny 或 done。阶段就是下一步转移。infer 调用模型,tool 执行已记录的调用, compact 做摘要,done 结束运行。折叠日志的 Actor 由 Meta Harness 组装;没有配方时使用默认内置组件树。

每次 send、stream 或 resume_run 最多执行 32 次转移。达到上限时运行以 StepLimit 错误停止,而不是悄悄继续。当会话从已保存历史恢复到一份空日志时, 之前的回合会记录为 model.turn 事实,因此不会再次发送给模型。已有日志保持不变。

停靠

confirm 和 question 是停靠阶段。折叠在这里停下,等待一个事实。事实日志不会 为停靠的调用设置超时。

  • 权限判定为 Ask 的工具调用停靠在 confirm。实时运行中,会话的确认管理器询问宿主 并发出 confirmation_required。confirm_tool_use(tool_id, approved, reason) 结束停靠。答案以 confirmation.answered 追加,折叠继续。任何超时都属于宿主的 确认策略,而不属于日志。
  • ask_user 调用停靠在 question 并发出 user_question。答案以 question.answered 追加。提问次数超过上限的运行会以 ask_user question cap exceeded 失败。

因为答案是事实,停靠的调用可以跨进程重启保留。重启后,对停靠的工具 ID 调用 confirm_tool_use 会追加答案,折叠从日志继续。

恢复

resume_run 折叠日志并从其阶段继续。如果该运行存在循环检查点,会先用检查点中的 对话记录重建 thread 的日志;检查点提供历史,而不是下一次模型调用。结果缺失的 tool 阶段会把该工具执行一次。静止的日志以零步恢复。没有检查点时,resume_run 折叠已有日志;如果日志为空,会以 resume_run requires a session_store on this session (没有存储)或 no loop checkpoint found for run '<id>' 失败。

Steer

steer 不会启动第二个回合。在下一次服务提供商调用时,运行会清空控制收件箱,把每个 steer 作为 user.message 事实追加,并随该请求一起发送。interrupt 协作式取消; 运行会记录结果并完成清理,然后才进入 cancelled。

工具预算与工具轮次上限

有两个计数器限制工具工作。二者都统计最近一次 user.message 之后的成功工具结果, 因此已应用的 steer 会重置它们。

  • 内置 budget 部件在实时会话中每轮允许 8 次成功工具结果,除非 Meta Harness 配方设置了 tool_budget。超过后的工具调用会记录 budget.denied,并在不执行 工具的情况下结束本轮。
  • max_tool_rounds 是工具轮次上限。它来自 ACL 键 max_tool_rounds,或按会话来自 SessionOptions::with_max_tool_rounds(Node.js maxToolRounds、Python max_tool_rounds、Go MaxToolRounds)。达到上限后,下一次补全请求携带空工具 列表,模型只能用文本回答。没有单独的收尾消息。

内核包装

每个事实运行都被两项检查包裹。它们属于内核,Meta Harness 配方无法去掉 (KernelPolicy::admit() 总是同时启用二者)。

权限投影

模型返回的每个工具调用,在折叠存储之前都会经过会话的 PermissionPolicy 投影:

  • 策略 deny 是最终结果。调用会被记录为 Permission denied 结果,并发出 permission_denied 事件。
  • 策略 allow 会执行工具。YOLO lane 在检查前就记录为 allow,因此不会把 Ask 变成确认提示。
  • ask 交给会话权限检查器。例如,计划模式护栏会拒绝默认策略本应询问的写操作。 如果检查器仍然询问,调用停靠在 confirm。
  • 没有确认管理器的会话关闭了确认,因此 Ask 会直接执行工具。

活动 Skill 的限制也在同一位置生效,并产生拒绝结果。

完成门禁

修改过工作区的运行,只有存在绑定变更 digest 的 Passed 验证报告,或宿主豁免覆盖 该 digest 时,才算完成。运行结束时,内核先合并宿主 CompletionAttestor(如果已 安装)给出的报告,然后判定:

  • Allow 把结果的 completion 设为 verified、waived 或 narrative(没有修改)。
  • Incomplete 或 Continue 返回错误,而不是已完成的结果。

当最近折叠的事实是工具结果,并且门禁已经以 verified 或 waived 允许完成时,运行 无需再调用服务提供商即可结束。narrative 允许仍会调用模型,让回答能使用工具输出。 助手文本永远不算证据。参见验证。

异步优先的会话构建

SessionOptions 是公开的配置补丁,不是半初始化的运行时状态。异步构建路径会把 它与 CodeConfig 合并,校验冲突选项,初始化异步资源,再生成唯一的内部 ResolvedSessionConfig。后续会话组装只消费这个已解析值,不会让不同 层各自重复决定同一个配置。

Rust 宿主应优先使用:

Rust
let session = agent
.session_builder("/repo")
.options(options)
.build()
.await?;

Agent::session_async、resume_session_async、session_for_agent_async 与 session_for_worker_async 使用同一个构建内核。文件型记忆和会话存储、队列、 轨迹记录器与会话 MCP 发现都在异步阶段初始化;失败时返回带具体资源信息的 SessionConfiguration 或 SessionInitialization 类型错误。

同步 Agent::session 只为已经显式传入预初始化记忆存储、并初始化好其他全部资源的 宿主保留兼容。它不会启动或阻塞 Tokio 运行时。任何需要异步工作的配置都会返回 CodeError::AsyncSessionBuildRequired;运行时不会悄悄换成更容易构建的后端。 通过 SessionOptions::with_mcp 传入的管理器始终需要异步发现能力;同步路径只能 继承智能体启动时已经缓存的全局 MCP 工具。

对话状态的单任务约束

对话历史通过准入机制串行化,而不是依赖乐观锁。同一个会话同时只能有一个会影响 对话记录的操作,包括 send、stream、两种附件变体、斜杠命令与 resume_run。 重叠调用会在读取历史或派发命令前立即返回 CodeError::SessionBusy。

流式调用会一直持有准入租约,直到流式运行时真正结束。丢弃或中止公开句柄不会在原 生产者仍写入事件和历史时短暂放行第二个操作。直接宿主工具调用属于控制面操作, 不占用对话租约。

调用上下文

每个已准入运行都会创建一个不可变的 InvocationContext,其中包含:

  • 运行标识与会话标识
  • 运行取消令牌
  • 事件发送器
  • 治理快照,包括当前预算守卫

它是服务提供商与工具工作的单一事实来源,也会把同一个取消令牌和会话身份安装进 ToolContext。因此取消可以传递到排队工具、嵌套的 batch/program、委派任务、 规划、结构化输出修复、压缩以及其他属于该运行的辅助调用。

分层提示词与运行连续性

默认系统提示词由少量互不重叠的契约组成。共用运行时契约只定义权威边界、自主性、 证据要求、连续性与停止语义;执行风格提示词只补充该模式需要的行为;响应格式片段 只描述目标输出。文件系统指令、Skills 与宿主上下文仍是独立输入,因此优化默认提示词 不会遮蔽运行时策略,也不会移除工具能力。

每个活动 Run 都拥有一个有界控制收件箱。steer 在下一次服务提供商调用时作为 user.message 追加到事实日志(参见 Steer);interrupt 发出协作取消请求,同时让正常清理、 检查点、钩子和事件持久化完成。幂等键、不可变 Run ID、可选回合修订号与截止时间会 阻止重试或过期界面状态影响较新的回合。上下文压缩与恢复会保留继续任务所需的目标、 约束、验收标准、已作决定与验证证据,不会静默改变范围。

作用域能力组合

一个 Session 会将 Tool、Skill、Agent、Command、Hook、MCP、Context、Flow、 Knowledge 与 UI 贡献发布为单个不可变能力目录代次。准备过程遵循有界依赖图,校验 覆盖完整投影,最后通过一次比较并交换让新代次可见。准备失败或取消时,已完成的副 作用会回滚,不会暴露部分目录。

每个已准入 Run 都会固定一个投影;当投影来自 A3S Use 时,还会取得与之匹配且不可 克隆的 Use 快照租约。Turn 与 Subtask 是有类型的子作用域:它们共享 Run 的结构代次, 只能收窄权限,不能重新发现 Session 的更新目录。Run 关闭时会先收敛受监督工作与可 逆副作用,再释放精确的代次租约。

RunCapabilityBindingV1 会把 Code 代次、目录摘要、完整权限上限摘要以及可选的 Use 游标写入 Run 与逻辑检查点。因此恢复会在目标 Run 准入前拒绝 N/N+1 漂移,包括准备 期间发生的切换。尚未发布能力的全新 Session 可以且只能引导一次完整历史批次;已经 发布能力的 Session 不能回退到旧代次。

大语言模型调用边界

属于运行的服务提供商工作统一经过有作用域的大语言模型调用器。它在每次服务提供商 调用前检查预算与取消,在成功响应后记录用量;流式路径会代理最终用量,并合并调用方 取消与运行取消。普通回合、规划、结构化输出及修复、压缩、记忆和辅助路径都使用这个 边界,不再各自维护预算逻辑。

硬预算拒绝会作为错误返回,绝不会转换成不受治理的回退。软上限会发出 budget_threshold_hit 事件,然后继续当前调用。

工具调用边界

工具调用器是模型选择、嵌套、程序化、委派与宿主直接调用的统一治理内核。对模型发起 的工作,它会执行当前技能限制、权限策略、前后置钩子、预算检查、人工确认、队列与 超时、取消、递归调用保护及输出净化。batch 和 program 接收的是有作用域的调用器, 而不是原始注册表,因此内部调用不能绕过这些检查。

直接 SDK 辅助方法使用显式的 HostDirectPolicy::TrustedControlPlane 来源。宿主已经 是选择该操作的权威,因此跳过面向模型的权限与人工确认决策;但前置钩子仍可阻止调用, 预算、队列与超时、取消、递归保护、后置钩子和输出净化仍然生效。应用在把这条特权 路径暴露给终端用户前,必须自行完成授权。

稳定事件协议

AgentEvent 是 Rust 内部运行时枚举。跨语言契约是无损信封:

JSON
{
"version": 1,
"type": "tool_end",
"payload": {},
"metadata": {}
}

v1 事件目录与 Rust 穷尽映射共享一个事实源,因此新增运行时变体却没有规范线协议 名称会直接导致编译失败。Node.js 与 Python 使用统一投影生成 text、toolName、 tool_name 等便捷字段。type 保持开放字符串:未来未知类型仍会完整保留载荷与 元数据,不会被压成 unknown 哨兵值。

原子会话持久化

SessionSnapshotV1 表示一个带版本号的完整持久化代次,包含对话、制品、追踪事件、 运行记录、验证报告与委派任务快照。session.save() 会物化这个聚合快照,并且只调用一次 SessionStore::save_snapshot。

文件存储先写入并同步临时文件,再通过原子替换发布一个完整 JSON 信封;内存存储在 同一把锁下替换整个聚合条目。两者都会声明原子快照能力。历史裸 SessionData 与 分片目录仍可读取以便迁移,但新保存不会发布碎片化代次。自定义存储必须显式实现 聚合保存;默认方法返回错误,不会把部分写入或空操作写入当成成功。

MCP 所有权与隔离

MCP 管理器的所有权是显式的:

  1. 智能体全局管理器持有从全局配置加载的服务器。
  2. 会话选项中由宿主传入的管理器是继承的只读能力来源。
  3. 每个会话都新建一个私有实时管理器。

能力按以上顺序组装,因此会话本地工具只能在当前会话内遮蔽继承工具。实时 add_mcp_server / remove_mcp_server 只改变私有管理器;移除本地遮蔽后会重新显露 继承能力。同级会话不能互相修改;委派的子智能体会继承调用同一批工具所需的有序 管理器来源,但不会取得它们的所有权。

本地 stdio 传输还拥有服务器进程的完整生命周期。它把服务器作为 Unix 进程组组长启动, 分别读取协议输出和 stderr,并在关闭或丢弃时终止整个进程组。任一管道关闭时,未完成 的请求会立即失败,而不是等待无关的请求超时。

程序化工具调用

program 工具在内嵌 QuickJS 虚拟机中运行 JavaScript,只暴露受控 ctx 对象。虚拟机 没有直接文件系统、网络、子进程或环境变量访问权;有用能力都通过 scoped tool 调用器回到 A3S Code。下一步需要判断时使用普通模型工具调用;动作序列已经确定、 只需让模型理解结果时,使用有边界的程序。

扩展点

通过带类型的会话选项、技能、智能体定义、钩子、MCP 服务器、记忆和会话存储、 安全提供程序、队列配置与工作区服务扩展运行时。优先使用显式策略和可回放证据, 不要引入平行执行路径。