Codex 的 ActiveTurn 不只承载普通聊天。手动 Compact、代码 Review 和 !command 都复用 SessionTask 的
启动、中止、指标与终态外壳,但它们不走相同的模型循环;Rollback 甚至不是 Task,而是受保护的历史
重建操作。
把它们统称为“特殊命令”会掩盖最重要的差别:谁调用模型、谁创建子 Session、谁修改历史、谁可以嵌入 活动 Turn,以及每条路径应发哪些生命周期事件。
SessionTask 是共享外壳,不是统一算法
SessionTask trait 只要求:
kind():ActiveTurn/telemetry 识别;span_name():全生命周期 trace;run(context, input, cancellation):具体工作;- 可选
abort():协作取消后的专用清理。
Regular、Compact、Review、UserShell 都进入 start_task,因此共享:ActiveTurn 独占、AgentControl execution
guard、CancellationToken、rollout flush、on_task_finished、TurnComplete/Aborted 和 idle lifecycle。
但“共享外壳”不代表共享 TurnStarted 逻辑。RegularTask 和 standalone UserShell 在各自 run 中显式发送 TurnStarted;Review 当前代码依赖共享完成外壳,却尚未发送对称的 parent TurnStarted,源码留有 TODO。
CompactTask:用专用算法替换上下文
Op::Compact 创建默认 TurnContext,以空输入启动 CompactTask。它被标记为 TaskKind::Compact,因此
活动期间拒绝 steering。
Compact 分派顺序是:
- TokenBudget Feature 启用 → token-budget manual compact;
- Provider 支持 remote compact:
- RemoteCompactionV2 启用 → remote v2;
- 否则 → remote legacy;
- 其他 Provider → local compact,用 config.compact_prompt 或默认 summarization prompt。
async def compact_task(session, turn):
if turn.features.token_budget:
return await run_token_budget_manual_compact(session, turn)
if provider_supports_remote_compact(turn.provider):
if turn.features.remote_compaction_v2:
return await run_remote_v2(session, turn)
return await run_remote(session, turn)
prompt = turn.config.compact_prompt or DEFAULT_SUMMARIZATION_PROMPT
return await run_local_compact(session, turn, synthetic_user_input(prompt))
Compaction 的产物是替换或追加后的历史摘要和 ContextCompacted 等事件,不是普通 assistant final answer。 CompactTask 只把 TurnAborted 向外抛给统一中止终态;其他 compact 实现错误通常已经在自身路径发出事件, Task 最终返回 Ok(None) 收口。
Regular Turn 中因 context limit 触发的是 inline auto compact,不另开用户 Turn;手动 Op::Compact 才是
这里的独立特殊 Task。
Review:父 Task 内再启动隔离的子 Codex
Review handler 先解析 target/diff request,再构造 review-specific TurnContext:
- model 可由
review_model覆盖; - 禁用 WebSearch、Goals;
- multi-agent version 设为 Disabled;
- 继承必要的环境、shell policy、permission profile 和 skill snapshot;
- 用合成 review prompt 作为 Task 输入;
- 发 EnteredReviewMode Item,让 UI 切换展示。
ReviewTask 的 run 不直接调用父 Session 的 run_turn,而是启动一次性 Review sub-Codex。子配置进一步:
- base instructions 固定为 REVIEW_PROMPT;
- approval policy 固定 Never;
- 禁 web search、Collab 与 MultiAgentV2;
- source 标记 SubAgentSource::Review;
- 可使用 review model。
这形成权限与上下文隔离:Reviewer 不能在检查代码时再派生 Agent、弹审批或随意搜索网络。
Review 事件为什么要过滤
父 Task 读取 child SessionIo.rx_event:
- 普通工具/状态事件可转发到父 Turn;
- assistant AgentMessage 暂存,只在下一条消息到来时转发前一条;
- assistant 的 ItemCompleted 与 text delta 被抑制,避免旧兼容事件提前显示非结构化终稿;
- TurnComplete 的 last_agent_message 被解析为 ReviewOutput;
- TurnAborted/Channel close 视为 interrupted。
解析先尝试整段 JSON,再提取第一个 {...} 区间;仍失败则把原文本放入
overall_explanation,保证 Reviewer 格式漂移不会让父 Turn 无输出。
退出 Review 时,父 Session 写入合成 user message 表达 review 结束语义,发 ExitedReviewMode Item,并把 结构化 review 渲染成 assistant history。中止也写明确的 interrupted guidance。最后显式 materialize rollout,因为 Review 可能发生在第一个普通 Turn 之前。
UserShell:不经过模型的 Turn
Op::RunUserShellCommand 表示用户显式输入的 !cmd。它执行默认 shell 语法,可以含 pipe、redirect 等,
并使用 ExecCommand/CommandExecution Item 流式报告。
入口先检查当前是否有 ActiveTurn:
| 模式 | 调度 | 生命周期 | 历史写法 |
|---|---|---|---|
| StandaloneTurn | spawn UserShellCommandTask | 自己发 TurnStarted,共享外壳发 TurnComplete | record_conversation_items + materialize |
| ActiveTurnAuxiliary | 独立 Tokio task,复用活动 TurnContext/Cancellation | 不发第二组 Started/Complete | inject_no_new_turn |
async def run_user_shell(session, command):
active = await session.active_turn_context_and_token()
if active:
spawn(execute_shell(command, active.context, active.token, mode="auxiliary"))
else:
turn = await session.new_default_turn()
await session.spawn_task(turn, [], UserShellTask(command))
Auxiliary 模式若再发 TurnStarted/Complete,会让一个公开 Turn 出现嵌套终态,App Server 无法正确维护 TurnState,所以源码明确禁止。
UserShell 的执行边界
它选择 TurnEnvironment,建立 exec env,处理 managed proxy env、runtime PATH prepend 与可选 shell snapshot, 再调用通用 exec runtime。Timeout 固定为一小时。输出形成 CommandExecution Started/Delta/Completed,失败 也形成 status=Failed 的 Item,而不是请求模型解释。
UserShell 是“用户直接执行”,不等同于模型 shell tool:审批语义、source 标记和模型 follow-up 都不同。
Rollback:历史重建,不是 SessionTask
Op::ThreadRollback 不进入 ActiveTurn Task 外壳。Handler 先检查:
num_turns >= 1;- 当前没有 ActiveTurn;
- Session 有 LiveThread persistence。
之后 flush 现有 writer、从 Store 加载历史,把 ThreadRolledBack(num_turns) marker 接到重放序列,调用
apply_rollout_reconstruction 重建内存状态,重新 arm rollout budget reminder、重算 token usage,再持久化
rollback marker 并发事件。
async def rollback(session, n):
require(n >= 1)
require(session.active_turn is None)
live = require_persistence(session)
await live.flush()
stored = await live.load_history()
marker = ThreadRolledBack(num_turns=n)
await session.reconstruct(stored.items + [marker])
await session.recompute_token_usage()
await session.persist_and_emit(marker)
它明确不恢复本地文件变更。历史告诉模型“忘掉最后 N 个用户 Turn”,工作区仍保持当前磁盘状态;若需要 撤销 patch,必须由 Git 或编辑器提供另一条操作。
其他非模型 Op 为什么不做 Task
ThreadSettings、SetThreadMemoryMode、RefreshMcpServers、ReloadUserConfig 是短时 Thread 状态操作;审批回答 是唤醒 waiter;CleanBackgroundTerminals 是资源管理。把它们包装成 ActiveTurn 会制造无意义 Started/ Complete,并阻止真正用户 Turn,所以它们直接在 submission handler 中执行。
选择是否使用 SessionTask 的判断标准是:是否需要独占一段可中止、可计时并具有 Turn 终态的用户可见 工作,而不是“代码是否异步”。
失败矩阵
| 路径 | 失败 | 终态/恢复 |
|---|---|---|
| Manual Compact 被 Interrupt | TurnAborted | 历史保留中止标记 |
| Compact provider 错误 | 实现层发错误,Task 收口 | 不伪造摘要 |
| Review request 解析失败 | Error,不 spawn Task | 无 Review mode |
| Review child 非 JSON 输出 | fallback overall_explanation | 正常退出 Review |
| Review child channel 提前关闭 | interrupted | 写退出提示 |
| Auxiliary UserShell 失败 | Failed CommandExecution | 主 Turn 可继续 |
| Standalone UserShell 无环境 | Error + TurnComplete | 无模型调用 |
| Rollback 时有活动 Turn | ThreadRollbackFailed | 原历史不变 |
| Rollback marker flush 失败 | 已重建,发 Warning | writer 后续重试 |
测试重点
async def test_standalone_shell_has_one_lifecycle(thread):
events = await run_shell(thread, "pwd")
assert count(events, "turn_started") == 1
assert count(events, "turn_complete") == 1
async def test_aux_shell_does_not_nest_turn(thread):
await start_blocked_regular_turn(thread)
events = await run_shell(thread, "git status")
assert count(events, "turn_started") == 0
assert events.contains("item_completed")
async def test_review_child_is_restricted(review_probe):
child = await review_probe.spawn()
assert child.approval_policy == "never"
assert "web_search" not in child.tools
assert "spawn_agent" not in child.tools
async def test_rollback_does_not_touch_files(session, file):
before = file.read_bytes()
await session.rollback(1)
assert file.read_bytes() == before
特殊 Task 的设计没有把所有命令强行塞进普通 Agent Loop,而是复用必要的生命周期骨架,再为压缩、隔离 审查、直接 shell 和历史重建保留不同的状态语义。
评论
登录后即可评论