@qilin-agent/llm 0.0.0-stage → 3.1.4

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.
@@ -0,0 +1,246 @@
1
+ /** Message value types, identity, and immutable construction helpers. */
2
+ import type { MessageId, ToolCallId } from './brand.ts';
3
+ import type { ContentBlock } from './types.ts';
4
+ /** Provider/model identity and adapter-private replay data for an assistant message. */
5
+ export interface AssistantProviderMetadata {
6
+ /** Provider route that produced the message. */
7
+ provider: string;
8
+ /** Provider model id that produced the message. */
9
+ model: string;
10
+ /**
11
+ * Lossless-JSON adapter state needed to replay the provider response.
12
+ * `LlmRuntime` exposes it to a target adapter only when that adapter instance
13
+ * currently owns both this historical provider and the target provider.
14
+ */
15
+ replayState?: unknown;
16
+ }
17
+ /** Required source of an assistant message produced by a routed model. */
18
+ export interface ModelMessageSource extends AssistantProviderMetadata {
19
+ kind: 'model';
20
+ }
21
+ /** Required source of a tool-role message carrying one tool result. */
22
+ export interface ToolMessageSource {
23
+ kind: 'tool';
24
+ callId: ToolCallId;
25
+ }
26
+ /** Required source of a system-role message produced by the system-prompt plugin. */
27
+ export interface SystemPromptMessageSource {
28
+ kind: 'system-prompt';
29
+ }
30
+ /**
31
+ * The kind of information in producer-supplied context, declared by the
32
+ * producer in the same `MessageSource`.
33
+ *
34
+ * `MessageSource.kind` answers *who produced this*; `form` answers *what kind
35
+ * of thing it is*, and the two axes are deliberately independent — several
36
+ * producers share one form, and one producer may emit more than one form over
37
+ * a session.
38
+ *
39
+ * The vocabulary is SEMANTIC, never visual: a value states that the content is
40
+ * a file's instructions or a catalog of available items, and a consumer decides
41
+ * what that looks like. Colors, icons, ordering, and collapse defaults are the
42
+ * consumer's business and must not enter this union. It grows one value at a
43
+ * time as producers gain the structured fields their form needs; an absent or
44
+ * unknown value is the documented default, presented as opaque content.
45
+ */
46
+ export type ContextForm =
47
+ /** Instructions read out of workspace files the model is expected to follow. */
48
+ 'instructions'
49
+ /** A catalog of items available in this session, republished as it changes. */
50
+ | 'catalog'
51
+ /** Current state, where a later snapshot from the same producer supersedes an earlier one. */
52
+ | 'snapshot'
53
+ /** A one-off account of something that just happened; it supersedes nothing. */
54
+ | 'notice'
55
+ /** A message another agent addressed to this one. */
56
+ | 'relay'
57
+ /** Material lifted out of another session's log, possibly reduced on the way in. */
58
+ | 'recall';
59
+ /** One named contribution to a `snapshot`-form context, in assembly order. */
60
+ export interface ContextSnapshotSection {
61
+ /** The contributing subsystem's name. */
62
+ readonly name: string;
63
+ /** That contribution's model-facing text, exactly as assembled. */
64
+ readonly text: string;
65
+ }
66
+ /**
67
+ * Producer-declared {@link ContextForm} and the fields that form requires,
68
+ * mixed into the source types that carry one.
69
+ *
70
+ * Discriminated by `form` so a producer cannot select a form without the
71
+ * fields needed to present it: a `notice` must record its one-line
72
+ * account, a `snapshot` its sections. Omitting `form` stays valid — an
73
+ * undeclared context is the documented default.
74
+ */
75
+ export type ContextFormed = {
76
+ readonly form?: never;
77
+ } | {
78
+ readonly form: 'instructions';
79
+ } | {
80
+ readonly form: 'catalog';
81
+ } | {
82
+ readonly form: 'snapshot';
83
+ /** The named contributions this snapshot assembled, in order. */
84
+ readonly sections: readonly ContextSnapshotSection[];
85
+ } | {
86
+ readonly form: 'notice';
87
+ /** One-line account of what happened, shown without expanding the row. */
88
+ readonly summary: string;
89
+ } | {
90
+ readonly form: 'relay';
91
+ } | {
92
+ readonly form: 'recall';
93
+ };
94
+ /**
95
+ * Where a message (or injected content) came from, in the harness's own
96
+ * vocabulary. Merge-extensible sum type — each producer declares its own
97
+ * `kind` in its own module; there is no shared catch-all `plugin` kind.
98
+ * Model and tool sources answer their role messages; user messages carry any
99
+ * producer's kind, and consumers fall through unknown kinds.
100
+ */
101
+ export interface MessageSourceMap {
102
+ user: {
103
+ kind: 'user';
104
+ };
105
+ model: ModelMessageSource;
106
+ tool: ToolMessageSource;
107
+ 'system-prompt': SystemPromptMessageSource;
108
+ }
109
+ /**
110
+ * Bound for a `notice` summary. Producers commit the one-line account to the
111
+ * durable log; its inputs — task labels, goal objectives, tool arguments —
112
+ * are caller text with no length of their own.
113
+ */
114
+ export declare const CONTEXT_SUMMARY_MAX_CHARS = 120;
115
+ /**
116
+ * Bound one `notice` summary to {@link CONTEXT_SUMMARY_MAX_CHARS}.
117
+ * @param summary - the producer's one-line account, of any length.
118
+ * @returns the account, ellipsized when it exceeds the bound.
119
+ */
120
+ export declare function boundContextSummary(summary: string): string;
121
+ /** Any known message source, derived from {@link MessageSourceMap}; switch on `kind` and fall through unknowns (merge-extensible). */
122
+ export type MessageSource = MessageSourceMap[keyof MessageSourceMap];
123
+ /** Shared immutable fields of every conversation message. */
124
+ interface MessageBase {
125
+ /** Stable identity preserved across every representation boundary. */
126
+ readonly id: MessageId;
127
+ /** Exact model-facing blocks. */
128
+ readonly content: readonly ContentBlock[];
129
+ /** Required source fields supplied by the producer.
130
+ * @persistenceSource user developer
131
+ */
132
+ readonly source: MessageSource;
133
+ }
134
+ /** A rendered system prompt attributed to the system-prompt producer; empty content sends no prompt. */
135
+ export interface SystemMessage extends MessageBase {
136
+ readonly role: 'system';
137
+ readonly source: MessageSourceMap['system-prompt'];
138
+ }
139
+ /** Incremental agent session changes in conversation order, currently tool additions and removals. */
140
+ export interface DeveloperMessage extends MessageBase {
141
+ readonly role: 'developer';
142
+ }
143
+ /** A user-role specialization of the shared message representation. */
144
+ export interface UserMessage extends MessageBase {
145
+ readonly role: 'user';
146
+ }
147
+ /** A model-produced assistant specialization of the shared message representation. */
148
+ export interface AssistantMessage extends MessageBase {
149
+ readonly role: 'assistant';
150
+ readonly source: ModelMessageSource;
151
+ }
152
+ /** A first-class tool-role message carrying the result of one tool invocation. */
153
+ export interface ToolResultMessage extends MessageBase {
154
+ readonly role: 'tool';
155
+ readonly source: ToolMessageSource;
156
+ /** Provider-issued id of the tool call this message answers. */
157
+ readonly toolCallId: ToolCallId;
158
+ /** Whether the tool invocation failed. */
159
+ readonly isError?: boolean;
160
+ }
161
+ /**
162
+ * The conversation messages persisted by Session, keyed by role. This map is
163
+ * closed because every model-visible role must have a durable Session event
164
+ * and an adapter projection.
165
+ */
166
+ export interface MessageRoleMap {
167
+ system: SystemMessage;
168
+ developer: DeveloperMessage;
169
+ user: UserMessage;
170
+ assistant: AssistantMessage;
171
+ tool: ToolResultMessage;
172
+ }
173
+ /** Any persisted conversation message, discriminated by its `role`. */
174
+ export type Message = MessageRoleMap[keyof MessageRoleMap];
175
+ type NewMessage = {
176
+ [Role in keyof MessageRoleMap]: Omit<MessageRoleMap[Role], 'id'>;
177
+ }[keyof MessageRoleMap];
178
+ type NewDeveloperMessage = Omit<DeveloperMessage, 'id' | 'role'>;
179
+ type NewUserMessage = Omit<UserMessage, 'id' | 'role'>;
180
+ type NewAssistantMessage = Omit<AssistantMessage, 'id' | 'role' | 'source'> & {
181
+ readonly source: Omit<ModelMessageSource, 'kind'> & {
182
+ readonly kind?: never;
183
+ };
184
+ };
185
+ /**
186
+ * Detach and deep-freeze a message whose identity already exists.
187
+ * @param message - complete message, including its stable identity.
188
+ * @returns an immutable snapshot that preserves the identity.
189
+ */
190
+ export declare function freezeMessage<T extends Message>(message: T): T;
191
+ /**
192
+ * Create one identified message and freeze it before publication.
193
+ * @param input - complete role, content, and source for a new message.
194
+ * @returns an immutable message with a fresh stable identity.
195
+ */
196
+ export declare function createMessage<T extends NewMessage>(input: T & {
197
+ readonly id?: never;
198
+ }): T & Pick<Message, 'id'>;
199
+ /**
200
+ * Create an identified, immutable developer message.
201
+ * @param input - content and producer source for the new message.
202
+ * @returns a detached developer message with a fresh identity.
203
+ */
204
+ export declare function createDeveloperMessage<T extends NewDeveloperMessage>(input: T & {
205
+ readonly id?: never;
206
+ readonly role?: never;
207
+ }): T & Pick<DeveloperMessage, 'id' | 'role'>;
208
+ /**
209
+ * Create one identified user-role message and freeze it before publication.
210
+ * @param input - complete content and source for a new user message.
211
+ * @returns an immutable user message with a fresh stable identity.
212
+ */
213
+ export declare function createUserMessage<T extends NewUserMessage>(input: T & {
214
+ readonly id?: never;
215
+ readonly role?: never;
216
+ }): T & Pick<UserMessage, 'id' | 'role'>;
217
+ /**
218
+ * Create one identified model-produced assistant message and freeze it before publication.
219
+ * @param input - complete content plus the provider, model, and optional replay state for a new assistant message.
220
+ * @returns an immutable assistant message with fixed role/source tags and a fresh stable identity.
221
+ */
222
+ export declare function createAssistantMessage(input: NewAssistantMessage & {
223
+ readonly id?: never;
224
+ readonly role?: never;
225
+ }): AssistantMessage;
226
+ /**
227
+ * Create and freeze one identified system-role message holding a rendered
228
+ * system prompt.
229
+ * @param text - the complete rendered prompt; `''` records "no system prompt".
230
+ * @returns an immutable system message with a fresh stable identity.
231
+ */
232
+ export declare function createSystemMessage(text: string): SystemMessage;
233
+ /** Input whose acceptance creates one tool-result message. */
234
+ export interface ToolResultMessageInput {
235
+ readonly callId: ToolCallId;
236
+ readonly content: readonly ContentBlock[];
237
+ readonly isError: boolean;
238
+ }
239
+ /**
240
+ * Create and freeze one identified tool-result message.
241
+ * @param input - call identity, raw result blocks, and outcome.
242
+ * @returns an immutable tool-role message that answers the tool call.
243
+ */
244
+ export declare function createToolResultMessage(input: ToolResultMessageInput): ToolResultMessage;
245
+ export {};
246
+ //# sourceMappingURL=message.d.ts.map
@@ -0,0 +1,101 @@
1
+ /** Message value types, identity, and immutable construction helpers. */
2
+ import { randomUUID } from '@qilin-agent/util-crypto';
3
+ import { brandString } from '@qilin-agent/brand';
4
+ import { deepFreeze } from '@qilin-agent/util-values';
5
+ /**
6
+ * Bound for a `notice` summary. Producers commit the one-line account to the
7
+ * durable log; its inputs — task labels, goal objectives, tool arguments —
8
+ * are caller text with no length of their own.
9
+ */
10
+ export const CONTEXT_SUMMARY_MAX_CHARS = 120;
11
+ /**
12
+ * Bound one `notice` summary to {@link CONTEXT_SUMMARY_MAX_CHARS}.
13
+ * @param summary - the producer's one-line account, of any length.
14
+ * @returns the account, ellipsized when it exceeds the bound.
15
+ */
16
+ export function boundContextSummary(summary) {
17
+ return summary.length <= CONTEXT_SUMMARY_MAX_CHARS
18
+ ? summary
19
+ : `${summary.slice(0, CONTEXT_SUMMARY_MAX_CHARS - 1)}…`;
20
+ }
21
+ /**
22
+ * Detach and deep-freeze a message whose identity already exists.
23
+ * @param message - complete message, including its stable identity.
24
+ * @returns an immutable snapshot that preserves the identity.
25
+ */
26
+ export function freezeMessage(message) {
27
+ return deepFreeze(structuredClone(message));
28
+ }
29
+ /**
30
+ * Create one identified message and freeze it before publication.
31
+ * @param input - complete role, content, and source for a new message.
32
+ * @returns an immutable message with a fresh stable identity.
33
+ */
34
+ export function createMessage(input) {
35
+ return deepFreeze(structuredClone({
36
+ ...input,
37
+ id: brandString(randomUUID()),
38
+ }));
39
+ }
40
+ /**
41
+ * Create an identified, immutable developer message.
42
+ * @param input - content and producer source for the new message.
43
+ * @returns a detached developer message with a fresh identity.
44
+ */
45
+ export function createDeveloperMessage(input) {
46
+ return createMessage({ ...input, role: 'developer' });
47
+ }
48
+ /**
49
+ * Create one identified user-role message and freeze it before publication.
50
+ * @param input - complete content and source for a new user message.
51
+ * @returns an immutable user message with a fresh stable identity.
52
+ */
53
+ export function createUserMessage(input) {
54
+ return createMessage({
55
+ ...input,
56
+ role: 'user',
57
+ });
58
+ }
59
+ /**
60
+ * Create one identified model-produced assistant message and freeze it before publication.
61
+ * @param input - complete content plus the provider, model, and optional replay state for a new assistant message.
62
+ * @returns an immutable assistant message with fixed role/source tags and a fresh stable identity.
63
+ */
64
+ export function createAssistantMessage(input) {
65
+ return createMessage({
66
+ role: 'assistant',
67
+ content: input.content,
68
+ source: {
69
+ kind: 'model',
70
+ ...input.source,
71
+ },
72
+ });
73
+ }
74
+ /**
75
+ * Create and freeze one identified system-role message holding a rendered
76
+ * system prompt.
77
+ * @param text - the complete rendered prompt; `''` records "no system prompt".
78
+ * @returns an immutable system message with a fresh stable identity.
79
+ */
80
+ export function createSystemMessage(text) {
81
+ return createMessage({
82
+ role: 'system',
83
+ content: text.length === 0 ? [] : [{ type: 'text', text }],
84
+ source: { kind: 'system-prompt' },
85
+ });
86
+ }
87
+ /**
88
+ * Create and freeze one identified tool-result message.
89
+ * @param input - call identity, raw result blocks, and outcome.
90
+ * @returns an immutable tool-role message that answers the tool call.
91
+ */
92
+ export function createToolResultMessage(input) {
93
+ return createMessage({
94
+ role: 'tool',
95
+ source: { kind: 'tool', callId: input.callId },
96
+ toolCallId: input.callId,
97
+ content: input.content,
98
+ isError: input.isError,
99
+ });
100
+ }
101
+ //# sourceMappingURL=message.js.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Provider-owned request-retry policy configuration and resolution.
3
+ *
4
+ * Adapters expose one resolved policy per registered provider route; the
5
+ * optional qilin-llm-retry plugin executes it on the agent's failed-step extension point.
6
+ *
7
+ * @module @qilin-agent/llm/retry-policy
8
+ */
9
+ import z from '@qilin-agent/schemastery';
10
+ /** Bounded exponential backoff with symmetric jitter around each local delay. */
11
+ export interface BackoffConfig {
12
+ /** Initial local exponential-backoff delay in milliseconds (default 500). */
13
+ initialDelayMs?: number;
14
+ /** Maximum locally scheduled or accepted provider delay in milliseconds (default 10000). */
15
+ maxDelayMs?: number;
16
+ /** Symmetric random multiplier range around one (default 0.1). */
17
+ jitterRatio?: number;
18
+ }
19
+ /** Current bounded transient retry behavior for one provider route. */
20
+ export interface NormalRetryPolicyConfig {
21
+ /** Retry only configured transient failure codes. */
22
+ mode: 'normal';
23
+ /** Maximum eligible retries after the first request (default 5). */
24
+ maxRetries?: number;
25
+ /** Stable failure codes eligible for this policy. */
26
+ retryableCodes?: string[];
27
+ /** Local exponential-backoff and jitter configuration. */
28
+ backoff?: BackoffConfig;
29
+ }
30
+ /** Unbounded retry behavior for every model-request failure on one provider route. */
31
+ export interface AlwaysRetryPolicyConfig {
32
+ /** Retry every model-request failure until success, cancellation, or disposal. */
33
+ mode: 'always';
34
+ /** Local exponential-backoff and jitter configuration. */
35
+ backoff?: BackoffConfig;
36
+ }
37
+ /** Provider-owned model-request retry policy configuration. */
38
+ export type RetryPolicyConfig = NormalRetryPolicyConfig | AlwaysRetryPolicyConfig;
39
+ /** Fully resolved backoff shared by both retry modes. */
40
+ export interface ResolvedRetryBackoff {
41
+ readonly initialDelayMs: number;
42
+ readonly maxDelayMs: number;
43
+ readonly jitterRatio: number;
44
+ }
45
+ /** Fully resolved bounded transient retry policy. */
46
+ export interface ResolvedNormalRetryPolicy extends ResolvedRetryBackoff {
47
+ readonly mode: 'normal';
48
+ readonly maxRetries: number;
49
+ readonly retryableCodes: readonly string[];
50
+ }
51
+ /** Fully resolved unbounded retry policy. */
52
+ export interface ResolvedAlwaysRetryPolicy extends ResolvedRetryBackoff {
53
+ readonly mode: 'always';
54
+ }
55
+ /** Immutable provider policy captured when its adapter route is registered. */
56
+ export type ResolvedRetryPolicy = ResolvedNormalRetryPolicy | ResolvedAlwaysRetryPolicy;
57
+ /** Cordis schema embedded by each concrete provider configuration. */
58
+ export declare const RetryPolicySchema: z<RetryPolicyConfig>;
59
+ /**
60
+ * Validate, default, and detach one provider-owned retry policy.
61
+ * @param config - optional provider configuration; omission selects normal defaults.
62
+ * @param path - diagnostic path naming the provider config that owns the value.
63
+ * @returns an immutable policy safe to capture in provider registration state.
64
+ */
65
+ export declare function resolveRetryPolicy(config: RetryPolicyConfig | undefined, path: string): ResolvedRetryPolicy;
66
+ //# sourceMappingURL=retry-policy.d.ts.map
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Provider-owned request-retry policy configuration and resolution.
3
+ *
4
+ * Adapters expose one resolved policy per registered provider route; the
5
+ * optional qilin-llm-retry plugin executes it on the agent's failed-step extension point.
6
+ *
7
+ * @module @qilin-agent/llm/retry-policy
8
+ */
9
+ import z from '@qilin-agent/schemastery';
10
+ import { MAX_TIMER_DELAY_MS } from '@qilin-agent/timeout';
11
+ import { EMPTY_RESPONSE_CODE } from "./error.js";
12
+ const DEFAULT_MAX_RETRIES = 5;
13
+ const DEFAULT_INITIAL_DELAY_MS = 500;
14
+ const DEFAULT_MAX_DELAY_MS = 10_000;
15
+ const DEFAULT_JITTER_RATIO = 0.1;
16
+ const DEFAULT_RETRYABLE_CODES = Object.freeze([
17
+ EMPTY_RESPONSE_CODE,
18
+ 'RATE_LIMIT',
19
+ 'SERVER',
20
+ 'TIMEOUT',
21
+ 'TRANSPORT',
22
+ ]);
23
+ const backoffSchema = z.object({
24
+ initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
25
+ maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
26
+ jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO),
27
+ });
28
+ const normalPolicySchema = z.object({
29
+ mode: z.const('normal').required(),
30
+ maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
31
+ retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
32
+ backoff: backoffSchema,
33
+ });
34
+ const alwaysPolicySchema = z.object({
35
+ mode: z.const('always').required(),
36
+ backoff: backoffSchema,
37
+ });
38
+ /** Cordis schema embedded by each concrete provider configuration. */
39
+ export const RetryPolicySchema = z.union([
40
+ normalPolicySchema,
41
+ alwaysPolicySchema,
42
+ ]);
43
+ const NORMAL_POLICY_KEYS = new Set([
44
+ 'mode', 'maxRetries', 'retryableCodes', 'backoff',
45
+ ]);
46
+ // Layered configuration can retain normal-only fields after switching modes;
47
+ // always mode ignores those inactive values while still rejecting unknown keys.
48
+ const ALWAYS_POLICY_KEYS = new Set([
49
+ 'mode', 'maxRetries', 'retryableCodes', 'backoff',
50
+ ]);
51
+ const BACKOFF_KEYS = new Set(['initialDelayMs', 'maxDelayMs', 'jitterRatio']);
52
+ function validateKeys(value, allowed, path) {
53
+ for (const key of Object.keys(value)) {
54
+ if (!allowed.has(key))
55
+ throw new Error(`${path}: unknown key "${key}"`);
56
+ }
57
+ }
58
+ function resolveBackoff(config, path) {
59
+ if (config !== undefined)
60
+ validateKeys(config, BACKOFF_KEYS, path);
61
+ const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
62
+ const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
63
+ const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
64
+ if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > MAX_TIMER_DELAY_MS) {
65
+ throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
66
+ }
67
+ if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > MAX_TIMER_DELAY_MS) {
68
+ throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
69
+ }
70
+ if (initialDelayMs > maxDelayMs) {
71
+ throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
72
+ }
73
+ if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) {
74
+ throw new Error(`${path}.jitterRatio must be between 0 and 1`);
75
+ }
76
+ return Object.freeze({ initialDelayMs, maxDelayMs, jitterRatio });
77
+ }
78
+ /**
79
+ * Validate, default, and detach one provider-owned retry policy.
80
+ * @param config - optional provider configuration; omission selects normal defaults.
81
+ * @param path - diagnostic path naming the provider config that owns the value.
82
+ * @returns an immutable policy safe to capture in provider registration state.
83
+ */
84
+ export function resolveRetryPolicy(config, path) {
85
+ if (config === undefined) {
86
+ return Object.freeze({
87
+ mode: 'normal',
88
+ maxRetries: DEFAULT_MAX_RETRIES,
89
+ retryableCodes: DEFAULT_RETRYABLE_CODES,
90
+ ...resolveBackoff(undefined, `${path}.backoff`),
91
+ });
92
+ }
93
+ switch (config.mode) {
94
+ case 'normal': {
95
+ validateKeys(config, NORMAL_POLICY_KEYS, path);
96
+ const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
97
+ const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
98
+ if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {
99
+ throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
100
+ }
101
+ if (retryableCodes.length === 0) {
102
+ throw new Error(`${path}.retryableCodes must not be empty`);
103
+ }
104
+ if (retryableCodes.some(code => typeof code !== 'string' || code.length === 0)) {
105
+ throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
106
+ }
107
+ if (new Set(retryableCodes).size !== retryableCodes.length) {
108
+ throw new Error(`${path}.retryableCodes must not contain duplicates`);
109
+ }
110
+ return Object.freeze({
111
+ mode: 'normal',
112
+ maxRetries,
113
+ retryableCodes: Object.freeze([...retryableCodes]),
114
+ ...resolveBackoff(config.backoff, `${path}.backoff`),
115
+ });
116
+ }
117
+ case 'always':
118
+ validateKeys(config, ALWAYS_POLICY_KEYS, path);
119
+ return Object.freeze({
120
+ mode: 'always',
121
+ ...resolveBackoff(config.backoff, `${path}.backoff`),
122
+ });
123
+ default:
124
+ throw new Error(`${path}.mode must be "normal" or "always"`);
125
+ }
126
+ }
127
+ //# sourceMappingURL=retry-policy.js.map