@orkestrel/agent 0.0.26 → 0.0.28

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.
@@ -1,4 +1,4 @@
1
- import { arrayOf, arrayShape, attempt, booleanShape, cloneJSONValue, createContract, isArray, isBoolean, isFiniteNumber, isInstance, isObject, isRecord, isString, jsonShape, literalShape, numberShape, objectShape, optionalShape, parseJSONAs, parseJSONValue, rawShape, recordShape, stringShape, unionShape } from "@orkestrel/contract";
1
+ import { arrayOf, arrayShape, attempt, booleanShape, boundsOf, cloneJSONValue, createContract, isArray, isBoolean, isFiniteNumber, isInstance, isJSONValue, isNumber, isObject, isRecord, isString, jsonShape, literalShape, nullableOf, numberShape, objectShape, optionalOf, optionalShape, parseJSON, parseJSONAs, parseJSONValue, rawShape, recordShape, stringShape, unionShape } from "@orkestrel/contract";
2
2
  import { createDatabase, createMemoryDriver } from "@orkestrel/database";
3
3
  import { createQueue } from "@orkestrel/queue";
4
4
  import { createRunner, errorToMessage } from "@orkestrel/workflow";
@@ -7,7 +7,7 @@ import { Timeout, createTimeout } from "@orkestrel/timeout";
7
7
  import { Emitter } from "@orkestrel/emitter";
8
8
  import { ToolManager, isToolCall } from "@orkestrel/tool";
9
9
  import { WorkspaceManager, isBinary, isText } from "@orkestrel/workspace";
10
- import { createTokenBudget } from "@orkestrel/budget";
10
+ import { createTokenBudget, isTokenUsage } from "@orkestrel/budget";
11
11
  //#region src/core/constants.ts
12
12
  /**
13
13
  * Caps an {@link AgentInterface} turn's tool iterations by default — `10` context → provider →
@@ -145,6 +145,8 @@ var INVALID_RELAY_STATUS = 400;
145
145
  * carried with no body and answered for an aborted inbound read as well.
146
146
  */
147
147
  var OVERSIZED_RELAY_STATUS = 413;
148
+ /** Names the System One decision endpoint shared by compatible servers. */
149
+ var SYSTEM_ONE_PATH = "/v1/systemone";
148
150
  //#endregion
149
151
  //#region src/core/errors.ts
150
152
  /**
@@ -360,6 +362,75 @@ var ProviderError = class extends Error {
360
362
  function isProviderError(value) {
361
363
  return isInstance(value, ProviderError);
362
364
  }
365
+ /**
366
+ * Reports a judge call cancelled by the caller's signal or its deadline, carrying the
367
+ * {@link JudgeResult} merged from the calls that completed before the cancel and the
368
+ * machine-readable `code` `'ABORT'`.
369
+ *
370
+ * @remarks
371
+ * `partial` keeps the answers and the usage of every completed call, so usage reported by
372
+ * completed calls is retained; a cancel before the first call carries an empty partial. `cause` holds the failure the
373
+ * cancel superseded when a throw raced the abort, and is undefined when the cancel was the only
374
+ * failure.
375
+ */
376
+ var JudgeAbortError = class extends Error {
377
+ /** Names the machine-readable condition — `'ABORT'`: a judge call cancelled mid-flight. */
378
+ code = "ABORT";
379
+ partial;
380
+ constructor(partial, options) {
381
+ super("judge call aborted", options);
382
+ this.name = "JudgeAbortError";
383
+ this.partial = partial;
384
+ }
385
+ };
386
+ /**
387
+ * Narrows a caught value to a {@link JudgeAbortError} through `instanceof`, so a `catch` can
388
+ * recover its `partial` result.
389
+ *
390
+ * @param value - The caught value
391
+ * @returns True if the value is a {@link JudgeAbortError}; false otherwise
392
+ * @example
393
+ * ```ts
394
+ * isJudgeAbortError(new JudgeAbortError({ model: 'tev1:0.8b', answers: {} })) // true
395
+ * ```
396
+ */
397
+ function isJudgeAbortError(value) {
398
+ return isInstance(value, JudgeAbortError);
399
+ }
400
+ /**
401
+ * Reports a coded judge failure with its HTTP status and underlying cause when available.
402
+ *
403
+ * @remarks
404
+ * The judge engine throws this error for a non-OK HTTP response, a missing or unparsable
405
+ * response body, and a request refused before inference. Concrete wire decoders also use it for
406
+ * a response they cannot read and for a wire limit. `status` is present only for an `HTTP`
407
+ * failure; other codes leave it undefined.
408
+ */
409
+ var JudgeError = class extends Error {
410
+ /** Names the machine-readable condition — `'HTTP'`: a non-OK response; `'PROTOCOL'`: a missing, unparsable, or unreadable response body; `'QUESTION'`: a request refused before inference. */
411
+ code;
412
+ /** Holds the response status for an HTTP failure, or undefined for other codes. */
413
+ status;
414
+ constructor(code, message, options) {
415
+ super(message, options);
416
+ this.name = "JudgeError";
417
+ this.code = code;
418
+ this.status = options?.status;
419
+ }
420
+ };
421
+ /**
422
+ * Narrows a caught value to the judge failure class through `instanceof`.
423
+ *
424
+ * @param value - The caught value
425
+ * @returns True if the value is a {@link JudgeError}; false otherwise
426
+ * @example
427
+ * ```ts
428
+ * isJudgeError(new JudgeError('HTTP', 'judge error: 429', { status: 429 })) // true
429
+ * ```
430
+ */
431
+ function isJudgeError(value) {
432
+ return isInstance(value, JudgeError);
433
+ }
363
434
  //#endregion
364
435
  //#region src/core/shapers.ts
