具体问题与边界
研究 Codex 的目的不是照搬 Rust crate,而是找出当模型、工具和外部世界形成闭环后仍然成立的合同。 这些合同应能用不同语言、Provider 和存储实现,同时在并发与故障下给出确定答案。本节把前十节压缩 为一组迁移顺序,并标出什么条件下可以简化。
可迁移合同
| 合同 | 最小表达 | 保护的不变量 | 可简化条件 |
|---|---|---|---|
| 输入/输出窄腰 | typed command + event | 控制与观察分离 | 完全同步、无 Interrupt 时直接调用 |
| 状态所有权 | 单一 owner + ID scope | 并发修改有唯一解释者 | 单线程局部状态 |
| 生命周期快照 | request/step snapshot | 旧请求自洽、新请求见新世界 | 无动态工具/环境时单层快照 |
| Spec/Runtime 同源 | one ToolPlan | 模型合同与执行绑定一致 | 固定工具集可启动时构建一次 |
| Call/Result 配对 | stable call_id | 历史可继续采样/恢复 | 仍不可删除,即使工具无副作用 |
| 副作用屏障 | validate→authorize→execute→commit | 失败残留可判断 | 纯计算工具可省审批 |
| 有界资源 | queue/timeout/capacity | 内存和并发不无限增长 | 仍需明确上限,不应用“足够小”代替 |
| 规范日志与派生投影 | append log + rebuildable index | 恢复方向唯一 | 无持久需求时可只保留内存 |
| 显式取消/终态 | cancellation token + terminal once | 不出现双终态/悬挂资源 | 同步短调用仍应定义超时 |
| 可注入 Adapter | protocol boundary | 离线故障测试可复现 | 生产实现可以不同,合同不变 |
建立最小 Runtime 的顺序
- 定义完整 Message/ToolCall/ToolResult 与控制 Operation,明确哪些对象可持久化。
- 建立单一 Session owner、有界输入/事件 Queue、CancellationToken 和 terminal-once。
- 把 Prompt 编译为纯函数输入,建立 request/step snapshot,不从 Handler 读取可变全局状态。
- 用同一 ToolPlan 生成模型 specs 和执行 bindings;未知/非法调用也返回配对结果。
- 将副作用路径拆为验证、策略、审批、执行、结果提交,并为每个提交点定义残留状态。
- 只有需要跨进程恢复时加入 append-only 规范日志,再按查询需求增加派生索引。
- 最后才增加动态 MCP、多 Agent、Hook/Plugin,并为每项能力设置容量、版本和失败边界。
这个顺序避免“先做二十个工具,再发现没有统一取消和恢复协议”。工具数量增加的是表面能力,合同 决定 Runtime 能否在第二次采样、进程崩溃或目录刷新后继续正确工作。
Python 风格伪代码
class AgentRuntime(Protocol):
async def submit(self, operation: Operation) -> SubmissionId: ...
def events(self) -> AsyncIterator[AgentEvent]: ...
@dataclass(frozen=True)
class RuntimeContracts:
model: ModelClient
prompt_compiler: PromptCompiler
tool_planner: ToolPlanner
execution: ExecutionEnvironment
approval: ApprovalPolicy
rollout: RolloutStore
async def run_step(turn: TurnSnapshot, contracts: RuntimeContracts) -> StepOutcome:
step = await contracts.tool_planner.capture(turn)
prompt = contracts.prompt_compiler.compile(turn, step)
response = await contracts.model.stream(prompt, step.tool_plan.specs, turn.cancellation)
items = await accumulate_complete_items(response)
results = await execute_and_pair(items.tool_calls, step, turn, contracts)
await contracts.rollout.append_and_flush((*items.complete, *results))
return decide_next_step_or_terminal(items, results)
每个 Protocol 都对应一个可替换失败源。接口数量不是目标;只有能在测试中注入不同环境并保持相同 行为合同的边界才值得保留。
迁移失败模式
| 迁移错误 | 症状 | 修正 |
|---|---|---|
| 按源码目录复制模块 | 依赖方向与目标产品不匹配 | 按所有权/生命周期重画边界 |
| 保留 Rust 名称但丢 await/取消 | 看似相似,故障语义不同 | 迁移状态机而非语法 |
| 只有 function-call demo | 第二次采样/恢复不合法 | 建完整 Item、配对和 History |
| 每层都抽象成万能 Adapter | 提交点被隐藏 | Adapter 合同写清资源和残留 |
| 一开始复制 SQLite/Graph/MCP 全家桶 | 维护成本先于需求 | 按功能触发条件逐层加入 |
| 只测输出文本 | 资源泄漏/双终态未被发现 | 断言事件序列和残留状态 |
设计判断
可迁移设计的共同点是“限制歧义”:谁修改、什么时候可见、失败后剩什么、恢复从哪里开始。技术选型 可以换成 TypeScript、Go 或 actor system;只要这些问题仍有明确答案,架构合同就保留下来。
迁移时每增加一层都应提供删除条件。如果目标 Runtime 没有多 Surface、热更新、跨进程恢复或不可信 命令,就应该主动省略对应产品复杂度,并把不支持写成边界。
证据与复现实验
本节是 1.1—10.10 的交叉归纳,证据矩阵见第十章总索引。Mini Codex 是一次 Python 迁移实验:
cd examples/mini-codex
uv run pytest -q
uv run mypy src
测试验证合同可跨语言表达,但不说明 Python 实现适合 Codex 的生产规模。
本节边界
可迁移合同已经列明;最后一节反向列出不该因为 Codex 存在就机械复制的产品规模复杂度。详细源码映射由研究仓库中的配套索引维护。
评论
登录后即可评论