refactor(agent): return request retry action

This commit is contained in:
_Kerman
2026-07-27 21:17:49 +08:00
parent c6073f07c2
commit ed67ad53d2
83 changed files with 277 additions and 309 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/llm-retry/README.md
README.md: 11b466a68804ae63a44f06d78c7aa5893c4041a7
README.zh.md: 1638078ed639271802fa841f3b26f6bd3f4c1737
README.md: 596a46e5395a4b5be9d400a85ee7c54d613ec2e6
README.zh.md: a6a48f203e4701688816d4365b03da1ae2cfad82

View File

@@ -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.

View File

@@ -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 可以在下界调度为零毫秒。

View File

@@ -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()