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