@deepseek-ai/dsh-llm-retry 0.2.0-rc.2 → 0.2.1-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 CHANGED
@@ -36,8 +36,8 @@
36
36
  en: fc4f15a14435698c
37
37
  zh: 3c6d83f5ba04ec0e
38
38
  /deepseek-ai-dsh-llm-retry/understand-the-implementation/source-map:
39
- en: f34b551859b68c88
40
- zh: 813ffa32497b1d9a
39
+ en: 9b79cdd6597930e0
40
+ zh: 0838f3efbc1e83ac
41
41
  /deepseek-ai-dsh-llm-retry/understand-the-implementation/recovery-flow:
42
42
  en: 85bace285b029101
43
43
  zh: 6e0237450ca6ccc0
@@ -66,5 +66,5 @@
66
66
  en: bef1baa545592b9c
67
67
  zh: 61984a1e10de8294
68
68
  /deepseek-ai-dsh-llm-retry/known-limitations-and-deferred-work/dev-note:
69
- en: ff38027230617ebb
70
- zh: 4a7913e851407b0c
69
+ en: add61ee91e4ae8e4
70
+ zh: 3877c147b541b2c9
package/README.md CHANGED
@@ -76,7 +76,6 @@ The executor is built on one rule: **durable before wait, open-step boundaries.*
76
76
  | File | Role |
77
77
  |---|---|
78
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
79
  | [`src/types.ts`](src/types.ts) | Browser-safe `llm/retry` and `llm/retry-started` event payload types |
81
80
  | [`src/brand.ts`](src/brand.ts) | The `RetryId` brand shared by the event payloads |
82
81
 
@@ -144,6 +143,5 @@ These limits define where the executor stops and future work begins. They are cu
144
143
  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
144
 
146
145
  - 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
146
 
149
147
  </details>
package/README.zh.md CHANGED
@@ -76,7 +76,6 @@ kind: "package-reference"
76
76
  | 文件 | 职责 |
77
77
  |---|---|
78
78
  | [`src/index.ts`](src/index.ts) | 函数插件:waterfall 监听器、策略查找、退避、持久事件追加 |
79
- | [`src/history.ts`](src/history.ts) | 从会话日志查找持久重试历史 |
80
79
  | [`src/types.ts`](src/types.ts) | 浏览器安全的 `llm/retry` 与 `llm/retry-started` 事件载荷类型 |
81
80
  | [`src/brand.ts`](src/brand.ts) | 事件载荷共享的 `RetryId` 品牌 |
82
81
 
@@ -144,6 +143,5 @@ kind: "package-reference"
144
143
  本开发备注是不具权威性的工作上下文:维护者备注与开放问题。已交付的行为与既定理由以上文、包代码和相关 Agent Note 为准。
145
144
 
146
145
  - 重试编号只在同一提供方与完整策略键的事件间延续,因此限额、code 成员或退避不同的路由替换会开启自己的历史;该键包含每个影响行为的字段,并因资格判断使用集合成员而对 normal mode code 排序。
147
- - 单独发布的 `./invariant` 伴生插件会对照会话日志校验每次计划的重试——点名当前打开轮次与最新闭合步骤、匹配失败请求的持久提供方,并要求每个 `llm/retry-started` 事件点名一次带相同重试 id、轮次、步骤与重试编号的先前计划尝试。
148
146
 
149
147
  </details>
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.2.0-rc.2",
4
+ "version": "0.2.1-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -18,10 +18,6 @@
18
18
  "types": "./lib/types/index.d.ts",
19
19
  "default": "./lib/index.js"
20
20
  },
21
- "./invariant": {
22
- "types": "./lib/types/invariant.d.ts",
23
- "default": "./lib/invariant.js"
24
- },
25
21
  "./types": {
26
22
  "types": "./lib/types/types.d.ts",
27
23
  "default": "./lib/types/types.js"
@@ -30,43 +26,40 @@
30
26
  },
31
27
  "files": [
32
28
  "lib/index.js",
33
- "lib/invariant.js",
34
29
  "lib/types/**/*.js",
35
30
  "lib/types/**/*.d.ts"
36
31
  ],
37
32
  "license": "MIT",
38
33
  "peerDependencies": {
39
- "@deepseek-ai/dsh-brand": "0.2.0-rc.2",
40
- "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
41
- "@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
42
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
43
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
44
- "@deepseek-ai/dsh-timeout": "0.2.0-rc.2",
45
- "@deepseek-ai/cordis": "~4.0.4",
46
- "@deepseek-ai/dsh-session-projection": "0.2.0-rc.2"
34
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
35
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
36
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
37
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
38
+ "@deepseek-ai/dsh-timeout": "0.2.1-alpha.2",
39
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
40
+ "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.2"
47
41
  },
