refactor(agent): minimize inbox message contract

This commit is contained in:
_Kerman
2026-07-24 17:00:42 +08:00
parent b3c1abac67
commit 90e69a3123
16 changed files with 58 additions and 124 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
2026-07-22-unified-send-and-coalesced-user-messages.md: dbd157ad4c81f278cf1417765816497b9e3568df
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: dea32f142c4ef005ff04d00ef8c8ae1ea19c9705
2026-07-22-unified-send-and-coalesced-user-messages.md: 359ea62b77685a267b7a8c36e58929f67569d92f
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: d4a0f661900a10a0b74ebdb67077881b6a9d24ee

View File

@@ -22,7 +22,7 @@ Separately, `context/message` and `user/message` had converged: the surface proj
**`send` returns an id.** `send` (and the aliases) return an opaque branded `AgentMessageId` for the accepted message; `send`'s previous return was `void`.
**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) each carry an `AgentMessage` the accepted message including its returned `id`, `target`/`wakeup`, source, and contexts — so a caller can correlate a queued item with its lifecycle. Injection never touches a FIFO and emits none of these. Every FIFO entry publishes an enqueue, including the loop-authored continuation-reason steer (`agent/turn-continuation` returning `{ action: 'continue', reason }`), 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.
**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) each type their `AgentMessage` payload with only the accepted message's returned `id`, content, and source, so a caller can correlate a queued item with its lifecycle without depending on driver routing state. Injection never touches a FIFO and emits none of these. Every FIFO entry publishes an enqueue, including the loop-authored continuation-reason steer (`agent/turn-continuation` returning `{ action: 'continue', reason }`), 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.
**cancel gains keepInbox.** `cancel(cause, { keepInbox? })`; callers choose the cause explicitly, and `keepInbox: true` aborts the active turn while preserving queued and steering items (no discard event, and un-started work is not dropped).

View File

@@ -22,7 +22,7 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
**`send` 返回一个 id。** `send`(以及其别名)为被接受的消息返回一个不透明的 branded `AgentMessageId``send` 此前的返回值是 `void`
**三个 inbox 事件取代 agent/queued。** `agent/inbox/enqueue`(一个队列项进入某个 FIFO`agent/inbox/dequeue`(驱动器认领了一个)和 `agent/inbox/discard``cancel()` 丢弃了待处理项)都携带一条 `AgentMessage`——即被接受消息,包含其返回的 `id``target`/`wakeup`、来源和上下文——因此调用方可以把一个排队项与其生命周期关联起来。注入从不触及 FIFO也不发出这些事件中的任何一个。每一次 FIFO 入队都会发布一个 enqueue 事件,包括由 loop 生成的携带继续原因的 steer`agent/turn-continuation` 返回 `{ action: 'continue', reason }`),因此账目会与其后的 dequeue 或 discard 保持平衡。`dsh-agent` 的不变量配套断言 FIFO 守恒:一个按 agent 计的未结算计数dequeue 和 discard 永远无法把它压到负数。
**三个 inbox 事件取代 agent/queued。** `agent/inbox/enqueue`(一个队列项进入某个 FIFO`agent/inbox/dequeue`(驱动器认领了一个)和 `agent/inbox/discard``cancel()` 丢弃了待处理项)都将各自的 `AgentMessage` 载荷类型限定为仅包含被接受消息返回的 `id`内容和来源;调用方因此可以把一个排队项与其生命周期关联起来,而无需依赖驱动器的路由状态。注入从不触及 FIFO也不发出这些事件中的任何一个。每一次 FIFO 入队都会发布一个 enqueue 事件,包括由 loop 生成的携带继续原因的 steer`agent/turn-continuation` 返回 `{ action: 'continue', reason }`),因此账目会与其后的 dequeue 或 discard 保持平衡。`dsh-agent` 的不变量配套断言 FIFO 守恒:一个按 agent 计的未结算计数dequeue 和 discard 永远无法把它压到负数。
**cancel 新增 keepInbox。** `cancel(cause, { keepInbox? })`;调用方显式选择 cause`keepInbox: true` 会中止活跃轮次,同时保留排队项和 steering 项(不发出 discard 事件,尚未启动的工作也不会被丢弃)。

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
2026-07-21-tui-steering-queue-badge.md: b29a4667e778e65b0678f946fcaa34b79c4d7da0
2026-07-21-tui-steering-queue-badge.zh.md: 4bfce461e11bce1773d6e0b15aabecf6a6a6144c
2026-07-21-tui-steering-queue-badge.md: b1d111187bb42dfaaa6f3618c1d31f25b7e4b455
2026-07-21-tui-steering-queue-badge.zh.md: 6ddb5351e9b8db5bd4e73f12768d37667483d874