365
436
  /**
@@ -1259,6 +1330,273 @@ async function* readChunks(body, signal) {
1259
1330
  }
1260
1331
  }
1261
1332
  }
1333
+ /**
1334
+ * Derives the winner, its probability, the published confidence, and a score answer's expected
1335
+ * level from a judge answer's distribution.
1336
+ *
1337
+ * @remarks
1338
+ * The winner is the first strictly greatest candidate in enumeration order: an option name in
1339
+ * criteria order, a level index, or `'false'` then `'true'` for a noul, so a noul of exactly 0.5
1340
+ * names `'false'`. Confidence follows the TypeSafe formulas, whatever confidence a server sent: a
1341
+ * choice reads `(max(p) - 1/n) / (1 - 1/n)`, a noul reads `|2p - 1|` (the choice formula at
1342
+ * n = 2), and a score reads `max(0, 1 - spread / even_spread)`, where `spread` sums each level's
1343
+ * probability times its distance from the winning level and `even_spread` is the mean distance
1344
+ * from the middle level. `score` is the expected level `sum(i * p_i)` and is set only for a score
1345
+ * answer.
1346
+ *
1347
+ * @param answer - The answer whose distribution is read
1348
+ * @returns The derived measures
1349
+ * @throws JudgeError Thrown with code `PROTOCOL` when a choice or score answer has fewer than 2
1350
+ * candidates, because the confidence formulas divide by the candidate count
1351
+ * @example
1352
+ * ```ts
1353
+ * computeReading({ form: 'choice', probabilities: { billing: 0.88, technical: 0.12, sales: 0 } })
1354
+ * // { winner: 'billing', probability: 0.88, confidence: 0.82 } to two decimals
1355
+ * computeReading({ form: 'score', probabilities: [0, 0.57, 0.43] })
1356
+ * // { winner: '1', probability: 0.57, confidence: 0.355, score: 1.43 } to three decimals
1357
+ * computeReading({ form: 'noul', noul: 0.5 }) // { winner: 'false', probability: 0.5, confidence: 0 }
1358
+ * ```
1359
+ */
1360
+ function computeReading(answer) {
1361
+ if (answer.form === "noul") {
1362
+ const yes = answer.noul > .5;
1363
+ return {
1364
+ winner: yes ? "true" : "false",
1365
+ probability: yes ? answer.noul : 1 - answer.noul,
1366
+ confidence: Math.abs(2 * answer.noul - 1)
1367
+ };
1368
+ }
1369
+ const candidates = answer.form === "choice" ? Object.entries(answer.probabilities) : answer.probabilities.map((probability, level) => [String(level), probability]);
1370
+ const count = candidates.length;
1371
+ if (count < 2) throw new JudgeError("PROTOCOL", "judge error: an answer needs at least 2 candidates");
1372
+ let winner = "";
1373
+ let index = 0;
1374
+ let probability = -Infinity;
1375
+ for (const [position, [name, candidate]] of candidates.entries()) if (candidate > probability) {
1376
+ winner = name;
1377
+ index = position;
1378
+ probability = candidate;
1379
+ }
1380
+ if (answer.form === "choice") return {
1381
+ winner,
1382
+ probability,
1383
+ confidence: (probability - 1 / count) / (1 - 1 / count)
1384
+ };
1385
+ let spread = 0;
1386
+ let even = 0;
1387
+ let score = 0;
1388
+ for (const [level, candidate] of answer.probabilities.entries()) {
1389
+ spread += candidate * Math.abs(level - index);
1390
+ even += Math.abs(level - (count - 1) / 2);
1391
+ score += level * candidate;
1392
+ }
1393
+ return {
1394
+ winner,
1395
+ probability,
1396
+ confidence: Math.max(0, 1 - spread / (even / count)),
1397
+ score
1398
+ };
1399
+ }
1400
+ /**
1401
+ * Merges the results of a judge request's calls into one result.
1402
+ *
1403
+ * @remarks
1404
+ * Pure and total. Answers and refusals are joined by question id; `refusals` is omitted when no
1405
+ * call refused a question. Each call's usage passes through {@link sanitizeUsage} and the totals
1406
+ * add through {@link sumUsage}; `usage` is omitted when no call reported one. The model is the
1407
+ * first call's, or the given model when the list is empty, as the empty partial of a cancel
1408
+ * before the first call requires.
1409
+ *
1410
+ * @param model - The judge's configured model, reported when no call completed
1411
+ * @param results - The completed calls' results in call order
1412
+ * @returns The merged result
1413
+ * @example
1414
+ * ```ts
1415
+ * buildJudgeResult('tev1:0.8b', []) // { model: 'tev1:0.8b', answers: {} }
1416
+ * buildJudgeResult('jev-latest', [
1417
+ * { model: 'jev-1.13.0', answers: { urgent: { form: 'noul', noul: 0.95 } } },
1418
+ * { model: 'jev-1.13.0', answers: {}, refusals: { team: { missing: ['sales'] } } },
1419
+ * ])
1420
+ * // { model: 'jev-1.13.0', answers: { urgent: ... }, refusals: { team: { missing: ['sales'] } } }
1421
+ * ```
1422
+ */
1423
+ function buildJudgeResult(model, results) {
1424
+ let answers = {};
1425
+ let refusals = {};
1426
+ let usage;
1427
+ for (const result of results) {
1428
+ answers = {
1429
+ ...answers,
1430
+ ...result.answers
1431
+ };
1432
+ refusals = {
1433
+ ...refusals,
1434
+ ...result.refusals
1435
+ };
1436
+ if (result.usage !== void 0) usage = sumUsage(usage, sanitizeUsage(result.usage));
1437
+ }
1438
+ return {
1439
+ model: results[0]?.model ?? model,
1440
+ answers,
1441
+ ...Object.keys(refusals).length === 0 ? {} : { refusals },
1442
+ ...usage === void 0 ? {} : { usage }
1443
+ };
1444
+ }
1445
+ /**
1446
+ * Builds a request's JSON headers, awaiting the caller's header hook inside the call's
1447
+ * cancellation bound.
1448
+ *
1449
+ * @remarks
1450
+ * The headers start with `Content-Type: application/json`; each entry the hook returns is set
1451
+ * over them, so the hook overrides the content type only when it returns that header. The hook
1452
+ * receives `signal` and races its abort, and the abort listener is removed on every exit. Both
1453
+ * HTTP engines, the provider and the judge, read their headers here.
1454
+ *
1455
+ * @param hook - The caller's header hook, or undefined for the JSON content type alone
1456
+ * @param signal - The call's combined caller and deadline signal
1457
+ * @returns The request headers
1458
+ * @throws Thrown when the signal aborts before the hook settles, with the signal's reason, and
1459
+ * when the hook throws or rejects, with the hook's own failure
1460
+ * @example
1461
+ * ```ts
1462
+ * const signal = new AbortController().signal
1463
+ * const headers = await readHeaders(() => ({ authorization: 'Bearer KEY' }), signal)
1464
+ * headers.get('authorization') // 'Bearer KEY'
1465
+ * headers.get('content-type') // 'application/json'
1466
+ * ```
1467
+ */
1468
+ async function readHeaders(hook, signal) {
1469
+ const headers = new Headers({ "Content-Type": "application/json" });
1470
+ if (hook === void 0) return headers;
1471
+ const cleanup = new AbortController();
1472
+ const aborted = Promise.withResolvers();
1473
+ signal.addEventListener("abort", () => aborted.reject(signal.reason), {
1474
+ once: true,
1475
+ signal: cleanup.signal
1476
+ });
1477
+ try {
1478
+ signal.throwIfAborted();
1479
+ const entries = await Promise.race([Promise.resolve().then(() => hook(signal)), aborted.promise]);
1480
+ for (const [key, value] of Object.entries(entries)) headers.set(key, value);
1481
+ return headers;
1482
+ } finally {
1483
+ cleanup.abort();
1484
+ }
1485
+ }
1486
+ /**
1487
+ * Owns a value by serializing it to JSON and parsing the text, so the copy shares nothing with its source.
1488
+ *
1489
+ * @remarks
1490
+ * A value that JSON cannot carry, such as a cycle or a bigint, and a value that serializes to
1491
+ * nothing, such as a function, both return undefined, so a guard over the result refuses them.
1492
+ * A proxied value serializes through its traps, where a structured clone refuses it.
1493
+ *
1494
+ * @param value - The value to own
1495
+ * @returns The owned JSON copy, or undefined when the value is not JSON
1496
+ * @example
1497
+ * ```ts
1498
+ * copyJSON({ state: 'A ticket.' }) // { state: 'A ticket.' }
1499
+ * copyJSON(() => 1) // undefined
1500
+ * ```
1501
+ */
1502
+ function copyJSON(value) {
1503
+ const text = attempt(() => JSON.stringify(value));
1504
+ if (!text.success || typeof text.value !== "string") return void 0;
1505
+ return parseJSON(text.value);
1506
+ }
1507
+ /**
1508
+ * Projects a judge question onto the System One wire while preserving omitted members.
1509
+ *
1510
+ * @param question - The domain question to send
1511
+ * @returns The wire question with `type` replacing `form`
1512
+ * @example
1513
+ * ```ts
1514
+ * questionToSystemOne({ form: 'noul' }) // { type: 'noul' }
1515
+ * ```
1516
+ */
1517
+ function questionToSystemOne(question) {
1518
+ return {
1519
+ type: question.form,
1520
+ ...question.instructions === void 0 ? {} : { instructions: question.instructions },
1521
+ ...question.criteria === void 0 ? {} : { criteria: question.criteria }
1522
+ };
1523
+ }
1524
+ /**
1525
+ * Extracts a System One distribution in question criteria order and drops server measures.
1526
+ *
1527
+ * @remarks
1528
+ * Every requested candidate must have a finite probability in [0, 1]; the helper checks each
1529
+ * requested candidate itself and reads no other member. Score maps and arrays project onto the
1530
+ * requested levels. Additional candidates are ignored, sums are unconstrained, and probabilities
1531
+ * are preserved without normalization.
1532
+ *
1533
+ * @param answer - The wire answer to decode
1534
+ * @param question - The question defining the form and candidate order
1535
+ * @returns The domain answer, or undefined for a mismatched form or a requested candidate whose
1536
+ * probability is missing, not finite, or outside [0, 1]
1537
+ * @example
1538
+ * ```ts
1539
+ * extractSystemOneAnswer({ type: 'noul', noul: 0.9 }, { form: 'noul' })
1540
+ * // { form: 'noul', noul: 0.9 }
1541
+ * ```
1542
+ */
1543
+ function extractSystemOneAnswer(answer, question) {
1544
+ const bounded = boundsOf(0, 1);
1545
+ if (answer.type === "noul" && question.form === "noul") return bounded(answer.noul) ? {
1546
+ form: "noul",
1547
+ noul: answer.noul
1548
+ } : void 0;
1549
+ if (answer.type === "choice" && question.form === "choice") {
1550
+ const entries = [];
1551
+ for (const label of Object.keys(question.criteria)) {
1552
+ if (!Object.hasOwn(answer.probabilities, label)) return void 0;
1553
+ const probability = answer.probabilities[label];
1554
+ if (!bounded(probability)) return void 0;
1555
+ entries.push([label, probability]);
1556
+ }
1557
+ return {
1558
+ form: "choice",
1559
+ probabilities: Object.fromEntries(entries)
1560
+ };
1561
+ }
1562
+ if (answer.type !== "score" || question.form !== "score") return void 0;
1563
+ const probabilities = [];
1564
+ for (let level = 0; level < question.criteria.length; level++) {
1565
+ if (!Object.hasOwn(answer.probabilities, level)) return void 0;
1566
+ const probability = isArray(answer.probabilities) ? answer.probabilities[level] : answer.probabilities[String(level)];
1567
+ if (!bounded(probability)) return void 0;
1568
+ probabilities.push(probability);
1569
+ }
1570
+ return {
1571
+ form: "score",
1572
+ probabilities
1573
+ };
1574
+ }
1575
+ /**
1576
+ * Maps complete System One token counts onto validated token usage.
1577
+ *
1578
+ * @param usage - The optional wire token counts
1579
+ * @returns Token usage, or undefined for missing, null, negative, or non-finite counts
1580
+ * @example
1581
+ * ```ts
1582
+ * extractSystemOneUsage({ input_tokens: 975, output_tokens: 4 })
1583
+ * // { prompt: 975, completion: 4, total: 979 }
1584
+ * ```
1585
+ */
1586
+ function extractSystemOneUsage(usage) {
1587
+ const extracted = attempt(() => {
1588
+ if (usage === void 0) return void 0;
1589
+ const { input_tokens: prompt, output_tokens: completion } = usage;
1590
+ if (!isNumber(prompt) || !isNumber(completion)) return void 0;
1591
+ const result = {
1592
+ prompt,
1593
+ completion,
1594
+ total: prompt + completion
1595
+ };
1596
+ return isTokenUsage(result) ? result : void 0;
1597
+ });
1598
+ return extracted.success ? extracted.value : void 0;
1599
+ }
1262
1600
  //#endregion
