具体问题与边界
一次 Turn 可能经历“模型请求工具—工具修改仓库—再次采样”。如果所有状态都在 Turn 开头冻结, 第二次采样看不到刚发生的变化;如果所有状态都随时读取全局最新值,同一个工具调用又可能被不同 Schema、权限或工作目录解释。Codex 因此把配置按稳定期分到 TurnContext 与 StepContext。
本节讨论时间一致性,不把某个具体字段当前归属包装成永恒规则。
状态所有权和刷新规则
| 状态 | 典型生命周期 | 为什么放在这一层 | 何时刷新 |
|---|---|---|---|
| model、cwd、审批/权限基线 | Turn | 构成用户本次请求的执行承诺 | 新 Turn 或明确权限响应 |
| ToolRouter/Registry | Step | 必须和本次模型所见 ToolSpec 同快照 | 每次新采样前 |
| MCP binding/revision | Step | 目录可刷新,但旧 Call 不能漂移 | 下一 Step |
| World State | Step 比较 | 工具可能改变文件与环境 | 下一 Step 重捕获 |
| CancellationToken | Turn | 取消整次任务链 | Turn 终态销毁 |
| stream retry state | 单次请求尝试 | Retry 不能偷偷换工具合同 | 请求结束或新 Step |
正常路径
- Session 根据用户输入与线程设置创建 TurnContext,确定模型、cwd、权限基线和本次取消范围。
- 第一次采样前捕获 StepContext:解析 required MCP、冻结 binding,并由同一计划构造 ToolSpec/Router。
- 模型返回 ToolCall;执行 Future 持有产生该 Call 的 StepContext,而不是回头查询全局最新 Registry。
- 工具修改文件或 MCP 目录刷新后,旧 Step 不被原地修改。
- 只有进入下一次模型采样,Runtime 才重新捕获 StepContext,并把 World State 变化编译进请求。
核心是“旧请求保持自洽,新请求看见新世界”。这比笼统的 immutable/mutable 二分更准确: StepContext 自身也是冻结快照,但下一 Step 会得到新实例。
Python 风格伪代码
@dataclass(frozen=True)
class TurnSnapshot:
turn_id: str
model: str
cwd: Path
permissions: PermissionProfile
cancellation: CancellationToken
@dataclass(frozen=True)
class StepSnapshot:
tool_plan: ToolPlan
mcp_revision: int
world_state: WorldState
async def run_turn(turn: TurnSnapshot) -> Terminal:
previous_world: WorldState | None = None
while not turn.cancellation.cancelled:
step = await capture_step(turn, previous_world)
response = await sample(compile_prompt(turn, step), step.tool_plan.specs)
if response.tool_calls:
await execute_calls(response.tool_calls, step.tool_plan.router, turn.cancellation)
previous_world = step.world_state
continue
return Completed(response.message)
return Aborted()
execute_calls 显式接收旧 Step 的 Router;这是防止目录刷新后“旧参数交给新实现”的关键,不是为了
方便测试而注入的可选对象。
失败与并发边界
| 失败设计 | 竞态结果 | 正确边界 |
|---|---|---|
| Turn 开头捕获全部状态 | Patch 后模型仍看到旧 World State | 动态状态按 Step 重捕获 |
| 工具执行时查询最新 Registry | 旧 Call 被新 Handler/Schema 解释 | Future 持有原 Step Router |
| Stream retry 重建 Step | 同一次请求重试时工具面改变 | Retry 复用请求快照 |
| 每个函数直接读全局 cwd/权限 | 同一 Turn 的副作用范围漂移 | 从 TurnContext 传入 |
| MCP refresh 原地修改 binding | 并发 Call 观察到部分更新 | 新 revision 原子替换,旧引用继续有效 |
设计判断
决定状态放在哪一层的标准不是“变化快不快”,而是变化能否在当前协议单元中安全可见。模型、工具 Schema 和执行 Handler 构成一次采样的因果闭包;World State 只在下一次采样时需要更新。
双快照增加构造与传参成本,也会保留短期旧对象。换来的收益是可重放、可测试和无锁读取。对没有 动态工具、环境刷新或多次采样的简单 Agent,一个请求级快照即可,不必照搬两层结构。
证据与 Mini Codex
生产锚点为 session/turn_context.rs、session/step_context.rs、session/turn.rs 与
tools/spec_plan.rs。3.9、3.10、4.12 和 6.10 已验证字段生命周期与 MCP 刷新行为。
Mini Codex 的 WorldState/Patch 与 Registry 热更新测试验证“下一 Step 看新状态、旧 ToolPlan 不漂移”:
cd examples/mini-codex
uv run pytest -q -k 'world_state or registry_snapshot'
Mini 没有生产级 required-server 初始化、Provider capability 和远程执行环境,因此不证明字段集合完整。
本节边界
已经证明:稳定期必须与因果作用域对齐。下一节把同样的时间一致性应用到 Prompt 编译与缓存前缀。 详细源码映射由研究仓库中的配套索引维护。
评论
登录后即可评论