fix(agent-loop): open turns before pre-step
This commit is contained in:
@@ -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/architecture/2026-07-28-identified-immutable-message-values.md
|
||||
2026-07-28-identified-immutable-message-values.md: a6cca5c3104cdb300aa076001ee7622007b2be3d
|
||||
2026-07-28-identified-immutable-message-values.zh.md: a48a20ecee129d67ace3dcae2f5da59c1c1f4e61
|
||||
2026-07-28-identified-immutable-message-values.md: 472de8b133ba323c3e1ff5e53c8dacb3d66525c5
|
||||
2026-07-28-identified-immutable-message-values.zh.md: af3624c97a2f40faa92dab245cbcafe8d7b6463f
|
||||
|
||||
@@ -38,7 +38,7 @@ Any operation that changes only the representation of an existing semantic messa
|
||||
|
||||
Every message producer must choose creation or import explicitly, and tests construct complete values rather than partial content/source records. UUID generation moves outward to the first semantic creation point, so deterministic fixtures that provide an existing id use `freezeMessage()` instead of `createMessage()`.
|
||||
|
||||
Live inbox events, durable events, derived history, and model requests can correlate one message without content equality or envelope-specific ids. Claim policy and UI attachment cleanup can compare `MessageId` before a turn exists. Deep freezing prevents a producer, hook, or observer from changing the value after identity is established.
|
||||
Live inbox events, durable events, derived history, and model requests can correlate one message without content equality or envelope-specific ids. Pending-input policy and UI attachment cleanup can compare `MessageId` before a turn exists, while claims retain that identity inside the open turn. Deep freezing prevents a producer, hook, or observer from changing the value after identity is established.
|
||||
|
||||
The shared representation removes the old `UserMessageData`/`AgentMessage` split and folds provider provenance into typed message sources. Event envelopes still own facts that are not message semantics, such as turn and step position, token usage, internal tool failure identity, and presentation metadata.
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ harness 曾存在多种形似消息的表示,各自采用不同的标识规则
|
||||
|
||||
每个消息生产方都必须显式选择创建或导入,测试也会构造完整值,而不是不完整的内容/来源记录。UUID 的生成会前移至最初的语义创建点,因此提供已有 id 的确定性 fixture 会使用 `freezeMessage()`,而不是 `createMessage()`。
|
||||
|
||||
实时 inbox 事件、持久事件、派生历史和模型请求可以关联同一条消息,无需比较内容或使用封装专用 id。领取策略和 UI 附件清理可以在轮次存在之前比较 `MessageId`。深度冻结可以防止生产方、钩子或观察方在标识建立后更改消息值。
|
||||
实时 inbox 事件、持久事件、派生历史和模型请求可以关联同一条消息,无需比较内容或使用封装专用 id。待处理输入策略和 UI 附件清理可以在轮次存在之前比较 `MessageId`,领取后则会在已打开的轮次内保留该标识。深度冻结可以防止生产方、钩子或观察方在标识建立后更改消息值。
|
||||
|
||||
共享表示移除了旧的 `UserMessageData`/`AgentMessage` 划分,并将提供方溯源信息纳入带类型的消息来源。事件封装仍持有不属于消息语义的事实,例如轮次与步骤位置、token 用量、内部工具失败标识和呈现元数据。
|
||||
|
||||
|
||||
@@ -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/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
|
||||
2026-07-31-claimed-pre-step-inbox-lifecycle.md: 06bc875722d9dfea118934e9e025b4ef60b54e65
|
||||
2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: a2f61aaf5cd971dd4b7a210532db495ca70a8a14
|
||||
2026-07-31-claimed-pre-step-inbox-lifecycle.md: 06a806375b5f4fe67271ebab842e9e27d5e4824d
|
||||
2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: ad6da9285a5a9f6882b887f2c4e62903ae2b0f09
|
||||
|
||||
@@ -12,9 +12,9 @@ Occurrence-local inbox wrappers also duplicated the identity already carried by
|
||||
|
||||
## Decision
|
||||
|
||||
Before every proposed step, `Inbox.claim(target)` atomically removes the complete batch: all `next-step` messages and, at a turn boundary, one `next-turn` message. Claiming records normalized `agent/inbox/spliced` pure deletions with no outcome. The loop then emits `agent/inbox/claimed { message, turn }` once per claimed message and awaits the single `agent/pre-step` waterfall with that exclusive batch and `{ turn, step, signal }`.
|
||||
Before every proposed step, `Inbox.claim(target)` atomically removes the complete batch: all `next-step` messages and, at a turn boundary, one `next-turn` message. At the initial boundary the loop first commits `turn/start`, so the claim and its single `agent/pre-step` decision have durable turn ownership. Claiming records normalized `agent/inbox/spliced` pure deletions with no outcome. The loop then emits `agent/inbox/claimed { message, turn }` once per claimed message and awaits the waterfall with that exclusive batch and `{ turn, step, signal }`.
|
||||
|
||||
`PreStepDecision` is `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`. Reject opens no turn or step and leaves the claimed batch removed. Enter supplies the complete batch appended as `user/message` events after `step/start`. A listener wrapping `next()` preserves downstream changes unless it intentionally replaces them, so all message rewrites settle once in the final return value. There is no `agent/prompt-prepare`, `agent/prompt-submit`, or `agent/step` seam.
|
||||
`PreStepDecision` is `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`. Reject opens no step, leaves the claimed batch removed, and closes the turn as blocked with `step: 0`. Empty entry, cancellation, and failure before `step/start` likewise close a balanced no-step turn. Enter supplies the complete batch appended as `user/message` events after `step/start`. A listener wrapping `next()` preserves downstream changes unless it intentionally replaces them, so all message rewrites settle once in the final return value. There is no `agent/prompt-prepare`, `agent/prompt-submit`, or `agent/step` seam.
|
||||
|
||||
The durable inbox remains two `UserMessage[]` lists addressed by `MessageId`. `append`, `prepend`, and `splice` take a target, while `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists before committing a normalized splice. Replacement may change identity and emits the old message as discarded followed by the new message as inserted. Every insertion emits `agent/inbox/inserted { message }`; an ordinary removal records `outcome: 'canceled'` and emits `agent/inbox/discarded { message }`. These live events add no placement, outcome, or batch fields.
|
||||
|
||||
@@ -34,7 +34,7 @@ The archived [addressable queue occurrence decision](../../archived/feature/2026
|
||||
|
||||
## Verification
|
||||
|
||||
Agent-loop coverage pins claim-before-pre-step ordering, exact live event payloads, rejection before any turn, final-batch rewriting, input inserted after a claim, listener failure, and cancellation. Inbox and consumer tests pin pure claim deletions, canceled ordinary removals, workspace-context delayed insertion and replacement, plan/goal/hook behavior, UI cleanup, compaction, checkpointing, and resumed durable projection. Generated event and type catalogs expose only the new seam and payloads.
|
||||
Agent-loop coverage pins turn-start-before-claim-before-pre-step ordering, exact live event payloads, balanced no-step rejection, final-batch rewriting, input inserted after a claim, listener failure, and cancellation. Inbox and consumer tests pin pure claim deletions, canceled ordinary removals, workspace-context delayed insertion and replacement, plan/goal/hook behavior, UI cleanup, compaction, checkpointing, and resumed durable projection. Generated event and type catalogs expose only the new seam and payloads.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
每个拟议步骤之前,`Inbox.claim(target)` 会原子移除完整批次:全部 `next-step` 消息,以及轮次边界上的一条 `next-turn` 消息。领取会记录规范化、不带 outcome 的纯删除 `agent/inbox/spliced`。随后,循环针对每条已领取消息发出一次 `agent/inbox/claimed { message, turn }`,并用该独占批次与 `{ turn, step, signal }` 等待唯一的 `agent/pre-step` waterfall(瀑布式事件)。
|
||||
每个拟议步骤之前,`Inbox.claim(target)` 会原子移除完整批次:全部 `next-step` 消息,以及轮次边界上的一条 `next-turn` 消息。在首次边界,循环会先提交 `turn/start`,使领取及其唯一一次 `agent/pre-step` 决策拥有持久轮次归属。领取会记录规范化、不带 outcome 的纯删除 `agent/inbox/spliced`。随后,循环针对每条已领取消息发出一次 `agent/inbox/claimed { message, turn }`,并用该独占批次与 `{ turn, step, signal }` 等待 waterfall(瀑布式事件)。
|
||||
|
||||
`PreStepDecision` 为 `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`。reject 不会打开轮次或步骤,并让已领取批次保持已删除。enter 提供在 `step/start` 后以 `user/message` 追加的完整批次。包装 `next()` 的监听器会保留下游变更,除非有意替换,因此全部消息改写只在最终返回值中一次性结算。系统不再存在 `agent/prompt-prepare`、`agent/prompt-submit` 或 `agent/step` seam。
|
||||
`PreStepDecision` 为 `{ kind: 'reject' } | { kind: 'enter'; messages: UserMessage[] }`。reject 不会打开步骤,会让已领取批次保持已删除,并以 `step: 0` 将轮次关闭为 blocked。空的 enter、取消以及 `step/start` 前的失败同样会关闭一个边界平衡的无步骤轮次。enter 提供在 `step/start` 后以 `user/message` 追加的完整批次。包装 `next()` 的监听器会保留下游变更,除非有意替换,因此全部消息改写只在最终返回值中一次性结算。系统不再存在 `agent/prompt-prepare`、`agent/prompt-submit` 或 `agent/step` seam。
|
||||
|
||||
持久 inbox 仍是两份通过 `MessageId` 寻址的 `UserMessage[]` 列表。`append`、`prepend` 与 `splice` 接受 target;`replace(messageId, newMessage)` 与 `remove(messageId)` 则在提交规范化 splice 前,通过 `MessageId` 跨两份列表定位待处理消息。替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。每次插入发出 `agent/inbox/inserted { message }`;普通删除记录 `outcome: 'canceled'` 并发出 `agent/inbox/discarded { message }`。这些实时事件不增加 placement、outcome 或批次字段。
|
||||
|
||||
@@ -34,7 +34,7 @@ Status: implemented
|
||||
|
||||
## 验证
|
||||
|
||||
Agent-loop 覆盖固定先领取再 pre-step 的顺序、实时事件的确切载荷、打开任何轮次前的 reject、最终批次改写、领取后插入的输入、监听器失败与取消。Inbox 和消费方测试固定纯领取删除、普通删除的 canceled 结果、workspace-context 的延后插入与替换、plan/goal/hook 行为、UI 清理、压缩、检查点以及恢复后的持久投影。生成的事件与类型目录只公开新的 seam 与载荷。
|
||||
Agent-loop 覆盖固定先 `turn/start`、再领取、后 pre-step 的顺序、实时事件的确切载荷、边界平衡的无步骤 reject、最终批次改写、领取后插入的输入、监听器失败与取消。Inbox 和消费方测试固定纯领取删除、普通删除的 canceled 结果、workspace-context 的延后插入与替换、plan/goal/hook 行为、UI 清理、压缩、检查点以及恢复后的持久投影。生成的事件与类型目录只公开新的 seam 与载荷。
|
||||
|
||||
## 后果
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`,而钩子驱动的快照端到端验证可观测的桥接行为。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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` 授权恢复。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Status: implemented
|
||||
|
||||
`agent/pre-step` 瀑布是进入栅栏。正数目标来源只有在完全匹配驱动器待处理的身份和内容、实时目标仍具有相同 id 与修订号、激活态仍为 armed,并且该回合仍是下一个编号时才会进入步骤。插件在委托下游监听器前检查一次,在下游返回后再检查一次。第二次检查防止异步监听器编辑或暂停目标后,旧提示词仍进入步骤。
|
||||
|
||||
只有最终产生的 `user/message` 才是进入步骤的目标回合,并推进目标折叠。陈旧预留会在轮次打开前被 reject;驱动器会把它标记为陈旧,不消耗回合数。若下游策略拒绝并非由陈旧状态导致,目标会进入 blocked,而不会绕过该策略自动重试。
|
||||
只有最终产生的 `user/message` 才是进入步骤的目标回合,并推进目标折叠。陈旧预留会关闭一个 blocked 的无步骤轮次;驱动器会把它标记为陈旧,不消耗回合数。若下游策略拒绝并非由陈旧状态导致,目标会进入 blocked,而不会绕过该策略自动重试。
|
||||
|
||||
### 人类工作与修订竞争
|
||||
|
||||
|
||||
@@ -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/simplification/2026-07-17-one-send-one-turn.md
|
||||
2026-07-17-one-send-one-turn.md: 6d2e9f57caf73754ea8c8d9e711126de2e7c885c
|
||||
2026-07-17-one-send-one-turn.zh.md: 1d2e74bbcf9478020cf0dfcf7ebdbb2349a86022
|
||||
2026-07-17-one-send-one-turn.md: 834eb2b3aace0a0aa5f86f483e7b7d7aabbe467e
|
||||
2026-07-17-one-send-one-turn.zh.md: aff3164d58dde238863a60fbcdbab6dab2d09961
|
||||
|
||||
@@ -20,7 +20,7 @@ Before inserting a message, `send()` checks the agent state and accepts an alrea
|
||||
|
||||
If messages A and B are both processed, B's turn starts only after A records `turn/end` and A's durability checkpoint settles. B's request therefore sees whatever closed result A left in the same session log. A checkpoint error is reported, but settlement only releases this ordering barrier; it does not make a failed write durable. Broad `cancel()`, disposal, or a failure before `turn/start` can instead discard an unstarted item without opening an empty turn.
|
||||
|
||||
At a turn boundary, the loop claims one follow-up after pending next-step input. `agent/pre-step` either rejects the proposal or returns the complete entering batch. A rejected follow-up remains removed without opening a turn or writing session history. Mixed ordinary follow-up branches do not exist.
|
||||
At a turn boundary, the loop opens the turn and claims one follow-up after pending next-step input. `agent/pre-step` either rejects the proposal or returns the complete entering batch. A rejected follow-up remains removed and closes a blocked no-step turn without writing model-visible history. Mixed ordinary follow-up branches do not exist.
|
||||
|
||||
The no-batching rule applies only to ordinary follow-up input. `steer()` puts input in the next-step inbox and wakes the driver. During a turn, the loop can claim it at a later step boundary; while idle, the waking next-step batch starts a new turn. Input arriving after a batch was claimed waits for a later boundary, while cancellation or disposal can discard it.
|
||||
|
||||
@@ -35,7 +35,7 @@ The no-batching rule applies only to ordinary follow-up input. `steer()` puts in
|
||||
- Unit and property tests submit sends from the same stack, neighboring microtasks, different producers, and reentrant callbacks; every message gets its own FIFO-ordered turn.
|
||||
- A built-stdio test submits two lines and observes two model requests and two turn boundaries.
|
||||
- Delayed and rejected first-turn checkpoints keep the next turn waiting and prove that its request sees the preceding assistant result.
|
||||
- Failure-path tests cover pre-step rejection, listener failure, broad cancellation, disposal, and failure before `turn/start`; rejection creates no turn, recorded turns stay balanced, messages do not merge, and surviving later work still drains.
|
||||
- Failure-path tests cover pre-step rejection, listener failure, broad cancellation, disposal, and failure before `turn/start`; initial pre-step exits close balanced no-step turns, messages do not merge, and surviving later work still drains.
|
||||
- Separate tests cover open-turn, failed-turn, and idle `steer()`, pending `inject()`, whole-agent status, and `whenIdle()`.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -20,7 +20,7 @@ Status: implemented
|
||||
|
||||
如果消息 A、B 都进入处理,B 的轮次只能在 A 记录 `turn/end` 且 A 的持久性检查点处理结束后开始。因此,B 的请求能看到 A 在同一会话日志中留下的已关闭结果。检查点错误会照常报告,但处理结束只表示解除这道顺序屏障,不表示失败的写入已经持久化。广义 `cancel()`、dispose(资源释放)或 `turn/start` 之前的失败也可能丢弃尚未启动的队列项,而不打开一个空轮次。
|
||||
|
||||
轮次边界上,循环会在待处理 next-step 输入之后领取一条 follow-up。`agent/pre-step` 要么拒绝提案,要么返回进入步骤的完整批次。被拒绝的 follow-up 保持已删除,不打开轮次,也不写入会话历史。实现中不存在混合普通 follow-up 分支。
|
||||
轮次边界上,循环会先打开轮次,再在待处理 next-step 输入之后领取一条 follow-up。`agent/pre-step` 要么拒绝提案,要么返回进入步骤的完整批次。被拒绝的 follow-up 保持已删除,并关闭一个 blocked 的无步骤轮次,不写入模型可见历史。实现中不存在混合普通 follow-up 分支。
|
||||
|
||||
上述不合批规则只适用于普通 follow-up 输入。`steer()` 会把输入放入 next-step inbox 并唤醒驱动器。在轮次期间,循环可以在后续步骤边界领取它;agent 空闲时,这个会唤醒的 next-step 批次会启动一个新轮次。批次被领取后才到达的输入会等待后续边界,而取消或 dispose 可以将其丢弃。
|
||||
|
||||
@@ -35,7 +35,7 @@ Status: implemented
|
||||
- 单元测试和性质测试从同一调用栈、相邻微任务、不同生产方和重入回调提交 send;每条消息都会得到一个按 FIFO 排序的独立轮次。
|
||||
- stdio 构建产物测试提交两行输入,并观察到两个模型请求和两个轮次边界。
|
||||
- 延迟和拒绝第一个轮次的检查点,都能让下一个轮次保持等待,并证明其请求可以看到前一条助手结果。
|
||||
- 失败路径测试覆盖 pre-step reject、监听器失败、广义取消、dispose 和 `turn/start` 之前的失败;reject 不会创建轮次,已记录的轮次保持边界平衡,消息不会合并,之后仍需处理的工作也能继续清空。
|
||||
- 失败路径测试覆盖 pre-step reject、监听器失败、广义取消、dispose 和 `turn/start` 之前的失败;首次 pre-step 的各种退出都会关闭边界平衡的无步骤轮次,消息不会合并,之后仍需处理的工作也能继续清空。
|
||||
- 其他测试分别覆盖轮次打开时、轮次失败后和空闲时的 `steer()`,以及待处理的 `inject()`、面向整个 agent 的状态和 `whenIdle()`。
|
||||
|
||||
## 后果
|
||||
|
||||
Reference in New Issue
Block a user