@qilin-agent/llm 0.0.0-stage → 3.1.3

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/lib/index.js ADDED
@@ -0,0 +1,2388 @@
1
+ import { createRequire } from "node:module";
2
+ import { Remote, RemoteError, TypertRemoteService } from "@qilin-agent/typert-protocol";
3
+ import { assertNever, deepFreeze, snapshotJsonValue } from "@qilin-agent/util-values";
4
+ import { randomUUID } from "@qilin-agent/util-crypto";
5
+ import { brandString } from "@qilin-agent/brand";
6
+ import z from "@qilin-agent/schemastery";
7
+ import { MAX_TIMER_DELAY_MS } from "@qilin-agent/timeout";
8
+ //#region lib/types/message.js
9
+ /** Message value types, identity, and immutable construction helpers. */
10
+ /**
11
+ * Bound for a `notice` summary. Producers commit the one-line account to the
12
+ * durable log; its inputs — task labels, goal objectives, tool arguments —
13
+ * are caller text with no length of their own.
14
+ */
15
+ const CONTEXT_SUMMARY_MAX_CHARS = 120;
16
+ /**
17
+ * Bound one `notice` summary to {@link CONTEXT_SUMMARY_MAX_CHARS}.
18
+ * @param summary - the producer's one-line account, of any length.
19
+ * @returns the account, ellipsized when it exceeds the bound.
20
+ */
21
+ function boundContextSummary(summary) {
22
+ return summary.length <= 120 ? summary : `${summary.slice(0, 119)}…`;
23
+ }
24
+ /**
25
+ * Detach and deep-freeze a message whose identity already exists.
26
+ * @param message - complete message, including its stable identity.
27
+ * @returns an immutable snapshot that preserves the identity.
28
+ */
29
+ function freezeMessage(message) {
30
+ return deepFreeze(structuredClone(message));
31
+ }
32
+ /**
33
+ * Create one identified message and freeze it before publication.
34
+ * @param input - complete role, content, and source for a new message.
35
+ * @returns an immutable message with a fresh stable identity.
36
+ */
37
+ function createMessage(input) {
38
+ return deepFreeze(structuredClone({
39
+ ...input,
40
+ id: brandString(randomUUID())
41
+ }));
42
+ }
43
+ /**
44
+ * Create an identified, immutable developer message.
45
+ * @param input - content and producer source for the new message.
46
+ * @returns a detached developer message with a fresh identity.
47
+ */
48
+ function createDeveloperMessage(input) {
49
+ return createMessage({
50
+ ...input,
51
+ role: "developer"
52
+ });
53
+ }
54
+ /**
55
+ * Create one identified user-role message and freeze it before publication.
56
+ * @param input - complete content and source for a new user message.
57
+ * @returns an immutable user message with a fresh stable identity.
58
+ */
59
+ function createUserMessage(input) {
60
+ return createMessage({
61
+ ...input,
62
+ role: "user"
63
+ });
64
+ }
65
+ /**
66
+ * Create one identified model-produced assistant message and freeze it before publication.
67
+ * @param input - complete content plus the provider, model, and optional replay state for a new assistant message.
68
+ * @returns an immutable assistant message with fixed role/source tags and a fresh stable identity.
69
+ */
70
+ function createAssistantMessage(input) {
71
+ return createMessage({
72
+ role: "assistant",
73
+ content: input.content,
74
+ source: {
75
+ kind: "model",
76
+ ...input.source
77
+ }
78
+ });
79
+ }
80
+ /**
81
+ * Create and freeze one identified system-role message holding a rendered
82
+ * system prompt.
83
+ * @param text - the complete rendered prompt; `''` records "no system prompt".
84
+ * @returns an immutable system message with a fresh stable identity.
85
+ */
86
+ function createSystemMessage(text) {
87
+ return createMessage({
88
+ role: "system",
89
+ content: text.length === 0 ? [] : [{
90
+ type: "text",
91
+ text
92
+ }],
93
+ source: { kind: "system-prompt" }
94
+ });
95
+ }
96
+ /**
97
+ * Create and freeze one identified tool-result message.
98
+ * @param input - call identity, raw result blocks, and outcome.
99
+ * @returns an immutable tool-role message that answers the tool call.
100
+ */
101
+ function createToolResultMessage(input) {
102
+ return createMessage({
103
+ role: "tool",
104
+ source: {
105
+ kind: "tool",
106
+ callId: input.callId
107
+ },
108
+ toolCallId: input.callId,
109
+ content: input.content,
110
+ isError: input.isError
111
+ });
112
+ }
113
+ //#endregion
114
+ //#region lib/types/error.js
115
+ /**
116
+ * Harness error base with a stable machine-routable code and chained cause.
117
+ * Package errors extend it so tool results and replay can retain failure class.
118
+ * @module @qilin-agent/llm/error
119
+ */
120
+ /**
121
+ * Base class for all harness errors. Carries a `code` (stable, programmatic —
122
+ * e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
123
+ * human-readable `message`, and supports `cause` chaining via the standard
124
+ * `ErrorOptions`. `name` defaults to the subclass constructor name.
125
+ */
126
+ var HarnessError = class extends Error {
127
+ /** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
128
+ code;
129
+ constructor(message, code, options) {
130
+ super(message, options);
131
+ this.code = code;
132
+ this.name = new.target.name;
133
+ }
134
+ };
135
+ /** Canonical provider-neutral code for a model request rejected because its context window was exceeded. */
136
+ const CONTEXT_WINDOW_EXCEEDED_CODE = "CONTEXT_WINDOW_EXCEEDED";
137
+ /** Canonical provider-neutral code for an exhausted account quota or balance. */
138
+ const QUOTA_EXCEEDED_CODE = "QUOTA";
139
+ /**
140
+ * Canonical provider-neutral code for a response that completed normally but
141
+ * carried no content blocks at all. Providers occasionally emit a degenerate
142
+ * completion (a terminal stop with zero output); adapters classify it as this
143
+ * failure instead of yielding an empty assistant message, because an empty
144
+ * message silently ends the turn with nothing for the user or the loop to act
145
+ * on. The attempt produced nothing durable, so retry policy treats it as safe
146
+ * to repeat.
147
+ */
148
+ const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
149
+ /**
150
+ * Canonical provider-neutral code for a credential that was supplied but
151
+ * cannot be used — malformed rather than absent. Distinct from
152
+ * `MISSING_CREDENTIAL` because the fix differs: correct the stored value
153
+ * rather than supply one. Deliberately outside the default retryable set —
154
+ * a malformed credential fails identically on every attempt.
155
+ */
156
+ const INVALID_CREDENTIAL_CODE = "INVALID_CREDENTIAL";
157
+ /** Structured codes and plain phrases that explicitly name a context bound being exceeded. */
158
+ const STRUCTURED_CONTEXT_OVERFLOW = new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
159
+ /** Request-size wording that ties "too large" directly to model context capacity. */
160
+ const TOO_LARGE_FOR_CONTEXT = new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
161
+ /** "Exceeds" wording is safe only when its object is explicitly the model context. */
162
+ const EXCEEDS_MODEL_CONTEXT = new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
163
+ /**
164
+ * Recognize the context-overflow wording used by OpenAI-compatible providers
165
+ * and library adapters. Adapters pass all available provider code, type, and
166
+ * message text so both thrown and in-band delivery styles share one classifier.
167
+ * @param detail - provider error code/type/message text joined into one string.
168
+ * @returns true when the detail identifies a request exceeding the model context window.
169
+ */
170
+ function isContextWindowExceededError(detail) {
171
+ return STRUCTURED_CONTEXT_OVERFLOW.test(detail) || /\b(?:maximum|max)(?:\s+(?:allowed|supported))?\s+context\s+(?:length|window)\b/i.test(detail) || TOO_LARGE_FOR_CONTEXT.test(detail) || /\b(?:input|prompt|request)\s+(?:is\s+)?too\s+(?:long|large)\s+for\s+(?:this|the)\s+model\b/i.test(detail) || EXCEEDS_MODEL_CONTEXT.test(detail);
172
+ }
173
+ /**
174
+ * Recognize provider wording that identifies an exhausted account quota rather
175
+ * than a transient request-rate limit.
176
+ * @param detail - provider error code/type/message text joined into one string.
177
+ * @returns true only for terminal quota, balance, credit, budget, or usage-limit wording.
178
+ */
179
+ function isQuotaExceededError(detail) {
180
+ return /\binsufficient[\s_-]+(?:quota|balance|credits?)\b/i.test(detail) || /\b(?:quota|usage[\s_-]+limit)[\s_-]+(?:exceeded|exhausted|reached)\b/i.test(detail) || /\bexceed(?:ed|s)?[\s_-]+(?:(?:your|the)[\s_-]+)?(?:current[\s_-]+)?quota\b/i.test(detail) || /\b(?:balance|credits?)[\s_-]+(?:exhausted|depleted)\b/i.test(detail) || /\bout[\s_-]+of[\s_-]+(?:credits?|budget)\b/i.test(detail);
181
+ }
182
+ /**
183
+ * Render a thrown value with its full `cause` chain and AggregateError
184
+ * members, so transport wrappers like undici's `TypeError: fetch failed`
185
+ * surface the underlying failure instead of masking it. Plain structured
186
+ * failures render their own data-backed `message`. Diagnostic-surface
187
+ * rendering only (messages, notices, logs) — never parse the result; route on
188
+ * {@link HarnessError.code}.
189
+ * @param value - the caught value (`unknown` in catch clauses).
190
+ * @returns the outermost message first, each cause appended with `: ` (skipped
191
+ * when it repeats the wrapper message verbatim), and AggregateError members
192
+ * bracketed and `; `-joined.
193
+ */
194
+ function errorChain(value) {
195
+ const path = /* @__PURE__ */ new Set();
196
+ const render = (current) => {
197
+ if (path.has(current)) return "<circular cause>";
198
+ path.add(current);
199
+ try {
200
+ if (!(current instanceof Error)) {
201
+ if (typeof current === "object" && current !== null) {
202
+ const descriptor = Object.getOwnPropertyDescriptor(current, "message");
203
+ if (descriptor !== void 0 && "value" in descriptor && typeof descriptor.value === "string") return descriptor.value;
204
+ }
205
+ return String(current);
206
+ }
207
+ const message = current.message === "" ? current.name : current.message;
208
+ const members = current instanceof AggregateError && current.errors.length > 0 ? ` [${current.errors.map(render).join("; ")}]` : "";
209
+ const causeText = current.cause === void 0 || current.cause === null ? "" : render(current.cause);
210
+ return `${message}${members}${causeText === "" || causeText === message ? "" : `: ${causeText}`}`;
211
+ } catch {
212
+ return "<unrenderable value>";
213
+ } finally {
214
+ path.delete(current);
215
+ }
216
+ };
217
+ return render(value);
218
+ }
219
+ /**
220
+ * Narrow an arbitrary thrown value to a HarnessError (for `instanceof` at runtime boundaries).
221
+ * @param value - the caught value (`unknown` in catch clauses).
222
+ * @returns true only for real instances; duck-typed or cross-realm errors do not narrow.
223
+ */
224
+ function isHarnessError(value) {
225
+ return value instanceof HarnessError;
226
+ }
227
+ /**
228
+ * Canonical code for a request an image-capable route cannot send until more
229
+ * of its images are offloaded. The failure's `offloadImages` names how many
230
+ * more of the oldest retained occurrences must be offloaded;
231
+ * `dsh-compaction-image-offload` records an `image/offload` selection before
232
+ * the agent or summarizer retries with freshly derived input.
233
+ */
234
+ const IMAGE_OFFLOAD_REQUIRED_CODE = "IMAGE_OFFLOAD_REQUIRED";
235
+ //#endregion
236
+ //#region lib/types/retry-policy.js
237
+ /**
238
+ * Provider-owned request-retry policy configuration and resolution.
239
+ *
240
+ * Adapters expose one resolved policy per registered provider route; the
241
+ * optional qilin-llm-retry plugin executes it on the agent's failed-step extension point.
242
+ *
243
+ * @module @qilin-agent/llm/retry-policy
244
+ */
245
+ const DEFAULT_MAX_RETRIES = 5;
246
+ const DEFAULT_INITIAL_DELAY_MS = 500;
247
+ const DEFAULT_MAX_DELAY_MS = 1e4;
248
+ const DEFAULT_JITTER_RATIO = .1;
249
+ const DEFAULT_RETRYABLE_CODES = Object.freeze([
250
+ EMPTY_RESPONSE_CODE,
251
+ "RATE_LIMIT",
252
+ "SERVER",
253
+ "TIMEOUT",
254
+ "TRANSPORT"
255
+ ]);
256
+ const backoffSchema = z.object({
257
+ initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
258
+ maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
259
+ jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
260
+ });
261
+ const normalPolicySchema = z.object({
262
+ mode: z.const("normal").required(),
263
+ maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
264
+ retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
265
+ backoff: backoffSchema
266
+ });
267
+ const alwaysPolicySchema = z.object({
268
+ mode: z.const("always").required(),
269
+ backoff: backoffSchema
270
+ });
271
+ /** Cordis schema embedded by each concrete provider configuration. */
272
+ const RetryPolicySchema = z.union([normalPolicySchema, alwaysPolicySchema]);
273
+ const NORMAL_POLICY_KEYS = new Set([
274
+ "mode",
275
+ "maxRetries",
276
+ "retryableCodes",
277
+ "backoff"
278
+ ]);
279
+ const ALWAYS_POLICY_KEYS = new Set([
280
+ "mode",
281
+ "maxRetries",
282
+ "retryableCodes",
283
+ "backoff"
284
+ ]);
285
+ const BACKOFF_KEYS = new Set([
286
+ "initialDelayMs",
287
+ "maxDelayMs",
288
+ "jitterRatio"
289
+ ]);
290
+ function validateKeys(value, allowed, path) {
291
+ for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${path}: unknown key "${key}"`);
292
+ }
293
+ function resolveBackoff(config, path) {
294
+ if (config !== void 0) validateKeys(config, BACKOFF_KEYS, path);
295
+ const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
296
+ const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
297
+ const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
298
+ if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > MAX_TIMER_DELAY_MS) throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
299
+ if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > MAX_TIMER_DELAY_MS) throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
300
+ if (initialDelayMs > maxDelayMs) throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
301
+ if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) throw new Error(`${path}.jitterRatio must be between 0 and 1`);
302
+ return Object.freeze({
303
+ initialDelayMs,
304
+ maxDelayMs,
305
+ jitterRatio
306
+ });
307
+ }
308
+ /**
309
+ * Validate, default, and detach one provider-owned retry policy.
310
+ * @param config - optional provider configuration; omission selects normal defaults.
311
+ * @param path - diagnostic path naming the provider config that owns the value.
312
+ * @returns an immutable policy safe to capture in provider registration state.
313
+ */
314
+ function resolveRetryPolicy(config, path) {
315
+ if (config === void 0) return Object.freeze({
316
+ mode: "normal",
317
+ maxRetries: DEFAULT_MAX_RETRIES,
318
+ retryableCodes: DEFAULT_RETRYABLE_CODES,
319
+ ...resolveBackoff(void 0, `${path}.backoff`)
320
+ });
321
+ switch (config.mode) {
322
+ case "normal": {
323
+ validateKeys(config, NORMAL_POLICY_KEYS, path);
324
+ const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
325
+ const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
326
+ if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
327
+ if (retryableCodes.length === 0) throw new Error(`${path}.retryableCodes must not be empty`);
328
+ if (retryableCodes.some((code) => typeof code !== "string" || code.length === 0)) throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
329
+ if (new Set(retryableCodes).size !== retryableCodes.length) throw new Error(`${path}.retryableCodes must not contain duplicates`);
330
+ return Object.freeze({
331
+ mode: "normal",
332
+ maxRetries,
333
+ retryableCodes: Object.freeze([...retryableCodes]),
334
+ ...resolveBackoff(config.backoff, `${path}.backoff`)
335
+ });
336
+ }
337
+ case "always":
338
+ validateKeys(config, ALWAYS_POLICY_KEYS, path);
339
+ return Object.freeze({
340
+ mode: "always",
341
+ ...resolveBackoff(config.backoff, `${path}.backoff`)
342
+ });
343
+ default: throw new Error(`${path}.mode must be "normal" or "always"`);
344
+ }
345
+ }
346
+ //#endregion
347
+ //#region lib/types/call-config.js
348
+ /**
349
+ * Conversation call configuration and freeze utilities. Provider routing,
350
+ * model, reasoning effort, and sampling values are request-header state that
351
+ * can affect cache reuse; request waterfalls replace them and the loop logs
352
+ * changed snapshots instead of allowing silent per-call drift.
353
+ * @module qilin-llm/call-config
354
+ */
355
+ /** Process-local identities of request objects assembled by qilin-agent-loop. */
356
+ const AGENT_LOOP_REQUESTS = /* @__PURE__ */ new WeakSet();
357
+ /**
358
+ * Field-wise equality over {@link LlmCallConfig} — the comparison a caller
359
+ * runs to decide whether a proposed configuration is a real change (worth a
360
+ * logged header snapshot) or the held one restated.
361
+ * @param a - one configuration.
362
+ * @param b - the other.
363
+ * @returns whether every field (including the `stop` list, element-wise) matches.
364
+ */
365
+ function callConfigEquals(a, b) {
366
+ if (a.provider !== b.provider || a.model !== b.model || a.reasoningEffort !== b.reasoningEffort || a.temperature !== b.temperature || a.maxTokens !== b.maxTokens) return false;
367
+ if (a.stop === void 0 || b.stop === void 0) return a.stop === b.stop;
368
+ return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
369
+ }
370
+ /**
371
+ * Mark one exact request object as assembled by qilin-agent-loop.
372
+ * @param request - loop-owned request envelope before LLM dispatch.
373
+ * @returns the same request object marked as created by the process-local agent loop.
374
+ */
375
+ function markAgentLoopRequest(request) {
376
+ AGENT_LOOP_REQUESTS.add(request);
377
+ return request;
378
+ }
379
+ /**
380
+ * Test whether the exact request object was assembled by qilin-agent-loop.
381
+ * @param request - request envelope observed at the LLM waterfall.
382
+ * @returns whether {@link markAgentLoopRequest} recorded this object.
383
+ */
384
+ function isAgentLoopRequest(request) {
385
+ return AGENT_LOOP_REQUESTS.has(request);
386
+ }
387
+ //#endregion
388
+ //#region lib/types/adapter-failure.js
389
+ /**
390
+ * Normalization for values thrown by a final LLM adapter boundary.
391
+ *
392
+ * @module @qilin-agent/llm/adapter-failure
393
+ */
394
+ /**
395
+ * Detach serializable provider facts from a value thrown by an adapter.
396
+ * @param value - arbitrary value thrown during adapter dispatch or iteration.
397
+ * @returns immutable provider-neutral facts suitable for a terminal finish chunk.
398
+ * @internal
399
+ */
400
+ function normalizeLlmFailure(value) {
401
+ const error = value instanceof Error ? value : new HarnessError(thrownMessage(value), "UNKNOWN", { cause: value });
402
+ const carried = ownFailureSnapshot(error);
403
+ if (carried !== void 0 && carried.code === ownErrorCode(error)) return carried;
404
+ return Object.freeze({
405
+ message: errorMessage(error),
406
+ code: harnessErrorCode(error)
407
+ });
408
+ }
409
+ /** Render a non-Error throw without letting hostile coercion escape normalization. */
410
+ function thrownMessage(value) {
411
+ try {
412
+ const message = String(value);
413
+ return message.length > 0 ? message : "LLM adapter failed";
414
+ } catch (_hostileThrownValue) {
415
+ return "LLM adapter failed";
416
+ }
417
+ }
418
+ /** Read a foreign error's own data-backed `code` without invoking accessors. */
419
+ function ownErrorCode(error) {
420
+ try {
421
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
422
+ return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
423
+ } catch (_sdkPropertyTrap) {
424
+ return;
425
+ }
426
+ }
427
+ /** Snapshot an own data property without invoking an SDK-defined accessor. */
428
+ function ownFailureSnapshot(error) {
429
+ try {
430
+ const descriptor = Object.getOwnPropertyDescriptor(error, "failure");
431
+ return descriptor !== void 0 && "value" in descriptor ? failureSnapshot(descriptor.value) : void 0;
432
+ } catch (_sdkPropertyTrap) {
433
+ return;
434
+ }
435
+ }
436
+ /** Validate and detach an arbitrary serializable failure payload. */
437
+ function failureSnapshot(value) {
438
+ if (typeof value !== "object" || value === null) return void 0;
439
+ try {
440
+ const candidate = value;
441
+ const message = candidate.message;
442
+ const code = candidate.code;
443
+ const status = candidate.status;
444
+ const providerRetryAfterMs = candidate.providerRetryAfterMs;
445
+ const requestId = candidate.requestId;
446
+ const offloadImages = candidate.offloadImages;
447
+ if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0) || offloadImages !== void 0 && (!Number.isSafeInteger(offloadImages) || offloadImages <= 0)) return void 0;
448
+ return Object.freeze({
449
+ message,
450
+ code,
451
+ ...status === void 0 ? {} : { status },
452
+ ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
453
+ ...requestId === void 0 ? {} : { requestId },
454
+ ...offloadImages === void 0 ? {} : { offloadImages }
455
+ });
456
+ } catch (_sdkFailureGetter) {
457
+ return;
458
+ }
459
+ }
460
+ /** Read an SDK error message without letting an accessor replace the primary failure. */
461
+ function errorMessage(error) {
462
+ try {
463
+ const message = error.message;
464
+ if (typeof message === "string" && message.length > 0) return message;
465
+ } catch (_sdkMessageGetter) {}
466
+ return "LLM adapter failed";
467
+ }
468
+ /** Trust only Harness-owned codes; third-party SDK codes are not our taxonomy. */
469
+ function harnessErrorCode(error) {
470
+ return error instanceof HarnessError ? error.code : "UNKNOWN";
471
+ }
472
+ //#endregion
473
+ //#region lib/types/api-key.js
474
+ /**
475
+ * The one definition of a well-formed provider API key, shared by every
476
+ * adapter that puts one in an HTTP header.
477
+ * @module @qilin-agent/llm/api-key
478
+ */
479
+ /**
480
+ * Characters an HTTP header value carries verbatim and every known provider
481
+ * key uses: printable ASCII, space excluded. A key outside this set cannot
482
+ * reach any provider — `fetch` refuses to build the header — so this is a
483
+ * transport invariant rather than one provider's policy. Latin-1 is excluded
484
+ * deliberately: a header could carry it, but no provider issues it, and
485
+ * admitting it trades a local explained refusal for an opaque 401.
486
+ */
487
+ const LEGAL_API_KEY = /^[\x21-\x7E]+$/;
488
+ /**
489
+ * Judge one *supplied* API key, trimming surrounding whitespace first.
490
+ *
491
+ * Trimming is silent because a padded key has one unambiguous reading; every
492
+ * other defect is reported. Absence is a configuration state this function
493
+ * never sees — a profile naming no credential authenticates through the
494
+ * provider's own ambient discovery or OAuth — so callers decide whether a
495
+ * value was supplied before asking.
496
+ * @param raw - the key exactly as configured, stored, or typed.
497
+ * @returns the trimmed key, or why it cannot be used.
498
+ */
499
+ function normalizeApiKey(raw) {
500
+ const value = raw.trim();
501
+ if (value.length === 0) return {
502
+ ok: false,
503
+ reason: "empty"
504
+ };
505
+ if (!LEGAL_API_KEY.test(value)) return {
506
+ ok: false,
507
+ reason: "illegalCharacters"
508
+ };
509
+ return {
510
+ ok: true,
511
+ value
512
+ };
513
+ }
514
+ //#endregion
515
+ //#region lib/types/content.js
516
+ /** Content-block structure helpers. @module @qilin-agent/llm/content */
517
+ /**
518
+ * Bridge one attachment provider's host object location into the mounted
519
+ * tool execution world. The consumer supplies the current filesystem
520
+ * provider's mapping without making attachment or LLM definitions depend on it.
521
+ * @param attachments - provider that owns the normalized attachment object.
522
+ * @param mapHostPath - map one absolute host path into the current tool execution world.
523
+ * @param ref - durable normalized attachment reference.
524
+ * @returns a read-only execution-world path, or undefined when either provider exposes no mapping.
525
+ * @throws an attachment error when the durable reference is invalid.
526
+ */
527
+ function resolveImageAttachmentAccess(attachments, mapHostPath, ref) {
528
+ const hostPath = attachments.imageHostPath(ref);
529
+ if (hostPath === void 0) return void 0;
530
+ const readonlyPath = mapHostPath(hostPath);
531
+ return readonlyPath === void 0 ? void 0 : { readonlyPath };
532
+ }
533
+ function quoted(value) {
534
+ return JSON.stringify(value);
535
+ }
536
+ function imageIdentity(ref) {
537
+ return ref.name === void 0 ? String(ref.attachmentId) : `${quoted(ref.name)} (${ref.attachmentId})`;
538
+ }
539
+ function extension(mediaType) {
540
+ switch (mediaType) {
541
+ case "image/png": return ".png";
542
+ case "image/jpeg": return ".jpg";
543
+ case "image/webp": return ".webp";
544
+ case "image/gif": return ".gif";
545
+ default: return assertNever(mediaType, "image extension");
546
+ }
547
+ }
548
+ function normalizedAccessText(ref, access) {
549
+ return ` Normalized copy (read-only; may be resized or re-encoded): ${quoted(access.readonlyPath)} (${ref.width}x${ref.height}px, ${ref.mediaType}). Source dimensions, format, and byte size may differ. Copy to a writable path ending in ${extension(ref.mediaType)} before editing.`;
550
+ }
551
+ /**
552
+ * Stable text shown to a model that cannot accept one durable image reference.
553
+ * @param ref - durable normalized attachment omitted from the request.
554
+ * @returns deterministic text-only placeholder.
555
+ */
556
+ function textOnlyImageText(ref) {
557
+ return `[image omitted because this model accepts text only; attachment sha256:${String(ref.attachmentId).slice(7, 15)}]`;
558
+ }
559
+ /**
560
+ * Stable model-facing handle for one exact request image. Identity comes from
561
+ * the occurrence's own durable reference: request versions are prepared per
562
+ * attachment id, so one shared version may serve occurrences whose display
563
+ * names differ.
564
+ * @param ref - the occurrence's durable normalized attachment.
565
+ * @param version - exact request-image dimensions shown beside the text.
566
+ * @param access - optional path resolved for the current tool execution world.
567
+ * @returns attachment handle and request-image dimensions.
568
+ */
569
+ function requestImageHandleText(ref, version, access) {
570
+ const preview = `Image ${imageIdentity(ref)}; request preview ${version.width}x${version.height}px.`;
571
+ return access === void 0 ? `${preview} It may be resized or re-encoded; source dimensions, format, and byte size may differ.` : preview + normalizedAccessText(ref, access);
572
+ }
573
+ /**
574
+ * Stable per-image placeholder for a request-limit omission.
575
+ * @param ref - durable normalized attachment omitted from this request.
576
+ * @param access - optional provider-resolved path for model tools.
577
+ * @returns identity, normalized metadata, and the available recovery path.
578
+ */
579
+ function offloadedImageText(ref, access) {
580
+ const identity = `image omitted to fit request image limits; ${imageIdentity(ref)}.`;
581
+ if (access === void 0) return `[${identity} No local normalized image path is available; ask the user to attach it again if needed.]`;
582
+ return `[${identity}${normalizedAccessText(ref, access)}]`;
583
+ }
584
+ /**
585
+ * True when typed model content contains an image block. This is the one image
586
+ * walk shared by every image policy (capability gating, text-only
587
+ * serialization, compaction survey), so a consumer cannot silently diverge.
588
+ * @param content - typed model content blocks.
589
+ * @returns whether any block is an image.
590
+ */
591
+ function contentHasImage(content) {
592
+ return content.some((block) => block.type === "image");
593
+ }
594
+ /**
595
+ * True when typed model content contains a file block.
596
+ * Reads current content on every call without retaining scan results.
597
+ * @param content - typed model content blocks.
598
+ * @returns whether any block is a file.
599
+ */
600
+ function contentHasFile(content) {
601
+ for (const block of content) if (block.type === "file") return true;
602
+ return false;
603
+ }
604
+ /**
605
+ * Stable model-facing handle for one durable file reference: the address of
606
+ * the verbatim stored copy and the instruction to read it on demand. This is
607
+ * the only representation a provider ever receives for a file.
608
+ * @param ref - durable verbatim file reference.
609
+ * @param readonlyPath - execution-world path of the stored copy, when resolvable.
610
+ * @returns deterministic handle text naming the file, its size, and its address.
611
+ */
612
+ function fileHandleText(ref, readonlyPath) {
613
+ const digest = String(ref.attachmentId).slice(7, 15);
614
+ const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
615
+ if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
616
+ return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
617
+ }
618
+ /** Replace every file occurrence with handle text. */
619
+ function replaceFilesWithHandles(blocks, resolvePath) {
620
+ let next;
621
+ for (const [index, block] of blocks.entries()) {
622
+ if (block.type === "file") {
623
+ next ??= blocks.slice(0, index);
624
+ next.push({
625
+ type: "text",
626
+ text: fileHandleText(block.attachment, resolvePath(block.attachment))
627
+ });
628
+ continue;
629
+ }
630
+ next?.push(block);
631
+ }
632
+ return next ?? blocks;
633
+ }
634
+ function projectFilesToText(messages, resolvePath) {
635
+ if (!messages.some((message) => contentHasFile(message.content))) return messages;
636
+ return messages.map((message) => {
637
+ const content = replaceFilesWithHandles(message.content, resolvePath);
638
+ return content === message.content ? message : {
639
+ ...message,
640
+ content
641
+ };
642
+ });
643
+ }
644
+ /** Base64 length of raw image bytes, including padding. */
645
+ function base64Length(bytes) {
646
+ return Math.ceil(bytes / 3) * 4;
647
+ }
648
+ /**
649
+ * Visit every image occurrence of typed content in message order.
650
+ * @param content - typed model content blocks.
651
+ * @param visit - called once per occurrence.
652
+ */
653
+ function visitImageBlocks(content, visit) {
654
+ for (const block of content) if (block.type === "image") visit(block);
655
+ }
656
+ /** Replace every offloaded occurrence with its placeholder. */
657
+ function replaceOffloadedImages(blocks, placeholder) {
658
+ let next;
659
+ for (const [index, block] of blocks.entries()) {
660
+ if (block.type === "image" && block.offloaded === true) {
661
+ next ??= blocks.slice(0, index);
662
+ next.push({
663
+ type: "text",
664
+ text: placeholder(block.attachment)
665
+ });
666
+ continue;
667
+ }
668
+ next?.push(block);
669
+ }
670
+ return next ?? blocks;
671
+ }
672
+ function projectOffloadedImages(messages, placeholder) {
673
+ return messages.map((message) => {
674
+ const content = replaceOffloadedImages(message.content, placeholder);
675
+ return content === message.content ? message : {
676
+ ...message,
677
+ content
678
+ };
679
+ });
680
+ }
681
+ /**
682
+ * Number of oldest retained image occurrences one route budget removes, in
683
+ * whole count and byte quanta, once the budget is exceeded. The result depends
684
+ * only on the represented lengths, so every route names the count the same
685
+ * way.
686
+ * @param lengths - represented byte length of every retained occurrence, oldest first.
687
+ * @param budget - count/byte budgets and removal quanta; unbounded when absent.
688
+ * @returns how many leading occurrences to offload.
689
+ */
690
+ function offloadedImagePrefixCount(lengths, budget) {
691
+ const total = lengths.reduce((sum, bytes) => sum + bytes, 0);
692
+ const excessCount = budget.maxImages === void 0 ? 0 : Math.max(0, lengths.length - budget.maxImages);
693
+ const excessBytes = budget.maxBytes === void 0 ? 0 : Math.max(0, total - budget.maxBytes);
694
+ if (excessCount === 0 && excessBytes === 0) return 0;
695
+ const countQuantum = budget.countQuantum ?? 1;
696
+ const byteQuantum = budget.byteQuantum ?? 1;
697
+ const removeCount = excessCount === 0 ? 0 : Math.ceil(excessCount / countQuantum) * countQuantum;
698
+ const removeBytes = excessBytes === 0 ? 0 : Math.ceil(excessBytes / byteQuantum) * byteQuantum;
699
+ let count = 0;
700
+ let removedBytes = 0;
701
+ for (const imageBytes of lengths) {
702
+ if (count >= removeCount && (removeBytes === 0 || (byteQuantum === 1 ? removedBytes >= removeBytes : removedBytes > removeBytes))) break;
703
+ removedBytes += imageBytes;
704
+ count += 1;
705
+ }
706
+ return count;
707
+ }
708
+ /**
709
+ * Number of oldest retained occurrences a route must still offload before a
710
+ * derived request fits its budget at the exact byte length the route sends;
711
+ * zero when the request fits. A route fails with `IMAGE_OFFLOAD_REQUIRED`
712
+ * carrying this count instead of offloading on its own.
713
+ * @param messages - derived request history carrying the surface's `offloaded` marks.
714
+ * @param budget - route representation, budgets, and removal quanta.
715
+ * @param versionBytes - exact request-version byte length of one retained occurrence.
716
+ * @returns how many more leading retained occurrences to offload.
717
+ */
718
+ function requiredImageOffload(messages, budget, versionBytes) {
719
+ const lengths = [];
720
+ for (const message of messages) visitImageBlocks(message.content, (block) => {
721
+ if (block.offloaded === true) return;
722
+ const bytes = versionBytes(block);
723
+ lengths.push(budget.representation === "base64" ? base64Length(bytes) : bytes);
724
+ });
725
+ return offloadedImagePrefixCount(lengths, budget);
726
+ }
727
+ /** Replace every image occurrence for a text-only model. */
728
+ function replaceImagesForTextModel(blocks) {
729
+ let next;
730
+ for (const [index, block] of blocks.entries()) {
731
+ if (block.type === "image") {
732
+ next ??= blocks.slice(0, index);
733
+ next.push({
734
+ type: "text",
735
+ text: textOnlyImageText(block.attachment)
736
+ });
737
+ continue;
738
+ }
739
+ next?.push(block);
740
+ }
741
+ return next ?? blocks;
742
+ }
743
+ function projectImagesForTextModel(messages) {
744
+ if (!messages.some((message) => contentHasImage(message.content))) return messages;
745
+ return messages.map((message) => {
746
+ const content = replaceImagesForTextModel(message.content);
747
+ return content === message.content ? message : {
748
+ ...message,
749
+ content
750
+ };
751
+ });
752
+ }
753
+ function withoutDeveloperMessages(messages) {
754
+ const retained = messages.filter((message) => message.role !== "developer");
755
+ return retained.length === messages.length ? messages : retained;
756
+ }
757
+ function toolDeclarations(tools, mode, history) {
758
+ const declarations = new Map(history.tools.map((tool) => [tool.name, tool]));
759
+ for (const update of history.updates) for (const tool of update.additions) if (!declarations.has(tool.name)) declarations.set(tool.name, {
760
+ ...tool,
761
+ deferLoading: true
762
+ });
763
+ switch (mode) {
764
+ case "in-history": return declarations;
765
+ case "addition-only": {
766
+ const activeNames = new Set(tools?.map((tool) => tool.name));
767
+ for (const name of declarations.keys()) if (!activeNames.has(name)) declarations.delete(name);
768
+ return declarations;
769
+ }
770
+ /* v8 ignore next 2 -- closed-union exhaustiveness guard */
771
+ default: return assertNever(mode);
772
+ }
773
+ }
774
+ /**
775
+ * Construct provider declarations from session-folded history without changing logged active tools.
776
+ * Unsupported routes and incomplete history use current declarations without developer updates.
777
+ * Explicitly deferred baseline tools become available only after their first retained addition.
778
+ * @param messages - complete request inputs, or the prefix selected for an auxiliary call.
779
+ * @param tools - currently active tool schemas.
780
+ * @param toolUpdate - the resolved route's update mode.
781
+ * @param history - immutable state folded from committed headers and developer messages.
782
+ * @returns provider declarations and the corresponding filtered history.
783
+ */
784
+ function projectToolUpdates(messages, tools, toolUpdate, history) {
785
+ if (toolUpdate === void 0) {
786
+ let immediateTools = tools;
787
+ if (tools?.some((tool) => tool.deferLoading === true)) immediateTools = tools.map(({ deferLoading: _loading, ...tool }) => tool);
788
+ return {
789
+ messages: withoutDeveloperMessages(messages),
790
+ tools: immediateTools
791
+ };
792
+ }
793
+ if (history === void 0) return {
794
+ messages: withoutDeveloperMessages(messages),
795
+ tools
796
+ };
797
+ const messageIds = new Set(messages.flatMap((message) => message.role === "developer" ? [message.id] : []));
798
+ if (history.updates.some((update) => !messageIds.has(update.messageId))) return {
799
+ messages: withoutDeveloperMessages(messages),
800
+ tools
801
+ };
802
+ const declarations = toolDeclarations(tools, toolUpdate, history);
803
+ const updateIds = new Set(history.updates.map((update) => update.messageId));
804
+ const offered = new Set(history.tools.filter((tool) => !tool.deferLoading).map((tool) => tool.name));
805
+ const projectedMessages = [];
806
+ for (const message of messages) {
807
+ if (message.role !== "developer") {
808
+ projectedMessages.push(message);
809
+ continue;
810
+ }
811
+ if (!updateIds.has(message.id)) continue;
812
+ const content = message.content.filter((block) => {
813
+ switch (block.type) {
814
+ case "tool-addition":
815
+ if (!declarations.has(block.toolName) || offered.has(block.toolName)) return false;
816
+ offered.add(block.toolName);
817
+ return true;
818
+ case "tool-removal":
819
+ if (toolUpdate !== "in-history") return false;
820
+ return offered.delete(block.toolName);
821
+ default: return true;
822
+ }
823
+ });
824
+ if (content.length === 0) continue;
825
+ if (content.length === message.content.length) projectedMessages.push(message);
826
+ else projectedMessages.push({
827
+ ...message,
828
+ content
829
+ });
830
+ }
831
+ return {
832
+ messages: projectedMessages.length === messages.length && projectedMessages.every((message, index) => message === messages[index]) ? messages : projectedMessages,
833
+ tools: [...declarations.values()]
834
+ };
835
+ }
836
+ //#endregion
837
+ //#region lib/types/attribution.js
838
+ /**
839
+ * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
840
+ * adapters from drifting. See
841
+ * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
842
+ *
843
+ * App-attribution vocabulary for provider requests.
844
+ * @module @qilin-agent/llm/attribution
845
+ */
846
+ const { version } = createRequire(import.meta.url)("../package.json");
847
+ /**
848
+ * The harness's own identity: the default every adapter sends. Deployments
849
+ * that need a white-label identity pass their own {@link AppIdentity} to
850
+ * {@link attributionHeaders} — omission falls back to this default; nothing
851
+ * can suppress attribution entirely.
852
+ */
853
+ const APP_IDENTITY = {
854
+ product: "qilin",
855
+ version,
856
+ url: "https://github.com/kkutysllb/QiLin"
857
+ };
858
+ /**
859
+ * The standard `User-Agent` value: `product/version (+url)`. The
860
+ * parenthesized `+url` comment is the conventional self-identification form
861
+ * (RFC 9110 §10.1.5 product + comment syntax).
862
+ * @param identity - the identity to render; defaults to {@link APP_IDENTITY}.
863
+ * @returns the ready-to-send header value.
864
+ */
865
+ function userAgent(identity = APP_IDENTITY) {
866
+ return `${identity.product}/${identity.version} (+${identity.url})`;
867
+ }
868
+ /**
869
+ * Build the attribution headers an adapter must send on every provider
870
+ * request. Header names are lowercase (HTTP field names are case-insensitive
871
+ * on the wire).
872
+ * @param identity - the identity to send; defaults to {@link APP_IDENTITY} — omission cannot suppress attribution.
873
+ * @returns headers to merge into the provider request (currently just `user-agent`).
874
+ */
875
+ function attributionHeaders(identity = APP_IDENTITY) {
876
+ return { "user-agent": userAgent(identity) };
877
+ }
878
+ //#endregion
879
+ //#region lib/types/brand.js
880
+ /**
881
+ * qilin-llm's owned branded ids: tool-call correlation and provider request
882
+ * diagnostics.
883
+ *
884
+ * The `Branded<B>` primitive and stateless constructor live in
885
+ * `@qilin-agent/brand` so every owner of a cross-boundary id can brand it
886
+ * without depending on qilin-llm; see that package's README for the
887
+ * nominal-typing policy.
888
+ *
889
+ * @module @qilin-agent/llm/brand
890
+ */
891
+ /**
892
+ * Brand a message identifier.
893
+ * @param id - the opaque message identifier.
894
+ * @returns the same string with the message-id brand.
895
+ */
896
+ function MessageId(id) {
897
+ return brandString(id);
898
+ }
899
+ /**
900
+ * Brand a string as a {@link ToolCallId}.
901
+ * @param id - the provider-issued or synthesized call id.
902
+ * @returns the same string with the tool-call-id brand.
903
+ */
904
+ function ToolCallId(id) {
905
+ return brandString(id);
906
+ }
907
+ /**
908
+ * Brand a provider-issued request identifier.
909
+ * @param id - the opaque provider-issued string.
910
+ * @returns the same string, branded; no validation is performed.
911
+ */
912
+ function ProviderRequestId(id) {
913
+ return brandString(id);
914
+ }
915
+ /**
916
+ * Brand one loop-owned streaming attempt identifier.
917
+ * @param id - the opaque Agent-lifecycle-local identifier.
918
+ * @returns the same string with the attempt-id brand.
919
+ */
920
+ function LlmAttemptId(id) {
921
+ return brandString(id);
922
+ }
923
+ /**
924
+ * Brand an adapter-owned reasoning-effort identifier.
925
+ * @param id - the opaque identifier exposed by one model capability.
926
+ * @returns the same string, branded; no validation is performed.
927
+ */
928
+ function ReasoningEffortId(id) {
929
+ return brandString(id);
930
+ }
931
+ //#endregion
932
+ //#region lib/types/assembler.js
933
+ /**
934
+ * Incremental chunk-to-message assembler. This is the single canonical assembly
935
+ * algorithm used by the agent loop to build an assistant message from a chunk
936
+ * stream while logging the raw chunks for replay fidelity.
937
+ *
938
+ * @module @qilin-agent/llm/assembler
939
+ */
940
+ /**
941
+ * Incrementally assembles raw {@link StreamChunk}s into complete
942
+ * {@link ContentBlock}s and a final assistant {@link Message}.
943
+ *
944
+ * The agent loop feeds it while logging raw chunks for replay fidelity, then
945
+ * reads `blocks()` / `message()` / `usage` / `finish` once the stream ends,
946
+ * or `interruptedBlocks()` when cancellation cut the stream short.
947
+ *
948
+ * Tolerant of delta-only protocols (no block-start/end); deltas arriving for
949
+ * an index already closed by `block-end` are ignored (malformed stream) so a
950
+ * misbehaving adapter cannot grow memory or corrupt a completed block.
951
+ */
952
+ var BlockAssembler = class {
953
+ partials = /* @__PURE__ */ new Map();
954
+ order = [];
955
+ _usage;
956
+ _finish;
957
+ _replayState;
958
+ /**
959
+ * Feed one chunk into the assembly state.
960
+ * @param chunk - the next raw chunk, in stream order.
961
+ */
962
+ push(chunk) {
963
+ switch (chunk.type) {
964
+ case "block-start":
965
+ if (!this.partials.has(chunk.index)) {
966
+ this.order.push(chunk.index);
967
+ this.partials.set(chunk.index, {
968
+ blockType: chunk.blockType,
969
+ text: "",
970
+ toolCallArguments: ""
971
+ });
972
+ }
973
+ return;
974
+ case "text-delta":
975
+ case "reasoning-delta": {
976
+ const partial = this.ensure(chunk.index, chunk.type === "text-delta" ? "text" : "reasoning");
977
+ if (partial.block) return;
978
+ partial.text += chunk.text;
979
+ return;
980
+ }
981
+ case "tool-call-delta": {
982
+ const partial = this.ensure(chunk.index, "tool-call");
983
+ if (partial.block) return;
984
+ partial.toolCallId = chunk.id;
985
+ if (chunk.name) partial.toolCallName = chunk.name;
986
+ partial.toolCallArguments += chunk.argumentsDelta;
987
+ return;
988
+ }
989
+ case "block-end": {
990
+ const partial = this.ensure(chunk.index, chunk.block.type);
991
+ if (partial.block) return;
992
+ partial.block = chunk.block;
993
+ return;
994
+ }
995
+ case "usage":
996
+ this._usage = chunk.usage;
997
+ return;
998
+ case "finish":
999
+ this._finish = chunk.reason;
1000
+ this._replayState = chunk.replayState;
1001
+ return;
1002
+ default: return assertNever(chunk, "BlockAssembler.push");
1003
+ }
1004
+ }
1005
+ ensure(index, blockType) {
1006
+ let partial = this.partials.get(index);
1007
+ if (!partial) {
1008
+ partial = {
1009
+ blockType,
1010
+ text: "",
1011
+ toolCallArguments: ""
1012
+ };
1013
+ this.partials.set(index, partial);
1014
+ this.order.push(index);
1015
+ }
1016
+ return partial;
1017
+ }
1018
+ assemble(partial, index) {
1019
+ if (partial.block) return partial.block;
1020
+ switch (partial.blockType) {
1021
+ case "text": return {
1022
+ type: "text",
1023
+ text: partial.text
1024
+ };
1025
+ case "reasoning": return {
1026
+ type: "reasoning",
1027
+ text: partial.text
1028
+ };
1029
+ case "tool-call": return {
1030
+ type: "tool-call",
1031
+ id: partial.toolCallId ?? brandString(`call-${index}`),
1032
+ name: partial.toolCallName ?? "",
1033
+ arguments: partial.toolCallArguments
1034
+ };
1035
+ default: throw new Error(`cannot assemble incomplete block of type "${partial.blockType}"`);
1036
+ }
1037
+ }
1038
+ /** Invariant accessor: every index in `order` has a partial. */
1039
+ mustGet(index) {
1040
+ const partial = this.partials.get(index);
1041
+ if (!partial) throw new Error(`BlockAssembler invariant violated: no partial for index ${index}`);
1042
+ return partial;
1043
+ }
1044
+ /**
1045
+ * The one shared keep/drop decision over all seen blocks: max-token
1046
+ * truncation drops tool calls that cannot be executed safely. Emitted blocks
1047
+ * and replay metadata both derive from this result, so they cannot disagree.
1048
+ */
1049
+ assembled() {
1050
+ const all = this.order.map((index) => this.assemble(this.mustGet(index), index));
1051
+ const kept = this.finish.kind === "max-tokens" ? all.map((block) => block.type !== "tool-call") : void 0;
1052
+ const blocks = kept === void 0 ? all : all.filter((_, position) => kept[position]);
1053
+ const envelope = this._replayState;
1054
+ if (envelope?.blocks === void 0) return {
1055
+ blocks,
1056
+ replay: envelope
1057
+ };
1058
+ if (envelope.blocks.length !== all.length) return {
1059
+ blocks,
1060
+ replay: void 0
1061
+ };
1062
+ return {
1063
+ blocks,
1064
+ replay: kept === void 0 || blocks.length === all.length ? envelope : {
1065
+ response: envelope.response,
1066
+ blocks: envelope.blocks.filter((_, position) => kept[position])
1067
+ }
1068
+ };
1069
+ }
1070
+ /**
1071
+ * Assemble all blocks seen so far, in stream order.
1072
+ * @returns one block per seen index, except that max-token truncation drops
1073
+ * tool calls that cannot be executed safely; an open block assembles from
1074
+ * its accumulated deltas (an unknown block type never closed by `block-end` throws).
1075
+ */
1076
+ blocks() {
1077
+ return this.assembled().blocks;
1078
+ }
1079
+ /**
1080
+ * Assemble the prefix an interrupted stream can safely finalize: closed and
1081
+ * open text/reasoning blocks with non-whitespace content, in stream order.
1082
+ * Tool calls are omitted because interruption precedes dispatch; retaining
1083
+ * one would require a fabricated result. Open unknown blocks are also omitted.
1084
+ * @returns the kept blocks; empty when nothing streamed before the interruption.
1085
+ */
1086
+ interruptedBlocks() {
1087
+ return this.order.map((index) => {
1088
+ const partial = this.mustGet(index);
1089
+ const type = partial.block?.type ?? partial.blockType;
1090
+ if (type !== "text" && type !== "reasoning") return void 0;
1091
+ return this.assemble(partial, index);
1092
+ }).filter((block) => (block?.type === "text" || block?.type === "reasoning") && block.text.trim() !== "");
1093
+ }
1094
+ /** Usage from the `usage` chunk; undefined until one arrives. */
1095
+ get usage() {
1096
+ return this._usage;
1097
+ }
1098
+ /** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
1099
+ get finish() {
1100
+ return this._finish ?? { kind: "stop" };
1101
+ }
1102
+ /**
1103
+ * Replay metadata from the terminal finish chunk, if any, with per-block
1104
+ * entries pruned in step with {@link blocks}. Undefined when the envelope's
1105
+ * entries do not align with the emitted blocks.
1106
+ */
1107
+ get replayState() {
1108
+ return this.assembled().replay;
1109
+ }
1110
+ /**
1111
+ * The assembled assistant message.
1112
+ * @param source - provider/model attribution (without the `kind` tag) for the assembled message.
1113
+ * @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules).
1114
+ */
1115
+ message(source) {
1116
+ return createAssistantMessage({
1117
+ content: this.blocks(),
1118
+ source
1119
+ });
1120
+ }
1121
+ };
1122
+ //#endregion
1123
+ //#region lib/types/assistant-stream.js
1124
+ /**
1125
+ * Lossless compact representation of one model-stream attempt, plus record-level
1126
+ * readers that answer common consumer questions without materializing members.
1127
+ * Readers trust the static record type; expandAssistantStream is the validating
1128
+ * path for records read at a durable boundary.
1129
+ */
1130
+ function safeTime(value) {
1131
+ if (!Number.isSafeInteger(value)) throw new TypeError(`Assistant stream time must be a safe integer, got ${String(value)}`);
1132
+ return value;
1133
+ }
1134
+ function safeIndex(value, label) {
1135
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new TypeError(`${label} index must be a non-negative safe integer`);
1136
+ return value;
1137
+ }
1138
+ function snapshotChunk(chunk) {
1139
+ const snapshot = snapshotJsonValue(chunk);
1140
+ if (snapshot === void 0) throw new TypeError("Assistant stream chunk must be losslessly JSON-serializable");
1141
+ return snapshot;
1142
+ }
1143
+ function safeGap(previous, next) {
1144
+ const gap = next - previous;
1145
+ return Number.isSafeInteger(gap) && previous + gap === next ? gap : void 0;
1146
+ }
1147
+ /** Incrementally compacts one attempt without retaining a second raw-chunk list. */
1148
+ var AssistantStreamAccumulator = class {
1149
+ records = [];
1150
+ /**
1151
+ * Add one timed chunk to the compact attempt stream.
1152
+ * @param value - model chunk and its original Session timestamp.
1153
+ * @returns a detached immutable copy for assembly and live publication.
1154
+ */
1155
+ push(value) {
1156
+ const time = safeTime(value.time);
1157
+ const chunk = snapshotChunk(value.chunk);
1158
+ const timed = deepFreeze({
1159
+ time,
1160
+ chunk
1161
+ });
1162
+ const previous = this.records.at(-1);
1163
+ switch (chunk.type) {
1164
+ case "text-delta":
1165
+ case "reasoning-delta": {
1166
+ safeIndex(chunk.index, chunk.type);
1167
+ if (typeof chunk.text !== "string") throw new TypeError(`${chunk.type} text must be a string`);
1168
+ const type = chunk.type === "text-delta" ? "text-chunks" : "reasoning-chunks";
1169
+ const gap = previous !== void 0 && previous.type === type ? safeGap(previous.lastTime, time) : void 0;
1170
+ if (previous !== void 0 && previous.type === type && previous.index === chunk.index && gap !== void 0) {
1171
+ previous.dt.push(gap);
1172
+ previous.texts.push(chunk.text);
1173
+ previous.lastTime = time;
1174
+ } else this.records.push({
1175
+ type,
1176
+ time0: time,
1177
+ index: chunk.index,
1178
+ dt: [],
1179
+ texts: [chunk.text],
1180
+ lastTime: time
1181
+ });
1182
+ return timed;
1183
+ }
1184
+ case "tool-call-delta": {
1185
+ safeIndex(chunk.index, chunk.type);
1186
+ if (typeof chunk.id !== "string") throw new TypeError("tool-call-delta id must be a string");
1187
+ if (Object.hasOwn(chunk, "name") && typeof chunk.name !== "string") throw new TypeError("tool-call-delta name must be a string");
1188
+ if (typeof chunk.argumentsDelta !== "string") throw new TypeError("tool-call-delta argumentsDelta must be a string");
1189
+ if (chunk.id.length === 0 || chunk.name === "") {
1190
+ this.records.push({
1191
+ type: "chunk",
1192
+ time,
1193
+ chunk
1194
+ });
1195
+ return timed;
1196
+ }
1197
+ const gap = previous?.type === "tool-call-chunks" ? safeGap(previous.lastTime, time) : void 0;
1198
+ const sameName = previous?.type === "tool-call-chunks" && Object.hasOwn(previous, "name") === Object.hasOwn(chunk, "name") && previous.name === chunk.name;
1199
+ if (previous?.type === "tool-call-chunks" && previous.index === chunk.index && previous.id === chunk.id && sameName && gap !== void 0) {
1200
+ previous.dt.push(gap);
1201
+ previous.args.push(chunk.argumentsDelta);
1202
+ previous.lastTime = time;
1203
+ } else this.records.push({
1204
+ type: "tool-call-chunks",
1205
+ time0: time,
1206
+ index: chunk.index,
1207
+ dt: [],
1208
+ id: chunk.id,
1209
+ ...Object.hasOwn(chunk, "name") ? { name: chunk.name } : {},
1210
+ args: [chunk.argumentsDelta],
1211
+ lastTime: time
1212
+ });
1213
+ return timed;
1214
+ }
1215
+ case "block-start":
1216
+ case "block-end":
1217
+ case "usage":
1218
+ case "finish":
1219
+ this.records.push({
1220
+ type: "chunk",
1221
+ time,
1222
+ chunk
1223
+ });
1224
+ return timed;
1225
+ default: return assertNever(chunk, "AssistantStreamAccumulator.push");
1226
+ }
1227
+ }
1228
+ /**
1229
+ * Return the current compact attempt stream.
1230
+ * @returns a detached immutable record list suitable for a durable event.
1231
+ */
1232
+ snapshot() {
1233
+ return deepFreeze(this.records.map((record) => {
1234
+ if (record.type === "chunk") return { ...record };
1235
+ const { lastTime: _lastTime, ...durable } = record;
1236
+ if (durable.type === "tool-call-chunks") return {
1237
+ ...durable,
1238
+ dt: [...durable.dt],
1239
+ args: [...durable.args]
1240
+ };
1241
+ return {
1242
+ ...durable,
1243
+ dt: [...durable.dt],
1244
+ texts: [...durable.texts]
1245
+ };
1246
+ }));
1247
+ }
1248
+ };
1249
+ /**
1250
+ * Expand compact records into the exact timed chunk sequence.
1251
+ * @param stream - compact records from one durable Assistant settlement.
1252
+ * @returns detached timed chunks with every original delta boundary preserved.
1253
+ * @throws {TypeError} when a record or reconstructed timestamp is invalid.
1254
+ */
1255
+ function expandAssistantStream(stream) {
1256
+ const chunks = [];
1257
+ for (const candidate of stream) {
1258
+ const record = validateRecord(candidate);
1259
+ if (record.type === "chunk") {
1260
+ chunks.push({
1261
+ time: record.time,
1262
+ chunk: record.chunk
1263
+ });
1264
+ continue;
1265
+ }
1266
+ const members = record.type === "tool-call-chunks" ? record.args : record.texts;
1267
+ let time = record.time0;
1268
+ for (let index = 0; index < members.length; index += 1) {
1269
+ if (index > 0) time += record.dt[index - 1];
1270
+ let chunk;
1271
+ if (record.type === "text-chunks") chunk = {
1272
+ type: "text-delta",
1273
+ index: record.index,
1274
+ text: members[index]
1275
+ };
1276
+ else if (record.type === "reasoning-chunks") chunk = {
1277
+ type: "reasoning-delta",
1278
+ index: record.index,
1279
+ text: members[index]
1280
+ };
1281
+ else chunk = {
1282
+ type: "tool-call-delta",
1283
+ index: record.index,
1284
+ id: record.id,
1285
+ ...Object.hasOwn(record, "name") ? { name: record.name } : {},
1286
+ argumentsDelta: members[index]
1287
+ };
1288
+ chunks.push({
1289
+ time,
1290
+ chunk
1291
+ });
1292
+ }
1293
+ }
1294
+ return chunks;
1295
+ }
1296
+ function hasNonWhitespace(text) {
1297
+ return /\S/.test(text);
1298
+ }
1299
+ function blockIsVisible(block) {
1300
+ if (block.type === "tool-call") return false;
1301
+ if (block.type === "text" || block.type === "reasoning") return hasNonWhitespace(block.text);
1302
+ return true;
1303
+ }
1304
+ /**
1305
+ * Whether one chunk carries the model's first output token for latency measurement.
1306
+ * @param chunk - any stream chunk.
1307
+ * @returns true for a non-empty text, reasoning, or Tool-call arguments fragment and for
1308
+ * every name-bearing Tool-call delta; false for block, usage, and finish chunks.
1309
+ */
1310
+ function isTokenDelta(chunk) {
1311
+ switch (chunk.type) {
1312
+ case "text-delta":
1313
+ case "reasoning-delta": return chunk.text !== "";
1314
+ case "tool-call-delta": return chunk.argumentsDelta !== "" || chunk.name !== void 0;
1315
+ default: return false;
1316
+ }
1317
+ }
1318
+ /**
1319
+ * Whether one chunk by itself contributes reader-visible transcript content.
1320
+ * Text and reasoning count only with non-whitespace content, streamed as a delta or
1321
+ * completed as a block; a block of any other kind counts at its start and its end,
1322
+ * except a Tool call, which is protocol rather than content. Usage and finish never count.
1323
+ * @param chunk - any stream chunk.
1324
+ * @returns whether a transcript reader would see this chunk.
1325
+ */
1326
+ function isVisibleChunk(chunk) {
1327
+ switch (chunk.type) {
1328
+ case "text-delta":
1329
+ case "reasoning-delta": return hasNonWhitespace(chunk.text);
1330
+ case "block-start": return chunk.blockType !== "text" && chunk.blockType !== "reasoning" && chunk.blockType !== "tool-call";
1331
+ case "block-end": return blockIsVisible(chunk.block);
1332
+ default: return false;
1333
+ }
1334
+ }
1335
+ /**
1336
+ * Whether one chunk carries non-whitespace text, as a text delta or a completed text block.
1337
+ * Reasoning, Tool calls, and other block kinds never count.
1338
+ * @param chunk - any stream chunk.
1339
+ * @returns whether the chunk contributes visible text.
1340
+ */
1341
+ function chunkHasVisibleText(chunk) {
1342
+ if (chunk.type === "text-delta") return hasNonWhitespace(chunk.text);
1343
+ return chunk.type === "block-end" && chunk.block.type === "text" && hasNonWhitespace(chunk.block.text);
1344
+ }
1345
+ function firstRunMemberTime(run, predicate) {
1346
+ const fragments = run.type === "tool-call-chunks" ? run.args : run.texts;
1347
+ let time = run.time0;
1348
+ for (let index = 0; index < fragments.length; index += 1) {
1349
+ if (index > 0) time += run.dt[index - 1];
1350
+ if (predicate(fragments[index])) return time;
1351
+ }
1352
+ }
1353
+ /**
1354
+ * Time of the first member of one packed run that {@link isTokenDelta} accepts: a
1355
+ * name-bearing Tool-call run starts at its first member, otherwise the first non-empty fragment.
1356
+ * Stops scanning at that member.
1357
+ * @param run - one packed delta run.
1358
+ * @returns the member's reconstructed time, or undefined when no member qualifies.
1359
+ */
1360
+ function runFirstTokenTime(run) {
1361
+ if (run.type === "tool-call-chunks" && run.name !== void 0) return run.time0;
1362
+ return firstRunMemberTime(run, (fragment) => fragment !== "");
1363
+ }
1364
+ /**
1365
+ * Time of the first member of one packed run that {@link isVisibleChunk} accepts: the first
1366
+ * non-whitespace text or reasoning fragment. A Tool-call run has none. Stops scanning at that member.
1367
+ * @param run - one packed delta run.
1368
+ * @returns the member's reconstructed time, or undefined when no member qualifies.
1369
+ */
1370
+ function runFirstVisibleTime(run) {
1371
+ return run.type === "tool-call-chunks" ? void 0 : firstRunMemberTime(run, hasNonWhitespace);
1372
+ }
1373
+ /**
1374
+ * Time of the first token in one compact stream per {@link isTokenDelta}, read from the
1375
+ * records themselves and stopping at the first qualifying member.
1376
+ * @param stream - compact records from one durable Assistant settlement.
1377
+ * @returns the first token's time, or undefined when the stream carries no token.
1378
+ */
1379
+ function assistantStreamFirstTokenTime(stream) {
1380
+ for (const record of stream) {
1381
+ const time = record.type === "chunk" ? isTokenDelta(record.chunk) ? record.time : void 0 : runFirstTokenTime(record);
1382
+ if (time !== void 0) return time;
1383
+ }
1384
+ }
1385
+ /**
1386
+ * Whether one compact stream carries any reader-visible content per {@link isVisibleChunk},
1387
+ * stopping at the first qualifying member.
1388
+ * @param stream - compact records from one durable Assistant settlement.
1389
+ * @returns whether a transcript reader would see anything from this stream.
1390
+ */
1391
+ function assistantStreamHasVisibleContent(stream) {
1392
+ return stream.some((record) => record.type === "chunk" ? isVisibleChunk(record.chunk) : runFirstVisibleTime(record) !== void 0);
1393
+ }
1394
+ /**
1395
+ * Whether one compact stream carries non-whitespace text per {@link chunkHasVisibleText},
1396
+ * stopping at the first qualifying member.
1397
+ * @param stream - compact records from one durable Assistant settlement.
1398
+ * @returns whether the stream contributes visible text.
1399
+ */
1400
+ function assistantStreamHasVisibleText(stream) {
1401
+ return stream.some((record) => record.type === "text-chunks" ? record.texts.some(hasNonWhitespace) : record.type === "chunk" && chunkHasVisibleText(record.chunk));
1402
+ }
1403
+ /**
1404
+ * The last raw chunk of one never-packed type, scanning backwards and stopping at the first hit.
1405
+ * @param stream - compact records from one durable Assistant settlement.
1406
+ * @param type - chunk type that only appears as a raw record.
1407
+ * @returns the stream's final chunk of that type, or undefined when it has none.
1408
+ */
1409
+ function lastAssistantStreamChunk(stream, type) {
1410
+ for (let index = stream.length - 1; index >= 0; index -= 1) {
1411
+ const record = stream[index];
1412
+ if (record.type === "chunk" && record.chunk.type === type) return record.chunk;
1413
+ }
1414
+ }
1415
+ /**
1416
+ * Every raw chunk of one never-packed type, in stream order.
1417
+ * @param stream - compact records from one durable Assistant settlement.
1418
+ * @param type - chunk type that only appears as a raw record.
1419
+ * @returns the matching chunks; empty when the stream has none.
1420
+ */
1421
+ function assistantStreamChunks(stream, type) {
1422
+ const chunks = [];
1423
+ for (const record of stream) if (record.type === "chunk" && record.chunk.type === type) chunks.push(record.chunk);
1424
+ return chunks;
1425
+ }
1426
+ /**
1427
+ * Every streamed text-delta fragment joined in stream order; reasoning and Tool-call fragments are excluded.
1428
+ * @param stream - compact records from one durable Assistant settlement.
1429
+ * @returns the joined text, empty when the stream carries no text delta.
1430
+ */
1431
+ function joinAssistantStreamText(stream) {
1432
+ const parts = [];
1433
+ for (const record of stream) if (record.type === "text-chunks") parts.push(record.texts.join(""));
1434
+ else if (record.type === "chunk" && record.chunk.type === "text-delta") parts.push(record.chunk.text);
1435
+ return parts.join("");
1436
+ }
1437
+ /**
1438
+ * Feed one compact stream into a {@link BlockAssembler} without materializing members.
1439
+ * Each run contributes one delta carrying its joined fragments, which assembles the same
1440
+ * blocks as the original per-member deltas because assembly only concatenates them;
1441
+ * raw chunks are pushed as recorded. The records are trusted, not validated: validate a
1442
+ * stream read at a durable boundary with {@link expandAssistantStream} first.
1443
+ * @param stream - compact records from one durable Assistant settlement.
1444
+ * @param assembler - assembler to feed; a fresh one by default.
1445
+ * @returns the same assembler after every record was pushed.
1446
+ */
1447
+ function assembleAssistantStream(stream, assembler = new BlockAssembler()) {
1448
+ for (const record of stream) switch (record.type) {
1449
+ case "chunk":
1450
+ assembler.push(record.chunk);
1451
+ break;
1452
+ case "text-chunks":
1453
+ assembler.push({
1454
+ type: "text-delta",
1455
+ index: record.index,
1456
+ text: record.texts.join("")
1457
+ });
1458
+ break;
1459
+ case "reasoning-chunks":
1460
+ assembler.push({
1461
+ type: "reasoning-delta",
1462
+ index: record.index,
1463
+ text: record.texts.join("")
1464
+ });
1465
+ break;
1466
+ case "tool-call-chunks":
1467
+ assembler.push({
1468
+ type: "tool-call-delta",
1469
+ index: record.index,
1470
+ id: record.id,
1471
+ ...record.name === void 0 ? {} : { name: record.name },
1472
+ argumentsDelta: record.args.join("")
1473
+ });
1474
+ break;
1475
+ default: assertNever(record, "assembleAssistantStream");
1476
+ }
1477
+ return assembler;
1478
+ }
1479
+ function validateRecord(value) {
1480
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("Assistant stream record must be an object");
1481
+ const record = value;
1482
+ switch (record.type) {
1483
+ case "text-chunks":
1484
+ case "reasoning-chunks": {
1485
+ exactKeys(record, [
1486
+ "type",
1487
+ "time0",
1488
+ "index",
1489
+ "dt",
1490
+ "texts"
1491
+ ], record.type);
1492
+ const texts = stringArray(record.texts, `${record.type} texts`);
1493
+ if (texts.length === 0) throw new TypeError(`${record.type} texts must be non-empty`);
1494
+ validateRun(record, texts.length, record.type);
1495
+ return record;
1496
+ }
1497
+ case "tool-call-chunks": {
1498
+ exactKeys(record, Object.hasOwn(record, "name") ? [
1499
+ "type",
1500
+ "time0",
1501
+ "index",
1502
+ "dt",
1503
+ "id",
1504
+ "name",
1505
+ "args"
1506
+ ] : [
1507
+ "type",
1508
+ "time0",
1509
+ "index",
1510
+ "dt",
1511
+ "id",
1512
+ "args"
1513
+ ], record.type);
1514
+ const args = stringArray(record.args, "tool-call-chunks args");
1515
+ if (args.length === 0) throw new TypeError("tool-call-chunks args must be non-empty");
1516
+ if (typeof record.id !== "string" || record.id.length === 0) throw new TypeError("tool-call-chunks id must be a non-empty string");
1517
+ if (record.name !== void 0 && (typeof record.name !== "string" || record.name.length === 0)) throw new TypeError("tool-call-chunks name must be a non-empty string");
1518
+ validateRun(record, args.length, record.type);
1519
+ return record;
1520
+ }
1521
+ case "chunk": {
1522
+ exactKeys(record, [
1523
+ "type",
1524
+ "time",
1525
+ "chunk"
1526
+ ], "chunk");
1527
+ const time = safeTime(record.time);
1528
+ if (typeof record.chunk !== "object" || record.chunk === null || Array.isArray(record.chunk)) throw new TypeError("Assistant stream raw chunk must be a lossless JSON object");
1529
+ let chunk;
1530
+ try {
1531
+ chunk = snapshotChunk(record.chunk);
1532
+ } catch (error) {
1533
+ throw new TypeError("Assistant stream raw chunk must be a lossless JSON object", { cause: error });
1534
+ }
1535
+ return deepFreeze({
1536
+ type: "chunk",
1537
+ time,
1538
+ chunk
1539
+ });
1540
+ }
1541
+ default: throw new TypeError(`Unsupported Assistant stream record ${JSON.stringify(record.type)}`);
1542
+ }
1543
+ }
1544
+ function validateRun(record, members, label) {
1545
+ safeTime(record.time0);
1546
+ safeIndex(record.index, label);
1547
+ if (!Array.isArray(record.dt) || record.dt.some((value) => !Number.isSafeInteger(value))) throw new TypeError(`${label} dt must contain safe integers`);
1548
+ if (record.dt.length !== members - 1) throw new TypeError(`${label} dt length must be one less than its members`);
1549
+ let time = record.time0;
1550
+ for (const gap of record.dt) {
1551
+ time += gap;
1552
+ if (!Number.isSafeInteger(time)) throw new TypeError(`${label} member times must stay safe integers`);
1553
+ }
1554
+ }
1555
+ function stringArray(value, label) {
1556
+ if (!Array.isArray(value) || value.some((member) => typeof member !== "string")) throw new TypeError(`${label} must be a string array`);
1557
+ return value;
1558
+ }
1559
+ function exactKeys(record, keys, label) {
1560
+ if (Object.keys(record).length !== keys.length || !keys.every((key) => Object.hasOwn(record, key))) throw new TypeError(`${label} Assistant stream record must contain exactly ${keys.join(", ")}`);
1561
+ }
1562
+ //#endregion
1563
+ //#region lib/types/index.js
1564
+ /**
1565
+ * LLM service: adapter registry with a waterfall-interceptable streaming call
1566
+ * API. Exports the `LlmRuntime` default, the abstract `LlmAdapter` for
1567
+ * provider backends, and `BlockAssembler` for chunk assembly.
1568
+ *
1569
+ * @module @qilin-agent/llm
1570
+ */
1571
+ var __runInitializers = function(thisArg, initializers, value) {
1572
+ var useValue = arguments.length > 2;
1573
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
1574
+ return useValue ? value : void 0;
1575
+ };
1576
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
1577
+ function accept(f) {
1578
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
1579
+ return f;
1580
+ }
1581
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
1582
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
1583
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
1584
+ var _, done = false;
1585
+ for (var i = decorators.length - 1; i >= 0; i--) {
1586
+ var context = {};
1587
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
1588
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
1589
+ context.addInitializer = function(f) {
1590
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
1591
+ extraInitializers.push(accept(f || null));
1592
+ };
1593
+ var result = (0, decorators[i])(kind === "accessor" ? {
1594
+ get: descriptor.get,
1595
+ set: descriptor.set
1596
+ } : descriptor[key], context);
1597
+ if (kind === "accessor") {
1598
+ if (result === void 0) continue;
1599
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
1600
+ if (_ = accept(result.get)) descriptor.get = _;
1601
+ if (_ = accept(result.set)) descriptor.set = _;
1602
+ if (_ = accept(result.init)) initializers.unshift(_);
1603
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
1604
+ else descriptor[key] = _;
1605
+ }
1606
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
1607
+ done = true;
1608
+ };
1609
+ /**
1610
+ * Typed error for LLM-related failures. Extends {@link HarnessError}, so the
1611
+ * `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
1612
+ */
1613
+ var LlmError = class extends HarnessError {
1614
+ /** Serializable facts retained beside this live Error. */
1615
+ failure;
1616
+ /**
1617
+ * @param message - non-empty human-readable failure summary.
1618
+ * @param code - non-empty stable provider-neutral machine code.
1619
+ * @param options - optional cause and validated serializable provider facts.
1620
+ */
1621
+ constructor(message, code, options) {
1622
+ if (typeof message !== "string" || message.length === 0) throw new Error("LlmError message must be a non-empty string");
1623
+ if (typeof code !== "string" || code.length === 0) throw new Error("LlmError code must be a non-empty string");
1624
+ if (options?.status !== void 0 && (!Number.isInteger(options.status) || options.status < 100 || options.status > 599)) throw new Error("LlmError status must be an integer from 100 through 599");
1625
+ if (options?.providerRetryAfterMs !== void 0 && (!Number.isFinite(options.providerRetryAfterMs) || options.providerRetryAfterMs <= 0)) throw new Error("LlmError providerRetryAfterMs must be a positive finite number");
1626
+ if (options?.requestId !== void 0 && (typeof options.requestId !== "string" || options.requestId.length === 0)) throw new Error("LlmError requestId must be a non-empty string");
1627
+ super(message, code, options);
1628
+ this.name = "LlmError";
1629
+ this.failure = Object.freeze({
1630
+ message,
1631
+ code,
1632
+ ...options?.status === void 0 ? {} : { status: options.status },
1633
+ ...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
1634
+ ...options?.requestId === void 0 ? {} : { requestId: options.requestId },
1635
+ ...options?.offloadImages === void 0 ? {} : { offloadImages: options.offloadImages }
1636
+ });
1637
+ }
1638
+ };
1639
+ /**
1640
+ * Accept one supplied credential, or refuse it as unusable.
1641
+ *
1642
+ * A stored key arrives from the credentials seam, a `.env` line, or a shell
1643
+ * export, all of which pick up surrounding whitespace, so trimming is silent.
1644
+ * Anything else fails here rather than inside `fetch`, whose ByteString
1645
+ * refusal names a UTF-16 code point instead of the setting to change. The key
1646
+ * never enters the message: `ref` names where to fix it, and echoing any part
1647
+ * of a secret into a log or a UI is the failure this diagnosis avoids.
1648
+ *
1649
+ * Lives beside {@link LlmError} rather than in `./api-key.ts` so the predicate
1650
+ * module stays dependency-free; both adapters share this one diagnosis instead
1651
+ * of keeping near-identical local copies.
1652
+ * @param raw - the credential exactly as supplied.
1653
+ * @param pkg - the refusing package name, prefixed to the diagnostic.
1654
+ * @param ref - the credential reference the value resolved through.
1655
+ * @returns the trimmed, usable key.
1656
+ */
1657
+ function assertUsableApiKey(raw, pkg, ref) {
1658
+ const checked = normalizeApiKey(raw);
1659
+ if (checked.ok) return checked.value;
1660
+ throw new LlmError(checked.reason === "empty" ? `${pkg}: the API key resolved from ${ref} is blank; set ${ref} to the raw key (the web Models page writes it) or export it in the launching environment` : `${pkg}: the API key resolved from ${ref} contains characters no HTTP header can carry; set ${ref} to the raw key alone (the web Models page writes it)`, INVALID_CREDENTIAL_CODE);
1661
+ }
1662
+ /**
1663
+ * Provider-wire adapter for the harness message and stream vocabulary. Register implementations
1664
+ * with `ctx.llm.registerAdapter(providers, adapter)`. Every provider HTTP request must include
1665
+ * `attributionHeaders()`; prove the headers are added in the wire request or library header hook. The direct-fetch
1666
+ * DeepSeek and library-backed pi-ai adapters meet this contract through different internals.
1667
+ */
1668
+ var LlmAdapter = class {
1669
+ /**
1670
+ * Describe one provider route owned by this adapter.
1671
+ * @param provider - a route passed to `registerAdapter()` for this instance.
1672
+ * @returns detached display metadata whose id must equal `provider`.
1673
+ */
1674
+ providerInfo(provider) {
1675
+ return {
1676
+ id: provider,
1677
+ name: provider
1678
+ };
1679
+ }
1680
+ /**
1681
+ * Return the provider-owned retry policy captured with this route.
1682
+ * @param _provider - a route passed to `registerAdapter()` for this instance.
1683
+ * @returns a resolved policy, or `undefined` to use the normal defaults.
1684
+ */
1685
+ providerRetryPolicy(_provider) {}
1686
+ /**
1687
+ * Resolve provider-side request-image pricing for one exact model route.
1688
+ * The default declares none, so consumers fall back to their own neutral
1689
+ * estimate. Implementations must answer synchronously without I/O; the
1690
+ * token meter resolves this per measurement.
1691
+ * @param _provider - a route passed to `registerAdapter()` for this instance.
1692
+ * @param _model - exact model id passed to {@link GenerateOptions.model}.
1693
+ * @returns route-owned image pricing, or `undefined` when the route declares none.
1694
+ */
1695
+ imageRequestPricing(_provider, _model) {}
1696
+ /**
1697
+ * List models this adapter can currently advertise for one owned provider.
1698
+ * Core routing accepts unlisted model ids; catalog-driven entry points such
1699
+ * as the GUI may require membership. Adapters used there must advertise
1700
+ * their available models; the base empty catalog offers no GUI selection.
1701
+ * @param _provider - one provider route owned by this adapter.
1702
+ * @returns discoverable models in adapter-preferred order.
1703
+ */
1704
+ listModels(_provider) {
1705
+ return Promise.resolve([]);
1706
+ }
1707
+ /**
1708
+ * Resolve all metadata available for one exact model. This query is
1709
+ * independent of the advisory catalog and does not validate request routing.
1710
+ * @param provider - one provider route owned by this adapter.
1711
+ * @param model - exact model id passed to {@link GenerateOptions.model}.
1712
+ * @param _signal - cancellation for this exact-model lookup; asynchronous
1713
+ * implementations must settle promptly after it aborts.
1714
+ * @returns provider/model identity plus any context, call-default, and reasoning metadata.
1715
+ */
1716
+ resolveModel(provider, model, _signal) {
1717
+ return Promise.resolve({
1718
+ provider,
1719
+ id: model,
1720
+ name: model
1721
+ });
1722
+ }
1723
+ /**
1724
+ * Bind exact model metadata and the eventual request dispatch to one adapter generation.
1725
+ * Dynamic adapters override this so settings changes between preparation and
1726
+ * dispatch cannot combine one generation's capabilities with another's endpoint.
1727
+ * @param provider - registered provider route.
1728
+ * @param model - exact model id.
1729
+ * @param signal - cancellation for model resolution.
1730
+ * @returns model metadata and a one-generation stream entry point.
1731
+ */
1732
+ async prepareCall(provider, model, signal) {
1733
+ return {
1734
+ model: await this.resolveModel(provider, model, signal),
1735
+ stream: (options) => this.stream(options)
1736
+ };
1737
+ }
1738
+ };
1739
+ /**
1740
+ * The abstract `llm` service: an adapter registry plus a streaming model-call
1741
+ * API, interceptable via the `llm/stream` waterfall.
1742
+ */
1743
+ let LlmRuntime = (() => {
1744
+ let _classSuper = TypertRemoteService;
1745
+ let _instanceExtraInitializers = [];
1746
+ let _listProviders_decorators;
1747
+ let _listConfigurableProviders_decorators;
1748
+ let _remoteDiscoverModels_decorators;
1749
+ return class LlmRuntime extends _classSuper {
1750
+ static {
1751
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
1752
+ _listProviders_decorators = [Remote];
1753
+ _listConfigurableProviders_decorators = [Remote];
1754
+ _remoteDiscoverModels_decorators = [Remote("discoverModels")];
1755
+ __esDecorate(this, null, _listProviders_decorators, {
1756
+ kind: "method",
1757
+ name: "listProviders",
1758
+ static: false,
1759
+ private: false,
1760
+ access: {
1761
+ has: (obj) => "listProviders" in obj,
1762
+ get: (obj) => obj.listProviders
1763
+ },
1764
+ metadata: _metadata
1765
+ }, null, _instanceExtraInitializers);
1766
+ __esDecorate(this, null, _listConfigurableProviders_decorators, {
1767
+ kind: "method",
1768
+ name: "listConfigurableProviders",
1769
+ static: false,
1770
+ private: false,
1771
+ access: {
1772
+ has: (obj) => "listConfigurableProviders" in obj,
1773
+ get: (obj) => obj.listConfigurableProviders
1774
+ },
1775
+ metadata: _metadata
1776
+ }, null, _instanceExtraInitializers);
1777
+ __esDecorate(this, null, _remoteDiscoverModels_decorators, {
1778
+ kind: "method",
1779
+ name: "remoteDiscoverModels",
1780
+ static: false,
1781
+ private: false,
1782
+ access: {
1783
+ has: (obj) => "remoteDiscoverModels" in obj,
1784
+ get: (obj) => obj.remoteDiscoverModels
1785
+ },
1786
+ metadata: _metadata
1787
+ }, null, _instanceExtraInitializers);
1788
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
1789
+ enumerable: true,
1790
+ configurable: true,
1791
+ writable: true,
1792
+ value: _metadata
1793
+ });
1794
+ }
1795
+ adapters = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Map());
1796
+ directory = /* @__PURE__ */ new Map();
1797
+ discoveries = /* @__PURE__ */ new Map();
1798
+ constructor(ctx) {
1799
+ super(ctx, "llm");
1800
+ }
1801
+ /** Notify topology observers without letting one broken listener veto the commit. */
1802
+ emitAdaptersUpdated() {
1803
+ let invariantFailure;
1804
+ for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
1805
+ const returned = listener();
1806
+ if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
1807
+ this.warnAdaptersListenerFailure(error);
1808
+ });
1809
+ } catch (error) {
1810
+ if (error?.code === "INVARIANT") {
1811
+ invariantFailure ??= error;
1812
+ continue;
1813
+ }
1814
+ this.warnAdaptersListenerFailure(error);
1815
+ }
1816
+ if (invariantFailure !== void 0) throw invariantFailure;
1817
+ }
1818
+ /** Contained-listener diagnostic shared by the sync and async failure paths. */
1819
+ warnAdaptersListenerFailure(error) {
1820
+ this.ctx.logger.warn("llm: an llm/adapters-updated listener failed");
1821
+ this.ctx.logger.warn(error);
1822
+ }
1823
+ /**
1824
+ * Register an adapter for the given provider routes. Throws `LlmError` with code
1825
+ * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
1826
+ * Disposed with the fiber.
1827
+ * @param providers - every provider route this adapter should serve.
1828
+ * @param adapter - the adapter that streams calls for those providers.
1829
+ * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
1830
+ */
1831
+ registerAdapter(providers, adapter) {
1832
+ const owned = /* @__PURE__ */ new Set();
1833
+ let released = false;
1834
+ const dispose = this.ctx.effect(function* () {
1835
+ if (providers.length === 0) throw new LlmError("an adapter must register at least one provider", "INVALID_ADAPTER");
1836
+ this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
1837
+ yield () => {
1838
+ released = true;
1839
+ for (const provider of owned) this.adapters.delete(provider);
1840
+ owned.clear();
1841
+ this.emitAdaptersUpdated();
1842
+ };
1843
+ }.bind(this), "llm.registerAdapter()");
1844
+ const handle = (() => void dispose());
1845
+ handle.replace = (next) => {
1846
+ if (released) throw new LlmError("a disposed adapter registration cannot replace its routes", "REGISTRATION_DISPOSED");
1847
+ this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
1848
+ };
1849
+ return handle;
1850
+ }
1851
+ /**
1852
+ * Validate one candidate route set for `adapter`, treating routes this
1853
+ * registration already holds as available. Nothing is mutated: a rejected
1854
+ * candidate leaves the registry exactly as it was.
1855
+ */
1856
+ prepareRoutes(providers, adapter, owned) {
1857
+ const unique = /* @__PURE__ */ new Set();
1858
+ const registrations = [];
1859
+ for (const provider of providers) {
1860
+ if (provider.length === 0) throw new LlmError("adapter provider names must be non-empty", "INVALID_ADAPTER");
1861
+ if (unique.has(provider) || this.adapters.has(provider) && !owned.has(provider)) throw new LlmError(`an adapter for provider "${provider}" is already registered`, "DUPLICATE_ADAPTER");
1862
+ const info = adapter.providerInfo(provider);
1863
+ if (typeof info.id !== "string" || info.id !== provider || typeof info.name !== "string" || info.name.length === 0) throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, "INVALID_ADAPTER");
1864
+ unique.add(provider);
1865
+ const retryPolicy = adapter.providerRetryPolicy(provider) ?? resolveRetryPolicy(void 0, `llm: provider "${provider}" retryPolicy`);
1866
+ registrations.push({
1867
+ adapter,
1868
+ provider: {
1869
+ id: info.id,
1870
+ name: info.name
1871
+ },
1872
+ retryPolicy
1873
+ });
1874
+ }
1875
+ return registrations;
1876
+ }
1877
+ /**
1878
+ * Swap this registration's routes for the prepared ones in one synchronous
1879
+ * section, so no observer can see the registry between the release and the
1880
+ * re-registration. The route set's one mutation point is also where
1881
+ * `llm/adapters-updated` is published, so a `replace` announces itself
1882
+ * exactly like a first registration.
1883
+ */
1884
+ commitRoutes(owned, registrations) {
1885
+ for (const provider of owned) this.adapters.delete(provider);
1886
+ owned.clear();
1887
+ for (const registration of registrations) {
1888
+ this.adapters.set(registration.provider.id, registration);
1889
+ owned.add(registration.provider.id);
1890
+ }
1891
+ this.emitAdaptersUpdated();
1892
+ }
1893
+ /**
1894
+ * Describe provider routes with a registered adapter.
1895
+ * @returns detached provider metadata in registration order.
1896
+ */
1897
+ listProviders() {
1898
+ return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
1899
+ }
1900
+ /**
1901
+ * Declare provider routes an adapter plugin can activate through
1902
+ * configuration. Registration is all-or-nothing: an empty list, invalid
1903
+ * entry, or a provider already declared by any registration throws
1904
+ * `LlmError` without registering the rest. Disposed with the fiber.
1905
+ * @param entries - every configurable provider this plugin owns.
1906
+ * @returns a handle that withdraws all of them, and can atomically replace them.
1907
+ */
1908
+ registerConfigurableProviders(entries) {
1909
+ let held = [];
1910
+ let disposed = false;
1911
+ /**
1912
+ * Validate a candidate set in full against everything this registration
1913
+ * does not already hold, then publish it. Nothing is written until the
1914
+ * whole set passes, so a refused candidate leaves the current entries in
1915
+ * place — the property that makes `replace` a swap rather than a
1916
+ * delete-then-add that can strand the directory empty.
1917
+ */
1918
+ const commit = (candidates) => {
1919
+ const detached = [];
1920
+ const own = new Set(held.map((entry) => entry.provider));
1921
+ for (const entry of candidates) {
1922
+ if (entry.provider.length === 0 || entry.displayName.length === 0 || entry.settingsNs.length === 0) throw new LlmError("configurable providers need a non-empty provider, displayName, and settingsNs", "INVALID_DIRECTORY");
1923
+ if (entry.settingsPath.some((segment) => segment.length === 0)) throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, "INVALID_DIRECTORY");
1924
+ if (this.directory.has(entry.provider) && !own.has(entry.provider) || detached.some((seen) => seen.provider === entry.provider)) throw new LlmError(`configurable provider "${entry.provider}" is already declared`, "DUPLICATE_DIRECTORY");
1925
+ detached.push({
1926
+ ...entry,
1927
+ settingsPath: [...entry.settingsPath]
1928
+ });
1929
+ }
1930
+ for (const entry of held) this.directory.delete(entry.provider);
1931
+ for (const entry of detached) this.directory.set(entry.provider, entry);
1932
+ held = detached;
1933
+ this.emitAdaptersUpdated();
1934
+ };
1935
+ const dispose = this.ctx.effect(function* () {
1936
+ if (entries.length === 0) throw new LlmError("a configurable-provider registration must declare at least one provider", "INVALID_DIRECTORY");
1937
+ commit(entries);
1938
+ yield () => {
1939
+ disposed = true;
1940
+ for (const entry of held) this.directory.delete(entry.provider);
1941
+ held = [];
1942
+ this.emitAdaptersUpdated();
1943
+ };
1944
+ }.bind(this), "llm.registerConfigurableProviders()");
1945
+ const handle = (() => void dispose());
1946
+ handle.replace = (next) => {
1947
+ if (disposed) throw new LlmError("this configurable-provider registration was disposed", "REGISTRATION_DISPOSED");
1948
+ commit(next);
1949
+ };
1950
+ return handle;
1951
+ }
1952
+ /**
1953
+ * List every declared configurable provider, registered or dormant.
1954
+ * @returns detached directory entries in declaration order.
1955
+ */
1956
+ listConfigurableProviders() {
1957
+ return [...this.directory.values()].map((entry) => ({
1958
+ ...entry,
1959
+ settingsPath: [...entry.settingsPath]
1960
+ }));
1961
+ }
1962
+ /**
1963
+ * Offer to interrogate provider endpoints on behalf of the settings
1964
+ * namespace this plugin owns. The namespace is the key because that is what
1965
+ * a configuration surface already holds from the configurable-provider
1966
+ * directory, and because a provider being *added* has no route to name yet.
1967
+ * Disposed with the fiber.
1968
+ * @param settingsNs - the namespace whose profiles this discovery serves.
1969
+ * @param discover - interrogates one endpoint and must honor the supplied signal.
1970
+ * @returns the disposer that withdraws the offer.
1971
+ */
1972
+ registerModelDiscovery(settingsNs, discover) {
1973
+ const dispose = this.ctx.effect(function* () {
1974
+ if (settingsNs.length === 0) throw new LlmError("model discovery needs a non-empty settings namespace", "INVALID_DISCOVERY");
1975
+ if (this.discoveries.has(settingsNs)) throw new LlmError(`model discovery for "${settingsNs}" is already registered`, "DUPLICATE_DISCOVERY");
1976
+ this.discoveries.set(settingsNs, discover);
1977
+ yield () => {
1978
+ this.discoveries.delete(settingsNs);
1979
+ };
1980
+ }.bind(this), "llm.registerModelDiscovery()");
1981
+ return () => void dispose();
1982
+ }
1983
+ /**
1984
+ * Interrogate one provider endpoint for the models it advertises. The
1985
+ * request describes a draft, not a stored route, so nothing here reads or
1986
+ * writes settings or credentials — the caller owns both, and the reply is
1987
+ * candidate metadata a surface may offer for adoption.
1988
+ * @param settingsNs - namespace whose registered discovery serves this draft.
1989
+ * @param request - the endpoint, protocol, and one-shot credential to use.
1990
+ * @param signal - caller cancellation.
1991
+ * @returns the advertised models, deduplicated in endpoint order.
1992
+ */
1993
+ async discoverModels(settingsNs, request, signal) {
1994
+ const discover = this.discoveries.get(settingsNs);
1995
+ if (discover === void 0) throw new LlmError(`no model discovery is registered for "${settingsNs}"`, "NO_DISCOVERY");
1996
+ if ((request.provider ?? "").length === 0 && (request.baseURL ?? "").length === 0) throw new LlmError("model discovery needs a provider route or a baseURL", "INVALID_DISCOVERY");
1997
+ const discovered = signal === void 0 ? await discover(request) : await discover(request, signal);
1998
+ const seen = /* @__PURE__ */ new Set();
1999
+ const models = [];
2000
+ for (const model of discovered) {
2001
+ if (typeof model.id !== "string" || model.id.length === 0 || seen.has(model.id)) continue;
2002
+ seen.add(model.id);
2003
+ models.push({
2004
+ id: model.id,
2005
+ ...model.name === void 0 ? {} : { name: model.name },
2006
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
2007
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
2008
+ ...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
2009
+ });
2010
+ }
2011
+ return models;
2012
+ }
2013
+ /**
2014
+ * Remote adapter for one draft provider interrogation.
2015
+ * @param settingsNs - namespace whose registered discovery serves this draft.
2016
+ * @param request - endpoint, protocol, and one-shot credential to use.
2017
+ * @param signal - caller cancellation supplied by the Remote carrier.
2018
+ * @returns advertised models in endpoint order.
2019
+ * @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
2020
+ */
2021
+ async remoteDiscoverModels(settingsNs, request, signal) {
2022
+ try {
2023
+ return await this.discoverModels(settingsNs, request, signal);
2024
+ } catch (error) {
2025
+ throw new RemoteError("llm/model-discovery-rejected", error instanceof Error ? error.message : String(error), {
2026
+ settingsNs,
2027
+ ...request.baseURL === void 0 ? {} : { baseURL: request.baseURL }
2028
+ }, { cause: error });
2029
+ }
2030
+ }
2031
+ /**
2032
+ * Resolve the retry policy captured when one provider route was registered.
2033
+ * @param provider - registered provider route to inspect.
2034
+ * @returns the provider-owned policy, with normal defaults already resolved.
2035
+ */
2036
+ providerRetryPolicy(provider) {
2037
+ return this.registration(provider).retryPolicy;
2038
+ }
2039
+ /**
2040
+ * Resolve provider-side request-image pricing for one exact route, or
2041
+ * `undefined` when the provider is unregistered or declares none. Unknown
2042
+ * providers degrade to `undefined` rather than throwing because callers
2043
+ * price durable history whose route may no longer be mounted.
2044
+ * @param provider - provider route named by a request header.
2045
+ * @param model - exact model id named by the same header.
2046
+ * @returns the owning adapter's image pricing for the route, when declared.
2047
+ */
2048
+ imageRequestPricing(provider, model) {
2049
+ return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
2050
+ }
2051
+ /**
2052
+ * Resolve the exact text one durable file occurrence contributes to every
2053
+ * provider request in the current execution environment.
2054
+ * @param ref - durable verbatim file reference from model history.
2055
+ * @returns the same deterministic handle text used at adapter dispatch.
2056
+ */
2057
+ fileRequestText(ref) {
2058
+ return fileHandleText(ref, this.fileReadPath(ref));
2059
+ }
2060
+ /** Detach typed adapter-owned modality metadata. */
2061
+ detachedModalities(modalities) {
2062
+ return modalities === void 0 ? void 0 : [...modalities];
2063
+ }
2064
+ /**
2065
+ * Discover models advertised by one registered provider. Catalog membership
2066
+ * does not constrain core routing. Catalog-driven entry points may restrict
2067
+ * selection and submission to the advertised models.
2068
+ * @param provider - registered provider route to inspect.
2069
+ * @returns detached model metadata in adapter-preferred order.
2070
+ */
2071
+ async listModels(provider) {
2072
+ const models = await this.registration(provider).adapter.listModels(provider);
2073
+ const seen = /* @__PURE__ */ new Set();
2074
+ return models.map((model) => {
2075
+ if (typeof model.provider !== "string" || model.provider !== provider || typeof model.id !== "string" || model.id.length === 0 || typeof model.name !== "string" || model.name.length === 0 || model.description !== void 0 && typeof model.description !== "string" || seen.has(model.id)) throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, "INVALID_CATALOG");
2076
+ seen.add(model.id);
2077
+ const inputModalities = this.detachedModalities(model.inputModalities);
2078
+ return {
2079
+ provider: model.provider,
2080
+ id: model.id,
2081
+ name: model.name,
2082
+ ...model.description === void 0 ? {} : { description: model.description },
2083
+ ...inputModalities === void 0 ? {} : { inputModalities }
2084
+ };
2085
+ });
2086
+ }
2087
+ /**
2088
+ * Resolve and validate all metadata from the adapter that owns one exact
2089
+ * route. The result is detached from adapter-owned objects; catalog
2090
+ * membership remains advisory and does not control request routing.
2091
+ * @param provider - registered provider route to inspect.
2092
+ * @param model - exact model id passed to the adapter.
2093
+ * @param signal - optional cancellation for adapter-owned asynchronous lookup.
2094
+ * @returns exact model identity plus available context and reasoning metadata.
2095
+ */
2096
+ async resolveModelInfo(provider, model, signal) {
2097
+ return this.resolveModelInfoFor(this.registration(provider), model, signal);
2098
+ }
2099
+ async resolveModelInfoFor(registration, model, signal) {
2100
+ const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
2101
+ return this.normalizeModelInfo(registration, model, resolved);
2102
+ }
2103
+ /** Validate and detach one adapter-returned exact model result. */
2104
+ normalizeModelInfo(registration, model, resolved) {
2105
+ const provider = registration.provider.id;
2106
+ if (typeof resolved.provider !== "string" || resolved.provider !== provider || typeof resolved.id !== "string" || resolved.id !== model || typeof resolved.name !== "string" || resolved.name.length === 0 || resolved.description !== void 0 && typeof resolved.description !== "string") throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
2107
+ const context = resolved.context;
2108
+ if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
2109
+ const inputModalities = this.detachedModalities(resolved.inputModalities);
2110
+ const systemPromptUpdate = resolved.systemPromptUpdate;
2111
+ if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
2112
+ const toolUpdate = resolved.toolUpdate;
2113
+ if (toolUpdate !== void 0 && toolUpdate !== "in-history" && toolUpdate !== "addition-only") throw new LlmError(`adapter returned invalid tool update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
2114
+ const defaultMaxTokens = resolved.defaultMaxTokens;
2115
+ if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
2116
+ const info = {
2117
+ provider,
2118
+ id: model,
2119
+ name: resolved.name,
2120
+ ...resolved.description === void 0 ? {} : { description: resolved.description },
2121
+ ...inputModalities === void 0 ? {} : { inputModalities },
2122
+ ...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
2123
+ ...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
2124
+ ...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate },
2125
+ ...resolved.toolUpdate === void 0 ? {} : { toolUpdate: resolved.toolUpdate }
2126
+ };
2127
+ const reasoning = resolved.reasoning;
2128
+ if (reasoning === void 0) return info;
2129
+ if (reasoning.efforts.length === 0) throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
2130
+ const seen = /* @__PURE__ */ new Set();
2131
+ const efforts = reasoning.efforts.map((effort) => {
2132
+ if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) throw new LlmError(`adapter returned invalid or duplicate reasoning effort metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
2133
+ seen.add(effort.id);
2134
+ return {
2135
+ id: effort.id,
2136
+ name: effort.name,
2137
+ ...effort.description === void 0 ? {} : { description: effort.description }
2138
+ };
2139
+ });
2140
+ if (reasoning.defaultEffort !== void 0 && !seen.has(reasoning.defaultEffort)) throw new LlmError(`adapter returned an unknown default reasoning effort for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
2141
+ return {
2142
+ ...info,
2143
+ reasoning: {
2144
+ efforts,
2145
+ ...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
2146
+ }
2147
+ };
2148
+ }
2149
+ /**
2150
+ * Validate a conversation call config against its exact model capability and
2151
+ * materialize adapter-configured defaults. Unsupported explicit efforts
2152
+ * reject before provider I/O; no clamping or aliasing is performed. This
2153
+ * standalone query does not bind a later dispatch; use {@link prepareCall}
2154
+ * when logging and streaming must share one adapter registration.
2155
+ * @param config - provider/model route and optional request controls.
2156
+ * @param signal - optional cancellation for adapter-owned capability lookup.
2157
+ * @returns a detached config only when a default must be materialized.
2158
+ */
2159
+ async resolveCallConfig(config, signal) {
2160
+ return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
2161
+ }
2162
+ async resolveCallFor(registration, config, signal) {
2163
+ const info = await this.resolveModelInfoFor(registration, config.model, signal);
2164
+ return this.resolveCallWithInfo(config, info);
2165
+ }
2166
+ /** Validate request controls against one already-bound exact model result. */
2167
+ resolveCallWithInfo(config, info) {
2168
+ const defaulted = config.maxTokens === void 0 && info.defaultMaxTokens !== void 0 ? {
2169
+ ...config,
2170
+ maxTokens: info.defaultMaxTokens
2171
+ } : config;
2172
+ const reasoning = info.reasoning;
2173
+ const requested = defaulted.reasoningEffort;
2174
+ let resolvedConfig = defaulted;
2175
+ if (reasoning === void 0) {
2176
+ if (requested !== void 0) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, "UNSUPPORTED_REASONING_EFFORT");
2177
+ } else {
2178
+ const effective = requested ?? reasoning.defaultEffort;
2179
+ if (effective !== void 0) {
2180
+ if (!reasoning.efforts.some((effort) => effort.id === effective)) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`, "UNSUPPORTED_REASONING_EFFORT");
2181
+ if (requested !== effective) resolvedConfig = {
2182
+ ...defaulted,
2183
+ reasoningEffort: effective
2184
+ };
2185
+ }
2186
+ }
2187
+ return {
2188
+ config: resolvedConfig,
2189
+ ...info.context === void 0 ? {} : { context: info.context },
2190
+ modelInfo: info
2191
+ };
2192
+ }
2193
+ /**
2194
+ * Resolve one call under its current adapter registration. The returned
2195
+ * one-shot handle keeps that registration across header logging and dispatch,
2196
+ * so HMR cannot combine one adapter's capability result with another adapter.
2197
+ * @param config - provider/model route and optional request controls.
2198
+ * @param signal - optional cancellation for adapter-owned capability lookup.
2199
+ * @returns a prepared config and its registration-bound stream entry point.
2200
+ */
2201
+ async prepareCall(config, signal) {
2202
+ const registration = this.registration(config.provider);
2203
+ const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
2204
+ const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
2205
+ const resolved = this.resolveCallWithInfo(config, modelInfo);
2206
+ const resolvedConfig = deepFreeze(structuredClone(resolved.config));
2207
+ const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
2208
+ const adapterDefaults = deepFreeze({
2209
+ ...config.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
2210
+ ...config.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
2211
+ });
2212
+ let dispatched = false;
2213
+ return Object.freeze({
2214
+ config: resolvedConfig,
2215
+ retryPolicy: registration.retryPolicy,
2216
+ adapterDefaults,
2217
+ ...context === void 0 ? {} : { context },
2218
+ ...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
2219
+ ...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
2220
+ ...modelInfo.toolUpdate === void 0 ? {} : { toolUpdate: modelInfo.toolUpdate },
2221
+ stream: (options) => {
2222
+ if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
2223
+ if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
2224
+ dispatched = true;
2225
+ return this.streamWithRegistration(options, {
2226
+ registration,
2227
+ config: resolvedConfig,
2228
+ modelInfo,
2229
+ dispatch: (options) => adapterCall.stream(options)
2230
+ });
2231
+ }
2232
+ });
2233
+ }
2234
+ registration(provider) {
2235
+ const registration = this.adapters.get(provider);
2236
+ if (!registration) throw new LlmError(`no adapter registered for provider "${provider}"`, "NO_ADAPTER");
2237
+ return registration;
2238
+ }
2239
+ /** Remove replay state whose historical route is owned by another adapter. */
2240
+ forAdapter(options, adapter) {
2241
+ const messages = options.messages.map((message) => {
2242
+ if (message.role !== "assistant") return message;
2243
+ const source = message.source;
2244
+ if (source.replayState === void 0) return message;
2245
+ if (this.adapters.get(source.provider)?.adapter === adapter) return message;
2246
+ return freezeMessage({
2247
+ ...message,
2248
+ source: {
2249
+ kind: "model",
2250
+ provider: source.provider,
2251
+ model: source.model
2252
+ }
2253
+ });
2254
+ });
2255
+ if (messages.every((message, index) => message === options.messages[index])) return options;
2256
+ const filtered = {
2257
+ ...options,
2258
+ messages
2259
+ };
2260
+ return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
2261
+ }
2262
+ /**
2263
+ * Resolve the current execution-world read path of one durable file
2264
+ * reference through the mounted attachment and filesystem providers.
2265
+ */
2266
+ fileReadPath(ref) {
2267
+ let hostPath;
2268
+ try {
2269
+ hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
2270
+ } catch {
2271
+ return;
2272
+ }
2273
+ if (hostPath === void 0) return void 0;
2274
+ return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
2275
+ }
2276
+ /**
2277
+ * Final adapter boundary. Adapter selection, dispatch, iterator construction,
2278
+ * and iteration failures become one terminal failure chunk. Middleware and
2279
+ * downstream consumer failures remain thrown plugin or consumer errors.
2280
+ */
2281
+ async *adapterStream(options, prepared) {
2282
+ let iterator;
2283
+ try {
2284
+ const registration = prepared?.registration ?? this.registration(options.provider);
2285
+ const adapter = registration.adapter;
2286
+ let modelInfo;
2287
+ let resolvedConfig;
2288
+ let dispatch;
2289
+ if (prepared === void 0) {
2290
+ const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
2291
+ modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
2292
+ resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
2293
+ dispatch = (options) => adapterCall.stream(options);
2294
+ } else {
2295
+ modelInfo = prepared.modelInfo;
2296
+ resolvedConfig = prepared.config;
2297
+ dispatch = prepared.dispatch;
2298
+ }
2299
+ if (prepared !== void 0 && !callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
2300
+ const resolvedOptions = callConfigEquals(options, resolvedConfig) ? options : Object.isFrozen(options) ? deepFreeze({
2301
+ ...options,
2302
+ ...resolvedConfig
2303
+ }) : {
2304
+ ...options,
2305
+ ...resolvedConfig
2306
+ };
2307
+ let projectedMessages = resolvedOptions.messages;
2308
+ if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
2309
+ if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
2310
+ const projectedTools = projectToolUpdates(projectedMessages, resolvedOptions.tools, modelInfo.toolUpdate, resolvedOptions.toolHistory);
2311
+ projectedMessages = projectedTools.messages;
2312
+ let projectedOptions = resolvedOptions;
2313
+ if (projectedMessages !== resolvedOptions.messages || projectedTools.tools !== resolvedOptions.tools) {
2314
+ projectedOptions = {
2315
+ ...resolvedOptions,
2316
+ messages: projectedMessages,
2317
+ ...projectedTools.tools === void 0 ? {} : { tools: projectedTools.tools }
2318
+ };
2319
+ if (Object.isFrozen(resolvedOptions)) deepFreeze(projectedOptions);
2320
+ }
2321
+ iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
2322
+ } catch (error) {
2323
+ yield adapterFailureChunk(error, options.signal);
2324
+ return;
2325
+ }
2326
+ let completed = false;
2327
+ try {
2328
+ while (true) {
2329
+ let item;
2330
+ try {
2331
+ const next = await iterator.next();
2332
+ item = next.done ? { done: true } : {
2333
+ done: false,
2334
+ value: next.value
2335
+ };
2336
+ } catch (error) {
2337
+ completed = true;
2338
+ yield adapterFailureChunk(error, options.signal);
2339
+ return;
2340
+ }
2341
+ if (item.done) {
2342
+ completed = true;
2343
+ return;
2344
+ }
2345
+ yield item.value;
2346
+ }
2347
+ } finally {
2348
+ if (!completed) {
2349
+ const close = iterator.return?.bind(iterator);
2350
+ if (close) await close();
2351
+ }
2352
+ }
2353
+ }
2354
+ /**
2355
+ * Stream one model call as raw chunks (token-level deltas). Replay state is
2356
+ * retained only when the same adapter instance owns its historical provider
2357
+ * and the target provider. Final adapter selection remains fixed through
2358
+ * asynchronous exact-model resolution and dispatch. Adapter selection,
2359
+ * dispatch, and iteration failures become terminal `error` or `aborted`
2360
+ * finish chunks; middleware, nested-call, cleanup, and consumer failures
2361
+ * remain thrown.
2362
+ * @param options - the full request; `options.provider` selects the adapter.
2363
+ * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
2364
+ */
2365
+ stream(options) {
2366
+ return this.streamWithRegistration(options);
2367
+ }
2368
+ streamWithRegistration(options, prepared) {
2369
+ return this.ctx.waterfall(this, "llm/stream", options, () => this.adapterStream(options, prepared));
2370
+ }
2371
+ };
2372
+ })();
2373
+ /** Convert one adapter throw into the stream protocol's terminal outcome. */
2374
+ function adapterFailureChunk(error, signal) {
2375
+ const failure = normalizeLlmFailure(error);
2376
+ return {
2377
+ type: "finish",
2378
+ reason: signal?.aborted || failure.code === "ABORTED" ? {
2379
+ kind: "aborted",
2380
+ failure
2381
+ } : {
2382
+ kind: "error",
2383
+ failure
2384
+ }
2385
+ };
2386
+ }
2387
+ //#endregion
2388
+ export { APP_IDENTITY, AssistantStreamAccumulator, BlockAssembler, CONTEXT_SUMMARY_MAX_CHARS, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, HarnessError, IMAGE_OFFLOAD_REQUIRED_CODE, INVALID_CREDENTIAL_CODE, LlmAdapter, LlmAttemptId, LlmError, LlmRuntime, LlmRuntime as default, MessageId, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, RetryPolicySchema, ToolCallId, assembleAssistantStream, assertUsableApiKey, assistantStreamChunks, assistantStreamFirstTokenTime, assistantStreamHasVisibleContent, assistantStreamHasVisibleText, attributionHeaders, boundContextSummary, callConfigEquals, chunkHasVisibleText, contentHasFile, contentHasImage, createAssistantMessage, createDeveloperMessage, createMessage, createSystemMessage, createToolResultMessage, createUserMessage, errorChain, expandAssistantStream, fileHandleText, freezeMessage, isAgentLoopRequest, isContextWindowExceededError, isHarnessError, isQuotaExceededError, isTokenDelta, isVisibleChunk, joinAssistantStreamText, lastAssistantStreamChunk, markAgentLoopRequest, normalizeApiKey, offloadedImageText, projectFilesToText, projectImagesForTextModel, projectOffloadedImages, projectToolUpdates, requestImageHandleText, requiredImageOffload, resolveImageAttachmentAccess, resolveRetryPolicy, runFirstTokenTime, runFirstVisibleTime, textOnlyImageText, userAgent };