@orkestrel/agent 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -13
- package/dist/src/core/index.cjs +973 -36
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +830 -26
- package/dist/src/core/index.d.ts +830 -26
- package/dist/src/core/index.js +948 -39
- package/dist/src/core/index.js.map +1 -1
- package/package.json +9 -7
package/dist/src/core/index.cjs
CHANGED
|
@@ -96,6 +96,56 @@ var MESSAGE_TOKEN_OVERHEAD = 4;
|
|
|
96
96
|
* exact count.
|
|
97
97
|
*/
|
|
98
98
|
var IMAGE_TOKEN_ESTIMATE = 512;
|
|
99
|
+
/**
|
|
100
|
+
* Holds the default provider deadline in milliseconds — `120_000`, the wall-clock bound a call
|
|
101
|
+
* runs under when `AgentProviderInput.timeout` is omitted, folded with the caller's signal so
|
|
102
|
+
* whichever trips first cancels the call.
|
|
103
|
+
*/
|
|
104
|
+
var DEFAULT_PROVIDER_TIMEOUT = 12e4;
|
|
105
|
+
/**
|
|
106
|
+
* Bounds the decoded error excerpt's input in bytes — `2048`, the leading bytes of a non-OK
|
|
107
|
+
* response body handed to the decoder before the read cancels the remainder, so a `ProviderError`
|
|
108
|
+
* message never carries a longer excerpt.
|
|
109
|
+
*/
|
|
110
|
+
var MAX_ERROR_BODY_LENGTH = 2048;
|
|
111
|
+
/**
|
|
112
|
+
* Holds the default relay request limit in bytes — `1_048_576`, the byte budget a relay applies
|
|
113
|
+
* to an inbound body when `RelayOptions.limit` is omitted, refusing a body that reaches it.
|
|
114
|
+
*/
|
|
115
|
+
var DEFAULT_RELAY_LIMIT = 1048576;
|
|
116
|
+
/**
|
|
117
|
+
* Names the relay's newline-delimited JSON content type — `'application/x-ndjson; charset=utf-8'`,
|
|
118
|
+
* the header a relay response carries beside `cache-control: no-store`.
|
|
119
|
+
*/
|
|
120
|
+
var RELAY_CONTENT_TYPE = "application/x-ndjson; charset=utf-8";
|
|
121
|
+
/**
|
|
122
|
+
* Names the public message for an unexpected upstream relay failure — `'relay provider failed'`,
|
|
123
|
+
* the fixed text every `error` frame carries, so an upstream failure's own message never reaches
|
|
124
|
+
* the browser.
|
|
125
|
+
*/
|
|
126
|
+
var RELAY_PROVIDER_MESSAGE = "relay provider failed";
|
|
127
|
+
/**
|
|
128
|
+
* Names the status a relay answers when the `authorize` callback returns anything but `true` or throws —
|
|
129
|
+
* `401`, carried with no body and reaching the browser as a `ProviderError` with the `HTTP` code.
|
|
130
|
+
*/
|
|
131
|
+
var UNAUTHORIZED_RELAY_STATUS = 401;
|
|
132
|
+
/**
|
|
133
|
+
* Names the status a relay answers when the upstream provider call cannot be constructed — `502`,
|
|
134
|
+
* carried with no body after `provider.stream` was entered and threw before returning its
|
|
135
|
+
* iterator.
|
|
136
|
+
*/
|
|
137
|
+
var UPSTREAM_RELAY_STATUS = 502;
|
|
138
|
+
/**
|
|
139
|
+
* Names the status a relay answers for a body that is missing, unreadable, or rejected by
|
|
140
|
+
* `providerRequestContract` — `400`, carried with no body and reaching the browser as a
|
|
141
|
+
* `ProviderError` with the `HTTP` code.
|
|
142
|
+
*/
|
|
143
|
+
var INVALID_RELAY_STATUS = 400;
|
|
144
|
+
/**
|
|
145
|
+
* Names the status a relay answers for a request body at or above its byte budget — `413`,
|
|
146
|
+
* carried with no body and answered for an aborted inbound read as well.
|
|
147
|
+
*/
|
|
148
|
+
var OVERSIZED_RELAY_STATUS = 413;
|
|
99
149
|
//#endregion
|
|
100
150
|
//#region src/core/errors.ts
|
|
101
151
|
/**
|
|
@@ -107,14 +157,16 @@ var IMAGE_TOKEN_ESTIMATE = 512;
|
|
|
107
157
|
* Lets a caller recover the partial content (and any tool calls / usage seen so far)
|
|
108
158
|
* on cancellation: `catch` the throw, narrow with {@link isProviderAbortError}, and
|
|
109
159
|
* read `partial`. `code` is the machine-readable condition (`'ABORT'` — the only one this
|
|
110
|
-
* error reports), so a `catch` branches on it rather than on the message string.
|
|
160
|
+
* error reports), so a `catch` branches on it rather than on the message string. `cause`
|
|
161
|
+
* holds the failure the cancel superseded when a throw raced the abort — the wire decoder's
|
|
162
|
+
* {@link ProviderError}, say — and is undefined when the cancel was the only failure.
|
|
111
163
|
*/
|
|
112
164
|
var ProviderAbortError = class extends Error {
|
|
113
165
|
/** Names the machine-readable condition — `'ABORT'`: a stream cancelled mid-flight. */
|
|
114
166
|
code = "ABORT";
|
|
115
167
|
partial;
|
|
116
|
-
constructor(partial) {
|
|
117
|
-
super("provider stream aborted");
|
|
168
|
+
constructor(partial, options) {
|
|
169
|
+
super("provider stream aborted", options);
|
|
118
170
|
this.name = "ProviderAbortError";
|
|
119
171
|
this.partial = partial;
|
|
120
172
|
}
|
|
@@ -275,6 +327,142 @@ var AgentError = class extends Error {
|
|
|
275
327
|
function isAgentError(value) {
|
|
276
328
|
return value instanceof AgentError;
|
|
277
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Reports a coded provider failure with its HTTP status and underlying cause when available.
|
|
332
|
+
*
|
|
333
|
+
* @remarks
|
|
334
|
+
* The provider base throws this error for a non-OK HTTP response, a missing response
|
|
335
|
+
* body, or a missing settled result when strict assembly is enabled. Concrete wire
|
|
336
|
+
* decoders also use it for malformed records and relayed provider failures.
|
|
337
|
+
* `status` is present only for an `HTTP` failure; other codes leave it undefined.
|
|
338
|
+
*/
|
|
339
|
+
var ProviderError = class extends Error {
|
|
340
|
+
/** Names the machine-readable condition — `'HTTP'`: a non-OK response, including a relay refusing an oversized request with 413; `'PROTOCOL'`: a missing response body, a malformed wire record, or a strict stream with no settled result; `'PROVIDER'`: an upstream failure carried by a relay error record. */
|
|
341
|
+
code;
|
|
342
|
+
/** Holds the response status for an HTTP failure, or undefined for other codes. */
|
|
343
|
+
status;
|
|
344
|
+
constructor(code, message, options) {
|
|
345
|
+
super(message, options);
|
|
346
|
+
this.name = "ProviderError";
|
|
347
|
+
this.code = code;
|
|
348
|
+
this.status = options?.status;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* Narrows a caught value to the provider failure class through instanceof.
|
|
353
|
+
*
|
|
354
|
+
* @param value - The caught value
|
|
355
|
+
* @returns True if the value is a provider failure; false otherwise
|
|
356
|
+
* @example
|
|
357
|
+
* ```ts
|
|
358
|
+
* isProviderError(new ProviderError('HTTP', 'unavailable', { status: 503 })) // true
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
function isProviderError(value) {
|
|
362
|
+
return value instanceof ProviderError;
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/core/shapers.ts
|
|
366
|
+
/**
|
|
367
|
+
* Describes a tool call's JSON wire projection.
|
|
368
|
+
*
|
|
369
|
+
* @remarks
|
|
370
|
+
* The wire is strictly narrower than the domain: non-JSON arguments are refused.
|
|
371
|
+
* Calls carry only id, name, and arguments; execution context stays local.
|
|
372
|
+
* The guard refuses every extra member; the contract parser drops extra members.
|
|
373
|
+
*/
|
|
374
|
+
var toolCallShape = (0, _orkestrel_contract.objectShape)({
|
|
375
|
+
id: (0, _orkestrel_contract.stringShape)(),
|
|
376
|
+
name: (0, _orkestrel_contract.stringShape)(),
|
|
377
|
+
arguments: (0, _orkestrel_contract.recordShape)((0, _orkestrel_contract.jsonShape)())
|
|
378
|
+
});
|
|
379
|
+
/**
|
|
380
|
+
* Describes a conversation message's JSON wire projection.
|
|
381
|
+
*
|
|
382
|
+
* @remarks
|
|
383
|
+
* The wire is strictly narrower than Message: non-JSON call arguments are refused,
|
|
384
|
+
* and execution context stays local. The guard refuses extra members; the parser drops them.
|
|
385
|
+
*/
|
|
386
|
+
var messageShape = (0, _orkestrel_contract.objectShape)({
|
|
387
|
+
id: (0, _orkestrel_contract.stringShape)(),
|
|
388
|
+
role: (0, _orkestrel_contract.literalShape)([
|
|
389
|
+
"system",
|
|
390
|
+
"user",
|
|
391
|
+
"assistant",
|
|
392
|
+
"tool"
|
|
393
|
+
]),
|
|
394
|
+
content: (0, _orkestrel_contract.stringShape)(),
|
|
395
|
+
calls: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)(toolCallShape)),
|
|
396
|
+
images: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)()))
|
|
397
|
+
});
|
|
398
|
+
/**
|
|
399
|
+
* Describes a provider request's JSON wire projection.
|
|
400
|
+
*
|
|
401
|
+
* @remarks
|
|
402
|
+
* The wire is strictly narrower than ProviderRequest: non-JSON arguments, parameters,
|
|
403
|
+
* or schema members are refused. Execution context stays local; the guard refuses
|
|
404
|
+
* extra members, and the parser drops them.
|
|
405
|
+
*/
|
|
406
|
+
var providerRequestShape = (0, _orkestrel_contract.objectShape)({
|
|
407
|
+
messages: (0, _orkestrel_contract.arrayShape)(messageShape),
|
|
408
|
+
tools: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
|
|
409
|
+
name: (0, _orkestrel_contract.stringShape)(),
|
|
410
|
+
description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
|
|
411
|
+
parameters: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.recordShape)((0, _orkestrel_contract.jsonShape)()))
|
|
412
|
+
}))),
|
|
413
|
+
options: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.objectShape)({
|
|
414
|
+
think: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)()),
|
|
415
|
+
schema: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.recordShape)((0, _orkestrel_contract.jsonShape)()))
|
|
416
|
+
}))
|
|
417
|
+
});
|
|
418
|
+
/**
|
|
419
|
+
* Describes a provider result's JSON wire projection.
|
|
420
|
+
*
|
|
421
|
+
* @remarks
|
|
422
|
+
* The wire is strictly narrower than ProviderResult: non-JSON call arguments are
|
|
423
|
+
* refused. Execution context stays local; the guard refuses extra members, and the parser drops them.
|
|
424
|
+
*/
|
|
425
|
+
var providerResultShape = (0, _orkestrel_contract.objectShape)({
|
|
426
|
+
content: (0, _orkestrel_contract.stringShape)(),
|
|
427
|
+
thinking: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
|
|
428
|
+
tools: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)(toolCallShape)),
|
|
429
|
+
usage: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.objectShape)({
|
|
430
|
+
prompt: (0, _orkestrel_contract.numberShape)(),
|
|
431
|
+
completion: (0, _orkestrel_contract.numberShape)(),
|
|
432
|
+
total: (0, _orkestrel_contract.numberShape)()
|
|
433
|
+
}))
|
|
434
|
+
});
|
|
435
|
+
/**
|
|
436
|
+
* Describes the channel-discriminated JSON relay wire projection.
|
|
437
|
+
*
|
|
438
|
+
* @remarks
|
|
439
|
+
* The wire is strictly narrower than RelayFrame through its result and partial
|
|
440
|
+
* fields: non-JSON arguments are refused. Execution context stays local; the guard
|
|
441
|
+
* refuses extra members, and the parser drops them.
|
|
442
|
+
*/
|
|
443
|
+
var relayFrameShape = (0, _orkestrel_contract.unionShape)((0, _orkestrel_contract.objectShape)({
|
|
444
|
+
channel: (0, _orkestrel_contract.literalShape)(["content", "thinking"]),
|
|
445
|
+
text: (0, _orkestrel_contract.stringShape)()
|
|
446
|
+
}), (0, _orkestrel_contract.objectShape)({
|
|
447
|
+
channel: (0, _orkestrel_contract.literalShape)(["result"]),
|
|
448
|
+
result: providerResultShape
|
|
449
|
+
}), (0, _orkestrel_contract.objectShape)({
|
|
450
|
+
channel: (0, _orkestrel_contract.literalShape)(["abort"]),
|
|
451
|
+
partial: providerResultShape
|
|
452
|
+
}), (0, _orkestrel_contract.objectShape)({
|
|
453
|
+
channel: (0, _orkestrel_contract.literalShape)(["error"]),
|
|
454
|
+
message: (0, _orkestrel_contract.stringShape)()
|
|
455
|
+
}));
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/core/contracts.ts
|
|
458
|
+
/** Validates and projects conversation messages at a JSON wire boundary. */
|
|
459
|
+
var messageContract = (0, _orkestrel_contract.createContract)(messageShape);
|
|
460
|
+
/** Validates and projects provider requests at a JSON wire boundary. */
|
|
461
|
+
var providerRequestContract = (0, _orkestrel_contract.createContract)(providerRequestShape);
|
|
462
|
+
/** Validates and projects provider results at a JSON wire boundary. */
|
|
463
|
+
var providerResultContract = (0, _orkestrel_contract.createContract)(providerResultShape);
|
|
464
|
+
/** Validates and projects channel-discriminated relay frames at a JSON wire boundary. */
|
|
465
|
+
var relayFrameContract = (0, _orkestrel_contract.createContract)(relayFrameShape);
|
|
278
466
|
//#endregion
|
|
279
467
|
//#region src/core/helpers.ts
|
|
280
468
|
/**
|
|
@@ -582,11 +770,13 @@ function sanitizeUsage(usage) {
|
|
|
582
770
|
* @example
|
|
583
771
|
* ```ts
|
|
584
772
|
* joinThinking(undefined, 'first') // 'first'
|
|
773
|
+
* joinThinking('', 'x') // 'x'
|
|
585
774
|
* joinThinking('first', 'second') // 'first\n\nsecond'
|
|
586
775
|
* ```
|
|
587
776
|
*/
|
|
588
777
|
function joinThinking(running, next) {
|
|
589
|
-
|
|
778
|
+
if (running === void 0 || running.length === 0) return next;
|
|
779
|
+
return next.length === 0 ? running : `${running}\n\n${next}`;
|
|
590
780
|
}
|
|
591
781
|
/**
|
|
592
782
|
* Adds two {@link TokenUsage} values field by field — the running total an agent run keeps
|
|
@@ -943,6 +1133,133 @@ function intersectKeys(parent, child) {
|
|
|
943
1133
|
const allowed = new Set(parent);
|
|
944
1134
|
return child.filter((key) => allowed.has(key));
|
|
945
1135
|
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Assembles a provider result with only populated optional fields.
|
|
1138
|
+
*
|
|
1139
|
+
* @param content - The authoritative answer
|
|
1140
|
+
* @param thinking - The joined reasoning
|
|
1141
|
+
* @param tools - The accumulated calls
|
|
1142
|
+
* @param usage - The reported token usage
|
|
1143
|
+
* @returns The assembled result
|
|
1144
|
+
* @example
|
|
1145
|
+
* ```ts
|
|
1146
|
+
* buildProviderResult('ok', '', [], undefined) // { content: 'ok' }
|
|
1147
|
+
* ```
|
|
1148
|
+
*/
|
|
1149
|
+
function buildProviderResult(content, thinking, tools, usage) {
|
|
1150
|
+
return {
|
|
1151
|
+
content,
|
|
1152
|
+
...thinking.length === 0 ? {} : { thinking },
|
|
1153
|
+
...tools.length === 0 ? {} : { tools },
|
|
1154
|
+
...usage === void 0 ? {} : { usage }
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Reads a UTF-8 prefix of a byte stream and cancels its remainder.
|
|
1159
|
+
*
|
|
1160
|
+
* @remarks
|
|
1161
|
+
* The `limit` parameter bounds bytes passed to the decoder, including a partial final
|
|
1162
|
+
* character. An omitted limit reads to completion. A source may deliver a chunk larger
|
|
1163
|
+
* than the remaining limit; its unused bytes are discarded without decoding.
|
|
1164
|
+
* The read can overshoot by one source chunk. An abort leaves completion false.
|
|
1165
|
+
*
|
|
1166
|
+
* @param body - The readable byte stream
|
|
1167
|
+
* @param limit - The maximum byte prefix, or undefined for the complete stream
|
|
1168
|
+
* @param signal - The optional cancellation bound; abort returns the decoded prefix
|
|
1169
|
+
* @returns The decoded prefix and whether EOF occurred within the byte budget
|
|
1170
|
+
* @example
|
|
1171
|
+
* ```ts
|
|
1172
|
+
* import { readText } from '@orkestrel/agent'
|
|
1173
|
+
*
|
|
1174
|
+
* const body = new Response('answer').body
|
|
1175
|
+
* if (body !== null) await readText(body, 3) // { text: 'ans', complete: false }
|
|
1176
|
+
* ```
|
|
1177
|
+
*/
|
|
1178
|
+
async function readText(body, limit, signal) {
|
|
1179
|
+
const reader = body.getReader();
|
|
1180
|
+
const decoder = new TextDecoder();
|
|
1181
|
+
const cleanup = new AbortController();
|
|
1182
|
+
let remaining = limit ?? Infinity;
|
|
1183
|
+
let text = "";
|
|
1184
|
+
let complete = false;
|
|
1185
|
+
try {
|
|
1186
|
+
signal?.addEventListener("abort", () => {
|
|
1187
|
+
reader.cancel(signal.reason).catch(() => {});
|
|
1188
|
+
}, {
|
|
1189
|
+
once: true,
|
|
1190
|
+
signal: cleanup.signal
|
|
1191
|
+
});
|
|
1192
|
+
if (signal?.aborted) await reader.cancel(signal.reason).catch(() => {});
|
|
1193
|
+
while (remaining > 0) {
|
|
1194
|
+
if (signal?.aborted) break;
|
|
1195
|
+
const step = await reader.read();
|
|
1196
|
+
if (signal?.aborted) break;
|
|
1197
|
+
if (step.done) {
|
|
1198
|
+
complete = true;
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
const bytes = step.value.subarray(0, remaining);
|
|
1202
|
+
text += decoder.decode(bytes, { stream: true });
|
|
1203
|
+
remaining -= bytes.byteLength;
|
|
1204
|
+
}
|
|
1205
|
+
return {
|
|
1206
|
+
text: text + decoder.decode(),
|
|
1207
|
+
complete
|
|
1208
|
+
};
|
|
1209
|
+
} finally {
|
|
1210
|
+
cleanup.abort();
|
|
1211
|
+
try {
|
|
1212
|
+
await reader.cancel();
|
|
1213
|
+
} catch {} finally {
|
|
1214
|
+
reader.releaseLock();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Decodes UTF-8 chunks with a final flush and releases the stream on every exit.
|
|
1220
|
+
*
|
|
1221
|
+
* @param body - The readable byte stream
|
|
1222
|
+
* @param signal - The optional cancellation bound; abort ends iteration without further chunks
|
|
1223
|
+
* @returns Decoded text chunks, including a held decoder tail
|
|
1224
|
+
* @example
|
|
1225
|
+
* ```ts
|
|
1226
|
+
* const body = new Response('answer').body
|
|
1227
|
+
* if (body !== null) for await (const chunk of readChunks(body)) render(chunk)
|
|
1228
|
+
* ```
|
|
1229
|
+
*/
|
|
1230
|
+
async function* readChunks(body, signal) {
|
|
1231
|
+
const reader = body.getReader();
|
|
1232
|
+
const decoder = new TextDecoder();
|
|
1233
|
+
const cleanup = new AbortController();
|
|
1234
|
+
try {
|
|
1235
|
+
signal?.addEventListener("abort", () => {
|
|
1236
|
+
reader.cancel(signal.reason).catch(() => {});
|
|
1237
|
+
}, {
|
|
1238
|
+
once: true,
|
|
1239
|
+
signal: cleanup.signal
|
|
1240
|
+
});
|
|
1241
|
+
if (signal?.aborted) {
|
|
1242
|
+
await reader.cancel(signal.reason).catch(() => {});
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
for (;;) {
|
|
1246
|
+
const step = await reader.read();
|
|
1247
|
+
if (signal?.aborted) return;
|
|
1248
|
+
if (step.done) break;
|
|
1249
|
+
const chunk = decoder.decode(step.value, { stream: true });
|
|
1250
|
+
if (chunk.length > 0) yield chunk;
|
|
1251
|
+
}
|
|
1252
|
+
const tail = decoder.decode();
|
|
1253
|
+
if (tail.length > 0) yield tail;
|
|
1254
|
+
} finally {
|
|
1255
|
+
cleanup.abort();
|
|
1256
|
+
try {
|
|
1257
|
+
await reader.cancel();
|
|
1258
|
+
} catch {} finally {
|
|
1259
|
+
reader.releaseLock();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
946
1263
|
//#endregion
|
|
947
1264
|
//#region src/core/conversations/Conversation.ts
|
|
948
1265
|
/**
|
|
@@ -1815,7 +2132,7 @@ var Agent = class {
|
|
|
1815
2132
|
} catch (error) {
|
|
1816
2133
|
if (abort.signal.aborted) {
|
|
1817
2134
|
if (isProviderAbortError(error)) {
|
|
1818
|
-
if (error.partial.thinking !== void 0) thinking = joinThinking(thinking, error.partial.thinking);
|
|
2135
|
+
if (error.partial.thinking !== void 0 && error.partial.thinking.length > 0) thinking = joinThinking(thinking, error.partial.thinking);
|
|
1819
2136
|
if (error.partial.usage !== void 0) {
|
|
1820
2137
|
const abortUsage = sanitizeUsage(error.partial.usage);
|
|
1821
2138
|
budget?.consume({
|
|
@@ -1848,13 +2165,19 @@ var Agent = class {
|
|
|
1848
2165
|
};
|
|
1849
2166
|
}
|
|
1850
2167
|
if (result.tools !== void 0 && result.tools.length > 0) {
|
|
2168
|
+
const authorization = this.#authorize(result.tools);
|
|
2169
|
+
if (abort.signal.aborted) {
|
|
2170
|
+
partial = true;
|
|
2171
|
+
broke = true;
|
|
2172
|
+
break;
|
|
2173
|
+
}
|
|
1851
2174
|
const assistant = this.#context.messages.add({
|
|
1852
2175
|
role: "assistant",
|
|
1853
2176
|
content: result.content,
|
|
1854
2177
|
calls: result.tools
|
|
1855
2178
|
});
|
|
1856
2179
|
messages.push(assistant);
|
|
1857
|
-
const results = await this.#
|
|
2180
|
+
const results = await this.#dispatch(tools, result.tools, abort.signal, authorization);
|
|
1858
2181
|
for (let index = 0; index < result.tools.length; index += 1) {
|
|
1859
2182
|
const call = result.tools[index];
|
|
1860
2183
|
const outcomeResult = results[index];
|
|
@@ -1914,8 +2237,8 @@ var Agent = class {
|
|
|
1914
2237
|
messages.splice(0, messages.length, ...this.#context.build(this.#provider.format));
|
|
1915
2238
|
return false;
|
|
1916
2239
|
}
|
|
1917
|
-
|
|
1918
|
-
if (this.#authority === void 0) return
|
|
2240
|
+
#authorize(calls) {
|
|
2241
|
+
if (this.#authority === void 0) return void 0;
|
|
1919
2242
|
const authority = this.#authority;
|
|
1920
2243
|
const allowed = [];
|
|
1921
2244
|
const denials = /* @__PURE__ */ new Map();
|
|
@@ -1935,7 +2258,15 @@ var Agent = class {
|
|
|
1935
2258
|
this.#emitter.emit("deny", call, decision.reason);
|
|
1936
2259
|
}
|
|
1937
2260
|
}
|
|
1938
|
-
|
|
2261
|
+
return {
|
|
2262
|
+
allowed,
|
|
2263
|
+
denials
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
async #dispatch(tools, calls, signal, authorization) {
|
|
2267
|
+
if (authorization === void 0) return tools.execute(calls, { signal });
|
|
2268
|
+
const { allowed, denials } = authorization;
|
|
2269
|
+
const executed = allowed.length > 0 ? await tools.execute(allowed, { signal }) : [];
|
|
1939
2270
|
const byId = new Map(denials);
|
|
1940
2271
|
for (const result of executed) byId.set(result.id, result);
|
|
1941
2272
|
return calls.map((call) => byId.get(call.id) ?? denyCall(call, void 0));
|
|
@@ -2126,41 +2457,32 @@ var Authority = class {
|
|
|
2126
2457
|
//#endregion
|
|
2127
2458
|
//#region src/core/validators.ts
|
|
2128
2459
|
/**
|
|
2129
|
-
* Checks whether
|
|
2130
|
-
* the {@link isConversationSnapshot} and {@link isSection} read-boundary narrows, where a present
|
|
2131
|
-
* `calls` must be an array of valid {@link import('@orkestrel/tool').ToolCall}s. Total, never
|
|
2132
|
-
* throwing, and never an assertion; the conversation analogue of
|
|
2133
|
-
* {@link import('@orkestrel/workspace').isFile}.
|
|
2460
|
+
* Checks whether a value satisfies the domain conversation-message contract.
|
|
2134
2461
|
*
|
|
2135
2462
|
* @remarks
|
|
2136
|
-
*
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
* {@link import('@orkestrel/tool').ToolCall} ({@link isToolCall} — the
|
|
2140
|
-
* fail-closed deepening: a tampered `calls` element rejects the message, so the snapshot
|
|
2141
|
-
* reads back as absent rather than replaying a malformed call) and an `images` that is an array
|
|
2142
|
-
* (an absent optional passes). The `role` is left as a broad `string` here (an open
|
|
2143
|
-
* {@link import('./types.js').MessageRole}, so any storage-read role string is accepted
|
|
2144
|
-
* defensively rather than rejected against the current literal set). Enough to safely impose the
|
|
2145
|
-
* {@link Message} type at
|
|
2146
|
-
* a storage boundary without a cast.
|
|
2147
|
-
*
|
|
2148
|
-
* @param value - The value to test (one element of a snapshot's `messages` / a section's `messages`)
|
|
2149
|
-
* @returns True if `value` has the structural shape of a {@link Message}; false otherwise
|
|
2463
|
+
* Roles belong to MessageRole and image elements are strings. Tool arguments may
|
|
2464
|
+
* carry non-JSON values, as the domain type permits; the message wire contract is
|
|
2465
|
+
* narrower. Unreadable fields and hostile inputs return false.
|
|
2150
2466
|
*
|
|
2467
|
+
* @param value - The unknown message candidate
|
|
2468
|
+
* @returns True if the domain message fields are valid; false otherwise
|
|
2151
2469
|
* @example
|
|
2152
2470
|
* ```ts
|
|
2153
2471
|
* isMessage({ id: '1', role: 'user', content: 'hi' }) // true
|
|
2154
|
-
* isMessage({ id: '1', role: '
|
|
2155
|
-
* isMessage({ id: '1', role: 'user' }) // false
|
|
2156
|
-
* isMessage({ id: '1', role: 'assistant', content: '', calls: [null] }) // false (malformed call)
|
|
2472
|
+
* isMessage({ id: '1', role: 'other', content: '' }) // false
|
|
2473
|
+
* isMessage({ id: '1', role: 'user', content: '', images: [1] }) // false
|
|
2157
2474
|
* ```
|
|
2158
2475
|
*/
|
|
2159
2476
|
function isMessage(value) {
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2477
|
+
const checked = (0, _orkestrel_contract.attempt)(() => {
|
|
2478
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
2479
|
+
const { id, role, content, calls, images } = value;
|
|
2480
|
+
if (!(0, _orkestrel_contract.isString)(id) || !(0, _orkestrel_contract.isString)(content)) return false;
|
|
2481
|
+
if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return false;
|
|
2482
|
+
if (calls !== void 0 && !(0, _orkestrel_contract.arrayOf)(_orkestrel_tool.isToolCall)(calls)) return false;
|
|
2483
|
+
return images === void 0 || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(images);
|
|
2484
|
+
});
|
|
2485
|
+
return checked.success && checked.value;
|
|
2164
2486
|
}
|
|
2165
2487
|
/**
|
|
2166
2488
|
* Checks whether an `unknown` is structurally a {@link Section} record — a `string` `id` and
|
|
@@ -2386,6 +2708,494 @@ var MemoryConversationStore = class {
|
|
|
2386
2708
|
}
|
|
2387
2709
|
};
|
|
2388
2710
|
//#endregion
|
|
2711
|
+
//#region src/core/AgentProvider.ts
|
|
2712
|
+
/**
|
|
2713
|
+
* Implements bounded HTTP streaming and result assembly behind concrete wire seams.
|
|
2714
|
+
*
|
|
2715
|
+
* @remarks
|
|
2716
|
+
* Every call owns its parser, splitter, deadline, and accumulation. Success bodies
|
|
2717
|
+
* have no size limit. A qwen3 implicit-open reclassification corrects the final
|
|
2718
|
+
* content while content deltas already yielded cannot be recalled. A subclass fills
|
|
2719
|
+
* `name`, `frame`, `body`, `read`, and `finish`; the constructor takes the `split`
|
|
2720
|
+
* and `strict` switches to control reasoning separation and settled-result requirements.
|
|
2721
|
+
*
|
|
2722
|
+
* @example Writing a provider for a new wire
|
|
2723
|
+
* ```ts
|
|
2724
|
+
* import type {
|
|
2725
|
+
* ProviderIncrement,
|
|
2726
|
+
* ProviderOptions,
|
|
2727
|
+
* ProviderParserInterface,
|
|
2728
|
+
* ProviderRequest,
|
|
2729
|
+
* } from '@orkestrel/agent'
|
|
2730
|
+
* import { AgentProvider } from '@orkestrel/agent'
|
|
2731
|
+
*
|
|
2732
|
+
* class TextFrame implements ProviderParserInterface<string> {
|
|
2733
|
+
* parse(chunk: string): readonly string[] {
|
|
2734
|
+
* return [chunk]
|
|
2735
|
+
* }
|
|
2736
|
+
* clear(): void {} // Raw text retains no framing state.
|
|
2737
|
+
* }
|
|
2738
|
+
*
|
|
2739
|
+
* interface TextOptions extends ProviderOptions {
|
|
2740
|
+
* readonly url: string
|
|
2741
|
+
* }
|
|
2742
|
+
*
|
|
2743
|
+
* class TextProvider extends AgentProvider<string> {
|
|
2744
|
+
* readonly name = 'text'
|
|
2745
|
+
* constructor(options: TextOptions) {
|
|
2746
|
+
* super({ ...options, path: '/generate' })
|
|
2747
|
+
* }
|
|
2748
|
+
* frame(): ProviderParserInterface<string> {
|
|
2749
|
+
* return new TextFrame()
|
|
2750
|
+
* }
|
|
2751
|
+
* body(request: ProviderRequest): object {
|
|
2752
|
+
* return { messages: request.messages }
|
|
2753
|
+
* }
|
|
2754
|
+
* read(record: string): ProviderIncrement {
|
|
2755
|
+
* return { content: record, thinking: '', tools: [] }
|
|
2756
|
+
* }
|
|
2757
|
+
* finish(_parser: ProviderParserInterface<string>): readonly string[] {
|
|
2758
|
+
* return [] // Raw text retains no records at end of input.
|
|
2759
|
+
* }
|
|
2760
|
+
* }
|
|
2761
|
+
* ```
|
|
2762
|
+
*/
|
|
2763
|
+
var AgentProvider = class {
|
|
2764
|
+
#id;
|
|
2765
|
+
#url;
|
|
2766
|
+
#path;
|
|
2767
|
+
#timeout;
|
|
2768
|
+
#transport;
|
|
2769
|
+
#headers;
|
|
2770
|
+
#format;
|
|
2771
|
+
#split;
|
|
2772
|
+
#strict;
|
|
2773
|
+
constructor(input) {
|
|
2774
|
+
this.#id = crypto.randomUUID();
|
|
2775
|
+
this.#url = input.url;
|
|
2776
|
+
this.#path = input.path ?? "";
|
|
2777
|
+
this.#timeout = input.timeout ?? 12e4;
|
|
2778
|
+
this.#transport = input.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2779
|
+
this.#headers = input.headers;
|
|
2780
|
+
this.#format = input.format;
|
|
2781
|
+
this.#split = input.split ?? true;
|
|
2782
|
+
this.#strict = input.strict ?? false;
|
|
2783
|
+
}
|
|
2784
|
+
/** Exposes the instance's minted UUID. */
|
|
2785
|
+
get id() {
|
|
2786
|
+
return this.#id;
|
|
2787
|
+
}
|
|
2788
|
+
/** Exposes the context framing exactly as supplied. */
|
|
2789
|
+
get format() {
|
|
2790
|
+
return this.#format;
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Generates a complete turn by draining the shared stream engine.
|
|
2794
|
+
*
|
|
2795
|
+
* @param messages - The conversation turns
|
|
2796
|
+
* @param signal - The caller's cancellation bound
|
|
2797
|
+
* @param tools - The advertised tool definitions
|
|
2798
|
+
* @param options - The per-call generation configuration
|
|
2799
|
+
* @returns The terminal stream result
|
|
2800
|
+
*/
|
|
2801
|
+
async generate(messages, signal, tools, options) {
|
|
2802
|
+
const stream = this.stream(messages, signal, tools, options);
|
|
2803
|
+
let step = await stream.next();
|
|
2804
|
+
while (!step.done) step = await stream.next();
|
|
2805
|
+
return step.value;
|
|
2806
|
+
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Streams decoded deltas and returns the authoritative or assembled turn result.
|
|
2809
|
+
*
|
|
2810
|
+
* @param messages - The conversation turns
|
|
2811
|
+
* @param signal - The caller's cancellation bound
|
|
2812
|
+
* @param tools - The advertised tool definitions
|
|
2813
|
+
* @param options - The per-call generation configuration
|
|
2814
|
+
* @returns Content and native thinking deltas followed by the settled result
|
|
2815
|
+
* @throws ProviderAbortError Thrown when the combined cancellation bound fires
|
|
2816
|
+
* @throws ProviderError Thrown for an HTTP or protocol failure
|
|
2817
|
+
*/
|
|
2818
|
+
async *stream(messages, signal, tools, options) {
|
|
2819
|
+
if (signal.aborted) throw new ProviderAbortError({ content: "" });
|
|
2820
|
+
const timeout = new _orkestrel_timeout.Timeout({ ms: this.#timeout });
|
|
2821
|
+
timeout.start();
|
|
2822
|
+
const combined = AbortSignal.any([timeout.signal, signal]);
|
|
2823
|
+
let parser;
|
|
2824
|
+
let splitter;
|
|
2825
|
+
let state = {
|
|
2826
|
+
content: "",
|
|
2827
|
+
thinking: "",
|
|
2828
|
+
tools: []
|
|
2829
|
+
};
|
|
2830
|
+
try {
|
|
2831
|
+
parser = this.frame();
|
|
2832
|
+
splitter = this.#split ? createThinkSplitter() : void 0;
|
|
2833
|
+
const response = await this.#request({
|
|
2834
|
+
messages,
|
|
2835
|
+
...tools === void 0 ? {} : { tools },
|
|
2836
|
+
...options === void 0 ? {} : { options }
|
|
2837
|
+
}, combined);
|
|
2838
|
+
if (response.body === null) throw new ProviderError("PROTOCOL", "provider error: no response body");
|
|
2839
|
+
for await (const chunk of readChunks(response.body, combined)) {
|
|
2840
|
+
combined.throwIfAborted();
|
|
2841
|
+
for (const record of parser.parse(chunk)) {
|
|
2842
|
+
yield* this.#fold(record, state, splitter, combined, (increment) => {
|
|
2843
|
+
state = increment;
|
|
2844
|
+
});
|
|
2845
|
+
combined.throwIfAborted();
|
|
2846
|
+
if (state.result !== void 0) return state.result;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
combined.throwIfAborted();
|
|
2850
|
+
for (const record of this.finish(parser)) {
|
|
2851
|
+
yield* this.#fold(record, state, splitter, combined, (increment) => {
|
|
2852
|
+
state = increment;
|
|
2853
|
+
});
|
|
2854
|
+
combined.throwIfAborted();
|
|
2855
|
+
if (state.result !== void 0) return state.result;
|
|
2856
|
+
}
|
|
2857
|
+
const tail = splitter?.flush() ?? "";
|
|
2858
|
+
if (tail.length > 0) yield {
|
|
2859
|
+
channel: "content",
|
|
2860
|
+
text: tail
|
|
2861
|
+
};
|
|
2862
|
+
combined.throwIfAborted();
|
|
2863
|
+
if (this.#strict) throw new ProviderError("PROTOCOL", "provider error: missing settled result");
|
|
2864
|
+
return buildProviderResult(splitter?.content ?? state.content, joinThinking(splitter?.thinking, state.thinking), state.tools, state.usage);
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
if (combined.aborted) {
|
|
2867
|
+
splitter?.flush();
|
|
2868
|
+
throw new ProviderAbortError(buildProviderResult(splitter?.content ?? state.content, joinThinking(splitter?.thinking, state.thinking), state.tools, state.usage), error === combined.reason ? void 0 : { cause: error });
|
|
2869
|
+
}
|
|
2870
|
+
throw error;
|
|
2871
|
+
} finally {
|
|
2872
|
+
try {
|
|
2873
|
+
parser?.clear();
|
|
2874
|
+
} finally {
|
|
2875
|
+
timeout.clear();
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
*#fold(record, previous, splitter, signal, commit) {
|
|
2880
|
+
const increment = this.read(record);
|
|
2881
|
+
if (increment.result !== void 0) {
|
|
2882
|
+
commit(increment);
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const content = splitter?.split(increment.content) ?? increment.content;
|
|
2886
|
+
const usage = increment.usage ?? previous.usage;
|
|
2887
|
+
commit({
|
|
2888
|
+
content: previous.content + increment.content,
|
|
2889
|
+
thinking: previous.thinking + increment.thinking,
|
|
2890
|
+
tools: [...previous.tools, ...increment.tools],
|
|
2891
|
+
...usage === void 0 ? {} : { usage }
|
|
2892
|
+
});
|
|
2893
|
+
if (content.length > 0) yield {
|
|
2894
|
+
channel: "content",
|
|
2895
|
+
text: content
|
|
2896
|
+
};
|
|
2897
|
+
signal.throwIfAborted();
|
|
2898
|
+
if (increment.thinking.length > 0) yield {
|
|
2899
|
+
channel: "thinking",
|
|
2900
|
+
text: increment.thinking
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
async #request(request, signal) {
|
|
2904
|
+
const headers = await this.#requestHeaders(signal);
|
|
2905
|
+
signal.throwIfAborted();
|
|
2906
|
+
const response = await this.#transport(this.#url + this.#path, {
|
|
2907
|
+
method: "POST",
|
|
2908
|
+
headers,
|
|
2909
|
+
body: JSON.stringify(this.body(request)),
|
|
2910
|
+
signal
|
|
2911
|
+
});
|
|
2912
|
+
if (!response.ok) {
|
|
2913
|
+
let detail;
|
|
2914
|
+
try {
|
|
2915
|
+
detail = response.body === null ? "" : (await readText(response.body, MAX_ERROR_BODY_LENGTH, signal)).text;
|
|
2916
|
+
} catch (cause) {
|
|
2917
|
+
throw new ProviderError("HTTP", `provider error: ${response.status} - (error body unavailable)`, {
|
|
2918
|
+
status: response.status,
|
|
2919
|
+
cause
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
signal.throwIfAborted();
|
|
2923
|
+
throw new ProviderError("HTTP", `provider error: ${response.status}${detail === "" ? "" : ` - ${detail}`}`, { status: response.status });
|
|
2924
|
+
}
|
|
2925
|
+
return response;
|
|
2926
|
+
}
|
|
2927
|
+
async #requestHeaders(signal) {
|
|
2928
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
2929
|
+
if (this.#headers === void 0) return headers;
|
|
2930
|
+
const cleanup = new AbortController();
|
|
2931
|
+
const aborted = Promise.withResolvers();
|
|
2932
|
+
signal.addEventListener("abort", () => aborted.reject(signal.reason), {
|
|
2933
|
+
once: true,
|
|
2934
|
+
signal: cleanup.signal
|
|
2935
|
+
});
|
|
2936
|
+
try {
|
|
2937
|
+
signal.throwIfAborted();
|
|
2938
|
+
const hook = this.#headers;
|
|
2939
|
+
const entries = await Promise.race([Promise.resolve().then(() => hook(signal)), aborted.promise]);
|
|
2940
|
+
for (const [key, value] of Object.entries(entries)) headers.set(key, value);
|
|
2941
|
+
return headers;
|
|
2942
|
+
} finally {
|
|
2943
|
+
cleanup.abort();
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
};
|
|
2947
|
+
//#endregion
|
|
2948
|
+
//#region src/core/providers/RelayProvider.ts
|
|
2949
|
+
/**
|
|
2950
|
+
* Carries provider calls over an authenticated NDJSON relay endpoint.
|
|
2951
|
+
*
|
|
2952
|
+
* @remarks
|
|
2953
|
+
* Tool execution context stays local; calls carry only `id`, `name`, and `arguments`.
|
|
2954
|
+
* The wire body is an owned snapshot of
|
|
2955
|
+
* the projection read through property descriptors, so a serializer reachable only through a
|
|
2956
|
+
* `get` trap or a prototype is never consulted; an own function-valued property such as a
|
|
2957
|
+
* `toJSON` method is a value outside JSON and is refused before fetching, with the clone's
|
|
2958
|
+
* failure as the refusal's `cause`. A remote abort reconstructs a `ProviderAbortError` instance
|
|
2959
|
+
* without aborting the
|
|
2960
|
+
* local signal; the `Agent` runtime treats that instance as an error unless its own bound
|
|
2961
|
+
* signal is aborted.
|
|
2962
|
+
* Content is preserved verbatim, including literal thinking tags.
|
|
2963
|
+
* A refusal reaches the browser as a `ProviderError` instance with the `HTTP` code and status.
|
|
2964
|
+
* This is the browser end alone; {@link createRelay} mounts the server end, and
|
|
2965
|
+
* {@link createRelayProvider}'s example is the browser half of that pair.
|
|
2966
|
+
*
|
|
2967
|
+
* @example
|
|
2968
|
+
* ```ts
|
|
2969
|
+
* import type { ProviderResult } from '@orkestrel/agent'
|
|
2970
|
+
* import { RelayProvider } from '@orkestrel/agent'
|
|
2971
|
+
* // The browser application supplies this parser dependency.
|
|
2972
|
+
* import { createNDJSONParser } from '@orkestrel/ndjson'
|
|
2973
|
+
*
|
|
2974
|
+
* export function ask(bearer: string, signal: AbortSignal): Promise<ProviderResult> {
|
|
2975
|
+
* const browser = new RelayProvider({
|
|
2976
|
+
* url: 'https://relay.example/relay',
|
|
2977
|
+
* parser: createNDJSONParser,
|
|
2978
|
+
* headers: () => ({ authorization: `Bearer ${bearer}` }),
|
|
2979
|
+
* })
|
|
2980
|
+
* return browser.generate([{ id: 'ask', role: 'user', content: 'ping' }], signal)
|
|
2981
|
+
* }
|
|
2982
|
+
* ```
|
|
2983
|
+
*/
|
|
2984
|
+
var RelayProvider = class extends AgentProvider {
|
|
2985
|
+
#parser;
|
|
2986
|
+
constructor(options) {
|
|
2987
|
+
const { url, timeout, fetch, headers, format } = options;
|
|
2988
|
+
super({
|
|
2989
|
+
url,
|
|
2990
|
+
split: false,
|
|
2991
|
+
strict: true,
|
|
2992
|
+
...timeout === void 0 ? {} : { timeout },
|
|
2993
|
+
...fetch === void 0 ? {} : { fetch },
|
|
2994
|
+
...headers === void 0 ? {} : { headers },
|
|
2995
|
+
...format === void 0 ? {} : { format }
|
|
2996
|
+
});
|
|
2997
|
+
this.#parser = options.parser;
|
|
2998
|
+
}
|
|
2999
|
+
/** Identifies the relay backend. */
|
|
3000
|
+
name = "relay";
|
|
3001
|
+
/** Creates fresh framing state for each response. */
|
|
3002
|
+
frame() {
|
|
3003
|
+
return this.#parser();
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Projects declared request fields and refuses values the JSON wire cannot carry.
|
|
3007
|
+
*
|
|
3008
|
+
* @remarks
|
|
3009
|
+
* The snapshot is taken from property descriptors, so a custom serializer a projected
|
|
3010
|
+
* value carries is ignored rather than consulted and cannot reach the wire.
|
|
3011
|
+
*
|
|
3012
|
+
* @param request - The domain conversation and call configuration
|
|
3013
|
+
* @returns The validated wire request
|
|
3014
|
+
* @throws {ProviderError} Thrown when the snapshot cannot be taken — a hostile read or a
|
|
3015
|
+
* value outside JSON — carrying that failure as its `cause`, and when the snapshot the
|
|
3016
|
+
* projection produced is not a valid wire request
|
|
3017
|
+
*/
|
|
3018
|
+
body(request) {
|
|
3019
|
+
let snapshot;
|
|
3020
|
+
try {
|
|
3021
|
+
const projected = {
|
|
3022
|
+
messages: request.messages.map((message) => ({
|
|
3023
|
+
id: message.id,
|
|
3024
|
+
role: message.role,
|
|
3025
|
+
content: message.content,
|
|
3026
|
+
...message.calls === void 0 ? {} : { calls: message.calls.map((call) => ({
|
|
3027
|
+
id: call.id,
|
|
3028
|
+
name: call.name,
|
|
3029
|
+
arguments: call.arguments
|
|
3030
|
+
})) },
|
|
3031
|
+
...message.images === void 0 ? {} : { images: message.images }
|
|
3032
|
+
})),
|
|
3033
|
+
...request.tools === void 0 ? {} : { tools: request.tools.map((tool) => ({
|
|
3034
|
+
name: tool.name,
|
|
3035
|
+
...tool.description === void 0 ? {} : { description: tool.description },
|
|
3036
|
+
...tool.parameters === void 0 ? {} : { parameters: tool.parameters }
|
|
3037
|
+
})) },
|
|
3038
|
+
...request.options === void 0 ? {} : { options: {
|
|
3039
|
+
...request.options.think === void 0 ? {} : { think: request.options.think },
|
|
3040
|
+
...request.options.schema === void 0 ? {} : { schema: request.options.schema }
|
|
3041
|
+
} }
|
|
3042
|
+
};
|
|
3043
|
+
snapshot = (0, _orkestrel_contract.cloneJSONValue)(projected);
|
|
3044
|
+
} catch (cause) {
|
|
3045
|
+
throw new ProviderError("PROTOCOL", "relay request is not JSON", { cause });
|
|
3046
|
+
}
|
|
3047
|
+
if (!providerRequestContract.is(snapshot)) throw new ProviderError("PROTOCOL", "relay request is not JSON");
|
|
3048
|
+
return snapshot;
|
|
3049
|
+
}
|
|
3050
|
+
/**
|
|
3051
|
+
* Validates a relay frame and translates its channel into the shared stream engine.
|
|
3052
|
+
*
|
|
3053
|
+
* @param record - The framed wire record
|
|
3054
|
+
* @returns A delta contribution or authoritative result
|
|
3055
|
+
* @throws {ProviderAbortError} Thrown for a remote abort carrying its partial
|
|
3056
|
+
* @throws {ProviderError} Thrown for a malformed frame or remote provider failure
|
|
3057
|
+
*/
|
|
3058
|
+
read(record) {
|
|
3059
|
+
if (!relayFrameContract.is(record)) throw new ProviderError("PROTOCOL", "invalid relay frame");
|
|
3060
|
+
switch (record.channel) {
|
|
3061
|
+
case "content": return {
|
|
3062
|
+
content: record.text,
|
|
3063
|
+
thinking: "",
|
|
3064
|
+
tools: []
|
|
3065
|
+
};
|
|
3066
|
+
case "thinking": return {
|
|
3067
|
+
content: "",
|
|
3068
|
+
thinking: record.text,
|
|
3069
|
+
tools: []
|
|
3070
|
+
};
|
|
3071
|
+
case "result": return {
|
|
3072
|
+
content: "",
|
|
3073
|
+
thinking: "",
|
|
3074
|
+
tools: [],
|
|
3075
|
+
result: record.result
|
|
3076
|
+
};
|
|
3077
|
+
case "abort": throw new ProviderAbortError(record.partial);
|
|
3078
|
+
case "error": throw new ProviderError("PROVIDER", record.message);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Recovers an unterminated final frame by completing its NDJSON line.
|
|
3083
|
+
*
|
|
3084
|
+
* @param parser - The call's retained framing state
|
|
3085
|
+
* @returns Records completed by the final newline
|
|
3086
|
+
*/
|
|
3087
|
+
finish(parser) {
|
|
3088
|
+
return parser.parse("\n");
|
|
3089
|
+
}
|
|
3090
|
+
};
|
|
3091
|
+
//#endregion
|
|
3092
|
+
//#region src/core/RelayStream.ts
|
|
3093
|
+
/**
|
|
3094
|
+
* Streams a provider call as validated NDJSON frames under response backpressure.
|
|
3095
|
+
*
|
|
3096
|
+
* @remarks
|
|
3097
|
+
* Cancellation aborts upstream before returning its iterator. Request listeners are
|
|
3098
|
+
* released on settlement. Unexpected provider failures carry a fixed public message.
|
|
3099
|
+
* An inbound abort leaves the response body neither closed nor errored — a server runtime
|
|
3100
|
+
* cancels that body when the client disconnects — so a consumer that aborts the inbound
|
|
3101
|
+
* signal itself must cancel the body rather than keep reading it.
|
|
3102
|
+
*
|
|
3103
|
+
* @example
|
|
3104
|
+
* ```ts
|
|
3105
|
+
* import type { ProviderInterface } from '@orkestrel/agent'
|
|
3106
|
+
* import { RelayStream } from '@orkestrel/agent'
|
|
3107
|
+
*
|
|
3108
|
+
* export function respond(provider: ProviderInterface, signal: AbortSignal): Response {
|
|
3109
|
+
* return new RelayStream({ provider, request: { messages: [] }, signal }).response
|
|
3110
|
+
* }
|
|
3111
|
+
* ```
|
|
3112
|
+
*/
|
|
3113
|
+
var RelayStream = class {
|
|
3114
|
+
#upstream = new AbortController();
|
|
3115
|
+
#encoder = new TextEncoder();
|
|
3116
|
+
#iterator;
|
|
3117
|
+
#signal;
|
|
3118
|
+
#listener;
|
|
3119
|
+
#response;
|
|
3120
|
+
#settled = false;
|
|
3121
|
+
constructor(options) {
|
|
3122
|
+
this.#signal = options.signal;
|
|
3123
|
+
this.#listener = this.#cancel.bind(this);
|
|
3124
|
+
if (this.#signal.aborted) this.#abortProvider();
|
|
3125
|
+
this.#iterator = options.provider.stream(options.request.messages, this.#upstream.signal, options.request.tools, options.request.options);
|
|
3126
|
+
this.#signal.addEventListener("abort", this.#listener, { once: true });
|
|
3127
|
+
this.#response = new Response(new ReadableStream({
|
|
3128
|
+
pull: this.#pull.bind(this),
|
|
3129
|
+
cancel: this.#cancel.bind(this)
|
|
3130
|
+
}), { headers: {
|
|
3131
|
+
"content-type": RELAY_CONTENT_TYPE,
|
|
3132
|
+
"cache-control": "no-store"
|
|
3133
|
+
} });
|
|
3134
|
+
}
|
|
3135
|
+
/** Exposes the pull-driven response for the provider call. */
|
|
3136
|
+
get response() {
|
|
3137
|
+
return this.#response;
|
|
3138
|
+
}
|
|
3139
|
+
async #pull(controller) {
|
|
3140
|
+
if (this.#settled) return;
|
|
3141
|
+
try {
|
|
3142
|
+
const step = await this.#iterator.next();
|
|
3143
|
+
if (this.#settled) return;
|
|
3144
|
+
this.#write(controller, step.done ? {
|
|
3145
|
+
channel: "result",
|
|
3146
|
+
result: step.value
|
|
3147
|
+
} : step.value);
|
|
3148
|
+
if (step.done) this.#finish(controller);
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
if (this.#settled) return;
|
|
3151
|
+
const frame = error instanceof ProviderAbortError ? {
|
|
3152
|
+
channel: "abort",
|
|
3153
|
+
partial: error.partial
|
|
3154
|
+
} : {
|
|
3155
|
+
channel: "error",
|
|
3156
|
+
message: RELAY_PROVIDER_MESSAGE
|
|
3157
|
+
};
|
|
3158
|
+
this.#write(controller, relayFrameContract.is(frame) ? frame : {
|
|
3159
|
+
channel: "error",
|
|
3160
|
+
message: RELAY_PROVIDER_MESSAGE
|
|
3161
|
+
});
|
|
3162
|
+
this.#finish(controller);
|
|
3163
|
+
this.#abortProvider();
|
|
3164
|
+
await this.#return();
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
async #cancel() {
|
|
3168
|
+
if (this.#settled) return;
|
|
3169
|
+
this.#settled = true;
|
|
3170
|
+
this.#abortProvider();
|
|
3171
|
+
try {
|
|
3172
|
+
await this.#return();
|
|
3173
|
+
} finally {
|
|
3174
|
+
this.#release();
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
async #return() {
|
|
3178
|
+
try {
|
|
3179
|
+
await this.#iterator.return({ content: "" });
|
|
3180
|
+
} catch {}
|
|
3181
|
+
}
|
|
3182
|
+
#write(controller, frame) {
|
|
3183
|
+
if (!relayFrameContract.is(frame)) throw new ProviderError("PROTOCOL", "invalid relay frame");
|
|
3184
|
+
controller.enqueue(this.#encoder.encode(`${JSON.stringify(frame)}\n`));
|
|
3185
|
+
}
|
|
3186
|
+
#finish(controller) {
|
|
3187
|
+
this.#settled = true;
|
|
3188
|
+
this.#release();
|
|
3189
|
+
controller.close();
|
|
3190
|
+
}
|
|
3191
|
+
#abortProvider() {
|
|
3192
|
+
this.#upstream.abort(this.#signal.reason);
|
|
3193
|
+
}
|
|
3194
|
+
#release() {
|
|
3195
|
+
this.#signal.removeEventListener("abort", this.#listener);
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
//#endregion
|
|
2389
3199
|
//#region src/core/scopes/Scope.ts
|
|
2390
3200
|
/**
|
|
2391
3201
|
* Represents a named, immutable filter over a richer context's items — an optional allow-list per
|
|
@@ -2640,6 +3450,105 @@ var ThinkSplitter = class {
|
|
|
2640
3450
|
//#endregion
|
|
2641
3451
|
//#region src/core/factories.ts
|
|
2642
3452
|
/**
|
|
3453
|
+
* Creates an authorized relay handler that validates a bounded JSON request before streaming.
|
|
3454
|
+
*
|
|
3455
|
+
* @remarks
|
|
3456
|
+
* Answers with the `401` status when authorization refuses or throws, the `400`
|
|
3457
|
+
* status when the body is missing, unreadable, or invalid, the `413` status when
|
|
3458
|
+
* the body fills its byte budget or the inbound read is aborted, and the `502`
|
|
3459
|
+
* status when the upstream provider call cannot be constructed. Refusals carry no body.
|
|
3460
|
+
*
|
|
3461
|
+
* @param options - The upstream provider, authorization decision, and optional byte budget
|
|
3462
|
+
* @returns A fetch-standard handler suitable for a router
|
|
3463
|
+
* @example Mounting the relay on your server
|
|
3464
|
+
* ```ts
|
|
3465
|
+
* import type { ProviderInterface } from '@orkestrel/agent'
|
|
3466
|
+
* import { createRelay } from '@orkestrel/agent'
|
|
3467
|
+
* import { createDispatcher } from '@orkestrel/router'
|
|
3468
|
+
* import { createServer } from '@orkestrel/server'
|
|
3469
|
+
*
|
|
3470
|
+
* declare const upstream: ProviderInterface // the server-side provider holding the credential
|
|
3471
|
+
* declare const bearer: string
|
|
3472
|
+
*
|
|
3473
|
+
* const handler = createRelay({
|
|
3474
|
+
* provider: upstream,
|
|
3475
|
+
* authorize: (request) => request.headers.get('authorization') === `Bearer ${bearer}`,
|
|
3476
|
+
* })
|
|
3477
|
+
* const dispatcher = createDispatcher({
|
|
3478
|
+
* routes: [{ method: 'POST', path: '/relay', handler }],
|
|
3479
|
+
* })
|
|
3480
|
+
*
|
|
3481
|
+
* export function serve(request: Request): Promise<Response> {
|
|
3482
|
+
* return dispatcher.handle(request, undefined)
|
|
3483
|
+
* }
|
|
3484
|
+
*
|
|
3485
|
+
* const server = createServer({ dispatcher, state: () => undefined })
|
|
3486
|
+
* await server.start()
|
|
3487
|
+
* process.on('SIGTERM', () => server.stop()) // signal cancellation, drain, then close the listener
|
|
3488
|
+
* ```
|
|
3489
|
+
*/
|
|
3490
|
+
function createRelay(options) {
|
|
3491
|
+
const { provider, authorize, limit } = options;
|
|
3492
|
+
return async (request) => {
|
|
3493
|
+
try {
|
|
3494
|
+
if (await authorize(request) !== true) return new Response(void 0, { status: 401 });
|
|
3495
|
+
} catch {
|
|
3496
|
+
return new Response(void 0, { status: 401 });
|
|
3497
|
+
}
|
|
3498
|
+
if (request.body === null) return new Response(void 0, { status: 400 });
|
|
3499
|
+
let read;
|
|
3500
|
+
try {
|
|
3501
|
+
read = await readText(request.body, limit ?? 1048576, request.signal);
|
|
3502
|
+
} catch {
|
|
3503
|
+
return new Response(void 0, { status: request.signal.aborted ? 413 : 400 });
|
|
3504
|
+
}
|
|
3505
|
+
if (!read.complete) return new Response(void 0, { status: 413 });
|
|
3506
|
+
const parsed = (0, _orkestrel_contract.parseJSONAs)(read.text, providerRequestContract.is);
|
|
3507
|
+
if (parsed === void 0) return new Response(void 0, { status: 400 });
|
|
3508
|
+
try {
|
|
3509
|
+
return new RelayStream({
|
|
3510
|
+
provider,
|
|
3511
|
+
request: parsed,
|
|
3512
|
+
signal: request.signal
|
|
3513
|
+
}).response;
|
|
3514
|
+
} catch {
|
|
3515
|
+
return new Response(void 0, { status: 502 });
|
|
3516
|
+
}
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Creates a provider that carries calls through a relay endpoint.
|
|
3521
|
+
*
|
|
3522
|
+
* @remarks
|
|
3523
|
+
* This is the browser end alone. {@link createRelay} mounts the server end, and its example
|
|
3524
|
+
* is the server half this one pairs with.
|
|
3525
|
+
*
|
|
3526
|
+
* @param options - The endpoint, parser factory, and HTTP call configuration
|
|
3527
|
+
* @returns The concrete relay provider
|
|
3528
|
+
* @example Reaching the relay from the browser
|
|
3529
|
+
* ```ts
|
|
3530
|
+
* import type { ProviderInterface } from '@orkestrel/agent'
|
|
3531
|
+
* import { createRelayProvider } from '@orkestrel/agent'
|
|
3532
|
+
* import { createAbort } from '@orkestrel/abort'
|
|
3533
|
+
* // The browser application supplies this parser dependency.
|
|
3534
|
+
* import { createNDJSONParser } from '@orkestrel/ndjson'
|
|
3535
|
+
*
|
|
3536
|
+
* declare const bearer: string
|
|
3537
|
+
* const abort = createAbort()
|
|
3538
|
+
* const messages = [{ id: '1', role: 'user', content: 'Say hello.' }] as const
|
|
3539
|
+
*
|
|
3540
|
+
* const browser: ProviderInterface = createRelayProvider({
|
|
3541
|
+
* url: 'https://app.example/relay',
|
|
3542
|
+
* parser: createNDJSONParser,
|
|
3543
|
+
* headers: () => ({ authorization: `Bearer ${bearer}` }),
|
|
3544
|
+
* })
|
|
3545
|
+
* const result = await browser.generate(messages, abort.signal) // a ProviderResult like a local provider's
|
|
3546
|
+
* ```
|
|
3547
|
+
*/
|
|
3548
|
+
function createRelayProvider(options) {
|
|
3549
|
+
return new RelayProvider(options);
|
|
3550
|
+
}
|
|
3551
|
+
/**
|
|
2643
3552
|
* Creates a conversation — a {@link ConversationInterface} grouping messages above a flat
|
|
2644
3553
|
* message store it owns directly, with compaction into summarized sections, a regenerated
|
|
2645
3554
|
* rollup `summary`, on-demand `rehydrate`, and substring `search`, driven by a
|
|
@@ -2660,8 +3569,8 @@ var ThinkSplitter = class {
|
|
|
2660
3569
|
*
|
|
2661
3570
|
* @example Conversations & compaction
|
|
2662
3571
|
* ```ts
|
|
2663
|
-
* import { createConversation } from '@orkestrel/agent'
|
|
2664
3572
|
* import type { ProviderInterface } from '@orkestrel/agent'
|
|
3573
|
+
* import { createConversation } from '@orkestrel/agent'
|
|
2665
3574
|
*
|
|
2666
3575
|
* declare const provider: ProviderInterface // any concrete implementation supplied by the host app
|
|
2667
3576
|
* // The summarizer seam — built from the provider by the runtime; core stays provider-agnostic.
|
|
@@ -3222,6 +4131,7 @@ exports.Agent = Agent;
|
|
|
3222
4131
|
exports.AgentContext = AgentContext;
|
|
3223
4132
|
exports.AgentError = AgentError;
|
|
3224
4133
|
exports.AgentJobError = AgentJobError;
|
|
4134
|
+
exports.AgentProvider = AgentProvider;
|
|
3225
4135
|
exports.AgentRegistry = AgentRegistry;
|
|
3226
4136
|
exports.Authority = Authority;
|
|
3227
4137
|
exports.CONVERSATION_RECAP_PREFIX = CONVERSATION_RECAP_PREFIX;
|
|
@@ -3232,23 +4142,36 @@ exports.ConversationManager = ConversationManager;
|
|
|
3232
4142
|
exports.DEFAULT_AGENT_LIMIT = DEFAULT_AGENT_LIMIT;
|
|
3233
4143
|
exports.DEFAULT_AUTHORITY_ZONE = DEFAULT_AUTHORITY_ZONE;
|
|
3234
4144
|
exports.DEFAULT_CONVERSATION_KEEP = DEFAULT_CONVERSATION_KEEP;
|
|
4145
|
+
exports.DEFAULT_PROVIDER_TIMEOUT = DEFAULT_PROVIDER_TIMEOUT;
|
|
4146
|
+
exports.DEFAULT_RELAY_LIMIT = DEFAULT_RELAY_LIMIT;
|
|
3235
4147
|
exports.DatabaseConversationStore = DatabaseConversationStore;
|
|
3236
4148
|
exports.IMAGE_TOKEN_ESTIMATE = IMAGE_TOKEN_ESTIMATE;
|
|
4149
|
+
exports.INVALID_RELAY_STATUS = INVALID_RELAY_STATUS;
|
|
3237
4150
|
exports.Instruction = Instruction;
|
|
3238
4151
|
exports.InstructionManager = InstructionManager;
|
|
4152
|
+
exports.MAX_ERROR_BODY_LENGTH = MAX_ERROR_BODY_LENGTH;
|
|
3239
4153
|
exports.MESSAGE_TOKEN_OVERHEAD = MESSAGE_TOKEN_OVERHEAD;
|
|
3240
4154
|
exports.MemoryConversationStore = MemoryConversationStore;
|
|
4155
|
+
exports.OVERSIZED_RELAY_STATUS = OVERSIZED_RELAY_STATUS;
|
|
3241
4156
|
exports.ProviderAbortError = ProviderAbortError;
|
|
4157
|
+
exports.ProviderError = ProviderError;
|
|
4158
|
+
exports.RELAY_CONTENT_TYPE = RELAY_CONTENT_TYPE;
|
|
4159
|
+
exports.RELAY_PROVIDER_MESSAGE = RELAY_PROVIDER_MESSAGE;
|
|
4160
|
+
exports.RelayProvider = RelayProvider;
|
|
4161
|
+
exports.RelayStream = RelayStream;
|
|
3242
4162
|
exports.Scope = Scope;
|
|
3243
4163
|
exports.ScopeManager = ScopeManager;
|
|
3244
4164
|
exports.THINK_CLOSE = THINK_CLOSE;
|
|
3245
4165
|
exports.THINK_OPEN = THINK_OPEN;
|
|
3246
4166
|
exports.ThinkSplitter = ThinkSplitter;
|
|
4167
|
+
exports.UNAUTHORIZED_RELAY_STATUS = UNAUTHORIZED_RELAY_STATUS;
|
|
4168
|
+
exports.UPSTREAM_RELAY_STATUS = UPSTREAM_RELAY_STATUS;
|
|
3247
4169
|
exports.WORKSPACE_SECTION_HEADER = WORKSPACE_SECTION_HEADER;
|
|
3248
4170
|
exports.agentResultToJSON = agentResultToJSON;
|
|
3249
4171
|
exports.assembleResult = assembleResult;
|
|
3250
4172
|
exports.attachImages = attachImages;
|
|
3251
4173
|
exports.attachUserImages = attachUserImages;
|
|
4174
|
+
exports.buildProviderResult = buildProviderResult;
|
|
3252
4175
|
exports.buildRecapMessage = buildRecapMessage;
|
|
3253
4176
|
exports.buildSummaryMessage = buildSummaryMessage;
|
|
3254
4177
|
exports.collectImageData = collectImageData;
|
|
@@ -3265,6 +4188,8 @@ exports.createDatabaseConversationStore = createDatabaseConversationStore;
|
|
|
3265
4188
|
exports.createInstruction = createInstruction;
|
|
3266
4189
|
exports.createInstructionManager = createInstructionManager;
|
|
3267
4190
|
exports.createMemoryConversationStore = createMemoryConversationStore;
|
|
4191
|
+
exports.createRelay = createRelay;
|
|
4192
|
+
exports.createRelayProvider = createRelayProvider;
|
|
3268
4193
|
exports.createScope = createScope;
|
|
3269
4194
|
exports.createScopeManager = createScopeManager;
|
|
3270
4195
|
exports.createThinkSplitter = createThinkSplitter;
|
|
@@ -3281,8 +4206,19 @@ exports.isConversationError = isConversationError;
|
|
|
3281
4206
|
exports.isConversationSnapshot = isConversationSnapshot;
|
|
3282
4207
|
exports.isMessage = isMessage;
|
|
3283
4208
|
exports.isProviderAbortError = isProviderAbortError;
|
|
4209
|
+
exports.isProviderError = isProviderError;
|
|
3284
4210
|
exports.isSection = isSection;
|
|
3285
4211
|
exports.joinThinking = joinThinking;
|
|
4212
|
+
exports.messageContract = messageContract;
|
|
4213
|
+
exports.messageShape = messageShape;
|
|
4214
|
+
exports.providerRequestContract = providerRequestContract;
|
|
4215
|
+
exports.providerRequestShape = providerRequestShape;
|
|
4216
|
+
exports.providerResultContract = providerResultContract;
|
|
4217
|
+
exports.providerResultShape = providerResultShape;
|
|
4218
|
+
exports.readChunks = readChunks;
|
|
4219
|
+
exports.readText = readText;
|
|
4220
|
+
exports.relayFrameContract = relayFrameContract;
|
|
4221
|
+
exports.relayFrameShape = relayFrameShape;
|
|
3286
4222
|
exports.renderFencedFile = renderFencedFile;
|
|
3287
4223
|
exports.renderSection = renderSection;
|
|
3288
4224
|
exports.resolveClose = resolveClose;
|
|
@@ -3292,5 +4228,6 @@ exports.sanitizeToken = sanitizeToken;
|
|
|
3292
4228
|
exports.sanitizeUsage = sanitizeUsage;
|
|
3293
4229
|
exports.settleAgentJob = settleAgentJob;
|
|
3294
4230
|
exports.sumUsage = sumUsage;
|
|
4231
|
+
exports.toolCallShape = toolCallShape;
|
|
3295
4232
|
|
|
3296
4233
|
//# sourceMappingURL=index.cjs.map
|