一次 agent turn 不是一次模型调用。用户提交一条消息以后,运行时可能先补项目规则和 Skill 目录,再发起模型请求;模型返回工具调用,权限层批准或拒绝,工具结果追加回历史,模型随后继续判断。这个小循环可以重复很多次,直到验证通过、需要用户输入,或者运行时决定停止。

因此,理解 coding agent 的运行时,不能只看 observe -> act -> observe。还要追踪每一类信息何时进入上下文、以什么身份进入、何时失效,以及 compact 和 subagent 怎样改变信息边界。

讨论范围限定为一次 turn 的上下文生命周期。Skill 的跨产品加载差异见 AI Coding Agent 的 Skill 加载机制深度解析,messages 的追加、裁剪、摘要和外化见 上下文管理全景,MCP、A2A 与 AG-UI 的协议分层见 Agent 互操作协议全景。这里不再重做产品图谱,只保留能解释当前 turn 的官方机制。

Turn 的边界

在本文中,turn 从用户输入进入运行时开始,到主 agent 产生一个对用户可见的最终结果或明确停在阻塞状态结束。turn 内部可以包含多次模型采样和工具执行:

1
2
3
4
5
6
7
8
9
user input
-> context build
-> model call
-> tool request
-> hook / permission / sandbox
-> tool result append
-> model call
-> verification
-> final response or blocker

会话和 turn 不是同一层。会话保存多个 turn 的历史、摘要和外部状态;turn 只处理当前输入引发的一串动作。工具循环也不是 turn:一次 turn 可以没有工具,也可以包含几十次工具调用。

模型看到的“上下文”同样不等于磁盘上的 session state。运行时可以把 TODO、checkpoint、完整 transcript、文件索引和审批记录存到模型窗口之外,只在需要时抽取其中一部分送给模型。逻辑上的模型输入通常由四层组成:

  • 稳定指令:system / developer prompt、项目规则、工作区约束和输出风格。
  • 能力目录:内置工具、MCP tools、Skill 描述、可用 subagent 描述。
  • 会话历史:用户消息、assistant 消息、工具请求、工具结果,以及 compact 后的摘要。
  • 本轮增量:新用户消息、hook 追加的上下文、刚产生的工具结果和审批结果。

这四层的来源、寿命和权限不同,不能当成一个大 prompt 处理。

组件结构:上下文从哪里来

组件图回答一个结构问题:一次模型调用之前,哪些组件共同决定模型能看见什么?

flowchart LR
  subgraph Sources["Context sources"]
    RULES["System / developer<br/>AGENTS.md / CLAUDE.md / rules"]
    CATALOG["Tool schemas<br/>MCP tool catalog<br/>Skill descriptions"]
    HISTORY["Messages<br/>tool results<br/>compact summary"]
    STATE["Files / memory / TODO<br/>checkpoint / repo state"]
  end

  subgraph Runtime["Agent runtime"]
    BUILDER["Context builder"]
    HOOKS["Lifecycle hooks"]
    POLICY["Permission policy<br/>sandbox"]
    TRACE["Trace / transcript"]
  end

  MODEL["LLM"]
  EXEC["Built-in tools<br/>MCP servers<br/>Skill loader<br/>subagents"]

  RULES --> BUILDER
  CATALOG --> BUILDER
  HISTORY --> BUILDER
  STATE -->|selected or retrieved| BUILDER
  HOOKS -->|additional context| BUILDER
  BUILDER --> MODEL
  MODEL -->|tool request| POLICY
  HOOKS -->|inspect or modify| POLICY
  POLICY -->|allow| EXEC
  POLICY -->|ask / deny| BUILDER
  EXEC -->|result| BUILDER
  BUILDER --> TRACE
  POLICY --> TRACE
  EXEC --> TRACE

Context builder 是这里经常被忽略的组件。它决定稳定指令的顺序、哪些目录只放 metadata、历史保留到哪里、工具结果怎样裁剪,以及 compact 后重新加载什么。模型负责在给定上下文里做选择,运行时负责构造选择空间。

启动上下文:规则、目录和正文

