diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml index 19c1dd7fce..d04fb39259 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml @@ -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-21-bounded-llm-request-recovery.md: 42134f6b28ed2ae60597868a73b800f6c488ab96 -2026-06-21-bounded-llm-request-recovery.zh.md: 171e29bca58e1634dc954e9df94541791da65571 +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md +2026-06-21-bounded-llm-request-recovery.md: dec396b992a56c50c1ccd57f04ff0e8954fed07d +2026-06-21-bounded-llm-request-recovery.zh.md: c32f2b3974d2cdb4dd8cd0f8ea132e76893f6007 diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md index 42134f6b28..dec396b992 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md @@ -50,7 +50,7 @@ The shared transient-code set is intentionally small: adapter mappings for `RATE `@deepseek-ai/dsh-llm-retry` is a function plugin that listens to `agent/request-error`. It introduces no service or new loop branch; the agent-loop package changes only the data carried through its existing failed-step recovery control flow. -The `agent/request-error` seam carries only the current `LlmFailure`; the loop owns no retry policy or attempt history. Each recovery plugin keeps a private per-agent counter for its own handled failures and clears it at terminal `agent/idle`. Alternating transient and context-overflow failures therefore consume the `dsh-llm-retry` and compact-basic budgets independently; the maximum request count is one plus the sum of the finite budgets of the loaded recovery policies. +The `agent/request-error` seam carries only the current `LlmFailure`; the loop owns no retry policy or attempt history. Each recovery plugin keeps a private per-agent counter for its own handled failures and clears it at terminal `agent/settled`. Alternating transient and context-overflow failures therefore consume the `dsh-llm-retry` and compact-basic budgets independently; the maximum request count is one plus the sum of the finite budgets of the loaded recovery policies. The plugin resolves and validates this deployment configuration at load: diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md index 171e29bca5..c32f2b3974 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md @@ -50,7 +50,7 @@ agent loop(智能体循环)会保留 `RequestError` 作为该精确的错误 `@deepseek-ai/dsh-llm-retry` 是监听 `agent/request-error` 的函数插件。它不引入服务或新的循环分支;agent-loop 包仅会更改通过现有失败步骤恢复控制流携带的数据。 -`agent/request-error` seam 只携带当前 `LlmFailure`;循环不拥有重试策略或尝试历史。每个恢复插件为自身处理的失败维护一个逐 agent 的私有计数器,并在终态 `agent/idle` 时清零。因此,暂时性失败与上下文溢出交替出现时,`dsh-llm-retry` 与 compact-basic 的预算独立消耗;最大请求数等于 1 加上已加载恢复策略的有限预算总和。 +`agent/request-error` seam 只携带当前 `LlmFailure`;循环不拥有重试策略或尝试历史。每个恢复插件为自身处理的失败维护一个逐 agent 的私有计数器,并在终态 `agent/settled` 时清零。因此,暂时性失败与上下文溢出交替出现时,`dsh-llm-retry` 与 compact-basic 的预算独立消耗;最大请求数等于 1 加上已加载恢复策略的有限预算总和。 该插件在加载时解析并验证以下部署配置: diff --git a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml index fdd7444512..b2a0baeb34 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write -2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: 83fe62276b235d5961ed0fee61a4a1603c383c56 -2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: fef9f48cbcfda42a13a88737f10b446ba317d5de +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md +2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: a83408128f164f76357fa2bdf33188caaa30d8a4 +2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: 86037016303aa05e9f370aba3c50789665ebef1d diff --git a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md index 83fe62276b..a83408128f 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md +++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md @@ -24,7 +24,7 @@ The loop fires awaited serial `agent/post-step(agent, turn, step, signal)` after `RequestError` and the `agent/request-error` waterfall represent failures after the final adapter has been selected. Each returned stream handle owns a private failure set that preserves the original thrown error identity across dispatch, iterator construction, and iteration without leaking nested-call provenance into an outer call. Terminal in-band `error` or `aborted` finishes enter the same path. Prompt assembly, request middleware, request logging, result processing, tools, step listeners, and cleanup remain ordinary failures. -The failed step closes before recovery runs. A handling listener repairs durable state, calls `agent.retry()`, and stops waterfall delegation. The loop then closes the failed turn and opens one retry turn from the durable log without an intervening idle notification. Retry policy and attempt counts remain plugin-owned; compact-basic clears its per-agent overflow count when the chain reaches terminal `agent/idle`. Both DeepSeek adapters normalize recognized provider context-limit failures to `CONTEXT_WINDOW_EXCEEDED`. +The failed step closes before recovery runs. A handling listener repairs durable state, calls `agent.retry()`, and stops waterfall delegation. The loop then closes the failed turn and opens one retry turn from the durable log without an intervening idle notification. Retry policy and attempt counts remain plugin-owned; compact-basic clears its per-agent overflow count when the chain reaches terminal `agent/settled`. Both DeepSeek adapters normalize recognized provider context-limit failures to `CONTEXT_WINDOW_EXCEEDED`. If cancellation lands after assistant tool calls are durable but before all calls dispatch, the loop records a synthetic `tool/call` and aborted `tool/result` pair for every undispatched call before following the normal abort path. The surface therefore never retains orphaned durable tool calls merely because cancellation won the race. diff --git a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md index fef9f48cbc..8603701630 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md @@ -24,7 +24,7 @@ Status: implemented `RequestError` 与 `agent/request-error` waterfall 表示最终适配器已经选定之后的失败。每个返回的流句柄都绑定一个私有失败集合;该集合在分发、异步迭代器构造与迭代过程中保留原始抛出错误的身份,同时防止把嵌套调用的错误来源误归到外层调用。终止性的带内 `error` 或 `aborted` finish 进入同一路径。提示词装配、请求中间件、请求日志、结果处理、工具、step 监听器与清理仍属于普通失败。 -恢复运行前,失败 step 已经关闭。负责处理的监听器修复持久状态、调用 `agent.retry()`,并停止 waterfall 委托。循环随后关闭失败 turn,并从持久日志开启一个重试 turn,中间不发布空闲通知。重试策略与尝试计数由插件自己拥有;compact-basic 在链路到达终态 `agent/idle` 时清除对应 agent 的溢出计数。两个 DeepSeek 适配器都把识别出的提供方上下文限制错误规范化为 `CONTEXT_WINDOW_EXCEEDED`。 +恢复运行前,失败 step 已经关闭。负责处理的监听器修复持久状态、调用 `agent.retry()`,并停止 waterfall 委托。循环随后关闭失败 turn,并从持久日志开启一个重试 turn,中间不发布空闲通知。重试策略与尝试计数由插件自己拥有;compact-basic 在链路到达终态 `agent/settled` 时清除对应 agent 的溢出计数。两个 DeepSeek 适配器都把识别出的提供方上下文限制错误规范化为 `CONTEXT_WINDOW_EXCEEDED`。 如果取消发生在 assistant 工具调用已经持久化之后、所有调用完成分发之前,循环会为每个尚未分发的调用记录一对合成的 `tool/call` 与 aborted `tool/result`,随后进入正常中止路径。因此,表层不会仅因取消赢得竞态而留下孤立的持久工具调用。 diff --git a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml index 92400a3ff6..114faf7817 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write -2026-07-22-unified-send-and-coalesced-user-messages.md: 47822d48496313a5e31d10abef85cb4ff24163c6 -2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 323ed974d4e3d88f8f9cf137f4418da81ac6098a +# 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: 0f5b3d64a5e1d12f3c5838111b6d153498564513 +2026-07-22-unified-send-and-coalesced-user-messages.zh.md: c3593cb483827e70529f680921d5e201178e245a diff --git a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md index 47822d4849..0f5b3d64a5 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md +++ b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md @@ -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 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 steering submitted by an `agent/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. +**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 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. **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. diff --git a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md index 323ed974d4..c3593cb483 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md @@ -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`、内容和来源;调用方因此可以把一个排队项与其生命周期关联起来,而无需依赖驱动器的路由状态。注入从不触及 FIFO,也不发出这些事件中的任何一个。每一次 FIFO 入队都会发布一个 enqueue 事件,包括 `agent/stopping` 监听器提交的 steering,因此账目会与其后的 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 事件,包括 `agent/turn-stopping` 监听器提交的 steering,因此账目会与其后的 dequeue 或 discard 保持平衡。`dsh-agent` 的不变量配套断言 FIFO 守恒:一个按 agent 计的未结算计数,dequeue 和 discard 永远无法把它压到负数。 **一条已接受消息只保留一种表示。** 持久的用户角色输入和附加的模型可见上下文都直接使用 `UserMessageData { content, source }`;公开的 `AgentMessage` 在此基础上增加用于关联的 `id`,循环私有的 `PendingMessage` 再增加 `wakeup`。一条成为 steering 的排队消息会以同一个 `PendingMessage` 对象进入 outbox,而注入和工具产生的上下文则以普通 `UserMessageData` 进入。因此,outbox 直接存储这两种类型的联合,而不再把 steering 与一份重复的内容和来源副本包装在一起。提供方原生的助手消息仍是适配器拥有的输出类型,不参与这套输入层级。 diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.i18n.yaml index 5f3920e58c..a7a00fd548 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write -2026-07-20-error-cause-chain-diagnostics.md: 700c36c45ae8c939b6d9161ab90ab5dfe2c92686 -2026-07-20-error-cause-chain-diagnostics.zh.md: b358ec606b7c92641f3e6138fe23d832431bd5ce +# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md +2026-07-20-error-cause-chain-diagnostics.md: 7de1f4f631cec90048ccc8eab7a6654560d84846 +2026-07-20-error-cause-chain-diagnostics.zh.md: 74820e80f729343a833c926adc6187b7cc9fc372 diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md index 700c36c45a..7de1f4f631 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md +++ b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md @@ -15,7 +15,7 @@ A TUI run against an unreachable DeepSeek endpoint failed with the single notice - `dsh-llm` exports `errorChain(value)`: renders a thrown value with its full `cause` chain (`outer: inner: …`) and AggregateError members (`msg [m1; m2]`), with circular-cause and hostile-coercion containment. It is a diagnostic-surface renderer only; routing stays on `HarnessError.code`. - The DeepSeek adapter wraps a pre-response transport failure in `LlmError('TRANSPORT')` naming the configured `baseURL` and chaining the original rejection as `cause`. An aborted request becomes `LlmError('ABORTED')`; because the turn signal is already aborted, the loop still classifies the turn as cancellation rather than recovery. -- Every diagnostic seam renders through `errorChain` instead of `error.message`/`String(error)`: the agent-loop's durable `turn/end` error message (`errorData`), its logger warnings, the TUI's `agent/error` notice and startup-failure line, and `dsh-stdio`'s startup-failure log lines. The live `agent/error` event and `IdleReason` preserve the thrown value as `unknown`; each diagnostic consumer renders it instead of the loop wrapping it into another error. The per-package `renderThrown` copies in `dsh-agent-loop`, `dsh-stdio`, and `dsh-tui` are deleted in favor of the one shared renderer. +- Every diagnostic seam renders through `errorChain` instead of `error.message`/`String(error)`: the agent-loop's durable `turn/end` error message (`errorData`), its logger warnings, the TUI's `agent/error` notice and startup-failure line, and `dsh-stdio`'s startup-failure log lines. The live `agent/error` event and `SettleReason` preserve the thrown value as `unknown`; each diagnostic consumer renders it instead of the loop wrapping it into another error. The per-package `renderThrown` copies in `dsh-agent-loop`, `dsh-stdio`, and `dsh-tui` are deleted in favor of the one shared renderer. - `dsh-stdio` renders failure `turn/end` reasons: `[turn failed ] `, `[turn aborted] `, `[turn rejected] `, `[turn interrupted by a previous process exit]`, and the output-token-limit notice. Unknown merge-extended kinds fall through as ordinary turn ends. `errorChain` lives in `dsh-llm` beside `HarnessError` for the same reason the base class does: it is the leaf package every consumer already imports, so sharing costs no new dependency edge. diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.zh.md b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.zh.md index b358ec606b..74820e80f7 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.zh.md @@ -15,7 +15,7 @@ TUI 连接不可达的 DeepSeek 端点时,失败只显示一条 `fetch failed` - `dsh-llm` 导出 `errorChain(value)`:渲染抛出值及其完整 `cause` 链(`outer: inner: …`)与 AggregateError 成员(`msg [m1; m2]`),并容错循环 cause 和恶意强制转换。它只是诊断表面的渲染器;路由仍然基于 `HarnessError.code`。 - DeepSeek 适配器把拿到响应之前的传输失败包装成 `LlmError('TRANSPORT')`,写明配置的 `baseURL` 并把原始拒绝值链为 `cause`。被中止的请求变为 `LlmError('ABORTED')`;由于轮次信号已处于中止状态,循环仍将该轮次归类为取消而非恢复。 -- 每个诊断接缝改用 `errorChain` 而非 `error.message`/`String(error)`:agent-loop 的持久化 `turn/end` 错误消息(`errorData`)、其日志警告、TUI 的 `agent/error` 通知与启动失败行、以及 `dsh-stdio` 的启动失败日志行。实时 `agent/error` 事件与 `IdleReason` 以 `unknown` 原样保留抛出值;各诊断消费者自行渲染,而不是由循环把它包装成另一个错误。`dsh-agent-loop`、`dsh-stdio`、`dsh-tui` 里各自的 `renderThrown` 副本被删除,统一使用这一个共享渲染器。 +- 每个诊断接缝改用 `errorChain` 而非 `error.message`/`String(error)`:agent-loop 的持久化 `turn/end` 错误消息(`errorData`)、其日志警告、TUI 的 `agent/error` 通知与启动失败行、以及 `dsh-stdio` 的启动失败日志行。实时 `agent/error` 事件与 `SettleReason` 以 `unknown` 原样保留抛出值;各诊断消费者自行渲染,而不是由循环把它包装成另一个错误。`dsh-agent-loop`、`dsh-stdio`、`dsh-tui` 里各自的 `renderThrown` 副本被删除,统一使用这一个共享渲染器。 - `dsh-stdio` 渲染失败的 `turn/end` reason:`[turn failed ] `、`[turn aborted] `、`[turn rejected] `、`[turn interrupted by a previous process exit]` 以及输出 token 上限通知。未知的 merge 扩展 kind 按普通 turn 结束处理。 `errorChain` 与 `HarnessError` 一样放在 `dsh-llm` 里,理由相同:它是每个消费者都已导入的叶子包,共享不增加新的依赖边。 diff --git a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml index dc44bbad28..8d57b6fdcc 100644 --- a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml @@ -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-30-hook-bridges.md: 04cbef6738bcc1d0f6ac6a395ce4abecdb16b984 -2026-06-30-hook-bridges.zh.md: 60146edcbab8098d52682d77e3f57c6d2676f291 +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-hook-bridges.md +2026-06-30-hook-bridges.md: 99c6b1941a10e198ec3028f5fe505dabfff9abbe +2026-06-30-hook-bridges.zh.md: 11ed3a5d177661271b30f1a58d034caa577b5348 diff --git a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md index 04cbef6738..99c6b1941a 100644 --- a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md +++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md @@ -6,7 +6,7 @@ English | [中文](2026-06-30-hook-bridges.zh.md) ## Problem -The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/prompt-submit`, `tools/pre-execute`, `tools/post-execute`, `agent/stopping`, `subagent/start`, or `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)). +The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/prompt-submit`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-stopping`, `subagent/start`, or `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)). The framing that shapes the whole design: **a bridge is a compatibility adapter, not a power tool.** Anything a bridge does (block a tool, inject context, force continuation, observe a subagent) a native cordis plugin does more powerfully — typed returns, full `ctx`, no serialization boundary. The bridge's reason to exist is to run the explicitly supported subset of external CC/Codex command hooks. That keeps each bridge thin: parse the config, pick a matcher mode, build the per-event payload, call `runHook` + `mergeHookOutputs` from the shared lib, and map the neutral outcome onto a seam Decision. The package READMEs own the exact current unsupported-event and partial-field inventory against the official protocols. @@ -27,7 +27,7 @@ Each bridge maps the neutral `MergedHookOutcome` from the shared lib onto the se | `agent/prompt-submit` | `deny`→`block`; context-only→delegate+fold | `block`→`block`; context-only→delegate+fold | | `tools/pre-execute` | `deny`→`deny`; `ask`→`ask` | `block`→`deny` (no allow/ask) | | `tools/post-execute` | `deny`→`block`+feedback; context-only→delegate+fold | same | -| `agent/stopping` | blocking Stop → next-step steering | same | +| `agent/turn-stopping` | blocking Stop → next-step steering | same | | `subagent/start` (emit) | additionalContext → inject into a live in-process child; a remote child has no local injection target | unsupported by this bridge | | `subagent/end` (emit) | observe-only | unsupported by this bridge | diff --git a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md index 60146edcba..11ed3a5d17 100644 --- a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md +++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.zh.md @@ -6,7 +6,7 @@ Status: implemented ## 问题 -harness 的扩展面是其类型化的拦截 seam(见[拦截 seam Agent Note](2026-06-30-interception-seams.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start`、`agent/prompt-submit`、`tools/pre-execute`、`tools/post-execute`、`agent/stopping`、`subagent/start` 或 `subagent/end`。但用户带着**既有的** Claude Code(CC)和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 Agent Note 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化 seam 上,构建于共享的协议格式(wire format)库之上(见 [hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md))。 +harness 的扩展面是其类型化的拦截 seam(见[拦截 seam Agent Note](2026-06-30-interception-seams.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start`、`agent/prompt-submit`、`tools/pre-execute`、`tools/post-execute`、`agent/turn-stopping`、`subagent/start` 或 `subagent/end`。但用户带着**既有的** Claude Code(CC)和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 Agent Note 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化 seam 上,构建于共享的协议格式(wire format)库之上(见 [hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md))。 贯穿整个设计的定位:**桥接是兼容性适配器,不是高级工具。** 桥接能做的事(阻止工具、注入上下文、强制继续、观察 subagent),原生 Cordis 插件都能做得更强——类型化返回值、完整 `ctx`、无序列化边界。桥接存在的理由是运行外部 CC/Codex 命令钩子中被明确支持的子集。这使每个桥接保持精简:解析配置、选择匹配模式、构建每事件的 payload、调用共享库的 `runHook` + `mergeHookOutputs`,再将中性结果映射为 seam Decision。各包的 README 维护着当前不支持的事件和部分字段的完整清单,以官方协议为参照。 @@ -27,7 +27,7 @@ harness 的扩展面是其类型化的拦截 seam(见[拦截 seam Agent Note]( | `agent/prompt-submit` | `deny`→`block`;仅上下文→delegate+fold | `block`→`block`;仅上下文→delegate+fold | | `tools/pre-execute` | `deny`→`deny`;`ask`→`ask` | `block`→`deny`(无 allow/ask) | | `tools/post-execute` | `deny`→`block`+feedback;仅上下文→delegate+fold | 同上 | -| `agent/stopping` | 阻塞的 Stop → 下一步 steering(中途引导) | 同上 | +| `agent/turn-stopping` | 阻塞的 Stop → 下一步 steering(中途引导) | 同上 | | `subagent/start`(emit) | additionalContext → 注入到存活的进程内 subagent;远程 subagent 无本地注入目标 | 本桥接不支持 | | `subagent/end`(emit) | 仅观察 | 本桥接不支持 | diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml b/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml index 0d5d8440bd..5845297101 100644 --- a/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml @@ -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-30-interception-seams.md: 9c293c273e2c14f168ef5a7e7fa70f06f82955ac -2026-06-30-interception-seams.zh.md: 39eb68983dfcfc4f2d77dc709880167c72469f83 +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-interception-seams.md +2026-06-30-interception-seams.md: 4658983e1f098ecd199eecec4408e7c2f134cbf7 +2026-06-30-interception-seams.zh.md: 13b7c56829412773111fcf6d75cc717c51d49c7b diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-seams.md b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md index 9c293c273e..4658983e1f 100644 --- a/.agents/notes/implemented/feature/2026-06-30-interception-seams.md +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md @@ -18,7 +18,7 @@ The canonical surface separates transformable policy, around-dispatch control, a - `agent/session-start(agent, source)` — emit, once before turn 1, carrying a `SessionStartSource` (`startup` for a fresh/forked create, `resume` for a reloaded persisted session; `clear`/`compact` reserved). A pure notification — it CANNOT block startup (a deliberate gap: a bridge logs/injects, it does not gate startup). A listener seeds context via `agent.inject()`. - `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` — waterfall, fired for one claimed queued message before the loop opens a turn or appends `user/message`. The explicit admission signal is placed before the final `next`; `allow` optionally rewrites the prompt `content` or attaches separately sourced `additionalContexts[]`, while `block` discards the candidate without creating session history. -**`agent/stopping`** is an awaited notification at the natural stop boundary. A listener that needs another step calls `agent.steer()` with explicitly sourced model-facing content; the loop then re-reads the outbox and either continues or closes the turn. +**`agent/turn-stopping`** is an awaited notification at the natural stop boundary. A listener that needs another step calls `agent.steer()` with explicitly sourced model-facing content; the loop then re-reads the outbox and either continues or closes the turn. ### The tool pipeline gives each phase one kind of authority @@ -47,7 +47,7 @@ Core dispatch and the tool body sit inside normalization boundaries, so tool, li ### Boundaries -The seam package does **not** declare `hook/*` session events (the durable hook-invocation log); those belong to `dsh-hook-protocol`, because a native plugin uses typed decisions without an external hook log. The native-plugin integration test (`packages/core/agent-loop/tests/interception.spec.ts`) composes the seams through the real loop with no `hook/*` protocol. Compaction (`PreCompact`/`PostCompact`), Notification, and Codex `PermissionRequest` remain outside this decision. The [approval seam](2026-07-06-approval-seam.md) resolves `ask` decisions through `ctx.approval`; terminal monotonic stopping is expressed by tool-result data, while `agent/stopping` is the last chance to steer another step. +The seam package does **not** declare `hook/*` session events (the durable hook-invocation log); those belong to `dsh-hook-protocol`, because a native plugin uses typed decisions without an external hook log. The native-plugin integration test (`packages/core/agent-loop/tests/interception.spec.ts`) composes the seams through the real loop with no `hook/*` protocol. Compaction (`PreCompact`/`PostCompact`), Notification, and Codex `PermissionRequest` remain outside this decision. The [approval seam](2026-07-06-approval-seam.md) resolves `ask` decisions through `ctx.approval`; terminal monotonic stopping is expressed by tool-result data, while `agent/turn-stopping` is the last chance to steer another step. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md b/.agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md index 39eb68983d..13b7c56829 100644 --- a/.agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.zh.md @@ -18,7 +18,7 @@ harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那 - `agent/session-start(agent, source)` ——emit,在第 1 轮次之前触发一次,携带 `SessionStartSource`(`startup` 表示全新/fork 创建,`resume` 表示重新加载的持久化会话;`clear`/`compact` 保留)。纯通知,不能阻塞启动(这是有意的空白:桥接可以记录/注入,但不管控启动)。监听器通过 `agent.inject()` 注入上下文。 - `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` ——waterfall,针对一条取得所有权的排队消息触发,早于循环开启轮次或追加 `user/message`。显式准入 signal 位于最后的 `next` 之前;`allow` 可以重写提示词 `content` 或附加来源各自独立的 `additionalContexts[]`,而 `block` 会丢弃该候选消息,不产生会话历史。 -**`agent/stopping`** 是自然停止边界上的一次 awaited 通知。需要再执行一步的监听器调用 `agent.steer()`,传入来源显式的面向模型的内容(steering,中途引导);循环随后重新读取 outbox,继续执行或关闭轮次。 +**`agent/turn-stopping`** 是自然停止边界上的一次 awaited 通知。需要再执行一步的监听器调用 `agent.steer()`,传入来源显式的面向模型的内容(steering,中途引导);循环随后重新读取 outbox,继续执行或关闭轮次。 ### 工具流水线为每个阶段赋予一种权限 @@ -47,7 +47,7 @@ harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那 ### 边界 -seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);那些属于 `dsh-hook-protocol`,因为原生插件使用类型化 decision 而无需外部钩子日志。原生插件集成测试(`packages/core/agent-loop/tests/interception.spec.ts`)通过真实循环组合这些 seam,不涉及 `hook/*` 协议。压缩(compaction)(`PreCompact`/`PostCompact`)、Notification 和 Codex `PermissionRequest` 不在本决策范围内。[审批 seam](2026-07-06-approval-seam.md) 通过 `ctx.approval` 解析 `ask` decision;终结性的单调停止由工具结果数据表达,而 `agent/stopping` 是引导再执行一步的最后机会。 +seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);那些属于 `dsh-hook-protocol`,因为原生插件使用类型化 decision 而无需外部钩子日志。原生插件集成测试(`packages/core/agent-loop/tests/interception.spec.ts`)通过真实循环组合这些 seam,不涉及 `hook/*` 协议。压缩(compaction)(`PreCompact`/`PostCompact`)、Notification 和 Codex `PermissionRequest` 不在本决策范围内。[审批 seam](2026-07-06-approval-seam.md) 通过 `ctx.approval` 解析 `ask` decision;终结性的单调停止由工具结果数据表达,而 `agent/turn-stopping` 是引导再执行一步的最后机会。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.i18n.yaml index 1aa37f9255..321f5b1dac 100644 --- a/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write -2026-07-24-agent-loop-observable-state-machine.md: 7a3607c5bcf6edece07433acd766a6dece551d1c -2026-07-24-agent-loop-observable-state-machine.zh.md: 7b53eb6087e99ce4c84caae7b94d275c0b6a10db +# 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: 2905e003a05bf1c8caa8f784787fb76f8e76b7d7 +2026-07-24-agent-loop-observable-state-machine.zh.md: 1d6b5e5d34d020ef5561b8016b5ad065c96bff12 diff --git a/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.md b/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.md index 7a3607c5bc..2905e003a0 100644 --- a/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.md +++ b/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.md @@ -19,15 +19,15 @@ 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`, correlated by `AgentMessageId`. 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/idle` reports only the terminal turn in that chain and remains distinct from the whole-agent transition to `status === 'idle'`. +- 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'`. -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/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 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. 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, calls `agent.retry()`, and returns without 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/idle` reports the terminal outcome, and `agent/error` remains the live error notification for consumers that report failures independently of turn settlement. +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, calls `agent.retry()`, and returns without 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 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 `agent/request-error` plus `agent.retry()`, and end-of-turn continuation uses `agent/stopping` plus steering. +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 `agent/request-error` plus `agent.retry()`, and end-of-turn continuation uses `agent/turn-stopping` plus steering. ## Alternatives considered @@ -41,7 +41,7 @@ The event taxonomy removes `agent/pre-step`, `agent/post-step`, `agent/session-p ## Consequences -The observable machine is smaller and compositional: registration lifetime, activity, item progress, and terminal settlement can be followed independently. In particular, `agent/idle` does not imply `agent.status === 'idle'`; it reports the terminal turn of one drain chain, while `agent/status` reports whether the whole agent is active. +The observable machine is smaller and compositional: registration lifetime, activity, item progress, and terminal settlement can be followed independently. In particular, `agent/settled` does not imply `agent.status === 'idle'`; it reports the terminal turn of one drain chain, while `agent/status` reports whether the whole agent is active. Plugins no longer rewrite every phase of the loop. There is no request-only message prefix, assistant-message transform, post-step checkpoint, generic continuation enum, generic terminal-stop result, or in-turn request retry. Extensions use the remaining owned channels instead of recreating those phases. diff --git a/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.zh.md b/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.zh.md index 7b53eb6087..1d6b5e5d34 100644 --- a/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.zh.md +++ b/.agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.zh.md @@ -19,15 +19,15 @@ agent 生命周期、agent 整体活动状态、收件箱条目的进度以及 - 注册生命周期是从 `agent/created` 到 `agent/disposed` 的区间。dispose(资源释放)是注册表的终止边界,而不是一种 `AgentStatus`。 - agent 整体活动状态为 `AgentStatus = 'idle' | 'running'`。连续多个轮次可以共用同一个 `running` 区间。 - 由 FIFO 支撑的消息从 `agent/inbox/enqueue` 开始,最终必然进入 `agent/inbox/dequeue` 或 `agent/inbox/discard` 二者之一,并通过 `AgentMessageId` 关联。收件箱事件描述接受、领取和移除,而不是轮次完成。 -- 已领取的轮次经过提示词准入和零个或多个请求步骤。自动重试会关闭失败轮次并立即开启另一个轮次;`agent/idle` 只报告该重试链的终态轮次,且仍不同于 agent 整体转换到 `status === 'idle'`。 +- 已领取的轮次经过提示词准入和零个或多个请求步骤。自动重试会关闭失败轮次并立即开启另一个轮次;`agent/settled` 只报告该重试链的终态轮次,且仍不同于 agent 整体转换到 `status === 'idle'`。 -循环保留五个状态机扩展事件。`agent/prompt-submit` 对已领取的提示词执行准入、改写或阻断。`agent/step` 是步骤之间唯一需要等待的检查点,在每次派生请求前运行。`agent/request` 是冻结调用配置所用的 waterfall;配置只能来自 `await next()`,不再通过重复的位置参数提供。`agent/request-error` 串行确定需要等待的模型请求恢复由谁负责。当轮次原本已经没有剩余工作时,`agent/stopping` 运行;需要再执行一个步骤的监听器使用 `agent.steer()` 记录真实的 steering(中途引导),循环在所有监听器完成后根据这份数据作出决定。 +循环保留五个状态机扩展事件。`agent/prompt-submit` 对已领取的提示词执行准入、改写或阻断。`agent/step` 是步骤之间唯一需要等待的检查点,在每次派生请求前运行。`agent/request` 是冻结调用配置所用的 waterfall;配置只能来自 `await next()`,不再通过重复的位置参数提供。`agent/request-error` 串行确定需要等待的模型请求恢复由谁负责。当轮次原本已经没有剩余工作时,`agent/turn-stopping` 运行;需要再执行一个步骤的监听器使用 `agent.steer()` 记录真实的 steering(中途引导),循环在所有监听器完成后根据这份数据作出决定。 是否继续和终止执行由数据表达,不再由返回的控制枚举表达。工具调用和已接受的 steering 要求再执行一个步骤。携带 `concludesTurn` 的工具结果会在其所属步骤终止工具循环。循环不再暴露通用的 `ContinuationDecision` 或终止停止返回通道。 -模型请求失败会先关闭当前步骤,再携带准确错误、标准化 `LlmFailure` 和仍有效的轮次信号进入 `agent/request-error`。负责恢复的监听器修复状态、调用 `agent.retry()`,并停止继续委托。循环会关闭失败轮次,并基于该状态开启一个重试轮次,中间不发布空闲通知;重试不是失败轮次内的另一个步骤。`agent/idle` 报告终态结果;对于需要脱离轮次结算单独报告失败的消费方,`agent/error` 仍作为实时错误通知保留。 +模型请求失败会先关闭当前步骤,再携带准确错误、标准化 `LlmFailure` 和仍有效的轮次信号进入 `agent/request-error`。负责恢复的监听器修复状态、调用 `agent.retry()`,并停止继续委托。循环会关闭失败轮次,并基于该状态开启一个重试轮次,中间不发布空闲通知;重试不是失败轮次内的另一个步骤。`agent/settled` 报告终态结果;对于需要脱离轮次结算单独报告失败的消费方,`agent/error` 仍作为实时错误通知保留。 -事件分类体系移除了 `agent/pre-step`、`agent/post-step`、`agent/session-prefix`、`agent/step-result`、`agent/turn-continuation` 和 `agent/turn-stop`。持久的轮次与步骤边界仍由会话事件记录。面向模型的新增内容使用有日志记录的消息通道,请求配置使用 `agent/request`,响应内容按组装后的原样记录,失败请求恢复使用 `agent/request-error` 加 `agent.retry()`,轮次结束时是否继续则使用 `agent/stopping` 加 steering 表达。 +事件分类体系移除了 `agent/pre-step`、`agent/post-step`、`agent/session-prefix`、`agent/step-result`、`agent/turn-continuation` 和 `agent/turn-stop`。持久的轮次与步骤边界仍由会话事件记录。面向模型的新增内容使用有日志记录的消息通道,请求配置使用 `agent/request`,响应内容按组装后的原样记录,失败请求恢复使用 `agent/request-error` 加 `agent.retry()`,轮次结束时是否继续则使用 `agent/turn-stopping` 加 steering 表达。 ## 考虑过的替代方案 @@ -41,7 +41,7 @@ agent 生命周期、agent 整体活动状态、收件箱条目的进度以及 ## 影响 -可观察状态机更小,也更容易组合:注册生命周期、活动状态、条目进度和终态结算可以分别追踪。尤其是,`agent/idle` 并不意味着 `agent.status === 'idle'`;前者报告一次排空链的终态轮次,`agent/status` 则报告整个 agent 是否处于活动状态。 +可观察状态机更小,也更容易组合:注册生命周期、活动状态、条目进度和终态结算可以分别追踪。尤其是,`agent/settled` 并不意味着 `agent.status === 'idle'`;前者报告一次排空链的终态轮次,`agent/status` 则报告整个 agent 是否处于活动状态。 插件不再能够改写循环的每个阶段。不再提供仅用于请求的消息前缀、助手消息变换、步骤后检查点、通用的继续执行枚举、通用的终止停止结果或轮次内请求重试。扩展改用剩余的归属明确的通道,而不是重新构造这些阶段。 diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index d0eedfb4c8..63bd889e5a 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -51,7 +51,7 @@ sequenceDiagram end Driver->>Session: post-tool context and steering (no prompt-submit) Driver->>Session: step/end - Driver->>Hooks: agent/stopping serial terminal checkpoint + Driver->>Hooks: agent/turn-stopping serial terminal checkpoint end Driver->>Session: turn/end Driver-->>SDK: agent/status idle diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index dd1726e40f..d094404d67 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -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 docs/architecture.md -architecture.md: 16565546387882aa9af34beb1e275e3177460161 -architecture.zh.md: ce5cb31dd8fceb9c6a6bb91ebb0ebfafadd273f7 +architecture.md: e293d3f3996210fd6b4d9784379eef58cd6c9f0b +architecture.zh.md: 9760511e979fa9bb20b812fbbbefcefa09565c55 diff --git a/docs/architecture.md b/docs/architecture.md index 1656554638..e293d3f399 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -99,8 +99,8 @@ forever: drain accepted tool context and steering 'step/end' continue for tools or steering unless a result concluded the turn - otherwise agent/stopping -> drain -> continue only for steering - 'turn/end' -> agent/idle + otherwise agent/turn-stopping -> drain -> continue only for steering + 'turn/end' -> agent/settled start the next waking queued message, or emit agent/status(idle) idle inject: @@ -176,7 +176,7 @@ New behavior attaches to a documented extension point; a loop change updates thi | Add background work | register on `ctx.tasks`; generic `task_*` tools collect or stop it | | Add filesystem access or policy | implement a `ctx.fs` provider or listen to `fs/*` policy events | | Confine spawned processes | use a `ctx.sandbox` backend; consumers wrap argv before spawning | -| Intercept a request, tool, or turn | use its `agent/*` or `tools/*` event; `agent/stopping` is the stop boundary | +| Intercept a request, tool, or turn | use its `agent/*` or `tools/*` event; `agent/turn-stopping` is the stop boundary | | Add model-facing context | call `agent.inject()` to append a sourced `user/message` without a turn | | Add UI or editor integration | drive `ctx.agents`, render from `session/event`; terminal-only overlays use `ctx.tui` | | Add durable session state | extend `SessionEventMap`; render and replay from the log | diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index ce5cb31dd8..9760511e97 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -99,8 +99,8 @@ forever: drain accepted tool context and steering 'step/end' continue for tools or steering unless a result concluded the turn - otherwise agent/stopping -> drain -> continue only for steering - 'turn/end' -> agent/idle + otherwise agent/turn-stopping -> drain -> continue only for steering + 'turn/end' -> agent/settled start the next waking queued message, or emit agent/status(idle) idle inject: @@ -176,7 +176,7 @@ idle inject: | 添加后台工作 | 在 `ctx.tasks` 上注册;通用 `task_*` 工具负责收集或停止 | | 添加文件系统访问或策略 | 实现 `ctx.fs` 提供方,或监听 `fs/*` 策略事件 | | 限制生成的进程 | 使用 `ctx.sandbox` 后端;消费方在生成前包装 argv | -| 拦截请求、工具或轮次 | 使用相应的 `agent/*` 或 `tools/*` 事件;`agent/stopping` 是停止边界 | +| 拦截请求、工具或轮次 | 使用相应的 `agent/*` 或 `tools/*` 事件;`agent/turn-stopping` 是停止边界 | | 添加模型可见上下文 | 调用 `agent.inject()`,追加带来源的 `user/message`,但不创建轮次 | | 添加 UI 或编辑器集成 | 驱动 `ctx.agents`,从 `session/event` 渲染;仅终端浮层使用 `ctx.tui` | | 添加持久会话状态 | 扩展 `SessionEventMap`;从日志渲染和回放 | diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml index dc1c57ee93..2a8096f502 100644 --- a/docs/cookbook/extension-cookbook.i18n.yaml +++ b/docs/cookbook/extension-cookbook.i18n.yaml @@ -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 -extension-cookbook.md: 3ef9e6e7d7117d76f20da274a0f1d689497df441 -extension-cookbook.zh.md: 575c8d5e543fe578feb925cec41f6704dbc2797c +# pnpm run verify-translation-pairing --write docs/cookbook/extension-cookbook.md +extension-cookbook.md: 51a87be037ddbf6d031d3607da7b70470087334c +extension-cookbook.zh.md: 389ac87be0a1cd14a7646374909d79e6e00d8b56 diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md index 3ef9e6e7d7..51a87be037 100644 --- a/docs/cookbook/extension-cookbook.md +++ b/docs/cookbook/extension-cookbook.md @@ -100,7 +100,7 @@ Every product feature maps to a listener on a documented extension seam — the | Product feature | Plugin mechanism | |---|---| -| Hook system (user + project level) | listeners on `agent/session-start`, `agent/prompt-submit`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/stopping`; the waterfall seams return typed decisions, while `agent/stopping` may steer another step; the `dsh-hooks-claude` / `dsh-hooks-codex` bridges map hook config files onto these seams | +| Hook system (user + project level) | listeners on `agent/session-start`, `agent/prompt-submit`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/turn-stopping`; the waterfall seams return typed decisions, while `agent/turn-stopping` may steer another step; the `dsh-hooks-claude` / `dsh-hooks-codex` bridges map hook config files onto these seams | | `/goal` | `ctx.goals` owns durable state, `dsh-goal-session` schedules same-session rounds through the public `Agent`, and separate command/tool producers expose human/model control | | `/loop` | on the `turn/end` session event, `followup()` the next iteration; or force-continue | | Dynamic workflow | `ctx.workflows` + the worker-thread engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and the structured-output execution's monotonic `concludeTurn()` marker | diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md index 575c8d5e54..389ac87be0 100644 --- a/docs/cookbook/extension-cookbook.zh.md +++ b/docs/cookbook/extension-cookbook.zh.md @@ -100,7 +100,7 @@ export function apply(ctx: Context) { | 产品功能 | 插件机制 | |---|---| -| 钩子系统(用户级 + 项目级) | `agent/session-start`、`agent/prompt-submit`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/stopping` 上的监听器;waterfall seam 返回类型化决策,`agent/stopping` 则可通过 steering 触发下一步;`dsh-hooks-claude` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些 seam 上 | +| 钩子系统(用户级 + 项目级) | `agent/session-start`、`agent/prompt-submit`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/turn-stopping` 上的监听器;waterfall seam 返回类型化决策,`agent/turn-stopping` 则可通过 steering 触发下一步;`dsh-hooks-claude` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些 seam 上 | | `/goal` | `ctx.goals` 管理持久状态,`dsh-goal-session` 通过公共 `Agent` 调度同会话回合,独立的命令/工具生产方分别提供人类/模型控制 | | `/loop` | 在 `turn/end` 会话事件上 `followup()` 下一次迭代;或强制继续 | | 动态工作流 | `ctx.workflows` + worker-thread 引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的 prompt/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和结构化输出执行的单调 `concludeTurn()` 标记来强制输出 | diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index b89a22be83..69cfbf4cba 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -98,31 +98,6 @@ Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structu Source: [`packages/core/agent/src/types.ts:405`](../../packages/core/agent/src/types.ts) -### `agent/idle` — emit - -One drain chain reached its terminal turn: that turn's `turn/end` is already committed. Automatically recovered failed turns do not emit this notification, and neither does a run that aborts or fails before its `turn/start` commits — there is no durable turn to settle against. `reason` says why; model-request recovery is exhausted when an error reaches it. - -```ts cordis-catalog -/** - * One drain chain reached its terminal turn: that turn's `turn/end` is - * already committed. Automatically recovered failed turns do not emit this - * notification, and neither does a run that aborts or fails before its - * `turn/start` commits — there is no durable turn to settle against. - * `reason` says why; model-request recovery is exhausted when an error - * reaches it. - * @param agent - the agent whose turn closed. - * @param turn - the terminal turn number. - * @param reason - why the terminal turn ended, with live error facts when it failed. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode emit - */ -'agent/idle'(this: Scoped, agent: Agent, turn: number, reason: IdleReason): void -``` - -Types: [Agent](../core-data-structures/core.md) · [IdleReason](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) - -Source: [`packages/core/agent/src/types.ts:392`](../../packages/core/agent/src/types.ts) - ### `agent/inbox/dequeue` — emit The driver claimed one item out of the inbox: a queued item at a turn boundary, or steering drained between steps. Fires after the item leaves its FIFO and before it becomes a durable message. @@ -282,6 +257,31 @@ Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structu Source: [`packages/core/agent/src/types.ts:307`](../../packages/core/agent/src/types.ts) +### `agent/settled` — emit + +One drain chain reached its terminal turn: that turn's `turn/end` is already committed. Automatically recovered failed turns do not emit this notification, and neither does a run that aborts or fails before its `turn/start` commits — there is no durable turn to settle against. `reason` says why; model-request recovery is exhausted when an error reaches it. + +```ts cordis-catalog +/** + * One drain chain reached its terminal turn: that turn's `turn/end` is + * already committed. Automatically recovered failed turns do not emit this + * notification, and neither does a run that aborts or fails before its + * `turn/start` commits — there is no durable turn to settle against. + * `reason` says why; model-request recovery is exhausted when an error + * reaches it. + * @param agent - the agent whose turn closed. + * @param turn - the terminal turn number. + * @param reason - why the terminal turn ended, with live error facts when it failed. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode emit + */ +'agent/settled'(this: Scoped, agent: Agent, turn: number, reason: SettleReason): void +``` + +Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SettleReason](../core-data-structures/core.md) + +Source: [`packages/core/agent/src/types.ts:392`](../../packages/core/agent/src/types.ts) + ### `agent/status` — emit Agent status changed (`idle` ⇄ `running`). `send()` does not enter `running` synchronously; drive lifecycle from this event. @@ -326,7 +326,7 @@ Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structu Source: [`packages/core/agent/src/types.ts:335`](../../packages/core/agent/src/types.ts) -### `agent/stopping` — serial +### `agent/turn-stopping` — serial The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (`agent.steer(...)`) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying `concludesTurn` ends the turn at its step. @@ -345,7 +345,7 @@ The turn is about to close: the model owes no response (no live tool calls, no f * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode serial */ -'agent/stopping'(this: Scoped, agent: Agent, turn: number, signal: AbortSignal): Promise | void +'agent/turn-stopping'(this: Scoped, agent: Agent, turn: number, signal: AbortSignal): Promise | void ``` Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) diff --git a/docs/core-data-structures/core.i18n.yaml b/docs/core-data-structures/core.i18n.yaml index 72791f0bde..b2526a0856 100644 --- a/docs/core-data-structures/core.i18n.yaml +++ b/docs/core-data-structures/core.i18n.yaml @@ -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 docs/core-data-structures/core.md -core.md: 8dcc08b0ca4524c8cc18c8585ce07701de17cc93 -core.zh.md: ec2c2462446d1df8f286b7bacebff394c5e80638 +core.md: bc4288872a9b1e47a82c86b095c13ae9defc957b +core.zh.md: abd1b7bd0aa5563e86cf652db354ad30e4fb74cc diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 8dcc08b0ca..bc4288872a 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -615,7 +615,7 @@ type PromptDecision = type RequestError = Error & { code?: string } ``` -`agent/step` is the single serial boundary before request derivation. `agent/stopping` runs when a turn has no tool or steering continuation, before one final steering drain. +`agent/step` is the single serial boundary before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain. `agent/session-start` carries a `SessionStartSource` (why the session lifecycle began; a bridge keys its SessionStart matcher on it): diff --git a/docs/core-data-structures/core.zh.md b/docs/core-data-structures/core.zh.md index ec2c246244..abd1b7bd0a 100644 --- a/docs/core-data-structures/core.zh.md +++ b/docs/core-data-structures/core.zh.md @@ -623,7 +623,7 @@ type PromptDecision = type RequestError = Error & { code?: string } ``` -`agent/step` 是请求推导前唯一的串行边界。`agent/stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。 +`agent/step` 是请求推导前唯一的串行边界。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。 `agent/session-start` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart): diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 0c86b3bdcc..3002e0b101 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -12,7 +12,6 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent/created` | `emit` | [`packages/core/agent/src/types.ts:236`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | | `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:245`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | | `agent/error` | `emit` | [`packages/core/agent/src/types.ts:405`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | -| `agent/idle` | `emit` | [`packages/core/agent/src/types.ts:392`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | | `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:272`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`tui`](../packages/ui/tui) | | `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:284`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`tui`](../packages/ui/tui) | | `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:262`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`goal-session`](../packages/goal/goal-session) | @@ -20,9 +19,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:348`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) | | `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:363`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | | `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:307`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-context`](../packages/context/workspace-context) | +| `agent/settled` | `emit` | [`packages/core/agent/src/types.ts:392`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | | `agent/status` | `emit` | [`packages/core/agent/src/types.ts:254`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | | `agent/step` | `serial` | [`packages/core/agent/src/types.ts:335`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`plan-mode`](../packages/plan/plan-mode), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`time-context`](../packages/context/time-context), [`tool-skill`](../packages/skill/tool-skill), [`user-approval`](../packages/ui/user-approval), [`workspace-context`](../packages/context/workspace-context) | -| `agent/stopping` | `serial` | [`packages/core/agent/src/types.ts:378`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/types.ts:378`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | | `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:30`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp) | | `commands/change` | `emit` | [`packages/ui/commands/src/index.ts:103`](../packages/ui/commands/src/index.ts) | [`commands`](../packages/ui/commands) (`events.dispatch`) | `apiproxy`, [`tui`](../packages/ui/tui) | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `apiproxy`, [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace) | diff --git a/packages/compact/compact-basic/src/index.ts b/packages/compact/compact-basic/src/index.ts index b4c3b74d30..6ac971c37a 100644 --- a/packages/compact/compact-basic/src/index.ts +++ b/packages/compact/compact-basic/src/index.ts @@ -155,7 +155,7 @@ export class BasicCompactService extends CompactService { } }) - ctx.on('agent/idle', (agent) => { + ctx.on('agent/settled', (agent) => { this.overflowRetries.delete(agent) }) diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 3ea9e87579..254a21e1a4 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -999,13 +999,6 @@ export const EVENT_API: readonly EventApiEntry[] = [ jsDoc: '/**\n * A step or turn errored. The machine reports a failure here (plus the\n * logger) even when the error has no in-turn position for a durable record.\n * @param agent - the agent whose turn errored.\n * @param turn - the turn in which the failure surfaced.\n * @param step - the step at which the failure surfaced.\n * @param error - the failure, verbatim.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */', summary: 'A step or turn errored.', }, - { - name: 'agent/idle', - mode: 'emit', - signature: '\'agent/idle\'(this: Scoped, agent: Agent, turn: number, reason: IdleReason): void', - jsDoc: '/**\n * One drain chain reached its terminal turn: that turn\'s `turn/end` is\n * already committed. Automatically recovered failed turns do not emit this\n * notification, and neither does a run that aborts or fails before its\n * `turn/start` commits — there is no durable turn to settle against.\n * `reason` says why; model-request recovery is exhausted when an error\n * reaches it.\n * @param agent - the agent whose turn closed.\n * @param turn - the terminal turn number.\n * @param reason - why the terminal turn ended, with live error facts when it failed.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */', - summary: 'One drain chain reached its terminal turn: that turn\'s `turn/end` is already committed.', - }, { name: 'agent/inbox/dequeue', mode: 'emit', @@ -1055,6 +1048,13 @@ export const EVENT_API: readonly EventApiEntry[] = [ jsDoc: '/**\n * The session lifecycle began, once before the first turn. Use\n * `agent.inject()` to seed model-facing context. This is a notification, not\n * a veto; disposal requested by a lifecycle owner is rechecked before the\n * driver starts.\n * @param agent - the agent whose session lifecycle began.\n * @param source - why the session started (fresh startup, resume, …).\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */', summary: 'The session lifecycle began, once before the first turn.', }, + { + name: 'agent/settled', + mode: 'emit', + signature: '\'agent/settled\'(this: Scoped, agent: Agent, turn: number, reason: SettleReason): void', + jsDoc: '/**\n * One drain chain reached its terminal turn: that turn\'s `turn/end` is\n * already committed. Automatically recovered failed turns do not emit this\n * notification, and neither does a run that aborts or fails before its\n * `turn/start` commits — there is no durable turn to settle against.\n * `reason` says why; model-request recovery is exhausted when an error\n * reaches it.\n * @param agent - the agent whose turn closed.\n * @param turn - the terminal turn number.\n * @param reason - why the terminal turn ended, with live error facts when it failed.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */', + summary: 'One drain chain reached its terminal turn: that turn\'s `turn/end` is already committed.', + }, { name: 'agent/status', mode: 'emit', @@ -1070,9 +1070,9 @@ export const EVENT_API: readonly EventApiEntry[] = [ summary: 'Awaited serial checkpoint before EVERY request of a turn is built (the first as well as each post-tools continuation).', }, { - name: 'agent/stopping', + name: 'agent/turn-stopping', mode: 'serial', - signature: '\'agent/stopping\'(this: Scoped, agent: Agent, turn: number, signal: AbortSignal): Promise | void', + signature: '\'agent/turn-stopping\'(this: Scoped, agent: Agent, turn: number, signal: AbortSignal): Promise | void', jsDoc: '/**\n * The turn is about to close: the model owes no response (no live tool\n * calls, no fresh steering). Awaited before the boundary commits — a\n * listener that objects steers (`agent.steer(...)`) and the machine\n * re-reads its inbox: fresh steering runs another step, none closes the\n * turn. Data decides, so listener order cannot change the outcome. The\n * inverse control (stop a tool loop early) is data too: a tool result\n * carrying `concludesTurn` ends the turn at its step.\n * @param agent - the agent whose turn is at its stop boundary.\n * @param turn - the turn about to close.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode serial\n */', summary: 'The turn is about to close: the model owes no response (no live tool calls, no fresh steering).', }, diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 3af86b15e9..a9fe695dc8 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -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 packages/core/agent-loop/README.md -README.md: 0edb9bf3fb73fd1d1e7c5000770eca87d4f58fcd -README.zh.md: 5d0ebc605e6c5ce29786b7113adaab3d5671b92e +README.md: bdf1367446bb9345d56f0a44e65e43467dd82144 +README.zh.md: 3540499bfc64fc465853bc26b9a9c7f3cd1df15e diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 0edb9bf3fb..bdf1367446 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -128,4 +128,4 @@ Append-only; each synthetic result follows the reusable request prefix and does - **Classification is unary** — calls whose safety depends on comparing siblings or resources must remain exclusive ([rationale](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)). - **Config labels are fresh by default** — omitting `sessionId` creates a fresh `${id}-session-` on every startup; exact resume-or-create behavior requires an explicit stable `sessionId`, while `resumeSessionId` requires existing persisted history. - **Config agents have no per-agent persona field or setup hook** — they use the deployment persona; scoped persona/tool composition is available only through the programmatic `ctx.agents.create()` / `resume()` factory options. -- **No built-in turn budget** — tool calls or steering continue the current turn; a policy that bounds runaway turns must cancel from an existing lifecycle seam such as `agent/stopping`. +- **No built-in turn budget** — tool calls or steering continue the current turn; a policy that bounds runaway turns must cancel from an existing lifecycle seam such as `agent/turn-stopping`. diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index 5d0ebc605e..3540499bfc 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -128,4 +128,4 @@ interface Config { - **分类是一元的**:安全性取决于比较同级调用或资源的调用必须保持独占(参见[设计原理](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md))。 - **配置 label 默认每次新建**:省略 `sessionId` 会在每次启动时创建全新的 `${id}-session-`;确切的恢复或创建行为要求显式提供稳定的 `sessionId`,而 `resumeSessionId` 要求已有持久化历史。 - **配置 agent 没有逐 agent persona 字段或 setup 钩子**:它们使用部署 persona;只有编程式 `ctx.agents.create()` / `resume()` 工厂选项支持带作用域的 persona/工具组合。 -- **没有内置轮次预算**:工具调用或 steering 会让当前轮次继续;限制失控轮次的策略必须从既有生命周期 seam(如 `agent/stopping`)执行取消。 +- **没有内置轮次预算**:工具调用或 steering 会让当前轮次继续;限制失控轮次的策略必须从既有生命周期 seam(如 `agent/turn-stopping`)执行取消。 diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 32bb940d03..ba5ec3dae9 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -19,7 +19,7 @@ import type { AgentInterruptReason, AgentOptions, AgentStatus, - IdleReason, + SettleReason, PromptDecision, RequestError, SendOptions, @@ -288,7 +288,7 @@ export class ReactLoopAgent implements Agent { let step = 0 let opened = false let reason: TurnEndReason = { kind: 'completed' } - let idle: IdleReason = { kind: 'completed' } + let settleReason: SettleReason = { kind: 'completed' } let retry = false const cancelRetry = (): void => { retry = false } signal.addEventListener('abort', cancelRetry, { once: true }) @@ -316,7 +316,7 @@ export class ReactLoopAgent implements Agent { if (outcome.maxTokens) reason = { kind: 'max-tokens' } // A concluding tool result is terminal: steering already in the // log waits for the next turn's request instead of reopening this - // one, and the agent/stopping drain below is skipped for the same + // one, and the agent/turn-stopping drain below is skipped for the same // reason. if (outcome.concluded) break steps if (outcome.continueTurn || this.outbox.some(item => 'id' in item)) continue @@ -354,14 +354,14 @@ export class ReactLoopAgent implements Agent { } const settlement = this.settle(turn, step, outcome.error, signal, outcome.failure) reason = settlement.reason - idle = settlement.idle + settleReason = settlement.settleReason break steps } /* v8 ignore next 2 -- closed-union exhaustiveness guard */ default: assertNever(outcome) } - await this.loopCtx.serial(agentCarrier(this), 'agent/stopping', this, turn, signal) + await this.loopCtx.serial(agentCarrier(this), 'agent/turn-stopping', this, turn, signal) signal.throwIfAborted() if (!this.drainOutbox(turn)) break } @@ -379,7 +379,7 @@ export class ReactLoopAgent implements Agent { this.loopCtx.logger.warn(`agent "${this.id}": closing step ${turn}/${step} failed: ${errorChain(closeError)}`) emitAgentEvent(this.loopCtx, this, 'agent/error', turn, step, closeError) } - ({ reason, idle } = this.settle(turn, step, caught, signal)) + ({ reason, settleReason } = this.settle(turn, step, caught, signal)) } finally { // Every step-close happens before this point on both success and // failure paths (step(), the request-failed branch, the catch), so the @@ -406,10 +406,10 @@ export class ReactLoopAgent implements Agent { if (retry) { await this.run({ kind: 'retry' }) } else { - // agent/idle names only committed turns: a run aborted or rejected + // agent/settled names only committed turns: a run aborted or rejected // before turn/start has no durable turn/end for consumers to settle // against, so it exits without the notification. - if (opened) emitAgentEvent(this.loopCtx, this, 'agent/idle', turn, idle) + if (opened) emitAgentEvent(this.loopCtx, this, 'agent/settled', turn, settleReason) this.continueOrIdle() } } @@ -615,7 +615,7 @@ export class ReactLoopAgent implements Agent { /** * The single settlement funnel: classify one turn failure (interruption - * beats error) into the durable turn/end reason and the live idle report. + * beats error) into the durable turn/end reason and live settlement report. */ private settle( turn: number, @@ -623,13 +623,16 @@ export class ReactLoopAgent implements Agent { error: unknown, signal: AbortSignal, failure?: LlmFailure, - ): { reason: TurnEndReason; idle: IdleReason } { + ): { reason: TurnEndReason; settleReason: SettleReason } { if (signal.aborted) { // Slot invariant, stated rather than re-validated: the turn controller // is machine-private and cancel() is its only aborter, always with one // frozen canonical cause as the reason. const interrupt = signal.reason as AgentInterruptReason - return { reason: { kind: interrupt.kind === 'disposed' ? 'disposed' : 'aborted' }, idle: { kind: 'aborted' } } + return { + reason: { kind: interrupt.kind === 'disposed' ? 'disposed' : 'aborted' }, + settleReason: { kind: 'aborted' }, + } } if (failure !== undefined) { emitAgentEvent(this.loopCtx, this, 'agent/error', turn, step, error) @@ -639,13 +642,13 @@ export class ReactLoopAgent implements Agent { const rendered = errorChain(error) return { reason: { kind: 'error', step, failure: { ...failure, ...rendered === '' ? {} : { message: rendered } } }, - idle: { kind: 'error', error, failure }, + settleReason: { kind: 'error', error, failure }, } } emitAgentEvent(this.loopCtx, this, 'agent/error', turn, step, error) return { reason: { kind: 'error', step, message: errorChain(error), ...isHarnessError(error) ? { code: error.code } : {} }, - idle: { kind: 'error', error }, + settleReason: { kind: 'error', error }, } } diff --git a/packages/core/agent-loop/tests/agent-initiator.spec.ts b/packages/core/agent-loop/tests/agent-initiator.spec.ts index 4d45203b3f..e99971aa95 100644 --- a/packages/core/agent-loop/tests/agent-initiator.spec.ts +++ b/packages/core/agent-loop/tests/agent-initiator.spec.ts @@ -178,7 +178,7 @@ describe('AgentLoop initiator scope', () => { if (subject === agent) capture(signal) return next() }) - ctx.on('agent/stopping', (subject, _turn, signal) => { + ctx.on('agent/turn-stopping', (subject, _turn, signal) => { if (subject === agent) capture(signal) }) ctx.tools.register(defineContentToolFixture({ diff --git a/packages/core/agent-loop/tests/cancel.spec.ts b/packages/core/agent-loop/tests/cancel.spec.ts index d0d11ed856..94d3db0e55 100644 --- a/packages/core/agent-loop/tests/cancel.spec.ts +++ b/packages/core/agent-loop/tests/cancel.spec.ts @@ -482,7 +482,7 @@ describe('Agent.cancel()', () => { }) let cancelled = false - ctx.on('agent/stopping', (subject) => { + ctx.on('agent/turn-stopping', (subject) => { if (subject === agent && !cancelled) { cancelled = true agent.cancel({ kind: 'user' }) @@ -719,7 +719,7 @@ describe('Agent.cancel()', () => { }) break case 'stopping': - ctx.on('agent/stopping', async (subject, _turn, signal) => { + ctx.on('agent/turn-stopping', async (subject, _turn, signal) => { if (subject === agent) await blockUntilAbort(signal) }) break diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts index a43d08170a..a31ac3369c 100644 --- a/packages/core/agent-loop/tests/contract-regressions.spec.ts +++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts @@ -270,7 +270,7 @@ describe('abort during tool execution ends the turn', () => { }) describe('steering from late extension points is never stranded', () => { - it('steer() from an agent/stopping listener continues the same turn', async () => { + it('steer() from an agent/turn-stopping listener continues the same turn', async () => { const adapter = new MockAdapter([ textResponse('no tools, would stop here'), textResponse('continued because of steering'), @@ -279,7 +279,7 @@ describe('steering from late extension points is never stranded', () => { const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) let steeredOnce = false - ctx.on('agent/stopping', () => { + ctx.on('agent/turn-stopping', () => { if (!steeredOnce) { steeredOnce = true agent.steer({ content: [{ type: 'text', text: 'one more thing' }], source: { kind: 'user' } }) @@ -355,13 +355,13 @@ describe('steering from late extension points is never stranded', () => { }) describe('plugin exceptions are contained', () => { - it('a throwing agent/stopping listener ends the turn with an error, loop survives', async () => { + it('a throwing agent/turn-stopping listener ends the turn with an error, loop survives', async () => { const adapter = new MockAdapter([textResponse('one'), textResponse('two')]) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) let threwOnce = false - ctx.on('agent/stopping', async () => { + ctx.on('agent/turn-stopping', async () => { if (!threwOnce) { threwOnce = true throw new Error('broken continuation plugin') diff --git a/packages/core/agent-loop/tests/coverage-edges.spec.ts b/packages/core/agent-loop/tests/coverage-edges.spec.ts index 5bd855a27b..99dcbd1d41 100644 --- a/packages/core/agent-loop/tests/coverage-edges.spec.ts +++ b/packages/core/agent-loop/tests/coverage-edges.spec.ts @@ -348,17 +348,18 @@ describe('stream failure edges', () => { }) describe('post-turn continuation edges', () => { - it('an agent/idle listener that enqueues a waking prompt preempts continueOrIdle', async () => { + it('an agent/settled listener that enqueues a waking prompt preempts continueOrIdle', async () => { const adapter = new MockAdapter([textResponse('one'), textResponse('two')]) const ctx = await harness(adapter) - const agent = ctx.agentLoop.create(SessionId('idle-preempt'), { provider: 'mock', model: 'mock' }) + const agent = ctx.agentLoop.create(SessionId('settled-preempt'), { provider: 'mock', model: 'mock' }) let injected = false - ctx.on('agent/idle', (subject) => { + ctx.on('agent/settled', (subject) => { if (subject !== agent || injected) return + expect(subject.status).toBe('running') injected = true // kick() installs the next admission synchronously, so the following // continueOrIdle() sees an abort owner and yields to it. - send(agent, 'follow-up from idle listener') + send(agent, 'follow-up from settled listener') }) send(agent, 'go') @@ -531,8 +532,8 @@ describe('driver bookkeeping edges', () => { // (the run's containment covers only session appends); the waiter's // catch arm must treat that rejection as quiescence instead of // propagating it. - ctx.on('agent/idle', (subject) => { - if (subject === agent) throw new Error('idle listener exploded') + ctx.on('agent/settled', (subject) => { + if (subject === agent) throw new Error('settled listener exploded') }) send(agent, 'one') diff --git a/packages/core/agent-loop/tests/interception.spec.ts b/packages/core/agent-loop/tests/interception.spec.ts index dd2eb53b05..abf512ab3c 100644 --- a/packages/core/agent-loop/tests/interception.spec.ts +++ b/packages/core/agent-loop/tests/interception.spec.ts @@ -11,7 +11,7 @@ import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' /** * The interception seams introduced by the hooks taxonomy: `agent/prompt-submit`, - * `agent/session-start`, `agent/stopping`, and the + * `agent/session-start`, `agent/turn-stopping`, and the * `tools/pre-execute` / `tools/post-execute` * split with `additionalContexts` buffering. These verify the canonical event * surface a hook bridge (or a native plugin) programs against, WITHOUT any diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index 0ea9d6b7a3..26d6297acb 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -463,7 +463,7 @@ describe('agent loop', () => { expect(agent.session.events.some(event => event.type === 'user/message' && event.data.source.kind === 'plugin')).toBe(false) }) - it('agent/stopping can steer another step (/loop pattern)', async () => { + it('agent/turn-stopping can steer another step (/loop pattern)', async () => { const adapter = new MockAdapter([ textResponse('step 1'), textResponse('step 2'), @@ -474,7 +474,7 @@ describe('agent loop', () => { let steps = 0 ctx.on('session/event', (_session, event) => { if (event.type === 'step/end') steps++ }) - ctx.on('agent/stopping', (subject) => { + ctx.on('agent/turn-stopping', (subject) => { if (steps < 3) { subject.steer({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'plugin', plugin: 'loop-test' } }) } @@ -722,7 +722,7 @@ describe('agent loop', () => { ctx.on('session/event', (_session, event) => { if (event.type === 'step/end') steps++ }) // Force exactly one continuation (step 1 → step 2), then defer to default // (step 2 is a plain stop with no tool calls → stops). - ctx.on('agent/stopping', (subject) => { + ctx.on('agent/turn-stopping', (subject) => { if (steps < 2) { subject.steer({ content: [{ type: 'text', text: 'continue after truncation' }], source: { kind: 'plugin', plugin: 'max-tokens-test' } }) } diff --git a/packages/core/agent-loop/tests/request-error.spec.ts b/packages/core/agent-loop/tests/request-error.spec.ts index ac95a261e9..376722dbf2 100644 --- a/packages/core/agent-loop/tests/request-error.spec.ts +++ b/packages/core/agent-loop/tests/request-error.spec.ts @@ -57,12 +57,12 @@ describe('agent/request-error', () => { const agent = ctx.agentLoop.create(SessionId('request-error-retry'), { provider: 'mock', model: 'mock' }) const seen: { turn: number; step: number; failure: LlmFailure }[] = [] const statuses: string[] = [] - const idleTurns: number[] = [] + const settledTurns: number[] = [] ctx.on('agent/status', (subject, status) => { if (subject === agent) statuses.push(status) }) - ctx.on('agent/idle', (subject, turn) => { - if (subject === agent) idleTurns.push(turn) + ctx.on('agent/settled', (subject, turn) => { + if (subject === agent) settledTurns.push(turn) }) ctx.on('agent/request-error', async (subject, turn, step, _error, failure) => { expect(subject).toBe(agent) @@ -101,7 +101,7 @@ describe('agent/request-error', () => { { kind: 'retry' }, ]) expect(statuses).toEqual(['running', 'idle']) - expect(idleTurns).toEqual([3]) + expect(settledTurns).toEqual([3]) }) it('lets cancellation win over a retry request', async () => { diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index 1f132514c6..b5fafcbb7d 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -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 packages/core/agent/README.md -README.md: 1a16c8ad5a8b7d6e06d4ac36a7b9978fda64e7b5 -README.zh.md: 4e03816707ba6a3a707697db1e6d8b117aa87a6a +README.md: d3aced3c680f2d4ae8df9c3f2bf83ae01435d05b +README.zh.md: e2c57ba50162deeac502d4f8eff23f7f2f7dab71 diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 1a16c8ad5a..d3aced3c68 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -48,7 +48,7 @@ Agent *creation* is provided by the plugin implementing `AgentFactory` (`dsh-age The lifecycle edges have two important local caveats. `agent/created` runs after scoped setup and after both session and agent registry entries exist. Setup is trusted composition-only code; the immediately following non-vetoing `agent/session-start` notification is the first supported startup injection point. `agent/disposed` always means the exact agent has left the registry. AgentLoop emits it after its driver is quiescent, while ordered teardown may still be detaching the session and unwinding the scope; custom agents registered directly own any stronger driver-ordering contract themselves. -Most interception points are cooperative waterfalls. Turn-scoped asynchronous seams receive one explicit `AbortSignal`, with `signal` immediately before a waterfall's final `next`; listeners may cooperate but must not retain it as authority over another turn. `agent/step` is the serial checkpoint before request derivation, while `agent/request-error` is the failed-model-request recovery waterfall: it receives the exact error, normalized failure facts, and signal after the failed step closes. A listener calls `agent.retry()` and returns without `next()` when it owns recovery; the loop closes the failed turn and opens one numbered retry turn. `agent/stopping` runs before an otherwise completed turn closes. Ordinary queued prompts remain intact. Effective broad cancellation first emits the observe-only `agent/cancel-requested` with its resolved typed cause, then clears queues and aborts; notification failures are contained and cannot veto the stop. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) owns signal lifetime; the [agent-scope runtime-design Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way) owns scoped dispatch and terminal settlement. +Most interception points are cooperative waterfalls. Turn-scoped asynchronous seams receive one explicit `AbortSignal`, with `signal` immediately before a waterfall's final `next`; listeners may cooperate but must not retain it as authority over another turn. `agent/step` is the serial checkpoint before request derivation, while `agent/request-error` is the failed-model-request recovery waterfall: it receives the exact error, normalized failure facts, and signal after the failed step closes. A listener calls `agent.retry()` and returns without `next()` when it owns recovery; the loop closes the failed turn and opens one numbered retry turn. `agent/turn-stopping` runs before an otherwise completed turn closes. Ordinary queued prompts remain intact. Effective broad cancellation first emits the observe-only `agent/cancel-requested` with its resolved typed cause, then clears queues and aborts; notification failures are contained and cannot veto the stop. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) owns signal lifetime; the [agent-scope runtime-design Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way) owns scoped dispatch and terminal settlement. `PromptDecision.additionalContexts` is an array so every context keeps its own source. Allowed prompt content and every additional context become separate model-facing `user/message` events before the turn runs. A listener that wraps a downstream allow preserves its `content` and `additionalContexts` unless it intentionally replaces either field; the returned allow is authoritative. diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index 4e03816707..e2c57ba501 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -48,7 +48,7 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供, 生命周期边有两个重要的本地注意事项。`agent/created` 在作用域 setup 之后、会话与 agent 注册表条目都存在之后运行。Setup 是受信任、仅用于组合的代码;紧随其后且不可 veto 的 `agent/session-start` 通知是第一个受支持的启动注入点。`agent/disposed` 始终表示确切 agent 已离开注册表。AgentLoop 在其驱动器静默后发出该事件,而有序 teardown 此时可能仍在分离会话并撤销作用域;直接注册的自定义 agent 自行拥有任何更强的驱动器顺序契约。 -大多数拦截点都是协作式 waterfall。轮次作用域的异步 seam 接收一个显式 `AbortSignal`,其中 `signal` 紧邻 waterfall 最终的 `next`;监听器可以配合,但不得将它保留为控制另一轮次的权限。`agent/step` 是派生请求前的串行检查点,而 `agent/request-error` 是失败模型请求的恢复 waterfall:失败步骤关闭后,它接收确切错误、规范化失败事实和信号。拥有恢复权的监听器调用 `agent.retry()` 并且不调用 `next()` 就返回;循环会关闭失败轮次,并打开一个编号重试轮次。`agent/stopping` 在本可完成的轮次关闭前运行。普通排队提示词保持原样。有效的广义取消会先发出只观测的 `agent/cancel-requested` 及其解析后的类型化原因,再清空队列并中止;通知失败会被收容,不能 veto 停止。信号生命周期由[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)拥有;作用域分发与终止结算由 [agent 作用域 runtime 设计 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way)拥有。 +大多数拦截点都是协作式 waterfall。轮次作用域的异步 seam 接收一个显式 `AbortSignal`,其中 `signal` 紧邻 waterfall 最终的 `next`;监听器可以配合,但不得将它保留为控制另一轮次的权限。`agent/step` 是派生请求前的串行检查点,而 `agent/request-error` 是失败模型请求的恢复 waterfall:失败步骤关闭后,它接收确切错误、规范化失败事实和信号。拥有恢复权的监听器调用 `agent.retry()` 并且不调用 `next()` 就返回;循环会关闭失败轮次,并打开一个编号重试轮次。`agent/turn-stopping` 在本可完成的轮次关闭前运行。普通排队提示词保持原样。有效的广义取消会先发出只观测的 `agent/cancel-requested` 及其解析后的类型化原因,再清空队列并中止;通知失败会被收容,不能 veto 停止。信号生命周期由[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)拥有;作用域分发与终止结算由 [agent 作用域 runtime 设计 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way)拥有。 `PromptDecision.additionalContexts` 是数组,因此每个上下文都保留自己的来源。获准的提示词内容与每个附加上下文都会在轮次运行前成为各自独立、面向模型的 `user/message` 事件。包装下游允许决策的监听器会保留其 `content` 与 `additionalContexts`,除非有意替换任一字段;返回的允许决策是权威来源。 diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts index dcef6c57ed..ac2350dafe 100644 --- a/packages/core/agent/src/types.ts +++ b/packages/core/agent/src/types.ts @@ -113,11 +113,11 @@ export type PromptDecision = export type RequestError = Error & { code?: string } /** - * Why a turn ended, reported live on `agent/idle` right after the turn's + * Why a turn ended, reported live on `agent/settled` right after the turn's * durable `turn/end`. `error` carries the thrown value verbatim for observers; * model-request recovery runs earlier through `agent/request-error`. */ -export type IdleReason = +export type SettleReason = | { kind: 'completed' } | { kind: 'aborted' } | { kind: 'error'; error: unknown; failure?: LlmFailure } @@ -375,7 +375,7 @@ declare module 'cordis' { * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode serial */ - 'agent/stopping'(this: Scoped, agent: Agent, turn: number, signal: AbortSignal): Promise | void + 'agent/turn-stopping'(this: Scoped, agent: Agent, turn: number, signal: AbortSignal): Promise | void /** * One drain chain reached its terminal turn: that turn's `turn/end` is * already committed. Automatically recovered failed turns do not emit this @@ -389,7 +389,7 @@ declare module 'cordis' { * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode emit */ - 'agent/idle'(this: Scoped, agent: Agent, turn: number, reason: IdleReason): void + 'agent/settled'(this: Scoped, agent: Agent, turn: number, reason: SettleReason): void // ---- error notifications (emit) ---- /** diff --git a/packages/core/scope/src/scoped-events.generated.ts b/packages/core/scope/src/scoped-events.generated.ts index 0302297b51..89515bf3c2 100644 --- a/packages/core/scope/src/scoped-events.generated.ts +++ b/packages/core/scope/src/scoped-events.generated.ts @@ -12,7 +12,6 @@ const scopedSubjectResolvers: Readonly args[0], 'agent/disposed': args => args[0], 'agent/error': args => args[0], - 'agent/idle': args => args[0], 'agent/inbox/dequeue': args => args[0], 'agent/inbox/discard': args => args[0], 'agent/inbox/enqueue': args => args[0], @@ -20,9 +19,10 @@ const scopedSubjectResolvers: Readonly args[0], 'agent/request-error': args => args[0], 'agent/session-start': args => args[0], + 'agent/settled': args => args[0], 'agent/status': args => args[0], 'agent/step': args => args[0], - 'agent/stopping': args => args[0], + 'agent/turn-stopping': args => args[0], 'approval/request': args => (args[0] as Record)['agent'], 'goal/changed': args => args[0], 'session/created': null, diff --git a/packages/core/scope/tests/invariant.spec.ts b/packages/core/scope/tests/invariant.spec.ts index fcafcaa129..e0acb2809c 100644 --- a/packages/core/scope/tests/invariant.spec.ts +++ b/packages/core/scope/tests/invariant.spec.ts @@ -58,8 +58,8 @@ describe('scoped-dispatch invariants', () => { signal, () => Promise.resolve(), ], - 'agent/stopping': [agent, 1, signal], - 'agent/idle': [agent, 1, { kind: 'completed' }], + 'agent/turn-stopping': [agent, 1, signal], + 'agent/settled': [agent, 1, { kind: 'completed' }], 'agent/error': [agent, 1, 0, new Error('x')], } satisfies { [K in AgentEventName]: EventArgs } const rows: Array<[string, unknown[]]> = [ diff --git a/packages/hooks/hooks-claude/README.i18n.yaml b/packages/hooks/hooks-claude/README.i18n.yaml index 820eba3854..c4d7c1bdc9 100644 --- a/packages/hooks/hooks-claude/README.i18n.yaml +++ b/packages/hooks/hooks-claude/README.i18n.yaml @@ -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 -README.md: b93907649ee345e666abcb5c773969ae1abb347f -README.zh.md: 8c1cbbdff518a6030233667c227e6f0acadd76d4 +# pnpm run verify-translation-pairing --write packages/hooks/hooks-claude/README.md +README.md: 24259c24ea35cd450f8ea27ca2cca423ed4406bd +README.zh.md: 9f58b782190721de08750e5bd4eac9e5effd5c6a diff --git a/packages/hooks/hooks-claude/README.md b/packages/hooks/hooks-claude/README.md index b93907649e..24259c24ea 100644 --- a/packages/hooks/hooks-claude/README.md +++ b/packages/hooks/hooks-claude/README.md @@ -40,7 +40,7 @@ The hooks **themselves** run in the agent's session workspace: for the agent-sco | `UserPromptSubmit` | `agent/prompt-submit` (waterfall) | `deny` → `PromptDecision.block`; additionalContext-only → delegate via `next()` then prepend a separately sourced context to downstream `additionalContexts` (a later listener can still block/rewrite) | | `PreToolUse` | `tools/pre-execute` (waterfall) | `deny` → `PreToolDecision.deny`; `ask` → `PreToolDecision.ask` | | `PostToolUse` | `tools/post-execute` (waterfall) | `deny` → `block` with feedback; additionalContext-only → delegate via `next()` then prepend a separately sourced context to the downstream decision; Code Mode defers sub-call contexts until the outer `run_code` result | -| `Stop` | `agent/stopping` (serial) | a blocking Stop hook feeds its reason through `steer()`, forcing another step | +| `Stop` | `agent/turn-stopping` (serial) | a blocking Stop hook feeds its reason through `steer()`, forcing another step | | `SubagentStart` | `subagent/start` (emit) | additionalContext → `agent.inject()` into a live in-process child; a remote child has no local injection target | | `SubagentStop` | `subagent/end` (emit) | observe-only | diff --git a/packages/hooks/hooks-claude/README.zh.md b/packages/hooks/hooks-claude/README.zh.md index 8c1cbbdff5..9f58b78219 100644 --- a/packages/hooks/hooks-claude/README.zh.md +++ b/packages/hooks/hooks-claude/README.zh.md @@ -40,7 +40,7 @@ hook **本身** 会在 agent 的会话工作区中运行:对 agent scope 点 | `UserPromptSubmit` | `agent/prompt-submit`(waterfall) | `deny` → `PromptDecision.block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游 `additionalContexts`(后续 listener 仍可阻塞/改写) | | `PreToolUse` | `tools/pre-execute`(waterfall) | `deny` → `PreToolDecision.deny`;`ask` → `PreToolDecision.ask` | | `PostToolUse` | `tools/post-execute`(waterfall) | `deny` → 带反馈的 `block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游决策;Code Mode 将子调用上下文延迟到外层 `run_code` 结果 | -| `Stop` | `agent/stopping`(serial) | 阻塞 Stop hook 通过 `steer()` 送入其原因,强制再执行一步 | +| `Stop` | `agent/turn-stopping`(serial) | 阻塞 Stop hook 通过 `steer()` 送入其原因,强制再执行一步 | | `SubagentStart` | `subagent/start`(emit) | additionalContext → `agent.inject()` 到实时同进程 child;远程 child 没有本地注入目标 | | `SubagentStop` | `subagent/end`(emit) | 只观测 | diff --git a/packages/hooks/hooks-claude/src/index.ts b/packages/hooks/hooks-claude/src/index.ts index 3e3370f8d8..2dc3b8996b 100644 --- a/packages/hooks/hooks-claude/src/index.ts +++ b/packages/hooks/hooks-claude/src/index.ts @@ -262,7 +262,7 @@ export function apply(ctx: Context, config: Config): void { // A blocking Stop hook steers at the stopping boundary, which makes the // machine observe pending input and run another step. // TODO(stop-loop-guard): cap consecutive forced continuations; hooks must self-limit meanwhile. - ctx.on('agent/stopping', async (agent, turn, signal): Promise => { + ctx.on('agent/turn-stopping', async (agent, turn, signal): Promise => { const merged = await runPoint('Stop', '', stopPayload(ctx, agent), { agent, turn, signal }) if (merged.decision === 'deny') { // A blocking Stop hook forces continuation. diff --git a/packages/hooks/hooks-codex/README.i18n.yaml b/packages/hooks/hooks-codex/README.i18n.yaml index 55eec27dec..bb102c814b 100644 --- a/packages/hooks/hooks-codex/README.i18n.yaml +++ b/packages/hooks/hooks-codex/README.i18n.yaml @@ -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 -README.md: 3a08f900b6a2c36c44a3703eb3a13aff1d746d00 -README.zh.md: 837c2afb7f3e68e13e46de2745d0cbe8919d254b +# pnpm run verify-translation-pairing --write packages/hooks/hooks-codex/README.md +README.md: fd57762c6fb91e0ea47ec57c30bf9850bc488a33 +README.zh.md: 367d6acd0fec486cb0f9fb50023ad2ed4cca7217 diff --git a/packages/hooks/hooks-codex/README.md b/packages/hooks/hooks-codex/README.md index 3a08f900b6..fd57762c6f 100644 --- a/packages/hooks/hooks-codex/README.md +++ b/packages/hooks/hooks-codex/README.md @@ -46,7 +46,7 @@ The hooks themselves run in the agent's session workspace: for the agent-scoped | `UserPromptSubmit` | `agent/prompt-submit` (waterfall) | `block` (exit 2) → `PromptDecision.block`; additionalContext-only → delegate via `next()` then prepend a separately sourced context to downstream `additionalContexts` | | `PreToolUse` | `tools/pre-execute` (waterfall) | `block` → `PreToolDecision.deny` (no `allow`/`ask`) | | `PostToolUse` | `tools/post-execute` (waterfall) | `block` → `block` with feedback; additionalContext-only → delegate via `next()` then prepend a separately sourced context to the downstream decision; Code Mode defers sub-call contexts until the outer `run_code` result | -| `Stop` | `agent/stopping` (serial) | a blocking Stop hook feeds its reason through `steer()`, forcing another step | +| `Stop` | `agent/turn-stopping` (serial) | a blocking Stop hook feeds its reason through `steer()`, forcing another step | A tool call's payload carries the real `tool_name` (the same value the matcher tests) and Codex's `tool_input: { command }` shape (the `command` arg when present, else `''`). The matcher subject is the tool name (`PreToolUse`/`PostToolUse`) or the session source (`SessionStart`); `UserPromptSubmit`/`Stop` ignore matchers. diff --git a/packages/hooks/hooks-codex/README.zh.md b/packages/hooks/hooks-codex/README.zh.md index 837c2afb7f..367d6acd0f 100644 --- a/packages/hooks/hooks-codex/README.zh.md +++ b/packages/hooks/hooks-codex/README.zh.md @@ -46,7 +46,7 @@ hook 本身会在 agent 的会话工作区中运行:对 agent scope 点,桥 | `UserPromptSubmit` | `agent/prompt-submit`(waterfall) | `block`(退出码 2)→ `PromptDecision.block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游 `additionalContexts` | | `PreToolUse` | `tools/pre-execute`(waterfall) | `block` → `PreToolDecision.deny`(没有 `allow`/`ask`) | | `PostToolUse` | `tools/post-execute`(waterfall) | `block` → 带反馈的 `block`;仅 additionalContext → 通过 `next()` 委托,再将一个单独标记源的上下文前置到下游决策;Code Mode 将子调用上下文延迟到外层 `run_code` 结果 | -| `Stop` | `agent/stopping`(serial) | 阻塞 Stop hook 通过 `steer()` 送入其原因,强制再执行一步 | +| `Stop` | `agent/turn-stopping`(serial) | 阻塞 Stop hook 通过 `steer()` 送入其原因,强制再执行一步 | 工具调用的 payload 携带真实 `tool_name`(matcher 测试的相同值)与 Codex `tool_input: { command }` 形状(存在 `command` arg 时使用该值,否则使用 `''`)。matcher subject 是工具名称(`PreToolUse`/`PostToolUse`)或会话源(`SessionStart`);`UserPromptSubmit`/`Stop` 忽略 matcher。 diff --git a/packages/hooks/hooks-codex/src/index.ts b/packages/hooks/hooks-codex/src/index.ts index b5b5f9d503..1100b0cd47 100644 --- a/packages/hooks/hooks-codex/src/index.ts +++ b/packages/hooks/hooks-codex/src/index.ts @@ -252,7 +252,7 @@ export function apply(ctx: Context, config: Config): void { // TODO(stop-loop-guard): Codex supplies `stop_hook_active` so a Stop hook can // avoid continuing the same turn indefinitely. It is always false here, so an // unconditionally blocking hook force-continues every step until it self-limits. - ctx.on('agent/stopping', async (agent, turn, signal): Promise => { + ctx.on('agent/turn-stopping', async (agent, turn, signal): Promise => { const merged = await runPoint('Stop', '', { ...turnBase(ctx, agent, 'Stop', model), stop_hook_active: false, last_assistant_message: null }, { agent, turn, signal }) /* jscpd:ignore-end */ if (merged.decision === 'deny') { diff --git a/packages/llm/llm-retry/README.i18n.yaml b/packages/llm/llm-retry/README.i18n.yaml index 43368e3e39..bab7b5cec8 100644 --- a/packages/llm/llm-retry/README.i18n.yaml +++ b/packages/llm/llm-retry/README.i18n.yaml @@ -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 -README.md: f9f4c9db4bd1c64bcb55697034af8411590a84ed -README.zh.md: 335c224e1209e2f50d3c5453088cde1d01dc16fe +# pnpm run verify-translation-pairing --write packages/llm/llm-retry/README.md +README.md: 11b466a68804ae63a44f06d78c7aa5893c4041a7 +README.zh.md: 1638078ed639271802fa841f3b26f6bd3f4c1737 diff --git a/packages/llm/llm-retry/README.md b/packages/llm/llm-retry/README.md index f9f4c9db4b..11b466a688 100644 --- a/packages/llm/llm-retry/README.md +++ b/packages/llm/llm-retry/README.md @@ -6,7 +6,7 @@ Function plugin that retries selected transient model-request failures through t The default policy permits two retries for `EMPTY_RESPONSE`, `RATE_LIMIT`, `SERVER`, `TIMEOUT`, and `TRANSPORT`, using bounded exponential backoff from 500 ms to 10 seconds with 10 percent jitter. `EMPTY_RESPONSE` is the adapters' classification of a degenerate provider completion (a terminal stop with zero content blocks); the attempt produced nothing durable, so repeating it is safe. Delay bounds must fit Node's supported timer range. A valid `providerRetryAfterMs` replaces local backoff when it is within the configured cap; an over-cap instruction delegates to the next recovery policy instead. -The recovery listener appends a non-surface `llm/retry` event after the failed step, waits for the backoff while the failed turn's signal remains live, then calls `agent.retry()`. The loop closes that failed turn and opens a retry turn over the same durable history. The policy keeps its own retry count across that uninterrupted recovery chain and clears it at terminal `agent/idle`. Turn cancellation and plugin disposal abort the wait. +The recovery listener appends a non-surface `llm/retry` event after the failed step, waits for the backoff while the failed turn's signal remains live, then calls `agent.retry()`. The loop closes that failed turn and opens a retry turn over the same durable history. The policy keeps its own retry count across that uninterrupted recovery chain and clears it at terminal `agent/settled`. Turn cancellation and plugin disposal abort the wait. The separately published `./invariant` companion checks that every retry record appears inside an open turn after its failed step, matches its position in the current retry chain, and carries a positive bounded retry budget and non-negative bounded timer delay. Full jitter may schedule zero milliseconds at its lower boundary. diff --git a/packages/llm/llm-retry/README.zh.md b/packages/llm/llm-retry/README.zh.md index 335c224e12..1638078ed6 100644 --- a/packages/llm/llm-retry/README.zh.md +++ b/packages/llm/llm-retry/README.zh.md @@ -6,7 +6,7 @@ 默认策略允许为 `EMPTY_RESPONSE`、`RATE_LIMIT`、`SERVER`、`TIMEOUT` 和 `TRANSPORT` 重试两次,使用从 500 ms 到 10 秒的有界指数退避与 10% jitter。`EMPTY_RESPONSE` 是适配器对退化提供方完成的分类(携带零个内容块的终止 stop);该尝试未产生持久内容,因此可安全重复。延迟边界必须适合 Node 支持的定时器范围。有效 `providerRetryAfterMs` 在已配置上限内时替换本地退避;超出上限的指令会委托给下一项恢复策略。 -恢复 listener 会在失败步骤之后追加一个非表层 `llm/retry` 事件,在失败轮次的信号仍存活期间等待退避,然后调用 `agent.retry()`。循环会关闭该失败轮次,并在同一持久历史上开启重试轮次。策略在这条不间断的恢复链中维护自己的重试计数,并在终态 `agent/idle` 时清零。轮次取消与插件 dispose 会中止等待。 +恢复 listener 会在失败步骤之后追加一个非表层 `llm/retry` 事件,在失败轮次的信号仍存活期间等待退避,然后调用 `agent.retry()`。循环会关闭该失败轮次,并在同一持久历史上开启重试轮次。策略在这条不间断的恢复链中维护自己的重试计数,并在终态 `agent/settled` 时清零。轮次取消与插件 dispose 会中止等待。 单独发布的 `./invariant` 配套模块会检查每个重试记录是否出现在开启轮次内的失败步骤之后,是否与其在当前重试链中的位置匹配,以及是否携带正数有界重试预算和非负有界定时器延迟。完整 jitter 可以在下界调度为零毫秒。 diff --git a/packages/llm/llm-retry/src/index.ts b/packages/llm/llm-retry/src/index.ts index 96dc44a072..6b863a1bbf 100644 --- a/packages/llm/llm-retry/src/index.ts +++ b/packages/llm/llm-retry/src/index.ts @@ -172,7 +172,7 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna agent.retry() } - ctx.on('agent/idle', (agent) => { + ctx.on('agent/settled', (agent) => { retries.delete(agent) }) diff --git a/packages/plan/plan-mode/tests/integration.spec.ts b/packages/plan/plan-mode/tests/integration.spec.ts index 60bd87ae23..ef4de3beb3 100644 --- a/packages/plan/plan-mode/tests/integration.spec.ts +++ b/packages/plan/plan-mode/tests/integration.spec.ts @@ -128,7 +128,7 @@ describe('plan mode through the agent loop', () => { expect(second.data.header.system).toContain('plan mode') }) - it('a mode flip at error idle shapes the retry before its assembly', async () => { + it('a mode flip at error settlement shapes the retry before its assembly', async () => { const failedRequest = [{ type: 'finish', reason: { kind: 'error', failure: { message: 'temporarily unavailable', code: 'SERVER', status: 503 } }, @@ -136,7 +136,7 @@ describe('plan mode through the agent loop', () => { const adapter = new MockAdapter([failedRequest, textResponse('Recovered in plan mode.')]) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('it-plan-retry-flip'), { provider: 'mock', model: 'mock' }) - ctx.on('agent/idle', (subject, _turn, reason) => { + ctx.on('agent/settled', (subject, _turn, reason) => { if (subject !== agent || reason.kind !== 'error') return ctx.planMode.set(agent, true) agent.retry() diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index f7d652d7dd..1f67ee605c 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -38,7 +38,7 @@ export const LINK_MAP: Record = { AgentMessage: 'core.md', AgentMessageId: 'core.md', HookContext: 'core.md', - IdleReason: 'core.md', + SettleReason: 'core.md', LlmCallConfig: 'core.md', LlmModelContext: 'core.md', LlmModelReasoningInfo: 'core.md', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index c5706e01bb..534f328fa3 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -1025,7 +1025,7 @@ function renderLifecycle(): string { ' end', ' Driver->>Session: post-tool context and steering (no prompt-submit)', ` Driver->>Session: ${mermaidCode('step/end')}`, - ` Driver->>Hooks: ${mermaidCode('agent/stopping')} serial terminal checkpoint`, + ` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`, ' end', ` Driver->>Session: ${mermaidCode('turn/end')}`, ` Driver-->>SDK: ${mermaidCode('agent/status')} idle`,