@qilin-agent/tmux-context 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/LICENSE +21 -0
- package/README.i18n.yaml +67 -0
- package/README.md +139 -2
- package/README.zh.md +140 -0
- package/lib/index.js +2254 -0
- package/lib/types/index.d.ts +65 -0
- package/package.json +50 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,2254 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import z from "@qilin-agent/schemastery";
|
|
3
|
+
import { z as z$1 } from "zod";
|
|
4
|
+
import { Context, Service } from "@qilin-agent/kylin";
|
|
5
|
+
//#region ../../typert/protocol/src/remote-error.ts
|
|
6
|
+
/**
|
|
7
|
+
* One Remote call failure: a real Error carrying its stable code and typed
|
|
8
|
+
* details. Owners throw it at the failure point; the Host Gateway encodes it
|
|
9
|
+
* onto the wire unchanged; the Client face rebuilds an instance for the
|
|
10
|
+
* `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
|
|
11
|
+
* Discrimination is always by `code`, never by instanceof.
|
|
12
|
+
*/
|
|
13
|
+
var RemoteError = class extends Error {
|
|
14
|
+
code;
|
|
15
|
+
details;
|
|
16
|
+
/** Structural marker: cross-realm/bundle identification never uses instanceof. */
|
|
17
|
+
isQILINRemoteError = true;
|
|
18
|
+
/**
|
|
19
|
+
* @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
|
|
20
|
+
* @param message - human diagnostic carried across the wire.
|
|
21
|
+
* @param details - structured payload typed by the code.
|
|
22
|
+
* @param options - standard Error options (`cause` survives in-process only).
|
|
23
|
+
*/
|
|
24
|
+
constructor(code, message, details, options) {
|
|
25
|
+
super(message, options);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.details = details;
|
|
28
|
+
this.name = "RemoteError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region ../../typert/protocol/src/index.ts
|
|
33
|
+
/**
|
|
34
|
+
* Remote decorators and explicit Gateway bindings backed by versioned
|
|
35
|
+
* descriptors carried on decorated class prototypes. Strict reflection
|
|
36
|
+
* remains a Typert compiler responsibility.
|
|
37
|
+
* @module @qilin-agent/typert-protocol
|
|
38
|
+
*/
|
|
39
|
+
const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
|
|
40
|
+
/**
|
|
41
|
+
* Test one generated Remote name against the Connection endpoint grammar.
|
|
42
|
+
* @param value - namespace, method, lookup, or Context segment.
|
|
43
|
+
* @returns whether the value can cross the shared RPC carrier unchanged.
|
|
44
|
+
*/
|
|
45
|
+
function isTypertRemoteSegment(value) {
|
|
46
|
+
return value !== "." && value !== ".." && TYPERT_REMOTE_SEGMENT_PATTERN.test(value);
|
|
47
|
+
}
|
|
48
|
+
const REMOTE_METHOD_DESCRIPTOR = "@qilin-agent/typert-protocol/remote-methods";
|
|
49
|
+
/**
|
|
50
|
+
* Bind one visible Service field to a Cordis key and Remote namespace. A
|
|
51
|
+
* service that owns a Cordis Context also gives its tree `ctx.invocation`,
|
|
52
|
+
* `undefined` outside a Remote call, so no `TypertRemoteService` is needed for
|
|
53
|
+
* a Host composition to read it.
|
|
54
|
+
* @param service - owning Service instance, normally `this`.
|
|
55
|
+
* @param serviceKey - exact Cordis service key.
|
|
56
|
+
* @param options - optional distinct wire namespace.
|
|
57
|
+
* @returns a frozen, inspectable binding with no compiler-injected metadata.
|
|
58
|
+
*/
|
|
59
|
+
function bindTypertRemote(service, serviceKey, options = {}) {
|
|
60
|
+
validateName("service key", serviceKey);
|
|
61
|
+
const namespace = options.namespace ?? serviceKey;
|
|
62
|
+
validateName("namespace", namespace);
|
|
63
|
+
const ctx = Reflect.get(service, "ctx");
|
|
64
|
+
if (ctx instanceof Context) provideInvocationAccessor(ctx);
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
service,
|
|
67
|
+
serviceKey,
|
|
68
|
+
namespace
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Cordis Service base that exposes its registered name through Typert Gateway. */
|
|
72
|
+
var TypertRemoteService = class extends Service {
|
|
73
|
+
/** Visible binding consumed by the Gateway's source-mode discovery. */
|
|
74
|
+
typertRemote;
|
|
75
|
+
/**
|
|
76
|
+
* Register the Service and bind the same key to Typert Gateway.
|
|
77
|
+
* @param ctx - owning Cordis Context.
|
|
78
|
+
* @param serviceKey - exact Cordis service key and default wire namespace.
|
|
79
|
+
* @param options - optional distinct wire namespace.
|
|
80
|
+
*/
|
|
81
|
+
constructor(ctx, serviceKey, options = {}) {
|
|
82
|
+
super(ctx, serviceKey);
|
|
83
|
+
this.typertRemote = bindTypertRemote(this, this.name, options);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Make `ctx.invocation` read as `undefined` outside a Remote call instead of the
|
|
88
|
+
* reflect service's "cannot get property" error; a call-derived Context shadows
|
|
89
|
+
* the accessor with its own property. The first Remote Service constructed in a
|
|
90
|
+
* tree registers it on the root, where it outlives any one Service.
|
|
91
|
+
*/
|
|
92
|
+
function provideInvocationAccessor(ctx) {
|
|
93
|
+
if (Object.hasOwn(ctx.root.reflect.props, "invocation")) return;
|
|
94
|
+
ctx.root.accessor("invocation", { get: () => void 0 });
|
|
95
|
+
}
|
|
96
|
+
function Remote(methodExportOrOptions, context) {
|
|
97
|
+
if (typeof methodExportOrOptions === "string") {
|
|
98
|
+
validateName("Remote export name", methodExportOrOptions);
|
|
99
|
+
return remoteDecorator({ kind: "direct" }, void 0, methodExportOrOptions);
|
|
100
|
+
}
|
|
101
|
+
if (typeof methodExportOrOptions === "object") {
|
|
102
|
+
if (remoteOptionMode(methodExportOrOptions) !== "stream" || Reflect.ownKeys(methodExportOrOptions).length !== 1) throw new TypeError("typert-protocol: Remote options must contain exactly mode: \"stream\"");
|
|
103
|
+
return remoteDecorator({ kind: "direct" }, "stream");
|
|
104
|
+
}
|
|
105
|
+
if (context === void 0) throw new TypeError("typert-protocol: Remote decorator context is missing");
|
|
106
|
+
addMarkerInitializer(context, { kind: "direct" });
|
|
107
|
+
}
|
|
108
|
+
function remoteOptionMode(options) {
|
|
109
|
+
return Reflect.get(options, "mode");
|
|
110
|
+
}
|
|
111
|
+
function remoteDecorator(invocation, mode, exportName) {
|
|
112
|
+
return function(_method, context) {
|
|
113
|
+
addMarkerInitializer(context, invocation, mode, exportName);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function readRemoteMethodDescriptor(prototype) {
|
|
117
|
+
const property = Object.getOwnPropertyDescriptor(prototype, REMOTE_METHOD_DESCRIPTOR);
|
|
118
|
+
if (property === void 0) return void 0;
|
|
119
|
+
const descriptor = property.value;
|
|
120
|
+
if (descriptor === null || typeof descriptor !== "object") throw new TypeError("typert-protocol: Remote method descriptor must be an object");
|
|
121
|
+
const version = Reflect.get(descriptor, "version");
|
|
122
|
+
if (version !== 1) throw new TypeError(`typert-protocol: unsupported Remote method descriptor version ${String(version)}`);
|
|
123
|
+
const methods = Reflect.get(descriptor, "methods");
|
|
124
|
+
if (!Array.isArray(methods)) throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
|
|
125
|
+
return descriptor;
|
|
126
|
+
}
|
|
127
|
+
function addMarkerInitializer(context, invocation, mode, exportName) {
|
|
128
|
+
if (context.private || context.static || typeof context.name !== "string") throw new TypeError("typert-protocol: Remote decorators require a public instance method with a string name");
|
|
129
|
+
const method = context.name;
|
|
130
|
+
context.addInitializer(function() {
|
|
131
|
+
const prototype = Object.getPrototypeOf(this);
|
|
132
|
+
if (prototype === null) throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`);
|
|
133
|
+
mark(prototype, method, invocation, mode, exportName);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function mark(prototype, method, invocation, mode, exportName) {
|
|
137
|
+
const descriptor = readRemoteMethodDescriptor(prototype);
|
|
138
|
+
const marker = Object.freeze({
|
|
139
|
+
method,
|
|
140
|
+
...exportName === void 0 || exportName === method ? {} : { exportName },
|
|
141
|
+
...mode === void 0 ? {} : { mode },
|
|
142
|
+
invocation: Object.freeze(invocation)
|
|
143
|
+
});
|
|
144
|
+
const current = descriptor?.methods.find((candidate) => candidate.method === method);
|
|
145
|
+
if (current !== void 0) {
|
|
146
|
+
if (current.exportName === marker.exportName && current.mode === marker.mode && sameInvocation(current.invocation, invocation)) return;
|
|
147
|
+
throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
|
|
148
|
+
}
|
|
149
|
+
Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
|
|
150
|
+
configurable: true,
|
|
151
|
+
value: Object.freeze({
|
|
152
|
+
version: 1,
|
|
153
|
+
methods: Object.freeze([...descriptor?.methods ?? [], marker])
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function sameInvocation(left, right) {
|
|
158
|
+
if (left.kind === "direct") return right.kind === "direct";
|
|
159
|
+
if (right.kind === "direct") return false;
|
|
160
|
+
return left.context === right.context;
|
|
161
|
+
}
|
|
162
|
+
function validateName(subject, value) {
|
|
163
|
+
if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region ../../util/values/src/partial-json.ts
|
|
167
|
+
/**
|
|
168
|
+
* Lazily scanned view of one JSON object's top-level fields, built from text
|
|
169
|
+
* that may still be streaming or from an already parsed object. Nothing is
|
|
170
|
+
* scanned until a reader asks; the view remembers every question it answered
|
|
171
|
+
* and reports changed answers when the owner refreshes for publication.
|
|
172
|
+
* Used for model tool-call arguments: a row reads the fields it
|
|
173
|
+
* cares about at whatever granularity it displays, at every stage of the call.
|
|
174
|
+
* @module @qilin-agent/util-values/src/partial-json
|
|
175
|
+
*/
|
|
176
|
+
const SIMPLE_ESCAPES = {
|
|
177
|
+
"\"": "\"",
|
|
178
|
+
"\\": "\\",
|
|
179
|
+
"/": "/",
|
|
180
|
+
b: "\b",
|
|
181
|
+
f: "\f",
|
|
182
|
+
n: "\n",
|
|
183
|
+
r: "\r",
|
|
184
|
+
t: " "
|
|
185
|
+
};
|
|
186
|
+
const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
|
|
187
|
+
function isWhitespace(c) {
|
|
188
|
+
return c === " " || c === "\n" || c === "\r" || c === " ";
|
|
189
|
+
}
|
|
190
|
+
function isHex(c) {
|
|
191
|
+
return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
|
|
192
|
+
}
|
|
193
|
+
(class PartialArguments {
|
|
194
|
+
/** The view of a call with no arguments available. */
|
|
195
|
+
static EMPTY = PartialArguments.fromObject({});
|
|
196
|
+
/**
|
|
197
|
+
* View finished argument text without scanning it until a reader asks.
|
|
198
|
+
* @param text - the complete argument JSON text.
|
|
199
|
+
* @returns a sealed view.
|
|
200
|
+
*/
|
|
201
|
+
static fromText(text) {
|
|
202
|
+
const view = new PartialArguments();
|
|
203
|
+
view.append(text);
|
|
204
|
+
view.sealed = true;
|
|
205
|
+
return view;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* View an already parsed argument payload, such as a PTC dispatch object.
|
|
209
|
+
* @param value - the parsed argument value.
|
|
210
|
+
* @returns a sealed view; a non-object payload has no fields.
|
|
211
|
+
*/
|
|
212
|
+
static fromObject(value) {
|
|
213
|
+
const view = new PartialArguments();
|
|
214
|
+
view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
215
|
+
view.sealed = true;
|
|
216
|
+
return view;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The source: text so far or a parsed object, plus whether it can still grow.
|
|
220
|
+
* These are the only enumerable fields, so two views over the same source
|
|
221
|
+
* compare equal structurally however far each has been read.
|
|
222
|
+
*/
|
|
223
|
+
chunks = [];
|
|
224
|
+
object;
|
|
225
|
+
sealed = false;
|
|
226
|
+
#ends = [];
|
|
227
|
+
#size = 0;
|
|
228
|
+
#consumed = 0;
|
|
229
|
+
#mode = "root";
|
|
230
|
+
#escape = false;
|
|
231
|
+
#keyStart = 0;
|
|
232
|
+
#keyEscaped = false;
|
|
233
|
+
#key = "";
|
|
234
|
+
#current = null;
|
|
235
|
+
#nestedEnds = [];
|
|
236
|
+
#nestedInString = false;
|
|
237
|
+
#invalidAt;
|
|
238
|
+
#invalidValue = false;
|
|
239
|
+
#entries = /* @__PURE__ */ new Map();
|
|
240
|
+
#order = [];
|
|
241
|
+
#reads = /* @__PURE__ */ new Map();
|
|
242
|
+
/** Whether this view rejects further appends; does not scan text or register reads. */
|
|
243
|
+
get isSealed() {
|
|
244
|
+
return this.sealed;
|
|
245
|
+
}
|
|
246
|
+
/** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
|
|
247
|
+
get invalid() {
|
|
248
|
+
this.scan();
|
|
249
|
+
return this.#mode === "invalid" || this.#invalidValue;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Retain streamed argument text without scanning or comparing observed answers.
|
|
253
|
+
* @param fragment - the text following every fragment appended before.
|
|
254
|
+
*/
|
|
255
|
+
append(fragment) {
|
|
256
|
+
if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
|
|
257
|
+
if (fragment.length === 0) return;
|
|
258
|
+
this.chunks.push(fragment);
|
|
259
|
+
this.#size += fragment.length;
|
|
260
|
+
this.#ends.push(this.#size);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Reconcile a streamed prefix with authoritative complete text without joining the fragments.
|
|
264
|
+
* @param text - the final argument text, which replaces missing or conflicting deltas.
|
|
265
|
+
* @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
|
|
266
|
+
*/
|
|
267
|
+
settle(text) {
|
|
268
|
+
if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
|
|
269
|
+
let offset = 0;
|
|
270
|
+
for (const chunk of this.chunks) {
|
|
271
|
+
if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
|
|
272
|
+
offset += chunk.length;
|
|
273
|
+
}
|
|
274
|
+
this.chunks = text.length === 0 ? [] : [text];
|
|
275
|
+
this.#ends = text.length === 0 ? [] : [text.length];
|
|
276
|
+
this.sealed = true;
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Compare observed answers and advance their publication baseline. Unread views remain unscanned.
|
|
281
|
+
* @returns whether any observed answer changed since its first read or the preceding refresh.
|
|
282
|
+
*/
|
|
283
|
+
refresh() {
|
|
284
|
+
if (this.#reads.size === 0) return false;
|
|
285
|
+
this.scan();
|
|
286
|
+
let changed = false;
|
|
287
|
+
let completions = false;
|
|
288
|
+
for (const read of this.#reads.values()) {
|
|
289
|
+
if (read.completion) {
|
|
290
|
+
completions = true;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
changed = this.refreshRead(read) || changed;
|
|
294
|
+
}
|
|
295
|
+
if (completions) {
|
|
296
|
+
for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
|
|
297
|
+
}
|
|
298
|
+
if (this.sealed) this.#reads.clear();
|
|
299
|
+
return changed;
|
|
300
|
+
}
|
|
301
|
+
refreshRead(read) {
|
|
302
|
+
const now = read.answer();
|
|
303
|
+
if (Object.is(now, read.last)) return false;
|
|
304
|
+
read.last = now;
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Check whether no further fields can arrive.
|
|
309
|
+
* @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
|
|
310
|
+
*/
|
|
311
|
+
closed() {
|
|
312
|
+
return this.remember("closed", "", () => this.closedNow());
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* List discovered fields in first-appearance order.
|
|
316
|
+
* @returns top-level keys seen so far, in first-appearance order.
|
|
317
|
+
*/
|
|
318
|
+
keys() {
|
|
319
|
+
return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Check whether a top-level field has appeared.
|
|
323
|
+
* @param key - argument name.
|
|
324
|
+
* @returns whether the field has appeared (a string opened or another value began).
|
|
325
|
+
*/
|
|
326
|
+
has(key) {
|
|
327
|
+
return this.remember("has", key, () => this.hasNow(key));
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Check whether a field's closing delimiter has arrived, without validating its contents.
|
|
331
|
+
* @param key - argument name.
|
|
332
|
+
* @returns whether its delimiter arrived and no content reader has reported an error for this value.
|
|
333
|
+
*/
|
|
334
|
+
complete(key) {
|
|
335
|
+
return this.remember("complete", key, () => this.completeNow(key));
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Read string length without materializing its text.
|
|
339
|
+
* @param key - argument name.
|
|
340
|
+
* @param options - change granularity for a streaming string.
|
|
341
|
+
* @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
|
|
342
|
+
*/
|
|
343
|
+
stringLength(key, options) {
|
|
344
|
+
const step = Math.max(1, Math.floor(options?.step ?? 1));
|
|
345
|
+
const offset = options?.offset ?? 0;
|
|
346
|
+
return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Check a string against a decoded UTF-16 length limit without materializing it.
|
|
350
|
+
* @param key - argument name.
|
|
351
|
+
* @param maxLength - decoded UTF-16 limit, floored to at least zero.
|
|
352
|
+
* @returns whether the string is longer than the limit; false when absent or not a string.
|
|
353
|
+
*/
|
|
354
|
+
stringExceeds(key, maxLength) {
|
|
355
|
+
const limit = Math.max(0, Math.floor(maxLength));
|
|
356
|
+
return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Read a decoded string, including a streaming prefix.
|
|
360
|
+
* @param key - argument name.
|
|
361
|
+
* @returns the string field's decoded text so far; undefined when absent or not a string.
|
|
362
|
+
*/
|
|
363
|
+
text(key) {
|
|
364
|
+
return this.remember("text", key, () => this.textNow(key));
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Read at most the first decoded UTF-16 units of a string.
|
|
368
|
+
* @param key - argument name.
|
|
369
|
+
* @param maxLength - maximum decoded UTF-16 length, floored to at least one.
|
|
370
|
+
* @returns the bounded string prefix; undefined when absent or not a string.
|
|
371
|
+
*/
|
|
372
|
+
textPrefix(key, maxLength) {
|
|
373
|
+
const limit = Math.max(1, Math.floor(maxLength));
|
|
374
|
+
return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Read a completed non-string argument.
|
|
378
|
+
* @param key - argument name.
|
|
379
|
+
* @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
|
|
380
|
+
*/
|
|
381
|
+
value(key) {
|
|
382
|
+
return this.remember("value", key, () => this.valueNow(key));
|
|
383
|
+
}
|
|
384
|
+
/** Answer a question and, on a streaming view, remember it for change detection. */
|
|
385
|
+
remember(kind, key, read, comparison) {
|
|
386
|
+
this.scan();
|
|
387
|
+
const result = read();
|
|
388
|
+
if (!this.sealed) {
|
|
389
|
+
const id = `${kind}/${key}`;
|
|
390
|
+
if (!this.#reads.has(id)) this.#reads.set(id, {
|
|
391
|
+
completion: kind === "complete",
|
|
392
|
+
answer: comparison === void 0 ? read : () => comparison(read()),
|
|
393
|
+
last: comparison === void 0 ? result : comparison(result)
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
closedNow() {
|
|
399
|
+
return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
|
|
400
|
+
}
|
|
401
|
+
keysNow() {
|
|
402
|
+
return this.object === void 0 ? this.#order : Object.keys(this.object);
|
|
403
|
+
}
|
|
404
|
+
hasNow(key) {
|
|
405
|
+
return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
|
|
406
|
+
}
|
|
407
|
+
completeNow(key) {
|
|
408
|
+
if (this.object !== void 0) return Object.hasOwn(this.object, key);
|
|
409
|
+
const entry = this.#entries.get(key);
|
|
410
|
+
return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
|
|
411
|
+
}
|
|
412
|
+
lengthNow(key, limit = Number.POSITIVE_INFINITY) {
|
|
413
|
+
if (this.object !== void 0) {
|
|
414
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
415
|
+
return typeof field === "string" ? field.length : void 0;
|
|
416
|
+
}
|
|
417
|
+
const entry = this.#entries.get(key);
|
|
418
|
+
if (entry?.kind !== "string") return void 0;
|
|
419
|
+
if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
|
|
420
|
+
const read = entry.length ??= {
|
|
421
|
+
at: entry.start,
|
|
422
|
+
length: 0,
|
|
423
|
+
text: ""
|
|
424
|
+
};
|
|
425
|
+
this.readString(entry, read, limit, false);
|
|
426
|
+
return read.length;
|
|
427
|
+
}
|
|
428
|
+
textNow(key) {
|
|
429
|
+
if (this.object !== void 0) {
|
|
430
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
431
|
+
return typeof field === "string" ? field : void 0;
|
|
432
|
+
}
|
|
433
|
+
const entry = this.#entries.get(key);
|
|
434
|
+
if (entry?.kind !== "string") return void 0;
|
|
435
|
+
if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
|
|
436
|
+
let text;
|
|
437
|
+
try {
|
|
438
|
+
text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
|
|
439
|
+
} catch (_error) {}
|
|
440
|
+
if (text !== void 0) entry.text = {
|
|
441
|
+
at: entry.end,
|
|
442
|
+
length: text.length,
|
|
443
|
+
text
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const read = entry.text ??= {
|
|
447
|
+
at: entry.start,
|
|
448
|
+
length: 0,
|
|
449
|
+
text: ""
|
|
450
|
+
};
|
|
451
|
+
this.readString(entry, read, Number.POSITIVE_INFINITY, true);
|
|
452
|
+
return read.text;
|
|
453
|
+
}
|
|
454
|
+
textPrefixNow(key, maxLength) {
|
|
455
|
+
if (this.object !== void 0) {
|
|
456
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
457
|
+
return typeof field === "string" ? field.slice(0, maxLength) : void 0;
|
|
458
|
+
}
|
|
459
|
+
const entry = this.#entries.get(key);
|
|
460
|
+
if (entry?.kind !== "string") return void 0;
|
|
461
|
+
const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
|
|
462
|
+
let read = prefixes.get(maxLength);
|
|
463
|
+
if (read === void 0) {
|
|
464
|
+
read = {
|
|
465
|
+
at: entry.start,
|
|
466
|
+
length: 0,
|
|
467
|
+
text: ""
|
|
468
|
+
};
|
|
469
|
+
prefixes.set(maxLength, read);
|
|
470
|
+
}
|
|
471
|
+
this.readString(entry, read, maxLength, true);
|
|
472
|
+
return read.text;
|
|
473
|
+
}
|
|
474
|
+
valueNow(key) {
|
|
475
|
+
if (this.object !== void 0) {
|
|
476
|
+
if (!Object.hasOwn(this.object, key)) return void 0;
|
|
477
|
+
const field = this.object[key];
|
|
478
|
+
return typeof field === "string" ? void 0 : field;
|
|
479
|
+
}
|
|
480
|
+
const entry = this.#entries.get(key);
|
|
481
|
+
if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
|
|
482
|
+
if (entry.parsed === void 0) try {
|
|
483
|
+
entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
|
|
484
|
+
} catch (_error) {
|
|
485
|
+
entry.invalid = true;
|
|
486
|
+
this.#invalidValue = true;
|
|
487
|
+
}
|
|
488
|
+
return entry.parsed;
|
|
489
|
+
}
|
|
490
|
+
chunkAt(at) {
|
|
491
|
+
let low = 0;
|
|
492
|
+
let high = this.#ends.length;
|
|
493
|
+
while (low < high) {
|
|
494
|
+
const mid = low + high >>> 1;
|
|
495
|
+
if (this.#ends[mid] <= at) low = mid + 1;
|
|
496
|
+
else high = mid;
|
|
497
|
+
}
|
|
498
|
+
return low;
|
|
499
|
+
}
|
|
500
|
+
/** Materialize only a requested range, never the cumulative source. */
|
|
501
|
+
slice(start, end) {
|
|
502
|
+
if (start >= end) return "";
|
|
503
|
+
const first = this.chunkAt(start);
|
|
504
|
+
const last = this.chunkAt(end - 1);
|
|
505
|
+
const base = first === 0 ? 0 : this.#ends[first - 1];
|
|
506
|
+
if (first === last) return this.chunks[first].slice(start - base, end - base);
|
|
507
|
+
const parts = [this.chunks[first].slice(start - base)];
|
|
508
|
+
for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
|
|
509
|
+
parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
|
|
510
|
+
return parts.join("");
|
|
511
|
+
}
|
|
512
|
+
readString(entry, read, limit, materialize) {
|
|
513
|
+
const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
|
|
514
|
+
if (!entry.needsDecoding) {
|
|
515
|
+
const length = Math.min(end - read.at, limit - read.length);
|
|
516
|
+
if (length <= 0) return;
|
|
517
|
+
if (materialize) read.text += this.slice(read.at, read.at + length);
|
|
518
|
+
read.at += length;
|
|
519
|
+
read.length += length;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
let chunkIndex = this.chunkAt(read.at);
|
|
523
|
+
while (read.at < end && read.length < limit) {
|
|
524
|
+
const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
|
|
525
|
+
const chunk = this.chunks[chunkIndex];
|
|
526
|
+
const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
|
|
527
|
+
const boundary = remaining.search(CONTENT_ESCAPE);
|
|
528
|
+
const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
|
|
529
|
+
if (length > 0) {
|
|
530
|
+
if (materialize) read.text += remaining.slice(0, length);
|
|
531
|
+
read.at += length;
|
|
532
|
+
read.length += length;
|
|
533
|
+
if (read.at === base + chunk.length) chunkIndex++;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
|
|
537
|
+
let decoded;
|
|
538
|
+
let width = 2;
|
|
539
|
+
if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
|
|
540
|
+
if (remaining[0] === "\\" && type === "u") {
|
|
541
|
+
const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
|
|
542
|
+
let valid = true;
|
|
543
|
+
for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
|
|
544
|
+
if (valid) {
|
|
545
|
+
if (hex.length < 4 && entry.end < 0) return;
|
|
546
|
+
if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
|
|
547
|
+
}
|
|
548
|
+
width = 6;
|
|
549
|
+
} else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
|
|
550
|
+
if (decoded === void 0) {
|
|
551
|
+
entry.invalidAt = read.at;
|
|
552
|
+
this.#invalidValue = true;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (materialize) read.text += decoded;
|
|
556
|
+
read.length++;
|
|
557
|
+
read.at += width;
|
|
558
|
+
while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/** Locate new field ranges without decoding or parsing their contents. */
|
|
562
|
+
scan() {
|
|
563
|
+
if (this.object !== void 0 || this.#consumed === this.#size) return;
|
|
564
|
+
for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
|
|
565
|
+
const pending = this.chunks[i];
|
|
566
|
+
const base = i === 0 ? 0 : this.#ends[i - 1];
|
|
567
|
+
for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
|
|
568
|
+
if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
|
|
569
|
+
const end = this.stringBoundary(pending, index);
|
|
570
|
+
this.#consumed += end - index;
|
|
571
|
+
index = end;
|
|
572
|
+
if (index === pending.length) break;
|
|
573
|
+
}
|
|
574
|
+
this.step(pending[index], this.#consumed);
|
|
575
|
+
this.#consumed++;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/** Only raw quotes and their preceding backslash runs can terminate a string. */
|
|
580
|
+
stringBoundary(fragment, start) {
|
|
581
|
+
let at = start;
|
|
582
|
+
while (true) {
|
|
583
|
+
const quote = fragment.indexOf("\"", at);
|
|
584
|
+
const end = quote < 0 ? fragment.length : quote;
|
|
585
|
+
if (this.#mode === "string") {
|
|
586
|
+
const entry = this.#current;
|
|
587
|
+
if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
|
|
588
|
+
}
|
|
589
|
+
let slashStart = end;
|
|
590
|
+
while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
|
|
591
|
+
const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
|
|
592
|
+
this.#escape = quote < 0 && escaped;
|
|
593
|
+
if (quote < 0 || !escaped) return end;
|
|
594
|
+
at = quote + 1;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
step(c, at) {
|
|
598
|
+
switch (this.#mode) {
|
|
599
|
+
case "root":
|
|
600
|
+
if (isWhitespace(c)) return;
|
|
601
|
+
if (c === "{") {
|
|
602
|
+
this.#mode = "key-or-end";
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
this.fail();
|
|
606
|
+
return;
|
|
607
|
+
case "key-or-end":
|
|
608
|
+
if (isWhitespace(c)) return;
|
|
609
|
+
if (c === "}") {
|
|
610
|
+
this.#mode = "closed";
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (c === "\"") {
|
|
614
|
+
this.beginKey(at);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
this.fail();
|
|
618
|
+
return;
|
|
619
|
+
case "key-only":
|
|
620
|
+
if (isWhitespace(c)) return;
|
|
621
|
+
if (c === "\"") {
|
|
622
|
+
this.beginKey(at);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
this.fail();
|
|
626
|
+
return;
|
|
627
|
+
case "key":
|
|
628
|
+
this.stepKey(c, at);
|
|
629
|
+
return;
|
|
630
|
+
case "colon":
|
|
631
|
+
if (isWhitespace(c)) return;
|
|
632
|
+
if (c === ":") {
|
|
633
|
+
this.#mode = "value";
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
this.fail();
|
|
637
|
+
return;
|
|
638
|
+
case "value":
|
|
639
|
+
this.beginValue(c, at);
|
|
640
|
+
return;
|
|
641
|
+
case "string": {
|
|
642
|
+
const entry = this.#current;
|
|
643
|
+
entry.end = at;
|
|
644
|
+
this.#current = null;
|
|
645
|
+
this.#mode = "comma-or-end";
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
case "scalar":
|
|
649
|
+
this.stepScalar(c, at);
|
|
650
|
+
return;
|
|
651
|
+
case "nested":
|
|
652
|
+
this.stepNested(c, at);
|
|
653
|
+
return;
|
|
654
|
+
case "comma-or-end":
|
|
655
|
+
if (isWhitespace(c)) return;
|
|
656
|
+
if (c === ",") {
|
|
657
|
+
this.#mode = "key-only";
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (c === "}") {
|
|
661
|
+
this.#mode = "closed";
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
this.fail();
|
|
665
|
+
return;
|
|
666
|
+
case "closed":
|
|
667
|
+
if (isWhitespace(c)) return;
|
|
668
|
+
this.fail();
|
|
669
|
+
return;
|
|
670
|
+
/* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
|
|
671
|
+
case "invalid": return;
|
|
672
|
+
/* v8 ignore next 2 -- Every scanner mode has a handler above. */
|
|
673
|
+
default: assertNever(this.#mode);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
fail() {
|
|
677
|
+
this.#invalidAt = this.#consumed;
|
|
678
|
+
this.#mode = "invalid";
|
|
679
|
+
this.#current = null;
|
|
680
|
+
}
|
|
681
|
+
beginKey(at) {
|
|
682
|
+
this.#mode = "key";
|
|
683
|
+
this.#keyStart = at + 1;
|
|
684
|
+
this.#keyEscaped = false;
|
|
685
|
+
this.#escape = false;
|
|
686
|
+
}
|
|
687
|
+
stepKey(c, at) {
|
|
688
|
+
if (c < " ") {
|
|
689
|
+
this.fail();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (this.#escape) {
|
|
693
|
+
this.#escape = false;
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (c === "\\") {
|
|
697
|
+
this.#escape = true;
|
|
698
|
+
this.#keyEscaped = true;
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (c !== "\"") return;
|
|
702
|
+
const raw = this.slice(this.#keyStart, at);
|
|
703
|
+
if (this.#keyEscaped) try {
|
|
704
|
+
this.#key = JSON.parse(`"${raw}"`);
|
|
705
|
+
} catch (_error) {
|
|
706
|
+
this.fail();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
else this.#key = raw;
|
|
710
|
+
this.#mode = "colon";
|
|
711
|
+
}
|
|
712
|
+
open(entry) {
|
|
713
|
+
if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
|
|
714
|
+
this.#entries.set(this.#key, entry);
|
|
715
|
+
this.#current = entry;
|
|
716
|
+
}
|
|
717
|
+
beginValue(c, at) {
|
|
718
|
+
if (isWhitespace(c)) return;
|
|
719
|
+
if (c === "\"") {
|
|
720
|
+
this.open({
|
|
721
|
+
kind: "string",
|
|
722
|
+
start: at + 1,
|
|
723
|
+
end: -1,
|
|
724
|
+
needsDecoding: false,
|
|
725
|
+
invalidAt: void 0,
|
|
726
|
+
length: void 0,
|
|
727
|
+
text: void 0,
|
|
728
|
+
prefixes: void 0
|
|
729
|
+
});
|
|
730
|
+
this.#escape = false;
|
|
731
|
+
this.#mode = "string";
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (c === "}" || c === "," || c === ":" || c === "]") {
|
|
735
|
+
this.fail();
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
this.open({
|
|
739
|
+
kind: "value",
|
|
740
|
+
start: at,
|
|
741
|
+
end: -1,
|
|
742
|
+
parsed: void 0,
|
|
743
|
+
invalid: false
|
|
744
|
+
});
|
|
745
|
+
if (c === "{" || c === "[") {
|
|
746
|
+
this.#mode = "nested";
|
|
747
|
+
this.#nestedEnds = [c === "{" ? "}" : "]"];
|
|
748
|
+
this.#nestedInString = false;
|
|
749
|
+
this.#escape = false;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
this.#mode = "scalar";
|
|
753
|
+
}
|
|
754
|
+
stepScalar(c, at) {
|
|
755
|
+
if (c !== "," && c !== "}" && !isWhitespace(c)) return;
|
|
756
|
+
this.closeValue(at);
|
|
757
|
+
this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
|
|
758
|
+
}
|
|
759
|
+
stepNested(c, at) {
|
|
760
|
+
if (this.#nestedInString) {
|
|
761
|
+
this.#nestedInString = false;
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (c === "\"") {
|
|
765
|
+
this.#nestedInString = true;
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (c === "{" || c === "[") {
|
|
769
|
+
this.#nestedEnds.push(c === "{" ? "}" : "]");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (c === "}" || c === "]") {
|
|
773
|
+
if (this.#nestedEnds.pop() !== c) {
|
|
774
|
+
this.fail();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (this.#nestedEnds.length === 0) {
|
|
778
|
+
this.closeValue(at + 1);
|
|
779
|
+
this.#mode = "comma-or-end";
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
closeValue(end) {
|
|
784
|
+
const entry = this.#current;
|
|
785
|
+
entry.end = end;
|
|
786
|
+
this.#current = null;
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
//#endregion
|
|
790
|
+
//#region ../../util/values/src/index.ts
|
|
791
|
+
/**
|
|
792
|
+
* Mark an unreachable closed-union branch.
|
|
793
|
+
* @param value - impossible value; an unhandled typed variant fails at the call site.
|
|
794
|
+
* @param context - optional switch-site label included in the failure message.
|
|
795
|
+
* @returns never; a runtime value that escaped its type always throws.
|
|
796
|
+
*/
|
|
797
|
+
function assertNever(value, context) {
|
|
798
|
+
const rendered = JSON.stringify(value) ?? String(value);
|
|
799
|
+
throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
|
|
803
|
+
* @param value - value to freeze.
|
|
804
|
+
* @returns the same value after every reachable enumerable child is frozen.
|
|
805
|
+
*/
|
|
806
|
+
function deepFreeze(value) {
|
|
807
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
808
|
+
const pending = [{
|
|
809
|
+
kind: "visit",
|
|
810
|
+
node: value
|
|
811
|
+
}];
|
|
812
|
+
while (pending.length > 0) {
|
|
813
|
+
const task = pending.pop();
|
|
814
|
+
/* v8 ignore next -- the loop condition guarantees one pending task. */
|
|
815
|
+
if (task === void 0) continue;
|
|
816
|
+
if (task.kind === "property") {
|
|
817
|
+
pending.push({
|
|
818
|
+
kind: "visit",
|
|
819
|
+
node: task.source[task.key]
|
|
820
|
+
});
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const node = task.node;
|
|
824
|
+
if (node === null || typeof node !== "object") continue;
|
|
825
|
+
if (node instanceof AbortSignal) continue;
|
|
826
|
+
if (seen.has(node)) continue;
|
|
827
|
+
seen.add(node);
|
|
828
|
+
Object.freeze(node);
|
|
829
|
+
const keys = Object.keys(node);
|
|
830
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
831
|
+
const key = keys[index];
|
|
832
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
833
|
+
if (key === void 0) continue;
|
|
834
|
+
pending.push({
|
|
835
|
+
kind: "property",
|
|
836
|
+
source: node,
|
|
837
|
+
key
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return value;
|
|
842
|
+
}
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region ../../util/crypto/src/index.ts
|
|
845
|
+
/**
|
|
846
|
+
* Random v4 UUID, minted from `crypto.getRandomValues`.
|
|
847
|
+
* @returns the UUID string.
|
|
848
|
+
*/
|
|
849
|
+
function randomUUID() {
|
|
850
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
851
|
+
const hex = Array.from(bytes, (byte, index) => {
|
|
852
|
+
return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
|
|
853
|
+
}).join("");
|
|
854
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region ../../util/brand/src/index.ts
|
|
858
|
+
/**
|
|
859
|
+
* Apply a compile-time string brand without changing the value.
|
|
860
|
+
* @param value - string admitted by the domain that owns the target brand.
|
|
861
|
+
* @returns the same string with the requested compile-time brand.
|
|
862
|
+
*/
|
|
863
|
+
function brandString(value) {
|
|
864
|
+
return value;
|
|
865
|
+
}
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region ../../llm/llm/src/message.ts
|
|
868
|
+
/** Message value types, identity, and immutable construction helpers. */
|
|
869
|
+
/**
|
|
870
|
+
* Detach and deep-freeze a message whose identity already exists.
|
|
871
|
+
* @param message - complete message, including its stable identity.
|
|
872
|
+
* @returns an immutable snapshot that preserves the identity.
|
|
873
|
+
*/
|
|
874
|
+
function freezeMessage(message) {
|
|
875
|
+
return deepFreeze(structuredClone(message));
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Create one identified message and freeze it before publication.
|
|
879
|
+
* @param input - complete role, content, and source for a new message.
|
|
880
|
+
* @returns an immutable message with a fresh stable identity.
|
|
881
|
+
*/
|
|
882
|
+
function createMessage(input) {
|
|
883
|
+
return deepFreeze(structuredClone({
|
|
884
|
+
...input,
|
|
885
|
+
id: brandString(randomUUID())
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Create one identified user-role message and freeze it before publication.
|
|
890
|
+
* @param input - complete content and source for a new user message.
|
|
891
|
+
* @returns an immutable user message with a fresh stable identity.
|
|
892
|
+
*/
|
|
893
|
+
function createUserMessage(input) {
|
|
894
|
+
return createMessage({
|
|
895
|
+
...input,
|
|
896
|
+
role: "user"
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region ../../util/timeout/src/index.ts
|
|
901
|
+
/** Largest delay Node schedules without clamping it to one millisecond. */
|
|
902
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
903
|
+
//#endregion
|
|
904
|
+
//#region ../../llm/llm/src/error.ts
|
|
905
|
+
/**
|
|
906
|
+
* Harness error base with a stable machine-routable code and chained cause.
|
|
907
|
+
* Package errors extend it so tool results and replay can retain failure class.
|
|
908
|
+
* @module @qilin-agent/llm/error
|
|
909
|
+
*/
|
|
910
|
+
/**
|
|
911
|
+
* Base class for all harness errors. Carries a `code` (stable, programmatic —
|
|
912
|
+
* e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
|
|
913
|
+
* human-readable `message`, and supports `cause` chaining via the standard
|
|
914
|
+
* `ErrorOptions`. `name` defaults to the subclass constructor name.
|
|
915
|
+
*/
|
|
916
|
+
var HarnessError = class extends Error {
|
|
917
|
+
/** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
|
|
918
|
+
code;
|
|
919
|
+
constructor(message, code, options) {
|
|
920
|
+
super(message, options);
|
|
921
|
+
this.code = code;
|
|
922
|
+
this.name = new.target.name;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
/**
|
|
926
|
+
* Canonical provider-neutral code for a response that completed normally but
|
|
927
|
+
* carried no content blocks at all. Providers occasionally emit a degenerate
|
|
928
|
+
* completion (a terminal stop with zero output); adapters classify it as this
|
|
929
|
+
* failure instead of yielding an empty assistant message, because an empty
|
|
930
|
+
* message silently ends the turn with nothing for the user or the loop to act
|
|
931
|
+
* on. The attempt produced nothing durable, so retry policy treats it as safe
|
|
932
|
+
* to repeat.
|
|
933
|
+
*/
|
|
934
|
+
const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
|
|
935
|
+
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");
|
|
936
|
+
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");
|
|
937
|
+
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");
|
|
938
|
+
//#endregion
|
|
939
|
+
//#region ../../llm/llm/src/retry-policy.ts
|
|
940
|
+
/**
|
|
941
|
+
* Provider-owned request-retry policy configuration and resolution.
|
|
942
|
+
*
|
|
943
|
+
* Adapters expose one resolved policy per registered provider route; the
|
|
944
|
+
* optional qilin-llm-retry plugin executes it on the agent's failed-step extension point.
|
|
945
|
+
*
|
|
946
|
+
* @module @qilin-agent/llm/retry-policy
|
|
947
|
+
*/
|
|
948
|
+
const DEFAULT_MAX_RETRIES = 5;
|
|
949
|
+
const DEFAULT_INITIAL_DELAY_MS = 500;
|
|
950
|
+
const DEFAULT_MAX_DELAY_MS = 1e4;
|
|
951
|
+
const DEFAULT_JITTER_RATIO = .1;
|
|
952
|
+
const DEFAULT_RETRYABLE_CODES = Object.freeze([
|
|
953
|
+
EMPTY_RESPONSE_CODE,
|
|
954
|
+
"RATE_LIMIT",
|
|
955
|
+
"SERVER",
|
|
956
|
+
"TIMEOUT",
|
|
957
|
+
"TRANSPORT"
|
|
958
|
+
]);
|
|
959
|
+
const backoffSchema = z.object({
|
|
960
|
+
initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
|
|
961
|
+
maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
|
|
962
|
+
jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
|
|
963
|
+
});
|
|
964
|
+
const normalPolicySchema = z.object({
|
|
965
|
+
mode: z.const("normal").required(),
|
|
966
|
+
maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
|
|
967
|
+
retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
|
|
968
|
+
backoff: backoffSchema
|
|
969
|
+
});
|
|
970
|
+
const alwaysPolicySchema = z.object({
|
|
971
|
+
mode: z.const("always").required(),
|
|
972
|
+
backoff: backoffSchema
|
|
973
|
+
});
|
|
974
|
+
z.union([normalPolicySchema, alwaysPolicySchema]);
|
|
975
|
+
const NORMAL_POLICY_KEYS = new Set([
|
|
976
|
+
"mode",
|
|
977
|
+
"maxRetries",
|
|
978
|
+
"retryableCodes",
|
|
979
|
+
"backoff"
|
|
980
|
+
]);
|
|
981
|
+
const ALWAYS_POLICY_KEYS = new Set([
|
|
982
|
+
"mode",
|
|
983
|
+
"maxRetries",
|
|
984
|
+
"retryableCodes",
|
|
985
|
+
"backoff"
|
|
986
|
+
]);
|
|
987
|
+
const BACKOFF_KEYS = new Set([
|
|
988
|
+
"initialDelayMs",
|
|
989
|
+
"maxDelayMs",
|
|
990
|
+
"jitterRatio"
|
|
991
|
+
]);
|
|
992
|
+
function validateKeys(value, allowed, path) {
|
|
993
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${path}: unknown key "${key}"`);
|
|
994
|
+
}
|
|
995
|
+
function resolveBackoff(config, path) {
|
|
996
|
+
if (config !== void 0) validateKeys(config, BACKOFF_KEYS, path);
|
|
997
|
+
const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
|
|
998
|
+
const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
999
|
+
const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
|
|
1000
|
+
if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > 2147483647) throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
1001
|
+
if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > 2147483647) throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
1002
|
+
if (initialDelayMs > maxDelayMs) throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
|
|
1003
|
+
if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) throw new Error(`${path}.jitterRatio must be between 0 and 1`);
|
|
1004
|
+
return Object.freeze({
|
|
1005
|
+
initialDelayMs,
|
|
1006
|
+
maxDelayMs,
|
|
1007
|
+
jitterRatio
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Validate, default, and detach one provider-owned retry policy.
|
|
1012
|
+
* @param config - optional provider configuration; omission selects normal defaults.
|
|
1013
|
+
* @param path - diagnostic path naming the provider config that owns the value.
|
|
1014
|
+
* @returns an immutable policy safe to capture in provider registration state.
|
|
1015
|
+
*/
|
|
1016
|
+
function resolveRetryPolicy(config, path) {
|
|
1017
|
+
if (config === void 0) return Object.freeze({
|
|
1018
|
+
mode: "normal",
|
|
1019
|
+
maxRetries: DEFAULT_MAX_RETRIES,
|
|
1020
|
+
retryableCodes: DEFAULT_RETRYABLE_CODES,
|
|
1021
|
+
...resolveBackoff(void 0, `${path}.backoff`)
|
|
1022
|
+
});
|
|
1023
|
+
switch (config.mode) {
|
|
1024
|
+
case "normal": {
|
|
1025
|
+
validateKeys(config, NORMAL_POLICY_KEYS, path);
|
|
1026
|
+
const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1027
|
+
const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
|
|
1028
|
+
if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
|
|
1029
|
+
if (retryableCodes.length === 0) throw new Error(`${path}.retryableCodes must not be empty`);
|
|
1030
|
+
if (retryableCodes.some((code) => typeof code !== "string" || code.length === 0)) throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
|
|
1031
|
+
if (new Set(retryableCodes).size !== retryableCodes.length) throw new Error(`${path}.retryableCodes must not contain duplicates`);
|
|
1032
|
+
return Object.freeze({
|
|
1033
|
+
mode: "normal",
|
|
1034
|
+
maxRetries,
|
|
1035
|
+
retryableCodes: Object.freeze([...retryableCodes]),
|
|
1036
|
+
...resolveBackoff(config.backoff, `${path}.backoff`)
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
case "always":
|
|
1040
|
+
validateKeys(config, ALWAYS_POLICY_KEYS, path);
|
|
1041
|
+
return Object.freeze({
|
|
1042
|
+
mode: "always",
|
|
1043
|
+
...resolveBackoff(config.backoff, `${path}.backoff`)
|
|
1044
|
+
});
|
|
1045
|
+
default: throw new Error(`${path}.mode must be "normal" or "always"`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region ../../llm/llm/src/call-config.ts
|
|
1050
|
+
/**
|
|
1051
|
+
* Field-wise equality over {@link LlmCallConfig} — the comparison a caller
|
|
1052
|
+
* runs to decide whether a proposed configuration is a real change (worth a
|
|
1053
|
+
* logged header snapshot) or the held one restated.
|
|
1054
|
+
* @param a - one configuration.
|
|
1055
|
+
* @param b - the other.
|
|
1056
|
+
* @returns whether every field (including the `stop` list, element-wise) matches.
|
|
1057
|
+
*/
|
|
1058
|
+
function callConfigEquals(a, b) {
|
|
1059
|
+
if (a.provider !== b.provider || a.model !== b.model || a.reasoningEffort !== b.reasoningEffort || a.temperature !== b.temperature || a.maxTokens !== b.maxTokens) return false;
|
|
1060
|
+
if (a.stop === void 0 || b.stop === void 0) return a.stop === b.stop;
|
|
1061
|
+
return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
|
|
1062
|
+
}
|
|
1063
|
+
//#endregion
|
|
1064
|
+
//#region ../../llm/llm/src/adapter-failure.ts
|
|
1065
|
+
/**
|
|
1066
|
+
* Normalization for values thrown by a final LLM adapter boundary.
|
|
1067
|
+
*
|
|
1068
|
+
* @module @qilin-agent/llm/adapter-failure
|
|
1069
|
+
*/
|
|
1070
|
+
/**
|
|
1071
|
+
* Detach serializable provider facts from a value thrown by an adapter.
|
|
1072
|
+
* @param value - arbitrary value thrown during adapter dispatch or iteration.
|
|
1073
|
+
* @returns immutable provider-neutral facts suitable for a terminal finish chunk.
|
|
1074
|
+
* @internal
|
|
1075
|
+
*/
|
|
1076
|
+
function normalizeLlmFailure(value) {
|
|
1077
|
+
const error = value instanceof Error ? value : new HarnessError(thrownMessage(value), "UNKNOWN", { cause: value });
|
|
1078
|
+
const carried = ownFailureSnapshot(error);
|
|
1079
|
+
if (carried !== void 0 && carried.code === ownErrorCode(error)) return carried;
|
|
1080
|
+
return Object.freeze({
|
|
1081
|
+
message: errorMessage(error),
|
|
1082
|
+
code: harnessErrorCode(error)
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
/** Render a non-Error throw without letting hostile coercion escape normalization. */
|
|
1086
|
+
function thrownMessage(value) {
|
|
1087
|
+
try {
|
|
1088
|
+
const message = String(value);
|
|
1089
|
+
return message.length > 0 ? message : "LLM adapter failed";
|
|
1090
|
+
} catch (_hostileThrownValue) {
|
|
1091
|
+
return "LLM adapter failed";
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/** Read a foreign error's own data-backed `code` without invoking accessors. */
|
|
1095
|
+
function ownErrorCode(error) {
|
|
1096
|
+
try {
|
|
1097
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, "code");
|
|
1098
|
+
return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
|
|
1099
|
+
} catch (_sdkPropertyTrap) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
/** Snapshot an own data property without invoking an SDK-defined accessor. */
|
|
1104
|
+
function ownFailureSnapshot(error) {
|
|
1105
|
+
try {
|
|
1106
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, "failure");
|
|
1107
|
+
return descriptor !== void 0 && "value" in descriptor ? failureSnapshot(descriptor.value) : void 0;
|
|
1108
|
+
} catch (_sdkPropertyTrap) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/** Validate and detach an arbitrary serializable failure payload. */
|
|
1113
|
+
function failureSnapshot(value) {
|
|
1114
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
1115
|
+
try {
|
|
1116
|
+
const candidate = value;
|
|
1117
|
+
const message = candidate.message;
|
|
1118
|
+
const code = candidate.code;
|
|
1119
|
+
const status = candidate.status;
|
|
1120
|
+
const providerRetryAfterMs = candidate.providerRetryAfterMs;
|
|
1121
|
+
const requestId = candidate.requestId;
|
|
1122
|
+
const offloadImages = candidate.offloadImages;
|
|
1123
|
+
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;
|
|
1124
|
+
return Object.freeze({
|
|
1125
|
+
message,
|
|
1126
|
+
code,
|
|
1127
|
+
...status === void 0 ? {} : { status },
|
|
1128
|
+
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
|
|
1129
|
+
...requestId === void 0 ? {} : { requestId },
|
|
1130
|
+
...offloadImages === void 0 ? {} : { offloadImages }
|
|
1131
|
+
});
|
|
1132
|
+
} catch (_sdkFailureGetter) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/** Read an SDK error message without letting an accessor replace the primary failure. */
|
|
1137
|
+
function errorMessage(error) {
|
|
1138
|
+
try {
|
|
1139
|
+
const message = error.message;
|
|
1140
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
1141
|
+
} catch (_sdkMessageGetter) {}
|
|
1142
|
+
return "LLM adapter failed";
|
|
1143
|
+
}
|
|
1144
|
+
/** Trust only Harness-owned codes; third-party SDK codes are not our taxonomy. */
|
|
1145
|
+
function harnessErrorCode(error) {
|
|
1146
|
+
return error instanceof HarnessError ? error.code : "UNKNOWN";
|
|
1147
|
+
}
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region ../../llm/llm/src/content.ts
|
|
1150
|
+
function quoted(value) {
|
|
1151
|
+
return JSON.stringify(value);
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Stable text shown to a model that cannot accept one durable image reference.
|
|
1155
|
+
* @param ref - durable normalized attachment omitted from the request.
|
|
1156
|
+
* @returns deterministic text-only placeholder.
|
|
1157
|
+
*/
|
|
1158
|
+
function textOnlyImageText(ref) {
|
|
1159
|
+
return `[image omitted because this model accepts text only; attachment sha256:${String(ref.attachmentId).slice(7, 15)}]`;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* True when typed model content contains an image block. This is the one image
|
|
1163
|
+
* walk shared by every image policy (capability gating, text-only
|
|
1164
|
+
* serialization, compaction survey), so a consumer cannot silently diverge.
|
|
1165
|
+
* @param content - typed model content blocks.
|
|
1166
|
+
* @returns whether any block is an image.
|
|
1167
|
+
*/
|
|
1168
|
+
function contentHasImage(content) {
|
|
1169
|
+
return content.some((block) => block.type === "image");
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* True when typed model content contains a file block.
|
|
1173
|
+
* Reads current content on every call without retaining scan results.
|
|
1174
|
+
* @param content - typed model content blocks.
|
|
1175
|
+
* @returns whether any block is a file.
|
|
1176
|
+
*/
|
|
1177
|
+
function contentHasFile(content) {
|
|
1178
|
+
for (const block of content) if (block.type === "file") return true;
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Stable model-facing handle for one durable file reference: the address of
|
|
1183
|
+
* the verbatim stored copy and the instruction to read it on demand. This is
|
|
1184
|
+
* the only representation a provider ever receives for a file.
|
|
1185
|
+
* @param ref - durable verbatim file reference.
|
|
1186
|
+
* @param readonlyPath - execution-world path of the stored copy, when resolvable.
|
|
1187
|
+
* @returns deterministic handle text naming the file, its size, and its address.
|
|
1188
|
+
*/
|
|
1189
|
+
function fileHandleText(ref, readonlyPath) {
|
|
1190
|
+
const digest = String(ref.attachmentId).slice(7, 15);
|
|
1191
|
+
const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
|
|
1192
|
+
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.]`;
|
|
1193
|
+
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.]`;
|
|
1194
|
+
}
|
|
1195
|
+
/** Replace every file occurrence with handle text. */
|
|
1196
|
+
function replaceFilesWithHandles(blocks, resolvePath) {
|
|
1197
|
+
let next;
|
|
1198
|
+
for (const [index, block] of blocks.entries()) {
|
|
1199
|
+
if (block.type === "file") {
|
|
1200
|
+
next ??= blocks.slice(0, index);
|
|
1201
|
+
next.push({
|
|
1202
|
+
type: "text",
|
|
1203
|
+
text: fileHandleText(block.attachment, resolvePath(block.attachment))
|
|
1204
|
+
});
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
next?.push(block);
|
|
1208
|
+
}
|
|
1209
|
+
return next ?? blocks;
|
|
1210
|
+
}
|
|
1211
|
+
function projectFilesToText(messages, resolvePath) {
|
|
1212
|
+
if (!messages.some((message) => contentHasFile(message.content))) return messages;
|
|
1213
|
+
return messages.map((message) => {
|
|
1214
|
+
const content = replaceFilesWithHandles(message.content, resolvePath);
|
|
1215
|
+
return content === message.content ? message : {
|
|
1216
|
+
...message,
|
|
1217
|
+
content
|
|
1218
|
+
};
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
/** Replace every image occurrence for a text-only model. */
|
|
1222
|
+
function replaceImagesForTextModel(blocks) {
|
|
1223
|
+
let next;
|
|
1224
|
+
for (const [index, block] of blocks.entries()) {
|
|
1225
|
+
if (block.type === "image") {
|
|
1226
|
+
next ??= blocks.slice(0, index);
|
|
1227
|
+
next.push({
|
|
1228
|
+
type: "text",
|
|
1229
|
+
text: textOnlyImageText(block.attachment)
|
|
1230
|
+
});
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
next?.push(block);
|
|
1234
|
+
}
|
|
1235
|
+
return next ?? blocks;
|
|
1236
|
+
}
|
|
1237
|
+
function projectImagesForTextModel(messages) {
|
|
1238
|
+
if (!messages.some((message) => contentHasImage(message.content))) return messages;
|
|
1239
|
+
return messages.map((message) => {
|
|
1240
|
+
const content = replaceImagesForTextModel(message.content);
|
|
1241
|
+
return content === message.content ? message : {
|
|
1242
|
+
...message,
|
|
1243
|
+
content
|
|
1244
|
+
};
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
function withoutDeveloperMessages(messages) {
|
|
1248
|
+
const retained = messages.filter((message) => message.role !== "developer");
|
|
1249
|
+
return retained.length === messages.length ? messages : retained;
|
|
1250
|
+
}
|
|
1251
|
+
function toolDeclarations(tools, mode, history) {
|
|
1252
|
+
const declarations = new Map(history.tools.map((tool) => [tool.name, tool]));
|
|
1253
|
+
for (const update of history.updates) for (const tool of update.additions) if (!declarations.has(tool.name)) declarations.set(tool.name, {
|
|
1254
|
+
...tool,
|
|
1255
|
+
deferLoading: true
|
|
1256
|
+
});
|
|
1257
|
+
switch (mode) {
|
|
1258
|
+
case "in-history": return declarations;
|
|
1259
|
+
case "addition-only": {
|
|
1260
|
+
const activeNames = new Set(tools?.map((tool) => tool.name));
|
|
1261
|
+
for (const name of declarations.keys()) if (!activeNames.has(name)) declarations.delete(name);
|
|
1262
|
+
return declarations;
|
|
1263
|
+
}
|
|
1264
|
+
/* v8 ignore next 2 -- closed-union exhaustiveness guard */
|
|
1265
|
+
default: return assertNever(mode);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Construct provider declarations from session-folded history without changing logged active tools.
|
|
1270
|
+
* Unsupported routes and incomplete history use current declarations without developer updates.
|
|
1271
|
+
* Explicitly deferred baseline tools become available only after their first retained addition.
|
|
1272
|
+
* @param messages - complete request inputs, or the prefix selected for an auxiliary call.
|
|
1273
|
+
* @param tools - currently active tool schemas.
|
|
1274
|
+
* @param toolUpdate - the resolved route's update mode.
|
|
1275
|
+
* @param history - immutable state folded from committed headers and developer messages.
|
|
1276
|
+
* @returns provider declarations and the corresponding filtered history.
|
|
1277
|
+
*/
|
|
1278
|
+
function projectToolUpdates(messages, tools, toolUpdate, history) {
|
|
1279
|
+
if (toolUpdate === void 0) {
|
|
1280
|
+
let immediateTools = tools;
|
|
1281
|
+
if (tools?.some((tool) => tool.deferLoading === true)) immediateTools = tools.map(({ deferLoading: _loading, ...tool }) => tool);
|
|
1282
|
+
return {
|
|
1283
|
+
messages: withoutDeveloperMessages(messages),
|
|
1284
|
+
tools: immediateTools
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
if (history === void 0) return {
|
|
1288
|
+
messages: withoutDeveloperMessages(messages),
|
|
1289
|
+
tools
|
|
1290
|
+
};
|
|
1291
|
+
const messageIds = new Set(messages.flatMap((message) => message.role === "developer" ? [message.id] : []));
|
|
1292
|
+
if (history.updates.some((update) => !messageIds.has(update.messageId))) return {
|
|
1293
|
+
messages: withoutDeveloperMessages(messages),
|
|
1294
|
+
tools
|
|
1295
|
+
};
|
|
1296
|
+
const declarations = toolDeclarations(tools, toolUpdate, history);
|
|
1297
|
+
const updateIds = new Set(history.updates.map((update) => update.messageId));
|
|
1298
|
+
const offered = new Set(history.tools.filter((tool) => !tool.deferLoading).map((tool) => tool.name));
|
|
1299
|
+
const projectedMessages = [];
|
|
1300
|
+
for (const message of messages) {
|
|
1301
|
+
if (message.role !== "developer") {
|
|
1302
|
+
projectedMessages.push(message);
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
if (!updateIds.has(message.id)) continue;
|
|
1306
|
+
const content = message.content.filter((block) => {
|
|
1307
|
+
switch (block.type) {
|
|
1308
|
+
case "tool-addition":
|
|
1309
|
+
if (!declarations.has(block.toolName) || offered.has(block.toolName)) return false;
|
|
1310
|
+
offered.add(block.toolName);
|
|
1311
|
+
return true;
|
|
1312
|
+
case "tool-removal":
|
|
1313
|
+
if (toolUpdate !== "in-history") return false;
|
|
1314
|
+
return offered.delete(block.toolName);
|
|
1315
|
+
default: return true;
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
if (content.length === 0) continue;
|
|
1319
|
+
if (content.length === message.content.length) projectedMessages.push(message);
|
|
1320
|
+
else projectedMessages.push({
|
|
1321
|
+
...message,
|
|
1322
|
+
content
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
messages: projectedMessages.length === messages.length && projectedMessages.every((message, index) => message === messages[index]) ? messages : projectedMessages,
|
|
1327
|
+
tools: [...declarations.values()]
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
//#endregion
|
|
1331
|
+
//#region ../../llm/llm/src/attribution.ts
|
|
1332
|
+
/**
|
|
1333
|
+
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
|
1334
|
+
* adapters from drifting. See
|
|
1335
|
+
* `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
|
1336
|
+
*
|
|
1337
|
+
* App-attribution vocabulary for provider requests.
|
|
1338
|
+
* @module @qilin-agent/llm/attribution
|
|
1339
|
+
*/
|
|
1340
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
1341
|
+
//#endregion
|
|
1342
|
+
//#region ../../llm/llm/src/index.ts
|
|
1343
|
+
/**
|
|
1344
|
+
* LLM service: adapter registry with a waterfall-interceptable streaming call
|
|
1345
|
+
* API. Exports the `LlmRuntime` default, the abstract `LlmAdapter` for
|
|
1346
|
+
* provider backends, and `BlockAssembler` for chunk assembly.
|
|
1347
|
+
*
|
|
1348
|
+
* @module @qilin-agent/llm
|
|
1349
|
+
*/
|
|
1350
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
1351
|
+
var useValue = arguments.length > 2;
|
|
1352
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
1353
|
+
return useValue ? value : void 0;
|
|
1354
|
+
};
|
|
1355
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
1356
|
+
function accept(f) {
|
|
1357
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
1358
|
+
return f;
|
|
1359
|
+
}
|
|
1360
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
1361
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
1362
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
1363
|
+
var _, done = false;
|
|
1364
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
1365
|
+
var context = {};
|
|
1366
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
1367
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
1368
|
+
context.addInitializer = function(f) {
|
|
1369
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
1370
|
+
extraInitializers.push(accept(f || null));
|
|
1371
|
+
};
|
|
1372
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
1373
|
+
get: descriptor.get,
|
|
1374
|
+
set: descriptor.set
|
|
1375
|
+
} : descriptor[key], context);
|
|
1376
|
+
if (kind === "accessor") {
|
|
1377
|
+
if (result === void 0) continue;
|
|
1378
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
1379
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
1380
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
1381
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
1382
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
1383
|
+
else descriptor[key] = _;
|
|
1384
|
+
}
|
|
1385
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
1386
|
+
done = true;
|
|
1387
|
+
};
|
|
1388
|
+
/**
|
|
1389
|
+
* Typed error for LLM-related failures. Extends {@link HarnessError}, so the
|
|
1390
|
+
* `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
|
|
1391
|
+
*/
|
|
1392
|
+
var LlmError = class extends HarnessError {
|
|
1393
|
+
/** Serializable facts retained beside this live Error. */
|
|
1394
|
+
failure;
|
|
1395
|
+
/**
|
|
1396
|
+
* @param message - non-empty human-readable failure summary.
|
|
1397
|
+
* @param code - non-empty stable provider-neutral machine code.
|
|
1398
|
+
* @param options - optional cause and validated serializable provider facts.
|
|
1399
|
+
*/
|
|
1400
|
+
constructor(message, code, options) {
|
|
1401
|
+
if (typeof message !== "string" || message.length === 0) throw new Error("LlmError message must be a non-empty string");
|
|
1402
|
+
if (typeof code !== "string" || code.length === 0) throw new Error("LlmError code must be a non-empty string");
|
|
1403
|
+
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");
|
|
1404
|
+
if (options?.providerRetryAfterMs !== void 0 && (!Number.isFinite(options.providerRetryAfterMs) || options.providerRetryAfterMs <= 0)) throw new Error("LlmError providerRetryAfterMs must be a positive finite number");
|
|
1405
|
+
if (options?.requestId !== void 0 && (typeof options.requestId !== "string" || options.requestId.length === 0)) throw new Error("LlmError requestId must be a non-empty string");
|
|
1406
|
+
super(message, code, options);
|
|
1407
|
+
this.name = "LlmError";
|
|
1408
|
+
this.failure = Object.freeze({
|
|
1409
|
+
message,
|
|
1410
|
+
code,
|
|
1411
|
+
...options?.status === void 0 ? {} : { status: options.status },
|
|
1412
|
+
...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
|
|
1413
|
+
...options?.requestId === void 0 ? {} : { requestId: options.requestId },
|
|
1414
|
+
...options?.offloadImages === void 0 ? {} : { offloadImages: options.offloadImages }
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
(() => {
|
|
1419
|
+
let _classSuper = TypertRemoteService;
|
|
1420
|
+
let _instanceExtraInitializers = [];
|
|
1421
|
+
let _listProviders_decorators;
|
|
1422
|
+
let _listConfigurableProviders_decorators;
|
|
1423
|
+
let _remoteDiscoverModels_decorators;
|
|
1424
|
+
return class LlmRuntime extends _classSuper {
|
|
1425
|
+
static {
|
|
1426
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
1427
|
+
_listProviders_decorators = [Remote];
|
|
1428
|
+
_listConfigurableProviders_decorators = [Remote];
|
|
1429
|
+
_remoteDiscoverModels_decorators = [Remote("discoverModels")];
|
|
1430
|
+
__esDecorate(this, null, _listProviders_decorators, {
|
|
1431
|
+
kind: "method",
|
|
1432
|
+
name: "listProviders",
|
|
1433
|
+
static: false,
|
|
1434
|
+
private: false,
|
|
1435
|
+
access: {
|
|
1436
|
+
has: (obj) => "listProviders" in obj,
|
|
1437
|
+
get: (obj) => obj.listProviders
|
|
1438
|
+
},
|
|
1439
|
+
metadata: _metadata
|
|
1440
|
+
}, null, _instanceExtraInitializers);
|
|
1441
|
+
__esDecorate(this, null, _listConfigurableProviders_decorators, {
|
|
1442
|
+
kind: "method",
|
|
1443
|
+
name: "listConfigurableProviders",
|
|
1444
|
+
static: false,
|
|
1445
|
+
private: false,
|
|
1446
|
+
access: {
|
|
1447
|
+
has: (obj) => "listConfigurableProviders" in obj,
|
|
1448
|
+
get: (obj) => obj.listConfigurableProviders
|
|
1449
|
+
},
|
|
1450
|
+
metadata: _metadata
|
|
1451
|
+
}, null, _instanceExtraInitializers);
|
|
1452
|
+
__esDecorate(this, null, _remoteDiscoverModels_decorators, {
|
|
1453
|
+
kind: "method",
|
|
1454
|
+
name: "remoteDiscoverModels",
|
|
1455
|
+
static: false,
|
|
1456
|
+
private: false,
|
|
1457
|
+
access: {
|
|
1458
|
+
has: (obj) => "remoteDiscoverModels" in obj,
|
|
1459
|
+
get: (obj) => obj.remoteDiscoverModels
|
|
1460
|
+
},
|
|
1461
|
+
metadata: _metadata
|
|
1462
|
+
}, null, _instanceExtraInitializers);
|
|
1463
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
1464
|
+
enumerable: true,
|
|
1465
|
+
configurable: true,
|
|
1466
|
+
writable: true,
|
|
1467
|
+
value: _metadata
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
adapters = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Map());
|
|
1471
|
+
directory = /* @__PURE__ */ new Map();
|
|
1472
|
+
discoveries = /* @__PURE__ */ new Map();
|
|
1473
|
+
constructor(ctx) {
|
|
1474
|
+
super(ctx, "llm");
|
|
1475
|
+
}
|
|
1476
|
+
/** Notify topology observers without letting one broken listener veto the commit. */
|
|
1477
|
+
emitAdaptersUpdated() {
|
|
1478
|
+
let invariantFailure;
|
|
1479
|
+
for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
|
|
1480
|
+
const returned = listener();
|
|
1481
|
+
if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
|
|
1482
|
+
this.warnAdaptersListenerFailure(error);
|
|
1483
|
+
});
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
if (error?.code === "INVARIANT") {
|
|
1486
|
+
invariantFailure ??= error;
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
this.warnAdaptersListenerFailure(error);
|
|
1490
|
+
}
|
|
1491
|
+
if (invariantFailure !== void 0) throw invariantFailure;
|
|
1492
|
+
}
|
|
1493
|
+
/** Contained-listener diagnostic shared by the sync and async failure paths. */
|
|
1494
|
+
warnAdaptersListenerFailure(error) {
|
|
1495
|
+
this.ctx.logger.warn("llm: an llm/adapters-updated listener failed");
|
|
1496
|
+
this.ctx.logger.warn(error);
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Register an adapter for the given provider routes. Throws `LlmError` with code
|
|
1500
|
+
* `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
|
|
1501
|
+
* Disposed with the fiber.
|
|
1502
|
+
* @param providers - every provider route this adapter should serve.
|
|
1503
|
+
* @param adapter - the adapter that streams calls for those providers.
|
|
1504
|
+
* @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
|
|
1505
|
+
*/
|
|
1506
|
+
registerAdapter(providers, adapter) {
|
|
1507
|
+
const owned = /* @__PURE__ */ new Set();
|
|
1508
|
+
let released = false;
|
|
1509
|
+
const dispose = this.ctx.effect(function* () {
|
|
1510
|
+
if (providers.length === 0) throw new LlmError("an adapter must register at least one provider", "INVALID_ADAPTER");
|
|
1511
|
+
this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
|
|
1512
|
+
yield () => {
|
|
1513
|
+
released = true;
|
|
1514
|
+
for (const provider of owned) this.adapters.delete(provider);
|
|
1515
|
+
owned.clear();
|
|
1516
|
+
this.emitAdaptersUpdated();
|
|
1517
|
+
};
|
|
1518
|
+
}.bind(this), "llm.registerAdapter()");
|
|
1519
|
+
const handle = (() => void dispose());
|
|
1520
|
+
handle.replace = (next) => {
|
|
1521
|
+
if (released) throw new LlmError("a disposed adapter registration cannot replace its routes", "REGISTRATION_DISPOSED");
|
|
1522
|
+
this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
|
|
1523
|
+
};
|
|
1524
|
+
return handle;
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Validate one candidate route set for `adapter`, treating routes this
|
|
1528
|
+
* registration already holds as available. Nothing is mutated: a rejected
|
|
1529
|
+
* candidate leaves the registry exactly as it was.
|
|
1530
|
+
*/
|
|
1531
|
+
prepareRoutes(providers, adapter, owned) {
|
|
1532
|
+
const unique = /* @__PURE__ */ new Set();
|
|
1533
|
+
const registrations = [];
|
|
1534
|
+
for (const provider of providers) {
|
|
1535
|
+
if (provider.length === 0) throw new LlmError("adapter provider names must be non-empty", "INVALID_ADAPTER");
|
|
1536
|
+
if (unique.has(provider) || this.adapters.has(provider) && !owned.has(provider)) throw new LlmError(`an adapter for provider "${provider}" is already registered`, "DUPLICATE_ADAPTER");
|
|
1537
|
+
const info = adapter.providerInfo(provider);
|
|
1538
|
+
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");
|
|
1539
|
+
unique.add(provider);
|
|
1540
|
+
const retryPolicy = adapter.providerRetryPolicy(provider) ?? resolveRetryPolicy(void 0, `llm: provider "${provider}" retryPolicy`);
|
|
1541
|
+
registrations.push({
|
|
1542
|
+
adapter,
|
|
1543
|
+
provider: {
|
|
1544
|
+
id: info.id,
|
|
1545
|
+
name: info.name
|
|
1546
|
+
},
|
|
1547
|
+
retryPolicy
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
return registrations;
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Swap this registration's routes for the prepared ones in one synchronous
|
|
1554
|
+
* section, so no observer can see the registry between the release and the
|
|
1555
|
+
* re-registration. The route set's one mutation point is also where
|
|
1556
|
+
* `llm/adapters-updated` is published, so a `replace` announces itself
|
|
1557
|
+
* exactly like a first registration.
|
|
1558
|
+
*/
|
|
1559
|
+
commitRoutes(owned, registrations) {
|
|
1560
|
+
for (const provider of owned) this.adapters.delete(provider);
|
|
1561
|
+
owned.clear();
|
|
1562
|
+
for (const registration of registrations) {
|
|
1563
|
+
this.adapters.set(registration.provider.id, registration);
|
|
1564
|
+
owned.add(registration.provider.id);
|
|
1565
|
+
}
|
|
1566
|
+
this.emitAdaptersUpdated();
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Describe provider routes with a registered adapter.
|
|
1570
|
+
* @returns detached provider metadata in registration order.
|
|
1571
|
+
*/
|
|
1572
|
+
listProviders() {
|
|
1573
|
+
return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Declare provider routes an adapter plugin can activate through
|
|
1577
|
+
* configuration. Registration is all-or-nothing: an empty list, invalid
|
|
1578
|
+
* entry, or a provider already declared by any registration throws
|
|
1579
|
+
* `LlmError` without registering the rest. Disposed with the fiber.
|
|
1580
|
+
* @param entries - every configurable provider this plugin owns.
|
|
1581
|
+
* @returns a handle that withdraws all of them, and can atomically replace them.
|
|
1582
|
+
*/
|
|
1583
|
+
registerConfigurableProviders(entries) {
|
|
1584
|
+
let held = [];
|
|
1585
|
+
let disposed = false;
|
|
1586
|
+
/**
|
|
1587
|
+
* Validate a candidate set in full against everything this registration
|
|
1588
|
+
* does not already hold, then publish it. Nothing is written until the
|
|
1589
|
+
* whole set passes, so a refused candidate leaves the current entries in
|
|
1590
|
+
* place — the property that makes `replace` a swap rather than a
|
|
1591
|
+
* delete-then-add that can strand the directory empty.
|
|
1592
|
+
*/
|
|
1593
|
+
const commit = (candidates) => {
|
|
1594
|
+
const detached = [];
|
|
1595
|
+
const own = new Set(held.map((entry) => entry.provider));
|
|
1596
|
+
for (const entry of candidates) {
|
|
1597
|
+
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");
|
|
1598
|
+
if (entry.settingsPath.some((segment) => segment.length === 0)) throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, "INVALID_DIRECTORY");
|
|
1599
|
+
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");
|
|
1600
|
+
detached.push({
|
|
1601
|
+
...entry,
|
|
1602
|
+
settingsPath: [...entry.settingsPath]
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
for (const entry of held) this.directory.delete(entry.provider);
|
|
1606
|
+
for (const entry of detached) this.directory.set(entry.provider, entry);
|
|
1607
|
+
held = detached;
|
|
1608
|
+
this.emitAdaptersUpdated();
|
|
1609
|
+
};
|
|
1610
|
+
const dispose = this.ctx.effect(function* () {
|
|
1611
|
+
if (entries.length === 0) throw new LlmError("a configurable-provider registration must declare at least one provider", "INVALID_DIRECTORY");
|
|
1612
|
+
commit(entries);
|
|
1613
|
+
yield () => {
|
|
1614
|
+
disposed = true;
|
|
1615
|
+
for (const entry of held) this.directory.delete(entry.provider);
|
|
1616
|
+
held = [];
|
|
1617
|
+
this.emitAdaptersUpdated();
|
|
1618
|
+
};
|
|
1619
|
+
}.bind(this), "llm.registerConfigurableProviders()");
|
|
1620
|
+
const handle = (() => void dispose());
|
|
1621
|
+
handle.replace = (next) => {
|
|
1622
|
+
if (disposed) throw new LlmError("this configurable-provider registration was disposed", "REGISTRATION_DISPOSED");
|
|
1623
|
+
commit(next);
|
|
1624
|
+
};
|
|
1625
|
+
return handle;
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* List every declared configurable provider, registered or dormant.
|
|
1629
|
+
* @returns detached directory entries in declaration order.
|
|
1630
|
+
*/
|
|
1631
|
+
listConfigurableProviders() {
|
|
1632
|
+
return [...this.directory.values()].map((entry) => ({
|
|
1633
|
+
...entry,
|
|
1634
|
+
settingsPath: [...entry.settingsPath]
|
|
1635
|
+
}));
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Offer to interrogate provider endpoints on behalf of the settings
|
|
1639
|
+
* namespace this plugin owns. The namespace is the key because that is what
|
|
1640
|
+
* a configuration surface already holds from the configurable-provider
|
|
1641
|
+
* directory, and because a provider being *added* has no route to name yet.
|
|
1642
|
+
* Disposed with the fiber.
|
|
1643
|
+
* @param settingsNs - the namespace whose profiles this discovery serves.
|
|
1644
|
+
* @param discover - interrogates one endpoint and must honor the supplied signal.
|
|
1645
|
+
* @returns the disposer that withdraws the offer.
|
|
1646
|
+
*/
|
|
1647
|
+
registerModelDiscovery(settingsNs, discover) {
|
|
1648
|
+
const dispose = this.ctx.effect(function* () {
|
|
1649
|
+
if (settingsNs.length === 0) throw new LlmError("model discovery needs a non-empty settings namespace", "INVALID_DISCOVERY");
|
|
1650
|
+
if (this.discoveries.has(settingsNs)) throw new LlmError(`model discovery for "${settingsNs}" is already registered`, "DUPLICATE_DISCOVERY");
|
|
1651
|
+
this.discoveries.set(settingsNs, discover);
|
|
1652
|
+
yield () => {
|
|
1653
|
+
this.discoveries.delete(settingsNs);
|
|
1654
|
+
};
|
|
1655
|
+
}.bind(this), "llm.registerModelDiscovery()");
|
|
1656
|
+
return () => void dispose();
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Interrogate one provider endpoint for the models it advertises. The
|
|
1660
|
+
* request describes a draft, not a stored route, so nothing here reads or
|
|
1661
|
+
* writes settings or credentials — the caller owns both, and the reply is
|
|
1662
|
+
* candidate metadata a surface may offer for adoption.
|
|
1663
|
+
* @param settingsNs - namespace whose registered discovery serves this draft.
|
|
1664
|
+
* @param request - the endpoint, protocol, and one-shot credential to use.
|
|
1665
|
+
* @param signal - caller cancellation.
|
|
1666
|
+
* @returns the advertised models, deduplicated in endpoint order.
|
|
1667
|
+
*/
|
|
1668
|
+
async discoverModels(settingsNs, request, signal) {
|
|
1669
|
+
const discover = this.discoveries.get(settingsNs);
|
|
1670
|
+
if (discover === void 0) throw new LlmError(`no model discovery is registered for "${settingsNs}"`, "NO_DISCOVERY");
|
|
1671
|
+
if ((request.provider ?? "").length === 0 && (request.baseURL ?? "").length === 0) throw new LlmError("model discovery needs a provider route or a baseURL", "INVALID_DISCOVERY");
|
|
1672
|
+
const discovered = signal === void 0 ? await discover(request) : await discover(request, signal);
|
|
1673
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1674
|
+
const models = [];
|
|
1675
|
+
for (const model of discovered) {
|
|
1676
|
+
if (typeof model.id !== "string" || model.id.length === 0 || seen.has(model.id)) continue;
|
|
1677
|
+
seen.add(model.id);
|
|
1678
|
+
models.push({
|
|
1679
|
+
id: model.id,
|
|
1680
|
+
...model.name === void 0 ? {} : { name: model.name },
|
|
1681
|
+
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
1682
|
+
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
|
|
1683
|
+
...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
return models;
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Remote adapter for one draft provider interrogation.
|
|
1690
|
+
* @param settingsNs - namespace whose registered discovery serves this draft.
|
|
1691
|
+
* @param request - endpoint, protocol, and one-shot credential to use.
|
|
1692
|
+
* @param signal - caller cancellation supplied by the Remote carrier.
|
|
1693
|
+
* @returns advertised models in endpoint order.
|
|
1694
|
+
* @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
|
|
1695
|
+
*/
|
|
1696
|
+
async remoteDiscoverModels(settingsNs, request, signal) {
|
|
1697
|
+
try {
|
|
1698
|
+
return await this.discoverModels(settingsNs, request, signal);
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
throw new RemoteError("llm/model-discovery-rejected", error instanceof Error ? error.message : String(error), {
|
|
1701
|
+
settingsNs,
|
|
1702
|
+
...request.baseURL === void 0 ? {} : { baseURL: request.baseURL }
|
|
1703
|
+
}, { cause: error });
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Resolve the retry policy captured when one provider route was registered.
|
|
1708
|
+
* @param provider - registered provider route to inspect.
|
|
1709
|
+
* @returns the provider-owned policy, with normal defaults already resolved.
|
|
1710
|
+
*/
|
|
1711
|
+
providerRetryPolicy(provider) {
|
|
1712
|
+
return this.registration(provider).retryPolicy;
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Resolve provider-side request-image pricing for one exact route, or
|
|
1716
|
+
* `undefined` when the provider is unregistered or declares none. Unknown
|
|
1717
|
+
* providers degrade to `undefined` rather than throwing because callers
|
|
1718
|
+
* price durable history whose route may no longer be mounted.
|
|
1719
|
+
* @param provider - provider route named by a request header.
|
|
1720
|
+
* @param model - exact model id named by the same header.
|
|
1721
|
+
* @returns the owning adapter's image pricing for the route, when declared.
|
|
1722
|
+
*/
|
|
1723
|
+
imageRequestPricing(provider, model) {
|
|
1724
|
+
return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Resolve the exact text one durable file occurrence contributes to every
|
|
1728
|
+
* provider request in the current execution environment.
|
|
1729
|
+
* @param ref - durable verbatim file reference from model history.
|
|
1730
|
+
* @returns the same deterministic handle text used at adapter dispatch.
|
|
1731
|
+
*/
|
|
1732
|
+
fileRequestText(ref) {
|
|
1733
|
+
return fileHandleText(ref, this.fileReadPath(ref));
|
|
1734
|
+
}
|
|
1735
|
+
/** Detach typed adapter-owned modality metadata. */
|
|
1736
|
+
detachedModalities(modalities) {
|
|
1737
|
+
return modalities === void 0 ? void 0 : [...modalities];
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Discover models advertised by one registered provider. Catalog membership
|
|
1741
|
+
* does not constrain core routing. Catalog-driven entry points may restrict
|
|
1742
|
+
* selection and submission to the advertised models.
|
|
1743
|
+
* @param provider - registered provider route to inspect.
|
|
1744
|
+
* @returns detached model metadata in adapter-preferred order.
|
|
1745
|
+
*/
|
|
1746
|
+
async listModels(provider) {
|
|
1747
|
+
const models = await this.registration(provider).adapter.listModels(provider);
|
|
1748
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1749
|
+
return models.map((model) => {
|
|
1750
|
+
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");
|
|
1751
|
+
seen.add(model.id);
|
|
1752
|
+
const inputModalities = this.detachedModalities(model.inputModalities);
|
|
1753
|
+
return {
|
|
1754
|
+
provider: model.provider,
|
|
1755
|
+
id: model.id,
|
|
1756
|
+
name: model.name,
|
|
1757
|
+
...model.description === void 0 ? {} : { description: model.description },
|
|
1758
|
+
...inputModalities === void 0 ? {} : { inputModalities }
|
|
1759
|
+
};
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Resolve and validate all metadata from the adapter that owns one exact
|
|
1764
|
+
* route. The result is detached from adapter-owned objects; catalog
|
|
1765
|
+
* membership remains advisory and does not control request routing.
|
|
1766
|
+
* @param provider - registered provider route to inspect.
|
|
1767
|
+
* @param model - exact model id passed to the adapter.
|
|
1768
|
+
* @param signal - optional cancellation for adapter-owned asynchronous lookup.
|
|
1769
|
+
* @returns exact model identity plus available context and reasoning metadata.
|
|
1770
|
+
*/
|
|
1771
|
+
async resolveModelInfo(provider, model, signal) {
|
|
1772
|
+
return this.resolveModelInfoFor(this.registration(provider), model, signal);
|
|
1773
|
+
}
|
|
1774
|
+
async resolveModelInfoFor(registration, model, signal) {
|
|
1775
|
+
const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
|
|
1776
|
+
return this.normalizeModelInfo(registration, model, resolved);
|
|
1777
|
+
}
|
|
1778
|
+
/** Validate and detach one adapter-returned exact model result. */
|
|
1779
|
+
normalizeModelInfo(registration, model, resolved) {
|
|
1780
|
+
const provider = registration.provider.id;
|
|
1781
|
+
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");
|
|
1782
|
+
const context = resolved.context;
|
|
1783
|
+
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");
|
|
1784
|
+
const inputModalities = this.detachedModalities(resolved.inputModalities);
|
|
1785
|
+
const systemPromptUpdate = resolved.systemPromptUpdate;
|
|
1786
|
+
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");
|
|
1787
|
+
const toolUpdate = resolved.toolUpdate;
|
|
1788
|
+
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");
|
|
1789
|
+
const defaultMaxTokens = resolved.defaultMaxTokens;
|
|
1790
|
+
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");
|
|
1791
|
+
const info = {
|
|
1792
|
+
provider,
|
|
1793
|
+
id: model,
|
|
1794
|
+
name: resolved.name,
|
|
1795
|
+
...resolved.description === void 0 ? {} : { description: resolved.description },
|
|
1796
|
+
...inputModalities === void 0 ? {} : { inputModalities },
|
|
1797
|
+
...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
|
|
1798
|
+
...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
|
|
1799
|
+
...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate },
|
|
1800
|
+
...resolved.toolUpdate === void 0 ? {} : { toolUpdate: resolved.toolUpdate }
|
|
1801
|
+
};
|
|
1802
|
+
const reasoning = resolved.reasoning;
|
|
1803
|
+
if (reasoning === void 0) return info;
|
|
1804
|
+
if (reasoning.efforts.length === 0) throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
|
|
1805
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1806
|
+
const efforts = reasoning.efforts.map((effort) => {
|
|
1807
|
+
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");
|
|
1808
|
+
seen.add(effort.id);
|
|
1809
|
+
return {
|
|
1810
|
+
id: effort.id,
|
|
1811
|
+
name: effort.name,
|
|
1812
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
1813
|
+
};
|
|
1814
|
+
});
|
|
1815
|
+
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");
|
|
1816
|
+
return {
|
|
1817
|
+
...info,
|
|
1818
|
+
reasoning: {
|
|
1819
|
+
efforts,
|
|
1820
|
+
...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Validate a conversation call config against its exact model capability and
|
|
1826
|
+
* materialize adapter-configured defaults. Unsupported explicit efforts
|
|
1827
|
+
* reject before provider I/O; no clamping or aliasing is performed. This
|
|
1828
|
+
* standalone query does not bind a later dispatch; use {@link prepareCall}
|
|
1829
|
+
* when logging and streaming must share one adapter registration.
|
|
1830
|
+
* @param config - provider/model route and optional request controls.
|
|
1831
|
+
* @param signal - optional cancellation for adapter-owned capability lookup.
|
|
1832
|
+
* @returns a detached config only when a default must be materialized.
|
|
1833
|
+
*/
|
|
1834
|
+
async resolveCallConfig(config, signal) {
|
|
1835
|
+
return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
|
|
1836
|
+
}
|
|
1837
|
+
async resolveCallFor(registration, config, signal) {
|
|
1838
|
+
const info = await this.resolveModelInfoFor(registration, config.model, signal);
|
|
1839
|
+
return this.resolveCallWithInfo(config, info);
|
|
1840
|
+
}
|
|
1841
|
+
/** Validate request controls against one already-bound exact model result. */
|
|
1842
|
+
resolveCallWithInfo(config, info) {
|
|
1843
|
+
const defaulted = config.maxTokens === void 0 && info.defaultMaxTokens !== void 0 ? {
|
|
1844
|
+
...config,
|
|
1845
|
+
maxTokens: info.defaultMaxTokens
|
|
1846
|
+
} : config;
|
|
1847
|
+
const reasoning = info.reasoning;
|
|
1848
|
+
const requested = defaulted.reasoningEffort;
|
|
1849
|
+
let resolvedConfig = defaulted;
|
|
1850
|
+
if (reasoning === void 0) {
|
|
1851
|
+
if (requested !== void 0) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, "UNSUPPORTED_REASONING_EFFORT");
|
|
1852
|
+
} else {
|
|
1853
|
+
const effective = requested ?? reasoning.defaultEffort;
|
|
1854
|
+
if (effective !== void 0) {
|
|
1855
|
+
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");
|
|
1856
|
+
if (requested !== effective) resolvedConfig = {
|
|
1857
|
+
...defaulted,
|
|
1858
|
+
reasoningEffort: effective
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return {
|
|
1863
|
+
config: resolvedConfig,
|
|
1864
|
+
...info.context === void 0 ? {} : { context: info.context },
|
|
1865
|
+
modelInfo: info
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Resolve one call under its current adapter registration. The returned
|
|
1870
|
+
* one-shot handle keeps that registration across header logging and dispatch,
|
|
1871
|
+
* so HMR cannot combine one adapter's capability result with another adapter.
|
|
1872
|
+
* @param config - provider/model route and optional request controls.
|
|
1873
|
+
* @param signal - optional cancellation for adapter-owned capability lookup.
|
|
1874
|
+
* @returns a prepared config and its registration-bound stream entry point.
|
|
1875
|
+
*/
|
|
1876
|
+
async prepareCall(config, signal) {
|
|
1877
|
+
const registration = this.registration(config.provider);
|
|
1878
|
+
const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
|
|
1879
|
+
const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
|
|
1880
|
+
const resolved = this.resolveCallWithInfo(config, modelInfo);
|
|
1881
|
+
const resolvedConfig = deepFreeze(structuredClone(resolved.config));
|
|
1882
|
+
const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
|
|
1883
|
+
const adapterDefaults = deepFreeze({
|
|
1884
|
+
...config.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
|
|
1885
|
+
...config.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
|
|
1886
|
+
});
|
|
1887
|
+
let dispatched = false;
|
|
1888
|
+
return Object.freeze({
|
|
1889
|
+
config: resolvedConfig,
|
|
1890
|
+
retryPolicy: registration.retryPolicy,
|
|
1891
|
+
adapterDefaults,
|
|
1892
|
+
...context === void 0 ? {} : { context },
|
|
1893
|
+
...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
|
|
1894
|
+
...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
|
|
1895
|
+
...modelInfo.toolUpdate === void 0 ? {} : { toolUpdate: modelInfo.toolUpdate },
|
|
1896
|
+
stream: (options) => {
|
|
1897
|
+
if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
|
|
1898
|
+
if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
1899
|
+
dispatched = true;
|
|
1900
|
+
return this.streamWithRegistration(options, {
|
|
1901
|
+
registration,
|
|
1902
|
+
config: resolvedConfig,
|
|
1903
|
+
modelInfo,
|
|
1904
|
+
dispatch: (options) => adapterCall.stream(options)
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
registration(provider) {
|
|
1910
|
+
const registration = this.adapters.get(provider);
|
|
1911
|
+
if (!registration) throw new LlmError(`no adapter registered for provider "${provider}"`, "NO_ADAPTER");
|
|
1912
|
+
return registration;
|
|
1913
|
+
}
|
|
1914
|
+
/** Remove replay state whose historical route is owned by another adapter. */
|
|
1915
|
+
forAdapter(options, adapter) {
|
|
1916
|
+
const messages = options.messages.map((message) => {
|
|
1917
|
+
if (message.role !== "assistant") return message;
|
|
1918
|
+
const source = message.source;
|
|
1919
|
+
if (source.replayState === void 0) return message;
|
|
1920
|
+
if (this.adapters.get(source.provider)?.adapter === adapter) return message;
|
|
1921
|
+
return freezeMessage({
|
|
1922
|
+
...message,
|
|
1923
|
+
source: {
|
|
1924
|
+
kind: "model",
|
|
1925
|
+
provider: source.provider,
|
|
1926
|
+
model: source.model
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
});
|
|
1930
|
+
if (messages.every((message, index) => message === options.messages[index])) return options;
|
|
1931
|
+
const filtered = {
|
|
1932
|
+
...options,
|
|
1933
|
+
messages
|
|
1934
|
+
};
|
|
1935
|
+
return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Resolve the current execution-world read path of one durable file
|
|
1939
|
+
* reference through the mounted attachment and filesystem providers.
|
|
1940
|
+
*/
|
|
1941
|
+
fileReadPath(ref) {
|
|
1942
|
+
let hostPath;
|
|
1943
|
+
try {
|
|
1944
|
+
hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
|
|
1945
|
+
} catch {
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
if (hostPath === void 0) return void 0;
|
|
1949
|
+
return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Final adapter boundary. Adapter selection, dispatch, iterator construction,
|
|
1953
|
+
* and iteration failures become one terminal failure chunk. Middleware and
|
|
1954
|
+
* downstream consumer failures remain thrown plugin or consumer errors.
|
|
1955
|
+
*/
|
|
1956
|
+
async *adapterStream(options, prepared) {
|
|
1957
|
+
let iterator;
|
|
1958
|
+
try {
|
|
1959
|
+
const registration = prepared?.registration ?? this.registration(options.provider);
|
|
1960
|
+
const adapter = registration.adapter;
|
|
1961
|
+
let modelInfo;
|
|
1962
|
+
let resolvedConfig;
|
|
1963
|
+
let dispatch;
|
|
1964
|
+
if (prepared === void 0) {
|
|
1965
|
+
const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
|
|
1966
|
+
modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
|
|
1967
|
+
resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
|
|
1968
|
+
dispatch = (options) => adapterCall.stream(options);
|
|
1969
|
+
} else {
|
|
1970
|
+
modelInfo = prepared.modelInfo;
|
|
1971
|
+
resolvedConfig = prepared.config;
|
|
1972
|
+
dispatch = prepared.dispatch;
|
|
1973
|
+
}
|
|
1974
|
+
if (prepared !== void 0 && !callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
1975
|
+
const resolvedOptions = callConfigEquals(options, resolvedConfig) ? options : Object.isFrozen(options) ? deepFreeze({
|
|
1976
|
+
...options,
|
|
1977
|
+
...resolvedConfig
|
|
1978
|
+
}) : {
|
|
1979
|
+
...options,
|
|
1980
|
+
...resolvedConfig
|
|
1981
|
+
};
|
|
1982
|
+
let projectedMessages = resolvedOptions.messages;
|
|
1983
|
+
if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
|
|
1984
|
+
if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
|
|
1985
|
+
const projectedTools = projectToolUpdates(projectedMessages, resolvedOptions.tools, modelInfo.toolUpdate, resolvedOptions.toolHistory);
|
|
1986
|
+
projectedMessages = projectedTools.messages;
|
|
1987
|
+
let projectedOptions = resolvedOptions;
|
|
1988
|
+
if (projectedMessages !== resolvedOptions.messages || projectedTools.tools !== resolvedOptions.tools) {
|
|
1989
|
+
projectedOptions = {
|
|
1990
|
+
...resolvedOptions,
|
|
1991
|
+
messages: projectedMessages,
|
|
1992
|
+
...projectedTools.tools === void 0 ? {} : { tools: projectedTools.tools }
|
|
1993
|
+
};
|
|
1994
|
+
if (Object.isFrozen(resolvedOptions)) deepFreeze(projectedOptions);
|
|
1995
|
+
}
|
|
1996
|
+
iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
yield adapterFailureChunk(error, options.signal);
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
let completed = false;
|
|
2002
|
+
try {
|
|
2003
|
+
while (true) {
|
|
2004
|
+
let item;
|
|
2005
|
+
try {
|
|
2006
|
+
const next = await iterator.next();
|
|
2007
|
+
item = next.done ? { done: true } : {
|
|
2008
|
+
done: false,
|
|
2009
|
+
value: next.value
|
|
2010
|
+
};
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
completed = true;
|
|
2013
|
+
yield adapterFailureChunk(error, options.signal);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
if (item.done) {
|
|
2017
|
+
completed = true;
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
yield item.value;
|
|
2021
|
+
}
|
|
2022
|
+
} finally {
|
|
2023
|
+
if (!completed) {
|
|
2024
|
+
const close = iterator.return?.bind(iterator);
|
|
2025
|
+
if (close) await close();
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Stream one model call as raw chunks (token-level deltas). Replay state is
|
|
2031
|
+
* retained only when the same adapter instance owns its historical provider
|
|
2032
|
+
* and the target provider. Final adapter selection remains fixed through
|
|
2033
|
+
* asynchronous exact-model resolution and dispatch. Adapter selection,
|
|
2034
|
+
* dispatch, and iteration failures become terminal `error` or `aborted`
|
|
2035
|
+
* finish chunks; middleware, nested-call, cleanup, and consumer failures
|
|
2036
|
+
* remain thrown.
|
|
2037
|
+
* @param options - the full request; `options.provider` selects the adapter.
|
|
2038
|
+
* @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
|
|
2039
|
+
*/
|
|
2040
|
+
stream(options) {
|
|
2041
|
+
return this.streamWithRegistration(options);
|
|
2042
|
+
}
|
|
2043
|
+
streamWithRegistration(options, prepared) {
|
|
2044
|
+
return this.ctx.waterfall(this, "llm/stream", options, () => this.adapterStream(options, prepared));
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
})();
|
|
2048
|
+
/** Convert one adapter throw into the stream protocol's terminal outcome. */
|
|
2049
|
+
function adapterFailureChunk(error, signal) {
|
|
2050
|
+
const failure = normalizeLlmFailure(error);
|
|
2051
|
+
return {
|
|
2052
|
+
type: "finish",
|
|
2053
|
+
reason: signal?.aborted || failure.code === "ABORTED" ? {
|
|
2054
|
+
kind: "aborted",
|
|
2055
|
+
failure
|
|
2056
|
+
} : {
|
|
2057
|
+
kind: "error",
|
|
2058
|
+
failure
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
//#endregion
|
|
2063
|
+
//#region lib/types/index.js
|
|
2064
|
+
/**
|
|
2065
|
+
* Opt-in request-preparation tmux-location context. Eligible step attempts
|
|
2066
|
+
* append durable, source-attributed context naming the tmux session, window,
|
|
2067
|
+
* and pane this agent process runs in, plus the window's pane-tree layout.
|
|
2068
|
+
*
|
|
2069
|
+
* The plugin pulls state once per turn, for the first request (`step === 1`), by
|
|
2070
|
+
* running one `tmux display-message` through the `ctx.shell` executor service. It
|
|
2071
|
+
* confirms this process genuinely runs inside the pane `$TMUX_PANE` names by
|
|
2072
|
+
* matching the pane's `#{pane_tty}` against this process's controlling terminal,
|
|
2073
|
+
* so a terminal that merely inherited `$TMUX`/`$TMUX_PANE` from a tmux ancestor
|
|
2074
|
+
* (e.g. a VS Code integrated terminal) reads as "not in tmux". It re-injects
|
|
2075
|
+
* only when the rendered tmux state changes since the last injection (a moved,
|
|
2076
|
+
* renamed, or re-laid-out pane), with an optional `refreshIntervalMs` floor
|
|
2077
|
+
* between injections. Absent tmux environment, an inherited-only environment,
|
|
2078
|
+
* absent `ctx.shell`, or a failed query is a no-op, never an error: an executor
|
|
2079
|
+
* rejection is contained and logged as a warning so the turn continues.
|
|
2080
|
+
*
|
|
2081
|
+
* @module @qilin-agent/tmux-context
|
|
2082
|
+
*/
|
|
2083
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
2084
|
+
const name = "tmux-context";
|
|
2085
|
+
/** The agent registry that owns pre-step processing. */
|
|
2086
|
+
const inject = ["agents", "sessionProjections"];
|
|
2087
|
+
/** Schemastery validation for {@link Config}. */
|
|
2088
|
+
const Config = z.object({ refreshIntervalMs: z.number() });
|
|
2089
|
+
/**
|
|
2090
|
+
* Tab-separated tmux format fields, in query order. Layout (`window_layout`)
|
|
2091
|
+
* is the pane-tree description; pane/window pixel sizes are intentionally
|
|
2092
|
+
* excluded (own location and layout only, per the package scope).
|
|
2093
|
+
*/
|
|
2094
|
+
const TMUX_FIELDS = [
|
|
2095
|
+
"#{session_name}",
|
|
2096
|
+
"#{window_index}",
|
|
2097
|
+
"#{window_name}",
|
|
2098
|
+
"#{pane_index}",
|
|
2099
|
+
"#{pane_id}",
|
|
2100
|
+
"#{window_active}",
|
|
2101
|
+
"#{pane_active}",
|
|
2102
|
+
"#{window_layout}"
|
|
2103
|
+
];
|
|
2104
|
+
/** Prefix marking the volatile turn/step preamble line of a rendered reading. */
|
|
2105
|
+
const READING_PREFIX = "tmux location (turn ";
|
|
2106
|
+
/**
|
|
2107
|
+
* Field separator between tmux format fields. tmux does not interpret C escapes
|
|
2108
|
+
* in a format, so the literal two-character sequence `\t` is emitted verbatim
|
|
2109
|
+
* and split back out here; this avoids embedding raw whitespace in the command.
|
|
2110
|
+
*/
|
|
2111
|
+
const FIELD_SEP = "\\t";
|
|
2112
|
+
/**
|
|
2113
|
+
* Read this process's tmux location through the bash seam, or `undefined` when
|
|
2114
|
+
* this process is not genuinely running inside a tmux pane or the query fails.
|
|
2115
|
+
*
|
|
2116
|
+
* `$TMUX_PANE` alone is insufficient: a terminal launched from a tmux shell
|
|
2117
|
+
* (e.g. VS Code's integrated terminal, a desktop launcher) inherits `$TMUX` and
|
|
2118
|
+
* `$TMUX_PANE` from that ancestor, so the variables are present even though this
|
|
2119
|
+
* process does not live in that pane. The command therefore also compares the
|
|
2120
|
+
* pane's `#{pane_tty}` against this process's own controlling terminal
|
|
2121
|
+
* (`ps -o tty=` for {@link processId}); a genuine pane owns this process's tty,
|
|
2122
|
+
* an inherited environment names some other pane's tty. Fields are emitted only
|
|
2123
|
+
* on a match, so an inherited environment reads as "not in tmux" and injects
|
|
2124
|
+
* nothing.
|
|
2125
|
+
*
|
|
2126
|
+
* The location is optional context, so an executor rejection is a failed query,
|
|
2127
|
+
* not a turn failure: `resolve()` may reject the command on policy grounds and
|
|
2128
|
+
* `run()` only promises to resolve for nonzero exits, timeouts, and aborts, so
|
|
2129
|
+
* both are contained and reported as a warning.
|
|
2130
|
+
*
|
|
2131
|
+
* @param bash - The executor service used to run the read-only tmux/ps commands.
|
|
2132
|
+
* @param logger - receives a warning when the executor rejects the query.
|
|
2133
|
+
* @param processId - this agent process's pid, whose controlling tty must match the pane.
|
|
2134
|
+
* @param signal - abort signal forwarded to the executor.
|
|
2135
|
+
* @returns the parsed location, or `undefined` when not in a real pane or on any failure.
|
|
2136
|
+
*/
|
|
2137
|
+
async function queryTmuxLocation(bash, logger, processId, signal) {
|
|
2138
|
+
const format = TMUX_FIELDS.join(FIELD_SEP);
|
|
2139
|
+
const command = [
|
|
2140
|
+
"[ -n \"$TMUX_PANE\" ] || exit 1",
|
|
2141
|
+
`self_tty=$(ps -o tty= -p ${processId} | tr -d ' ')`,
|
|
2142
|
+
"[ -n \"$self_tty\" ] || exit 1",
|
|
2143
|
+
"pane_tty=$(tmux display-message -t \"$TMUX_PANE\" -p '#{pane_tty}') || exit 1",
|
|
2144
|
+
"[ \"$pane_tty\" = \"/dev/$self_tty\" ] || exit 1",
|
|
2145
|
+
`exec tmux display-message -t "$TMUX_PANE" -p '${format}'`
|
|
2146
|
+
].join("\n");
|
|
2147
|
+
let result;
|
|
2148
|
+
try {
|
|
2149
|
+
result = await bash.run(bash.resolve({
|
|
2150
|
+
command,
|
|
2151
|
+
signal
|
|
2152
|
+
}));
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2155
|
+
logger.warn(`tmux location query failed: ${message}; injecting no location this turn`);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
if (result.exitCode !== 0) return void 0;
|
|
2159
|
+
const parts = result.stdout.text.split("\n", 1)[0].split(FIELD_SEP);
|
|
2160
|
+
if (parts.length !== TMUX_FIELDS.length) return void 0;
|
|
2161
|
+
const [sessionName, windowIndex, windowName, paneIndex, paneId, windowActive, paneActive, windowLayout] = parts;
|
|
2162
|
+
if (paneId.length === 0) return void 0;
|
|
2163
|
+
return {
|
|
2164
|
+
sessionName,
|
|
2165
|
+
windowIndex,
|
|
2166
|
+
windowName,
|
|
2167
|
+
paneIndex,
|
|
2168
|
+
paneId,
|
|
2169
|
+
windowActive,
|
|
2170
|
+
paneActive,
|
|
2171
|
+
windowLayout
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Render the stable tmux state block: the part of a reading compared for
|
|
2176
|
+
* change suppression. It excludes the turn preamble so re-injection is driven
|
|
2177
|
+
* only by tmux state, not by loop position.
|
|
2178
|
+
*/
|
|
2179
|
+
function renderState(location) {
|
|
2180
|
+
return `session ${location.sessionName}, window ${location.windowIndex} ${JSON.stringify(location.windowName)}, pane ${location.paneIndex} ${location.paneId}\nwindow active=${location.windowActive}, pane active=${location.paneActive}, layout ${location.windowLayout}`;
|
|
2181
|
+
}
|
|
2182
|
+
/** Render the full durable reading, including the volatile turn preamble. */
|
|
2183
|
+
function renderReading(location, turn) {
|
|
2184
|
+
return `${READING_PREFIX}${turn}):\n${renderState(location)}`;
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* The stable state block of this plugin's latest durable injection, or
|
|
2188
|
+
* `undefined` when the session has none. Scans raw durable events so the
|
|
2189
|
+
* schedule survives compaction and resumed processes without process-local
|
|
2190
|
+
* cache state.
|
|
2191
|
+
*/
|
|
2192
|
+
/** Reject refresh intervals that cannot represent an exact elapsed-millisecond threshold. */
|
|
2193
|
+
function validateRefreshInterval(refreshIntervalMs) {
|
|
2194
|
+
if (refreshIntervalMs !== void 0 && (!Number.isSafeInteger(refreshIntervalMs) || refreshIntervalMs < 0)) throw new TypeError(`tmux-context: refreshIntervalMs must be a non-negative safe integer, got ${String(refreshIntervalMs)}`);
|
|
2195
|
+
}
|
|
2196
|
+
const tmuxContextStateSchema = z$1.object({
|
|
2197
|
+
state: z$1.string(),
|
|
2198
|
+
time: z$1.number()
|
|
2199
|
+
}).nullable();
|
|
2200
|
+
function apply(ctx, config) {
|
|
2201
|
+
const refreshIntervalMs = config.refreshIntervalMs;
|
|
2202
|
+
validateRefreshInterval(refreshIntervalMs);
|
|
2203
|
+
ctx.sessionProjections.register({
|
|
2204
|
+
key: "tmuxContext",
|
|
2205
|
+
stateVersion: 1,
|
|
2206
|
+
stateSchema: tmuxContextStateSchema,
|
|
2207
|
+
init: () => null,
|
|
2208
|
+
apply: (state, event) => {
|
|
2209
|
+
if (event.type !== "user/message" || event.data.source.kind !== "tmux-context") return state;
|
|
2210
|
+
const [block] = event.data.content;
|
|
2211
|
+
if (block?.type !== "text") return state;
|
|
2212
|
+
const newline = block.text.indexOf("\n");
|
|
2213
|
+
return {
|
|
2214
|
+
state: newline === -1 ? "" : block.text.slice(newline + 1),
|
|
2215
|
+
time: event.time
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2219
|
+
ctx.on("agent/pre-step", async ({ agent, turn, step, signal }, next) => {
|
|
2220
|
+
const decision = await next();
|
|
2221
|
+
if (decision.kind === "reject" || signal.aborted || step !== 1) return decision;
|
|
2222
|
+
const bash = ctx.get("shell");
|
|
2223
|
+
if (bash === void 0) return decision;
|
|
2224
|
+
const previous = ctx.sessionProjections.stateOf(agent.session, "tmuxContext");
|
|
2225
|
+
if (refreshIntervalMs !== void 0 && refreshIntervalMs > 0 && previous !== null) {
|
|
2226
|
+
const now = Date.now();
|
|
2227
|
+
if (now >= previous.time && now - previous.time < refreshIntervalMs) return decision;
|
|
2228
|
+
}
|
|
2229
|
+
const location = await queryTmuxLocation(bash, ctx.logger, process.pid, signal);
|
|
2230
|
+
if (location === void 0) return decision;
|
|
2231
|
+
const state = renderState(location);
|
|
2232
|
+
if (previous !== null && previous.state === state) return decision;
|
|
2233
|
+
const text = renderReading(location, turn);
|
|
2234
|
+
return {
|
|
2235
|
+
...decision,
|
|
2236
|
+
messages: [createUserMessage({
|
|
2237
|
+
content: [{
|
|
2238
|
+
type: "text",
|
|
2239
|
+
text
|
|
2240
|
+
}],
|
|
2241
|
+
source: {
|
|
2242
|
+
kind: name,
|
|
2243
|
+
form: "snapshot",
|
|
2244
|
+
sections: [{
|
|
2245
|
+
name,
|
|
2246
|
+
text
|
|
2247
|
+
}]
|
|
2248
|
+
}
|
|
2249
|
+
}), ...decision.messages]
|
|
2250
|
+
};
|
|
2251
|
+
}, { prepend: true });
|
|
2252
|
+
}
|
|
2253
|
+
//#endregion
|
|
2254
|
+
export { Config, apply, inject, name };
|