会话启动或恢复时,运行时先建立相对稳定的前缀。项目规则通常属于这一层。Codex 会按目录层级加载适用的 AGENTS.md;Claude Code 会加载相应的 CLAUDE.md 和 rules;OpenCode 的 rules 与 instructions 也属于长期指导,而不是某次工具结果。

Skill 需要拆成目录和正文两个对象:

1
2
Skill catalog = name + description + optional location / invocation metadata
Skill body = SKILL.md instructions + selected references / scripts

目录解决“有哪些能力、什么时候可能适用”,正文解决“选中以后具体怎么做”。两者同时常驻,会让未使用的工作流占满上下文;只保留正文而没有目录,模型又无法发现能力。

截至 2026-07-17,Claude Code 官方文档明确写明:普通会话里 Skill descriptions 先进入上下文,完整正文在调用时加载;OpenCode 也通过原生 skill tool 按需返回完整 SKILL.md。Claude Code 的 subagent skills 字段是一个例外,被指定的 Skill 正文会在子 agent 启动时预加载。Codex 官方把 Skill 描述为按需使用的可复用工作流,但公开文档没有承诺和 Claude Code 完全相同的内部消息布局。

正文被调用以后也不等于永久有效。Claude Code 当前会把渲染后的 Skill 正文作为一条消息放进会话;相同正文再次调用时只追加“已加载”的短提示,动态参数或动态命令输出改变后才追加新正文。OpenCode 文档只保证 skill tool 把内容返回到 conversation,不应据此推断每次都重新读取磁盘,或推断正文在所有 compact 路径里原样保留。

Tool 和 MCP:schema 先到,结果后到

Tool 的目录和 Tool 的执行结果也要分开。

模型在决定是否调用工具之前,至少要看到工具名、用途和输入 schema。内置工具、插件工具和 MCP tool 最终都会形成这类可调用描述。文件内容、命令输出、数据库记录或远程 API 响应,只有执行以后才成为新的 observation。

MCP 2025-11-25 规格把 server 能力分成 prompts、resources 和 tools:

MCP primitive 规格中的控制方 进入上下文的典型方式
prompts 用户控制 用户选择模板后,由 host 取得消息内容
resources 应用控制 host 决定是否选择、检索或附加资源
tools 模型控制 模型提出调用,host 执行并处理结果

“模型控制”不等于绕过用户。MCP tools 规格要求 server 校验输入、做访问控制并清理输出,也建议 client 对敏感操作请求确认、在交给模型前校验结果、设置超时并记录审计日志。MCP 规定 tools/listtools/call 和结果结构;是否把某个 tool 暴露给当前 agent、是否需要审批、结果截断多少、是否写入 transcript,仍由 host runtime 决定。

这也解释了为什么 MCP server 不是安全边界。它是能力与数据的协议边界,permission、sandbox、hook 和 trace 仍在 host 一侧。

运行时视角:一次工具循环

时序图回答时间问题:一条工具结果怎样回到模型,并推动同一个 turn 继续运行?

sequenceDiagram
  participant U as User
  participant R as Runtime
  participant H as Hooks
  participant M as Model
  participant P as Permission / Sandbox
  participant T as Tool / MCP
  participant X as Trace

  U->>R: submit prompt
  R->>H: UserPromptSubmit / context hook
  H-->>R: optional additional context
  R->>M: stable prefix + history + current input
  M-->>R: tool request
  R->>H: pre-tool hook
  H-->>R: allow / modify / block metadata
  R->>P: evaluate tool and arguments

  alt allowed
    P->>T: execute
    T-->>R: structured result / error
    R->>H: post-tool hook
    H-->>R: additional context / validation result
  else ask
    P-->>U: approval request
    U-->>R: approve or reject
  else denied
    P-->>R: blocked observation
  end

  R->>X: append events and decisions
  R->>M: append new observation
  M-->>R: next tool request or final answer
  R->>H: Stop / verification hook
  H-->>R: continue or stop
  R-->>U: result or blocker

不同产品不保证 hook 与 permission 的内部先后顺序完全一致,图中表示的是控制点,不是跨产品统一 ABI。需要依赖顺序的安全策略,应以具体产品的事件文档和测试结果为准。

Messages append:观察结果怎样变成状态

不开 compact、prune 或显式清理时,常规工具循环主要做追加:

1
2
3
4
5
6
[user prompt]
[assistant tool call]
[tool result]
[assistant tool call]
[tool result]
[assistant final]

第二次模型调用能利用第一次工具结果,是因为运行时把结果放进了后续模型输入。结果可能在 API 层表现为 tool_result、tool message 或 provider 专用 block,应用内部也可能使用自己的 ToolPart、event 或 transcript 结构。文章里把它统一称为“工具结果消息”,只描述逻辑角色,不把某一家 provider 的 wire format 当成通用格式。

追加带来三个后果。

第一,工具输出会持续占用上下文。一次无边界的日志读取可能污染后面很多轮,不能把“读到了”理解成“运行时会自动提炼并丢弃原文”。

第二,错误也是 observation。退出码、stderr、MCP isError、权限拒绝和 sandbox denial 都应回到控制循环,否则模型只知道动作没有成功,却不知道下一步该换参数、换工具还是停止。

第三,工具结果不等于验证证据。npm test 返回成功可以支持“测试通过”,读了一段源码只能支持“看到这段实现”。Trace 需要记录 claim 与 evidence 的对应关系,而不是把所有成功退出码都当成完成。

Hook:给不确定循环插入确定性控制点

Hook 可以做三类事:

  • 观察:记录事件、发送通知、采集耗时和结果摘要。
  • 决策:阻止停止、拒绝危险工具、要求审批,或在产品允许时改写参数。
  • 注入:把额外规则、当前状态或验证结果放入后续模型上下文。

一个 hook 不一定同时拥有三种权力。事件名相似,也不代表输出协议相同。

截至 2026-07-17,Codex 官方 hooks 文档列出 PreToolUsePermissionRequestPostToolUsePreCompactPostCompactUserPromptSubmitSubagentStartSubagentStopStopSessionStart 等关键事件。大部分工具、压缩和停止事件属于 turn scope;SessionStartSubagentStart 属于线程或子 agent 启动边界。Codex 还明确提醒:多个匹配 command hooks 可能并发启动,不能依赖一个 hook 抢先阻止另一个启动。

Claude Code 的事件面更广,handler 可以是 command、HTTP、MCP tool、prompt 或实验性的 agent。它的 additionalContext 会按事件位置进入上下文:session 与 subagent 启动信息放在开头,用户输入 hook 跟随当前 prompt,工具前后的补充信息靠近工具结果。已经写入 transcript 的中途 hook 内容在 resume 时会被重放,而不是为过去的 turn 重新执行。

OpenCode 插件以 JavaScript / TypeScript 模块提供 tool.execute.beforetool.execute.afterpermission.askedpermission.repliedsession.compacted、message/file/session events 和 custom tools。其可编程性很强,相应地也必须把插件来源和执行权限纳入信任模型。

Hook 越靠近副作用,越需要来源标记、审计和禁用路径。能重写 shell 参数的 hook 已经不是普通提示词扩展,而是执行链的一部分。

Permission 和 sandbox 是两道门

Permission 回答“这个动作是否被策略允许”,sandbox 回答“即使允许,它实际能触碰到哪里”。两者不能互相替代。

1
2
3
4
model asks to run command
-> permission: allow / ask / deny
-> sandbox: filesystem / network / process boundary
-> executor: actual side effect

适合自动放行的动作通常作用域明确、可逆、可重跑并且有 trace,例如工作区内读取非敏感源码。跨项目目录、读取凭据、安装依赖、访问网络和调用 remote MCP 会扩大数据与供应链边界,更适合 ask 或精细 allowlist。删除、重写历史、强推和生产发布需要更强的显式授权。

OpenCode 把 doom_loop 也做成 permission key:相同工具以相同输入重复三次时进入审批。这个设计很有代表性,权限系统不只防越权,也可以防止 agent 在同一个失败动作上耗尽预算。

审批结果必须回到 loop。拒绝不是异常吞掉,而是一条新的约束:当前路径不可用,模型需要换成只读方案、缩小范围或报告 blocker。

Stop 也是一个运行时事件

模型生成一段自然语言不代表任务完成。停止前至少需要回答:

  • 目标 claim 是什么?
  • 哪个工具结果或人工检查点支持它?
  • 失败路径是否已经处理?
  • 是否还有待执行的授权动作?
  • 当前停止是完成、取消、阻塞,还是预算终止?

