mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(agent): return request retry action
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-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
|
||||
2026-06-21-bounded-llm-request-recovery.md: 9c9d8a02595b988535158c9aa0ec43d6f1fa0c89
|
||||
2026-06-21-bounded-llm-request-recovery.zh.md: bb9e430eaf87789452fd4cc89085d7d635f54ed1
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-06-21-bounded-llm-request-recovery.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh-llm` can report provider failures either by throwing during adapter dispatch or iteration or by ending with `finish { kind: 'error' | 'aborted' }`. The final adapter boundary tags thrown failures so `dsh-agent-loop` can distinguish them from middleware and result-processing defects, and the loop normalizes both delivery forms into `agent/request-error` after closing the failed step. An unhandled failure is terminal; a handling listener repairs policy-owned state, calls `agent.retry()`, and stops waterfall delegation.
|
||||
`dsh-llm` can report provider failures either by throwing during adapter dispatch or iteration or by ending with `finish { kind: 'error' | 'aborted' }`. The final adapter boundary tags thrown failures so `dsh-agent-loop` can distinguish them from middleware and result-processing defects, and the loop normalizes both delivery forms into `agent/request-error` after closing the failed step. An unhandled failure is terminal; a handling listener repairs policy-owned state, returns `{ kind: 'retry' }`, and stops waterfall delegation. The [retry-action decision](../simplification/2026-07-27-request-error-retry-action.md) owns this return contract.
|
||||
|
||||
That boundary is already safe for another request attempt. Raw `assistant/chunk` events carry the failed `turn` and `step`, message derivation ignores them unless a successful `assistant/message` cites them, tool calls are dispatched only after a successful terminal finish and assembly, and a retry opens a new numbered turn from the durable log. The harness therefore does not need a second response lifecycle or tentative-output protocol to keep two attempts separate.
|
||||
|
||||
@@ -72,7 +72,7 @@ The plugin owns a lifetime `AbortController` and tracks every active backoff cal
|
||||
|
||||
Before sleeping, `dsh-llm-retry` appends one non-surface `llm/retry` session event containing the turn, failed step, one-based transient retry number, configured maximum, scheduled delay, and `LlmFailure`. The plugin owns the `SessionEventMap` augmentation; `dsh-session` remains generic persistence and does not absorb the optional policy's vocabulary. The event says what was scheduled, not that the next request completed; cancellation during the delay is subsequently visible on `turn/end`. The event ships only with a production renderer and replay/snapshot coverage, because its purpose is operational state rather than trace collection.
|
||||
|
||||
The listener calls `next()` for a non-transient code, an exhausted policy budget, or an over-cap provider delay. This preserves composition with context-overflow recovery and later policy plugins. For an owned failure it records and awaits the delay, then calls `agent.retry()` without delegating. Turn cancellation and plugin disposal end the wait without requesting a retry; the loop's cancellation/disposal checks remain authoritative.
|
||||
The listener calls `next()` for a non-transient code, an exhausted policy budget, or an over-cap provider delay. This preserves composition with context-overflow recovery and later policy plugins. For an owned failure it records and awaits the delay, then returns `{ kind: 'retry' }` without delegating. Turn cancellation and plugin disposal end the wait without returning a retry; the loop's cancellation/disposal checks remain authoritative.
|
||||
|
||||
The agent-spine demo bundle loads the plugin so the shared stdio/TUI, one-shot CLI, and ACP example compositions use the same bounded policy. Library consumers retain explicit plugin composition: omitting the plugin leaves request failures terminal.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`dsh-llm` 可能在适配器分发或迭代时抛出异常,也可能以 `finish { kind: 'error' | 'aborted' }` 结束,以这两种形式报告提供方失败。最终适配器边界会标记抛出的失败,使 `dsh-agent-loop` 能将其与中间件和结果处理缺陷区分开。循环关闭失败步骤后,会把两种交付形式统一规范化为 `agent/request-error`。未被处理的失败是终态;处理失败的监听器修复策略自有状态,调用 `agent.retry()`,并停止 waterfall 委托。
|
||||
`dsh-llm` 可能在适配器分发或迭代时抛出异常,也可能以 `finish { kind: 'error' | 'aborted' }` 结束,以这两种形式报告提供方失败。最终适配器边界会标记抛出的失败,使 `dsh-agent-loop` 能将其与中间件和结果处理缺陷区分开。循环关闭失败步骤后,会把两种交付形式统一规范化为 `agent/request-error`。未被处理的失败是终态;处理失败的监听器修复策略自有状态,返回 `{ kind: 'retry' }`,并停止 waterfall 委托。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.md)规定这一返回契约。
|
||||
|
||||
该边界已能安全地再次发起请求。原始 `assistant/chunk` 事件携带失败的 `turn` 和 `step`;除非某条成功的 `assistant/message` 引用这些事件,否则消息派生会忽略它们。只有终止性 finish 成功且组装完成后,系统才会分发工具调用;重试则会从持久日志开启新的编号轮次。因此,harness 无需引入第二套响应生命周期或暂定输出协议,即可分隔两次尝试。
|
||||
|
||||
@@ -72,7 +72,7 @@ interface Config {
|
||||
|
||||
休眠前,`dsh-llm-retry` 会追加一条不进入表层的 `llm/retry` 会话事件,其中包含轮次、失败步骤、从 1 开始的暂时性重试编号、已配置上限、计划延迟和 `LlmFailure`。该插件拥有 `SessionEventMap` 声明合并;`dsh-session` 继续负责通用持久化,不会吸收可选策略的词汇。事件记录已安排的内容,而不是下一个请求已完成;延迟期间取消随后会在 `turn/end` 中可见。因为该事件的目的是表示运行状态,而不是收集跟踪数据,所以它仅与生产渲染器及回放/快照覆盖一起交付。
|
||||
|
||||
对非暂时性 code、耗尽的策略预算或超出上限的提供方延迟,监听器会调用 `next()`。这保留了与上下文溢出恢复及后续策略插件的组合能力。对自身处理的失败,它会记录并等待延迟,然后在不委托的情况下调用 `agent.retry()`。轮次取消和插件释放会结束等待且不请求重试,此后仍以循环的取消/释放检查为准。
|
||||
对非暂时性 code、耗尽的策略预算或超出上限的提供方延迟,监听器会调用 `next()`。这保留了与上下文溢出恢复及后续策略插件的组合能力。对自身处理的失败,它会记录并等待延迟,然后在不委托的情况下返回 `{ kind: 'retry' }`。轮次取消和插件释放会结束等待且不返回重试动作,此后仍以循环的取消/释放检查为准。
|
||||
|
||||
agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次性 CLI(命令行界面)和 ACP(Agent Client Protocol)示例组合使用同一有界策略。库消费方仍需显式组合插件:省略该插件时,请求失败保持终态。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-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
|
||||
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: 51d488db28c57426c75c9ed1cfc90892261c0224
|
||||
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: ae33cf5c2e944e584cd3d3c6ff76d93619adf7dc
|
||||
|
||||
@@ -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/settled`. 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, returns `{ kind: '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`. The [retry-action decision](../simplification/2026-07-27-request-error-retry-action.md) owns the return boundary.
|
||||
|
||||
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.
|
||||
|
||||
@@ -34,7 +34,7 @@ If cancellation lands after assistant tool calls are durable but before all call
|
||||
|
||||
For `pressure`, compact-basic resolves the durable provider/model target's adapter-owned capacity and exact-target policy, then applies the resulting threshold and retained-tail budgets to one unified `ctx.tokenMeter.measure()` result. Below pressure it returns without pruning. Once pressure qualifies, optional `ctx.toolResultPrune` rewrites oversized current results and compact-basic remeasures through the same meter; safe pressure skips the model call, while remaining pressure selects and summarizes from the pruned surface. The same singleton meter owns range pricing, provenance, shadowed token counts, and non-shrinking-summary rejection. Common defaults remain threshold ratio `0.8`, retained-history ratio `0.16`, summarization provider/model `''`, `maxTokens: 8192`, `compactionRetries: 1`, and `auto: true`; optional `modelPolicies` entries override them for an exact provider/model pair.
|
||||
|
||||
For canonical overflow, compact-basic requires no capacity metadata and bypasses scalar pressure and the normal retained-token budget. It prunes first, then chooses the maximal tool-balanced head range while leaving the newest indivisible unit and attempts one shrinking summary compaction under the same signal when a range exists. The automatic listener snapshots `session.surface.replaceGeneration` and calls `agent.retry()` whenever pruning or summarization increases it. This remains true when pruning lands before later summary work throws; cancellation still wins. A backend returning a result without replacement cannot authorize retry, while pruning-only progress can authorize a retry without a `CompactionResult`.
|
||||
For canonical overflow, compact-basic requires no capacity metadata and bypasses scalar pressure and the normal retained-token budget. It prunes first, then chooses the maximal tool-balanced head range while leaving the newest indivisible unit and attempts one shrinking summary compaction under the same signal when a range exists. The automatic listener snapshots `session.surface.replaceGeneration` and returns `{ kind: 'retry' }` whenever pruning or summarization increases it. This remains true when pruning lands before later summary work throws; cancellation still wins. A backend returning a result without replacement cannot authorize retry, while pruning-only progress can authorize a retry without a `CompactionResult`.
|
||||
|
||||
`maxOverflowRetries` is optional and defaults to `1`; `0` disables overflow recovery without disabling pressure. `auto: false` registers neither automatic listener. Noncanonical errors, exhausted attempts, an already-aborted signal, a missing routed model, no safe range, no generation change, and recovery throws before any replacement all delegate to the next listener. With no later recovery, the loop reports the original provider error object and code. A recovery throw after generation advances authorizes retry from durable progress; cancellation or disposal remains authoritative even if recovery work completes concurrently.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Status: implemented
|
||||
|
||||
`RequestError` 与 `agent/request-error` waterfall 表示最终适配器已经选定之后的失败。每个返回的流句柄都绑定一个私有失败集合;该集合在分发、异步迭代器构造与迭代过程中保留原始抛出错误的身份,同时防止把嵌套调用的错误来源误归到外层调用。终止性的带内 `error` 或 `aborted` finish 进入同一路径。提示词装配、请求中间件、请求日志、结果处理、工具、step 监听器与清理仍属于普通失败。
|
||||
|
||||
恢复运行前,失败 step 已经关闭。负责处理的监听器修复持久状态、调用 `agent.retry()`,并停止 waterfall 委托。循环随后关闭失败 turn,并从持久日志开启一个重试 turn,中间不发布空闲通知。重试策略与尝试计数由插件自己拥有;compact-basic 在链路到达终态 `agent/settled` 时清除对应 agent 的溢出计数。两个 DeepSeek 适配器都把识别出的提供方上下文限制错误规范化为 `CONTEXT_WINDOW_EXCEEDED`。
|
||||
恢复运行前,失败 step 已经关闭。负责处理的监听器修复持久状态、返回 `{ kind: 'retry' }`,并停止 waterfall 委托。循环随后关闭失败 turn,并从持久日志开启一个重试 turn,中间不发布空闲通知。重试策略与尝试计数由插件自己拥有;compact-basic 在链路到达终态 `agent/settled` 时清除对应 agent 的溢出计数。两个 DeepSeek 适配器都把识别出的提供方上下文限制错误规范化为 `CONTEXT_WINDOW_EXCEEDED`。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.md)规定这一返回边界。
|
||||
|
||||
如果取消发生在 assistant 工具调用已经持久化之后、所有调用完成分发之前,循环会为每个尚未分发的调用记录一对合成的 `tool/call` 与 aborted `tool/result`,随后进入正常中止路径。因此,表层不会仅因取消赢得竞态而留下孤立的持久工具调用。
|
||||
|
||||
@@ -34,7 +34,7 @@ Status: implemented
|
||||
|
||||
对于 `pressure`,compact-basic 先解析持久提供方/模型目标的适配器所属容量与精确目标策略,再把得到的阈值与保留尾部预算应用到一次统一的 `ctx.tokenMeter.measure()` 结果。低于压力时直接返回,不执行剪枝。压力达到条件后,可选的 `ctx.toolResultPrune` 会改写当前表层中过大的工具结果,compact-basic 再通过同一个 meter 重新计量;若压力恢复安全则跳过模型调用,否则从已剪枝表层选择范围并生成摘要。范围定价、来源、被遮蔽 token 数与非缩小摘要拒绝也由同一个单例 meter 完成。通用默认值保持为阈值比例 `0.8`、保留历史比例 `0.16`、摘要提供方/模型 `''`、`maxTokens: 8192`、`compactionRetries: 1` 与 `auto: true`;可选 `modelPolicies` 项可以按精确提供方/模型组合覆盖这些值。
|
||||
|
||||
对于规范化溢出,compact-basic 不要求容量元数据,并绕过标量压力与普通保留 token 预算。它先执行剪枝,再在保留最新不可分割单元的同时选择最大的工具配对平衡头部范围;存在范围时,才在同一 signal 下尝试一次缩小摘要压缩。自动监听器先记录 `session.surface.replaceGeneration`,剪枝或摘要让 generation 增加时就调用 `agent.retry()`。即使剪枝先落盘而后续摘要工作抛错,这条规则仍然成立;取消依然优先。后端若只返回结果但没有替换表层,不能授权重试;只有剪枝取得进展时,即使没有 `CompactionResult` 也可以授权重试。
|
||||
对于规范化溢出,compact-basic 不要求容量元数据,并绕过标量压力与普通保留 token 预算。它先执行剪枝,再在保留最新不可分割单元的同时选择最大的工具配对平衡头部范围;存在范围时,才在同一 signal 下尝试一次缩小摘要压缩。自动监听器先记录 `session.surface.replaceGeneration`,剪枝或摘要让 generation 增加时就返回 `{ kind: 'retry' }`。即使剪枝先落盘而后续摘要工作抛错,这条规则仍然成立;取消依然优先。后端若只返回结果但没有替换表层,不能授权重试;只有剪枝取得进展时,即使没有 `CompactionResult` 也可以授权重试。
|
||||
|
||||
`maxOverflowRetries` 可选且默认为 `1`;`0` 只禁用溢出恢复,不会禁用压力检查。`auto: false` 不注册任何自动监听器。非规范化错误、尝试耗尽、已经中止的 signal、缺失路由模型、没有安全范围、generation 未变化,以及在任何替换之前恢复抛错,都会委托给下一个监听器。若没有后续恢复,循环报告原始提供方错误对象与代码。generation 增加后的恢复抛错会基于持久进展授权重试;即使恢复工作并发完成,取消或销毁仍具有最终优先级。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-24-separate-context-injection-from-turn-execution.md
|
||||
2026-07-24-separate-context-injection-from-turn-execution.md: 37798795b706719ca46a4495ef3f82a7968c501f
|
||||
2026-07-24-separate-context-injection-from-turn-execution.zh.md: 560f97d00c491adad4deeeeee8c8ed7ea8b88028
|
||||
2026-07-24-separate-context-injection-from-turn-execution.md: b74cd6bdc48e795e57d780ab31a907ffe94dd518
|
||||
2026-07-24-separate-context-injection-from-turn-execution.zh.md: f2421d2fc7b8c1329dd1349a6fb088407ac5fc75
|
||||
|
||||
@@ -30,7 +30,7 @@ During prompt admission or an open turn, `inject()` stages context in the loop o
|
||||
|
||||
Outside that window, `inject()` appends its `user/message` immediately. It does not increment turn numbering, emit `turn/start` or `turn/end`, change agent status, or run the model; persistence observes the append through `session/event`.
|
||||
|
||||
If prompt admission blocks or fails, a caller-staged context-only batch appends immediately without a turn. Steering and context staged beside it remain in the outbox for `retry()` or a later admitted prompt; cancellation or disposal may discard them. Hook-produced `additionalContexts` never materialize because they belong to the rejected admission decision.
|
||||
If prompt admission blocks or fails, a caller-staged context-only batch appends immediately without a turn. Steering and context staged beside it remain in the outbox for a later admitted prompt; cancellation or disposal may discard them. Hook-produced `additionalContexts` never materialize because they belong to the rejected admission decision.
|
||||
|
||||
The session invariant permits `user/message` between turns while continuing to require turn enclosure for execution events, steering, assistant output, tools, and package-added events by default. Persistence, recovery, resume, fork, and compaction treat a valid out-of-turn `user/message` as committed session history rather than an interrupted or discardable turn tail.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ agent API 曾用三种相互重叠的方式表示面向模型的补充输入:
|
||||
|
||||
在该窗口之外,`inject()` 会立即追加对应的 `user/message`。它不会增加轮次编号、发出 `turn/start` 或 `turn/end`、改变 agent 状态,也不会运行模型;持久化通过 `session/event` 观察这次追加。
|
||||
|
||||
如果提示词准入被阻止或失败,调用方暂存的仅含上下文的批次会立即追加,且不产生轮次。steering 及与其一同暂存的上下文会留在 outbox 中,供 `retry()` 或后续获准提示词使用;取消或 dispose(资源释放)可能丢弃它们。钩子产生的 `additionalContexts` 属于被拒绝的准入决策,因此永远不会落入日志。
|
||||
如果提示词准入被阻止或失败,调用方暂存的仅含上下文的批次会立即追加,且不产生轮次。steering 及与其一同暂存的上下文会留在 outbox 中,供后续获准提示词使用;取消或 dispose(资源释放)可能丢弃它们。钩子产生的 `additionalContexts` 属于被拒绝的准入决策,因此永远不会落入日志。
|
||||
|
||||
会话不变量允许 `user/message` 位于两个轮次之间,同时继续要求执行事件、steering、助手输出、工具事件以及默认的包扩展事件均受轮次边界约束。持久化、恢复、resume、fork 和压缩会把合法的轮次外 `user/message` 当作已提交会话历史,而不是中断轮次或可丢弃的日志尾部。
|
||||
|
||||
|
||||
@@ -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-18-compaction-capability-seam.md: 73fd78af9d4ef86ae7bafc3a3608ff1afbbd87bd
|
||||
2026-06-18-compaction-capability-seam.zh.md: 8fc92ccd5ae6ad61a174e01b3ee3afa1df0a4529
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md
|
||||
2026-06-18-compaction-capability-seam.md: 3c219b734e148b963fb5857de89c16f28c2bd402
|
||||
2026-06-18-compaction-capability-seam.zh.md: b2c7e9720b596705b60a284e6ccf1448a782b7fc
|
||||
|
||||
@@ -39,7 +39,7 @@ An earlier draft put the full algorithm (the retention walk, token-summing, text
|
||||
|
||||
Successful-call pressure cannot run at pre-step because final `agent/request` routing, provider output, tool results, buffered context, and steering do not exist there. Serial `agent/post-step(agent, turn, step, signal)` fires after those facts are durable and before `step/end`. `dsh-compact-basic` measures the canonical logged request through `ctx.tokenMeter`, so the next request sees any replacement without a speculative envelope override. Once pressure qualifies, optional `ctx.toolResultPrune` rewriting runs before summary selection; compact-basic remeasures the durable surface and skips summarization if pruning restores safe pressure.
|
||||
|
||||
Canonical provider context overflow takes a separate path. The failed step closes and `agent/request-error` receives the original request error. Compact-basic owns its per-agent overflow count, prunes before forcing one useful balanced reduction, and calls `agent.retry()` only if `session.surface.replaceGeneration` increases, including pruning-only progress when no summary range exists. The loop then closes the failed turn, opens a new numbered retry turn, and reconstructs its request from the durable log. No replacement, a recovery failure before any replacement, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. If pruning already advanced the generation before later summary work fails, recovery retries from that durable pruned surface unless cancellation or disposal wins. The complete lifecycle decision is in the [after-call recovery Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md).
|
||||
Canonical provider context overflow takes a separate path. The failed step closes and `agent/request-error` receives the original request error. Compact-basic owns its per-agent overflow count, prunes before forcing one useful balanced reduction, and returns `{ kind: 'retry' }` only if `session.surface.replaceGeneration` increases, including pruning-only progress when no summary range exists. The loop then closes the failed turn, opens a new numbered retry turn, and reconstructs its request from the durable log. No replacement, a recovery failure before any replacement, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. If pruning already advanced the generation before later summary work fails, recovery retries from that durable pruned surface unless cancellation or disposal wins. The complete lifecycle decision is in the [after-call recovery Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md).
|
||||
|
||||
```
|
||||
assistant/message → tool/result/context/steering
|
||||
|
||||
@@ -39,7 +39,7 @@ Status: implemented
|
||||
|
||||
成功调用的压力检查不能在步骤前运行,因为最终的 `agent/request` 路由、提供方输出、工具结果、缓冲上下文与 steering 当时尚不存在。串行的 `agent/post-step(agent, turn, step, signal)` 会在这些事实持久化后、`step/end` 之前触发。`dsh-compact-basic` 通过 `ctx.tokenMeter` 测量规范的已记录请求,因此下一个请求无需推测性覆盖信封即可看到任何替换。压力达到条件后,可选的 `ctx.toolResultPrune` 重写在摘要范围选择前运行;compact-basic 重新测量持久 surface,如果修剪恢复到安全压力便跳过摘要生成。
|
||||
|
||||
规范的提供方上下文溢出走另一条路径。失败步骤先关闭,`agent/request-error` 接收原始请求错误。compact-basic 自行持有按 agent 计的溢出次数,在强制执行一次有效且平衡的缩减前先修剪,且仅当 `session.surface.replaceGeneration` 增加时才调用 `agent.retry()`;这包括没有摘要范围时仅修剪取得的进展。随后循环关闭失败轮次,开启新的编号重试轮次,并从持久日志重建请求。没有替换、任何替换前的恢复失败、取消、耗尽的上限或无关错误都会保留原始提供方失败。如果修剪已经推进 generation,而后续摘要工作失败,恢复会从该持久的已修剪 surface 重试,除非取消或资源释放胜出。完整生命周期决策见[调用后恢复 Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)。
|
||||
规范的提供方上下文溢出走另一条路径。失败步骤先关闭,`agent/request-error` 接收原始请求错误。compact-basic 自行持有按 agent 计的溢出次数,在强制执行一次有效且平衡的缩减前先修剪,且仅当 `session.surface.replaceGeneration` 增加时才返回 `{ kind: 'retry' }`;这包括没有摘要范围时仅修剪取得的进展。随后循环关闭失败轮次,开启新的编号重试轮次,并从持久日志重建请求。没有替换、任何替换前的恢复失败、取消、耗尽的上限或无关错误都会保留原始提供方失败。如果修剪已经推进 generation,而后续摘要工作失败,恢复会从该持久的已修剪 surface 重试,除非取消或资源释放胜出。完整生命周期决策见[调用后恢复 Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)。
|
||||
|
||||
```
|
||||
assistant/message → tool/result/context/steering
|
||||
|
||||
@@ -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-17-one-send-one-turn.md: d6c771a2b87e14c52b05c9cd8accaa44405fba16
|
||||
2026-07-17-one-send-one-turn.zh.md: a057ebce38208f0ed6be3b8b97dd4c434dd3e721
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md
|
||||
2026-07-17-one-send-one-turn.md: dcc6c0aa483a0e53205dbaeef4e2b903f5f6a215
|
||||
2026-07-17-one-send-one-turn.zh.md: 8c12481defe6608c13ee81132b432b4d8b17b681
|
||||
|
||||
@@ -22,7 +22,7 @@ If messages A and B are both processed, B's turn starts only after A records `tu
|
||||
|
||||
Prompt admission decides one message at a time before a turn opens. An allowed prompt becomes that turn's `user/message`; a blocked prompt is discarded without opening a turn or writing session history. Mixed-batch and all-blocked-batch branches do not exist.
|
||||
|
||||
The no-batching rule applies only to ordinary `send()`. Running `steer()` puts input in the outbox. While a turn remains open, the loop records that input at the next step boundary and steering makes another step the default. A failure before that boundary leaves the steering staged without waking the agent; `retry()` or a later prompt takes it, while cancellation or disposal can discard it. When the agent is idle, `steer()` delegates to `send()`, so it creates an independent ordinary queue item.
|
||||
The no-batching rule applies only to ordinary `send()`. Running `steer()` puts input in the outbox. While a turn remains open, the loop records that input at the next step boundary and steering makes another step the default. A failure before that boundary leaves the steering staged without waking the agent; a request-error retry action or a later prompt takes it, while cancellation or disposal can discard it. When the agent is idle, `steer()` delegates to `send()`, so it creates an independent ordinary queue item.
|
||||
|
||||
`inject()` continues to add model-facing context without submitting an ordinary message. During a turn it waits in the outbox for a safe step boundary; while idle it appends a `user/message` directly, without opening a turn or running the model. Persistence owns the resulting eager drain. `cancel()` remains a whole-agent operation that can clear all unstarted ordinary and steering input and abort the current step. `status` and `whenIdle()` also describe the whole agent, not one message. Several one-message turns can share one `running` interval, so `running` does not prove that a turn is open.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Status: implemented
|
||||
|
||||
提示词准入会在轮次打开前,每次只决定一条消息。获准提示词成为该轮次的 `user/message`;被阻止的提示词会被丢弃,不打开轮次,也不写入会话历史。实现中不存在混合批次或全阻止批次分支。
|
||||
|
||||
上述不合批规则只适用于普通 `send()`。agent 运行时,`steer()` 会把输入放入 outbox。只要当前轮次仍然打开,agent loop 就会在下一个步骤边界记录该输入,而 steering(中途引导)会默认让循环再执行一个步骤。在到达该边界前发生失败,会让 steering 保持暂存且不唤醒 agent;`retry()` 或后续提示词会取走它,而取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 委托给 `send()`,因此会创建一个独立的普通队列项。
|
||||
上述不合批规则只适用于普通 `send()`。agent 运行时,`steer()` 会把输入放入 outbox。只要当前轮次仍然打开,agent loop 就会在下一个步骤边界记录该输入,而 steering(中途引导)会默认让循环再执行一个步骤。在到达该边界前发生失败,会让 steering 保持暂存且不唤醒 agent;请求错误的重试动作或后续提示词会取走它,而取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 委托给 `send()`,因此会创建一个独立的普通队列项。
|
||||
|
||||
`inject()` 继续添加面向模型的上下文,而不提交普通消息。轮次打开时,该上下文会留在 outbox 中,等待安全的步骤边界;agent 空闲时,系统会直接追加一条 `user/message`,既不打开轮次,也不运行模型。持久化层独立负责由此产生的即时排空。`cancel()` 仍是面向整个 agent 的操作,可以清空所有尚未启动的普通输入和 steering,并中止当前步骤。`status` 和 `whenIdle()` 描述的也是整个 agent,而不是某一条消息。多个单消息轮次可以共用一个 `running` 区间,因此 `running` 不表示轮次一定处于打开状态。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-24-agent-loop-observable-state-machine.md
|
||||
2026-07-24-agent-loop-observable-state-machine.md: 2905e003a05bf1c8caa8f784787fb76f8e76b7d7
|
||||
2026-07-24-agent-loop-observable-state-machine.zh.md: 1d6b5e5d34d020ef5561b8016b5ad065c96bff12
|
||||
2026-07-24-agent-loop-observable-state-machine.md: 54730de8aa73342b609d423dc478edb076d7844b
|
||||
2026-07-24-agent-loop-observable-state-machine.zh.md: 206ac701472f14823300df0c812f2cc818f852f5
|
||||
|
||||
@@ -25,9 +25,9 @@ The loop keeps five machine extension events. `agent/prompt-submit` admits, rewr
|
||||
|
||||
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/settled` 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, returns `{ kind: 'retry' }`, and stops delegating. The loop closes the failed turn and opens one retry turn over that state without an intervening idle notification; retry is not another step inside the failed turn. `agent/settled` reports the terminal outcome, and `agent/error` remains the live error notification for consumers that report failures independently of turn settlement. The [retry-action decision](2026-07-27-request-error-retry-action.md) supersedes the command-shaped part of this design.
|
||||
|
||||
The event taxonomy removes `agent/pre-step`, `agent/post-step`, `agent/session-prefix`, `agent/step-result`, `agent/turn-continuation`, and `agent/turn-stop`. Durable turn and step boundaries remain session events. Model-facing additions use logged message channels, request configuration uses `agent/request`, response content is recorded as assembled, failed-request recovery uses `agent/request-error` plus `agent.retry()`, and end-of-turn continuation uses `agent/turn-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 the `agent/request-error` return action, and end-of-turn continuation uses `agent/turn-stopping` plus steering.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -35,7 +35,7 @@ The event taxonomy removes `agent/pre-step`, `agent/post-step`, `agent/session-p
|
||||
|
||||
**Represent disposal as a third `AgentStatus`.** This gives retained handles a terminal status value but duplicates the registry lifecycle already expressed by `agent/disposed`. The decision keeps `AgentStatus` about live activity and makes registration lifetime a separate dimension.
|
||||
|
||||
**Return a retry decision from `agent/request-error`.** A returned instruction duplicates the existing `agent.retry()` command and requires the loop to carry policy history across attempts. The waterfall remains useful for ordered ownership: an unhandled listener delegates, while a handling listener performs its awaited repair, calls `agent.retry()`, and stops delegation.
|
||||
**Return a retry decision from `agent/request-error`.** This alternative is superseded by the [retry-action decision](2026-07-27-request-error-retry-action.md), which removes the duplicate command and keeps the decision local to the waterfall result.
|
||||
|
||||
**Mirror durable turn and step boundaries as agent events.** This gives live consumers a second event stream for the same facts. The decision keeps the session log as the source of truth and exposes only extension checkpoints or live-only facts that the durable stream cannot carry.
|
||||
|
||||
@@ -45,7 +45,7 @@ The observable machine is smaller and compositional: registration lifetime, acti
|
||||
|
||||
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.
|
||||
|
||||
Continuation plugins publish durable steering rather than returning an unlogged reason. Recovery plugins act after the failed step and explicitly schedule another turn through `agent.retry()`. This makes every attempt a complete turn while keeping asynchronous repair and policy ownership at one narrow waterfall boundary.
|
||||
Continuation plugins publish durable steering rather than returning an unlogged reason. Recovery plugins act after the failed step and return an explicit retry action. This makes every attempt a complete turn while keeping asynchronous repair and policy ownership at one narrow waterfall boundary.
|
||||
|
||||
The inbox lifecycle complements, rather than replaces, the durable session log. `AgentMessageId` correlates acceptance with claim or discard; turn and step numbers, messages, tool activity, and terminal reasons remain session facts.
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ agent 生命周期、agent 整体活动状态、收件箱条目的进度以及
|
||||
|
||||
是否继续和终止执行由数据表达,不再由返回的控制枚举表达。工具调用和已接受的 steering 要求再执行一个步骤。携带 `concludesTurn` 的工具结果会在其所属步骤终止工具循环。循环不再暴露通用的 `ContinuationDecision` 或终止停止返回通道。
|
||||
|
||||
模型请求失败会先关闭当前步骤,再携带准确错误、标准化 `LlmFailure` 和仍有效的轮次信号进入 `agent/request-error`。负责恢复的监听器修复状态、调用 `agent.retry()`,并停止继续委托。循环会关闭失败轮次,并基于该状态开启一个重试轮次,中间不发布空闲通知;重试不是失败轮次内的另一个步骤。`agent/settled` 报告终态结果;对于需要脱离轮次结算单独报告失败的消费方,`agent/error` 仍作为实时错误通知保留。
|
||||
模型请求失败会先关闭当前步骤,再携带准确错误、标准化 `LlmFailure` 和仍有效的轮次信号进入 `agent/request-error`。负责恢复的监听器修复状态、返回 `{ kind: 'retry' }`,并停止继续委托。循环会关闭失败轮次,并基于该状态开启一个重试轮次,中间不发布空闲通知;重试不是失败轮次内的另一个步骤。`agent/settled` 报告终态结果;对于需要脱离轮次结算单独报告失败的消费方,`agent/error` 仍作为实时错误通知保留。[重试动作决策](2026-07-27-request-error-retry-action.md)取代了本设计中命令形式的部分。
|
||||
|
||||
事件分类体系移除了 `agent/pre-step`、`agent/post-step`、`agent/session-prefix`、`agent/step-result`、`agent/turn-continuation` 和 `agent/turn-stop`。持久的轮次与步骤边界仍由会话事件记录。面向模型的新增内容使用有日志记录的消息通道,请求配置使用 `agent/request`,响应内容按组装后的原样记录,失败请求恢复使用 `agent/request-error` 加 `agent.retry()`,轮次结束时是否继续则使用 `agent/turn-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/turn-stopping` 加 steering 表达。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -35,7 +35,7 @@ agent 生命周期、agent 整体活动状态、收件箱条目的进度以及
|
||||
|
||||
**将 dispose 表示为第三种 `AgentStatus`。** 这样会让仍被持有的句柄得到一个终止状态值,但也会重复表达 `agent/disposed` 已经体现的注册表生命周期。当前决策让 `AgentStatus` 只表示活动中 agent 的状态,并将注册生命周期作为独立维度。
|
||||
|
||||
**让 `agent/request-error` 返回重试决策。** 返回指令会与现有的 `agent.retry()` 命令重复,还要求循环跨尝试携带策略历史。Waterfall 只需保留有序归属能力:未处理的监听器继续委托,负责处理的监听器完成可等待的修复、调用 `agent.retry()`,然后停止委托。
|
||||
**让 `agent/request-error` 返回重试决策。** 这一替代方案已由[重试动作决策](2026-07-27-request-error-retry-action.md)取代;新决策移除了重复命令,并将决策局限于 waterfall 的返回结果。
|
||||
|
||||
**将持久的轮次与步骤边界映射为 agent 事件。** 这样会为同一事实向实时消费方提供第二条事件流。当前决策将会话日志保留为真源,仅暴露扩展检查点或持久事件流无法承载的纯实时事实。
|
||||
|
||||
@@ -45,7 +45,7 @@ agent 生命周期、agent 整体活动状态、收件箱条目的进度以及
|
||||
|
||||
插件不再能够改写循环的每个阶段。不再提供仅用于请求的消息前缀、助手消息变换、步骤后检查点、通用的继续执行枚举、通用的终止停止结果或轮次内请求重试。扩展改用剩余的归属明确的通道,而不是重新构造这些阶段。
|
||||
|
||||
负责继续执行的插件发布可持久化的 steering,而不是返回未记录到日志中的原因。恢复插件在失败步骤结束后处理错误,并通过 `agent.retry()` 显式安排另一个轮次。这样,每次尝试都会成为完整轮次,同时异步修复和策略归属集中在一个狭窄的 waterfall 边界。
|
||||
负责继续执行的插件发布可持久化的 steering,而不是返回未记录到日志中的原因。恢复插件在失败步骤结束后处理错误,并返回显式重试动作。这样,每次尝试都会成为完整轮次,同时异步修复和策略归属集中在一个狭窄的 waterfall 边界。
|
||||
|
||||
收件箱生命周期用于补充持久会话日志,而非取代它。`AgentMessageId` 将接受操作与领取或丢弃操作关联起来;轮次编号与步骤编号、消息、工具活动和终止原因仍属于会话事实。
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-27-request-error-retry-action.md
|
||||
2026-07-27-request-error-retry-action.md: 3057b9fa28cf203c9374930fe97421918b4c1a6f
|
||||
2026-07-27-request-error-retry-action.zh.md: bcb4e592f0c3d86f896e279cf0e3ea400741a1bb
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Request-error retry action
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-27-request-error-retry-action.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Model-request recovery was decided inside `agent/request-error` but communicated through `Agent.retry()`. That public command was valid during one narrow waterfall window and while idle, rejected other running states, and required `ReactLoopAgent` to retain a mutable retry window beside the waterfall result. The recovery plugins were the only production callers, so the wider live-agent capability exposed states and behavior unrelated to their policy decision.
|
||||
|
||||
## Decision
|
||||
|
||||
`agent/request-error` returns `RequestErrorAction`, whose handling action is `{ kind: 'retry' }`; the default `undefined` keeps the failed turn terminal. A listener that does not own the failure calls `next()`. A listener that owns it performs any awaited repair and returns the retry action without delegating.
|
||||
|
||||
The loop reads the action after the waterfall settles, closes the failed turn, and opens one retry turn from durable history. It rechecks the turn signal when consuming the action, so cancellation or disposal during recovery prevents the retry even if a listener returns it afterward. A thrown recovery never produces an action.
|
||||
|
||||
`Agent` and `ReactLoopAgent` expose no `retry()` method. Ordinary new work enters through `send()` and its `followup()`, `steer()`, and `inject()` presets; only a handled model-request failure can open a promptless retry turn.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep `Agent.retry()` as the recovery command.** Runtime guards can restrict the command to the request-error window, but the interface still advertises an idle resummon operation with no production consumer and the loop still needs mutable side-channel state to recover a decision already owned by the waterfall.
|
||||
|
||||
**Return an explicit terminal action.** `undefined` already represents the waterfall's unhandled default and composes directly through `next()`. A second `{ kind: 'fail' }` value would add no distinct behavior or ownership information.
|
||||
|
||||
## Consequences
|
||||
|
||||
Recovery ownership, asynchronous repair, and the retry decision share one typed return path. The live-agent interface and concrete loop lose the idle resummon capability and retry-window state. Callers cannot restart arbitrary failed non-request work without submitting a later prompt, while transient and context-overflow policies retain numbered retry turns, durable-history reconstruction, finite private budgets, and cancellation precedence.
|
||||
|
||||
Focused agent-loop tests pin retry chaining, terminal fallthrough, recovery failure, and cancellation races. The llm-retry and compact-basic suites pin their policy-owned action returns, and the ACP, goal-session, and plan-mode integrations pin successor-turn adoption.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: 请求错误重试动作
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-27-request-error-retry-action.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
模型请求恢复由 `agent/request-error` 内部决定,却通过 `Agent.retry()` 传达。这个公开命令只在一个狭窄的 waterfall(瀑布式事件)窗口内和空闲时有效,在其他运行状态下会被拒绝,并要求 `ReactLoopAgent` 在 waterfall 结果旁保留一个可变的重试窗口。恢复插件是仅有的生产调用方,因此更宽泛的活跃 agent(智能体)能力暴露了与其策略决策无关的状态与行为。
|
||||
|
||||
## 决策
|
||||
|
||||
`agent/request-error` 返回 `RequestErrorAction`,其中负责处理的动作是 `{ kind: 'retry' }`;默认的 `undefined` 会让失败轮次保持终态。不拥有该失败的监听器调用 `next()`。拥有该失败的监听器执行所有需要等待的修复,然后直接返回重试动作而不继续委托。
|
||||
|
||||
waterfall 结算后,循环读取该动作,关闭失败轮次,并从持久历史开启一个重试轮次。循环在使用该动作时会再次检查轮次信号,因此即使监听器随后返回重试动作,恢复期间发生的取消或资源释放仍会阻止重试。抛出异常的恢复不会产生动作。
|
||||
|
||||
`Agent` 与 `ReactLoopAgent` 均不暴露 `retry()` 方法。普通新工作通过 `send()` 及其 `followup()`、`steer()` 和 `inject()` 预设进入;只有已处理的模型请求失败才能开启没有提示词的重试轮次。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留 `Agent.retry()` 作为恢复命令。** 运行时防护检查可以将该命令限制在请求错误窗口内,但接口仍会暴露一个没有生产消费方的空闲无提示词再运行操作,循环也仍需通过可变的旁路状态恢复已经由 waterfall 决定的结果。
|
||||
|
||||
**返回显式终态动作。** `undefined` 已经表示 waterfall 未处理时的默认值,并可直接通过 `next()` 组合。再添加一个 `{ kind: 'fail' }` 值不会提供不同的行为或归属信息。
|
||||
|
||||
## 后果
|
||||
|
||||
恢复归属、异步修复和重试决策共用一条类型化返回路径。活跃 agent 接口与具体循环不再具有空闲无提示词再运行能力和重试窗口状态。调用方如果不提交后续提示词,就无法重启任意失败的非请求工作;瞬时策略与上下文溢出策略则保留编号重试轮次、从持久历史重建、有限的策略私有预算和取消优先级。
|
||||
|
||||
聚焦的 agent-loop 测试固定了重试链、未处理失败保持终态、恢复失败和取消竞态。llm-retry 与 compact-basic 测试套件固定其策略自有的动作返回,而 ACP、goal-session 和 plan-mode 集成测试固定后继轮次承接。
|
||||
@@ -38,7 +38,7 @@ sequenceDiagram
|
||||
alt final adapter or terminal in-band request failure
|
||||
Driver->>Session: <code>step/end</code>
|
||||
Driver->>Hooks: <code>agent/request-error</code> waterfall
|
||||
Hooks-->>Driver: call agent.retry() or preserve the original error
|
||||
Hooks-->>Driver: return retry action or preserve the original error
|
||||
else model request succeeded
|
||||
Driver->>Session: <code>assistant/message</code>
|
||||
Driver->>Tools: classify pending call by executionMode
|
||||
|
||||
@@ -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: 779690f659ea0a2df1a53a13fdaa95d6a5eabce7
|
||||
architecture.zh.md: cf585f11d5bce556e2878a5f0d8511fe5020a7e8
|
||||
architecture.md: 3b0f72400ab1e6a9157aed2966b4a17c36e0c3ac
|
||||
architecture.zh.md: fa21c82a686d88a9bbec02180feae1dd0dbf4e47
|
||||
|
||||
@@ -117,11 +117,11 @@ Each step assembles ordered prompt sections, tool schemas, and variables; unknow
|
||||
|
||||
Admission-time and active-turn `inject()` stage for the next step; post-tool `additionalContexts` settles after results. Steering shares that staging boundary and requests another step. Idle `inject()` appends immediately without changing turn numbers; persistence drains eagerly.
|
||||
|
||||
Pruning precedes summaries; overflow retries require durable progress. Recovery uses `agent/request-error` after the failed step and before turn close. Its policy calls `agent.retry()` to schedule one retry turn; cancellation wins ([compaction](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md), [retry](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md)).
|
||||
Pruning precedes summaries; overflow retries require durable progress. Recovery uses `agent/request-error` after the failed step and before turn close. Its policy returns a retry action to schedule one retry turn; cancellation wins ([compaction](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md), [retry](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md)).
|
||||
|
||||
### Failure Boundaries
|
||||
|
||||
Adapter failures close the step before `agent/request-error` receives the exact `Error`, normalized `LlmFailure`, and turn signal. A handling listener calls `agent.retry()`; the loop closes the failed turn and opens another from durable history without idle notification. Exhaustion leaves the failed `turn/end` terminal; failed chunks commit neither message nor tool call.
|
||||
Adapter failures close the step before `agent/request-error` receives the exact `Error`, normalized `LlmFailure`, and turn signal. A handling listener returns `{ kind: 'retry' }`; the loop closes the failed turn and opens another from durable history without idle notification. Exhaustion leaves the failed `turn/end` terminal; failed chunks commit neither message nor tool call.
|
||||
|
||||
Other failures use `agent/error`. Cancellation and disposal beat recovery. Before request-header commit, the turn signal cancels asynchronous model-capability preparation; undispatched tools get synthetic `tool/call`/`ABORTED_BEFORE_DISPATCH` pairs. Effective `cancel(cause)` emits its cause before queue clearing and abort; observers cannot veto; idle calls emit nothing. Durability records user or parent cancellation as `aborted`, teardown as `disposed`; teardown awaits quiescence. The cause affects reporting, not late result-context handling ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)).
|
||||
|
||||
@@ -151,7 +151,7 @@ Durability is a plugin concern. Backends eagerly drain synchronous `session/even
|
||||
|
||||
Messages use typed blocks from merge-extensible `ContentBlockMap`; the pattern also types `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`. New blocks coordinate adapters, UI, compaction, token metering, and persistence; replay measurements live in [token-meter.md](core-data-structures/token-meter.md).
|
||||
|
||||
Streaming uses raw chunks and `BlockAssembler`. Each `LlmAdapter.stream()` is one provider attempt; adapters report normalized failure facts, and a handling `agent/request-error` plugin calls `agent.retry()`. The loop logs chunks, successful provenance, and replay state. Remote adapters use per-read idle watchdogs. Replay crosses routes only through a shared adapter instance ([contract](core-data-structures/llm-streaming.md)).
|
||||
Streaming uses raw chunks and `BlockAssembler`. Each `LlmAdapter.stream()` is one provider attempt; adapters report normalized failure facts, and a handling `agent/request-error` plugin returns a retry action. The loop logs chunks, successful provenance, and replay state. Remote adapters use per-read idle watchdogs. Replay crosses routes only through a shared adapter instance ([contract](core-data-structures/llm-streaming.md)).
|
||||
|
||||
## Extension And Composition
|
||||
|
||||
|
||||
@@ -117,11 +117,11 @@ idle inject:
|
||||
|
||||
接纳期间和活跃轮次内的 `inject()` 会为下一步骤暂存;工具执行后的 `additionalContexts` 会在结果记录完毕后落定。steering 与其共用这一暂存边界,并请求再执行一个步骤。空闲状态下的 `inject()` 会立即追加,且不改变轮次编号;持久化层会尽快排空。
|
||||
|
||||
裁剪先于摘要;溢出重试必须取得持久进展。恢复会在失败步骤关闭后、轮次关闭前使用 `agent/request-error`。其策略调用 `agent.retry()` 安排一个重试轮次;取消优先([压缩](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)、[重试](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md))。
|
||||
裁剪先于摘要;溢出重试必须取得持久进展。恢复会在失败步骤关闭后、轮次关闭前使用 `agent/request-error`。其策略返回重试动作以安排一个重试轮次;取消优先([压缩](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)、[重试](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md))。
|
||||
|
||||
### 失败边界
|
||||
|
||||
适配器故障会先关闭步骤,再由 `agent/request-error` 接收准确的 `Error`、标准化的 `LlmFailure` 和轮次信号。负责处理的监听器调用 `agent.retry()`;循环关闭失败轮次,并从持久历史开启另一个轮次,不发出空闲通知。重试耗尽后,失败的 `turn/end` 即为终态记录;失败分片不会提交消息或工具调用。
|
||||
适配器故障会先关闭步骤,再由 `agent/request-error` 接收准确的 `Error`、标准化的 `LlmFailure` 和轮次信号。负责处理的监听器返回 `{ kind: 'retry' }`;循环关闭失败轮次,并从持久历史开启另一个轮次,不发出空闲通知。重试耗尽后,失败的 `turn/end` 即为终态记录;失败分片不会提交消息或工具调用。
|
||||
|
||||
其他故障使用 `agent/error`。取消和资源释放优先于恢复。在提交请求头之前,轮次信号会取消异步模型能力准备;尚未分派的工具会得到合成的 `tool/call`/`ABORTED_BEFORE_DISPATCH` 对。实际生效的 `cancel(cause)` 在清空队列和中止前发出原因;观察方不能否决;空闲调用不发事件。持久化层将用户或父级取消记录为 `aborted`,拆卸记录为 `disposed`;拆卸会等待完全停稳。原因只影响报告方式,不影响延迟完成的结果上下文处理([决策](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md))。
|
||||
|
||||
@@ -151,7 +151,7 @@ idle inject:
|
||||
|
||||
消息使用从可合并扩展的 `ContentBlockMap` 派生的类型化块;同一模式也为 `MessageSource`、`FinishReason`、`TurnTrigger` 和 `TurnEndReason` 定义类型。新增块会协调适配器、UI、压缩、token 计量和持久化;回放计量见 [token-meter.md](core-data-structures/token-meter.md)。
|
||||
|
||||
流式输出使用原始分片和 `BlockAssembler`。每次 `LlmAdapter.stream()` 调用代表一次提供方尝试;适配器报告标准化的故障事实,负责处理的 `agent/request-error` 插件会调用 `agent.retry()`。循环会记录分片、成功结果的来源信息和回放状态。远程适配器使用逐次读取空闲看门狗。回放仅通过共用的适配器实例跨路由传递([契约](core-data-structures/llm-streaming.md))。
|
||||
流式输出使用原始分片和 `BlockAssembler`。每次 `LlmAdapter.stream()` 调用代表一次提供方尝试;适配器报告标准化的故障事实,负责处理的 `agent/request-error` 插件会返回重试动作。循环会记录分片、成功结果的来源信息和回放状态。远程适配器使用逐次读取空闲看门狗。回放仅通过共用的适配器实例跨路由传递([契约](core-data-structures/llm-streaming.md))。
|
||||
|
||||
## 扩展与组合
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Effective broad cancellation was requested, before queued/outbox work is cleared
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentCancelCause](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:314`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:308`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/created` — emit
|
||||
|
||||
@@ -54,7 +54,7 @@ A fully configured agent and live session were published. Setup is composition-o
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:253`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:247`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/disposed` — emit
|
||||
|
||||
@@ -74,7 +74,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence and sco
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:262`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:256`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/error` — emit
|
||||
|
||||
@@ -96,7 +96,7 @@ A step or turn errored. The machine reports a failure here (plus the logger) eve
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:425`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:419`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/inbox/dequeue` — emit
|
||||
|
||||
@@ -117,7 +117,7 @@ The driver claimed one item out of the inbox: a queued item at a turn boundary,
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:292`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:286`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/inbox/discard` — emit
|
||||
|
||||
@@ -140,7 +140,7 @@ Pending inbox items were dropped without delivering them, so every enqueued id r
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:304`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:298`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/inbox/enqueue` — emit
|
||||
|
||||
@@ -162,7 +162,7 @@ An item entered the queued or steering inbox. `placement` is the acceptance-time
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [InboxPlacement](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:282`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:276`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/prompt-submit` — waterfall
|
||||
|
||||
@@ -186,7 +186,7 @@ Allow, rewrite, or block one claimed prompt before it becomes a user message or
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:342`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:336`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request` — waterfall
|
||||
|
||||
@@ -210,18 +210,18 @@ Replace the frozen call configuration. `await next()` yields the config the mach
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:368`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:362`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request-error` — waterfall
|
||||
|
||||
Handle a model-request failure after its failed step has closed but before the failed turn closes. A listener calls Agent.retry to schedule one retry turn, returns without `next()` when it owns the error, or calls `next()` to delegate. The default leaves the failure terminal.
|
||||
Handle a model-request failure after its failed step has closed but before the failed turn closes. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns the error, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Handle a model-request failure after its failed step has closed but
|
||||
* before the failed turn closes. A listener calls {@link Agent.retry} to
|
||||
* schedule one retry turn, returns without `next()` when it owns the error,
|
||||
* or calls `next()` to delegate. The default leaves the failure terminal.
|
||||
* before the failed turn closes. A listener returns `{ kind: 'retry' }`
|
||||
* without calling `next()` when it owns the error, or calls `next()` to
|
||||
* delegate. The default `undefined` leaves the failure terminal.
|
||||
* @param agent - the agent whose request failed.
|
||||
* @param turn - the open turn number.
|
||||
* @param step - the failed step number.
|
||||
@@ -231,12 +231,12 @@ Handle a model-request failure after its failed step has closed but before the f
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'agent/request-error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, signal: AbortSignal, next: () => Promise<void>): Promise<void>
|
||||
'agent/request-error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, signal: AbortSignal, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [LlmFailure](../core-data-structures/llm-streaming.md) · [RequestError](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [LlmFailure](../core-data-structures/llm-streaming.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorAction](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:383`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:377`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/session-start` — emit
|
||||
|
||||
@@ -258,7 +258,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:327`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:321`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/settled` — emit
|
||||
|
||||
@@ -283,7 +283,7 @@ One drain chain reached its terminal turn: that turn's `turn/end` is already com
|
||||
|
||||
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:412`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:406`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/status` — emit
|
||||
|
||||
@@ -303,7 +303,7 @@ Agent status changed (`idle` ⇄ `running`). `send()` does not enter `running` s
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:271`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:265`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/step` — serial
|
||||
|
||||
@@ -327,7 +327,7 @@ Awaited serial checkpoint before EVERY request of a turn is built (the first as
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:355`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:349`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/turn-stopping` — serial
|
||||
|
||||
@@ -353,7 +353,7 @@ The turn is about to close: the model owes no response (no live tool calls, no f
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:398`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:392`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
## `agent-loop/*`
|
||||
|
||||
|
||||
@@ -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
|
||||
compaction.md: 0f2b6a8678b992170a29666fe3ac6e10b1d6ae8e
|
||||
compaction.zh.md: 8fb4b9d1bb25ac7b2ca7fa00f69422d45e8bda3b
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/compaction.md
|
||||
compaction.md: 3ba5edd96c509e064ac7033b175b7ddd3c972452
|
||||
compaction.zh.md: d082b0d0545802500278e96ca41a273bce275f53
|
||||
|
||||
@@ -62,7 +62,7 @@ type CompactionTrigger = 'pressure' | 'context-overflow'
|
||||
|
||||
`CompactService` exposes `compactIfNeeded(agent, trigger, signal)` for automatic `pressure` or `context-overflow` policy, returning `null` when no safe work exists, and `compactRegion(...)` for an explicit inclusive surface range. Every backend marks its replacement `user/message` with the package-exported `COMPACT_CHECKPOINT_SOURCE`; consumers call `isCompactCheckpointSource()` instead of coupling checkpoint recognition to one backend. Implementations must forward the supplied signal to summarization. The seam owns no pricing API: the singleton [`ctx.tokenMeter`](token-meter.md) directly owns estimation and replay, while `dsh-compact-basic` owns retention, event sequencing, routed summarization calls, and their configuration.
|
||||
|
||||
Pressure compaction runs at serial `agent/step` before request derivation. Once pressure or canonical overflow qualifies, compact-basic invokes optional [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md) before range selection, remeasures through `ctx.tokenMeter`, and can advance the surface without a summary. Failed-request recovery runs through `agent/request-error` after the failed step closes and calls `agent.retry()` only when the surface replacement generation advances, even if later summary work throws after pruning; cancellation still wins. Region boundaries preserve tool-call/result pairing but not whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling.
|
||||
Pressure compaction runs at serial `agent/step` before request derivation. Once pressure or canonical overflow qualifies, compact-basic invokes optional [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md) before range selection, remeasures through `ctx.tokenMeter`, and can advance the surface without a summary. Failed-request recovery runs through `agent/request-error` after the failed step closes and returns a retry action only when the surface replacement generation advances, even if later summary work throws after pruning; cancellation still wins. Region boundaries preserve tool-call/result pairing but not whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling.
|
||||
|
||||
The seam exports `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)` for those edge checks. Both validate current surface membership and reject missing seqs and orphan results; the [package contract](../../packages/compact/compact/README.md#tool-pairing-boundaries) owns their cache semantics.
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ type CompactionTrigger = 'pressure' | 'context-overflow'
|
||||
|
||||
`CompactService` 暴露 `compactIfNeeded(agent, trigger, signal)` 以执行自动 `pressure` 或 `context-overflow` 策略;没有可安全执行的工作时返回 `null`。它还针对显式、两端均包含的 surface 范围暴露 `compactRegion(...)`。每个后端都使用包导出的 `COMPACT_CHECKPOINT_SOURCE` 标记其替换用的 `user/message`;消费方调用 `isCompactCheckpointSource()`,而不是把检查点识别逻辑耦合到某一个后端。实现必须把传入的 signal 转发给摘要流程。该 seam 不拥有计价 API:单例 [`ctx.tokenMeter`](token-meter.md) 直接拥有估算与回放,而 `dsh-compact-basic` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。
|
||||
|
||||
压力压缩在串行 `agent/step` 中运行,先于请求推导。一旦压力或规范化溢出满足条件,compact-basic 会在选择范围前调用可选的 [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md),再通过 `ctx.tokenMeter` 重新测量,并且可以在不生成摘要的情况下推进 surface。失败请求的恢复在失败的步骤关闭后通过 `agent/request-error` 运行;仅当 surface replacement generation 前进时才调用 `agent.retry()`,即便后续摘要工作在剪枝后抛异常亦如此;取消仍然优先。区域边界保持工具调用/结果配对,但不保持整个轮次,因此一个过大轮次中较早关闭的步骤可以被压缩。`dsh-compact-basic` 拥有阈值、保留尾部策略、溢出上限与失败处理。
|
||||
压力压缩在串行 `agent/step` 中运行,先于请求推导。一旦压力或规范化溢出满足条件,compact-basic 会在选择范围前调用可选的 [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md),再通过 `ctx.tokenMeter` 重新测量,并且可以在不生成摘要的情况下推进 surface。失败请求的恢复在失败的步骤关闭后通过 `agent/request-error` 运行;仅当 surface replacement generation 前进时才返回重试动作,即便后续摘要工作在剪枝后抛异常亦如此;取消仍然优先。区域边界保持工具调用/结果配对,但不保持整个轮次,因此一个过大轮次中较早关闭的步骤可以被压缩。`dsh-compact-basic` 拥有阈值、保留尾部策略、溢出上限与失败处理。
|
||||
|
||||
该 seam 导出 `toolPairingBalancedBefore(session, seq)` 与 `toolPairingBalancedAfter(session, seq)`,用于这些边缘检查。两者都会验证当前 surface 成员关系,并拒绝缺失的 seq 与遗留结果;其缓存语义由[包契约](../../packages/compact/compact/README.md#tool-pairing-boundaries)规定。
|
||||
|
||||
|
||||
@@ -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: 9d58b81dc595cf18591d69b78b6815b8f3249153
|
||||
core.zh.md: 91e6df2c1a8d84b3a4a573a98403d69692cd04d5
|
||||
core.md: 357712bd197ac2e0661e6bc61a638aa8a4738356
|
||||
core.zh.md: dcb7210d37377da99ac2cd68b1ce18fa6e90e0b8
|
||||
|
||||
@@ -586,15 +586,6 @@ interface Agent {
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
inject(input: UserMessageData): AgentMessageId
|
||||
|
||||
/**
|
||||
* Re-open a turn on the current session log without a new prompt — the
|
||||
* explicit resummon verb. During `agent/request-error`, this schedules one
|
||||
* retry turn after the failed turn closes; while idle, it starts one
|
||||
* immediately. Repeated calls before the scheduled retry coalesce.
|
||||
* @throws while other agent work is running.
|
||||
*/
|
||||
retry(): void
|
||||
}
|
||||
```
|
||||
|
||||
@@ -628,7 +619,12 @@ type PromptDecision =
|
||||
| { kind: 'block'; reason: string }
|
||||
```
|
||||
|
||||
`agent/request-error` runs after a failed model step closes and before its turn closes. Listeners can repair durable state or await policy work while the failed turn's signal is still live. A handling listener calls `agent.retry()` and returns without `next()`; repeated calls coalesce into one retry turn.
|
||||
`agent/request-error` runs after a failed model step closes and before its turn closes. Listeners can repair durable state or await policy work while the failed turn's signal is still live. A handling listener returns `{ kind: 'retry' }` without calling `next()`; the default `undefined` leaves the failure terminal.
|
||||
|
||||
```ts type-equiv
|
||||
/** Action returned by a listener that owns model-request recovery. */
|
||||
type RequestErrorAction = { kind: 'retry' } | undefined
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Model-request failure with an optional machine-routable provider code. */
|
||||
|
||||
@@ -594,15 +594,6 @@ interface Agent {
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
inject(input: UserMessageData): AgentMessageId
|
||||
|
||||
/**
|
||||
* Re-open a turn on the current session log without a new prompt — the
|
||||
* explicit resummon verb. During `agent/request-error`, this schedules one
|
||||
* retry turn after the failed turn closes; while idle, it starts one
|
||||
* immediately. Repeated calls before the scheduled retry coalesce.
|
||||
* @throws while other agent work is running.
|
||||
*/
|
||||
retry(): void
|
||||
}
|
||||
```
|
||||
|
||||
@@ -636,7 +627,12 @@ type PromptDecision =
|
||||
| { kind: 'block'; reason: string }
|
||||
```
|
||||
|
||||
`agent/request-error` 在失败的模型步骤关闭之后、其轮次关闭之前运行。listener 可以在失败轮次的 signal 仍然存活时修复持久状态或 await 策略工作。处理该错误的 listener 调用 `agent.retry()` 并直接返回而不调用 `next()`;重复调用会合并成一个重试轮次。
|
||||
`agent/request-error` 在失败的模型步骤关闭之后、其轮次关闭之前运行。listener 可以在失败轮次的 signal 仍然存活时修复持久状态或 await 策略工作。处理该错误的 listener 返回 `{ kind: 'retry' }` 且不调用 `next()`;默认的 `undefined` 会让失败保持终态。
|
||||
|
||||
```ts type-equiv
|
||||
/** Action returned by a listener that owns model-request recovery. */
|
||||
type RequestErrorAction = { kind: 'retry' } | undefined
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Model-request failure with an optional machine-routable provider code. */
|
||||
|
||||
@@ -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/llm-streaming.md
|
||||
llm-streaming.md: 1109c02ab08885bd0b00cd52db3b313aff578b5f
|
||||
llm-streaming.zh.md: 196b1ce6a18f48ec50c92bee922d05e75f18760c
|
||||
llm-streaming.md: a6aaaf3fed2ab25821efdf308c7297526ee7f3b5
|
||||
llm-streaming.zh.md: 521df7d1dad620cea322865fbefa8e4e2615fa78
|
||||
|
||||
@@ -59,7 +59,7 @@ Every adapter MUST obey these, and every consumer may rely on them:
|
||||
|
||||
- **`usage` before `finish`, nothing after `finish`.** Defer both to the provider's end-of-stream marker so a trailing usage-only chunk can't violate the ordering.
|
||||
- **Tool-call `arguments` stay raw JSON strings end-to-end.** Partial fragments stream via `argumentsDelta`; a provider that hands back parsed objects re-stringifies at `block-end`.
|
||||
- **Two sanctioned error paths, one fact shape.** A failure may either THROW from `stream()` (transport/protocol errors) **or** end the stream with `finish {kind:'error'|'aborted', failure}` (provider in-band errors, for adapters that can't throw mid-stream). `LlmError.failure` carries the same `LlmFailure`. The final adapter boundary preserves the exact thrown `Error` object and associates immutable facts with that call; the agent loop closes the failed step and offers the error and facts to `agent/request-error`. A handling listener calls `agent.retry()` after its awaited repair; absent recovery the structured failure becomes the turn error, and no normal assistant message or tool side effect is committed for that attempt.
|
||||
- **Two sanctioned error paths, one fact shape.** A failure may either THROW from `stream()` (transport/protocol errors) **or** end the stream with `finish {kind:'error'|'aborted', failure}` (provider in-band errors, for adapters that can't throw mid-stream). `LlmError.failure` carries the same `LlmFailure`. The final adapter boundary preserves the exact thrown `Error` object and associates immutable facts with that call; the agent loop closes the failed step and offers the error and facts to `agent/request-error`. A handling listener returns `{ kind: 'retry' }` after its awaited repair; absent recovery the structured failure becomes the turn error, and no normal assistant message or tool side effect is committed for that attempt.
|
||||
- **One adapter call is one provider attempt.** Adapters disable library retries. Agent-level recovery opens another durable numbered step; direct `ctx.llm.stream()` callers remain single-attempt.
|
||||
- **Provider stalls are bounded at the transport.** Both shipping remote adapters expose positive finite `streamIdleTimeoutMs` with a five-minute default. The watchdog arms only while iterator `next()` is outstanding, uses one stable signal for the whole request, maps its own expiry to `TIMEOUT`, and keeps an earlier caller abort as `ABORTED`.
|
||||
- **Context overflow has one canonical code.** Both DeepSeek adapters classify explicit provider detail through `isContextWindowExceededError()` and surface `CONTEXT_WINDOW_EXCEEDED`, whether the failure arrives as a thrown HTTP `LlmError` or an in-band finish error. Consumers route on the code, never provider text.
|
||||
|
||||
@@ -59,7 +59,7 @@ interface LlmFailure {
|
||||
|
||||
- **`usage` 在 `finish` 之前,`finish` 之后不再有任何分片。** 将两者都推迟到提供方的流结束标记,这样尾部的 usage-only 分片就不会违反顺序。
|
||||
- **工具调用的 `arguments` 全程保持原始 JSON 字符串。** 部分片段通过 `argumentsDelta` 流式传输;如果提供方返回的是已解析的对象,适配器在 `block-end` 时重新序列化为字符串。
|
||||
- **两条受支持的错误路径,一种事实形状。** 失败可以从 `stream()` 抛出(传输/协议错误),**或者**以 `finish {kind:'error'|'aborted', failure}` 结束流(无法在流中途抛异常的适配器用它表示提供方带内错误)。`LlmError.failure` 携带同一个 `LlmFailure`。最终适配器边界保留被抛出的确切 `Error` 对象,并将不可变事实关联到该调用;agent loop(智能体循环)关闭失败的步骤,再把错误与事实提供给 `agent/request-error`。处理该错误的 listener 在其 await 的修复完成后调用 `agent.retry()`;若未恢复,结构化失败会成为轮次错误,并且该次尝试不会提交正常 assistant 消息或工具副作用。
|
||||
- **两条受支持的错误路径,一种事实形状。** 失败可以从 `stream()` 抛出(传输/协议错误),**或者**以 `finish {kind:'error'|'aborted', failure}` 结束流(无法在流中途抛异常的适配器用它表示提供方带内错误)。`LlmError.failure` 携带同一个 `LlmFailure`。最终适配器边界保留被抛出的确切 `Error` 对象,并将不可变事实关联到该调用;agent loop(智能体循环)关闭失败的步骤,再把错误与事实提供给 `agent/request-error`。处理该错误的 listener 在其 await 的修复完成后返回 `{ kind: 'retry' }`;若未恢复,结构化失败会成为轮次错误,并且该次尝试不会提交正常 assistant 消息或工具副作用。
|
||||
- **一次适配器调用就是一次提供方尝试。** 适配器禁用库重试。agent 层恢复会打开另一个持久、带编号的步骤;直接调用 `ctx.llm.stream()` 的调用方仍然只尝试一次。
|
||||
- **提供方停顿在传输层受到时限约束。** 两个已交付的远程适配器都暴露正数且有限的 `streamIdleTimeoutMs`,默认五分钟。watchdog 只在 iterator `next()` 尚未完成时启动,整个请求使用同一个稳定 signal,把自身到期映射为 `TIMEOUT`,并把更早发生的调用方中止保留为 `ABORTED`。
|
||||
- **上下文溢出只有一个规范 code。** 两个 DeepSeek 适配器都通过 `isContextWindowExceededError()` 对提供方的显式细节分类并暴露 `CONTEXT_WINDOW_EXCEEDED`,无论失败以抛出的 HTTP `LlmError` 还是带内 finish error 到达。消费方按 code 路由,绝不依赖提供方文本。
|
||||
|
||||
@@ -8,21 +8,21 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:140`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`tui`](../packages/ui/tui) |
|
||||
| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:314`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal-session`](../packages/goal/goal-session) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:253`](../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:262`](../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:425`](../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/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:292`](../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:304`](../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:282`](../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/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:342`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`tui`](../packages/ui/tui) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:368`](../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:383`](../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:327`](../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:412`](../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:271`](../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:355`](../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/turn-stopping` | `serial` | [`packages/core/agent/src/types.ts:398`](../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/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:308`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal-session`](../packages/goal/goal-session) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:247`](../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:256`](../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:419`](../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/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:286`](../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:298`](../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:276`](../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/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:336`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`tui`](../packages/ui/tui) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:362`](../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:377`](../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:321`](../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:406`](../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:265`](../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:349`](../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/turn-stopping` | `serial` | [`packages/core/agent/src/types.ts:392`](../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) |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -78,9 +78,9 @@ interface SessionRecord {
|
||||
reject: (error: Error) => void
|
||||
turn: number | undefined
|
||||
/**
|
||||
* A failed turn's terminal reason, held until quiescence: a retry turn
|
||||
* (`agent.retry()` closes the failed turn and opens a successor) adopts
|
||||
* the prompt instead, so rejecting at `turn/end` would race the recovery.
|
||||
* A failed turn's terminal reason, held until quiescence: a retry action
|
||||
* closes the failed turn and opens a successor that adopts the prompt, so
|
||||
* rejecting at `turn/end` would race the recovery.
|
||||
*/
|
||||
pendingError: Extract<TurnEndReason, { kind: 'error' }> | undefined
|
||||
} | undefined
|
||||
@@ -172,7 +172,7 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
}
|
||||
} else if (inflight !== undefined && event.type === 'turn/end' && inflight.turn === event.data.turn) {
|
||||
if (event.data.reason.kind === 'error') {
|
||||
// Hold the rejection: agent.retry() may adopt the prompt with a
|
||||
// Hold the rejection: request recovery may adopt the prompt with a
|
||||
// successor turn; quiescence without one delivers this error.
|
||||
inflight.turn = undefined
|
||||
inflight.pendingError = event.data.reason
|
||||
|
||||
@@ -171,10 +171,10 @@ describe('ACP prompt lifecycle', () => {
|
||||
harness = await makeBridgeHarness({ script: [errorResponse('transient boom'), textResponse('recovered')] })
|
||||
// A recovery policy: schedule one retry for the failed request.
|
||||
let retried = false
|
||||
harness.ctx.on('agent/request-error', async (subject) => {
|
||||
harness.ctx.on('agent/request-error', async (_subject) => {
|
||||
if (!retried) {
|
||||
retried = true
|
||||
subject.retry()
|
||||
return { kind: 'retry' }
|
||||
}
|
||||
})
|
||||
const sessionId = await newSession(harness)
|
||||
|
||||
@@ -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: 6881b190b96b0be41579cb448c552864d4501268
|
||||
README.zh.md: 840fc0e4e915d4af5185ccc399cd5485cb5bd46d
|
||||
# pnpm run verify-translation-pairing --write packages/compact/compact-basic/README.md
|
||||
README.md: 775355f1ac1a7c79c16f66a5b2489d73df7b960d
|
||||
README.zh.md: 2f7ccc7dd00fa5599d3d6bbe66e81d3312d38dd9
|
||||
|
||||
@@ -17,7 +17,7 @@ This backend owns the compaction policy:
|
||||
- **Convergence** — retry head-checkpoint compaction up to `compactionRetries`; reject a summary that does not shrink its source, and throw if retries cannot return below threshold.
|
||||
- **Summarization** — a direct `llm/stream` call uses the configured provider/model pair and cap, falling back to the latest logged request target and then the agent target, without running the loop-only `agent/request` seam. The call replays the conversation's own system prompt, tools, and shadowed-region messages verbatim and appends the compaction instruction as the final user message, so it reuses the provider's warm prefix cache instead of invalidating it. It sets `GenerateOptions.purpose` to `compaction`, which adapters may forward as request attribution (the DeepSeek adapter sends `x-deepseek-harness-compact: 1`) without touching the model-visible body. Only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call.
|
||||
- **Framing** — the replacement user message marks established checkpoint context with `<compacted-summary>` tags. The raw summary remains on the provenance event, and later automatic cycles merge the prior checkpoint.
|
||||
- **Lifecycle** — `compactRegion()` mutates `agent.session` and records its start, summary, replacement, and end. After asynchronous summarization it rejects a changed surface-node snapshot, while unrelated log-only events may append without invalidating the selected span. The serial `agent/step` listener checks pressure before request derivation. A canonical provider overflow is offered through `agent/request-error` after the failed step; the plugin compacts there and calls `agent.retry()` only after durable surface progress.
|
||||
- **Lifecycle** — `compactRegion()` mutates `agent.session` and records its start, summary, replacement, and end. After asynchronous summarization it rejects a changed surface-node snapshot, while unrelated log-only events may append without invalidating the selected span. The serial `agent/step` listener checks pressure before request derivation. A canonical provider overflow is offered through `agent/request-error` after the failed step; the plugin compacts there and returns a retry action only after durable surface progress.
|
||||
- **Overflow recovery** — provider-confirmed overflow needs no capacity metadata: it bypasses normal pressure and retention, prunes, then attempts one maximal balanced head reduction while leaving the newest indivisible unit. Retry is authorized whenever `surface.replaceGeneration` advances, including when pruning lands before later summary work throws. No replacement, an exhausted target-specific cap, cancellation, or an unknown/noncanonical error preserves the original provider failure.
|
||||
- **Failure handling** — an unmatched `compact/start` is an inert crash marker because no summary replacement landed. A region failure records an error end; the surface remains unchanged unless pruning already landed. Operational pressure failures warn and continue, while overflow-recovery failure preserves the original provider error only when no earlier replacement advanced the surface. Cancellation remains authoritative after any progress.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- **收敛**:最多按 `compactionRetries` 重试头部检查点压缩;拒绝不能缩小源内容的摘要,如果重试仍无法回到阈值以下,则抛出异常。
|
||||
- **摘要**:直接 `llm/stream` 调用使用已配置的提供方/模型对与上限,回退到最新已记录请求目标,然后再回退到 agent(智能体)目标,而不运行仅用于 loop 的 `agent/request` seam。该调用会逐字回放会话自身的系统提示词、工具与已遮蔽区域消息,并将压缩指令作为最后一条 user 消息追加,从而复用提供方的热前缀 cache,而非使它失效。它将 `GenerateOptions.purpose` 设为 `compaction`,适配器可将其作为请求归因转发(DeepSeek 适配器发送 `x-deepseek-harness-compact: 1`),但不会触碰模型可见主体。只有返回文本会进入检查点;会排除可能泄露私有推理或产生遗留调用的 reasoning 与工具调用。
|
||||
- **框定**:替换 user 消息使用 `<compacted-summary>` 标签标记已建立的检查点上下文。原始摘要保留在溯源事件上,后续自动周期会合并之前的检查点。
|
||||
- **生命周期**:`compactRegion()` 会更改 `agent.session`,并记录开始、摘要、替换与结束。异步摘要后,它会拒绝已改变的表层节点快照,而不相关的仅日志事件可以追加,不会使已选 span 失效。串行 `agent/step` listener 会在派生请求之前检查压力。规范提供方溢出会在失败步骤之后经由 `agent/request-error` 交给本插件;插件在此执行压缩,并且只在表层取得持久进展后才调用 `agent.retry()`。
|
||||
- **生命周期**:`compactRegion()` 会更改 `agent.session`,并记录开始、摘要、替换与结束。异步摘要后,它会拒绝已改变的表层节点快照,而不相关的仅日志事件可以追加,不会使已选 span 失效。串行 `agent/step` listener 会在派生请求之前检查压力。规范提供方溢出会在失败步骤之后经由 `agent/request-error` 交给本插件;插件在此执行压缩,并且只在表层取得持久进展后才返回重试动作。
|
||||
- **溢出恢复**:提供方已确认的溢出不需容量元数据。它会绕过常规压力与保留,执行剪枝,再尝试一次最大平衡头部缩减,并留下最新不可分单元。只要 `surface.replaceGeneration` 前进,就允许重试,包括剪枝在后续摘要工作抛出异常前已落地的情况。如果没有替换、精确目标上限已耗尽、已取消,或遇到未知/非规范错误,则保留原始提供方失败。
|
||||
- **失败处理**:不匹配的 `compact/start` 是惰性崩溃标记,因为没有摘要替换落地。区域失败会记录错误结束;除非剪枝已落地,否则表层保持不变。操作性压力失败会发出警告并继续;只当之前没有替换使表层前进时,溢出恢复失败才保留原始提供方错误。取消在任何进展后仍具有最高权威。
|
||||
|
||||
|
||||
@@ -200,8 +200,7 @@ export class BasicCompactService extends CompactService {
|
||||
+ 'retrying from the replacement surface',
|
||||
)
|
||||
this.overflowRetries.set(agent, retries + 1)
|
||||
agent.retry()
|
||||
return
|
||||
return { kind: 'retry' }
|
||||
}
|
||||
ctx.logger.warn(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the signal can abort while recovery is awaited.
|
||||
@@ -216,7 +215,7 @@ export class BasicCompactService extends CompactService {
|
||||
|| agent.session.surface.replaceGeneration <= generation) return next()
|
||||
if (result !== null) logResult(result, 'context overflow recovery')
|
||||
this.overflowRetries.set(agent, retries + 1)
|
||||
agent.retry()
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { agentEvents, type Agent, type RequestErrorAction } from '@deepseek-ai/dsh-agent'
|
||||
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
|
||||
const SIGNAL = new AbortController().signal
|
||||
@@ -79,7 +79,6 @@ function agent(session: Session, model?: string): Agent {
|
||||
return {
|
||||
session,
|
||||
options: model === undefined ? {} : { provider: model, model },
|
||||
retry() {},
|
||||
} as Agent
|
||||
}
|
||||
|
||||
@@ -1292,15 +1291,13 @@ describe('automatic listener and loader composition', () => {
|
||||
owner: Agent,
|
||||
error: Error & { code?: string },
|
||||
signal = SIGNAL,
|
||||
next: () => Promise<void> = () => Promise.resolve(),
|
||||
next: () => Promise<RequestErrorAction> = () => Promise.resolve(undefined),
|
||||
): Promise<boolean> {
|
||||
const failure: LlmFailure = { message: error.message, code: error.code ?? 'UNKNOWN' }
|
||||
const turn = owner.session.events.findLast(event => event.type === 'turn/start')?.data.turn ?? 1
|
||||
let retried = false
|
||||
owner.retry = () => { retried = true }
|
||||
return agentEvents(ctx, owner).waterfall(
|
||||
'agent/request-error', turn, 1, error, failure, signal, next,
|
||||
).then(() => retried)
|
||||
).then(action => action?.kind === 'retry')
|
||||
}
|
||||
|
||||
function overflow(message = 'provider overflow'): Error & { code: string } {
|
||||
@@ -1584,7 +1581,7 @@ describe('automatic listener and loader composition', () => {
|
||||
|
||||
const decision = await recover(ctx, agent(session, MODEL), original, SIGNAL, () => {
|
||||
delegations += 1
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(undefined)
|
||||
})
|
||||
|
||||
expect(decision).toBe(false)
|
||||
|
||||
@@ -51,7 +51,6 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
|
||||
},
|
||||
send: () => AgentMessageId('stub'),
|
||||
cancel() {},
|
||||
retry() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
|
||||
},
|
||||
send: () => AgentMessageId('stub'),
|
||||
cancel() {},
|
||||
retry() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1047,8 +1047,8 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
{
|
||||
name: 'agent/request-error',
|
||||
mode: 'waterfall',
|
||||
signature: '\'agent/request-error\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, signal: AbortSignal, next: () => Promise<void>): Promise<void>',
|
||||
jsDoc: '/**\n * Handle a model-request failure after its failed step has closed but\n * before the failed turn closes. A listener calls {@link Agent.retry} to\n * schedule one retry turn, returns without `next()` when it owns the error,\n * or calls `next()` to delegate. The default leaves the failure terminal.\n * @param agent - the agent whose request failed.\n * @param turn - the open turn number.\n * @param step - the failed step number.\n * @param error - the original model-request failure.\n * @param failure - serializable facts normalized at the final adapter boundary.\n * @param signal - the turn abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */',
|
||||
signature: '\'agent/request-error\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, signal: AbortSignal, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>',
|
||||
jsDoc: '/**\n * Handle a model-request failure after its failed step has closed but\n * before the failed turn closes. A listener returns `{ kind: \'retry\' }`\n * without calling `next()` when it owns the error, or calls `next()` to\n * delegate. The default `undefined` leaves the failure terminal.\n * @param agent - the agent whose request failed.\n * @param turn - the open turn number.\n * @param step - the failed step number.\n * @param error - the original model-request failure.\n * @param failure - serializable facts normalized at the final adapter boundary.\n * @param signal - the turn abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */',
|
||||
summary: 'Handle a model-request failure after its failed step has closed but before the failed turn closes.',
|
||||
},
|
||||
{
|
||||
@@ -1330,7 +1330,7 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
{
|
||||
name: 'Agent',
|
||||
declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly acceptsNextStep: boolean;\n readonly ctx: Context;\n send(input: UserMessageData, options: SendOptions): AgentMessageId;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise<void>;\n followup(input: UserMessageData): AgentMessageId;\n steer(input: UserMessageData): AgentMessageId;\n inject(input: UserMessageData): AgentMessageId;\n retry(): void;\n}',
|
||||
declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly acceptsNextStep: boolean;\n readonly ctx: Context;\n send(input: UserMessageData, options: SendOptions): AgentMessageId;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise<void>;\n followup(input: UserMessageData): AgentMessageId;\n steer(input: UserMessageData): AgentMessageId;\n inject(input: UserMessageData): AgentMessageId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'AgentCancelCause',
|
||||
|
||||
@@ -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: 25fa42ce09a7ad6463e9c52c08bf718d1f55adc1
|
||||
README.zh.md: 0366ce01f8d093dcc142c0ae25cfdfd7b6effbc2
|
||||
README.md: ab6df4d49f05ff00b16a210830e7fd21504a0a9f
|
||||
README.zh.md: 5adb2a11ba3de2bb6fc7ff57b9d6dd07ac7f650e
|
||||
|
||||
@@ -64,7 +64,7 @@ Every provider call that reaches a successful finish appends exactly one `assist
|
||||
|
||||
After `agent/request` returns a provider/model call config, the loop asks `ctx.llm.prepareCall()` to validate any adapter-owned reasoning effort and materialize its configured default under the active turn signal. The prepared call retains the exact adapter registration across this asynchronous resolution, `request/header` logging, and terminal dispatch, so HMR cannot mix one adapter's capability result with another adapter's request. The effective config is logged before dispatch, so a listener can change effort between steps without hidden request drift. A route with no registered adapter preserves the proposed config so an `llm/stream` listener can own and short-circuit it; unhandled terminal dispatch still fails with `NO_ADAPTER`. A new loop instance restores the last effort only when its initial provider/model route exactly matches the logged route; a route change discards that opaque model-owned ID and resolves the new model independently.
|
||||
|
||||
Plugin failure ends the current turn, not the loop. A model-request failure first closes its step and enters `agent/request-error` with the exact live error, normalized provider facts, and the turn signal. A handling listener calls `agent.retry()`; the loop coalesces repeated calls, closes the failed turn with its error, and opens one numbered retry turn without an intervening idle notification. An unhandled failure is terminal. Other failures close directly. AgentLoop owns one cancellation signal for the current admission or turn. An effective `cancel(cause)` clears pending work unless `keepInbox` is set and cooperatively aborts that signal; idle cancellation is a no-op. Durable `turn/end` records `aborted` for `user` and `parent`, while disposal records `disposed`; undispatched model tool calls receive synthetic `tool/call` and `ABORTED_BEFORE_DISPATCH` result pairs. The cancellation cause changes reporting, not how result context finalized after cancellation is handled. Disposal waits for signal-ignoring work before registry removal. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) owns the lifecycle and race contract.
|
||||
Plugin failure ends the current turn, not the loop. A model-request failure first closes its step and enters `agent/request-error` with the exact live error, normalized provider facts, and the turn signal. A handling listener returns `{ kind: 'retry' }`; the loop closes the failed turn with its error and opens one numbered retry turn without an intervening idle notification. An unhandled failure is terminal. Other failures close directly. AgentLoop owns one cancellation signal for the current admission or turn. An effective `cancel(cause)` clears pending work unless `keepInbox` is set and cooperatively aborts that signal; idle cancellation is a no-op. Durable `turn/end` records `aborted` for `user` and `parent`, while disposal records `disposed`; undispatched model tool calls receive synthetic `tool/call` and `ABORTED_BEFORE_DISPATCH` result pairs. The cancellation cause changes reporting, not how result context finalized after cancellation is handled. Disposal waits for signal-ignoring work before registry removal. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) owns the lifecycle and race contract.
|
||||
|
||||
Within a step, exclusive calls form barriers; parallel-safe calls use a bounded rolling pool and are reclassified before start. Only dispatch/body overlaps. Policy, durable results, and result context remain model-ordered. Abort stops new calls, drains started results, and retains their finalized result context without distinguishing the cancellation cause.
|
||||
|
||||
@@ -73,7 +73,7 @@ Within a step, exclusive calls form barriers; parallel-safe calls use a bounded
|
||||
Everything that goes beyond "call the model, run the tools, repeat" belongs to plugins listening on the event taxonomy:
|
||||
- Hooks and policy: the relevant `agent/*` checkpoints plus the guarded `tools/pre-execute` → `tools/execute` → `tools/post-execute` → definition-owned `finalizeContent` → `tools/result` pipeline; exact event signatures and modes live in the [generated event catalog](../../../docs/cordis-catalog/events.md)
|
||||
- Compaction: pressure on `agent/step`; canonical overflow repair on `agent/request-error`
|
||||
- Transient model recovery: `dsh-llm-retry` records and waits its finite backoff on `agent/request-error`, then calls `agent.retry()`
|
||||
- Transient model recovery: `dsh-llm-retry` records and waits its finite backoff on `agent/request-error`, then returns a retry action
|
||||
- Sandbox, permission, plan mode: `tools/pre-execute` for extensible deny/ask, `tools.guard()` for monotonic owner policy, `tools/post-execute` for result decisions, and `tools/result` for final observation
|
||||
- Sub-agents: implemented outside the loop as `ctx.subagents` providers; in-process providers use `ctx.agents.create()` and owned `AgentHandle` teardown, while generic [`ctx.tasks`](../../tasks/tasks/) plus [`dsh-tool-subagent`](../../subagent/tool-subagent/) own background collection.
|
||||
- Persistence: eager write-behind from `session/event`; `session/flush` is an explicit observation barrier
|
||||
|
||||
@@ -64,7 +64,7 @@ interface Config {
|
||||
|
||||
在 `agent/request` 返回提供方/模型调用配置后,循环会调用 `ctx.llm.prepareCall()`,在活跃轮次信号的控制下校验由适配器持有的推理(reasoning)强度,并填入其配置默认值。准备完成的调用会在这次异步解析、`request/header` 日志记录和最终分派期间保留同一项确切的适配器注册,因此 HMR(热模块替换)不会把某个适配器的能力解析结果与另一适配器的请求混用。生效配置会在分派前写入日志,因此监听器可以在步骤之间更改推理强度,而不会产生未记录的请求变化。没有已注册适配器的路由会保留原定配置,使 `llm/stream` 监听器可以接管并短路该请求;最终分派仍会以 `NO_ADAPTER` 拒绝未得到处理的路由。新循环实例仅在初始提供方/模型路由与日志路由完全一致时恢复上次的推理强度;路由变化会丢弃由前一模型持有的不透明 ID,并单独解析新模型。
|
||||
|
||||
插件失败会结束当前轮次,而不是结束循环。模型请求失败会先关闭其步骤,再带着确切的实时错误、规范化的提供方事实和轮次信号进入 `agent/request-error`。处理失败的监听器调用 `agent.retry()`;循环会合并重复调用,用其错误关闭失败轮次,并在不插入空闲通知的情况下开启一个编号重试轮次。未被处理的失败是终态。其他失败直接关闭轮次。AgentLoop 为当前接纳或轮次拥有一个取消信号。有效的 `cancel(cause)` 在未设置 `keepInbox` 时清除待处理工作,并以协作方式中止该信号;空闲取消是空操作。持久 `turn/end` 为 `user` 和 `parent` 记录 `aborted`,dispose 则记录 `disposed`;未分发的模型工具调用会收到合成的 `tool/call` 与 `ABORTED_BEFORE_DISPATCH` 结果对。取消原因只改变报告方式,不改变对取消后已定案结果上下文的处理。Dispose 会等待忽略信号的工作完成,然后才从注册表移除。[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)规定生命周期与竞态契约。
|
||||
插件失败会结束当前轮次,而不是结束循环。模型请求失败会先关闭其步骤,再带着确切的实时错误、规范化的提供方事实和轮次信号进入 `agent/request-error`。处理失败的监听器返回 `{ kind: 'retry' }`;循环用其错误关闭失败轮次,并在不插入空闲通知的情况下开启一个编号重试轮次。未被处理的失败是终态。其他失败直接关闭轮次。AgentLoop 为当前接纳或轮次拥有一个取消信号。有效的 `cancel(cause)` 在未设置 `keepInbox` 时清除待处理工作,并以协作方式中止该信号;空闲取消是空操作。持久 `turn/end` 为 `user` 和 `parent` 记录 `aborted`,dispose 则记录 `disposed`;未分发的模型工具调用会收到合成的 `tool/call` 与 `ABORTED_BEFORE_DISPATCH` 结果对。取消原因只改变报告方式,不改变对取消后已定案结果上下文的处理。Dispose 会等待忽略信号的工作完成,然后才从注册表移除。[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)规定生命周期与竞态契约。
|
||||
|
||||
在步骤内,独占调用形成屏障;并行安全调用使用有界滚动池,并在启动前重新分类。只有分发/主体会重叠。策略、持久结果和结果上下文仍保持模型顺序。中止会停止新调用,drain 已启动的结果,并保留其已定案的结果上下文,不区分取消原因。
|
||||
|
||||
@@ -73,7 +73,7 @@ interface Config {
|
||||
超出「调用模型、运行工具、重复」的所有内容,都属于监听事件分类体系的插件:
|
||||
- 钩子与策略:相关的 `agent/*` 检查点,加上受守卫保护的 `tools/pre-execute` → `tools/execute` → `tools/post-execute` → 定义拥有的 `finalizeContent` → `tools/result` 流水线;确切事件签名与 mode 位于生成的[事件目录](../../../docs/cordis-catalog/events.md)
|
||||
- 压缩(compaction):在 `agent/step` 上观测压力;在 `agent/request-error` 上修复规范溢出
|
||||
- 瞬时模型恢复:`dsh-llm-retry` 在 `agent/request-error` 上记录并等待其有限退避,然后调用 `agent.retry()`
|
||||
- 瞬时模型恢复:`dsh-llm-retry` 在 `agent/request-error` 上记录并等待其有限退避,然后返回重试动作
|
||||
- 沙箱、权限、计划模式:使用 `tools/pre-execute` 提供可扩展的拒绝/询问,使用 `tools.guard()` 提供单调拥有方策略,使用 `tools/post-execute` 处理结果决定,并使用 `tools/result` 进行最终观测
|
||||
- subagent:在循环外部实现为 `ctx.subagents` 提供方;进程内提供方使用 `ctx.agents.create()` 和拥有的 `AgentHandle` 进行 teardown,而通用的 [`ctx.tasks`](../../tasks/tasks/) 与 [`dsh-tool-subagent`](../../subagent/tool-subagent/) 负责后台收集。
|
||||
- 持久化:从 `session/event` 立即后写;`session/flush` 是显式观测屏障
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
SettleReason,
|
||||
PromptDecision,
|
||||
RequestError,
|
||||
RequestErrorAction,
|
||||
SendOptions,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import {
|
||||
@@ -58,8 +59,6 @@ export class ReactLoopAgent implements Agent {
|
||||
acceptsNextStep = false
|
||||
/** Abort owner for the current admission or turn. */
|
||||
private abort: AbortController | undefined
|
||||
/** Coalesced retry capability scoped to the active request-error waterfall. */
|
||||
private retryWindow: { requested: boolean } | undefined
|
||||
/** Resolves when the current admission and turn exit. */
|
||||
done: Promise<void> = Promise.resolve()
|
||||
|
||||
@@ -180,25 +179,10 @@ export class ReactLoopAgent implements Agent {
|
||||
this.outbox.length = 0
|
||||
if (discarded.length > 0) emitAgentEvent(this.loopCtx, this, 'agent/inbox/discard', discarded)
|
||||
}
|
||||
if (this.retryWindow !== undefined) this.retryWindow.requested = false
|
||||
const reason = Object.freeze({ kind: cause.kind })
|
||||
this.abort?.abort(reason)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-open a turn on the current session log without a new prompt — the
|
||||
* recovery verb. A request-error listener schedules the retry that follows
|
||||
* its failed turn; an idle caller starts one immediately.
|
||||
*/
|
||||
retry(): void {
|
||||
if (this.abort !== undefined) {
|
||||
if (this.retryWindow === undefined) throw new Error(`agent "${this.id}" cannot retry while busy`)
|
||||
if (!this.abort.signal.aborted) this.retryWindow.requested = true
|
||||
return
|
||||
}
|
||||
this.done = this.loopCtx.agents.withInitiator(this, () => this.run({ kind: 'retry' }))
|
||||
}
|
||||
|
||||
/** Resolve at idle quiescence: no run driving and no waking prompt waiting. */
|
||||
async whenIdle(): Promise<void> {
|
||||
// `done` is replaced per activity, so re-reading it follows chained turns.
|
||||
@@ -281,7 +265,7 @@ export class ReactLoopAgent implements Agent {
|
||||
}
|
||||
}
|
||||
|
||||
// cancel() aborts but never clears the slot, and kick()/run()/retry()
|
||||
// cancel() aborts but never clears the slot, and kick()/run()
|
||||
// all refuse to install a new owner while one exists, so the admission
|
||||
// still owns the slot here and releasing it unconditionally is exact.
|
||||
this.abort = undefined
|
||||
@@ -321,16 +305,12 @@ export class ReactLoopAgent implements Agent {
|
||||
inheritedOutboxLength = 0,
|
||||
): Promise<void> {
|
||||
// Both entries hold the invariant: kick() clears the admission slot before
|
||||
// awaiting run(), and retry() returns early whenever a slot owner exists.
|
||||
// awaiting run(), and a retry is entered only after the prior run clears it.
|
||||
/* v8 ignore next -- unreachable guard: every caller clears or checks the abort slot first */
|
||||
if (this.abort !== undefined) throw new Error(`agent "${this.id}" is already running`)
|
||||
const controller = new AbortController()
|
||||
this.abort = controller
|
||||
this.acceptsNextStep = true
|
||||
if (!this.busy) {
|
||||
this.busy = true
|
||||
emitAgentEvent(this.loopCtx, this, 'agent/status', 'running')
|
||||
}
|
||||
const signal = controller.signal
|
||||
const turn = this.lastTurn + 1
|
||||
let step = 0
|
||||
@@ -378,30 +358,18 @@ export class ReactLoopAgent implements Agent {
|
||||
this.stepOpen = false
|
||||
this.session.append('step/end', { turn, step })
|
||||
if (!signal.aborted) {
|
||||
const retryWindow = { requested: false }
|
||||
this.retryWindow = retryWindow
|
||||
let recoveryCompleted = false
|
||||
try {
|
||||
await this.loopCtx.waterfall(
|
||||
const action = await this.loopCtx.waterfall(
|
||||
agentCarrier(this), 'agent/request-error', this, turn, step, outcome.error,
|
||||
outcome.failure, signal,
|
||||
() => Promise.resolve(),
|
||||
() => Promise.resolve<RequestErrorAction>(undefined),
|
||||
)
|
||||
recoveryCompleted = true
|
||||
retry = action?.kind === 'retry' && !signal.aborted
|
||||
} catch (recoveryError: unknown) {
|
||||
this.loopCtx.logger.warn(
|
||||
`agent "${this.id}": request recovery failed at turn ${turn}, step ${step}: ${errorChain(recoveryError)}`,
|
||||
)
|
||||
} finally {
|
||||
// Nothing else writes the window while the waterfall runs:
|
||||
// cancel() only flips `requested` and a second run cannot
|
||||
// start, so unconditional retirement is exact.
|
||||
this.retryWindow = undefined
|
||||
}
|
||||
// A requested retry implies the signal is still live: cancel()
|
||||
// retires the window before it aborts, and retry() refuses to
|
||||
// arm a window whose signal already aborted.
|
||||
retry = recoveryCompleted && retryWindow.requested
|
||||
}
|
||||
const settlement = this.settle(turn, step, outcome.error, signal, outcome.failure)
|
||||
reason = settlement.reason
|
||||
@@ -447,7 +415,6 @@ export class ReactLoopAgent implements Agent {
|
||||
this.loopCtx.logger.warn(`agent "${this.id}": closing turn ${turn} failed: ${errorChain(error)}`)
|
||||
emitAgentEvent(this.loopCtx, this, 'agent/error', turn, step, error)
|
||||
}
|
||||
this.retryWindow = undefined
|
||||
// cancel() aborts but never clears the slot, and no second run can
|
||||
// install a controller while this one is still unwinding, so the slot
|
||||
// is still this run's controller here.
|
||||
@@ -727,7 +694,6 @@ export class ReactLoopAgent implements Agent {
|
||||
|
||||
/** Continue with a waking prompt, or publish the idle status. */
|
||||
private continueOrIdle(): void {
|
||||
if (this.abort !== undefined) return
|
||||
if (this.queued.some(item => item.wakeup)) {
|
||||
this.kick()
|
||||
} else {
|
||||
|
||||
@@ -255,21 +255,8 @@ describe('structured tool error propagation (the runtime-validation Agent Note,
|
||||
})
|
||||
})
|
||||
|
||||
describe('retry() edges', () => {
|
||||
it('throws while a turn runs with no request-error window open', async () => {
|
||||
const adapter = new MockAdapter(['hang'])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('retry-busy'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
send(agent, 'go')
|
||||
// Wait until the hung request is in flight (the run owns this.abort).
|
||||
await new Promise(r => setTimeout(r, 30))
|
||||
expect(() => { agent.retry() }).toThrow('cannot retry while busy')
|
||||
agent.cancel({ kind: 'user' })
|
||||
await agent.whenIdle()
|
||||
})
|
||||
|
||||
it('ignores a retry request arriving after the recovery window was aborted', async () => {
|
||||
describe('request-error action edges', () => {
|
||||
it('ignores a retry action returned after the turn was aborted', async () => {
|
||||
const { LlmError } = await import('@deepseek-ai/dsh-llm')
|
||||
const adapter = new MockAdapter([
|
||||
() => { throw new LlmError('busy', 'RATE_LIMIT') },
|
||||
@@ -278,10 +265,8 @@ describe('retry() edges', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('retry-after-cancel'), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/request-error', async (subject) => {
|
||||
// Cancellation lands first; the window survives structurally but its
|
||||
// signal is aborted, so the request must not arm a retry turn.
|
||||
subject.cancel({ kind: 'user' })
|
||||
subject.retry()
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
|
||||
send(agent, 'go')
|
||||
@@ -302,11 +287,9 @@ describe('retry() edges', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('retry-raced'), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/request-error', async (subject, _turn, _step, _error, _failure, signal, next) => {
|
||||
await next()
|
||||
// Recovery completes and requested the retry, but the turn signal
|
||||
// aborts before the loop reads the window.
|
||||
subject.retry()
|
||||
subject.cancel({ kind: 'user' })
|
||||
expect(signal.aborted).toBe(true)
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
|
||||
send(agent, 'go')
|
||||
@@ -348,29 +331,6 @@ describe('stream failure edges', () => {
|
||||
})
|
||||
|
||||
describe('post-turn continuation edges', () => {
|
||||
it('an agent/settled listener that starts a retry preempts continueOrIdle', async () => {
|
||||
const adapter = new MockAdapter([textResponse('one'), textResponse('two')])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('settled-preempt'), { provider: 'mock', model: 'mock' })
|
||||
let injected = false
|
||||
ctx.on('agent/settled', (subject) => {
|
||||
if (subject !== agent || injected) return
|
||||
expect(subject.status).toBe('running')
|
||||
injected = true
|
||||
// retry() installs the next run synchronously, so the following
|
||||
// continueOrIdle() sees its abort owner and yields to it.
|
||||
subject.retry()
|
||||
})
|
||||
|
||||
send(agent, 'go')
|
||||
await agent.whenIdle()
|
||||
|
||||
expect(adapter.requests).toHaveLength(2)
|
||||
const starts = agent.session.events.filter(e => e.type === 'turn/start')
|
||||
expect(starts).toHaveLength(2)
|
||||
// The busy interval never broke between the turns: one running->idle cycle.
|
||||
})
|
||||
|
||||
it('whenIdle resolves for a waiter whose awaited run fails', async () => {
|
||||
const adapter = new MockAdapter([textResponse('unused')])
|
||||
const ctx = await harness(adapter)
|
||||
@@ -472,8 +432,8 @@ describe('turn close failure containment', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('recovery without a retry request', () => {
|
||||
it('a completed recovery that never calls retry() leaves the failed turn terminal', async () => {
|
||||
describe('recovery without a retry action', () => {
|
||||
it('a completed recovery that returns no action leaves the failed turn terminal', async () => {
|
||||
const { LlmError } = await import('@deepseek-ai/dsh-llm')
|
||||
const adapter = new MockAdapter([
|
||||
() => { throw new LlmError('down', 'SERVICE_UNAVAILABLE') },
|
||||
|
||||
@@ -286,7 +286,7 @@ describe('agent/prompt-submit', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('blocked-admission-outbox'), { provider: 'mock', model: 'mock' })
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const decision = Promise.withResolvers<PromptDecision>()
|
||||
ctx.on('agent/prompt-submit', async () => {
|
||||
const disposeBlock = ctx.on('agent/prompt-submit', async () => {
|
||||
entered.resolve(undefined)
|
||||
return decision.promise
|
||||
})
|
||||
@@ -307,13 +307,17 @@ describe('agent/prompt-submit', () => {
|
||||
expect(events(agent)).toEqual([])
|
||||
expect(adapter.requests).toEqual([])
|
||||
|
||||
const retryIdle = waitForIdle(ctx, agent)
|
||||
agent.retry()
|
||||
await retryIdle
|
||||
disposeBlock()
|
||||
send(agent, 'resume')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const staged = events(agent).filter(event =>
|
||||
event.type === 'user/message' || event.type === 'steering/message')
|
||||
expect(staged.map(event => event.type)).toEqual(['user/message', 'steering/message'])
|
||||
expect(staged.map(event => event.type)).toEqual([
|
||||
'user/message',
|
||||
'steering/message',
|
||||
'user/message',
|
||||
])
|
||||
expect(JSON.stringify(adapter.requests[0]?.messages)).not.toContain('blocked prompt')
|
||||
expect(JSON.stringify(adapter.requests[0]?.messages)).toContain('staged context')
|
||||
expect(JSON.stringify(adapter.requests[0]?.messages)).toContain('staged steering')
|
||||
@@ -408,7 +412,7 @@ describe('agent/prompt-submit', () => {
|
||||
})
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const decision = Promise.withResolvers<PromptDecision>()
|
||||
ctx.on('agent/prompt-submit', async () => {
|
||||
const disposeBlock = ctx.on('agent/prompt-submit', async () => {
|
||||
entered.resolve(undefined)
|
||||
return decision.promise
|
||||
})
|
||||
@@ -425,9 +429,9 @@ describe('agent/prompt-submit', () => {
|
||||
expect(events(agent)).toEqual([])
|
||||
expect(warned).toHaveBeenCalledWith(expect.stringContaining('append unavailable'))
|
||||
|
||||
const idle = waitForIdle(ctx, agent)
|
||||
agent.retry()
|
||||
await idle
|
||||
disposeBlock()
|
||||
send(agent, 'resume')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(events(agent).some(event => event.type === 'user/message'
|
||||
&& JSON.stringify(event.data.content).includes('retained context'))).toBe(true)
|
||||
|
||||
@@ -317,7 +317,7 @@ describe('agent loop', () => {
|
||||
expect(JSON.stringify(adapter.requests[1]?.messages)).toContain('second idle steer')
|
||||
})
|
||||
|
||||
it('keeps steering staged after a failed step until retry', async () => {
|
||||
it('keeps steering staged after a failed step until the next admitted turn', async () => {
|
||||
const adapter = new MockAdapter([textResponse('recovered')])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('failed-steering'), { provider: 'mock', model: 'mock' })
|
||||
@@ -336,9 +336,8 @@ describe('agent loop', () => {
|
||||
expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(1)
|
||||
expect(agent.session.events.some(event => event.type === 'steering/message')).toBe(false)
|
||||
|
||||
const idle = waitForIdle(ctx, agent)
|
||||
agent.retry()
|
||||
await idle
|
||||
send(agent, 'resume')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(2)
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('agent/request-error', () => {
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('lets each failed request schedule a retry before its turn closes', async () => {
|
||||
it('lets each failed request return a retry action before its turn closes', async () => {
|
||||
const adapter = new MockAdapter([
|
||||
fail('busy', 'RATE_LIMIT'),
|
||||
fail('unavailable', 'SERVICE_UNAVAILABLE'),
|
||||
@@ -71,8 +71,7 @@ describe('agent/request-error', () => {
|
||||
data: { turn, step },
|
||||
})
|
||||
seen.push({ turn, step, failure })
|
||||
subject.retry()
|
||||
subject.retry()
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
|
||||
@@ -104,13 +103,13 @@ describe('agent/request-error', () => {
|
||||
expect(settledTurns).toEqual([3])
|
||||
})
|
||||
|
||||
it('lets cancellation win over a retry request', async () => {
|
||||
it('lets cancellation win over a retry action', async () => {
|
||||
const adapter = new MockAdapter([fail('busy', 'RATE_LIMIT'), textResponse('unused')])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('request-error-cancel'), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/request-error', async (subject) => {
|
||||
subject.retry()
|
||||
subject.cancel({ kind: 'user' })
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
|
||||
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
|
||||
@@ -124,15 +123,14 @@ describe('agent/request-error', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('does not honor a retry requested by a failing recovery listener', async () => {
|
||||
it('does not retry when the recovery listener fails before returning its action', async () => {
|
||||
const adapter = new MockAdapter([fail('busy', 'RATE_LIMIT'), textResponse('unused')])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('request-error-recovery-failed'), {
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
})
|
||||
ctx.on('agent/request-error', async (subject) => {
|
||||
subject.retry()
|
||||
ctx.on('agent/request-error', async () => {
|
||||
throw new Error('recovery failed')
|
||||
})
|
||||
|
||||
|
||||
@@ -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: b9e7914ac2238b677e5c799db58ca54088140352
|
||||
README.zh.md: 1a88f0d2e4c4619defae95a4cbe49caec7768599
|
||||
README.md: bb48fd8b227484a43af8f9f9e55f8adc8b990ce6
|
||||
README.zh.md: 531db9905b3c091a5c129d31e944e4095e66123b
|
||||
|
||||
@@ -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/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.
|
||||
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 returns `{ kind: 'retry' }` without calling `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.
|
||||
|
||||
|
||||
@@ -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/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)拥有。
|
||||
大多数拦截点都是协作式 waterfall。轮次作用域的异步 seam 接收一个显式 `AbortSignal`,其中 `signal` 紧邻 waterfall 最终的 `next`;监听器可以配合,但不得将它保留为控制另一轮次的权限。`agent/step` 是派生请求前的串行检查点,而 `agent/request-error` 是失败模型请求的恢复 waterfall:失败步骤关闭后,它接收确切错误、规范化失败事实和信号。拥有恢复权的监听器返回 `{ kind: '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`,除非有意替换任一字段;返回的允许决策是权威来源。
|
||||
|
||||
|
||||
@@ -116,6 +116,9 @@ export type PromptDecision =
|
||||
/** Model-request failure with an optional machine-routable provider code. */
|
||||
export type RequestError = Error & { code?: string }
|
||||
|
||||
/** Action returned by a listener that owns model-request recovery. */
|
||||
export type RequestErrorAction = { kind: 'retry' } | undefined
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -226,15 +229,6 @@ export interface Agent {
|
||||
* @returns the accepted message's {@link AgentMessageId}.
|
||||
*/
|
||||
inject(input: UserMessageData): AgentMessageId
|
||||
|
||||
/**
|
||||
* Re-open a turn on the current session log without a new prompt — the
|
||||
* explicit resummon verb. During `agent/request-error`, this schedules one
|
||||
* retry turn after the failed turn closes; while idle, it starts one
|
||||
* immediately. Repeated calls before the scheduled retry coalesce.
|
||||
* @throws while other agent work is running.
|
||||
*/
|
||||
retry(): void
|
||||
}
|
||||
|
||||
declare module 'cordis' {
|
||||
@@ -368,9 +362,9 @@ declare module 'cordis' {
|
||||
'agent/request'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
|
||||
/**
|
||||
* Handle a model-request failure after its failed step has closed but
|
||||
* before the failed turn closes. A listener calls {@link Agent.retry} to
|
||||
* schedule one retry turn, returns without `next()` when it owns the error,
|
||||
* or calls `next()` to delegate. The default leaves the failure terminal.
|
||||
* before the failed turn closes. A listener returns `{ kind: 'retry' }`
|
||||
* without calling `next()` when it owns the error, or calls `next()` to
|
||||
* delegate. The default `undefined` leaves the failure terminal.
|
||||
* @param agent - the agent whose request failed.
|
||||
* @param turn - the open turn number.
|
||||
* @param step - the failed step number.
|
||||
@@ -380,7 +374,7 @@ declare module 'cordis' {
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'agent/request-error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, signal: AbortSignal, next: () => Promise<void>): Promise<void>
|
||||
'agent/request-error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, signal: AbortSignal, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
|
||||
/**
|
||||
* The turn is about to close: the model owes no response (no live tool
|
||||
* calls, no fresh steering). Awaited before the boundary commits — a
|
||||
|
||||
@@ -29,7 +29,6 @@ function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject: () => AgentMessageId('stub'),
|
||||
cancel() {},
|
||||
retry() {},
|
||||
whenIdle() { return Promise.resolve() },
|
||||
}
|
||||
return Object.assign(agent, overrides)
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('scoped-dispatch invariants', () => {
|
||||
new Error('request'),
|
||||
{ message: 'request', code: 'UNKNOWN' },
|
||||
signal,
|
||||
() => Promise.resolve(),
|
||||
() => Promise.resolve(undefined),
|
||||
],
|
||||
'agent/turn-stopping': [agent, 1, signal],
|
||||
'agent/settled': [agent, 1, { kind: 'completed' }],
|
||||
|
||||
@@ -37,7 +37,6 @@ function stubAgent(id: string): { agent: Agent; session: Session } {
|
||||
steer: () => AgentMessageId('stub'),
|
||||
inject(input) { appendInjection(session, input); return AgentMessageId('stub') },
|
||||
cancel() { status = 'idle' },
|
||||
retry() {},
|
||||
whenIdle() { return Promise.resolve() },
|
||||
}
|
||||
return { agent, session }
|
||||
|
||||
@@ -486,10 +486,10 @@ describe('same-session goal driving', () => {
|
||||
])
|
||||
// The llm-retry shape: schedule one retry for the failed goal-round request.
|
||||
let retried = false
|
||||
test.ctx.on('agent/request-error', async (subject) => {
|
||||
test.ctx.on('agent/request-error', async (_subject) => {
|
||||
if (!retried) {
|
||||
retried = true
|
||||
subject.retry()
|
||||
return { kind: 'retry' }
|
||||
}
|
||||
})
|
||||
test.ctx.goals.create(test.agent, { objective: 'survive a transient failure', maxGoalRounds: 1 })
|
||||
@@ -557,10 +557,10 @@ describe('same-session goal driving', () => {
|
||||
textResponse('goal round ran'),
|
||||
])
|
||||
let retried = false
|
||||
test.ctx.on('agent/request-error', async (subject) => {
|
||||
test.ctx.on('agent/request-error', async (_subject) => {
|
||||
if (!retried) {
|
||||
retried = true
|
||||
subject.retry()
|
||||
return { kind: 'retry' }
|
||||
}
|
||||
})
|
||||
// A human prompt fails and retries while a goal is armed but its round
|
||||
|
||||
@@ -56,7 +56,6 @@ function stubAgentForSession(session: Session): StubAgent {
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
cancel() {},
|
||||
retry() {},
|
||||
whenIdle() { return Promise.resolve() },
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -40,7 +40,6 @@ function stubAgent(rawId: string, supplied?: Session): StubAgent {
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
cancel() {},
|
||||
retry() {},
|
||||
whenIdle() { return Promise.resolve() },
|
||||
}
|
||||
return { agent, session, setStatus(value) { status = value } }
|
||||
|
||||
@@ -50,7 +50,6 @@ function stubAgent(session: Session): Agent {
|
||||
inject: () => AgentMessageId('stub'),
|
||||
send: () => AgentMessageId('stub'),
|
||||
cancel() {},
|
||||
retry() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/llm/llm-retry/README.md
|
||||
README.md: 11b466a68804ae63a44f06d78c7aa5893c4041a7
|
||||
README.zh.md: 1638078ed639271802fa841f3b26f6bd3f4c1737
|
||||
README.md: 596a46e5395a4b5be9d400a85ee7c54d613ec2e6
|
||||
README.zh.md: a6a48f203e4701688816d4365b03da1ae2cfad82
|
||||
|
||||
@@ -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/settled`. 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 returns `{ kind: '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.
|
||||
|
||||
|
||||
@@ -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/settled` 时清零。轮次取消与插件 dispose 会中止等待。
|
||||
恢复 listener 会在失败步骤之后追加一个非表层 `llm/retry` 事件,在失败轮次的信号仍存活期间等待退避,然后返回 `{ kind: 'retry' }`。循环会关闭该失败轮次,并在同一持久历史上开启重试轮次。策略在这条不间断的恢复链中维护自己的重试计数,并在终态 `agent/settled` 时清零。轮次取消与插件 dispose 会中止等待。
|
||||
|
||||
单独发布的 `./invariant` 配套模块会检查每个重试记录是否出现在开启轮次内的失败步骤之后,是否与其在当前重试链中的位置匹配,以及是否携带正数有界重试预算和非负有界定时器延迟。完整 jitter 可以在下界调度为零毫秒。
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import type { Agent, RequestError } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, RequestError, RequestErrorAction } from '@deepseek-ai/dsh-agent'
|
||||
import type { LlmFailure } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-session'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
@@ -145,7 +145,7 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna
|
||||
const resolved = resolveConfig(config)
|
||||
const random = internals.random ?? Math.random
|
||||
const lifetime = new AbortController()
|
||||
const active = new Set<Promise<void>>()
|
||||
const active = new Set<Promise<RequestErrorAction>>()
|
||||
const retries = new WeakMap<Agent, number>()
|
||||
|
||||
async function backoff(
|
||||
@@ -156,7 +156,7 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna
|
||||
retry: number,
|
||||
delayMs: number,
|
||||
signal: AbortSignal,
|
||||
): Promise<void> {
|
||||
): Promise<RequestErrorAction> {
|
||||
const fusedSignal = AbortSignal.any([signal, lifetime.signal])
|
||||
if (fusedSignal.aborted) return
|
||||
agent.session.append('llm/retry', {
|
||||
@@ -169,7 +169,7 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna
|
||||
})
|
||||
retries.set(agent, retry)
|
||||
if (!await cancellableDelay(delayMs, fusedSignal)) return
|
||||
agent.retry()
|
||||
return { kind: 'retry' }
|
||||
}
|
||||
|
||||
ctx.on('agent/settled', (agent) => {
|
||||
@@ -191,12 +191,12 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna
|
||||
_error: RequestError,
|
||||
failure: LlmFailure,
|
||||
signal: AbortSignal,
|
||||
next: () => Promise<void>,
|
||||
next: () => Promise<RequestErrorAction>,
|
||||
) => {
|
||||
// A waterfall may have captured this callback before its registration was
|
||||
// removed. Lifetime cancellation must prevent that stale callback from
|
||||
// entering a downstream policy after disposal.
|
||||
if (lifetime.signal.aborted) return Promise.resolve()
|
||||
if (lifetime.signal.aborted) return Promise.resolve<RequestErrorAction>(undefined)
|
||||
if (!resolved.retryableCodes.has(failure.code)) return next()
|
||||
const priorRetries = retries.get(agent) ?? 0
|
||||
if (priorRetries >= resolved.maxTransientRetries) return next()
|
||||
|
||||
@@ -136,10 +136,10 @@ 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/settled', (subject, _turn, reason) => {
|
||||
if (subject !== agent || reason.kind !== 'error') return
|
||||
ctx.on('agent/request-error', async (subject, _turn, _step, _error, _failure, _signal, next) => {
|
||||
if (subject !== agent) return next()
|
||||
ctx.planMode.set(agent, true)
|
||||
agent.retry()
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
|
||||
const idle = waitForIdle(ctx, agent)
|
||||
|
||||
@@ -42,7 +42,7 @@ function agent(ctx: Context): Agent {
|
||||
const id = SessionId('agent')
|
||||
return {
|
||||
id, options: {}, session: new Session(id), status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ describe('pty-local plugin shape', () => {
|
||||
const ownerFiber = await ctx.plugin(() => {})
|
||||
const owner: Agent = {
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx: ownerFiber.ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
ctx.agents.register(owner)
|
||||
const providerFiber = await registerStubLocalBackend(ctx, () => stubLocalSession())
|
||||
@@ -292,7 +292,7 @@ describe('pty-local plugin shape', () => {
|
||||
const ownerFiber = await ctx.plugin(() => {})
|
||||
const owner: Agent = {
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx: ownerFiber.ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
ctx.agents.register(owner)
|
||||
const gate = Promise.withResolvers<undefined>()
|
||||
|
||||
@@ -35,7 +35,7 @@ function stubAgent(ctx: Context, rawId: string): Agent {
|
||||
const scope = ctx.plugin(() => {})
|
||||
return {
|
||||
id, options: {}, session: new Session(id), status: 'idle', acceptsNextStep: false, ctx: scope.ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ function stubAgent(ctx: Context, rawId: string): Agent {
|
||||
inject: () => AgentMessageId('stub'),
|
||||
send: () => AgentMessageId('stub'),
|
||||
cancel() {},
|
||||
retry() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
agentScopeDisposers.set(agent, async () => { await scopeFiber.dispose() })
|
||||
|
||||
@@ -40,7 +40,7 @@ function agent(ctx: Context): Agent {
|
||||
const id = SessionId('pty-loader-agent')
|
||||
const value: Agent = {
|
||||
id, options: {}, session: new Session(id), status: 'idle', acceptsNextStep: false, ctx: scope.ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
ctx.agents.register(value)
|
||||
return value
|
||||
|
||||
@@ -18,7 +18,7 @@ function fakeAgent(ctx: Context, rawId: string): Agent {
|
||||
const id = SessionId(rawId)
|
||||
const agent: Agent = {
|
||||
id, options: {}, session: new Session(id), status: 'idle', acceptsNextStep: false, ctx: scope.ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
ctx.agents.register(agent)
|
||||
return agent
|
||||
|
||||
@@ -54,7 +54,6 @@ function agentForCwd(cwd: string): Agent {
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
cancel() {},
|
||||
retry() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ function stubAgent(ctx: Context, rawId: string): Agent {
|
||||
inject: () => AgentMessageId('stub'),
|
||||
send: () => AgentMessageId('stub'),
|
||||
cancel() {},
|
||||
retry() {},
|
||||
whenIdle() { return Promise.resolve() },
|
||||
}
|
||||
agentScopeDisposers.set(agent, async () => { await scopeFiber.dispose() })
|
||||
|
||||
@@ -230,7 +230,6 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
cancel(cause) {
|
||||
cancelled.push(cause)
|
||||
},
|
||||
retry() {},
|
||||
whenIdle() {
|
||||
return Promise.resolve()
|
||||
},
|
||||
|
||||
@@ -3752,7 +3752,7 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
mountTui(ctx, { color: false }, { terminal, exit: vi.fn() })
|
||||
@@ -3776,7 +3776,7 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
// Mirror dsh-tui's own inject (minus loader, the absence under test).
|
||||
@@ -3810,14 +3810,14 @@ describe('terminal mounting', () => {
|
||||
const otherSession = ctx.sessions.create(SessionId('other-session'))
|
||||
ctx.agents.register({
|
||||
id: otherSession.id, options: {}, session: otherSession, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
expect(terminal.started).toBe(0)
|
||||
|
||||
const session = ctx.sessions.create(SessionId('late-session'))
|
||||
const agent = {
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
} as Agent
|
||||
ctx.agents.register(agent)
|
||||
await tick()
|
||||
@@ -3847,7 +3847,7 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main-session'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
await tick()
|
||||
expect(terminal.started).toBe(0)
|
||||
@@ -3889,7 +3889,7 @@ describe('terminal mounting', () => {
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'running', acceptsNextStep: true, ctx,
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, retry() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
terminal.start = () => { throw new Error('terminal startup failed') }
|
||||
|
||||
@@ -50,6 +50,7 @@ export const LINK_MAP: Record<string, string> = {
|
||||
Message: 'core.md',
|
||||
MessageSource: 'core.md',
|
||||
PromptDecision: 'core.md',
|
||||
RequestErrorAction: 'core.md',
|
||||
RequestError: 'core.md',
|
||||
PreparedReferencedMessage: 'session-reference.md',
|
||||
SessionReferenceCandidate: 'session-reference.md',
|
||||
|
||||
@@ -1022,7 +1022,7 @@ function renderLifecycle(): string {
|
||||
' alt final adapter or terminal in-band request failure',
|
||||
` Driver->>Session: ${mermaidCode('step/end')}`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/request-error')} waterfall`,
|
||||
' Hooks-->>Driver: call agent.retry() or preserve the original error',
|
||||
' Hooks-->>Driver: return retry action or preserve the original error',
|
||||
' else model request succeeded',
|
||||
` Driver->>Session: ${mermaidCode('assistant/message')}`,
|
||||
' Driver->>Tools: classify pending call by executionMode',
|
||||
|
||||
@@ -131,6 +131,11 @@
|
||||
"symbol": "PromptDecision",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "RequestErrorAction",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "RequestError",
|
||||
|
||||
Reference in New Issue
Block a user