fix(agent-loop): open turns before pre-step

This commit is contained in:
_Kerman
2026-08-04 13:49:35 +08:00
parent 0ac95437b4
commit d4fa26023d
154 changed files with 1104 additions and 1099 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-hook-bridges.md
2026-06-30-hook-bridges.md: 39164c8462391baab1e92a54d80cfbc2963f3157
2026-06-30-hook-bridges.zh.md: 5565b04b3985dea11133387904dd2fb1ba35103d
2026-06-30-hook-bridges.md: a31116818c57e18d04e656f371efd1101a63b6af
2026-06-30-hook-bridges.zh.md: d071186eeb503b262c52886bf88a1c05022e0a44

View File

@@ -37,7 +37,7 @@ The CC bridge's `ask` result is a real permission path, not a terminal bridge de
Every bridge `inject()` and additional-context input explicitly passes `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`. Unit coverage pins the resulting `user/message.source` as the plugin rather than the user.
`UserPromptSubmit` runs at pre-step. For an initial follow-up it executes before any turn opens and therefore writes no turn-scoped `hook/invoked` / `hook/result` pair: rejection leaves the claimed input removed with no transcript, while entered additional context is durably represented by its sourced `user/message`. The Codex payload still receives the candidate next `turn_id`; rejection does not consume that number.
`UserPromptSubmit` runs at pre-step after `turn/start`, so every invocation writes its turn-scoped `hook/invoked` / `hook/result` pair. Rejection leaves the claimed input removed, closes the turn as blocked with no step, and retains the hook pair as its durable decision evidence. The Codex payload receives that open turn's `turn_id`.
### Adding context is not a veto — delegate, then prepend

View File

@@ -37,7 +37,7 @@ CC 桥接的 `ask` 结果是一条真正的权限路径,而非终态桥接决
每个桥接的 `inject()` 和 additional-context 输入都显式传入 `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`。单元测试覆盖率固定验证结果中的 `user/message.source` 为插件而非用户。
`UserPromptSubmit` 在 pre-step 运行。对于初始 follow-up,它发生在任何轮次开启前,因此不会写入轮次范围的 `hook/invoked` / `hook/result` 对:reject 让已领取输入保持删除且不留下 transcript(文本记录),而进入步骤的额外上下文由其带来源的 `user/message` 持久呈现。Codex payload 仍会收到候选的下一个 `turn_id`;reject 不会消耗该编号。
`UserPromptSubmit` 在 `turn/start` 之后的 pre-step 运行,因此每次调用都会写入轮次范围的 `hook/invoked` / `hook/result` 对。reject 会让已领取输入保持删除,将轮次关闭为 blocked 且不包含步骤,并保留该 hook 对作为持久决策证据。Codex payload 会收到这个已打开轮次的 `turn_id`。
### 添加上下文不是否决——先 delegate,再 prepend

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-interception-seams.md
2026-06-30-interception-seams.md: 07d4ca28030ac5ba8a8c9325851fc34c3072e786
2026-06-30-interception-seams.zh.md: b71bccdb684c34fb55b11c1b7b0eca7892b23155
2026-06-30-interception-seams.md: 629a1aed509bd9bce9a2da89ce84b17a1db8e6b6
2026-06-30-interception-seams.zh.md: de282894fb0854153d9cf6ffd7bd2eb4f67bfde7

View File