View File

@@ -10,10 +10,10 @@ While a turn runs, an editor submission calls `agent.steer()` and joins the stee
## Decision
The agent's inbox is the authoritative steering queue but is not observable from the TUI, so the badge is a live count reconstructed from the public `agent/queued` and `steering/message` events rather than a projection of the queue itself.
The agent's inbox is the authoritative steering queue but is not observable from the TUI, so the badge is a live count reconstructed from successful TUI steering submissions and `steering/message` events rather than a projection of the queue itself.
- The running status line composes through `formatTurnStatus`, which inserts a `${queued} queued · ` badge before the `Enter sends steering, Esc cancels` hint when `queued > 0` and shows the plain hint at zero; the phase label and elapsed timing before it are the [verbose status line](2026-07-21-tui-verbose-status-line.md)'s.
- `createTuiChat` owns a `pendingSteering` counter: `+1` on each `agent/queued` for this agent whose `info.steering` is set, `-1` (floored at zero) on each `steering/message` session event as the loop drains one, and reset to zero whenever the agent leaves `running`.
- `createTuiChat` records each successful running-state `agent.steer()` submission, removes its matching source on each `steering/message` session event as the loop drains one, and resets the list whenever the agent leaves `running`.
- The count refreshes onto the live `Loader` through `setMessage`; the refresh is a no-op while idle because the loader exists only during a running turn.
- The reset lives in the `agent/status` transition, not in `setStatus`, because `setStatus` also runs on mid-turn palette changes and must not clear a live count.
@@ -23,7 +23,7 @@ The agent's inbox is the authoritative steering queue but is not observable from
**Reset inside `setStatus`.** Rejected: `setStatus` re-runs on `applyColorScheme` mid-turn, which would wrongly zero a live count; the status transition is the only place a turn actually ends.
**Drop the decrement clamp.** Rejected: loop-authored steering (e.g. continuation reasons) logs `steering/message` with no matching user-queued increment, which would drive the count negative; the zero floor keeps the badge a lower bound rather than a lie.
**Count every public inbox enqueue.** Rejected: `AgentMessage` intentionally omits driver routing state, so an observer cannot distinguish queued turns from steering. The TUI instead owns the submissions represented by its badge.
**Make the wording or a threshold configurable.** Rejected: the no-hardcoded-tunables rule targets deployment-varying behavior, not brand copy; the `welcome`/hint strings are already fixed presentation.
@@ -31,9 +31,9 @@ The agent's inbox is the authoritative steering queue but is not observable from
- The badge is best-effort live UI state, not a logged surface: it is rebuilt from events and reset each turn, never persisted, so a resumed running turn starts its badge from zero.
- A cancellation mid-queue clears the badge cleanly through the non-running reset, and a drain past zero is a no-op — neither can strand a stale count.
- A loop continuation that keeps the agent `running` while re-enqueuing undrained late steering can transiently over-count until the next idle reset; the badge is advisory, so the window is acceptable.
- Steering submitted outside this TUI is absent from the badge; the count describes feedback for this editor's submissions rather than the agent's complete inbox.
- `packages/ui/tui/src/index.ts` stays at 100 % per-file coverage.
## Testing
`packages/ui/tui/tests/tui.spec.ts` drives the running status frame through the real `createTuiChat`: the plain hint at zero, a foreign-agent queue ignored, the increment to `2 queued`, a non-steering queue left untouched, the decrement as each message drains, the clamp on a drain past zero, and the reset when the turn ends. Verified live in tmux — the badge showed `3 queued` after three `agent.steer()` calls, then `1 queued` as two drained.
`packages/ui/tui/tests/tui.spec.ts` drives the running status frame through the real `createTuiChat`: the plain hint at zero, the increment to `2 queued` after editor submissions, the decrement as each message drains, an unrelated drain ignored, and the reset when the turn ends.

View File

