Merge remote-tracking branch 'origin/master' into worktree/context-source-cards
# Conflicts: # .agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.i18n.yaml # .agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md # .agents/notes/implemented/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md # apps/web/tests/seeded-history.e2e.ts # apps/web/tests/snapshots/queue-actions/layout.expected.md # docs/core-data-structures/core.i18n.yaml # examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/workspace-context/session.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/headless-agent/tests/snapshots/pty-tools/session.jsonl # packages/client/runtime/README.i18n.yaml # packages/client/runtime/tests/history-fold.spec.ts # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx # packages/context/workspace-context/src/index.ts # packages/context/workspace-context/tests/workspace-context.spec.ts # packages/skill/tool-skill/README.i18n.yaml # packages/skill/tool-skill/README.md # packages/skill/tool-skill/README.zh.md # packages/skill/tool-skill/src/index.ts # packages/skill/tool-skill/tests/tool-skill.spec.ts
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/simplification/2026-06-20-public-agent-stop-surface.md
|
||||
2026-06-20-public-agent-stop-surface.md: 7e8f6f691c999fd78c9b4133eaeac40f2d1c1ba9
|
||||
2026-06-20-public-agent-stop-surface.zh.md: 18111d95975fa9b0e7600a72e53b1df1ecd5d151
|
||||
2026-06-20-public-agent-stop-surface.md: e9c713c7ba8c784d3d66fe8f391e1b197f7394d2
|
||||
2026-06-20-public-agent-stop-surface.zh.md: 5c1eb0ec8245c60613d6d4d7c1d105b5243bc938
|
||||
|
||||
@@ -36,4 +36,4 @@ A plugin can abort the active turn while preserving queued prompts through `keep
|
||||
|
||||
## 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()`.
|
||||
|
||||
@@ -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()` 完成。
|
||||
|
||||
@@ -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: 3ae43f137206f25bdbc563875c17e24211f17d6b
|
||||
2026-07-17-one-send-one-turn.zh.md: fddda8cd2c168ee48d13b0d36d09e9ddb3c0a8fc
|
||||
2026-07-17-one-send-one-turn.md: 834eb2b3aace0a0aa5f86f483e7b7d7aabbe467e
|
||||
2026-07-17-one-send-one-turn.zh.md: d22893b912462cffffb0d6ecd81811e9cb5c523d
|
||||
|
||||
@@ -10,21 +10,21 @@ Suppose a caller submits message A and then message B with two `Agent.send()` ca
|
||||
|
||||
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.
|
||||
|
||||
This grouping changes behavior, not just the number of model calls. One ordinary turn owns prompt admission, `turn/start`, `turn/end`, and a durability checkpoint. If message B shares message A's turn, B can enter A's model request instead of first seeing A's closed result in the session log. Allowing one message while blocking another also requires a mixed state that no caller requested.
|
||||
This grouping changes behavior, not just the number of model calls. One ordinary turn owns one claimed follow-up, `turn/start`, `turn/end`, and a durability checkpoint. If message B shares message A's turn, B can enter A's model request instead of first seeing A's closed result in the session log. Entering one follow-up while rejecting another also requires a mixed state that no caller requested.
|
||||
|
||||
## 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.
|
||||
|
||||
Before enqueueing an item, `send()` checks the agent state and accepts an already identified, deeply frozen message. It mints an occurrence-local `InboxItemId` and publishes `agent/inbox/enqueue`; the pending occurrence remains addressable under the [addressable queue operations](../feature/2026-07-29-addressable-queue-operations.md) decision until the driver claims or discards it.
|
||||
Before inserting a message, `send()` checks the agent state and accepts an already identified, deeply frozen value. The durable splice and `agent/inbox/inserted { message }` retain its `MessageId`; the pending message remains addressable through `Inbox.replace()` and `Inbox.remove()` until the driver claims or discards it. The [claimed pre-step inbox decision](../architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md) owns the current lifecycle.
|
||||
|
||||
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.
|
||||
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 `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 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.
|
||||
|
||||
`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.
|
||||
`inject()` continues to add model-facing context without submitting ordinary input or waking the driver. It always waits in the next-step inbox for a later pre-step, including while idle; AgentLoop records it as `user/message` only when an enter decision returns it inside a turn. `cancel()` remains a whole-agent operation that can clear all unstarted ordinary input, steering, and injection and abort the current step. `status` and `whenIdle()` also describe the whole agent, not one message.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -35,11 +35,11 @@ The no-batching rule applies only to ordinary `send()`. Running `steer()` puts i
|
||||
- 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 prompt veto, listener failure, broad cancellation, disposal, and failure before `turn/start`; rejected admission creates no turn, recorded turns stay balanced, messages do not merge, and surviving queued work still drains.
|
||||
- Separate tests cover open-turn, failed-turn, and idle `steer()`, plus `inject()`, whole-agent status, and `whenIdle()`.
|
||||
- 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
|
||||
|
||||
Ordinary turn boundaries are predictable: messages A and B stay separate, and B runs only after A has closed and reached its checkpoint. Callers still do not receive a per-send completion handle; a pending occurrence can be removed through its live `InboxItemId`, broad cancellation can discard the entire unstarted tail, and status and quiescence remain agent-wide observations.
|
||||
Ordinary turn boundaries are predictable: messages A and B stay separate, and B runs only after A has closed and reached its checkpoint. Callers still do not receive a per-send completion handle; a pending message can be removed through its `MessageId`, broad cancellation can discard the entire unstarted tail, and status and quiescence remain agent-wide observations.
|
||||
|
||||
The trade-off is more model requests and more checkpoints. A busy queue can take longer to drain and can grow under sustained producers. Ordinary-send batching returns only through an explicit, measured contract.
|
||||
|
||||
@@ -10,21 +10,21 @@ Status: implemented
|
||||
|
||||
这种分组取决于运行时机,而不是调用方的意图。因此,即使所有调用方使用相同 API,来自同一个同步调用栈、相邻微任务、事件监听器和模型回调的调用也可能产生不同分组。
|
||||
|
||||
这种分组改变的不只是模型调用次数。一个普通轮次包含提示词准入、`turn/start`、`turn/end` 和持久性检查点。如果消息 B 与消息 A 共用轮次,B 可能直接进入 A 的模型请求,而不是先看到 A 在会话日志中已经关闭的结果。若系统允许一条消息、阻止另一条消息,还需要引入调用方没有请求的混合状态。
|
||||
这种分组改变的不只是模型调用次数。一个普通轮次包含一条已领取 follow-up、`turn/start`、`turn/end` 和持久性检查点。如果消息 B 与消息 A 共用轮次,B 可能直接进入 A 的模型请求,而不是先看到 A 在会话日志中已经关闭的结果。若系统让一条 follow-up 进入、却拒绝另一条,还需要引入调用方没有请求的混合状态。
|
||||
|
||||
## 决策
|
||||
|
||||
规则很简单:一次成功的 `send()` 创建一个独立的 FIFO 队列项。该队列项如果运行,就是所在轮次中唯一的普通消息。队列项可能在启动前被丢弃,因此精确保证是最多一个轮次,而不是必定一个轮次;两次 send 绝不会被悄悄合并。
|
||||
|
||||
队列项入队之前,`send()` 会检查 agent 状态,并接受已有标识且经过深度冻结的消息。它会铸造一个仅属于本次入队的 `InboxItemId`,并发布 `agent/inbox/enqueue`;根据[可寻址队列操作](../feature/2026-07-29-addressable-queue-operations.md)决策,在驱动器认领或丢弃该项之前,这次待处理入队始终可以被寻址。
|
||||
消息插入之前,`send()` 会检查 agent 状态,并接受已有标识且经过深度冻结的值。持久 splice 与 `agent/inbox/inserted { message }` 会保留其 `MessageId`;在驱动器领取或丢弃该消息之前,可以通过 `Inbox.replace()` 与 `Inbox.remove()` 寻址。当前生命周期由[已领取 pre-step inbox 决策](../architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md)规定。
|
||||
|
||||
如果消息 A、B 都进入处理,B 的轮次只能在 A 记录 `turn/end` 且 A 的持久性检查点处理结束后开始。因此,B 的请求能看到 A 在同一会话日志中留下的已关闭结果。检查点错误会照常报告,但处理结束只表示解除这道顺序屏障,不表示失败的写入已经持久化。面向整个 agent 的 `cancel()`、dispose(资源释放)或 `turn/start` 之前的失败也可能丢弃尚未启动的队列项,而不打开一个空轮次。
|
||||
|
||||
提示词准入会在轮次打开前,每次只决定一条消息。获准提示词成为该轮次的 `user/message`;被阻止的提示词会被丢弃,不打开轮次,也不写入会话历史。实现中不存在混合批次或全阻止批次分支。
|
||||
轮次边界上,循环会先打开轮次,再在待处理 next-step 输入之后领取一条 follow-up。`agent/pre-step` 要么拒绝提案,要么返回进入步骤的完整批次。被拒绝的 follow-up 保持已删除,并关闭一个 blocked 的无步骤轮次,不写入模型可见历史。实现中不存在混合普通 follow-up 分支。
|
||||
|
||||
上述不合批规则只适用于普通 `send()`。agent 运行时,`steer()` 会把输入放入 outbox。只要当前轮次仍然打开,agent loop 就会在下一个步骤边界记录该输入,而 steering(中途引导)会默认让循环再执行一个步骤。在到达该边界前发生失败,会让 steering 保持暂存且不唤醒 agent;请求错误的重试动作或后续提示词会取走它,而取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 委托给 `send()`,因此会创建一个独立的普通队列项。
|
||||
上述不合批规则只适用于普通 follow-up 输入。`steer()` 会把输入放入 next-step inbox 并唤醒驱动器。在轮次期间,循环可以在后续步骤边界领取它;agent 空闲时,这个会唤醒的 next-step 批次会启动一个新轮次。批次被领取后才到达的输入会等待后续边界,而取消或 dispose 可以将其丢弃。
|
||||
|
||||
`inject()` 继续添加面向模型的上下文,而不提交普通消息。轮次打开时,该上下文会留在 outbox 中,等待安全的步骤边界;agent 空闲时,系统会直接追加一条 `user/message`,既不打开轮次,也不运行模型。持久化层独立负责由此产生的即时排空。`cancel()` 仍是面向整个 agent 的操作,可以清空所有尚未启动的普通输入和 steering,并中止当前步骤。`status` 和 `whenIdle()` 描述的也是整个 agent,而不是某一条消息。多个单消息轮次可以共用一个 `running` 区间,因此 `running` 不表示轮次一定处于打开状态。
|
||||
`inject()` 继续添加面向模型的上下文,但不提交普通输入,也不唤醒驱动器。即使 agent 空闲,它也始终在 next-step inbox 中等待后续 pre-step;AgentLoop 只会在 enter 决策于轮次内返回它时,将其记录为 `user/message`。`cancel()` 仍是面向整个 agent 的操作,可以清空所有尚未启动的普通输入、steering 和注入,并中止当前步骤。`status` 和 `whenIdle()` 描述的也是整个 agent,而不是某一条消息。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -35,11 +35,11 @@ Status: implemented
|
||||
- 单元测试和基于属性的测试从同一调用栈、相邻微任务、不同生产方和重入回调提交 send;每条消息都会得到一个按 FIFO 排序的独立轮次。
|
||||
- stdio 构建产物测试提交两行输入,并观察到两个模型请求和两个轮次边界。
|
||||
- 延迟和拒绝第一个轮次的检查点,都能让下一个轮次保持等待,并证明其请求可以看到前一条助手结果。
|
||||
- 失败路径测试覆盖提示词否决、监听器失败、面向整个 agent 的取消、dispose 和 `turn/start` 之前的失败;准入拒绝不会创建轮次,已记录的轮次保持边界平衡,消息不会合并,仍需处理的排队工作也能继续清空。
|
||||
- 其他测试分别覆盖轮次打开时、轮次失败后和空闲时的 `steer()`,以及 `inject()`、面向整个 agent 的状态和 `whenIdle()`。
|
||||
- 失败路径测试覆盖 pre-step reject、监听器失败、广义取消、dispose 和 `turn/start` 之前的失败;首次 pre-step 的各种退出都会关闭边界平衡的无步骤轮次,消息不会合并,之后仍需处理的工作也能继续清空。
|
||||
- 其他测试分别覆盖轮次打开时、轮次失败后和空闲时的 `steer()`,以及待处理的 `inject()`、面向整个 agent 的状态和 `whenIdle()`。
|
||||
|
||||
## 后果
|
||||
|
||||
普通轮次的边界可预测:消息 A、B 始终分开,B 只能在 A 关闭并到达检查点后运行。调用方仍然拿不到逐次 send 的完成句柄;待处理项可通过其仍有效的 `InboxItemId` 移除,面向整个 agent 的取消可以丢弃整个尚未启动的队尾,而状态与完全停稳仍是面向整个 agent 的观察结果。
|
||||
普通轮次的边界可预测:消息 A、B 始终分开,B 只能在 A 关闭并到达检查点后运行。调用方仍然拿不到逐次 send 的完成句柄;待处理消息可通过其 `MessageId` 移除,广义取消可以丢弃整个尚未启动的队尾,而状态与完全停稳仍是面向整个 agent 的观察。
|
||||
|
||||
代价是模型请求和检查点都会增加。繁忙队列可能需要更长时间才能清空;如果生产方持续提交消息,队列也可能增长。只有建立显式且经过测量的契约后,才能重新引入普通 send 批处理。
|
||||
|
||||
@@ -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-20-unwrap-injected-content-envelopes.md
|
||||
2026-07-20-unwrap-injected-content-envelopes.md: 32642660f7bcea748c349933b99552b1974922c5
|
||||
2026-07-20-unwrap-injected-content-envelopes.zh.md: 8af8fc4ab3e63de43fc1ed01dc79022aec827ac5
|
||||
2026-07-20-unwrap-injected-content-envelopes.md: aff458d76027bf5518497328ee1bd852869793b9
|
||||
2026-07-20-unwrap-injected-content-envelopes.zh.md: 59cbd823da3305185a05c79207fbd7b702a11fba
|
||||
|
||||
@@ -15,7 +15,7 @@ Two problems:
|
||||
|
||||
## Decision
|
||||
|
||||
Injected session content projects verbatim; the caller owns any framing. `deriveEventMessage` renders `user/message`, `context/message`, and `steering/message` through one shared case returning `{ role: 'user', content: event.data.content }`; their content blocks reach the model unchanged. `context/message`'s `source`/`meta` and `steering/message`'s `turn` stay in the durable event log but do not render.
|
||||
Injected session content projects verbatim; the caller owns any framing. `deriveEventMessage` renders `user/message` content blocks to the model unchanged; `source` stays in the durable event log but does not render.
|
||||
|
||||
The `ContextEnvelope` type and every `envelope` field are removed — `context/message` in `SessionEventMap`, `InjectOptions`, `HookContext`, and the `inject()`/`additionalContexts` plumbing in `dsh-agent-loop`. `workspace-context` no longer requests `'raw'`; its self-framed content renders as before. The `renderTagged`/`renderContextEnvelope` helpers are deleted. `context/message.meta` still carries durable, model-hidden JSON state.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
注入的会话内容逐字投影,框架由调用方自行负责。`deriveEventMessage` 通过一个共享分支渲染 `user/message`、`context/message` 和 `steering/message`,都返回 `{ role: 'user', content: event.data.content }`;它们的内容块原样到达模型。`context/message` 的 `source`/`meta` 和 `steering/message` 的 `turn` 保留在持久事件日志中,但不渲染。
|
||||
注入的会话内容逐字投影,框架由调用方自行负责。`deriveEventMessage` 把 `user/message` 的内容块原样送达模型;`source` 保留在持久事件日志中,但不渲染。
|
||||
|
||||
`ContextEnvelope` 类型和所有 `envelope` 字段都被移除——包括 `SessionEventMap` 中的 `context/message`、`InjectOptions`、`HookContext`,以及 `dsh-agent-loop` 中 `inject()`/`additionalContexts` 的相关管线。`workspace-context` 不再请求 `'raw'`;它自带框架的内容渲染方式不变。`renderTagged`/`renderContextEnvelope` 辅助函数被删除。`context/message.meta` 仍携带持久的、对模型隐藏的 JSON 状态。
|
||||
|
||||
|
||||
@@ -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-22-plan-specific-collaboration-state.md
|
||||
2026-07-22-plan-specific-collaboration-state.md: fb26d15238f0eb1b63fdccc7e48a6c49a44236cf
|
||||
2026-07-22-plan-specific-collaboration-state.zh.md: c3cbf3d6f1892f6cc00684e56c13b5d433d1b7ba
|
||||
2026-07-22-plan-specific-collaboration-state.md: d139f3d861244c80acfb604d17172461bbf0cd57
|
||||
2026-07-22-plan-specific-collaboration-state.zh.md: a11ed1f270f58fc49a41be0e475889f84fa7cd40
|
||||
|
||||
@@ -14,7 +14,7 @@ Plan mode also needs a durable stance, a reviewable plan artifact, an explicit h
|
||||
|
||||
## Decision
|
||||
|
||||
Plan mode owns a plan-specific product package: `@deepseek-ai/dsh-plan-mode` at `packages/plan/plan-mode/`. The durable fact is `plan/mode: { active: boolean }`, folded by `foldPlanMode(events)` with `false` as the empty-log value. `ctx.planMode.get(agent)` returns `{ active, pending? }`, and `set(agent, active)` records the boundary-applied selection. The existing prompt-submit, continuation, retry, append-failure, and disposal fences remain unchanged in meaning.
|
||||
Plan mode owns a plan-specific product package: `@deepseek-ai/dsh-plan-mode` at `packages/plan/plan-mode/`. The durable fact is `plan/mode: { active: boolean }`, folded by `foldPlanMode(events)` with `false` as the empty-log value. `ctx.planMode.get(agent)` returns `{ active, pending? }`, and `set(agent, active)` records the boundary-applied selection. The pre-step, retry, append-failure, and disposal fences preserve the same state-transition ownership.
|
||||
|
||||
Configuration is exactly `{ section: string }`. The package registers the fixed `plan:policy` section, `/plan [message]`, the exact `/plan off` direct-exit form, and `exit_plan_mode` itself. Bare `/plan` selects active; another non-empty argument selects it first and then sends the trimmed text through `agent.steer()`, making the text an ordinary logged user message in the affected step. `/plan off` selects inactive without model input and can cancel an entry that is still pending at the boundary. The exit tool remains registered while plan mode is inactive so the request tool catalog stays stable.
|
||||
|
||||
@@ -24,7 +24,7 @@ Sandbox mode and approval policy remain separate enforcement axes. Plan mode nei
|
||||
|
||||
### Boundary and model contract
|
||||
|
||||
`plan/mode` is log-only and non-surface, so resume, fork, and compaction recover the state without a live mirror. A spawned agent begins inactive because there is no creation-time plan option. Pending user selections flush before the affected request assembly on prompt submission, ordinary continuation, or a request-recovery retry; a failed durable append leaves the intent pending for a later boundary.
|
||||
`plan/mode` is log-only and non-surface, so resume, fork, and compaction recover the state without a live mirror. A spawned agent begins inactive because there is no creation-time plan option. Pending user selections flush before the affected request assembly at initial or continuation pre-step, or on a request-recovery retry; a failed durable append leaves the intent pending for a later boundary.
|
||||
|
||||
The active state contributes the deployment's section at prompt order 50. Inactive state contributes no section, while `exit_plan_mode` remains registered in both states, so a transition changes the logged request header but not native tool schemas or the Code Mode SDK. A user-driven transition appends one plugin-sourced notice only when the last request header described the opposite state; a pre-first-request or net-zero selection adds none, and an approved tool exit relies on its tool result instead of a second notice.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Plan mode 还需要持久协作状态、可评审的计划产物、显式人工
|
||||
|
||||
## 决策
|
||||
|
||||
Plan mode 拥有一个 plan 专用产品包:位于 `packages/plan/plan-mode/` 的 `@deepseek-ai/dsh-plan-mode`。持久化事实为 `plan/mode: { active: boolean }`,由 `foldPlanMode(events)` 折叠,空日志值为 `false`。`ctx.planMode.get(agent)` 返回 `{ active, pending? }`,`set(agent, active)` 则记录在边界生效的选择。现有的提示词提交、continuation、重试、追加失败和 dispose(资源释放)栅栏在语义上保持不变。
|
||||
Plan mode 拥有一个 plan 专用产品包:位于 `packages/plan/plan-mode/` 的 `@deepseek-ai/dsh-plan-mode`。持久化事实为 `plan/mode: { active: boolean }`,由 `foldPlanMode(events)` 折叠,空日志值为 `false`。`ctx.planMode.get(agent)` 返回 `{ active, pending? }`,`set(agent, active)` 则记录在边界生效的选择。pre-step、重试、追加失败和 dispose(资源释放)栅栏保留相同的状态转换归属。
|
||||
|
||||
配置严格为 `{ section: string }`。该包自行注册固定的 `plan:policy` 段、`/plan [message]`、精确匹配的 `/plan off` 主动退出形式,以及 `exit_plan_mode`。不带参数的 `/plan` 选择激活;其他非空参数则先选择激活,再通过 `agent.steer()` 发送去除首尾空白后的文本,使该文本在受影响的步骤中成为一条记录到日志的普通用户消息。`/plan off` 选择未激活,不产生模型输入,并可取消仍待在边界生效的进入选择。即使 plan mode 未激活,退出工具仍保持注册,以确保请求工具目录稳定。
|
||||
|
||||
@@ -24,7 +24,7 @@ Plan mode 拥有一个 plan 专用产品包:位于 `packages/plan/plan-mode/`
|
||||
|
||||
### 边界与模型契约
|
||||
|
||||
`plan/mode` 仅记录到日志且不进入表层,因此恢复、fork 和压缩(compaction)都能恢复该状态,无需实时镜像。spawn 出的 agent(智能体)初始处于未激活状态,因为创建时没有 plan 选项。待生效的用户选择会在提示词提交、普通 continuation 或请求恢复重试时,于受影响的请求组装前写入日志;持久追加失败会让意图保持待定,留到后续边界处理。
|
||||
`plan/mode` 仅记录到日志且不进入表层,因此恢复、fork 和压缩都能恢复该状态,无需实时镜像。spawn 出的 agent 初始处于未激活状态,因为创建时没有 plan 选项。待生效的用户选择会在初始或续步 pre-step、或请求恢复重试时,于受影响的请求组装前写入日志;持久追加失败会让意图保持待定,留到后续边界处理。
|
||||
|
||||
激活状态在提示词顺序 50 处贡献部署提供的区段。未激活状态不贡献区段,但 `exit_plan_mode` 在两种状态下都保持注册,因此状态转换会改变已记录的请求头,却不改变原生工具 schema 或 Code Mode SDK。用户发起的转换只会在上一条请求头描述相反状态时追加一条来源为插件的通知;第一次请求前的选择或最终状态未变化的选择不会追加通知,经批准的工具退出则依赖其工具结果,不再追加第二条通知。
|
||||
|
||||
|
||||
@@ -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: 6c7f4312cb9b85bcb3d6b249ee93f2cc266fcb84
|
||||
2026-07-24-agent-loop-observable-state-machine.md: b1773fdf7fd3bcbea9c6b9a4a0ce37c715bce4ca
|
||||
2026-07-24-agent-loop-observable-state-machine.zh.md: a4ed98117b965b445ec0888c55a1b1b27887532a
|
||||
|
||||
@@ -18,16 +18,16 @@ The public contract exposes four orthogonal state dimensions:
|
||||
|
||||
- Registration lifetime is the `agent/created` to `agent/disposed` interval. Disposal is the terminal registry edge, not an `AgentStatus`.
|
||||
- Whole-agent activity is `AgentStatus = 'idle' | 'running'`. Consecutive turns may share one `running` interval.
|
||||
- A FIFO-backed message progresses from `agent/inbox/enqueue` to exactly one `agent/inbox/dequeue` or `agent/inbox/discard`. Enqueue and dequeue correlate an occurrence by `MessageId` plus its queued-or-steering placement; same-placement repeats retire in FIFO order. The inbox events describe acceptance, claim, and removal rather than turn completion.
|
||||
- A claimed turn passes through prompt admission and zero or more request steps. An automatic retry closes the failed turn and immediately opens another; `agent/settled` reports only the terminal turn in that chain and remains distinct from the whole-agent transition to `status === 'idle'`.
|
||||
- A pending message emits `agent/inbox/inserted` when inserted, then either `agent/inbox/claimed` after an atomic pure-deletion claim or `agent/inbox/discarded` after an ordinary removal. `MessageId` correlates the exact message; durable splice coordinates retain placement and cancellation. Inbox events describe insertion, claim, and discard rather than turn completion.
|
||||
- A claimed turn passes through pre-step entry and zero or more request steps. An automatic retry closes the failed turn and immediately opens another; `agent/settled` reports only the terminal turn in that chain and remains distinct from the whole-agent transition to `status === 'idle'`.
|
||||
|
||||
The loop keeps five machine extension events. `agent/prompt-submit` admits, rewrites, or blocks a claimed prompt. `agent/step` is the single awaited between-steps checkpoint and runs before every request is derived. `agent/request` is the waterfall for the frozen call configuration; the configuration comes only from `await next()`, not from a duplicate positional argument. `agent/request-error` serializes ownership of awaited model-request recovery. `agent/turn-stopping` runs when the turn otherwise has no work left; a listener that needs another step records real steering with `agent.steer()`, and the loop decides from that data after all listeners settle.
|
||||
The loop keeps four machine extension events. `agent/pre-step` decides reject or enter for one exclusive claimed batch and runs before every proposed step. `agent/request` is the waterfall for the frozen call configuration; the configuration comes only from `await next()`, not from a duplicate positional argument. `agent/request-error` serializes ownership of awaited model-request recovery. `agent/turn-stopping` runs when the turn otherwise has no work left; a listener that needs another step records real steering with `agent.steer()`, and the loop decides from that data after all listeners settle.
|
||||
|
||||
Continuation and termination are data rather than returned control enums. Tool calls and accepted steering require another step. A tool result carrying `concludesTurn` ends the tool loop at its step. The loop does not expose general `ContinuationDecision` or terminal-stop return channels.
|
||||
|
||||
A model-request failure closes its step, then enters `agent/request-error` with the exact error, normalized `LlmFailure`, and live turn signal. A listener that owns recovery repairs state, returns `{ kind: 'retry' }`, and stops delegating. The loop closes the failed turn and opens one retry turn over that state without an intervening idle notification; retry is not another step inside the failed turn. `agent/settled` reports the terminal outcome, and `agent/error` remains the live error notification for consumers that report failures independently of turn settlement. The [retry-action decision](2026-07-27-request-error-retry-action.md) supersedes the command-shaped part of this design.
|
||||
|
||||
The event taxonomy removes `agent/pre-step`, `agent/post-step`, `agent/session-prefix`, `agent/step-result`, `agent/turn-continuation`, and `agent/turn-stop`. Durable turn and step boundaries remain session events. Model-facing additions use logged message channels, request configuration uses `agent/request`, response content is recorded as assembled, failed-request recovery uses the `agent/request-error` return action, and end-of-turn continuation uses `agent/turn-stopping` plus steering.
|
||||
The event taxonomy removes the legacy prompt preparation/submission and serial step hooks together with `agent/post-step`, `agent/session-prefix`, `agent/step-result`, `agent/turn-continuation`, and `agent/turn-stop`. The single `agent/pre-step` waterfall owns claimed-message entry. Durable turn and step boundaries remain session events. Model-facing additions use logged message channels, request configuration uses `agent/request`, response content is recorded as assembled, failed-request recovery uses the `agent/request-error` return action, and end-of-turn continuation uses `agent/turn-stopping` plus steering.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -18,16 +18,16 @@ agent 生命周期、agent 整体活动状态、收件箱条目的进度以及
|
||||
|
||||
- 注册生命周期是从 `agent/created` 到 `agent/disposed` 的区间。dispose(资源释放)是注册表的终止边界,而不是一种 `AgentStatus`。
|
||||
- agent 整体活动状态为 `AgentStatus = 'idle' | 'running'`。连续多个轮次可以共用同一个 `running` 区间。
|
||||
- 由 FIFO 支撑的消息从 `agent/inbox/enqueue` 开始,最终必然进入 `agent/inbox/dequeue` 或 `agent/inbox/discard` 二者之一。enqueue 与 dequeue 通过 `MessageId` 加 queued 或 steering(中途引导)放置方式关联一次消息出现;放置方式相同的重复项按 FIFO 顺序结算。收件箱事件描述接受、领取和移除,而不是轮次完成。
|
||||
- 已领取的轮次经过提示词准入和零个或多个请求步骤。自动重试会关闭失败轮次并立即开启另一个轮次;`agent/settled` 只报告该重试链的终态轮次,且仍不同于 agent 整体转换到 `status === 'idle'`。
|
||||
- 待处理消息插入时会发出 `agent/inbox/inserted`,随后要么在原子纯删除领取后发出 `agent/inbox/claimed`,要么在普通删除后发出 `agent/inbox/discarded`。`MessageId` 关联确切消息;持久 splice 坐标保留 placement 与取消信息。inbox 事件描述插入、领取和丢弃,而不是轮次完成。
|
||||
- 已领取的轮次经过 pre-step 进入决策和零个或多个请求步骤。自动重试会关闭失败轮次并立即开启另一个轮次;`agent/settled` 只报告该重试链的终态轮次,且仍不同于 agent 整体转换到 `status === 'idle'`。
|
||||
|
||||
循环保留五个状态机扩展事件。`agent/prompt-submit` 对已领取的提示词执行准入、改写或阻断。`agent/step` 是步骤之间唯一需要等待的检查点,在每次派生请求前运行。`agent/request` 是冻结调用配置所用的 waterfall;配置只能来自 `await next()`,不再通过重复的位置参数提供。`agent/request-error` 串行确定需要等待的模型请求恢复由谁负责。当轮次原本已经没有剩余工作时,`agent/turn-stopping` 运行;需要再执行一个步骤的监听器使用 `agent.steer()` 记录真实的 steering,循环在所有监听器完成后根据这份数据作出决定。
|
||||
循环保留四个状态机扩展事件。`agent/pre-step` 对独占的已领取批次执行 reject 或 enter 决策,并在每个拟议步骤前运行。`agent/request` 是冻结调用配置所用的 waterfall;配置只能来自 `await next()`,不再通过重复的位置参数提供。`agent/request-error` 串行确定需要等待的模型请求恢复由谁负责。当轮次原本已经没有剩余工作时,`agent/turn-stopping` 运行;需要再执行一个步骤的监听器使用 `agent.steer()` 记录真实的 steering,循环在所有监听器完成后根据这份数据作出决定。
|
||||
|
||||
是否继续和终止执行由数据表达,不再由返回的控制枚举表达。工具调用和已接受的 steering 要求再执行一个步骤。携带 `concludesTurn` 的工具结果会在其所属步骤终止工具循环。循环不再暴露通用的 `ContinuationDecision` 或终止返回通道。
|
||||
|
||||
模型请求失败会先关闭当前步骤,再携带该错误本身、标准化 `LlmFailure` 和仍有效的轮次信号进入 `agent/request-error`。负责恢复的监听器修复状态、返回 `{ kind: 'retry' }`,并停止继续委托。循环会关闭失败轮次,并基于该状态开启一个重试轮次,中间不发布空闲通知;重试不是失败轮次内的另一个步骤。`agent/settled` 报告终态结果;对于需要脱离轮次结算单独报告失败的消费方,`agent/error` 仍作为实时错误通知保留。[重试动作决策](2026-07-27-request-error-retry-action.md)取代了本设计中命令形式的部分。
|
||||
|
||||
事件分类体系移除了 `agent/pre-step`、`agent/post-step`、`agent/session-prefix`、`agent/step-result`、`agent/turn-continuation` 和 `agent/turn-stop`。持久的轮次与步骤边界仍由会话事件记录。面向模型的新增内容使用有日志记录的消息通道,请求配置使用 `agent/request`,响应内容按组装后的原样记录,失败请求恢复使用 `agent/request-error` 返回动作,轮次结束时是否继续则使用 `agent/turn-stopping` 加 steering 表达。
|
||||
事件分类体系移除了旧的提示词准备/提交与串行 step hook,以及 `agent/post-step`、`agent/session-prefix`、`agent/step-result`、`agent/turn-continuation` 和 `agent/turn-stop`。唯一的 `agent/pre-step` waterfall 负责已领取消息能否进入步骤。持久的轮次与步骤边界仍由会话事件记录。面向模型的新增内容使用有日志记录的消息通道,请求配置使用 `agent/request`,响应内容按组装后的原样记录,失败请求恢复使用 `agent/request-error` 返回动作,轮次结束时是否继续则使用 `agent/turn-stopping` 加 steering 表达。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: ebefdd91b34f82e9353e41327e42491ac821a792
|
||||
2026-07-27-request-error-retry-action.md: 18ae9bc4ba26d1ad3cb7d1328d9e9d3e8de8377c
|
||||
2026-07-27-request-error-retry-action.zh.md: 5b36a38b6baed9b954fb04bca9e49c24edf9a969
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
waterfall 结算后,循环读取该动作,关闭失败轮次,并从持久历史开启一个重试轮次。循环在使用该动作时会再次检查轮次信号,因此即使监听器随后返回重试动作,恢复期间发生的取消或 dispose(资源释放)仍会阻止重试。抛出异常的恢复不会产生动作。
|
||||
|
||||
`Agent` 与 `ReactLoopAgent` 均不暴露 `retry()` 方法。普通新工作通过 `send()` 及其 `followup()`、`steer()` 和 `inject()` 预设进入;只有已处理的模型请求失败才能开启没有提示词的重试轮次。
|
||||
`Agent` 与 `ReactLoopAgent` 均不暴露 `retry()` 方法。普通新工作通过 `followup()`、`steer()` 和 `inject()` 进入;只有已处理的模型请求失败才能开启没有提示词的重试轮次。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -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-28-remove-synthetic-log-only-turns.md
|
||||
2026-07-28-remove-synthetic-log-only-turns.md: fc76667924ec839301aad993efd996112c9a6b09
|
||||
2026-07-28-remove-synthetic-log-only-turns.zh.md: 7520c33e2219c5fe7ab7d8da6312247e42cc69b0
|
||||
2026-07-28-remove-synthetic-log-only-turns.md: 720aa6bbe246ea960a61f6d6f67d36d1f85af08a
|
||||
2026-07-28-remove-synthetic-log-only-turns.zh.md: 587165a58ae322192b9ed1a9b3312f61e0b21ce6
|
||||
|
||||
@@ -8,7 +8,7 @@ English | [中文](2026-07-28-remove-synthetic-log-only-turns.zh.md)
|
||||
|
||||
The session store exposed `appendOutOfBand()` so a plugin could publish a late log-only event while no agent turn was running. The method wrapped that event in `turn/start` and `turn/end`, then flushed it. This preserved the old rule that every durable event had to live inside a turn, but it made one identifier mean both a model-loop execution and a persistence-only update.
|
||||
|
||||
That rule was introduced when persistence recovery treated the last `turn/end` as the only committed boundary. The persistence scanners now preserve every valid contiguous event, and crash repair reacts only to an actually open turn. Idle context already uses the same capability by appending `user/message` between turns. Retaining synthetic turns for title updates therefore inflated turn counts, produced execution outcomes for work that never ran the model, and let a late metadata write consume the next turn number.
|
||||
That rule was introduced when persistence recovery treated the last `turn/end` as the only committed boundary. The persistence scanners now preserve every valid contiguous event, and crash repair reacts only to an actually open turn. Retaining synthetic turns for title updates therefore inflated turn counts, produced execution outcomes for work that never ran the model, and let a late metadata write consume the next turn number.
|
||||
|
||||
The generic seam also duplicated domain policy. Its marker map said which plugin events were eligible, while the title capability already owned cancellation, liveness, and stale-result rules. Replacing it with another generic or title-specific append wrapper would preserve the same type indirection for two literal event types.
|
||||
|
||||
@@ -20,7 +20,7 @@ Core session invariants continue to enforce core-owned execution relations: turn
|
||||
|
||||
The title service appends `session/title` directly after its existing service, revision, cancellation, and live-session checks. The bundled model helper appends its literal `session/title-llm-request` record before dispatch. Persistence observes both through the eager `session/event` path and drains them at ordinary checkpoints and lifecycle teardown; neither append forces a flush merely because it is between turns. A fallback, auxiliary request record, or accepted provider title may therefore appear after `turn/end` and before the next `turn/start`. Manual compaction uses the same between-turn capability for a `compact/* { turn: null }` bracket, but explicitly flushes the closed attempt because `/compact` promises durability before releasing queued prompt admission.
|
||||
|
||||
A session fork may end at any stable event position outside an open turn, not only at `turn/end`. This preserves standalone title and context records in a default fork while still rejecting a prefix cut through active execution.
|
||||
A session fork may end at any stable event position outside an open turn, not only at `turn/end`. This preserves standalone title and other plugin-owned log-only records in a default fork while still rejecting a prefix cut through active execution.
|
||||
|
||||
The historical [universal turn-enclosure decision](../../archived/architecture/2026-06-15-turn-enclosure-invariant.md) remains useful only as the reason the synthetic mechanism was introduced. The [context-injection decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) established the current meaning: one turn represents one model-loop execution. The [queued manual compaction decision](../feature/2026-07-30-queued-manual-compaction.md) applies that rule to a durable multi-event bracket and owns its marker and admission semantics.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Status: implemented
|
||||
|
||||
会话存储曾暴露 `appendOutOfBand()`,让插件可以在没有 agent(智能体)轮次运行时发布延迟到达的纯日志事件。该方法会用 `turn/start` 和 `turn/end` 包住事件,再将其刷写。这保留了「每个持久事件都必须位于轮次内」的旧规则,却让同一个标识符既表示模型循环执行,又表示仅持久化更新。
|
||||
|
||||
引入该规则时,持久化恢复曾将最后一个 `turn/end` 视为唯一的已提交边界。如今,持久化扫描器会保留每个合法且连续的事件,崩溃修复也只处理确实处于开放状态的轮次。空闲上下文早已采用同一机制,在轮次之间追加 `user/message`。因此,为标题更新保留合成轮次会夸大轮次计数、为从未运行模型的工作产生执行结果,还会让延迟到达的元数据写入占用下一个轮次编号。
|
||||
引入该规则时,持久化恢复曾将最后一个 `turn/end` 视为唯一的已提交边界。如今,持久化扫描器会保留每个合法且连续的事件,崩溃修复也只处理确实处于开放状态的轮次。因此,为标题更新保留合成轮次会夸大轮次计数、为从未运行模型的工作产生执行结果,还会让延迟到达的元数据写入占用下一个轮次编号。
|
||||
|
||||
通用 seam 还重复了领域策略。它的标记映射说明哪些插件事件符合条件,而标题功能本就拥有取消、活跃性和陈旧结果处理规则。改用另一个通用或标题专属追加包装层,仍会为两个字面量事件类型保留同一层类型间接性。
|
||||
|
||||
@@ -20,7 +20,7 @@ Status: implemented
|
||||
|
||||
标题服务会在完成既有的服务状态、修订、取消和实时会话检查后,直接追加 `session/title`。随附模型辅助函数会在发起调用前追加其字面量 `session/title-llm-request` 记录。持久化通过尽快处理的 `session/event` 路径观察两者,并在常规检查点与生命周期 teardown 时排空;二者都不会仅因为位于轮次之间就强制 flush。因此,回退标题、辅助请求记录或已接受的提供方标题可以出现在 `turn/end` 之后、下一个 `turn/start` 之前。手动压缩(compaction)利用同一项轮次间能力记录 `compact/* { turn: null }` 标记对,但会显式 flush 已闭合的尝试,因为 `/compact` 承诺在释放排队提示词接纳预留前完成持久化。
|
||||
|
||||
会话 fork 可以结束于开放轮次之外的任意稳定事件位置,而不限于 `turn/end`。这样,默认 fork 会保留独立标题和上下文记录,同时仍拒绝在活跃执行过程中截断前缀。
|
||||
会话 fork 可以结束于开放轮次之外的任意稳定事件位置,而不限于 `turn/end`。这样,默认 fork 会保留独立标题和其他插件所属的纯日志记录,同时仍拒绝在活跃执行过程中截断前缀。
|
||||
|
||||
历史上的[通用轮次封闭决策](../../archived/architecture/2026-06-15-turn-enclosure-invariant.md)如今只适合用于解释为何曾引入合成机制。[上下文注入决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)确立了当前语义:一个轮次表示一次模型循环执行。[排队手动压缩决策](../feature/2026-07-30-queued-manual-compaction.md)将该规则应用于持久多事件标记对,并拥有其标记与接纳语义。
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 })` 仍会保留已通过受支持交付路径进入待处理状态的工作。
|
||||
@@ -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-31-web-ui-no-steer-entry-or-interjection-chrome.md
|
||||
2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md: b7d4d4fa1e8189d8c5b0c275b15c332cba1b4e6a
|
||||
2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md: 9ce5ac32797a08b4b1f95cd8b350d3f1a7148f80
|
||||
2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md: e2d821f3951af472ef1a13b7b6df88a3aa96a318
|
||||
2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md: b55d4a271e0c5f2729222f4652fb0cb43e5cc9f9
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.zh.md
|
||||
|
||||
## Problem
|
||||
|
||||
Mid-turn steering is a host/agent-loop capability (`mode:'steer'`, durable `steering/message`). The Web product already locked the composer while a turn runs and never shipped a queue/steer menu, yet the client still threaded `'queue' | 'steer'` through the input machine, `conversation.send`, and locale keys, and rendered consumed steering as a badged 「插话」/「Interjection」 bubble. That left a half-built UI surface: an unused submit mode, a product label for a gesture users cannot perform, and e2e goldens that pinned chrome the product does not own.
|
||||
Mid-turn steering is a host/agent-loop capability (`mode:'steer'`, a durable `user/message`). The Web product already locked the composer while a turn runs and never shipped a queue/steer menu, yet the client still threaded `'queue' | 'steer'` through the input machine, `conversation.send`, and locale keys, and rendered consumed steering as a badged 「插话」/「Interjection」 bubble. That left a half-built UI surface: an unused submit mode, a product label for a gesture users cannot perform, and e2e goldens that pinned chrome the product does not own.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -14,7 +14,7 @@ Keep host and runtime steering intact. Remove only the Web UI entry and chrome:
|
||||
|
||||
- `InputMachine` / `SessionInput` / `InputActions.submit` / hub `defaultSink` are queue-only; they always call `session.prompt(..., 'queue')`.
|
||||
- `ConversationService.send(text)` drops its mode argument and always queues.
|
||||
- `MessageItem`'s `steering` arm still folds durable `steering/message` content into a plain right-aligned bubble (no badge, no user IconActions) so external/host steers stay visible on replay.
|
||||
- Durable steer content renders as a plain right-aligned bubble (no badge, no user IconActions) so external/host steers stay visible on replay.
|
||||
- Delete `message.steering` locale strings and the unused badge CSS.
|
||||
- The web steering e2e still POSTs `mode:'steer'` over `/api/session.prompt` and asserts durable + model-visible obedience; it no longer expects interjection chrome. Update [web input machine note](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.md) fact lines to match.
|
||||
|
||||
@@ -22,18 +22,20 @@ Keep host and runtime steering intact. Remove only the Web UI entry and chrome:
|
||||
|
||||
**Delete host steering entirely.** Out of scope; the user asked only for Web UI display and entry. Agent-loop drain, session events, and the wire mode remain load-bearing for ACP/TUI/automation.
|
||||
|
||||
**Hide `steering/message` from the transcript.** Would lie on replay when an external client steers; rejected in favor of a plain bubble.
|
||||
**Hide durable steer `user/message` content from the transcript.** Would lie on replay when an external client steers; rejected in favor of a plain bubble.
|
||||
|
||||
**Keep the mode parameter but only ever pass `'queue'`.** Leaves dead API surface and tests that invent `'steer'` paths the composer cannot reach.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Superseded in part.** Every clause in the Decision about the steer ENTRY, the `queue`/`steer` mode union, the interjection caption, its locale strings, and the goldens that pinned their absence no longer describes master — that is bullets 1 and 3 through 5. Composer steering shipped afterwards, and the [context-source and steer marks decision](../feature/2026-08-04-web-context-source-and-steer-marks.md) then supplied the product decision this note's reintroduction clause required and owns the caption. The consequences and testing below state current fact.
|
||||
- Host steering ownership is unchanged: agent-loop drain, session events, and the wire mode remain load-bearing for ACP, automation, and any non-Web client.
|
||||
- `ConversationService.send(text)` still takes no mode and always queues; the composer's Steer gesture goes through `session.prompt(mode: 'steer')` instead.
|
||||
- `steering/message` still folds into the durable transcript, so an externally submitted steer stays truthful on replay. It now carries the interjection caption rather than rendering as a bare bubble.
|
||||
- **Superseded in part.** Decision bullets 1 and 3 through 5 no longer describe master: composer steering shipped later, and the [context-source and steer marks decision](../feature/2026-08-04-web-context-source-and-steer-marks.md) owns its caption. The current facts follow.
|
||||
- Host steering ownership is unchanged: agent-loop drain, session events, and the wire mode remain load-bearing for ACP, automation, and non-Web clients.
|
||||
- `ConversationService.send(text)` still takes no mode and always queues; the composer's Steer gesture uses `session.prompt(mode: 'steer')` instead.
|
||||
- Durable steer `user/message` content still folds into the transcript, so an externally submitted steer stays truthful on replay. It now carries the interjection caption instead of rendering as a bare bubble.
|
||||
- Non-user next-step items (`agent.inject` context: approval notices, task completion, attached snapshots) broadcast with the `context` placement and never render as pending steering bubbles; they stay invisible until claimed as durable `user/message` context cards.
|
||||
|
||||
## Testing
|
||||
|
||||
- `packages/client/ui-conversation` unit/jsdom coverage: input machine enter/sink, ConversationService routing, the MessageItem steering arm, InputBar submit.
|
||||
- `apps/web/tests/steering.e2e.ts` keyless replay plus its goldens, which now pin the caption.
|
||||
- `apps/web/tests/steering.e2e.ts` keyless replay plus its goldens, which pin the caption.
|
||||
- `packages/host/apiproxy` `session/queue` projection test asserts user-origin next-step items stay `steering` while plugin-origin items land as `context`.
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
中途 steering 是 host/agent-loop 能力(`mode:'steer'`、持久 `steering/message`)。Web 产品已在 turn 运行中锁定 composer,且从未交付排队/steer 菜单,但客户端仍把 `'queue' | 'steer'` 穿进 input machine、`conversation.send` 与 locale 键,并把已消费的 steering 渲染成带「插话」/「Interjection」徽章的气泡。这留下半成品 UI:用不到的提交 mode、用户做不到的手势却有产品文案,以及把产品并不拥有的 chrome 钉死在 e2e golden 上。
|
||||
中途 steering 是 host/agent-loop 能力(`mode:'steer'`、持久 `user/message`)。Web 产品已在 turn 运行中锁定 composer,且从未交付排队/steer 菜单,但客户端仍把 `'queue' | 'steer'` 穿进 input machine、`conversation.send` 与 locale 键,并把已消费的 steering 渲染成带「插话」/「Interjection」徽章的气泡。这留下半成品 UI:用不到的提交 mode、用户做不到的手势却有产品文案,以及把产品并不拥有的 chrome 钉死在 e2e golden 上。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
- `InputMachine`/`SessionInput`/`InputActions.submit`/hub `defaultSink` 仅 queue;始终调用 `session.prompt(..., 'queue')`。
|
||||
- `ConversationService.send(text)` 去掉 mode 参数,始终排队。
|
||||
- `MessageItem` 的 `steering` 分支仍把持久 `steering/message` 内容折成右对齐普通气泡(无徽章、无用户 IconActions),以便外部/host steer 在回放时仍可见。
|
||||
- 持久 steer 内容渲染为右对齐普通气泡(无徽章、无用户 IconActions),以便外部/host steer 在回放时仍可见。
|
||||
- 删除 `message.steering` locale 字符串与未使用的徽章 CSS。
|
||||
- web steering e2e 仍通过 `/api/session.prompt` POST `mode:'steer'`,并断言持久化与模型可见服从;不再期望插话 chrome。同步更新 [web input machine note](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.md) 中的事实行。
|
||||
|
||||
@@ -22,18 +22,20 @@ Status: implemented
|
||||
|
||||
**整段删除 host steering。** 超出范围;用户只要求清 Web UI 展示与入口。agent-loop 排空、session 事件与线缆 mode 对 ACP/TUI/自动化仍是承重能力。
|
||||
|
||||
**在 transcript 中隐藏 `steering/message`。** 外部客户端 steer 时回放会撒谎;改为普通气泡。
|
||||
**在 transcript 中隐藏持久 steer `user/message` 内容。** 外部客户端 steer 时回放会失真,因此改为普通气泡。
|
||||
|
||||
**保留 mode 参数但永远只传 `'queue'`。** 留下死 API 面与只会虚构 composer 到不了的 `'steer'` 路径的测试。
|
||||
|
||||
## 后果
|
||||
|
||||
- **部分被取代。** Decision 中所有关于 steer **入口**、`queue`/`steer` mode 联合类型、插话标注、其 locale 字符串、以及钉住这些「不存在」的黄金基线的子句,均已不再描述 master——即第 1 条与第 3 至 5 条。composer steering 在此之后落地,随后[上下文来源与 steer 标识决策](../feature/2026-08-04-web-context-source-and-steer-marks.md)提供了本 note 重新引入条款所要求的产品决策,并成为该标注的归属者。下面的后果与测试陈述的是当前事实。
|
||||
- host 侧 steering 的归属未变:agent-loop 排空、session 事件与线缆 mode 对 ACP、自动化以及任何非 Web 客户端仍是承重能力。
|
||||
- `ConversationService.send(text)` 仍然不接 mode、始终排队;composer 的 Steer 手势改走 `session.prompt(mode: 'steer')`。
|
||||
- `steering/message` 仍然折叠进持久 transcript,因此从外部提交的 steer 在回放时依旧如实;只是它现在带上了插话标注,而不再是一个无标识的气泡。
|
||||
- **部分被取代。** 决策中的第 1 条和第 3 至 5 条已经不再描述 master:composer steering 后来已经交付,[上下文来源与 steer 标识决策](../feature/2026-08-04-web-context-source-and-steer-marks.md)负责定义其标注。下面列出当前事实。
|
||||
- host 侧 steering 的归属未变:agent-loop 排空、session 事件与线缆 mode 对 ACP、自动化和非 Web 客户端仍然必要。
|
||||
- `ConversationService.send(text)` 仍然不接 mode,始终排队;composer 的 Steer 手势改走 `session.prompt(mode: 'steer')`。
|
||||
- 持久 steer `user/message` 内容仍然折叠进 transcript,因此外部提交的 steer 会如实出现在回放中。它现在带有插话标注,而不是无标识气泡。
|
||||
- 非用户来源的 next-step 项(`agent.inject` 上下文:审批通知、任务完成、附加快照)以 `context` placement 广播,绝不渲染为待处理 steering 气泡;领取为持久 `user/message` context card 前保持不可见。
|
||||
|
||||
## 测试
|
||||
|
||||
- `packages/client/ui-conversation` unit/jsdom 覆盖:input machine enter/sink、ConversationService 路由、MessageItem 的 steering 分支、InputBar submit。
|
||||
- `apps/web/tests/steering.e2e.ts` 无密钥回放及其黄金基线,后者现在钉住该标注。
|
||||
- `packages/client/ui-conversation` unit/jsdom 覆盖:input machine enter/sink、ConversationService 路由、MessageItem steering 分支、InputBar submit。
|
||||
- `apps/web/tests/steering.e2e.ts` 无密钥回放及其黄金基线,后者会检查插话标注。
|
||||
- `packages/host/apiproxy` 的 `session/queue` 投影测试断言用户来源的 next-step 项保持 `steering`,而插件来源的项落入 `context`。
|
||||
|
||||
Reference in New Issue
Block a user