1263
1601
  //#region src/core/conversations/Conversation.ts
1264
1602
  /**
@@ -2109,7 +2447,8 @@ var Agent = class {
2109
2447
  broke = true;
2110
2448
  break;
2111
2449
  }
2112
- const advertised = filterAllowList(this.#context.scope?.tools, tools.definitions(), (definition) => definition.name);
2450
+ const names = this.#context.scope?.tools?.slice();
2451
+ const advertised = filterAllowList(names, tools.definitions(), (definition) => definition.name);
2113
2452
  const definitions = advertised.length > 0 ? advertised : void 0;
2114
2453
  let charged = 0;
2115
2454
  let turnContent = "";
@@ -2163,8 +2502,10 @@ var Agent = class {
2163
2502
  usage: resultUsage
2164
2503
  };
2165
2504
  }
2166
- if (result.tools !== void 0 && result.tools.length > 0) {
2167
- const authorization = this.#authorize(result.tools);
2505
+ if (definitions === void 0) for (const call of result.tools ?? []) this.#emitter.emit("deny", call, "no tool is advertised in the active scope");
2506
+ else if (result.tools !== void 0 && result.tools.length > 0) {
2507
+ const admitted = filterAllowList(names, result.tools, (call) => call.name);
2508
+ const authorization = this.#authorize(result.tools, admitted);
2168
2509
  if (abort.signal.aborted) {
2169
2510
  partial = true;
2170
2511
  broke = true;
@@ -2189,7 +2530,7 @@ var Agent = class {
2189
2530
  };
2190
2531
  const toolMessage = this.#context.messages.add({
2191
2532
  role: "tool",
2192
- content: outcomeResult.success ? JSON.stringify(outcomeResult.value) : outcomeResult.error
2533
+ content: outcomeResult.success ? typeof outcomeResult.value === "string" ? outcomeResult.value : JSON.stringify(outcomeResult.value) : outcomeResult.error
2193
2534
  });
2194
2535
  messages.push(toolMessage);
2195
2536
  }
@@ -2236,24 +2577,33 @@ var Agent = class {
2236
2577
  messages.splice(0, messages.length, ...this.#context.build(this.#provider.format));
2237
2578
  return false;
2238
2579
  }
2239
- #authorize(calls) {
2240
- if (this.#authority === void 0) return void 0;
2580
+ #authorize(calls, admitted) {
2241
2581
  const authority = this.#authority;
2242
- const allowed = [];
2582
+ const allowed = /* @__PURE__ */ new Map();
2243
2583
  const denials = /* @__PURE__ */ new Map();