@@ -10,10 +10,10 @@ Status: implemented
## Decision
agent智能体的收件箱inbox才是权威的 steering 队列,但 TUI 无法观测它,因此徽标是从公开的 `agent/queued` `steering/message` 事件重建出的实时计数,而非对队列本身的投影。
agent智能体的收件箱inbox才是权威的 steering 队列,但 TUI 无法观测它,因此徽标是根据 TUI 成功提交的 steering 和 `steering/message` 事件重建出的实时计数,而非对队列本身的投影。
- 运行时的状态行经 `formatTurnStatus` 组装:`queued > 0` 时在 `Enter sends steering, Esc cancels` 提示前插入 `${queued} queued · ` 徽标,为零时是纯提示文本;其前的阶段标签与耗时归[详细状态行](2026-07-21-tui-verbose-status-line.md)所有。
- `createTuiChat` 持有一个 `pendingSteering` 计数器:每收到一个针对本 agent 且 `info.steering` 为真`agent/queued``+1`agent loop智能体循环每排空一条时随对应的 `steering/message` 会话事件 `-1`(下限为零),agent 一旦离开 `running` 状态即重置为零
- `createTuiChat` 记录每一次在 `running` 状态下成功`agent.steer()` 提交;agent loop智能体循环每排空一条并发出 `steering/message` 会话事件时,就按来源移除匹配项;agent 离开 `running` 时则重置整个列表
- 计数通过 `setMessage` 刷新到实时的 `Loader` 上;空闲时刷新是空操作,因为 loader 只在运行中的轮次期间存在。
- 重置放在 `agent/status` 状态切换里,而非 `setStatus` 中,因为 `setStatus` 在轮次中途的颜色方案变化时也会运行,绝不能清掉一个实时计数。
@@ -23,7 +23,7 @@ agent智能体的收件箱inbox才是权威的 steering 队列,但
**在 `setStatus` 内重置。** 否决:`setStatus` 会在轮次中途的 `applyColorScheme` 时重新运行,会错误地把实时计数清零;状态切换才是轮次真正结束的唯一位置。
**去掉递减的下限钳制** 否决:agent loop 自行产生的 steering如 continuation 续跑原因)会记录 `steering/message`,却没有对应的用户入队递增,这会把计数压到负数;零下限让徽标成为下界,而非谎报
**统计每一次公开的 inbox 入队** 否决:`AgentMessage` 刻意省略驱动器路由状态,因此观察方无法区分排队轮次与 steering。TUI 转而自行维护徽标所代表的那些提交
**把措辞或某个阈值做成配置。** 否决:「插件里不许硬编码可调参数」规则针对的是随部署变化的行为,不是品牌文案;`welcome`/提示字符串本就是固定的展示文案。
@@ -31,9 +31,9 @@ agent智能体的收件箱inbox才是权威的 steering 队列,但
- 徽标是尽力而为的实时 UI 状态不写入日志它由事件重建、每轮重置、从不持久化因此恢复resume出的运行中轮次徽标从零开始。
- 队列中途取消会经由「离开运行态即重置」干净地清掉徽标,排空到零以下则是空操作——两者都不会残留一个陈旧计数。
- 如果 agent loop 续跑时让 agent 保持 `running`、同时把未排空的迟到 steering 重新入队,则可能短暂多计,直到下一次空闲重置;徽标只作参考,因此这个窗口可以接受
- 通过此 TUI 以外的入口提交的 steering 不会出现在徽标中;该计数反馈的是此编辑器的提交,而不是 agent 的完整 inbox
- `packages/ui/tui/src/index.ts` 保持 100% 的单文件覆盖率。
## Testing
`packages/ui/tui/tests/tui.spec.ts` 通过真实的 `createTuiChat` 驱动运行时状态帧:为零时纯提示、忽略他方 agent 的入队、递增到 `2 queued`、非 steering 的入队保持不变、每条消息排空时递减、排空到零以下时的钳制、以及轮次结束时重置。已在 tmux 中实机验证——三次 `agent.steer()` 调用后徽标显示 `3 queued`,随后两条排空时显示 `1 queued`
`packages/ui/tui/tests/tui.spec.ts` 通过真实的 `createTuiChat` 驱动运行时状态帧:为零时显示纯提示,编辑器提交后递增到 `2 queued`每条消息排空时递减,忽略无关的排空,并在轮次结束时重置。