48
42
  "dependencies": {
49
- "zod": "^4.4.3",
50
- "@deepseek-ai/schemastery": "~3.18.4"
43
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.1",
44
+ "zod": "^4.4.3"
51
45
  },
52
46
  "devDependencies": {
53
- "@deepseek-ai/dsh-brand": "0.2.0-rc.2",
54
- "@deepseek-ai/cordis-plugin-include": "~1.0.9",
55
- "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
56
- "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
57
- "@deepseek-ai/dsh-agent-loop": "0.2.0-rc.2",
58
- "@deepseek-ai/dsh-agent-loop-testkit": "0.2.0-rc.2",
59
- "@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
60
- "@deepseek-ai/dsh-llm-deepseek": "0.2.0-rc.2",
61
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
62
- "@deepseek-ai/dsh-llm-deepseek-api-key": "0.2.0-rc.2",
63
- "@deepseek-ai/dsh-llm-mock-server": "0.2.0-rc.2",
64
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
65
- "@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
66
- "@deepseek-ai/dsh-session-persistence-jsonl": "0.2.0-rc.2",
67
- "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
68
- "@deepseek-ai/cordis": "~4.0.4",
69
- "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
70
- "@deepseek-ai/dsh-timeout": "0.2.0-rc.2"
47
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
48
+ "@deepseek-ai/cordis-plugin-include": "~1.0.10-alpha.1",
49
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.6-alpha.1",
50
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
51
+ "@deepseek-ai/dsh-agent-loop": "0.2.1-alpha.2",
52
+ "@deepseek-ai/dsh-agent-loop-testkit": "0.2.1-alpha.2",
53
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
54
+ "@deepseek-ai/dsh-llm-deepseek": "0.2.1-alpha.2",
55
+ "@deepseek-ai/dsh-llm-deepseek-api-key": "0.2.1-alpha.2",
56
+ "@deepseek-ai/dsh-llm-mock-server": "0.2.1-alpha.2",
57
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
58
+ "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.2",
59
+ "@deepseek-ai/dsh-session-persistence-jsonl": "0.2.1-alpha.2",
60
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-alpha.2",
61
+ "@deepseek-ai/dsh-timeout": "0.2.1-alpha.2",
62
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
63
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1"
71
64
  }
72
65
  }