@@ -35,7 +35,7 @@ Core dispatch and the tool body sit inside normalization boundaries, so tool, li
### Three load-bearing loop decisions
1. **Run pre-step policy at every proposed step.** The initial decision runs before the loop opens its turn, so rejection creates no turn or durable message. A tool continuation with no newly claimed input still submits an empty batch, allowing per-request context producers to add logged messages to that exact request. On enter, the loop opens the step and appends the returned batch as `user/message` events before request derivation. Each claimed follow-up remains the sole direct prompt in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md).
1. **Run pre-step policy at every proposed step.** The loop opens the turn before the initial claim and decision, so rejection closes a durable blocked turn with no step or model-visible message. A tool continuation with no newly claimed input still submits an empty batch, allowing per-request context producers to add logged messages to that exact request. On enter, the loop opens the step and appends the returned batch as `user/message` events before request derivation. Each claimed follow-up remains the sole direct prompt in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md).
2. **Post-tool `additionalContexts` and asynchronous injections enter the active-batch FIFO and append when that batch settles.** `content`/`feedback` shape the result `execute()` returns, but each context is a separate sourced `user/message`, and a single step or composite tool can produce many. Appending context immediately would interleave `result(c1) → context → result(c2)` or place nested context before its outer result, breaking tool-call/result adjacency. `ToolRunContext.deferContext()` therefore collects nested-dispatch context through failures, `execute()` surfaces the ordered array on `ToolExecutionResult`, and the loop accepts it into the same FIFO as `agent.inject()` calls made during execution. The FIFO appends after every recorded result when the batch settles, including before an interrupted turn closes. An accepted outer call preserves deferred contexts before decision contexts; an outer block discards deferred contexts and exposes only contexts explicitly supplied by the blocking decision.
@@ -56,4 +56,4 @@ The seam package does **not** declare `hook/*` session events (the durable hook-
## Consequences
The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, pre-step claim settlement, post-tool context buffering, and stopping; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge settles an initial pre-step rejection as `cancelled` after the agent becomes idle with no owned turn, while hook-driven snapshots verify the observable bridge behavior end to end.
The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, pre-step claim settlement, post-tool context buffering, and stopping; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge settles an initial pre-step rejection from its blocked no-step turn as `end_turn`, while hook-driven snapshots verify the observable bridge behavior end to end.

View File

@@ -35,7 +35,7 @@ harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那
### 三个承重的循环决策
1. **在每个拟议步骤运行 pre-step 策略。** 首次决策发生在循环开启轮次之前,因此 reject 不会创建轮次,也不产生持久消息。即使工具续步没有新取得所有权的输入,也会提交空批次,使逐请求上下文生产方可以把带日志的消息加入这一次请求。enter 时,循环先开启步骤,再把返回批次作为 `user/message` 追加,然后派生请求。依照[一次 send 对应一个轮次的简化](../simplification/2026-07-17-one-send-one-turn.md),每个已领取 follow-up 仍是其轮次中唯一的直接提示词。
1. **在每个拟议步骤运行 pre-step 策略。** 循环会在首次领取和决策之前打开轮次,因此 reject 会关闭一个持久、blocked 且不含步骤或模型可见消息的轮次。即使工具续步没有新取得所有权的输入,也会提交空批次,使逐请求上下文生产方可以把带日志的消息加入这一次请求。enter 时,循环先开启步骤,再把返回批次作为 `user/message` 追加,然后派生请求。依照[一次 send 对应一个轮次的简化](../simplification/2026-07-17-one-send-one-turn.md),每个已领取 follow-up 仍是其轮次中唯一的直接提示词。
2. **工具执行后的 `additionalContexts` 与异步注入进入活跃批次 FIFO,并在该批次结算时追加。** `content`/`feedback` 塑造 `execute()` 返回的结果,但每项上下文都是一条独立的带来源 `user/message`,而单个步骤或组合工具可以产生许多上下文。立即追加上下文会产生 `result(c1) → context → result(c2)` 的交错,或把嵌套上下文放在外层结果之前,破坏工具调用/结果邻接性。因此 `ToolRunContext.deferContext()` 会在失败路径上也收集嵌套调度上下文,`execute()` 在 `ToolExecutionResult` 上暴露有序数组,循环再把它接纳到与执行期间 `agent.inject()` 调用相同的 FIFO 中。FIFO 在批次结算时,于每个已记录结果之后追加,其中也包括被中断轮次关闭之前。被接受的外层调用将 deferred contexts 保留在 decision contexts 之前;被外层阻止时则丢弃 deferred contexts,只暴露阻止 decision 显式提供的上下文。
@@ -56,4 +56,4 @@ seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);
## 后果
规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层做包装,终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、pre-step 领取结算、工具执行后上下文缓冲和 stopping;`dsh-tools` 负责身份封存与五阶段执行流水线。它们的契约记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/core-data-structures/core.md#interception-decisions) 与[工具结构](../../../../docs/core-data-structures/tools.md)中。ACP 桥接在 agent 空闲且不再拥有轮次后,将初始 pre-step reject 结算为 `cancelled`,而钩子驱动的快照端到端验证可观测的桥接行为。
规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层做包装,终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、pre-step 领取结算、工具执行后上下文缓冲和 stopping;`dsh-tools` 负责身份封存与五阶段执行流水线。它们的契约记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/core-data-structures/core.md#interception-decisions) 与[工具结构](../../../../docs/core-data-structures/tools.md)中。ACP 桥接会把 blocked 无步骤轮次中的首次 pre-step reject 结算为 `end_turn`,而钩子驱动的快照端到端验证可观测的桥接行为。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-16-harness-level-loop.md: 0cebbcc305b1532d28aeffd6824edd870d42aec6
2026-07-16-harness-level-loop.zh.md: 911a5990b72e63f6a840b544648f94cadf0d1345
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-16-harness-level-loop.md
2026-07-16-harness-level-loop.md: 36a567204ee1082d48126369ee0b7277c9f24ca8
2026-07-16-harness-level-loop.zh.md: 4839a031bff6b5969d23359fe46daeb5064964ef

View File

