refactor(agent-loop): simplify message machine

This commit is contained in:
_Kerman
2026-07-30 13:49:57 +08:00
parent d554ae3019
commit f2e20c1ef0
212 changed files with 1326 additions and 2382 deletions

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-06-20-public-agent-stop-surface.md: e22c4389df18f3c9ca96763fc097eabefcc5b761
2026-06-20-public-agent-stop-surface.zh.md: e2647b498a8c906579b4fd2b50f94d1c326fe784
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md
2026-06-20-public-agent-stop-surface.md: 95c00f886360b94584f17c4221e0795be8ec1a61
2026-06-20-public-agent-stop-surface.zh.md: 983833b53c123d0e384ad1d356808c6ca2f37edc

View File

@@ -36,4 +36,4 @@ A future plugin cannot abort only the current model/tool step while preserving q
## Related
This Agent Note only removes the redundant stop verb. Mid-turn steering remains an intentional message path; quiescence observation remains via `whenIdle()`. The resulting public surface is `send()`, `steer()`, `inject()`, `cancel()`, `whenIdle()`, status, options, session, and identity.
This Agent Note only removes the redundant stop verb. Mid-turn steering remains an intentional message path; quiescence observation remains via `whenIdle()`. The resulting delivery surface is `followup()`, `steer()`, and `inject()`; stopping and observation remain with `cancel()` and `whenIdle()`.

View File

@@ -36,4 +36,4 @@ Status: implemented
## 相关
本 Agent Note 只移除冗余的停止动词。轮次中途 steering 仍是一条有意保留的消息路径;完全停稳观察仍通过 `whenIdle()` 完成。最终公共表面包括 `send()``steer()``inject()``cancel()``whenIdle()`、status、options、会话和 identity
本 Agent Note 只移除冗余的停止动词。轮次中途 steering 仍是一条有意保留的消息路径;完全停稳观察仍通过 `whenIdle()` 完成。最终交付接口包括 `followup()``steer()``inject()`;停止与观察仍通过 `cancel()``whenIdle()` 完成

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/simplification/2026-07-17-one-send-one-turn.md
2026-07-17-one-send-one-turn.md: dcc6c0aa483a0e53205dbaeef4e2b903f5f6a215
2026-07-17-one-send-one-turn.zh.md: 8c12481defe6608c13ee81132b432b4d8b17b681
2026-07-17-one-send-one-turn.md: 4787a25042f3b002d6c247202d5614a0f7bfa673
2026-07-17-one-send-one-turn.zh.md: c44c99771b22f7ecb1b802f8758a103d2774a140

View File