2244
- for (const call of calls) {
2584
+ for (const [index, call] of calls.entries()) {
2585
+ if (!admitted.includes(call)) {
2586
+ const reason = `${call.name} is not in the active scope`;
2587
+ denials.set(index, denyCall(call, reason));
2588
+ this.#emitter.emit("deny", call, reason);
2589
+ continue;
2590
+ }
2591
+ if (authority === void 0) {
2592
+ allowed.set(index, call);
2593
+ continue;
2594
+ }
2245
2595
  let decision;
2246
2596
  try {
2247
2597
  decision = authority.evaluate({ call });
2248
2598
  } catch (error) {
2249
2599
  const reason = errorToMessage(error);
2250
- denials.set(call.id, denyCall(call, reason));
2600
+ denials.set(index, denyCall(call, reason));
2251
2601
  this.#emitter.emit("deny", call, reason);
2252
2602
  continue;
2253
2603
  }
2254
- if (decision.allowed) allowed.push(call);
2604
+ if (decision.allowed) allowed.set(index, call);
2255
2605
  else {
2256
- denials.set(call.id, denyCall(call, decision.reason));
2606
+ denials.set(index, denyCall(call, decision.reason));
2257
2607
  this.#emitter.emit("deny", call, decision.reason);
2258
2608
  }
2259
2609
  }
@@ -2263,12 +2613,16 @@ var Agent = class {
2263
2613
  };
2264
2614
  }
2265
2615
  async #dispatch(tools, calls, signal, authorization) {
2266
- if (authorization === void 0) return tools.execute(calls, { signal });
2267
2616
  const { allowed, denials } = authorization;
2268
- const executed = allowed.length > 0 ? await tools.execute(allowed, { signal }) : [];
2269
- const byId = new Map(denials);
2270
- for (const result of executed) byId.set(result.id, result);
2271
- return calls.map((call) => byId.get(call.id) ?? denyCall(call, void 0));
2617
+ const executed = allowed.size > 0 ? await tools.execute([...allowed.values()], { signal }) : [];
2618
+ const results = new Map(denials);
2619
+ let offset = 0;
2620
+ for (const index of allowed.keys()) {
2621
+ const result = executed[offset];
2622
+ if (result !== void 0) results.set(index, result);
2623
+ offset += 1;
2624
+ }
2625
+ return calls.map((call, index) => results.get(index) ?? denyCall(call, void 0));
2272
2626
  }
2273
2627
  async *#provide(messages, signal, definitions, think, schema, onDelta) {
2274
2628
  const options = {};
@@ -2549,6 +2903,117 @@ function isConversationSnapshot(value) {
2549
2903
  if (!isArray(value.sections) || !value.sections.every(isSection)) return false;
2550
2904
  return isArray(value.messages) && value.messages.every(isMessage);
2551
2905
  }