@@ -60,7 +60,7 @@ Forked sessions inherit the durable goal prefix because that is the natural repl
The goal-round driver owns at most one pending reservation per exact live agent. It admits a reservation only when the goal is active and armed, the agent is idle, no competing human work exists, the latest mutation has passed its durability checkpoint, the exact goal id/revision/round still matches, and downstream pre-step policy accepts it. Its `agent/pre-step` fence checks those facts both before and after downstream listeners, preventing an edit, pause, human message, or unload race from admitting obsolete work.
Only an admitted positive-round goal-sourced `user/message` charges a round. A stale reservation is rejected before a turn opens without consuming the cap. A concurrent goal revision wins over settlement from an older round.
Only an admitted positive-round goal-sourced `user/message` charges a round. A stale reservation closes a blocked no-step turn without consuming the cap. A concurrent goal revision wins over settlement from an older round.
Normal turn completion schedules another round only while the goal remains active, armed, and below its cap. Cancellation pauses. Rate limiting or quota exhaustion blocks with code `usage-limited`; cap exhaustion blocks with `round-limit`; queue failure uses `queue-failed`; turn errors, max-token stops, policy rejection, and unknown terminal results use their corresponding blocker codes. An independently composed request-recovery plugin may retry transient provider failures within that same turn; the goal driver never invents another round after an abnormal terminal outcome. A human can later authorize resume through ordinary language or `/goal resume`.

View File

@@ -60,7 +60,7 @@ fork 会话会继承持久目标前缀,因为这是自然的重放结果。for
Goal Round 驱动器为每个准确实时 agent 至多拥有一个待定预留。只有 goal 处于活跃且已激活状态、agent 空闲、不存在竞争性人类工作、最新变更已经通过持久性检查点、准确 goal id/revision/Round 仍匹配,并且下游 pre-step 策略接受时,它才会接纳预留。其 `agent/pre-step` 围栏会在下游监听器前后检查这些事实,防止编辑、暂停、人类消息或卸载竞争接纳陈旧工作。
只有已准入、Round 为正数且来源为 goal 的 `user/message` 会计入一个 Round。陈旧预留会在轮次打开前被拒绝,不会消耗上限。并发 goal revision 会胜过旧 Round 的结算。
只有已准入、Round 为正数且来源为 goal 的 `user/message` 会计入一个 Round。陈旧预留会关闭一个 blocked 的无步骤轮次,不会消耗上限。并发 goal revision 会胜过旧 Round 的结算。
普通 Turn 完成后,只有目标仍活跃、已激活且低于上限时才会安排另一个 Round。取消会暂停。速率限制或配额耗尽以代码 `usage-limited` 阻塞;上限耗尽使用 `round-limit`;队列失败使用 `queue-failed`;Turn 错误、max-token 停止、策略拒绝与未知终止结果使用各自对应的阻塞代码。独立组合的请求恢复插件可以在同一个 Turn 内重试暂时性 provider 失败;目标驱动器绝不会在异常终止结果后凭空发起另一个 Round。人类随后可以通过普通语言或 `/goal resume` 授权恢复。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-19-same-session-goal-round-driver.md: adca7ab2b33f9742fe341a4b6f5cf1517f29374b
2026-07-19-same-session-goal-round-driver.zh.md: 10fa9180d4b75c39691d621a59d971a05d7cf380
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md
2026-07-19-same-session-goal-round-driver.md: 29f804ff9de5e69eb8af3063af1bed023fef1146
2026-07-19-same-session-goal-round-driver.zh.md: bcee71e28c61dfd43b1b2b797503554b3397bd73

View File

@@ -24,7 +24,7 @@ When an agent is idle, has no competing queued work, and its current goal is `ac
The `agent/pre-step` waterfall is the entry fence. A positive goal source enters only when it exactly matches the driver's pending identity and content, the live goal still has that id and revision, activation remains armed, and the round is still the next number. The plugin checks once before delegating and again after downstream listeners return. This second check prevents an async listener from editing or pausing the goal while still entering the old prompt.
Only the resulting `user/message` is an entered round and advances the goal fold. A stale reservation is rejected before a turn opens; the driver marks it stale and does not charge the round. A downstream policy rejection that is not caused by staleness blocks the goal rather than retrying around policy.
Only the resulting `user/message` is an entered round and advances the goal fold. A stale reservation closes a blocked no-step turn; the driver marks it stale and does not charge the round. A downstream policy rejection that is not caused by staleness blocks the goal rather than retrying around policy.
### Human work and revision races

View File

@@ -24,7 +24,7 @@ Status: implemented
`agent/pre-step` 瀑布是进入栅栏。正数目标来源只有在完全匹配驱动器待处理的身份和内容、实时目标仍具有相同 id 与修订号、激活态仍为 armed,并且该回合仍是下一个编号时才会进入步骤。插件在委托下游监听器前检查一次,在下游返回后再检查一次。第二次检查防止异步监听器编辑或暂停目标后,旧提示词仍进入步骤。
只有最终产生的 `user/message` 才是进入步骤的目标回合,并推进目标折叠。陈旧预留会在轮次打开前被 reject;驱动器会把它标记为陈旧,不消耗回合数。若下游策略拒绝并非由陈旧状态导致,目标会进入 blocked,而不会绕过该策略自动重试。
只有最终产生的 `user/message` 才是进入步骤的目标回合,并推进目标折叠。陈旧预留会关闭一个 blocked 的无步骤轮次;驱动器会把它标记为陈旧,不消耗回合数。若下游策略拒绝并非由陈旧状态导致,目标会进入 blocked,而不会绕过该策略自动重试。
### 人类工作与修订竞争