@@ -6,7 +6,7 @@ English | [中文](2026-07-17-one-send-one-turn.zh.md)
## Problem
Suppose a caller submits message A and then message B with two `Agent.send()` calls. Implicit batching can put A and B in one turn simply because both are waiting when the driver reads its queue. The caller made two calls, but the loop silently turns them into one unit of work.
Suppose a caller submits message A and then message B with two `Agent.followup()` calls. Implicit batching can put A and B in one turn simply because both are waiting when the driver reads its queue. The caller made two calls, but the loop silently turns them into one unit of work.
That grouping depends on timing rather than caller intent. Calls from one synchronous stack, neighboring microtasks, event listeners, and model callbacks could be grouped differently even though every caller used the same API.
@@ -14,15 +14,15 @@ This grouping changes behavior, not just the number of model calls. One ordinary
## Decision
The rule is simple: each successful `send()` creates one independent FIFO queue item. If that item runs, it is the only ordinary message in its turn. An item can be dropped before it starts, so the precise guarantee is at most one turn rather than exactly one; two sends are never silently combined.
The rule is simple: each successful `followup()` creates one independent FIFO queue item. If that item runs, it is the only ordinary message in its turn. An item can be dropped before it starts, so the precise guarantee is at most one turn rather than exactly one; two follow-ups are never silently combined.
Before enqueueing an item, `send()` checks the agent state and makes a detached, deeply frozen snapshot of the content and resolved source. After enqueueing it, `send()` publishes `agent/queued`.
Before enqueueing an item, `followup()` checks the agent state and makes a detached, deeply frozen snapshot of the content and resolved source. After enqueueing it, the agent publishes `agent/queued`.
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.
Prompt admission decides one message at a time before a turn opens. An allowed prompt becomes that turn's `user/message`; a blocked prompt is discarded without opening a turn or writing session history. Mixed-batch and all-blocked-batch branches do not exist.
The no-batching rule applies only to ordinary `send()`. Running `steer()` puts input in the outbox. While a turn remains open, the loop records that input at the next step boundary and steering makes another step the default. A failure before that boundary leaves the steering staged without waking the agent; a request-error retry action or a later prompt takes it, while cancellation or disposal can discard it. When the agent is idle, `steer()` delegates to `send()`, so it creates an independent ordinary queue item.
The no-batching rule applies only to ordinary `followup()`. Running `steer()` puts input in the outbox. While a turn remains open, the loop records that input at the next step boundary and steering makes another step the default. A failure before that boundary leaves the steering staged without waking the agent; a request-error retry action or a later prompt takes it, while cancellation or disposal can discard it. When the agent is idle, `steer()` creates an independent ordinary queue item.
`inject()` continues to add model-facing context without submitting an ordinary message. During a turn it waits in the outbox for a safe step boundary; while idle it appends a `user/message` directly, without opening a turn or running the model. Persistence owns the resulting eager drain. `cancel()` remains a whole-agent operation that can clear all unstarted ordinary and steering input and abort the current step. `status` and `whenIdle()` also describe the whole agent, not one message. Several one-message turns can share one `running` interval, so `running` does not prove that a turn is open.

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
假设调用方连续两次调用 `Agent.send()`,先提交消息 A再提交消息 B。隐式批处理可能只因为驱动器读取队列时两条消息都在等待就把 A、B 放进同一个轮次。调用方明明调用了两次agent loop智能体循环却悄悄把它们变成一个工作单元。
假设调用方连续两次调用 `Agent.followup()`,先提交消息 A再提交消息 B。隐式批处理可能只因为驱动器读取队列时两条消息都在等待就把 A、B 放进同一个轮次。调用方明明调用了两次agent loop智能体循环却悄悄把它们变成一个工作单元。
这种分组取决于运行时机,而不是调用方的意图。因此,即使所有调用方使用相同 API来自同一个同步调用栈、相邻微任务、事件监听器和模型回调的调用也可能产生不同分组。
@@ -14,15 +14,15 @@ Status: implemented
## 决策
规则很简单:一次成功的 `send()` 创建一个独立的 FIFO 队列项。该队列项如果运行,就是所在轮次中唯一的普通消息。队列项可能在启动前被丢弃,因此精确保证是最多一个轮次,而不是必定一个轮次;两次 send 绝不会被悄悄合并。
规则很简单:一次成功的 `followup()` 创建一个独立的 FIFO 队列项。该队列项如果运行,就是所在轮次中唯一的普通消息。队列项可能在启动前被丢弃,因此精确保证是最多一个轮次,而不是必定一个轮次;两次 follow-up 绝不会被悄悄合并。
队列项入队之前,`send()` 会检查 agent 状态,并为内容和解析后的来源创建一份脱离调用方对象、经过深度冻结的快照。队列项入队之后,`send()` 发布 `agent/queued`
队列项入队之前,`followup()` 会检查 agent 状态,并为内容和解析后的来源创建一份脱离调用方对象、经过深度冻结的快照。队列项入队之后,agent 发布 `agent/queued`
如果消息 A、B 都进入处理B 的轮次只能在 A 记录 `turn/end` 且 A 的持久性检查点处理结束后开始。因此B 的请求能看到 A 在同一会话日志中留下的已关闭结果。检查点错误会照常报告,但处理结束只表示解除这道顺序屏障,不表示失败的写入已经持久化。广义 `cancel()`、dispose资源释放`turn/start` 之前的失败也可能丢弃尚未启动的队列项,而不打开一个空轮次。
提示词准入会在轮次打开前,每次只决定一条消息。获准提示词成为该轮次的 `user/message`;被阻止的提示词会被丢弃,不打开轮次,也不写入会话历史。实现中不存在混合批次或全阻止批次分支。
上述不合批规则只适用于普通 `send()`。agent 运行时,`steer()` 会把输入放入 outbox。只要当前轮次仍然打开agent loop 就会在下一个步骤边界记录该输入,而 steering中途引导会默认让循环再执行一个步骤。在到达该边界前发生失败会让 steering 保持暂存且不唤醒 agent请求错误的重试动作或后续提示词会取走它而取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 委托给 `send()`,因此会创建一个独立的普通队列项。
上述不合批规则只适用于普通 `followup()`。agent 运行时,`steer()` 会把输入放入 outbox。只要当前轮次仍然打开agent loop 就会在下一个步骤边界记录该输入,而 steering中途引导会默认让循环再执行一个步骤。在到达该边界前发生失败会让 steering 保持暂存且不唤醒 agent请求错误的重试动作或后续提示词会取走它而取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 会创建一个独立的普通队列项。
`inject()` 继续添加面向模型的上下文,而不提交普通消息。轮次打开时,该上下文会留在 outbox 中等待安全的步骤边界agent 空闲时,系统会直接追加一条 `user/message`,既不打开轮次,也不运行模型。持久化层独立负责由此产生的即时排空。`cancel()` 仍是面向整个 agent 的操作,可以清空所有尚未启动的普通输入和 steering并中止当前步骤。`status``whenIdle()` 描述的也是整个 agent而不是某一条消息。多个单消息轮次可以共用一个 `running` 区间,因此 `running` 不表示轮次一定处于打开状态。

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/simplification/2026-07-24-agent-loop-observable-state-machine.md
2026-07-24-agent-loop-observable-state-machine.md: a25657c6a41e2c0989db620046f44ea3254be151
2026-07-24-agent-loop-observable-state-machine.zh.md: 058d89d3cb9e3d30963f95fda1510ef3c5bf281e
2026-07-24-agent-loop-observable-state-machine.md: 2024662495d8396e946e7c43f010164cd9414b83
2026-07-24-agent-loop-observable-state-machine.zh.md: dc83a310ff2d7945e6b7e79625224102f0f4871c