2906
+ /**
2907
+ * Checks whether a value is a judge entry: a string, a JSON record, or a JSON array.
2908
+ *
2909
+ * @remarks
2910
+ * Total: a cycle, a non-JSON member, a class instance, and a hostile input return false. `null`
2911
+ * is not an entry; a criteria guard admits it where the protocol keeps it.
2912
+ *
2913
+ * @param value - The unknown entry candidate
2914
+ * @returns True if the value is a string or JSON structure the model can read; false otherwise
2915
+ * @example
2916
+ * ```ts
2917
+ * isJudgeEntry('Customer asks for a refund') // true
2918
+ * isJudgeEntry({ ticket: 4182, tags: ['billing'] }) // true
2919
+ * isJudgeEntry(null) // false
2920
+ * isJudgeEntry({ opened: new Date() }) // false
2921
+ * ```
2922
+ */
2923
+ function isJudgeEntry(value) {
2924
+ if (isString(value)) return true;
2925
+ return (isRecord(value) || isArray(value)) && isJSONValue(value);
2926
+ }
2927
+ /**
2928
+ * Checks whether a value is a well-formed judge question of the choice, score, or noul form.
2929
+ *
2930
+ * @remarks
2931
+ * Total: a hostile input returns false. A choice needs at least 2 options and a score at least 2
2932
+ * levels, because the confidence formula divides by the candidate count; a description or a level
2933
+ * can be `null`. `instructions` and noul `criteria` are omitted when absent and never `null`.
2934
+ * Server limits on option, level, and question counts are left to the server.
2935
+ *
2936
+ * @param value - The unknown question candidate
2937
+ * @returns True if the value is a question the judge engine can send; false otherwise
2938
+ * @example
2939
+ * ```ts
2940
+ * isJudgeQuestion({ form: 'choice', criteria: { billing: null, bug: 'Software defect' } }) // true
2941
+ * isJudgeQuestion({ form: 'score', criteria: ['Cosmetic', null, 'Blocking'] }) // true
2942
+ * isJudgeQuestion({ form: 'noul', instructions: 'Is a refund owed?' }) // true
2943
+ * isJudgeQuestion({ form: 'choice', criteria: { billing: null } }) // false
2944
+ * ```
2945
+ */
2946
+ function isJudgeQuestion(value) {
2947
+ const checked = attempt(() => {
2948
+ if (!isRecord(value)) return false;
2949
+ const { form, instructions, criteria } = value;
2950
+ if (instructions !== void 0 && !isJudgeEntry(instructions)) return false;
2951
+ if (form === "choice") return isRecord(criteria) && Object.keys(criteria).length >= 2 && Object.values(criteria).every(nullableOf(isJudgeEntry));
2952
+ if (form === "score") return arrayOf(nullableOf(isJudgeEntry))(criteria) && criteria.length >= 2;
2953
+ if (form !== "noul") return false;
2954
+ if (criteria === void 0) return true;
2955
+ if (!isRecord(criteria)) return false;
2956
+ return optionalOf(isJudgeEntry)(criteria.true) && optionalOf(isJudgeEntry)(criteria.false);
2957
+ });
2958
+ return checked.success && checked.value;
2959
+ }
2960
+ /**
2961
+ * Checks whether a value is a System One response envelope with optional model and usage.
2962
+ *
2963
+ * @remarks
2964
+ * Answers remain unknown until checked against their questions. Missing and null usage counts
2965
+ * are accepted. Extra members are ignored, and unreadable fields return false.
2966
+ *
2967
+ * @param value - The unknown response candidate
2968
+ * @returns True if the envelope fields have their wire types; false otherwise
2969
+ * @example
2970
+ * ```ts
2971
+ * isSystemOneResponse({ answers: {}, usage: { input_tokens: null } }) // true
2972
+ * ```
2973
+ */
2974
+ function isSystemOneResponse(value) {
2975
+ const checked = attempt(() => {
2976
+ if (!isRecord(value)) return false;
2977
+ const { model, answers, usage } = value;
2978
+ if (!optionalOf(isString)(model) || !isRecord(answers)) return false;
2979
+ if (usage === void 0) return true;
2980
+ if (!isRecord(usage)) return false;
2981
+ return optionalOf(nullableOf(isNumber))(usage.input_tokens) && optionalOf(nullableOf(isNumber))(usage.output_tokens);
2982
+ });
2983
+ return checked.success && checked.value;
2984
+ }
2985
+ /**
2986
+ * Checks whether a value is a System One answer whose type and distribution the wire can read.
2987
+ *
2988
+ * @remarks
2989
+ * The guard checks `type` and the distribution the wire dereferences: the `noul` number for a
2990
+ * noul, the `probabilities` map for a choice, and the `probabilities` map or dense array for a
2991
+ * score, each probability a finite number in [0, 1]. The `choice`, `score`, `confidence`, and
2992
+ * `legend` members are carried unchecked, because the wire never reads them, so a value this
2993
+ * guard accepts can hold any value in those members. Distribution sums are not constrained;
2994
+ * values are never normalized. Hostile reads return false.
2995
+ *
2996
+ * @param value - The unknown answer candidate
2997
+ * @returns True if the type and the distribution satisfy the wire contract; false otherwise
2998
+ * @example
2999
+ * ```ts
3000
+ * isSystemOneAnswer({ type: 'noul', noul: 0.9 }) // true
3001
+ * isSystemOneAnswer({ type: 'score', probabilities: [0.2, 0.8] }) // true
3002
+ * isSystemOneAnswer({ type: 'noul', noul: 1.1 }) // false
3003
+ * ```
3004
+ */
3005
+ function isSystemOneAnswer(value) {
3006
+ const checked = attempt(() => {
3007
+ if (!isRecord(value)) return false;
3008
+ const { type } = value;
3009
+ if (type === "noul") return boundsOf(0, 1)(value.noul);
3010
+ if (type !== "choice" && type !== "score") return false;
3011
+ const { probabilities } = value;
3012
+ if (type === "score" && isArray(probabilities)) return arrayOf(boundsOf(0, 1))(probabilities);
3013
+ return isRecord(probabilities) && Object.values(probabilities).every(boundsOf(0, 1));
3014
+ });
3015
+ return checked.success && checked.value;
3016
+ }
2552
3017
  //#endregion
2553
3018
  //#region src/core/conversations/stores/DatabaseConversationStore.ts
