@deepseek-ai/dsh-llm-retry 0.1.1-rc.1 → 0.1.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +104 -8
- package/README.zh.md +113 -17
- package/lib/index.js +32 -10
- package/lib/types/index.d.ts +12 -0
- package/lib/types/index.js +31 -14
- package/package.json +29 -26
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/llm/llm-retry/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 0ea815130212a165582540e43aad7d59fad32d2c
|
|
6
|
+
README.zh.md: 43a0187c68d272e2764d34db35fa26e51a9d3a83
|
package/README.md
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "The retry executor for users and maintainers configuring provider-routed model-request recovery at durable agent-step boundaries."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-llm-retry
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`@deepseek-ai/dsh-llm-retry` is the retry executor for failed model requests: it applies each provider's resolved retry policy at the agent loop's open-step `agent/request-error` extension point, so every retry re-runs the same step inside the same open turn over the same durable history. It does not wrap the streaming call itself — every adapter call remains one provider attempt, and direct `ctx.llm.stream()` consumers stay single-attempt. Retry scheduling is durable: the plugin appends `llm/retry` events to the session log before waiting, and cancellation during backoff leaves the log consistent. Normal mode retries a bounded set of failure codes up to `maxRetries` with exponential backoff; always mode asks downstream recovery first, then retries every failure without an attempt limit.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
6
24
|
|
|
7
|
-
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
8
27
|
|
|
9
|
-
|
|
28
|
+
Mount this plugin when agent runs should recover from transient model-request failures — rate limits, server errors, timeouts, transport errors — instead of ending the turn. It is the executor: the retry policy itself lives on each provider adapter's configuration, and this package has no configuration of its own.
|
|
10
29
|
|
|
11
|
-
|
|
30
|
+
### When to choose it
|
|
12
31
|
|
|
13
|
-
|
|
32
|
+
Choose it when a composition runs the agent loop and wants durable request recovery. The plugin is a function plugin with no config; provider adapters such as `dsh-llm-deepseek` and `dsh-llm-pi-ai` own the `retryPolicy` for their routes, and multi-provider adapters place it inside each provider profile. Skip it when calls go through `ctx.llm.stream()` directly without the agent loop: those consumers remain single-attempt because a raw stream cannot separate already-emitted chunks durably.
|
|
33
|
+
|
|
34
|
+
### Minimal configuration
|
|
14
35
|
|
|
15
36
|
```yaml
|
|
16
37
|
- name: '@deepseek-ai/dsh-llm-deepseek'
|
|
@@ -26,15 +47,72 @@ The separately published `./invariant` companion checks that every scheduled ret
|
|
|
26
47
|
- name: '@deepseek-ai/dsh-llm-retry'
|
|
27
48
|
```
|
|
28
49
|
|
|
29
|
-
|
|
50
|
+
Omission of `retryPolicy` uses normal mode: five retries for `EMPTY_RESPONSE`, `RATE_LIMIT`, `SERVER`, `TIMEOUT`, and `TRANSPORT`, with bounded exponential backoff from 500 ms to 10 seconds and 10 percent jitter. Normal mode can change its finite budget, eligible codes, and backoff; always mode asks downstream recovery first, then retries every model-request failure without an attempt limit, stopping only on success, cancellation, or plugin disposal.
|
|
51
|
+
|
|
52
|
+
### What you can observe
|
|
53
|
+
|
|
54
|
+
Each scheduled retry is durable before its wait: the plugin appends a non-surface `llm/retry` event carrying the retry id, provider, mode, policy key, failure, and scheduled delay, then a `llm/retry-started` event immediately before the retry begins. A valid `Retry-After` from the provider replaces local backoff when it fits the policy bounds. When the wait completes, the loop re-runs the failed step inside the same open turn over the same durable history, so the retried request is reconstructable from the session log exactly like the original. Cancellation or plugin disposal aborts active backoff, drains active delegated recovery, and makes a callback captured before disposal fail closed.
|
|
55
|
+
|
|
56
|
+
### Failures and recovery
|
|
57
|
+
|
|
58
|
+
A failure before any final adapter is selected has no provider policy and delegates downstream unchanged. In normal mode, a failure code outside the eligible set, or an exhausted budget, delegates; in always mode, an over-cap provider delay uses the configured local backoff so the policy cannot terminate on that instruction. Nothing here is model-visible: no retry event, delay, provider error, or failed partial output reaches the model or derived messages.
|
|
59
|
+
|
|
60
|
+
-----
|
|
61
|
+
|
|
62
|
+
<a id="understand-the-implementation"></a>
|
|
63
|
+
## Understand the implementation
|
|
64
|
+
|
|
65
|
+
<details>
|
|
66
|
+
<summary>Implementation internals — click to expand</summary>
|
|
67
|
+
|
|
68
|
+
This section explains the design behind the executor; the observable behavior is fully covered in [Use this package](#use-this-package).
|
|
69
|
+
|
|
70
|
+
### Design philosophy
|
|
71
|
+
|
|
72
|
+
The executor is built on one rule: **durable before wait, open-step boundaries.** A retry is scheduled through the session log before any timer starts, so a crash or cancellation never leaves an invisible pending retry. Recovery runs on the agent loop's `agent/request-error` waterfall, the open-step extension point, rather than wrapping `ctx.llm.stream()` — a raw stream cannot separate already-emitted chunks durably, while the loop can re-run the failed step inside the same open turn.
|
|
73
|
+
|
|
74
|
+
### Source map
|
|
75
|
+
|
|
76
|
+
| File | Role |
|
|
77
|
+
|---|---|
|
|
78
|
+
| [`src/index.ts`](src/index.ts) | The function plugin: waterfall listener, policy lookup, backoff, durable event appends |
|
|
79
|
+
| [`src/history.ts`](src/history.ts) | Durable retry-history lookup from the session log |
|
|
80
|
+
| [`src/types.ts`](src/types.ts) | Browser-safe `llm/retry` and `llm/retry-started` event payload types |
|
|
81
|
+
| [`src/brand.ts`](src/brand.ts) | The `RetryId` brand shared by the event payloads |
|
|
30
82
|
|
|
83
|
+
### Recovery flow
|
|
84
|
+
|
|
85
|
+
A failed step arrives on the waterfall with its provider and resolved policy. Always mode settles downstream recovery first and honors a downstream `retry` decision; normal mode first checks that the failure code is eligible and the budget is not exhausted. The plugin computes the delay — provider `Retry-After` when valid and within bounds, otherwise local bounded exponential backoff with symmetric jitter — appends the `llm/retry` event, waits on a cancellable timer, appends `llm/retry-started`, and returns `{ kind: 'retry' }`. The loop then re-runs the failed step inside the same open turn over the same durable history.
|
|
86
|
+
|
|
87
|
+
### Waterfall composition
|
|
88
|
+
|
|
89
|
+
The plugin is one listener in the `agent/request-error` waterfall. Always mode's "downstream first" posture means a later policy that ignores cancellation and never settles also prevents fallback, turn quiescence, and plugin disposal from completing; success, cancellation, or disposal stops always mode after active delegated recovery reaches quiescence.
|
|
90
|
+
|
|
91
|
+
</details>
|
|
92
|
+
|
|
93
|
+
-----
|
|
94
|
+
|
|
95
|
+
<a id="further-exploration"></a>
|
|
96
|
+
## Further Exploration
|
|
97
|
+
|
|
98
|
+
Read these pages when the package-level contract is not enough. They move from the service contract to the adapters that own retry policies.
|
|
99
|
+
|
|
100
|
+
- [dsh-llm service](../llm/README.md) — the provider-neutral service whose adapters own `retryPolicy`.
|
|
101
|
+
- [llm-deepseek adapter](../llm-deepseek/README.md) — a provider adapter with a route-level `retryPolicy`.
|
|
102
|
+
- [llm-pi-ai adapter](../llm-pi-ai/README.md) — a multi-provider adapter with per-profile `retryPolicy`.
|
|
103
|
+
- [Terminal LLM stream failures](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.md) — how failures reach the service boundary as terminal chunks.
|
|
104
|
+
- [LLM streaming subsystem](../../../docs/subsystems/llm-streaming.md) — the `StreamChunk` protocol and adapter contract.
|
|
105
|
+
|
|
106
|
+
-----
|
|
107
|
+
|
|
108
|
+
<a id="model-experience"></a>
|
|
31
109
|
## Model Experience
|
|
32
110
|
|
|
33
111
|
### Model-request recovery
|
|
34
112
|
|
|
35
113
|
#### What the model sees
|
|
36
114
|
|
|
37
|
-
No retry event, delay, provider error, or failed partial output is model-visible. The
|
|
115
|
+
No retry event, delay, provider error, or failed partial output is model-visible. The retried step reconstructs the same explicit provider/model request from durable surface history unless a downstream recovery policy deliberately changes that surface; failed chunks never enter derived messages.
|
|
38
116
|
|
|
39
117
|
#### Token effect
|
|
40
118
|
|
|
@@ -46,8 +124,26 @@ The reconstructed request preserves the prior prefix and is eligible for provide
|
|
|
46
124
|
|
|
47
125
|
## Known Limitations and Deferred Work
|
|
48
126
|
|
|
127
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
These limits define where the executor stops and future work begins. They are current package constraints, not a general retry comparison or a task backlog.
|
|
131
|
+
|
|
49
132
|
- **Agent turns are the only retry boundary** — direct `ctx.llm.stream()` consumers remain single-attempt because a raw stream cannot separate already-emitted chunks durably.
|
|
50
133
|
- **Always mode retries permanent failures** — authentication, quota, invalid-request, protocol, and unrecoverable context errors continue until success, cancellation, or disposal; deployments own provider-specific cost and latency controls.
|
|
51
134
|
- **Finite plugin budgets add** — normal mode counts only its configured codes and exact provider policy, while context-overflow compaction owns a separate budget. Any overlapping policy must define registration-order behavior.
|
|
52
135
|
- **Recovery policies compose by waterfall order** — always mode accepts a downstream retry before applying its fallback. A later policy that ignores cancellation and never settles also prevents fallback, turn quiescence, and plugin disposal from completing.
|
|
53
136
|
- **`llm/retry` records scheduling, not completion** — later step and turn events establish success, exhaustion, or cancellation.
|
|
137
|
+
|
|
138
|
+
<a id="dev-note"></a>
|
|
139
|
+
### Dev Note
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
143
|
+
|
|
144
|
+
This Dev Note is non-authoritative working context: notes for maintainers and open questions. Shipped behavior and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
|
|
145
|
+
|
|
146
|
+
- Retry numbers continue only across events with the same provider and complete policy key, so a route replacement with different limits, code membership, or backoff starts its own history; the key includes every behavior-affecting field and sorts normal-mode codes because eligibility uses set membership.
|
|
147
|
+
- The separately published `./invariant` companion validates each scheduled retry against the session log — naming the current open turn and latest closed step, matching the failed request's durable provider, and requiring each `llm/retry-started` event to name one prior scheduled attempt with the same retry id, turn, step, and retry number.
|
|
148
|
+
|
|
149
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "面向用户与维护者的重试执行器说明:在持久 agent 步骤边界上配置按提供方路由的模型请求恢复。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-llm-retry
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`@deepseek-ai/dsh-llm-retry` 是失败模型请求的重试执行器:它在 agent loop 的打开步骤 `agent/request-error` 扩展点上应用各提供方解析后的重试策略,因此每次重试都会在同一个打开的轮次内重跑同一个步骤(基于同一份持久历史)。它不包装流式调用本身——每次适配器调用仍是一次提供方尝试,直接 `ctx.llm.stream()` 消费方仍是单次尝试。重试调度是持久的:插件在等待之前就把 `llm/retry` 事件追加进会话日志,退避期间取消会让日志保持一致。normal mode 以指数退避重试一组有界的失败 code,最多 `maxRetries` 次;always mode 先询问下游恢复,然后无尝试上限地重试每个失败。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
6
24
|
|
|
7
|
-
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
8
27
|
|
|
9
|
-
|
|
28
|
+
当 agent 运行应从暂时性模型请求失败——速率限制、服务端错误、超时、传输错误——中恢复而不是结束轮次时,挂载本插件。它是执行器:重试策略本身位于各提供方适配器的配置上,本包没有任何自己的配置。
|
|
10
29
|
|
|
11
|
-
|
|
30
|
+
### 何时选择
|
|
12
31
|
|
|
13
|
-
|
|
32
|
+
当组合运行 agent loop 并需要持久请求恢复时选择它。本插件是无配置的函数插件;`dsh-llm-deepseek` 与 `dsh-llm-pi-ai` 等提供方适配器拥有各自路由的 `retryPolicy`,多提供方适配器把它放进每个 provider profile。当调用不经 agent loop、直接走 `ctx.llm.stream()` 时跳过它:这些消费方仍是单次尝试,因为原始流无法持久地区分已发出的分片。
|
|
33
|
+
|
|
34
|
+
### 最小配置
|
|
14
35
|
|
|
15
36
|
```yaml
|
|
16
37
|
- name: '@deepseek-ai/dsh-llm-deepseek'
|
|
@@ -26,28 +47,103 @@
|
|
|
26
47
|
- name: '@deepseek-ai/dsh-llm-retry'
|
|
27
48
|
```
|
|
28
49
|
|
|
29
|
-
|
|
50
|
+
省略 `retryPolicy` 时使用 normal mode:对 `EMPTY_RESPONSE`、`RATE_LIMIT`、`SERVER`、`TIMEOUT` 与 `TRANSPORT` 最多重试五次,退避从 500 毫秒到 10 秒、带 10% 抖动。normal mode 可以更改其有界预算、合格 code 与退避;always mode 先询问下游恢复,然后无尝试上限地重试每个模型请求失败,只在成功、取消或插件释放时停止。
|
|
51
|
+
|
|
52
|
+
### 你可以观察到什么
|
|
53
|
+
|
|
54
|
+
每次计划的重试在等待前就是持久的:插件会先追加携带重试 id、提供方、模式、策略键、失败与计划延迟的非 surface `llm/retry` 事件,然后在重试开始前立即追加 `llm/retry-started` 事件。提供方给出且符合策略边界的有效 `Retry-After` 会替换本地退避。等待完成后,loop 会在同一个打开的轮次内重跑失败步骤,仍基于同一份持久历史,因此重试请求与原始请求一样可以从会话日志重建。取消或插件释放会中止进行中的退避、排空活动中的委派恢复,并让释放前捕获的回调快速失败。
|
|
55
|
+
|
|
56
|
+
### 失败与恢复
|
|
57
|
+
|
|
58
|
+
在任何最终适配器被选中之前发生的失败没有提供方策略,原样委派下游。normal mode 中,不在合格集合内的失败 code 或已耗尽的预算会委派;always mode 中,超上限的提供方延迟使用配置的本地退避,因此策略不会因该指令终止。这里没有任何模型可见内容:重试事件、延迟、提供方错误或失败的部分输出都不会到达模型或派生消息。
|
|
59
|
+
|
|
60
|
+
-----
|
|
61
|
+
|
|
62
|
+
<a id="understand-the-implementation"></a>
|
|
63
|
+
## 理解实现
|
|
64
|
+
|
|
65
|
+
<details>
|
|
66
|
+
<summary>实现细节——点击展开</summary>
|
|
67
|
+
|
|
68
|
+
本节解释执行器背后的设计;可观察行为已在[使用本包](#use-this-package)中完整说明。
|
|
69
|
+
|
|
70
|
+
### 设计理念
|
|
71
|
+
|
|
72
|
+
执行器建立在一条规定之上:**先持久、后等待,打开步骤边界。** 任何定时器启动之前,重试就已通过会话日志计划,因此崩溃或取消永远不会留下不可见的待处理重试。恢复运行在 agent loop 的 `agent/request-error` waterfall(打开步骤扩展点)上,而不是包装 `ctx.llm.stream()`——原始流无法持久地区分已发出的分片,而 loop 可以在同一个打开的轮次内重跑失败步骤。
|
|
73
|
+
|
|
74
|
+
### 源码地图
|
|
75
|
+
|
|
76
|
+
| 文件 | 职责 |
|
|
77
|
+
|---|---|
|
|
78
|
+
| [`src/index.ts`](src/index.ts) | 函数插件:waterfall 监听器、策略查找、退避、持久事件追加 |
|
|
79
|
+
| [`src/history.ts`](src/history.ts) | 从会话日志查找持久重试历史 |
|
|
80
|
+
| [`src/types.ts`](src/types.ts) | 浏览器安全的 `llm/retry` 与 `llm/retry-started` 事件载荷类型 |
|
|
81
|
+
| [`src/brand.ts`](src/brand.ts) | 事件载荷共享的 `RetryId` 品牌 |
|
|
30
82
|
|
|
83
|
+
### 恢复流程
|
|
84
|
+
|
|
85
|
+
失败步骤连同其提供方与解析后的策略一起到达 waterfall。always mode 先结算下游恢复,并遵循下游的 `retry` 决定;normal mode 先检查失败 code 是否合格、预算是否未耗尽。插件计算延迟——有效且在边界内的提供方 `Retry-After`,否则带对称抖动的本地有界指数退避——追加 `llm/retry` 事件,在可取消定时器上等待,追加 `llm/retry-started`,然后返回 `{ kind: 'retry' }`。loop 随后在同一个打开的轮次内重跑失败步骤(仍基于同一份持久历史)。
|
|
86
|
+
|
|
87
|
+
### Waterfall 组合
|
|
88
|
+
|
|
89
|
+
本插件是 `agent/request-error` waterfall 中的一个监听器。always mode 的"下游优先"姿态意味着,之后忽略取消且永不结算的策略也会阻止回退、轮次完全停稳与插件释放完成;成功、取消或释放会在活动委派恢复达到完全停稳后停止 always mode。
|
|
90
|
+
|
|
91
|
+
</details>
|
|
92
|
+
|
|
93
|
+
-----
|
|
94
|
+
|
|
95
|
+
<a id="further-exploration"></a>
|
|
96
|
+
## 进一步探索
|
|
97
|
+
|
|
98
|
+
当包级约定不够用时阅读以下页面。它们从服务约定逐步进入拥有重试策略的适配器。
|
|
99
|
+
|
|
100
|
+
- [dsh-llm 服务](../llm/README.zh.md)——其适配器拥有 `retryPolicy` 的提供方无关服务。
|
|
101
|
+
- [llm-deepseek 适配器](../llm-deepseek/README.zh.md)——带路由级 `retryPolicy` 的提供方适配器。
|
|
102
|
+
- [llm-pi-ai 适配器](../llm-pi-ai/README.zh.md)——带逐 profile `retryPolicy` 的多提供方适配器。
|
|
103
|
+
- [LLM 流终止失败](../../../.agents/notes/implemented/architecture/2026-07-29-terminal-llm-stream-failures.zh.md)——失败如何以终止分片到达服务边界。
|
|
104
|
+
- [LLM 流式子系统](../../../docs/subsystems/llm-streaming.zh.md)——`StreamChunk` 协议与适配器约定。
|
|
105
|
+
|
|
106
|
+
-----
|
|
107
|
+
|
|
108
|
+
<a id="model-experience"></a>
|
|
31
109
|
## 模型体验
|
|
32
110
|
|
|
33
111
|
### 模型请求恢复
|
|
34
112
|
|
|
35
|
-
####
|
|
113
|
+
#### 模型看到什么
|
|
36
114
|
|
|
37
|
-
|
|
115
|
+
重试事件、延迟、提供方错误或失败的部分输出都不会对模型可见。除非下游恢复策略刻意改变表面,重试步骤从持久表面历史重建同一显式提供方/模型请求;失败分片绝不进入派生消息。
|
|
38
116
|
|
|
39
117
|
#### Token 影响
|
|
40
118
|
|
|
41
|
-
|
|
119
|
+
每次重试都是一次新的提供方请求,可能重复输入 token 计费。normal mode 有有界预算;always mode 在成功或取消前可能消耗无上限请求。`llm/retry` 本身不贡献任何 token。
|
|
42
120
|
|
|
43
121
|
#### KV Cache 影响
|
|
44
122
|
|
|
45
|
-
|
|
123
|
+
重建的请求保留此前前缀,有资格按该提供方规则复用提供方缓存。非 surface 重试事件不改变缓存标识。
|
|
124
|
+
|
|
125
|
+
## 已知限制与延期工作
|
|
126
|
+
|
|
127
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
这些限制说明执行器在哪里停止、由未来工作接续。它们是当前包约束,不是通用重试对比或任务积压。
|
|
131
|
+
|
|
132
|
+
- **agent 轮次是唯一重试边界**——直接 `ctx.llm.stream()` 消费方仍是单次尝试,因为原始流无法持久地区分已发出的分片。
|
|
133
|
+
- **always mode 会重试永久性失败**——认证、配额、无效请求、协议与不可恢复的上下文错误会持续到成功、取消或释放;部署方负责提供方专属的成本与延迟控制。
|
|
134
|
+
- **有界插件预算相加**——normal mode 只统计其配置的 code 与精确提供方策略,而上下文溢出压缩(compaction)拥有独立预算。任何重叠策略都必须定义注册顺序行为。
|
|
135
|
+
- **恢复策略按 waterfall 顺序组合**——always mode 先接受下游重试,再应用其回退。之后忽略取消且永不结算的策略也会阻止回退、轮次完全停稳与插件释放完成。
|
|
136
|
+
- **`llm/retry` 记录调度,而非完成**——后续步骤与轮次事件才确立成功、耗尽或取消。
|
|
137
|
+
|
|
138
|
+
<a id="dev-note"></a>
|
|
139
|
+
### 开发备注
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
143
|
+
|
|
144
|
+
本开发备注是不具权威性的工作上下文:维护者备注与开放问题。已交付的行为与既定理由以上文、包代码和相关 Agent Note 为准。
|
|
46
145
|
|
|
47
|
-
|
|
146
|
+
- 重试编号只在同一提供方与完整策略键的事件间延续,因此限额、code 成员或退避不同的路由替换会开启自己的历史;该键包含每个影响行为的字段,并因资格判断使用集合成员而对 normal mode code 排序。
|
|
147
|
+
- 单独发布的 `./invariant` 伴生插件会对照会话日志校验每次计划的重试——点名当前打开轮次与最新闭合步骤、匹配失败请求的持久提供方,并要求每个 `llm/retry-started` 事件点名一次带相同重试 id、轮次、步骤与重试编号的先前计划尝试。
|
|
48
148
|
|
|
49
|
-
|
|
50
|
-
- **always mode 会重试永久性失败**:身份验证、配额、无效请求、协议和无法恢复的上下文错误都会继续重试,直至成功、取消或 dispose;部署负责提供方特定的成本与延迟控制。
|
|
51
|
-
- **有限插件预算可叠加**:normal mode 只统计已配置 code 和确切提供方策略,上下文溢出压缩(compaction)则拥有独立预算。任何重叠策略都必须定义注册顺序行为。
|
|
52
|
-
- **恢复策略按 waterfall 顺序组合**:always mode 会先接受下游重试,再应用自己的回退。后续策略如果忽略取消且永不结算,也会阻止回退、轮次完全停稳和插件 dispose 完成。
|
|
53
|
-
- **`llm/retry` 记录调度,不是完成**:后续步骤与轮次事件用于确立成功、耗尽或取消。
|
|
149
|
+
</details>
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { z as z$1 } from "zod";
|
|
3
4
|
//#region lib/types/brand.js
|
|
4
5
|
/**
|
|
5
6
|
* Brand an implementation-minted retry-chain identity.
|
|
@@ -18,7 +19,7 @@ function RetryId(id) {
|
|
|
18
19
|
* @module @deepseek-ai/dsh-llm-retry
|
|
19
20
|
*/
|
|
20
21
|
const name = "llm-retry";
|
|
21
|
-
const inject = ["agents"];
|
|
22
|
+
const inject = ["agents", "sessionProjections"];
|
|
22
23
|
/** Runtime schema for {@link Config}. */
|
|
23
24
|
const Config = z.object({});
|
|
24
25
|
function validateConfig(config) {
|
|
@@ -61,6 +62,9 @@ function retryPolicyKey(policy) {
|
|
|
61
62
|
policy.jitterRatio
|
|
62
63
|
]);
|
|
63
64
|
}
|
|
65
|
+
function retryStateKey(provider, policyKey) {
|
|
66
|
+
return JSON.stringify([provider, policyKey]);
|
|
67
|
+
}
|
|
64
68
|
function cancellableDelay(delayMs, signal) {
|
|
65
69
|
if (signal.aborted) return Promise.resolve(false);
|
|
66
70
|
return new Promise((resolve) => {
|
|
@@ -75,14 +79,32 @@ function cancellableDelay(delayMs, signal) {
|
|
|
75
79
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
76
80
|
});
|
|
77
81
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* @param internals - non-serializable deterministic hooks for tests.
|
|
83
|
-
*/
|
|
82
|
+
const llmRetryStateSchema = z$1.record(z$1.string(), z$1.object({
|
|
83
|
+
retry: z$1.number().int().nonnegative(),
|
|
84
|
+
retryId: z$1.string()
|
|
85
|
+
}));
|
|
84
86
|
function apply(ctx, config = {}, internals = {}) {
|
|
85
87
|
validateConfig(config);
|
|
88
|
+
ctx.sessionProjections.register({
|
|
89
|
+
key: "llmRetry",
|
|
90
|
+
stateVersion: 1,
|
|
91
|
+
stateSchema: llmRetryStateSchema,
|
|
92
|
+
init: () => ({}),
|
|
93
|
+
apply: (state, event) => {
|
|
94
|
+
if (event.type === "step/start" || event.type === "turn/end") return {};
|
|
95
|
+
if (event.type !== "llm/retry") return state;
|
|
96
|
+
const key = retryStateKey(event.data.provider, event.data.policyKey);
|
|
97
|
+
const entry = state[key];
|
|
98
|
+
if (entry?.retry === event.data.retry && entry.retryId === event.data.retryId) return state;
|
|
99
|
+
return {
|
|
100
|
+
...state,
|
|
101
|
+
[key]: {
|
|
102
|
+
retry: event.data.retry,
|
|
103
|
+
retryId: event.data.retryId
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
});
|
|
86
108
|
const random = internals.random ?? Math.random;
|
|
87
109
|
const lifetime = new AbortController();
|
|
88
110
|
const active = /* @__PURE__ */ new Set();
|
|
@@ -137,11 +159,11 @@ function apply(ctx, config = {}, internals = {}) {
|
|
|
137
159
|
if (downstream.type === "decision" && downstream.decision?.kind === "retry") return downstream.decision;
|
|
138
160
|
} else if (!policy.retryableCodes.includes(failure.code)) return next();
|
|
139
161
|
const policyKey = retryPolicyKey(policy);
|
|
140
|
-
const
|
|
141
|
-
const previousRetry =
|
|
162
|
+
const previous = ctx.sessionProjections.stateOf(agent.session, "llmRetry")[retryStateKey(provider, policyKey)];
|
|
163
|
+
const previousRetry = previous?.retry ?? 0;
|
|
142
164
|
if (policy.mode === "normal" && previousRetry >= policy.maxRetries) return next();
|
|
143
165
|
const retry = previousRetry + 1;
|
|
144
|
-
const retryId =
|
|
166
|
+
const retryId = previous?.retryId ?? RetryId(randomUUID());
|
|
145
167
|
let delayMs;
|
|
146
168
|
if (failure.providerRetryAfterMs !== void 0 && Number.isFinite(failure.providerRetryAfterMs) && failure.providerRetryAfterMs > 0) if (failure.providerRetryAfterMs > policy.maxDelayMs) {
|
|
147
169
|
if (policy.mode === "normal") return next();
|
package/lib/types/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis';
|
|
8
8
|
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import { RetryId } from './brand.ts';
|
|
9
10
|
export type { LlmRetryEventData, LlmRetryStartedEventData } from './types.ts';
|
|
10
11
|
export { RetryId } from './brand.ts';
|
|
11
12
|
export declare const name = "llm-retry";
|
|
@@ -25,5 +26,16 @@ export interface RetryInternals {
|
|
|
25
26
|
* @param config - empty executor config; provider registrations own policy.
|
|
26
27
|
* @param internals - non-serializable deterministic hooks for tests.
|
|
27
28
|
*/
|
|
29
|
+
interface RetryStateEntry {
|
|
30
|
+
retry: number;
|
|
31
|
+
retryId: RetryId;
|
|
32
|
+
}
|
|
33
|
+
type LlmRetryState = Record<string, RetryStateEntry>;
|
|
34
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
35
|
+
interface SessionProjectionStateMap {
|
|
36
|
+
/** Retry state for the current step by provider and policy. */
|
|
37
|
+
llmRetry: LlmRetryState;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
28
40
|
export declare function apply(ctx: Context, config?: Config, internals?: RetryInternals): void;
|
|
29
41
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/index.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import { z as zod } from 'zod';
|
|
9
10
|
import { RetryId } from "./brand.js";
|
|
10
11
|
export { RetryId } from "./brand.js";
|
|
11
12
|
export const name = 'llm-retry';
|
|
12
|
-
export const inject = ['agents'];
|
|
13
|
+
export const inject = ['agents', 'sessionProjections'];
|
|
13
14
|
/** Runtime schema for {@link Config}. */
|
|
14
15
|
export const Config = z.object({});
|
|
15
16
|
function validateConfig(config) {
|
|
@@ -47,6 +48,9 @@ function retryPolicyKey(policy) {
|
|
|
47
48
|
policy.jitterRatio,
|
|
48
49
|
]);
|
|
49
50
|
}
|
|
51
|
+
function retryStateKey(provider, policyKey) {
|
|
52
|
+
return JSON.stringify([provider, policyKey]);
|
|
53
|
+
}
|
|
50
54
|
function cancellableDelay(delayMs, signal) {
|
|
51
55
|
if (signal.aborted)
|
|
52
56
|
return Promise.resolve(false);
|
|
@@ -62,14 +66,30 @@ function cancellableDelay(delayMs, signal) {
|
|
|
62
66
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
63
67
|
});
|
|
64
68
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*/
|
|
69
|
+
// The cast bridges the branded retry id, which Zod cannot express directly.
|
|
70
|
+
const llmRetryStateSchema = zod.record(zod.string(), zod.object({
|
|
71
|
+
retry: zod.number().int().nonnegative(),
|
|
72
|
+
retryId: zod.string(),
|
|
73
|
+
}));
|
|
71
74
|
export function apply(ctx, config = {}, internals = {}) {
|
|
72
75
|
validateConfig(config);
|
|
76
|
+
ctx.sessionProjections.register({
|
|
77
|
+
key: 'llmRetry',
|
|
78
|
+
stateVersion: 1,
|
|
79
|
+
stateSchema: llmRetryStateSchema,
|
|
80
|
+
init: () => ({}),
|
|
81
|
+
apply: (state, event) => {
|
|
82
|
+
if (event.type === 'step/start' || event.type === 'turn/end')
|
|
83
|
+
return {};
|
|
84
|
+
if (event.type !== 'llm/retry')
|
|
85
|
+
return state;
|
|
86
|
+
const key = retryStateKey(event.data.provider, event.data.policyKey);
|
|
87
|
+
const entry = state[key];
|
|
88
|
+
if (entry?.retry === event.data.retry && entry.retryId === event.data.retryId)
|
|
89
|
+
return state;
|
|
90
|
+
return { ...state, [key]: { retry: event.data.retry, retryId: event.data.retryId } };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
73
93
|
const random = internals.random ?? Math.random;
|
|
74
94
|
const lifetime = new AbortController();
|
|
75
95
|
const active = new Set();
|
|
@@ -135,16 +155,13 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
135
155
|
return next();
|
|
136
156
|
}
|
|
137
157
|
const policyKey = retryPolicyKey(policy);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
&& event.data.provider === provider
|
|
142
|
-
&& event.data.policyKey === policyKey);
|
|
143
|
-
const previousRetry = priorPolicyRetry?.data.retry ?? 0;
|
|
158
|
+
const retryState = ctx.sessionProjections.stateOf(agent.session, 'llmRetry');
|
|
159
|
+
const previous = retryState[retryStateKey(provider, policyKey)];
|
|
160
|
+
const previousRetry = previous?.retry ?? 0;
|
|
144
161
|
if (policy.mode === 'normal' && previousRetry >= policy.maxRetries)
|
|
145
162
|
return next();
|
|
146
163
|
const retry = previousRetry + 1;
|
|
147
|
-
const retryId =
|
|
164
|
+
const retryId = previous?.retryId ?? RetryId(randomUUID());
|
|
148
165
|
let delayMs;
|
|
149
166
|
if (failure.providerRetryAfterMs !== undefined
|
|
150
167
|
&& Number.isFinite(failure.providerRetryAfterMs)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-llm-retry",
|
|
3
3
|
"description": "Provider-routed LLM request retry policy for the DeepSeek Harness",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -36,34 +36,37 @@
|
|
|
36
36
|
],
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@deepseek-ai/dsh-
|
|
40
|
-
"@deepseek-ai/dsh-
|
|
41
|
-
"@deepseek-ai/dsh-
|
|
42
|
-
"@deepseek-ai/dsh-
|
|
43
|
-
"@deepseek-ai/dsh-
|
|
44
|
-
"@deepseek-ai/
|
|
45
|
-
"@deepseek-ai/
|
|
39
|
+
"@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
|
|
40
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
41
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
43
|
+
"@deepseek-ai/dsh-timeout": "^0.1.2-alpha.2",
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
45
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
|
|
46
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"
|
|
49
|
+
"zod": "^4.4.3",
|
|
50
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
|
-
"@deepseek-ai/dsh-brand": "^0.1.
|
|
52
|
-
"@deepseek-ai/cordis-plugin-include": "^1.0.
|
|
53
|
-
"@deepseek-ai/cordis-plugin-loader": "^1.0.
|
|
54
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
55
|
-
"@deepseek-ai/dsh-agent-loop": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
58
|
-
"@deepseek-ai/dsh-llm-deepseek": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-session
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-
|
|
66
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
67
|
-
"@deepseek-ai/dsh-
|
|
53
|
+
"@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
|
|
54
|
+
"@deepseek-ai/cordis-plugin-include": "^1.0.7",
|
|
55
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.3",
|
|
56
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-agent-loop": "^0.1.2-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
59
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-llm-deepseek": "^0.1.2-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-llm-mock-server": "^0.1.2-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-session-persistence-sqlite": "^0.1.2-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.2-alpha.2",
|
|
65
|
+
"@deepseek-ai/dsh-timeout": "^0.1.2-alpha.2",
|
|
66
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
|
|
67
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2",
|
|
68
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
69
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
|
|
70
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.2-alpha.2"
|
|
68
71
|
}
|
|
69
72
|
}
|