package/lib/invariant.js DELETED
@@ -1,109 +0,0 @@
1
- import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
2
- //#region lib/types/history.js
3
- /** Durable request-route lookup for one open model step. @module @deepseek-ai/dsh-llm-retry/history */
4
- /**
5
- * Find the provider in force for one currently open step.
6
- * Request headers remain effective across turn boundaries until a newer full
7
- * snapshot changes them; every provider change requires a newer full snapshot.
8
- * @param events - session events ending inside the open step.
9
- * @param turn - turn that owns the failed step.
10
- * @param step - failed step whose provider is required.
11
- * @returns the provider from the request header in force for the step.
12
- */
13
- function providerForOpenStep(events, turn, step) {
14
- const stepStartIndex = events.findLastIndex((event) => event.type === "step/start" && event.data.turn === turn && event.data.step === step);
15
- if (stepStartIndex < 0 || events.slice(stepStartIndex + 1).some((event) => event.type === "step/end" || event.type === "turn/end")) return void 0;
16
- for (let index = events.length - 1; index >= 0; index -= 1) {
17
- const event = events[index];
18
- if (event.type === "request/header") return event.data.header.config.provider;
19
- }
20
- }
21
- //#endregion
22
- //#region lib/types/invariant.js
23
- /** Package-owned durable retry-event invariants. @module @deepseek-ai/dsh-llm-retry/invariant */
24
- const PACKAGE_NAME = "@deepseek-ai/dsh-llm-retry";
25
- /** Cordis companion plugin name. */
26
- const name = "llm-retry-invariant";
27
- /** Service required before the companion can reserve package ownership. */
28
- const inject = ["invariants"];
29
- /** Validate the complete provider-neutral failure payload at the durable boundary. */
30
- function validateFailure(value, fail) {
31
- if (typeof value !== "object" || value === null) fail("llm/retry failure must be an object");
32
- const failure = value;
33
- if (typeof failure.message !== "string" || failure.message.length === 0) fail("llm/retry failure.message must be a non-empty string");
34
- if (typeof failure.code !== "string" || failure.code.length === 0) fail("llm/retry failure.code must be a non-empty string");
35
- if (failure.status !== void 0 && (!Number.isInteger(failure.status) || failure.status < 100 || failure.status > 599)) fail("llm/retry failure.status must be an integer from 100 through 599 when present");
36
- if (failure.providerRetryAfterMs !== void 0 && (!Number.isFinite(failure.providerRetryAfterMs) || failure.providerRetryAfterMs <= 0)) fail("llm/retry failure.providerRetryAfterMs must be a positive finite number when present");
37
- if (failure.requestId !== void 0 && (typeof failure.requestId !== "string" || failure.requestId.length === 0)) fail("llm/retry failure.requestId must be a non-empty string when present");
38
- }
39
- /** Validate one retry record against the currently open request step. */
40
- function validateRetry(history, event, fail) {
41
- const { retryId, turn, step, provider, mode, policyKey, retry, delayMs } = event.data;
42
- if (typeof retryId !== "string" || retryId.length === 0) fail("llm/retry retryId must be a non-empty string");
43
- const failure = event.data.failure;
44
- validateFailure(failure, fail);
45
- if (!Number.isSafeInteger(retry) || retry < 1) fail("llm/retry retry must be a positive safe integer");
46
- if (typeof provider !== "string" || provider.length === 0) fail("llm/retry provider must be a non-empty string");
47
- if (typeof policyKey !== "string" || policyKey.length === 0) fail("llm/retry policyKey must be a non-empty string");
48
- switch (mode) {
49
- case "normal": {
50
- const { maxRetries } = event.data;
51
- if (!Number.isSafeInteger(maxRetries) || maxRetries < 1 || retry > maxRetries) fail(`llm/retry retry ${retry} must not exceed a positive safe maxRetries ${maxRetries}`);
52
- break;
53
- }
54
- case "always":
55
- if ("maxRetries" in event.data) fail("llm/retry always mode must omit maxRetries");
56
- break;
57
- default: fail(`llm/retry mode must be normal or always, got ${String(mode)}`);
58
- }
59
- if (typeof delayMs !== "number" || !Number.isFinite(delayMs) || delayMs < 0 || delayMs > MAX_TIMER_DELAY_MS) fail(`llm/retry delayMs must be a finite number within 0..${MAX_TIMER_DELAY_MS}`);
60
- const turnBoundary = history.findLast((prior) => prior.type === "turn/start" || prior.type === "turn/end");
61
- if (turnBoundary?.type !== "turn/start") fail("llm/retry must be appended inside an open turn");
62
- if (turn !== turnBoundary.data.turn) fail(`llm/retry names turn ${turn}, but the open turn is ${turnBoundary.data.turn}`);
63
- const stepBoundary = history.findLast((prior) => prior.type === "step/start" || prior.type === "step/end");
64
- if (stepBoundary?.type !== "step/start") fail("llm/retry must be appended inside an open step");
65
- if (step !== stepBoundary.data.step || turn !== stepBoundary.data.turn) fail(`llm/retry names turn ${turn}/step ${step}, but the open step is ${stepBoundary.data.turn}/${stepBoundary.data.step}`);
66
- const routedProvider = providerForOpenStep(history, turn, step);
67
- if (routedProvider !== provider) fail(`llm/retry provider ${provider} does not match the failed request provider ${String(routedProvider)}`);
68
- const priorPolicyRetry = history.findLast((prior) => prior.type === "llm/retry" && prior.data.turn === turn && prior.data.step === step && prior.data.provider === provider && prior.data.policyKey === policyKey);
69
- const expectedRetry = (priorPolicyRetry?.data.retry ?? 0) + 1;
70
- if (retry !== expectedRetry) fail(`llm/retry retry ${retry} must equal provider policy retry ${expectedRetry}`);
71
- if (priorPolicyRetry !== void 0 && priorPolicyRetry.data.retryId !== retryId) fail("llm/retry must preserve retryId across one provider-policy chain");
72
- if (priorPolicyRetry === void 0 && history.some((prior) => (prior.type === "llm/retry" || prior.type === "llm/retry-started") && prior.data.retryId === retryId)) fail(`llm/retry retryId ${JSON.stringify(retryId)} is already owned by another chain`);
73
- }
74
- /** Validate one wait-complete transition against its scheduled attempt. */
75
- function validateStarted(history, event, fail) {
76
- const { retryId, turn, step, retry } = event.data;
77
- if (typeof retryId !== "string" || retryId.length === 0) fail("llm/retry-started retryId must be a non-empty string");
78
- const scheduled = history.findLast((prior) => prior.type === "llm/retry" && prior.data.retryId === retryId && prior.data.retry === retry);
79
- if (scheduled === void 0) fail("llm/retry-started pairs no prior scheduled attempt");
80
- if (scheduled.data.turn !== turn || scheduled.data.step !== step) fail("llm/retry-started turn/step must match its scheduled attempt");
81
- if (history.some((prior) => prior.type === "llm/retry-started" && prior.data.retryId === retryId && prior.data.retry === retry)) fail("llm/retry-started repeats one scheduled attempt");
82
- }
83
- /** Validate every retry record already present in one loaded session. */
84
- function validateSession(session, fail) {
85
- const events = session.snapshotEvents();
86
- for (const [index, event] of events.entries()) if (event.type === "llm/retry") validateRetry(events.slice(0, index), event, fail);
87
- else if (event.type === "llm/retry-started") validateStarted(events.slice(0, index), event, fail);
88
- }
89
- /** Install validation for loaded and newly appended retry records. */
90
- const install = Object.assign((ctx, fail) => {
91
- for (const session of ctx.sessions.list()) validateSession(session, fail);
92
- ctx.on("session/created", (session) => {
93
- validateSession(session, fail);
94
- }, { global: true });
95
- ctx.on("internal/dispatch", (_mode, eventName, args) => {
96
- if (eventName !== "session/event") return;
97
- const [session, event] = args;
98
- if (event.type === "llm/retry") validateRetry(session.snapshotEvents(), event, fail);
99
- else if (event.type === "llm/retry-started") validateStarted(session.snapshotEvents(), event, fail);
100
- }, { global: true });
101
- }, { inject: ["sessions"] });
102
- /**
103
- * Register the LLM retry invariant companion.
104
- * @param ctx - Cordis context carrying the invariant service.
105
- * @returns the installed registration's disposer after setup succeeds.
106
- */
107
- const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
108
- //#endregion
109
- export { apply, inject, name };
@@ -1,13 +0,0 @@
1
- /** Durable request-route lookup for one open model step. @module @deepseek-ai/dsh-llm-retry/history */
2
- import type { SessionEvent } from '@deepseek-ai/dsh-session';
3
- /**
4
- * Find the provider in force for one currently open step.
5
- * Request headers remain effective across turn boundaries until a newer full
6
- * snapshot changes them; every provider change requires a newer full snapshot.
7
- * @param events - session events ending inside the open step.
8
- * @param turn - turn that owns the failed step.
9
- * @param step - failed step whose provider is required.
10
- * @returns the provider from the request header in force for the step.
11
- */
12
- export declare function providerForOpenStep(events: readonly SessionEvent[], turn: number, step: number): string | undefined;
13
- //# sourceMappingURL=history.d.ts.map
@@ -1,26 +0,0 @@
1
- /** Durable request-route lookup for one open model step. @module @deepseek-ai/dsh-llm-retry/history */
2
- /**
3
- * Find the provider in force for one currently open step.
4
- * Request headers remain effective across turn boundaries until a newer full
5
- * snapshot changes them; every provider change requires a newer full snapshot.
6
- * @param events - session events ending inside the open step.
7
- * @param turn - turn that owns the failed step.
8
- * @param step - failed step whose provider is required.
9
- * @returns the provider from the request header in force for the step.
10
- */
11
- export function providerForOpenStep(events, turn, step) {
12
- const stepStartIndex = events.findLastIndex(event => event.type === 'step/start'
13
- && event.data.turn === turn
14
- && event.data.step === step);
15
- if (stepStartIndex < 0 || events.slice(stepStartIndex + 1).some(event => event.type === 'step/end' || event.type === 'turn/end'))
16
- return undefined;
17
- for (let index = events.length - 1; index >= 0; index -= 1) {
18
- // The loop bounds prove this indexed read exists.
19
- // oxlint-disable-next-line typescript/no-non-null-assertion
20
- const event = events[index];
21
- if (event.type === 'request/header')
22
- return event.data.header.config.provider;
23
- }
24
- return undefined;
25
- }
26
- //# sourceMappingURL=history.js.map
@@ -1,13 +0,0 @@
1
- /** Package-owned durable retry-event invariants. @module @deepseek-ai/dsh-llm-retry/invariant */
2
- import type { Context } from '@deepseek-ai/cordis';
3
- /** Cordis companion plugin name. */
4
- export declare const name = "llm-retry-invariant";
5
- /** Service required before the companion can reserve package ownership. */
6
- export declare const inject: string[];
7
- /**
8
- * Register the LLM retry invariant companion.
9
- * @param ctx - Cordis context carrying the invariant service.
10
- * @returns the installed registration's disposer after setup succeeds.
11
- */
12
- export declare const apply: (ctx: Context) => Promise<() => void>;
13
- //# sourceMappingURL=invariant.d.ts.map
@@ -1,156 +0,0 @@
1
- /** Package-owned durable retry-event invariants. @module @deepseek-ai/dsh-llm-retry/invariant */
2
- import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
3
- import { providerForOpenStep } from "./history.js";
4
- const PACKAGE_NAME = '@deepseek-ai/dsh-llm-retry';
5
- /** Cordis companion plugin name. */
6
- export const name = 'llm-retry-invariant';
7
- /** Service required before the companion can reserve package ownership. */
8
- export const inject = ['invariants'];
9
- /** Validate the complete provider-neutral failure payload at the durable boundary. */
10
- function validateFailure(value, fail) {
11
- if (typeof value !== 'object' || value === null) {
12
- fail('llm/retry failure must be an object');
13
- }
14
- const failure = value;
15
- if (typeof failure.message !== 'string' || failure.message.length === 0) {
16
- fail('llm/retry failure.message must be a non-empty string');
17
- }
18
- if (typeof failure.code !== 'string' || failure.code.length === 0) {
19
- fail('llm/retry failure.code must be a non-empty string');
20
- }
21
- if (failure.status !== undefined
22
- && (!Number.isInteger(failure.status) || failure.status < 100 || failure.status > 599)) {
23
- fail('llm/retry failure.status must be an integer from 100 through 599 when present');
24
- }
25
- if (failure.providerRetryAfterMs !== undefined
26
- && (!Number.isFinite(failure.providerRetryAfterMs) || failure.providerRetryAfterMs <= 0)) {
27
- fail('llm/retry failure.providerRetryAfterMs must be a positive finite number when present');
28
- }
29
- if (failure.requestId !== undefined
30
- && (typeof failure.requestId !== 'string' || failure.requestId.length === 0)) {
31
- fail('llm/retry failure.requestId must be a non-empty string when present');
32
- }
33
- }
34
- /** Validate one retry record against the currently open request step. */
35
- function validateRetry(history, event, fail) {
36
- const { retryId, turn, step, provider, mode, policyKey, retry, delayMs } = event.data;
37
- if (typeof retryId !== 'string' || retryId.length === 0) {
38
- fail('llm/retry retryId must be a non-empty string');
39
- }
40
- const failure = event.data.failure;
41
- validateFailure(failure, fail);
42
- if (!Number.isSafeInteger(retry) || retry < 1) {
43
- fail('llm/retry retry must be a positive safe integer');
44
- }
45
- if (typeof provider !== 'string' || provider.length === 0) {
46
- fail('llm/retry provider must be a non-empty string');
47
- }
48
- if (typeof policyKey !== 'string' || policyKey.length === 0) {
49
- fail('llm/retry policyKey must be a non-empty string');
50
- }
51
- switch (mode) {
52
- case 'normal': {
53
- const { maxRetries } = event.data;
54
- if (!Number.isSafeInteger(maxRetries) || maxRetries < 1 || retry > maxRetries) {
55
- fail(`llm/retry retry ${retry} must not exceed a positive safe maxRetries ${maxRetries}`);
56
- }
57
- break;
58
- }
59
- case 'always':
60
- if ('maxRetries' in event.data)
61
- fail('llm/retry always mode must omit maxRetries');
62
- break;
63
- default:
64
- fail(`llm/retry mode must be normal or always, got ${String(mode)}`);
65
- }
66
- if (typeof delayMs !== 'number' || !Number.isFinite(delayMs)
67
- || delayMs < 0 || delayMs > MAX_TIMER_DELAY_MS) {
68
- fail(`llm/retry delayMs must be a finite number within 0..${MAX_TIMER_DELAY_MS}`);
69
- }
70
- const turnBoundary = history.findLast(prior => prior.type === 'turn/start' || prior.type === 'turn/end');
71
- if (turnBoundary?.type !== 'turn/start') {
72
- fail('llm/retry must be appended inside an open turn');
73
- }
74
- if (turn !== turnBoundary.data.turn) {
75
- fail(`llm/retry names turn ${turn}, but the open turn is ${turnBoundary.data.turn}`);
76
- }
77
- const stepBoundary = history.findLast(prior => prior.type === 'step/start' || prior.type === 'step/end');
78
- if (stepBoundary?.type !== 'step/start') {
79
- fail('llm/retry must be appended inside an open step');
80
- }
81
- if (step !== stepBoundary.data.step || turn !== stepBoundary.data.turn) {
82
- fail(`llm/retry names turn ${turn}/step ${step}, but the open step is ${stepBoundary.data.turn}/${stepBoundary.data.step}`);
83
- }
84
- const routedProvider = providerForOpenStep(history, turn, step);
85
- if (routedProvider !== provider) {
86
- fail(`llm/retry provider ${provider} does not match the failed request provider ${String(routedProvider)}`);
87
- }
88
- const priorPolicyRetry = history.findLast((prior) => prior.type === 'llm/retry'
89
- && prior.data.turn === turn
90
- && prior.data.step === step
91
- && prior.data.provider === provider
92
- && prior.data.policyKey === policyKey);
93
- const expectedRetry = (priorPolicyRetry?.data.retry ?? 0) + 1;
94
- if (retry !== expectedRetry) {
95
- fail(`llm/retry retry ${retry} must equal provider policy retry ${expectedRetry}`);
96
- }
97
- if (priorPolicyRetry !== undefined && priorPolicyRetry.data.retryId !== retryId) {
98
- fail('llm/retry must preserve retryId across one provider-policy chain');
99
- }
100
- if (priorPolicyRetry === undefined && history.some(prior => (prior.type === 'llm/retry' || prior.type === 'llm/retry-started')
101
- && prior.data.retryId === retryId)) {
102
- fail(`llm/retry retryId ${JSON.stringify(retryId)} is already owned by another chain`);
103
- }
104
- }
105
- /** Validate one wait-complete transition against its scheduled attempt. */
106
- function validateStarted(history, event, fail) {
107
- const { retryId, turn, step, retry } = event.data;
108
- if (typeof retryId !== 'string' || retryId.length === 0) {
109
- fail('llm/retry-started retryId must be a non-empty string');
110
- }
111
- const scheduled = history.findLast((prior) => prior.type === 'llm/retry' && prior.data.retryId === retryId && prior.data.retry === retry);
112
- if (scheduled === undefined)
113
- fail('llm/retry-started pairs no prior scheduled attempt');
114
- if (scheduled.data.turn !== turn || scheduled.data.step !== step) {
115
- fail('llm/retry-started turn/step must match its scheduled attempt');
116
- }
117
- if (history.some(prior => prior.type === 'llm/retry-started'
118
- && prior.data.retryId === retryId && prior.data.retry === retry)) {
119
- fail('llm/retry-started repeats one scheduled attempt');
120
- }
121
- }
122
- /** Validate every retry record already present in one loaded session. */
123
- function validateSession(session, fail) {
124
- // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
125
- const events = session.snapshotEvents();
126
- for (const [index, event] of events.entries()) {
127
- if (event.type === 'llm/retry')
128
- validateRetry(events.slice(0, index), event, fail);
129
- else if (event.type === 'llm/retry-started')
130
- validateStarted(events.slice(0, index), event, fail);
131
- }
132
- }
133
- /** Install validation for loaded and newly appended retry records. */
134
- const install = Object.assign((ctx, fail) => {
135
- for (const session of ctx.sessions.list())
136
- validateSession(session, fail);
137
- ctx.on('session/created', (session) => { validateSession(session, fail); }, { global: true });
138
- ctx.on('internal/dispatch', (_mode, eventName, args) => {
139
- if (eventName !== 'session/event')
140
- return;
141
- const [session, event] = args;
142
- // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
143
- if (event.type === 'llm/retry')
144
- validateRetry(session.snapshotEvents(), event, fail);
145
- // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
146
- else if (event.type === 'llm/retry-started')
147
- validateStarted(session.snapshotEvents(), event, fail);
148
- }, { global: true });
149
- }, { inject: ['sessions'] });
150
- /**
151
- * Register the LLM retry invariant companion.
152
- * @param ctx - Cordis context carrying the invariant service.
153
- * @returns the installed registration's disposer after setup succeeds.
154
- */
155
- export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
156
- //# sourceMappingURL=invariant.js.map