“能够调用函数的聊天模型”与“能在真实仓库里安全工作的 Coding Agent”之间,差的不是一个
shell 按钮。开放任意命令执行只解决了“可以产生副作用”,没有解决“在哪个工作区”、
“按什么项目规则”、“有什么权限”、“怎样观察真实结果”、“中断后如何恢复”和“如何证明改了
什么”。
本节的“普通聊天 Agent”是一个概念对比基线:它能维护对话,也可以有通用 Tool Call/Result
闭环,但不假设它拥有项目工作区和编码生命周期。Codex 源码里没有一个名为 ChatAgent 的
对照类;下面的“增加”是系统能力差异,不是虚构两个 Rust 类做代码 diff。
先给出结论:Coding Agent 的关键增量是一个拥有项目状态、可控副作用、真实环境反馈、 持久化历史与终态的 Runtime。模型仍负责提出下一个行动和解释观察,Runtime 则决定这个行动 能否、在哪里、用什么权限执行,并保存执行后真正发生的事。
先建立三层能力栈
不应把“聊天模型”和“Coding Agent”看成两个完全无关的系统。后者是在前者的生成能力上分层 增加 Runtime 合同:
对话生成层
输入 system/developer/user 层次的指令与历史,模型产生 assistant Message、Reasoning 或结构化 Tool Call 意图。这一层不直接持有本地文件句柄、子进程 PID 或沙箱权限。
通用 Agent 层
增加 typed Tool Call/Result、call_id、ToolRouter/Registry、并发与取消,以及可重复多次采样的 Agent Loop。模型可以从环境取得新观察,不再只用已有 Prompt 生成一段话。
Coding Runtime 层
增加项目与环境快照、命令/进程/补丁等副作用工具、PermissionProfile、审批与沙箱、 stdout/stderr/exit code、Turn Diff、World State、Rollout、压缩、中断与恢复。这些能力才把 “调用一个函数”改造成“在持续变化的真实仓库中完成可审计任务”。
增量一:“我在哪里工作”成为显式状态
聊天对话可以让用户粘贴一段代码,但粘贴的文本没有完整项目环境:它不告诉模型当前 cwd、 工作区根、Git 状态、项目规则、shell 类型、选定远程环境、MCP 连接和此时真正可用的工具。
Codex 将这些事实放进两个不同时间尺度的对象。
TurnContext:整个用户回合的合同
TurnContext 保存整个 Turn 不应在每次采样中随意漂移的语义:
- Turn ID、trace、Session source、parent thread 和 originator;
- model/provider/reasoning/personality/mode;
- config、developer instructions、history mode 和 extension data;
- approval policy、PermissionProfile、network proxy 与 platform sandbox 配置;
- dynamic tools、Skills context、final output schema;
- timing、metadata 和 terminal error。
这些字段让工具调用不必从 Prompt 文本里猜“当前是什么权限模式”或“应该使用哪个模型”。
StepContext:一次采样的可变环境快照
StepContext 每次后续采样前可重新捕获,当前明确绑定:
- TurnEnvironmentSnapshot 和 ready environment capability roots;
- executor 产生的 capability discovery snapshot;
- 该步精确 MCP binding 与固定 MCP tool list;
- 既用于广告又用于执行的定稿 ToolRouter;
- 该环境观察到的 canonical AGENTS.md。
这一层解决了 Coding Agent 特有的“世界会在行动后改变”。模型通过 Apply Patch 改了文件,或用户 在运行中追加了一个 MCP 引用,下一次采样不应无脑复用旧快照。但在同一次 sampling 内, 模型看到的 tools 和真正执行 Call 的 Router 又必须是同一份定稿视图。
async def capture_coding_step(turn, pending_input):
required_mcp = await discover_mcp_mentions(pending_input)
environments = await select_ready_environments(turn)
agents_rules = await agents_md_manager.refresh_if_selection_changed(
environments
)
mcp_binding = await bind_mcp(required_mcp, environments)
router = await build_tool_router(
turn_context=turn,
environments=environments,
mcp=mcp_binding,
dynamic_tools=turn.dynamic_tools,
)
return StepContext(
turn=turn,
environments=environments,
loaded_agents_md=agents_rules,
mcp=mcp_binding,
tool_router=router,
)
AGENTS.md 也因此不是一段在 Session 开始时永久粘住的 prompt 文本。AgentsMdManager 按环境选择 缓存;选择变化时重新加载项目指令。具体搜索路径、层级作用域与大小限制会在 4.3—4.4 单独拆开。
增量二:工具不只是通用 API,而是编码副作用面
通用 Agent 可以调天气、查订单、发邮件。Coding Agent 的不同之处不是这些 API 比较“技术”,而是 它的工具直接作用于一个长寿命、可变、可恢复的项目环境。
Codex 的 ToolRouter 将多种能力收敛进同一分派边界:
- Shell/Unified Exec:启动命令、收集 stdout/stderr/exit code,并管理持续进程;
- Apply Patch:用受限语法表达 add/update/delete/move 变更,产生精确 delta;
- View Image/媒体:将工作区中不只是文本的产物变成模型可观察内容;
- MCP、Dynamic 和 Extension tools:把项目外服务与扩展纳入同一 call_id/历史合同;
- Request User Input:在不能安全假设时暂停并获得新信息;
- Multi-Agent tools:把独立子任务、通信和终态也放入受控工具路径。
本节只建立能力地图,不假装已经解释每种工具的实现。后续 6.x 会逐项拆 Tool Spec、Handler、 Runtime、子进程、截断、审批和沙箱;7.x 再处理 Multi-Agent 和扩展。
这些工具的共同基础不是“名称都放进一个 dict”,而是:
- 模型可见 ToolSpec 与真正 Registry 之间有明确构建关系;
- namespace/name 和 payload kind 决定分派;
- call_id 关联行动与观察;
- Handler 声明并发、取消和 diff consumer 能力;
- Tool lifecycle 经过 Hook、telemetry 与类型化 Output;
- 可恢复错误返模型,合同错误向 Runtime 传播。
增量三:权限从 Prompt 外部进入确定性控制面
聊天对话中,“请不要执行危险命令”可以是 developer instruction。它会影响模型选择,但不能作为 唯一安全边界。模型输出是非确定性的,Prompt 文本也不能创建或撤销操作系统权限。
Codex 在 TurnContext 里显式持有:
approval_policy:什么情况需要请求用户或审查者;PermissionProfile:当前文件系统、网络与执行能力的结构化配置;- network proxy/managed network 状态;
- Windows/Linux 等平台沙箱选择所需配置。
Tool Orchestrator 把这些对象与工具请求结合,决定初始 SandboxAttempt,必要时请求审批,并仅在 策略允许时对沙箱拒绝做受控升级。这个判定不读 assistant Message 里的自我声明。
async def authorize_and_execute(invocation):
turn = invocation.step_context.turn
policy = derive_execution_policy(
permission_profile=turn.permission_profile,
approval_policy=turn.approval_policy,
network=turn.network,
platform_sandbox=turn.windows_sandbox_level,
)
attempt = policy.initial_attempt(invocation)
if attempt.needs_approval:
await approval_service.require(attempt.request)
result = await execute_in_selected_environment(invocation, attempt)
if result.is_sandbox_denied and policy.permits_escalation(result):
await approval_service.require(policy.retry_request(result))
result = await execute_in_selected_environment(
invocation,
policy.retry_attempt(result),
)
return result
这一层是 Coding Agent 与“模型直接拿到 shell”的安全分界线。Tool Call 表达意图,PermissionProfile 限定能力,Approval 处理动态授权,Sandbox 在实际执行时实施边界。四者不能合并成一句 prompt。
增量四:真实观测不依赖 Agent 自己宣称
一个普通聊天回答可以说“代码已经修好”,但如果没有连接工作区,这只是一句生成文本。Coding Agent 需要两类 Runtime 观测:
工具结果观测
Shell/Exec 输出包含 stdout、stderr、exit code、duration、timeout 状态等。Apply Patch 结果包含哪些变更 已提交,拒绝/部分失败时仍尽量保留已知前缀。这些结果通过 call_id 与原 Call 配对,进入下一次 Prompt。
Turn 净差异观测
Codex 还维护 TurnDiffTracker。它不是在每次改动后粗暴地对整个工作区重跑 Git diff,而是基于
已提交 Apply Patch mutation 的精确 delta,在内存里维护每个 path 的 baseline/current content、revision
和 rename origin,最后生成整个 Turn 的净 unified diff。
class TurnDiffTracker:
def track_delta(self, environment_id, patch_delta):
if not self.valid:
return
if not patch_delta.is_exact:
self.invalidate()
return
for change in patch_delta.changes:
self.apply_change(environment_id, change)
self.unified_diff = self.render_net_diff()
如果 delta 不精确,tracker 选择 invalidate,不伪造一份“看起来很精确”的 diff。这是 Coding Agent 可审计性里很重要的诚实边界。
Sampling request 在工具收束与 cancellation 检查后,若 tracker 有净 diff,向客户端发 TurnDiff Event。 所以客户端不需要从 assistant 最终文本里用正则抽取“修改了哪些文件”。
增量五:“运行验证”是能力闭环,不是 Runtime 硬编码脚本
可靠 Coding Agent 应该在可能时运行与变更匹配的测试、类型检查、lint 或 build。但这句设计目标
不等于 Codex 的 run_turn 里有一个写死的:
inspect()
edit()
always_run("pytest")
final()
这种硬编码无法覆盖 Rust、Go、JavaScript、移动端、单仓多包、远程构建和用户自定义工作流。
Codex 提供的是可组合验证能力:
- shell/exec 可执行项目本身的测试命令;
- AGENTS.md 和 developer instructions 可以指定项目应运行什么;
- Tool Output 把 exit code 和完整诊断回灌给模型;
- Agent Loop 让模型根据失败继续修正;
- Stop Hook 可在模型拟完成时检查条件,并用明确 continuation prompt 要求继续;
- 最终回答可说明已运行的验证和未能验证的限制。
因此,“Codex 支持验证闭环”是源码可支撑的说法;“Codex 永远强制每次修改后跑全量测试” 不是。具体步骤由模型在项目规则、工具观察、权限与 Hook 约束下选择。
增量六:历史不再只是用户和 assistant 的文本
普通聊天历史通常只需回放消息。Coding Agent 要恢复一份工作,还需要回答:
- 当时使用的 Base/Developer/Project rules 是什么?
- 模型发出过哪些 Tool Call,每个 Output 是什么?
- 哪一次 Turn 正常完成,哪一次中断?
- World State 怎样从全量快照经 patch 变到后续状态?
- 历史是否做过 compaction,哪个 window 是当前窗口?
- 用户是否 rollback,之后哪些项不应再生效?
Codex 的 Context History 保存 ResponseItems,并在生成 Prompt 时做 Call/Output 配对修复、孤儿 Output 删除、 媒体能力适配与超长工具输出截断。Rollout 还记录 SessionMeta、TurnContext、World State、Compaction、 Rollback、TurnStarted/Complete/Aborted 和其他事件。
@dataclass
class RecoverableCodingHistory:
session_meta: SessionMeta
response_items: list[ResponseItem]
turn_context_records: list[TurnContextRecord]
world_state_events: list[WorldStateFull | WorldStatePatch]
compaction_events: list[CompactionRecord]
rollback_events: list[RollbackRecord]
terminal_events: list[TurnComplete | TurnAborted]
rollout_reconstruction 并不把文件每一行都盲目追加到当前历史,而是理解 compaction、rollback、
World State 与终态的语义来重建。后续 8.x 会把持久化和恢复算法拆到足够细;本节只需建立一个认识:
Coding Agent 的历史是行动与环境状态的事件日志,不是纯聊天文本。
增量七:中断、进程与终态是一等责任
普通文本生成被中断,最直接后果是回答没输完。Coding Agent 被中断时,可能还有:
- 正在运行的 shell 子进程与孙进程;
- 正在等待的审批或 request-user-input;
- 正在写文件的 Apply Patch;
- 正在运行的 MCP 请求或子 Agent;
- 已经记录的 Call,但尚未记录 Output;
- 需要保存到 Rollout 的部分改动与中断标记。
因此 Codex 用 Turn CancellationToken 穿过 sampling、tools 和 compaction,ToolCallRuntime 区分优雅清理与 task abort,并尽量生成 aborted Tool Output。Task wrapper 在宽限期后强制收尾,Interrupted 还要先写模型可见 标记,flush 后再发 TurnAborted。
正常 TurnComplete 也需要两道持久化屏障:Task body 返回后先 flush 普通项,终态事件追加后 再 flush。这些细节看起来与“写代码”无关,却决定客户端在收到完成/中断后重读状态时, 看到的是否是同一个世界。
增量八:开放式能力必须有扩展与约束接口
编码任务不可能由内置工具覆盖所有场景。Codex 因此不只有一个固定 tools 数组,还提供:
- MCP bindings 与发现工具;
- dynamic tools 和 extension tool executors;
- Skills 与 Plugins 的提示/能力注入;
- PreToolUse/PostToolUse/Stop/Compact/Session 等 Hooks;
- Multi-Agent 子任务和通信工具;
- Code Mode 与不同产品表面的适配。
扩展性又必须受现有协议约束,否则每个插件都会自己定义身份、权限、取消、错误和持久化。 所以扩展 Tool 仍经 ToolRouter/Registry、ToolInvocation、call_id、ToolOutput 与 Agent Loop;Prompt fragment 仍经 Developer/Contextual User 槽位和顺序管理;Hook 仍返结构化 Outcome。
def install_extension(extension, runtime):
for prompt_fragment in extension.prompt_fragments():
runtime.prompt_compiler.add_to_declared_slot(prompt_fragment)
for tool in extension.tools():
runtime.tool_router.register(
name=tool.namespaced_name,
spec=tool.model_visible_spec,
handler=tool.handler,
)
for hook in extension.hooks():
runtime.hooks.register_typed(hook)
扩展性是 Coding Agent 适应不同仓库和组织的必要条件,协议化则是它不因扩展而失去安全与可恢复性的条件。
一张表看清差异不在“会不会写代码”
| 系统问题 | 对话生成基线 | 通用 Agent Runtime | Codex Coding Runtime |
|---|---|---|---|
| 输出是什么 | Assistant Message | Message + typed Call/Result | 同左,配合编码工具与环境事件 |
| 世界从哪里来 | Prompt 文本 | Tool Output | StepContext + Environment + AGENTS.md + World State + Tool Output |
| 如何改变世界 | 没有 Runtime 副作用 | 通用 Handler | Shell/Exec/Apply Patch/MCP/扩展/子 Agent |
| 权限由谁决定 | 无或产品外层 | Tool host | PermissionProfile + policy + approval + sandbox |
| 失败怎样表达 | 回答或客户端 Error | Tool Result | 保留 exit/output/diff 的观察 + Fatal/Turn 边界 |
| 任务怎样继续 | 下一条对话 | Agent Loop | 多 sampling Turn + pending input + compaction + StopGate |
| 怎样知道改了什么 | 依赖文本陈述 | 视工具而定 | Apply Patch delta + TurnDiffTracker + lifecycle |
| 怎样恢复 | 对话消息 | 视实现而定 | Rollout + History + World State + compaction/rollback/terminal replay |
| 怎样验证 | 给出建议 | 可调验证 API | 运行项目命令、消费真实结果、Hook 可阻止无证据完成 |
这张表也提醒一件事:Coding Agent 能否生成高质量代码仍然与模型能力密切相关,但一个强模型 若缺少上述 Runtime 边界,仍只能给出高质量建议,无法将“仓库已被正确修改”变成可审计事实。
一个 Mini Coding Agent 至少要保留什么
如果在第九章用 Python 复刻最小 Coding Agent,不需要一开始就实现 Codex 所有 Provider、MCP、多 Agent 和持久化数据库,但下列边界不应省略:
@dataclass
class MiniCodingTurn:
turn_id: str
workspace: WorkspaceSnapshot
project_rules: list[ScopedRule]
permission_profile: PermissionProfile
history: list[ResponseItem]
cancellation: CancellationToken
diff_tracker: TurnDiffTracker
async def run_mini_coding_agent(user_task, runtime):
turn = await runtime.start_turn(user_task)
while True:
step = await runtime.capture_step(
workspace=turn.workspace,
project_rules=turn.project_rules,
)
prompt = runtime.build_prompt(turn.history, step)
response = await runtime.model.sample(
prompt,
tools=step.router.model_visible_specs,
)
calls = []
for item in response.completed_items:
await runtime.record(item)
if call := step.router.decode_call(item):
calls.append(call)
if calls:
results = await runtime.execute_all(
calls,
permission_profile=turn.permission_profile,
cancellation=turn.cancellation,
diff_tracker=turn.diff_tracker,
)
for result in results.in_call_order():
await runtime.record(result)
continue
if await runtime.stop_gate_requires_continuation(turn):
await runtime.record(runtime.stop_gate_prompt())
continue
await runtime.flush_before_terminal_event()
return await runtime.complete_turn(
last_message=response.last_message,
diff=turn.diff_tracker.unified_diff,
)
这份最小实现仍然需要指定:
- workspace snapshot 如何捕获和更新;
- project rules 如何按目录作用域加载;
- ToolSpec 与 Registry 如何保持合同;
- 哪些工具可并发,哪些必须串行;
- 审批和沙箱怎样实际限制副作用;
- Tool Result 如何保留 call_id、exit code 和截断信息;
- 中断时如何终止子进程并修复历史形状;
- 哪些事件要持久化,终态前后的 flush 屏障是什么。
少了任何一项,仍可能做出一个令人印象深刻的 Demo,但它还没有建立生产 Coding Agent 所需的可靠边界。
应该如何验证“这是一个 Coding Agent Runtime”
验收不能只给它一个知识题,看最后回答似不似程序员。应该给出可观测的仓库任务和失败注入:
async def test_project_rule_changes_tool_behavior():
workspace.write("AGENTS.md", "do not modify generated files")
workspace.write("generated.py", "old")
await agent.run("change generated.py")
assert workspace.read("generated.py") == "old"
async def test_patch_is_reported_by_runtime_diff():
await agent.run("rename function")
assert terminal.turn_diff == expected_unified_diff
assert terminal.turn_diff != parse_diff_from_assistant_text()
async def test_permission_denial_is_not_bypassed_by_prompt_claim():
model.force_call("shell", {"cmd": "forbidden"})
model.force_message("I have permission")
await agent.run("act")
assert sandbox.execution_count == 0
assert model.next_request.has_failed_tool_output
async def test_resume_preserves_call_output_and_abort_state():
rollout = await run_until_interrupted_after_tool_call()
restored = await runtime.resume(rollout)
assert restored.history.has_paired_or_aborted_output()
assert restored.previous_turn.status == "aborted"
async def test_tool_changes_refresh_next_step_snapshot():
await agent.run("create config then inspect it")
assert captured_steps[1].world_state != captured_steps[0].world_state
Codex 当前测试体系正是按这种类型拆开。TurnDiffTracker 有 add/update/delete/rename、多环境和 invalidation 测试;Apply Patch、Shell 和 Unified Exec 有独立 Handler/Runtime 测试;Context History 有 Call/Output 配对和截断测试;Rollout Reconstruction 用大量序列验证 Compaction、Rollback、World State 和 Turn 终态。这些证据比“跑了一个 Demo,回答看起来很专业”更能保护架构。
第一章结论:从预测下一 Token 到管理可恢复副作用
第一章从模型最基础的下一 Token 预测出发,到这里已经建立了一条完整链条:
- Token 与上下文窗口解释了模型如何在有限输入上产生下一段输出;
- System/Developer/User/Assistant 和 typed ResponseItem 将权威来源、消息与机器控制项分开;
- ToolSpec、FunctionCall、call_id、ToolRouter 和 Registry 将模型输出变成可校验行动意图;
- Tool Result 与第二次采样把行动后的真实观察放回模型,形成闭环;
- Agent Loop 用采样屏障、工具收束、pending input、compaction、Stop Hook 和 Turn 终态管理多次闭环;
- 错误边界决定什么可重试、什么应返模型、什么必须终止,防止虚假观察与重复副作用;
- Coding Runtime 再加上项目快照、文件/进程工具、权限沙箱、diff、验证能力、持久化与恢复。
因此,Agent 不是“一个更会思考的聊天模型”。它是模型之外的控制系统。Coding Agent 又不只是 “Agent + shell”,而是一个能够把项目状态编译成 Prompt,把模型意图转成受权限约束的副作用,再把真实结果 变成新观察并可持久恢复的 Runtime。
第一章到此完成的是“从语言模型到 Coding Agent 的基础因果链”,不是把 Codex 所有实现细节 一次性讲完。后续章节将沿这张地图向下钻取:进程入口与配置、Thread/Turn/Step 内核、Prompt 与 History、 模型运输、每一种工具和安全机制、多 Agent 与扩展,以及 Rollout 和恢复。每一项都还会拆成源码级子章节。
评论
登录后即可评论