Stop hook 可以把“未验证即完成”重新送回 loop。SubagentStop 可以要求子 agent 补证据。两者都不能凭空制造正确性;它们只能检查验证是否发生、结果是否满足规则。

Codex 的公开 OTel 事件包括 conversation、API request、stream event、tool decision 和 tool result 等记录。Claude Code hook 输入暴露 transcript 路径和事件字段。OpenCode 提供 session、permission、tool、message 与 file events。这些入口足以构造控制面 trace,但默认日志是否保留完整上下文、是否脱敏、是否持久化,要分别查看配置。

Compact:摘要不是恢复

上下文接近窗口上限时,运行时通常会压缩较早历史。这个动作至少包含两步:

  1. compact:把旧消息转换成更短的 continuation summary,或者移除部分旧工具输出;
  2. rehydration:从磁盘、配置或外部状态重新放回仍然适用的规则、Skill、memory 和任务状态。

“Rehydration”是本文使用的工程术语,不是各家共同定义的 API 名称。它强调一个事实:摘要只能保留模型选中的信息;长期约束如果有稳定来源,应该从来源重新加载,而不是期待摘要逐字记住。

flowchart LR
  subgraph Before["Before compact"]
    B1["Stable instructions"]
    B2["Old messages<br/>tool results<br/>hook context"]
    B3["Recent tail"]
  end

  COMP["Compact<br/>summarize or prune"]
  SUM["Continuation summary"]
  RELOAD["Rehydrate from sources<br/>rules / memory / skills / state"]

  subgraph After["After compact"]
    A1["Rebuilt stable context"]
    A2["Summary"]
    A3["Preserved recent context<br/>if the product keeps it"]
  end

  B2 --> COMP
  COMP --> SUM
  B1 --> RELOAD
  SUM --> A2
  RELOAD --> A1
  B3 --> A3

Claude Code 当前给出了最明确的公开存活表。系统提示和 output style 不属于 message history;项目根 CLAUDE.md、非 path-scoped rules 与 auto memory 会重新注入;path-scoped rules 和嵌套 CLAUDE.md 要等匹配文件再次读取;已调用 Skill 会在预算内重新附加,每个最多 5,000 tokens,总预算 25,000 tokens,较旧 Skill 可能被丢弃。这些数字是 Claude Code 当前实现约束,不是 Agent Skills 标准。

OpenCode 当前配置中 compaction.auto 默认开启,compaction.prune 默认关闭;开启 prune 后会移除旧工具输出。其 experimental.session.compacting 插件 hook 可以向 continuation summary 增加状态,也可以替换 compaction prompt。官方文档没有承诺所有旧工具结果或 Skill 正文都会原样恢复。

Codex 支持手动 /compact,并暴露 PreCompactPostCompact 以及 SessionStart(source = compact) 事件。公开 hook 契约可以用来记录压缩、补充后续上下文或运行检查,但不应从事件名反推内部摘要格式。

可靠的 compact 设计需要一份恢复契约:

信息 更合适的来源 compact 后策略
项目硬规则 版本化规则文件 重新加载
当前目标与完成条件 task state / checkpoint 结构化注入
已做决策及理由 决策账本 摘要并保留证据指针
大型日志和文件内容 文件系统 / artifact 保留位置与校验值,按需重读
临时探索过程 message history 允许摘要或丢弃
权限授予 permission state 按产品契约处理,不能由摘要自行延长

把关键状态只留在聊天散文里,compact 迟早会把它变成近似值。

Subagent:上下文隔离,不是消息复制

Subagent 的主要价值之一,是把高噪声工具结果隔离在另一个上下文窗口。父 agent 发送委托消息,子 agent 在自己的规则、工具和权限下工作,最终把结果或摘要返回父上下文。原始 grep 输出、测试日志和中间尝试可以留在子线程。

flowchart LR
  subgraph Parent["Parent context"]
    P1["Goal / constraints"]
    P2["Delegation message"]
    P3["Returned summary<br/>evidence pointers"]
  end

  subgraph Child["Child context"]
    C1["Child system prompt"]
    C2["Selected rules / skills"]
    C3["Tool calls and noisy results"]
    C4["Child final result"]
  end

  P1 --> P2
  P2 --> C1
  C1 --> C2 --> C3 --> C4
  C4 --> P3