View File

@@ -51,7 +51,7 @@ The inbox lifecycle complements, rather than replaces, the durable session log.
## Related
- [Unify agent delivery on send(target × wakeup) and coalesce injected context into user/message](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)
- [Unify agent delivery routing and coalesce injected context into user/message](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)
- [Remove implicit batching from ordinary sends](2026-07-17-one-send-one-turn.md)
- [Microkernel event taxonomy](../architecture/2026-06-11-microkernel-event-taxonomy.md)
- [Bounded LLM request recovery](../architecture/2026-06-21-bounded-llm-request-recovery.md)

View File

@@ -51,7 +51,7 @@ agent 生命周期、agent 整体活动状态、收件箱条目的进度以及
## 相关内容
- [统一通过 send(target × wakeup) 交付 agent 消息,并将注入上下文合并到 user/message](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)
- [统一 agent 交付路由,并将注入上下文合并到 user/message](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)
- [移除普通发送中的隐式批处理](2026-07-17-one-send-one-turn.md)
- [微内核事件分类体系](../architecture/2026-06-11-microkernel-event-taxonomy.md)
- [有界 LLM 请求恢复](../architecture/2026-06-21-bounded-llm-request-recovery.md)

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/simplification/2026-07-27-request-error-retry-action.md
2026-07-27-request-error-retry-action.md: 3057b9fa28cf203c9374930fe97421918b4c1a6f
2026-07-27-request-error-retry-action.zh.md: bcb4e592f0c3d86f896e279cf0e3ea400741a1bb
2026-07-27-request-error-retry-action.md: 18ae9bc4ba26d1ad3cb7d1328d9e9d3e8de8377c
2026-07-27-request-error-retry-action.zh.md: a4092033eba236c95dbd237fbd93bf750f1055ab

View File

@@ -14,7 +14,7 @@ Model-request recovery was decided inside `agent/request-error` but communicated
The loop reads the action after the waterfall settles, closes the failed turn, and opens one retry turn from durable history. It rechecks the turn signal when consuming the action, so cancellation or disposal during recovery prevents the retry even if a listener returns it afterward. A thrown recovery never produces an action.
`Agent` and `ReactLoopAgent` expose no `retry()` method. Ordinary new work enters through `send()` and its `followup()`, `steer()`, and `inject()` presets; only a handled model-request failure can open a promptless retry turn.
`Agent` and `ReactLoopAgent` expose no `retry()` method. Ordinary new work enters through `followup()`, `steer()`, and `inject()`; only a handled model-request failure can open a promptless retry turn.
## Alternatives considered

View File

@@ -14,7 +14,7 @@ Status: implemented
waterfall 结算后,循环读取该动作,关闭失败轮次,并从持久历史开启一个重试轮次。循环在使用该动作时会再次检查轮次信号,因此即使监听器随后返回重试动作,恢复期间发生的取消或资源释放仍会阻止重试。抛出异常的恢复不会产生动作。
`Agent``ReactLoopAgent` 均不暴露 `retry()` 方法。普通新工作通过 `send()` 及其 `followup()``steer()``inject()` 预设进入;只有已处理的模型请求失败才能开启没有提示词的重试轮次。
`Agent``ReactLoopAgent` 均不暴露 `retry()` 方法。普通新工作通过 `followup()``steer()``inject()` 进入;只有已处理的模型请求失败才能开启没有提示词的重试轮次。
## 曾考虑的替代方案