2554
3019
  /**
@@ -2900,7 +3365,7 @@ var AgentProvider = class {
2900
3365
  };
2901
3366
  }
2902
3367
  async #request(request, signal) {
2903
- const headers = await this.#requestHeaders(signal);
3368
+ const headers = await readHeaders(this.#headers, signal);
2904
3369
  signal.throwIfAborted();
2905
3370
  const response = await this.#transport(this.#url + this.#path, {
2906
3371
  method: "POST",
@@ -2923,25 +3388,6 @@ var AgentProvider = class {
2923
3388
  }
2924
3389
  return response;
2925
3390
  }
2926
- async #requestHeaders(signal) {
2927
- const headers = new Headers({ "Content-Type": "application/json" });
2928
- if (this.#headers === void 0) return headers;
2929
- const cleanup = new AbortController();
2930
- const aborted = Promise.withResolvers();
2931
- signal.addEventListener("abort", () => aborted.reject(signal.reason), {
2932
- once: true,
2933
- signal: cleanup.signal
2934
- });
2935
- try {
2936
- signal.throwIfAborted();
2937
- const hook = this.#headers;
2938
- const entries = await Promise.race([Promise.resolve().then(() => hook(signal)), aborted.promise]);
2939
- for (const [key, value] of Object.entries(entries)) headers.set(key, value);
2940
- return headers;
2941
- } finally {
2942
- cleanup.abort();
2943
- }
2944
- }
2945
3391
  };
2946
3392
  //#endregion
2947
3393
  //#region src/core/providers/RelayProvider.ts
@@ -3088,6 +3534,258 @@ var RelayProvider = class extends AgentProvider {
3088
3534
  }
3089
3535
  };
3090
3536
  //#endregion
3537
+ //#region src/core/AgentJudge.ts
3538
+ /**
3539
+ * Implements the bounded HTTP calls, validation, and result merging of a judge behind concrete
3540
+ * wire seams.
3541
+ *
3542
+ * @remarks
3543
+ * A subclass fills `name`, `body`, and `read`; the constructor takes the `batch` switch that
3544
+ * decides whether one call carries every question or each question gets its own call in key
3545
+ * order. `ask` validates the request before any call, builds every body before the first call so
3546
+ * a wire limit refuses the request before inference, and runs the calls one after another, each
3547
+ * under its own deadline folded with the caller's signal. A cancel throws a `JudgeAbortError`
3548
+ * whose `partial` merges the calls that completed. A transport error reaches the caller unchanged.
3549
+ *
3550
+ * @example Writing a judge wire
3551
+ * ```ts
3552
+ * import type { JudgeRequest, JudgeResult } from '@orkestrel/agent'
3553
+ * import { AgentJudge, JudgeError } from '@orkestrel/agent'
3554
+ * import { isFiniteNumber, isRecord } from '@orkestrel/contract'
3555
+ *
3556
+ * // A wire whose server answers one yes/no question per call as { "yes": 0.93 }.
3557
+ * class YesJudge extends AgentJudge {
3558
+ * readonly name = 'yes'
3559
+ * body(request: JudgeRequest): object {
3560
+ * return { model: this.model, state: request.state, questions: request.questions }
3561
+ * }
3562
+ * read(value: unknown, request: JudgeRequest): JudgeResult {
3563
+ * const [id] = Object.keys(request.questions)
3564
+ * if (id === undefined || !isRecord(value) || !isFiniteNumber(value.yes)) {
3565
+ * throw new JudgeError('PROTOCOL', 'judge error: unreadable answer')
3566
+ * }
3567
+ * return { model: this.model, answers: { [id]: { form: 'noul', noul: value.yes } } }
3568
+ * }
3569
+ * }
3570
+ *
3571
+ * const judge = new YesJudge({
3572
+ * url: 'http://localhost:8010',
3573
+ * path: '/v1/yes',
3574
+ * model: 'yes-1',
3575
+ * batch: false,
3576
+ * })
3577
+ * ```
3578
+ */
3579
+ var AgentJudge = class {
3580
+ #id;
3581
+ #url;
3582
+ #path;
3583
+ #model;
3584
+ #timeout;
3585
+ #transport;
3586
+ #headers;
3587
+ #batch;
3588
+ constructor(input) {
3589
+ this.#id = crypto.randomUUID();
3590
+ this.#url = input.url;
3591
+ this.#path = input.path ?? "";
3592
+ this.#model = input.model;
3593
+ this.#timeout = input.timeout ?? 12e4;
3594
+ this.#transport = input.fetch ?? globalThis.fetch.bind(globalThis);
3595
+ this.#headers = input.headers;
3596
+ this.#batch = input.batch ?? true;
3597
+ }
3598
+ /** Exposes the instance's minted UUID. */
3599
+ get id() {
3600
+ return this.#id;
3601
+ }
3602
+ /** Exposes the configured model identity. */
3603
+ get model() {
3604
+ return this.#model;
3605
+ }
3606
+ /**
3607
+ * Asks every question of the request about its state and merges the answers of every call.
3608
+ *
3609
+ * @param request - The state and the questions keyed by caller id
3610
+ * @param signal - The caller's cancellation bound
3611
+ * @returns The merged answers, refusals, and usage of every call
3612
+ * @throws JudgeAbortError Thrown when the caller's signal or a call's deadline fires, carrying
3613
+ * the merged result of the completed calls
3614
+ * @throws JudgeError Thrown with code `QUESTION` for an empty question map, a malformed question
3615
+ * or state, or a wire refusal before any call; with code `HTTP` for a non-OK response; and with
3616
+ * code `PROTOCOL` for a missing or unparsable response body
3617
+ */
3618
+ async ask(request, signal) {
3619
+ if (signal.aborted) throw new JudgeAbortError(buildJudgeResult(this.#model, []));
3620
+ const state = copyJSON(request.state);
3621
+ if (!isJudgeEntry(state)) throw new JudgeError("QUESTION", "judge error: state is not a judge entry");
3622
+ const questions = isRecord(request.questions) ? Object.entries(request.questions) : [];
3623
+ if (questions.length === 0) throw new JudgeError("QUESTION", "judge error: no questions");
3624
+ const entries = [];
3625
+ for (const [id, question] of questions) {
3626
+ const copy = copyJSON(question);
3627
+ if (!isJudgeQuestion(copy)) throw new JudgeError("QUESTION", `judge error: question ${id} is malformed`);
3628
+ entries.push([id, copy]);
3629
+ }
3630
+ const owned = {
3631
+ state,
3632
+ questions: Object.fromEntries(entries)
3633
+ };
3634
+ const calls = (this.#batch ? [owned] : entries.map(([id, question]) => ({
3635
+ state,
3636
+ questions: { [id]: question }
3637
+ }))).map((part) => ({
3638
+ request: part,
3639
+ body: JSON.stringify(this.body(part))
3640
+ }));
3641
+ const results = [];
3642
+ for (const call of calls) results.push(await this.#call(call.request, call.body, signal, results));
3643
+ return buildJudgeResult(this.#model, results);
3644
+ }
3645
+ async #call(request, body, signal, completed) {
3646
+ const timeout = new Timeout({ ms: this.#timeout });
3647
+ timeout.start();
3648
+ const combined = AbortSignal.any([timeout.signal, signal]);
3649
+ try {
3650
+ const headers = await readHeaders(this.#headers, combined);
3651
+ combined.throwIfAborted();
3652
+ const response = await this.#transport(this.#url + this.#path, {
3653
+ method: "POST",
3654
+ headers,
3655
+ body,
3656
+ signal: combined
3657
+ });
3658
+ if (!response.ok) {
3659
+ let detail;
3660
+ try {
3661
+ detail = response.body === null ? "" : (await readText(response.body, MAX_ERROR_BODY_LENGTH, combined)).text;
3662
+ } catch (cause) {
3663
+ throw new JudgeError("HTTP", `judge error: ${response.status} - (error body unavailable)`, {
3664
+ status: response.status,
3665
+ cause
3666
+ });
3667
+ }
3668
+ combined.throwIfAborted();
3669
+ throw new JudgeError("HTTP", `judge error: ${response.status}${detail === "" ? "" : ` - ${detail}`}`, { status: response.status });
3670
+ }
3671
+ if (response.body === null) throw new JudgeError("PROTOCOL", "judge error: no response body");
3672
+ const text = await readText(response.body, void 0, combined);
3673
+ combined.throwIfAborted();
3674
+ const value = parseJSON(text.text);
3675
+ if (value === void 0) throw new JudgeError("PROTOCOL", "judge error: invalid JSON body");
3676
+ const result = this.read(value, request);
3677
+ combined.throwIfAborted();
3678
+ return result;
3679
+ } catch (error) {
3680
+ if (combined.aborted) throw new JudgeAbortError(buildJudgeResult(this.#model, completed), error === combined.reason ? void 0 : { cause: error });
3681
+ throw error;
3682
+ } finally {
3683
+ timeout.clear();
3684
+ }
3685
+ }
3686
+ };
3687
+ //#endregion
3688
+ //#region src/core/providers/SystemOneJudge.ts
3689
+ /**
3690
+ * Carries judge questions over the System One protocol and derives answers from server distributions.
3691
+ *
3692
+ * @remarks
3693
+ * The caller supplies the server origin and model. Every question travels in one request.
3694
+ * Server measures are ignored; the response model is preserved.
3695
+ * `headers` supplies authentication through the shared judge engine.
3696
+ *
3697
+ * @example Asking a System One server a choice, a noul, and a score
3698
+ * ```ts
3699
+ * import { computeReading, createSystemOneJudge } from '@orkestrel/agent'
3700
+ *
3701
+ * const judge = createSystemOneJudge({ url: 'http://localhost:11434', model: 'tev1:0.8b' })
3702
+ * const result = await judge.ask(
3703
+ * {
3704
+ * state: 'Our checkout has returned 500 errors since 9am. I want a refund for today.',
3705
+ * questions: {
3706
+ * label: {
3707
+ * form: 'choice',
3708
+ * instructions: 'Which label fits this ticket?',
3709
+ * criteria: { billing: 'Payments and refunds', bug: 'Software errors', account: null },
3710
+ * },
3711
+ * refund: {
3712
+ * form: 'noul',
3713
+ * instructions: 'Does the customer ask for money back?',
3714
+ * criteria: {
3715
+ * true: 'The customer asks for a refund or for money back.',
3716
+ * false: 'The customer does not ask for money back.',
3717
+ * },
3718
+ * },
3719
+ * severity: {
3720
+ * form: 'score',
3721
+ * instructions: 'How severe is the reported issue?',
3722
+ * criteria: ['Cosmetic; no impact', 'Degraded, workaround exists', 'Blocking; no workaround'],
3723
+ * },
3724
+ * },
3725
+ * },
3726
+ * AbortSignal.timeout(30_000),
3727
+ * )
3728
+ * const readings = Object.fromEntries(
3729
+ * Object.entries(result.answers).map(([id, answer]) => [id, computeReading(answer)]),
3730
+ * )
3731
+ * result.model // 'tev1:0.8b' — the model the server named
3732
+ * result.usage // { prompt: 975, completion: 4, total: 979 }
3733
+ * readings.label // { winner: 'bug', probability: 0.9691, confidence: 0.9536 } to four decimals
3734
+ * readings.refund // { winner: 'true', probability: 0.9979, confidence: 0.9958 } to four decimals
3735
+ * readings.severity // { winner: '1', probability: 0.9494, confidence: 0.9241, score: 0.9919 } to four decimals
3736
+ * ```
3737
+ */
3738
+ var SystemOneJudge = class extends AgentJudge {
3739
+ constructor(options) {
3740
+ super({
3741
+ ...options,
3742
+ path: SYSTEM_ONE_PATH,
3743
+ batch: true
3744
+ });
3745
+ }
3746
+ /** Identifies the System One protocol. */
3747
+ name = "systemone";
3748
+ /**
3749
+ * Projects the state and questions onto the System One request with the configured model.
3750
+ *
3751
+ * @param request - The state and questions keyed by caller id
3752
+ * @returns The System One request body
3753
+ */
3754
+ body(request) {
3755
+ return {
3756
+ state: request.state,
3757
+ model: this.model,
3758
+ questions: Object.fromEntries(Object.entries(request.questions).map(([id, question]) => [id, questionToSystemOne(question)]))
3759
+ };
3760
+ }
3761
+ /**
3762
+ * Decodes requested System One answers and reports the server model and available usage.
3763
+ *
3764
+ * @param value - The parsed response body
3765
+ * @param request - The questions defining the expected answers
3766
+ * @returns The decoded distributions, model, and available usage
3767
+ * @throws JudgeError Thrown with code `PROTOCOL` for a malformed envelope or an invalid answer naming its question id
3768
+ */
3769
+ read(value, request) {
3770
+ if (!isSystemOneResponse(value)) throw new JudgeError("PROTOCOL", "judge error: invalid System One response envelope");
3771
+ const entries = [];
3772
+ for (const [id, question] of Object.entries(request.questions)) {
3773
+ if (!Object.hasOwn(value.answers, id)) throw new JudgeError("PROTOCOL", `judge error: question ${id} has no System One answer`);
3774
+ const wire = value.answers[id];
3775
+ if (!isSystemOneAnswer(wire)) throw new JudgeError("PROTOCOL", `judge error: question ${id} has an invalid System One answer`);
3776
+ const answer = extractSystemOneAnswer(wire, question);
3777
+ if (answer === void 0) throw new JudgeError("PROTOCOL", `judge error: question ${id} has a mismatched or incomplete System One answer`);
3778
+ entries.push([id, answer]);
3779
+ }
3780
+ const usage = extractSystemOneUsage(value.usage);
3781
+ return {
3782
+ model: value.model ?? this.model,
3783
+ answers: Object.fromEntries(entries),
3784
+ ...usage === void 0 ? {} : { usage }
3785
+ };
3786
+ }
3787
+ };
3788
+ //#endregion
3091
3789
  //#region src/core/RelayStream.ts
3092
3790
  /**
3093
3791
  * Streams a provider call as validated NDJSON frames under response backpressure.
@@ -4125,7 +4823,20 @@ function createAgentRunner(options) {
4125
4823
  handler: handleAgentRunnerJob.bind(void 0, registry, partial)
4126
4824
  });
4127
4825
  }
4826
+ /**
4827
+ * Creates a judge that sends every question through the configured System One server.
4828
+ *
4829
+ * @param options - The server origin, model, and optional transport, headers, and timeout
4830
+ * @returns The configured judge behind its shared interface
4831
+ * @example
4832
+ * ```ts
4833
+ * const judge = createSystemOneJudge({ url: 'http://localhost:11434', model: 'tev1:0.8b' })
4834
+ * ```
4835
+ */
4836
+ function createSystemOneJudge(options) {
4837
+ return new SystemOneJudge(options);
4838
+ }
4128
4839
  //#endregion
4129
- export { Agent, AgentContext, AgentError, AgentJobError, AgentProvider, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DEFAULT_PROVIDER_TIMEOUT, DEFAULT_RELAY_LIMIT, DatabaseConversationStore, IMAGE_TOKEN_ESTIMATE, INVALID_RELAY_STATUS, Instruction, InstructionManager, MAX_ERROR_BODY_LENGTH, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, OVERSIZED_RELAY_STATUS, ProviderAbortError, ProviderError, RELAY_CONTENT_TYPE, RELAY_PROVIDER_MESSAGE, RelayProvider, RelayStream, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, UNAUTHORIZED_RELAY_STATUS, UPSTREAM_RELAY_STATUS, WORKSPACE_SECTION_HEADER, agentResultToJSON, assembleResult, attachImages, attachUserImages, buildProviderResult, buildRecapMessage, buildSummaryMessage, collectImageData, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createChannel, createConversation, createConversationManager, createDatabaseConversationStore, createInstruction, createInstructionManager, createMemoryConversationStore, createRelay, createRelayProvider, createScope, createScopeManager, createThinkSplitter, denyCall, estimateMessages, estimateTokens, filterAllowList, handleAgentQueueJob, handleAgentRunnerJob, intersectKeys, isAgentError, isAgentJobError, isConversationError, isConversationSnapshot, isMessage, isProviderAbortError, isProviderError, isSection, joinThinking, messageContract, messageShape, providerRequestContract, providerRequestShape, providerResultContract, providerResultShape, readChunks, readText, relayFrameContract, relayFrameShape, renderFencedFile, renderSection, resolveClose, resolveItem, resolveOpen, sanitizeToken, sanitizeUsage, settleAgentJob, sumUsage, toolCallShape };
4840
+ export { Agent, AgentContext, AgentError, AgentJobError, AgentJudge, AgentProvider, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DEFAULT_PROVIDER_TIMEOUT, DEFAULT_RELAY_LIMIT, DatabaseConversationStore, IMAGE_TOKEN_ESTIMATE, INVALID_RELAY_STATUS, Instruction, InstructionManager, JudgeAbortError, JudgeError, MAX_ERROR_BODY_LENGTH, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, OVERSIZED_RELAY_STATUS, ProviderAbortError, ProviderError, RELAY_CONTENT_TYPE, RELAY_PROVIDER_MESSAGE, RelayProvider, RelayStream, SYSTEM_ONE_PATH, Scope, ScopeManager, SystemOneJudge, THINK_CLOSE, THINK_OPEN, ThinkSplitter, UNAUTHORIZED_RELAY_STATUS, UPSTREAM_RELAY_STATUS, WORKSPACE_SECTION_HEADER, agentResultToJSON, assembleResult, attachImages, attachUserImages, buildJudgeResult, buildProviderResult, buildRecapMessage, buildSummaryMessage, collectImageData, computeReading, copyJSON, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createChannel, createConversation, createConversationManager, createDatabaseConversationStore, createInstruction, createInstructionManager, createMemoryConversationStore, createRelay, createRelayProvider, createScope, createScopeManager, createSystemOneJudge, createThinkSplitter, denyCall, estimateMessages, estimateTokens, extractSystemOneAnswer, extractSystemOneUsage, filterAllowList, handleAgentQueueJob, handleAgentRunnerJob, intersectKeys, isAgentError, isAgentJobError, isConversationError, isConversationSnapshot, isJudgeAbortError, isJudgeEntry, isJudgeError, isJudgeQuestion, isMessage, isProviderAbortError, isProviderError, isSection, isSystemOneAnswer, isSystemOneResponse, joinThinking, messageContract, messageShape, providerRequestContract, providerRequestShape, providerResultContract, providerResultShape, questionToSystemOne, readChunks, readHeaders, readText, relayFrameContract, relayFrameShape, renderFencedFile, renderSection, resolveClose, resolveItem, resolveOpen, sanitizeToken, sanitizeUsage, settleAgentJob, sumUsage, toolCallShape };
4130
4841
 
4131
4842
  //# sourceMappingURL=index.js.map