截至 2026-07-17,Codex 把 subagent 工作放进独立 agent thread,主线程收集返回结果;subagent 继承父 turn 的 sandbox 与 permission mode,custom agent 可以进一步收窄。Claude Code 的普通 subagent 也从新上下文开始,只接收委托消息、自己的 system prompt、配置允许的项目指令和预加载 Skill;完成后通常只有摘要和少量 metadata 回到主会话。Claude Code 的 fork 是例外,它继承父会话历史,但自己的工具调用仍留在 fork 中。

OpenCode 以 child session 表示 subagent,并允许 primary agent 自动调用或由用户显式 @ 调用。其 task permission 可以限制哪些 subagent 能启动。不同产品对子 agent 传递规则、git 状态、Skill 和权限的细节不同,委托 prompt 不能假设“子 agent 自动知道父会话里的一切”。

Subagent 返回过细,主上下文仍会被污染;返回过粗,父 agent 又无法验证。比较稳的返回结构包括结论、证据位置、执行过的验证、未解决风险和建议下一步。必要时保留 child thread 或 artifact 引用,不把完整 transcript 复制回来。

Subagent 是一种 Agent 边界,不必与工作阶段边界重合。何时继续当前 turn、compact、delegate 或从 checkpoint 冷启动,见《Agent 会话边界设计》

编排层处在 turn 之外

OMX 这类 meta-runtime 不是另一种 tool message。官方项目把自己定义为 Codex CLI 上的 workflow layer,并把 guidance、plans、logs 和 state 放进 .omx/。它处理的是模式路由、角色、团队、checkpoint、后台任务和恢复,再把当前阶段需要的信息交给宿主 agent。

这类编排状态不应全量塞进每次模型调用。当前目标、阶段、owner、blocker 和验收条件适合进入上下文;完整队列、历史指标、所有 worker 日志适合留在外部状态,需要时再检索。宿主的 permission 与 sandbox 仍然约束最终工具执行。

Trace 要记录上下文变换

只记录“调用了什么工具”不足以解释一次 turn。可调试 trace 至少要覆盖以下事件:

1
2
3
4
5
6
7
8
9
10
11
12
turn.started
context.built
hook.completed
model.requested
model.tool_requested
permission.decided
tool.completed
message.appended
compaction.started / completed
subagent.started / returned
verification.completed
turn.stopped

context.built 不必保存所有敏感正文,但应保存 manifest:加载了哪些规则、暴露了哪些 Skill 和 MCP server、使用了哪个 compact summary、哪些来源被裁剪。permission.decided 要记录命中的策略与审批结果。tool.completed 要区分协议错误、执行错误、sandbox 拒绝和正常结果。turn.stopped 要有完成、阻塞、取消或失败原因。

Trajectory 只能复述模型和工具走过的路径。Control-plane trace 还能说明某条信息何时进入上下文、哪个 hook 改过参数、哪个权限门拒绝了动作、compact 丢掉了什么,以及父 agent 最终收到了哪份子任务摘要。

一组运行时不变量

一次 turn 能否稳定收敛,可以用几条不变量检查:

  • 目录与正文分离。Tool schema、Skill description 负责发现,正文和结果按需进入。
  • 每个副作用动作先经过权限和执行边界,再产生工具结果。
  • 每个工具结果都作为新的 observation 回到 loop,错误和拒绝也不例外。
  • 验证证据与完成 claim 对应,Stop 不能只依赖模型措辞。
  • Compact 有明确的恢复来源,摘要不承担永久记忆职责。
  • Subagent 有独立上下文契约,父会话只接收足以判断的结果。
  • Trace 能还原上下文变换和控制面决策,同时避免把密钥与原始敏感数据写进日志。

模型决定下一步动作,运行时决定模型在什么信息和权力边界内做这个决定。Hook、permission、Skill、MCP、compact 和 subagent 都在修改这条边界,只是发生的位置不同。

系列文章

参考资料

版本快照:2026-07-17。文中产品行为只采用当日可访问的官方文档;事件名、Skill 生命周期、compact 预算和权限默认值都可能随版本变化。