View File

@@ -0,0 +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 .agents/notes/implemented/simplification/2026-07-30-private-agent-send.md
2026-07-30-private-agent-send.md: 43353c309f98eab1bff91b8fbe0c9cfaa2bbc69b
2026-07-30-private-agent-send.zh.md: 49f7c999ec60c7305bdca570a4d59039c7c1923b

View File

@@ -0,0 +1,27 @@
# Agent Note: Keep agent routing private
Status: implemented
English | [中文](2026-07-30-private-agent-send.zh.md)
## Problem
The public `Agent.send()` method exposed the concrete loop's routing matrix even though production callers use only the semantic `followup()`, `steer()`, and `inject()` operations. Its fourth combination, `next-turn` with `wakeup: false`, had no consumer beyond tests. Keeping that latent capability public also required alternate `Agent` implementations and test fakes to accept implementation-level routing policy.
## Decision
`Agent` exposes `followup()`, `steer()`, and `inject()` as its complete delivery contract. `ReactLoopAgent` keeps a private `send()` helper that shares routing mechanics among those methods, while `SendTarget` and `SendOptions` are no longer exported from `dsh-agent`.
The public interface cannot queue a turn without waking the driver. A follow-up always requests execution, steering requests the nearest step, and injection supplies model-facing context without requesting execution. This partially supersedes the public-surface portion of the [unified delivery decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md) while retaining its internal routing and unified `user/message` representation.
## Alternatives considered
**Keep the routing matrix public.** This preserves the unused quiet-queue combination, but exposes mechanism instead of caller intent and imposes it on every alternate driver.
**Add a public quiet-queue method.** A named method would be clearer than raw routing flags, but no production workflow currently needs work that remains parked until an unrelated delivery wakes it.
## Consequences
Plugins choose among three semantic operations instead of constructing routing options. Alternate drivers and structural test fakes implement a smaller contract, and the Cordis API catalog no longer advertises `send`, `SendTarget`, or `SendOptions`.
The removed quiet-queue capability can return only with a named consumer and explicit lifecycle semantics. `cancel({ keepInbox: true })` still preserves work already pending through the supported delivery paths.

View File

@@ -0,0 +1,27 @@
# Agent Note: 将 agent 路由保留为私有实现
Status: implemented
[English](2026-07-30-private-agent-send.md) | 中文
## 问题
公开的 `Agent.send()` 方法暴露了实体循环的路由矩阵,但生产调用方只使用语义明确的 `followup()``steer()``inject()` 操作。第四种组合,即 `next-turn` 配合 `wakeup: false`,除测试外没有消费方。将这项潜在能力保留为公开接口,还会迫使其他 `Agent` 实现和测试替身接受实现层的路由策略。
## 决策
`Agent``followup()``steer()``inject()` 作为完整的交付契约公开。`ReactLoopAgent` 保留私有的 `send()` 辅助方法,供这三个方法共用路由机制;`dsh-agent` 不再导出 `SendTarget``SendOptions`
公开接口无法在不唤醒驱动器的情况下让一个轮次入队。`followup()` 始终请求执行,`steer()` 请求最近的步骤,`inject()` 则提供面向模型的上下文而不请求执行。本决策部分取代[统一交付决策](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)中关于公开接口的内容,同时保留其内部路由与统一的 `user/message` 表示。
## 曾考虑的替代方案
**让路由矩阵保持公开。** 这会保留未使用的无唤醒排队组合,但也会暴露机制而非调用方意图,并要求每个替代驱动器都支持该机制。
**添加公开的无唤醒排队方法。** 使用具名方法会比原始路由标志更清晰,但目前没有生产工作流需要让工作持续处于等待状态,直到无关的交付将其唤醒。
## 后果
插件从三种语义操作中选择不再自行构造路由选项。其他驱动器和结构型测试替身只需实现更小的契约Cordis API 目录也不再列出 `send``SendTarget``SendOptions`
只有出现明确的消费方并定义显式的生命周期语义后,才能恢复已移除的无唤醒排队能力。`cancel({ keepInbox: true })` 仍会保留已通过受支持交付路径进入待处理状态的工作。