fix(agent-loop): settle rejected admission context
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-22-unified-send-and-coalesced-user-messages.md
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.md: 1d7d0fe6f4ed02474ca69cb2dd2dd59afadbdabe
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 6c9603118ac1b15e23947fc9820cbc3c4352fe9b
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.md: 63af2a34a331e0008009457d5dbc5b9381795ace
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: baf81b064899a58ac3d370187135ac99afbbdec7
|
||||
|
||||
@@ -24,7 +24,7 @@ Separately, `context/message` and `user/message` had converged: the surface proj
|
||||
|
||||
**Three inbox events replace agent/queued.** `agent/inbox/enqueue` (an item entered a FIFO), `agent/inbox/dequeue` (the driver claimed one), and `agent/inbox/discard` (`cancel()` dropped pending items) type their `AgentMessage` payload with only the accepted message's returned `id`, content, and source. Enqueue separately carries the resolved `queued | steering` placement captured by the producer at acceptance time, so observers and reconnect mirrors never reconstruct routing from later status or session history. Injection never touches a FIFO and emits none of these. Every FIFO entry publishes an enqueue, including steering submitted by an `agent/turn-stopping` listener, so the ledger stays balanced with its later dequeue or discard. The `dsh-agent` invariant companion asserts FIFO conservation: a per-agent outstanding count that dequeue and discard can never drive negative.
|
||||
|
||||
**Admission accepts next-step input without becoming a turn.** The loop opens a private next-step acceptance window before `agent/prompt-submit`, keeps it open through the turn, and closes it before `turn/end`. Steering and injection received during admission therefore remain together in the outbox and join an allowed turn; a blocked or failed admission leaves caller-staged input available to retry while writing no admitted prompt or hook-produced context. Closing the window before `turn/end` preserves the rule that reentrant late steering becomes an independent queued turn. `Agent.acceptsNextStep` exposes whether a `next-step` send would currently join this window; `status` remains the broader activity signal rather than a routing predicate.
|
||||
**Admission accepts next-step input without becoming a turn.** The loop opens a private next-step acceptance window before `agent/prompt-submit`, keeps it open through the turn, and closes it before `turn/end`. Steering and injection received during admission therefore remain together in the outbox and join an allowed turn. If admission blocks or fails, a context-only caller batch takes idle injection's immediate append, while steering and context staged beside it remain available to retry; neither path writes the rejected prompt or hook-produced context. Closing the window before `turn/end` preserves the rule that reentrant late steering becomes an independent queued turn. `Agent.acceptsNextStep` exposes whether a `next-step` send would currently join this window; `status` remains the broader activity signal rather than a routing predicate.
|
||||
|
||||
**One accepted message keeps one representation.** Durable user-role input and additional model-facing context both use `UserMessageData { content, source }` directly; public `AgentMessage` extends it with the correlation `id`, and the loop-private `PendingMessage` extends that with `wakeup`. A queued message that becomes steering enters the outbox as the same `PendingMessage` object, while injected and tool-produced context enters as plain `UserMessageData`. The outbox therefore stores their union directly instead of wrapping steering beside a duplicate copy of its content and source. Provider-native assistant messages remain adapter-owned output types and do not participate in this input hierarchy.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
|
||||
|
||||
**三个 inbox 事件取代 agent/queued。** `agent/inbox/enqueue`(一个队列项进入某个 FIFO)、`agent/inbox/dequeue`(驱动器认领了一个)和 `agent/inbox/discard`(`cancel()` 丢弃了待处理项)都将各自的 `AgentMessage` 载荷类型限定为仅包含被接受消息所返回的 `id`、内容和来源。enqueue 还会单独携带生产方在接受消息时捕获的已解析 `queued | steering` 放置方式,因此观察方和重连镜像永远不必根据后续状态或会话历史重建路由。注入从不触及 FIFO,也不发出这些事件中的任何一个。每一次 FIFO 入队都会发布一个 enqueue 事件,包括 `agent/turn-stopping` 监听器提交的 steering,因此账目会与其后的 dequeue 或 discard 保持平衡。`dsh-agent` 的不变量配套断言 FIFO 守恒:一个按 agent 计的未结算计数,dequeue 和 discard 永远无法把它压到负数。
|
||||
|
||||
**准入接受 next-step 输入,但不会因此成为一个轮次。** 循环会在 `agent/prompt-submit` 前打开一个私有的 next-step 接受窗口,使其贯穿整个轮次,并在 `turn/end` 前关闭。因此,在准入期间收到的 steering 和注入会一起留在 outbox 中并加入获准轮次;准入被阻止或失败时,调用方暂存的输入仍可重试,同时不会写入已准入提示词或钩子产生的上下文。在 `turn/end` 前关闭窗口,可以保留这样的规则:可重入的晚到 steering 会成为一个独立的排队轮次。`Agent.acceptsNextStep` 会公开一次 `next-step` 发送当前是否会加入该窗口;`status` 仍是更宽泛的活动信号,而非路由判据。
|
||||
**准入接受 next-step 输入,但不会因此成为一个轮次。** 循环会在 `agent/prompt-submit` 前打开一个私有的 next-step 接受窗口,使其贯穿整个轮次,并在 `turn/end` 前关闭。因此,在准入期间收到的 steering 和注入会一起留在 outbox 中并加入获准轮次。如果准入被阻止或失败,仅含调用方上下文的批次会采用空闲注入的立即追加行为,而 steering 及与其一同暂存的上下文仍可重试;两种路径都不会写入被拒绝的提示词或钩子产生的上下文。在 `turn/end` 前关闭窗口,可以保留这样的规则:可重入的晚到 steering 会成为一个独立的排队轮次。`Agent.acceptsNextStep` 会公开一次 `next-step` 发送当前是否会加入该窗口;`status` 仍是更宽泛的活动信号,而非路由判据。
|
||||
|
||||
**一条已接受消息只保留一种表示。** 持久的用户角色输入和附加的模型可见上下文都直接使用 `UserMessageData { content, source }`;公开的 `AgentMessage` 在此基础上增加用于关联的 `id`,循环私有的 `PendingMessage` 再增加 `wakeup`。一条成为 steering 的排队消息会以同一个 `PendingMessage` 对象进入 outbox,而注入和工具产生的上下文则以普通 `UserMessageData` 进入。因此,outbox 直接存储这两种类型的联合,而不再把 steering 与一份重复的内容和来源副本包装在一起。提供方原生的助手消息仍是适配器拥有的输出类型,不参与这套输入层级。
|
||||
|
||||
|
||||
@@ -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-24-separate-context-injection-from-turn-execution.md
|
||||
2026-07-24-separate-context-injection-from-turn-execution.md: d3603a744301f8a1b7e4536c93dfe981c518d8a1
|
||||
2026-07-24-separate-context-injection-from-turn-execution.zh.md: 5bc655543e0d008b887e3c5fc1dac6b8cb103767
|
||||
2026-07-24-separate-context-injection-from-turn-execution.md: 37798795b706719ca46a4495ef3f82a7968c501f
|
||||
2026-07-24-separate-context-injection-from-turn-execution.zh.md: 560f97d00c491adad4deeeeee8c8ed7ea8b88028
|
||||
|
||||
@@ -30,7 +30,7 @@ During prompt admission or an open turn, `inject()` stages context in the loop o
|
||||
|
||||
Outside that window, `inject()` appends its `user/message` immediately. It does not increment turn numbering, emit `turn/start` or `turn/end`, change agent status, or run the model; persistence observes the append through `session/event`.
|
||||
|
||||
If prompt admission blocks or fails, caller-staged next-step input remains in the outbox for `retry()` or a later admitted prompt; cancellation or disposal may discard it. Hook-produced `additionalContexts` never materialize because they belong to the rejected admission decision.
|
||||
If prompt admission blocks or fails, a caller-staged context-only batch appends immediately without a turn. Steering and context staged beside it remain in the outbox for `retry()` or a later admitted prompt; cancellation or disposal may discard them. Hook-produced `additionalContexts` never materialize because they belong to the rejected admission decision.
|
||||
|
||||
The session invariant permits `user/message` between turns while continuing to require turn enclosure for execution events, steering, assistant output, tools, and package-added events by default. Persistence, recovery, resume, fork, and compaction treat a valid out-of-turn `user/message` as committed session history rather than an interrupted or discardable turn tail.
|
||||
|
||||
@@ -63,7 +63,7 @@ This decision preserves the caller-owned framing decision from [unwrapped inject
|
||||
- Prompt-prefix placement, prompt envelopes, and `context/message` are absent from public types, durable events, projection, and UI replay.
|
||||
- Idle `inject()` appends one sourced `user/message` without a turn or model call.
|
||||
- Admission-time and active-turn injection drain at safe boundaries after complete tool-result batches and before the request that consumes them.
|
||||
- Blocked prompt admission opens no turn and appends neither the prompt nor hook-produced additional contexts; caller-staged next-step input remains available to retry.
|
||||
- Blocked prompt admission opens no turn and appends neither the prompt nor hook-produced additional contexts; caller context alone falls back to an idle append, while a steering boundary remains available to retry.
|
||||
- Unit, persistence/resume, invariant, host/client queue, and TUI coverage pin event order, admission ownership, and reconnect classification.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -30,7 +30,7 @@ agent API 曾用三种相互重叠的方式表示面向模型的补充输入:
|
||||
|
||||
在该窗口之外,`inject()` 会立即追加对应的 `user/message`。它不会增加轮次编号、发出 `turn/start` 或 `turn/end`、改变 agent 状态,也不会运行模型;持久化通过 `session/event` 观察这次追加。
|
||||
|
||||
如果提示词准入被阻止或失败,调用方暂存的 next-step 输入会留在 outbox 中,供 `retry()` 或后续获准提示词使用;取消或 dispose(资源释放)可能丢弃这些输入。钩子产生的 `additionalContexts` 属于被拒绝的准入决策,因此永远不会落入日志。
|
||||
如果提示词准入被阻止或失败,调用方暂存的仅含上下文的批次会立即追加,且不产生轮次。steering 及与其一同暂存的上下文会留在 outbox 中,供 `retry()` 或后续获准提示词使用;取消或 dispose(资源释放)可能丢弃它们。钩子产生的 `additionalContexts` 属于被拒绝的准入决策,因此永远不会落入日志。
|
||||
|
||||
会话不变量允许 `user/message` 位于两个轮次之间,同时继续要求执行事件、steering、助手输出、工具事件以及默认的包扩展事件均受轮次边界约束。持久化、恢复、resume、fork 和压缩会把合法的轮次外 `user/message` 当作已提交会话历史,而不是中断轮次或可丢弃的日志尾部。
|
||||
|
||||
@@ -63,7 +63,7 @@ agent API 曾用三种相互重叠的方式表示面向模型的补充输入:
|
||||
- 公共类型、持久事件、投影和 UI 回放中均不存在 prompt-prefix 放置方式、提示词封套与 `context/message`。
|
||||
- 空闲 `inject()` 在不产生轮次或模型调用的情况下,追加一条带来源的 `user/message`。
|
||||
- 准入期间和活跃轮次中的注入会在完整工具结果批次之后的安全边界排空,并在消费它们的请求之前进入日志。
|
||||
- 被提示词准入阻止的消息不会打开轮次,也不会追加提示词或钩子产生的额外上下文;调用方暂存的 next-step 输入仍可重试。
|
||||
- 被阻止的提示词准入不会打开轮次,也不会追加提示词或钩子产生的额外上下文;仅有调用方上下文时会回退为空闲追加,而带 steering 的边界仍可重试。
|
||||
- 单元测试、持久化与 resume 测试、不变量测试、宿主/客户端队列测试和 TUI 覆盖会固定事件顺序、准入归属和重连分类。
|
||||
|
||||
## 后果
|
||||
|
||||
Reference in New Issue
Block a user