@orkestrel/agent 0.0.35 → 0.0.37

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,11 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _orkestrel_contract = require("@orkestrel/contract");
3
3
  let _orkestrel_budget = require("@orkestrel/budget");
4
+ let _orkestrel_tool = require("@orkestrel/tool");
4
5
  let _orkestrel_ndjson = require("@orkestrel/ndjson");
5
6
  let _orkestrel_timeout = require("@orkestrel/timeout");
6
7
  let _orkestrel_database = require("@orkestrel/database");
7
8
  let _orkestrel_emitter = require("@orkestrel/emitter");
8
- let _orkestrel_tool = require("@orkestrel/tool");
9
9
  let _orkestrel_workspace = require("@orkestrel/workspace");
10
10
  let _orkestrel_queue = require("@orkestrel/queue");
11
11
  let _orkestrel_workflow = require("@orkestrel/workflow");
@@ -105,6 +105,23 @@ var toolCallShape = (0, _orkestrel_contract.objectShape)({
105
105
  arguments: (0, _orkestrel_contract.recordShape)((0, _orkestrel_contract.jsonShape)())
106
106
  });
107
107
  /**
108
+ * Describes a message input's JSON projection: a {@link messageShape} with no `id`, which the
109
+ * storing conversation mints.
110
+ *
111
+ * @remarks
112
+ * A conversation's `add` checks a hoisted hand guard on every message and reads this shape's `audit`
113
+ * only to fill a refusal's faults. A job's seed messages cross it at `build`. The role literal admits
114
+ * `system`, so the conversation's `ROLE` refusal explains that case.
115
+ */
116
+ var messageInputShape = (0, _orkestrel_contract.objectShape)({
117
+ role: (0, _orkestrel_contract.literalShape)(MESSAGE_ROLES),
118
+ content: (0, _orkestrel_contract.stringShape)(),
119
+ calls: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)(toolCallShape)),
120
+ call: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
121
+ images: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)())),
122
+ thinking: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)())
123
+ });
124
+ /**
108
125
  * Describes a conversation message's JSON wire projection.
109
126
  *
110
127
  * @remarks
@@ -121,20 +138,83 @@ var messageShape = (0, _orkestrel_contract.objectShape)({
121
138
  thinking: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)())
122
139
  });
123
140
  /**
141
+ * Describes the text or JSON a judge reads: a non-empty string, a non-empty JSON array, or a JSON record.
142
+ *
143
+ * @remarks
144
+ * A shape has no `minProperties`, so this one admits the empty record that `isJudgeEntry` refuses;
145
+ * the guard is the door, and this shape only fills a refusal's faults.
146
+ */
147
+ var judgeEntryShape = (0, _orkestrel_contract.unionShape)((0, _orkestrel_contract.stringShape)({ min: 1 }), (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.jsonShape)(), { min: 1 }), (0, _orkestrel_contract.recordShape)((0, _orkestrel_contract.jsonShape)()));
148
+ /**
149
+ * Describes a judge question: one exact object per form, the schema an LLM-facing wire can publish.
150
+ *
151
+ * @remarks
152
+ * `isJudgeQuestion` is the door; this shape fills a refusal's faults. A choice needing at least 2
153
+ * options is the guard's alone, because a shape has no `minProperties`.
154
+ */
155
+ var judgeQuestionShape = (0, _orkestrel_contract.unionShape)((0, _orkestrel_contract.objectShape)({
156
+ form: (0, _orkestrel_contract.literalShape)(["choice"]),
157
+ instructions: (0, _orkestrel_contract.optionalShape)(judgeEntryShape),
158
+ criteria: (0, _orkestrel_contract.recordShape)((0, _orkestrel_contract.nullableShape)(judgeEntryShape))
159
+ }), (0, _orkestrel_contract.objectShape)({
160
+ form: (0, _orkestrel_contract.literalShape)(["score"]),
161
+ instructions: (0, _orkestrel_contract.optionalShape)(judgeEntryShape),
162
+ criteria: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.nullableShape)(judgeEntryShape), { min: 2 })
163
+ }), (0, _orkestrel_contract.objectShape)({
164
+ form: (0, _orkestrel_contract.literalShape)(["noul"]),
165
+ instructions: (0, _orkestrel_contract.optionalShape)(judgeEntryShape),
166
+ criteria: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.objectShape)({
167
+ true: (0, _orkestrel_contract.optionalShape)(judgeEntryShape),
168
+ false: (0, _orkestrel_contract.optionalShape)(judgeEntryShape)
169
+ }))
170
+ }));
171
+ /**
172
+ * Describes a judge answer: one exact object per form, each probability in `[0, 1]`.
173
+ *
174
+ * @remarks
175
+ * The ties an answer holds to its question run at the write doors through `scanJudgmentTies`, which a
176
+ * shape over the answer alone cannot state.
177
+ */
178
+ var judgeAnswerShape = (0, _orkestrel_contract.unionShape)((0, _orkestrel_contract.objectShape)({
179
+ form: (0, _orkestrel_contract.literalShape)(["choice"]),
180
+ probabilities: (0, _orkestrel_contract.recordShape)((0, _orkestrel_contract.numberShape)({
181
+ min: 0,
182
+ max: 1
183
+ }))
184
+ }), (0, _orkestrel_contract.objectShape)({
185
+ form: (0, _orkestrel_contract.literalShape)(["score"]),
186
+ probabilities: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.numberShape)({
187
+ min: 0,
188
+ max: 1
189
+ }))
190
+ }), (0, _orkestrel_contract.objectShape)({
191
+ form: (0, _orkestrel_contract.literalShape)(["noul"]),
192
+ noul: (0, _orkestrel_contract.numberShape)({
193
+ min: 0,
194
+ max: 1
195
+ })
196
+ }));
197
+ /**
124
198
  * Describes a token usage record's JSON wire projection — its `prompt`, `completion`, and `total`
125
199
  * counts, shared by the provider result and the agent frame wires.
126
200
  *
127
201
  * @remarks
128
- * The guard refuses extra members; the contract parser drops them.
202
+ * Each count is a nonnegative number. The guard refuses extra members; the contract parser drops them.
129
203
  */
130
204
  var tokenUsageShape = (0, _orkestrel_contract.objectShape)({
131
- prompt: (0, _orkestrel_contract.numberShape)(),
132
- completion: (0, _orkestrel_contract.numberShape)(),
133
- total: (0, _orkestrel_contract.numberShape)()
205
+ prompt: (0, _orkestrel_contract.numberShape)({ min: 0 }),
206
+ completion: (0, _orkestrel_contract.numberShape)({ min: 0 }),
207
+ total: (0, _orkestrel_contract.numberShape)({ min: 0 })
134
208
  });
135
209
  // src/core/contracts.ts
136
210
  /** Validates and projects conversation messages at a JSON wire boundary. */
137
211
  var messageContract = (0, _orkestrel_contract.createContract)(messageShape);
212
+ /** Reads the faults of a refused judge question. */
213
+ var judgeQuestionContract = (0, _orkestrel_contract.createContract)(judgeQuestionShape);
214
+ /** Reads the faults of a refused judge answer. */
215
+ var judgeAnswerContract = (0, _orkestrel_contract.createContract)(judgeAnswerShape);
216
+ /** Reads the faults of a refused message input and validates a job's seed messages. */
217
+ var messageInputContract = (0, _orkestrel_contract.createContract)(messageInputShape);
138
218
  // src/core/cloners.ts
139
219
  /**
140
220
  * Owns a value by serializing it to JSON and parsing the text, so the copy shares nothing with its source.
@@ -281,6 +361,168 @@ function stripThinking(messages, replay) {
281
361
  });
282
362
  }
283
363
  /**
364
+ * Estimates the context-token footprint of a string — the deterministic `ceil(length / 4)`
365
+ * character heuristic {@link import('./agents/helpers.js').estimateMessages} sums over a conversation's
366
+ * messages (the default context-budget estimator).
367
+ *
368
+ * @remarks
369
+ * Approximates `ceil(length / 4)` (4 characters per token), so the same input always yields the same estimate (no model round-trip).
370
+ * Empty text is `0`. This is a planning heuristic for reasoning about how much a turn's
371
+ * messages cost the next request, not an exact tokenizer count — it never calls a provider,
372
+ * so the agent layer stays provider-agnostic and synchronous where it can be.
373
+ *
374
+ * @param text - The text to estimate (a section summary, a message's content)
375
+ * @returns The estimated token count (`ceil(text.length / 4)`; `0` for empty text)
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * estimateTokens('') // 0
380
+ * estimateTokens('hello') // 2 (ceil(5 / 4))
381
+ * estimateTokens('a'.repeat(40)) // 10
382
+ * ```
383
+ */
384
+ function estimateTokens(text) {
385
+ return Math.ceil(text.length / 4);
386
+ }
387
+ /**
388
+ * Describes a snapshot a store refuses, naming its id, the first fault, and what a host does about it.
389
+ *
390
+ * @remarks
391
+ * A store's row is the host's table, so the text tells the host to read the row itself and rewrite it,
392
+ * or to delete it, rather than reading a refused row as absent, which would let `open` overwrite it.
393
+ * The id and each fault segment are JSON-encoded and length-capped.
394
+ *
395
+ * @param noun - What the snapshot holds, such as `conversation`
396
+ * @param id - The id the row is keyed by
397
+ * @param faults - The faults of the snapshot's shape
398
+ * @returns The refusal's text
399
+ * @example
400
+ * ```ts
401
+ * describeSnapshotRefusal('conversation', 'c1', [{ reason: 'extra', path: ['summary'] }])
402
+ * // 'conversation "c1" holds a snapshot this version refuses (extra key "summary" at the root; ...'
403
+ * ```
404
+ */
405
+ function describeSnapshotRefusal(noun, id, faults) {
406
+ const [first] = faults;
407
+ const path = first === void 0 ? [] : (0, _orkestrel_contract.isString)(first.path) ? [first.path] : first.path;
408
+ const held = (0, _orkestrel_contract.preview)(id);
409
+ return `${noun} ${held} holds a snapshot this version refuses (${first?.reason === "extra" ? `extra key ${(0, _orkestrel_contract.preview)(path.at(-1))} at ${path.length === 1 ? "the root" : (0, _orkestrel_contract.preview)(path.slice(0, -1).join("."))}` : first === void 0 ? "a value its shape cannot name" : `${first.reason} at ${path.length === 0 ? "the root" : (0, _orkestrel_contract.preview)(path.join("."))}`}; a snapshot written before this change can carry it); read the row through your own table and ${first?.reason === "extra" ? "rewrite it without the key" : "rewrite it to the declared shape"}, or delete it with store.delete(${held})`;
410
+ }
411
+ /**
412
+ * Describes why a judge question refuses, in the text both of its doors throw.
413
+ *
414
+ * @remarks
415
+ * The rule travels in the text because the question shape cannot state it: a record has no minimum
416
+ * size, so a single-option choice refuses at the guard with no fault for the list to name.
417
+ *
418
+ * @param id - The question's id
419
+ * @param faults - What the question shape's audit found
420
+ * @param schema - The question shape's JSON Schema
421
+ * @returns The refusal text, with the fault list on the lines after it
422
+ * @example
423
+ * ```ts
424
+ * describeQuestionRefusal('team', [], judgeQuestionContract.schema)
425
+ * // 'question team is malformed; a choice names two or more criteria, a score two or more levels, and a noul only true and false'
426
+ * ```
427
+ */
428
+ function describeQuestionRefusal(id, faults, schema) {
429
+ const refusal = `question ${id} is malformed; a choice names two or more criteria, a score two or more levels, and a noul only true and false`;
430
+ return faults.length === 0 ? refusal : `${refusal}\n${(0, _orkestrel_contract.describeFaults)(faults, schema)}`;
431
+ }
432
+ /**
433
+ * Scans what a judge's answer must hold to its question: the same form, every probability in
434
+ * `[0, 1]`, a choice's keys equal to the criteria keys, and a score as long as its levels.
435
+ *
436
+ * @remarks
437
+ * These ties run where a judgment is written, at `JudgmentManager.add` and the `AgentJudge` result
438
+ * ingestion. A read of a stored record checks only exact keys, literal forms, and number types,
439
+ * because records written before the ties can miss them.
440
+ *
441
+ * @param question - The question that was asked
442
+ * @param answer - The answer the judge gave
443
+ * @returns Every broken tie as a fault, or an empty list
444
+ * @example
445
+ * ```ts
446
+ * scanJudgmentTies({ form: 'noul' }, { form: 'noul', noul: 2 })
447
+ * // [{ reason: 'constraint', path: ['answer', 'noul'], expected: 'number', constraint: 'max', limit: 1, received: '2' }]
448
+ * ```
449
+ */
450
+ function scanJudgmentTies(question, answer) {
451
+ if (question.form === "noul" && answer.form === "noul") return scanProbability(answer.noul, ["answer", "noul"]);
452
+ if (question.form === "choice" && answer.form === "choice") {
453
+ const wanted = Object.keys(question.criteria);
454
+ const held = Object.keys(answer.probabilities);
455
+ return [
456
+ ...wanted.filter((key) => !Object.hasOwn(answer.probabilities, key)).map((key) => ({
457
+ reason: "missing",
458
+ path: [
459
+ "answer",
460
+ "probabilities",
461
+ key
462
+ ],
463
+ expected: "number"
464
+ })),
465
+ ...held.filter((key) => !Object.hasOwn(question.criteria, key)).map((key) => ({
466
+ reason: "extra",
467
+ path: [
468
+ "answer",
469
+ "probabilities",
470
+ key
471
+ ]
472
+ })),
473
+ ...held.flatMap((key) => scanProbability(answer.probabilities[key], [
474
+ "answer",
475
+ "probabilities",
476
+ key
477
+ ]))
478
+ ];
479
+ }
480
+ if (question.form === "score" && answer.form === "score") {
481
+ const levels = question.criteria.length;
482
+ const count = answer.probabilities.length;
483
+ return [...count === levels ? [] : [{
484
+ reason: "constraint",
485
+ path: ["answer", "probabilities"],
486
+ expected: "array",
487
+ constraint: count < levels ? "min" : "max",
488
+ limit: levels,
489
+ received: String(count)
490
+ }], ...answer.probabilities.flatMap((value, index) => scanProbability(value, [
491
+ "answer",
492
+ "probabilities",
493
+ String(index)
494
+ ]))];
495
+ }
496
+ return [{
497
+ reason: "type",
498
+ path: ["answer", "form"],
499
+ expected: "literal",
500
+ received: answer.form
501
+ }];
502
+ }
503
+ /**
504
+ * Scans one probability against `[0, 1]`.
505
+ *
506
+ * @param value - The probability
507
+ * @param path - Where it sits
508
+ * @returns A constraint fault when it is out of bounds, or an empty list
509
+ * @example
510
+ * ```ts
511
+ * scanProbability(0.5, ['answer', 'noul']) // []
512
+ * ```
513
+ */
514
+ function scanProbability(value, path) {
515
+ if (value !== void 0 && value >= 0 && value <= 1) return [];
516
+ return [{
517
+ reason: "constraint",
518
+ path,
519
+ expected: "number",
520
+ constraint: value !== void 0 && value < 0 ? "min" : "max",
521
+ limit: value !== void 0 && value < 0 ? 0 : 1,
522
+ received: String(value)
523
+ }];
524
+ }
525
+ /**
284
526
  * Sanitizes one reported token count into a non-negative integer — a non-finite or
285
527
  * non-positive value becomes `0`, and a positive fractional value floors down.
286
528
  *
@@ -299,7 +541,8 @@ function sanitizeToken(value) {
299
541
  /**
300
542
  * Sanitizes a {@link TokenUsage} into non-negative integers — the guard an agent's
301
543
  * abort-usage path applies to a provider's partial usage before it is charged against a
302
- * budget or folded into the run total.
544
+ * budget or folded into the run total, because a cancelled stream has no settled result to refuse;
545
+ * a settled result's usage crosses the provider-result door instead.
303
546
  *
304
547
  * @remarks
305
548
  * Per field (`prompt` / `completion` / `total`): a non-finite value (`NaN`, `+Infinity`,
@@ -579,14 +822,16 @@ function extractLiterals(text) {
579
822
  }
580
823
  // src/core/validators.ts
581
824
  /**
582
- * Checks whether a value is a judge entry: a string, a JSON record, or a JSON array.
825
+ * Checks whether a value is a judge entry: a non-empty string, a non-empty JSON record, or a non-empty
826
+ * JSON array.
583
827
  *
584
828
  * @remarks
585
- * Total: a cycle, a non-JSON member, a class instance, and an untrusted input return false. `null`
586
- * is not an entry; a criteria guard admits it where the protocol keeps it.
829
+ * Total: a cycle, a non-JSON member, a class instance, and an untrusted input return false. `''`,
830
+ * `[]`, and `{}` give a judge nothing to read, so they return false. `null` is not an entry; a
831
+ * criteria guard admits it where the protocol keeps it.
587
832
  *
588
833
  * @param value - The unknown entry candidate
589
- * @returns True if the value is a string or JSON structure the model can read; false otherwise
834
+ * @returns True if the value is a non-empty string or JSON structure the model can read; false otherwise
590
835
  * @example
591
836
  * ```ts
592
837
  * isJudgeEntry('Customer asks for a refund') // true
@@ -596,8 +841,9 @@ function extractLiterals(text) {
596
841
  * ```
597
842
  */
598
843
  function isJudgeEntry(value) {
599
- if ((0, _orkestrel_contract.isString)(value)) return true;
600
- return ((0, _orkestrel_contract.isRecord)(value) || (0, _orkestrel_contract.isArray)(value)) && (0, _orkestrel_contract.isJSONValue)(value);
844
+ if ((0, _orkestrel_contract.isString)(value)) return value.length > 0;
845
+ if ((0, _orkestrel_contract.isArray)(value)) return (0, _orkestrel_contract.isJSONValue)(value) && value.length > 0;
846
+ return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isJSONValue)(value) && Object.keys(value).length > 0;
601
847
  }
602
848
  /**
603
849
  * Checks whether a value is a well-formed judge question of the choice, score, or noul form.
@@ -605,8 +851,10 @@ function isJudgeEntry(value) {
605
851
  * @remarks
606
852
  * Total: an untrusted input returns false. A choice needs at least 2 options and a score at least 2
607
853
  * levels, because the confidence formula divides by the candidate count; a description or a level
608
- * can be `null`. `instructions` and noul `criteria` are omitted when absent and never `null`.
609
- * Server limits on option, level, and question counts are left to the server.
854
+ * can be `null`. `instructions` and noul `criteria` are omitted when absent and never `null`. The
855
+ * guard is exact on the question's own keys: an unknown member, and an own `undefined` optional,
856
+ * return false, as `judgeQuestionContract` does. Server limits on option, level, and question counts
857
+ * are left to the server.
610
858
  *
611
859
  * @param value - The unknown question candidate
612
860
  * @returns True if the value is a question the judge engine can send; false otherwise
@@ -623,16 +871,60 @@ function isJudgeQuestion(value) {
623
871
  if (!(0, _orkestrel_contract.isRecord)(value)) return false;
624
872
  const { form, instructions, criteria } = value;
625
873
  if (instructions !== void 0 && !isJudgeEntry(instructions)) return false;
874
+ const owned = 1 + (instructions === void 0 ? 0 : 1) + (criteria === void 0 ? 0 : 1);
875
+ if (Object.keys(value).length !== owned) return false;
626
876
  if (form === "choice") return (0, _orkestrel_contract.isRecord)(criteria) && Object.keys(criteria).length >= 2 && Object.values(criteria).every((0, _orkestrel_contract.nullableOf)(isJudgeEntry));
627
877
  if (form === "score") return (0, _orkestrel_contract.arrayOf)((0, _orkestrel_contract.nullableOf)(isJudgeEntry))(criteria) && criteria.length >= 2;
628
878
  if (form !== "noul") return false;
629
879
  if (criteria === void 0) return true;
630
880
  if (!(0, _orkestrel_contract.isRecord)(criteria)) return false;
631
- return (0, _orkestrel_contract.optionalOf)(isJudgeEntry)(criteria.true) && (0, _orkestrel_contract.optionalOf)(isJudgeEntry)(criteria.false);
881
+ const held = (criteria.true === void 0 ? 0 : 1) + (criteria.false === void 0 ? 0 : 1);
882
+ return Object.keys(criteria).length === held && (0, _orkestrel_contract.optionalOf)(isJudgeEntry)(criteria.true) && (0, _orkestrel_contract.optionalOf)(isJudgeEntry)(criteria.false);
632
883
  });
633
884
  return checked.success && checked.value;
634
885
  }
635
886
  /**
887
+ * Checks whether a value is a stored judge answer of the choice, score, or noul form.
888
+ *
889
+ * @remarks
890
+ * Total: an untrusted input returns false. The guard is exact on the answer's own keys and reads each
891
+ * probability as a finite number, with noul bounded to `[0, 1]`. Whether the answer fits
892
+ * its question belongs to the write. Every
893
+ * judgment of a hydrate reads this guard, so it counts keys by hand rather than through a built one,
894
+ * and `judgeAnswerContract` holds it in lockstep: an answer the contract accepts passes here.
895
+ *
896
+ * @param value - The value to test
897
+ * @returns True if the value has the shape of a {@link JudgeAnswer}; false otherwise
898
+ * @example
899
+ * ```ts
900
+ * isJudgeAnswer({ form: 'noul', noul: 0.9 }) // true
901
+ * isJudgeAnswer({ form: 'choice', probabilities: { billing: 1 } }) // true
902
+ * isJudgeAnswer({ form: 'noul', noul: 0.9, note: 'extra' }) // false
903
+ * ```
904
+ */
905
+ function isJudgeAnswer(value) {
906
+ return (0, _orkestrel_contract.holds)(() => {
907
+ if (!(0, _orkestrel_contract.isRecord)(value) || Object.keys(value).length !== 2) return false;
908
+ const { form, noul, probabilities } = value;
909
+ if (form === "noul") return (0, _orkestrel_contract.isFiniteNumber)(noul) && noul >= 0 && noul <= 1;
910
+ if (form === "score") return (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isFiniteNumber)(probabilities);
911
+ return form === "choice" && (0, _orkestrel_contract.isRecord)(probabilities) && Object.values(probabilities).every(_orkestrel_contract.isFiniteNumber);
912
+ });
913
+ }
914
+ /**
915
+ * Checks whether a value is a positive safe integer.
916
+ * @param value - The value to inspect
917
+ * @returns True if the value is a positive safe integer; false otherwise
918
+ * @example
919
+ * ```ts
920
+ * isPositiveSafeInteger(4096) // true
921
+ * isPositiveSafeInteger(0) // false
922
+ * ```
923
+ */
924
+ function isPositiveSafeInteger(value) {
925
+ return (0, _orkestrel_contract.isFiniteNumber)(value) && Number.isSafeInteger(value) && value > 0;
926
+ }
927
+ /**
636
928
  * Checks whether a value is a non-negative safe integer, `-0` included.
637
929
  *
638
930
  * @remarks
@@ -792,7 +1084,7 @@ function isProviderAbortError(value) {
792
1084
  *
793
1085
  * @remarks
794
1086
  * The provider base throws this error for a non-OK HTTP response, a missing response
795
- * body, or a missing settled result when strict assembly is enabled, and with code `'PROTOCOL'`
1087
+ * body, a stream that ended with no settled result and no terminal record, a result or delta the published contract refuses, and with code `'PROTOCOL'`
796
1088
  * when the framing seam's `parse` or `finish` throws anything but a provider error, with the throw
797
1089
  * as `cause`. Concrete wire decoders also use it for malformed records and relayed provider
798
1090
  * failures.
@@ -803,12 +1095,15 @@ var ProviderError = class extends Error {
803
1095
  code;
804
1096
  /** Holds the response status for an HTTP failure, or undefined for other codes. */
805
1097
  status;
1098
+ /** Holds what the refusal found, such as its faults, or undefined when it carries none. */
1099
+ context;
806
1100
  constructor(code, message, options) {
807
1101
  super(message, options);
808
1102
  this.name = "ProviderError";
809
1103
  _orkestrel_contract.INTRINSICS.define(this, Symbol.for("@orkestrel/agent.ProviderError"), { value: this });
810
1104
  this.code = code;
811
1105
  this.status = options?.status;
1106
+ this.context = options?.context;
812
1107
  }
813
1108
  };
814
1109
  /**
@@ -827,7 +1122,8 @@ function isProviderError(value) {
827
1122
  const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
828
1123
  if (!(code === "HTTP" || code === "PROTOCOL" || code === "PROVIDER")) return false;
829
1124
  const status = _orkestrel_contract.INTRINSICS.reflect.read(value, "status");
830
- return status === void 0 || (0, _orkestrel_contract.isNumber)(status);
1125
+ const context = _orkestrel_contract.INTRINSICS.reflect.read(value, "context");
1126
+ return (status === void 0 || (0, _orkestrel_contract.isNumber)(status)) && (context === void 0 || (0, _orkestrel_contract.isObject)(context) || (0, _orkestrel_contract.isFunction)(context));
831
1127
  });
832
1128
  }
833
1129
  /**
@@ -844,12 +1140,15 @@ var JudgeError = class extends Error {
844
1140
  code;
845
1141
  /** Holds the response status for an HTTP failure, or undefined for other codes. */
846
1142
  status;
1143
+ /** Holds the values that locate the failure, such as the `faults` of a refused question, or `undefined` when none were supplied. */
1144
+ context;
847
1145
  constructor(code, message, options) {
848
1146
  super(message, options);
849
1147
  this.name = "JudgeError";
850
1148
  _orkestrel_contract.INTRINSICS.define(this, Symbol.for("@orkestrel/agent.JudgeError"), { value: this });
851
1149
  this.code = code;
852
1150
  this.status = options?.status;
1151
+ this.context = options?.context;
853
1152
  }
854
1153
  };
855
1154
  /**
@@ -868,7 +1167,8 @@ function isJudgeError(value) {
868
1167
  const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
869
1168
  if (!(code === "HTTP" || code === "PROTOCOL" || code === "QUESTION")) return false;
870
1169
  const status = _orkestrel_contract.INTRINSICS.reflect.read(value, "status");
871
- return status === void 0 || (0, _orkestrel_contract.isNumber)(status);
1170
+ const context = _orkestrel_contract.INTRINSICS.reflect.read(value, "context");
1171
+ return (status === void 0 || (0, _orkestrel_contract.isNumber)(status)) && (context === void 0 || (0, _orkestrel_contract.isObject)(context) || (0, _orkestrel_contract.isFunction)(context));
872
1172
  });
873
1173
  }
874
1174
  // src/core/providers/shapers.ts
@@ -934,6 +1234,100 @@ var providerRequestContract = (0, _orkestrel_contract.createContract)(providerRe
934
1234
  var providerResultContract = (0, _orkestrel_contract.createContract)(providerResultShape);
935
1235
  /** Validates and projects channel-discriminated relay frames at a JSON wire boundary. */
936
1236
  var relayFrameContract = (0, _orkestrel_contract.createContract)(relayFrameShape);
1237
+ // src/core/providers/validators.ts
1238
+ /**
1239
+ * Checks whether a value carries the members {@link ProviderResult} publishes: a string `content`, and
1240
+ * `thinking`, `tools`, `usage`, and `truncated` absent or of their published types.
1241
+ *
1242
+ * @remarks
1243
+ * A provider is a foreign interface, so the guard is open: unknown members and a class instance pass,
1244
+ * and each `usage` count is a finite nonnegative number with no integer check. An unreadable member
1245
+ * returns false. {@link import('./helpers.js').requireProviderResult} runs it on an owned copy, the one
1246
+ * door a run's result crosses.
1247
+ *
1248
+ * @param value - The unknown result candidate
1249
+ * @returns True if the value satisfies the published result; false otherwise
1250
+ *
1251
+ * @example
1252
+ * ```ts
1253
+ * isProviderResult({ content: 'answer', extra: true }) // true
1254
+ * isProviderResult({}) // false
1255
+ * isProviderResult({ content: 'answer', usage: { prompt: -1, completion: 0, total: 0 } }) // false
1256
+ * ```
1257
+ */
1258
+ var isProviderResult = (0, _orkestrel_contract.objectOf)({
1259
+ content: _orkestrel_contract.isString,
1260
+ thinking: _orkestrel_contract.isString,
1261
+ tools: (0, _orkestrel_contract.arrayOf)(_orkestrel_tool.isToolCall),
1262
+ usage: (0, _orkestrel_contract.objectOf)({
1263
+ prompt: _orkestrel_budget.isBudgetAmount,
1264
+ completion: _orkestrel_budget.isBudgetAmount,
1265
+ total: _orkestrel_budget.isBudgetAmount
1266
+ }),
1267
+ truncated: _orkestrel_contract.isBoolean
1268
+ }, [
1269
+ "thinking",
1270
+ "tools",
1271
+ "usage",
1272
+ "truncated"
1273
+ ]);
1274
+ /**
1275
+ * Checks whether a value is a System One response envelope with optional model and usage.
1276
+ *
1277
+ * @remarks
1278
+ * Answers remain unknown until checked against their questions. Missing and null usage counts
1279
+ * are accepted. Extra members are ignored, and unreadable fields return false.
1280
+ *
1281
+ * @param value - The unknown response candidate
1282
+ * @returns True if the envelope fields have their wire types; false otherwise
1283
+ * @example
1284
+ * ```ts
1285
+ * isSystemOneResponse({ answers: {}, usage: { input_tokens: null } }) // true
1286
+ * ```
1287
+ */
1288
+ function isSystemOneResponse(value) {
1289
+ const checked = (0, _orkestrel_contract.attempt)(() => {
1290
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
1291
+ const { model, answers, usage } = value;
1292
+ if (!(0, _orkestrel_contract.optionalOf)(_orkestrel_contract.isString)(model) || !(0, _orkestrel_contract.isRecord)(answers)) return false;
1293
+ if (usage === void 0) return true;
1294
+ if (!(0, _orkestrel_contract.isRecord)(usage)) return false;
1295
+ 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);
1296
+ });
1297
+ return checked.success && checked.value;
1298
+ }
1299
+ /**
1300
+ * Checks whether a value is a System One answer whose type and distribution the wire can read.
1301
+ *
1302
+ * @remarks
1303
+ * The guard checks `type` and the distribution the wire dereferences: the `noul` number for a
1304
+ * noul, the `probabilities` map for a choice, and the `probabilities` map or dense array for a
1305
+ * score, each probability a finite number in [0, 1]. The `choice`, `score`, `confidence`, and
1306
+ * `legend` members are carried unchecked, because the wire never reads them, so a value this
1307
+ * guard accepts can hold any value in those members. Distribution sums are not constrained;
1308
+ * values are never normalized. A read that throws returns false.
1309
+ *
1310
+ * @param value - The unknown answer candidate
1311
+ * @returns True if the type and the distribution satisfy the wire contract; false otherwise
1312
+ * @example
1313
+ * ```ts
1314
+ * isSystemOneAnswer({ type: 'noul', noul: 0.9 }) // true
1315
+ * isSystemOneAnswer({ type: 'score', probabilities: [0.2, 0.8] }) // true
1316
+ * isSystemOneAnswer({ type: 'noul', noul: 1.1 }) // false
1317
+ * ```
1318
+ */
1319
+ function isSystemOneAnswer(value) {
1320
+ const checked = (0, _orkestrel_contract.attempt)(() => {
1321
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
1322
+ const { type } = value;
1323
+ if (type === "noul") return (0, _orkestrel_contract.boundsOf)(0, 1)(value.noul);
1324
+ if (type !== "choice" && type !== "score") return false;
1325
+ const { probabilities } = value;
1326
+ if (type === "score" && (0, _orkestrel_contract.isArray)(probabilities)) return (0, _orkestrel_contract.arrayOf)((0, _orkestrel_contract.boundsOf)(0, 1))(probabilities);
1327
+ return (0, _orkestrel_contract.isRecord)(probabilities) && Object.values(probabilities).every((0, _orkestrel_contract.boundsOf)(0, 1));
1328
+ });
1329
+ return checked.success && checked.value;
1330
+ }
937
1331
  // src/core/providers/helpers.ts
938
1332
  /**
939
1333
  * Assembles a provider result with only populated optional fields.
@@ -959,6 +1353,76 @@ function buildProviderResult(content, thinking, tools, usage, truncated) {
959
1353
  };
960
1354
  }
961
1355
  /**
1356
+ * Reads a foreign delta's two members once into an owned copy, the value a run checks and consumes.
1357
+ *
1358
+ * @remarks
1359
+ * A stream checks every delta, so this is a hand read rather than a record guard. Reading each member
1360
+ * once means a getter can't pass the check with one value and hand the run another. Unknown members
1361
+ * and a class instance pass, and an unreadable member reads as no delta.
1362
+ *
1363
+ * @param value - The value the stream yielded
1364
+ * @returns The owned delta, or undefined when the value is not one
1365
+ *
1366
+ * @example
1367
+ * ```ts
1368
+ * readProviderDelta({ channel: 'content', text: 'Hi' }) // { channel: 'content', text: 'Hi' }
1369
+ * readProviderDelta({ channel: 'audio', text: 'Hi' }) // undefined
1370
+ * ```
1371
+ */
1372
+ function readProviderDelta(value) {
1373
+ if (typeof value !== "object" || value === null) return void 0;
1374
+ try {
1375
+ const { channel, text } = value;
1376
+ return (channel === "content" || channel === "thinking") && typeof text === "string" ? {
1377
+ channel,
1378
+ text
1379
+ } : void 0;
1380
+ } catch {
1381
+ return;
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Owns a provider's settled result: reads each published member of the foreign value once into a fresh
1386
+ * record, checks that copy, and returns it, or throws a coded error the run rejects with.
1387
+ *
1388
+ * @remarks
1389
+ * The check is {@link isProviderResult}, so a class instance carrying the published members passes and
1390
+ * an unknown member is left behind. `usage` is copied count by count; `tools` keeps the array the
1391
+ * provider returned. An absent `usage` stays absent, which the loop bills as its estimate.
1392
+ *
1393
+ * @param value - The value a provider's stream returned
1394
+ * @returns The owned result
1395
+ * @throws {ProviderError} Thrown when the value does not carry the published members (code `PROTOCOL`)
1396
+ *
1397
+ * @example
1398
+ * ```ts
1399
+ * requireProviderResult({ content: 'answer', extra: true }) // { content: 'answer' }
1400
+ * requireProviderResult({}) // throws ProviderError ('PROTOCOL')
1401
+ * ```
1402
+ */
1403
+ function requireProviderResult(value) {
1404
+ const owned = (0, _orkestrel_contract.attempt)(() => {
1405
+ if (!(0, _orkestrel_contract.isObject)(value) && !(0, _orkestrel_contract.isFunction)(value)) return void 0;
1406
+ const thinking = Reflect.get(value, "thinking");
1407
+ const tools = Reflect.get(value, "tools");
1408
+ const usage = Reflect.get(value, "usage");
1409
+ const truncated = Reflect.get(value, "truncated");
1410
+ return {
1411
+ content: Reflect.get(value, "content"),
1412
+ ...thinking === void 0 ? {} : { thinking },
1413
+ ...tools === void 0 ? {} : { tools },
1414
+ ...usage === void 0 ? {} : { usage: (0, _orkestrel_contract.isObject)(usage) ? {
1415
+ prompt: Reflect.get(usage, "prompt"),
1416
+ completion: Reflect.get(usage, "completion"),
1417
+ total: Reflect.get(usage, "total")
1418
+ } : usage },
1419
+ ...truncated === void 0 ? {} : { truncated }
1420
+ };
1421
+ });
1422
+ if (owned.success && isProviderResult(owned.value)) return owned.value;
1423
+ throw new ProviderError("PROTOCOL", `provider error: a provider must return string content, with thinking, tools, usage, and truncated absent or of their published types, and usage counts finite and nonnegative; received ${(0, _orkestrel_contract.preview)(owned.success ? owned.value : value)}`, owned.success ? void 0 : { cause: owned.error });
1424
+ }
1425
+ /**
962
1426
  * Cancels a stream reader and releases its lock, swallowing a cancellation failure so the
963
1427
  * caller's own outcome stands.
964
1428
  *
@@ -1779,7 +2243,8 @@ var ThinkSplitter = class {
1779
2243
  * have no size limit. A qwen3 implicit-open reclassification corrects the final
1780
2244
  * content while content deltas already yielded cannot be recalled. A subclass fills
1781
2245
  * `name`, `frame`, `encode`, `read`, and `finish`; the constructor takes the `split`
1782
- * and `strict` switches to control reasoning separation and settled-result requirements.
2246
+ * and `strict` switches to control reasoning separation and whether a stream must end with a settled
2247
+ * `result` or a `done` record, which it must by default.
1783
2248
  * The `replay` input names which stored thinking the agent and relay send back. Default: `'none'`.
1784
2249
  * A throw from the framing seam (`parse`, or `finish`) becomes a `ProviderError` with the
1785
2250
  * `PROTOCOL` code whose `cause` is the throw, unless it is already a `ProviderError` or
@@ -1811,7 +2276,8 @@ var ThinkSplitter = class {
1811
2276
  * class TextProvider extends AgentProvider<string> {
1812
2277
  * readonly name = 'text'
1813
2278
  * constructor(options: TextOptions) {
1814
- * super({ ...options, path: '/generate' })
2279
+ * // Raw text has no terminal record, so a stream that stops reads as complete.
2280
+ * super({ ...options, path: '/generate', strict: false })
1815
2281
  * }
1816
2282
  * frame(): ProviderParserInterface<string> {
1817
2283
  * return new TextFrame()
@@ -1847,7 +2313,7 @@ var AgentProvider = class {
1847
2313
  this.#transport = input.fetch ?? globalThis.fetch.bind(globalThis);
1848
2314
  this.#headers = input.headers;
1849
2315
  this.#split = input.split ?? true;
1850
- this.#strict = input.strict ?? false;
2316
+ this.#strict = input.strict ?? true;
1851
2317
  }
1852
2318
  /** Exposes the instance's minted UUID. */
1853
2319
  get id() {
@@ -1942,7 +2408,7 @@ var AgentProvider = class {
1942
2408
  text: tail
1943
2409
  };
1944
2410
  combined.throwIfAborted();
1945
- if (this.#strict) throw new ProviderError("PROTOCOL", "provider error: missing settled result");
2411
+ if (this.#strict && increments.at(-1)?.done !== true) throw new ProviderError("PROTOCOL", "provider error: the stream ended with no settled result and no terminal record; the response was cut, or a wire with no terminal record passes strict: false");
1946
2412
  return this.#assemble(splitter, increments);
1947
2413
  } catch (error) {
1948
2414
  throw this.#translate(error, combined, splitter, increments);
@@ -2130,7 +2596,9 @@ var RelayProvider = class extends AgentProvider {
2130
2596
  * order. `ask` validates the request before any call, builds every body before the first call so
2131
2597
  * a wire limit refuses the request before inference, and runs the calls one after another, each
2132
2598
  * under its own deadline folded with the caller's signal. A cancel throws a `JudgeAbortError`
2133
- * whose `partial` merges the calls that completed. A transport error reaches the caller unchanged.
2599
+ * whose `partial` merges the calls that completed. A transport error reaches the caller unchanged. An
2600
+ * answer that does not fit its question throws code `PROTOCOL`, and a call whose `read` reports no
2601
+ * usage bills the estimate of its response text.
2134
2602
  *
2135
2603
  * @example Writing a judge wire
2136
2604
  * ```ts
@@ -2208,7 +2676,13 @@ var AgentJudge = class {
2208
2676
  if (questions.length === 0) throw new JudgeError("QUESTION", "judge error: no questions");
2209
2677
  const entries = questions.map(([id, question]) => {
2210
2678
  const copy = copyJSON(question);
2211
- if (!isJudgeQuestion(copy)) throw new JudgeError("QUESTION", `judge error: question ${id} is malformed`);
2679
+ if (!isJudgeQuestion(copy)) {
2680
+ const faults = judgeQuestionContract.audit(copy);
2681
+ throw new JudgeError("QUESTION", `judge error: ${describeQuestionRefusal(id, faults, judgeQuestionContract.schema)}`, { context: {
2682
+ question: id,
2683
+ faults
2684
+ } });
2685
+ }
2212
2686
  return [id, copy];
2213
2687
  });
2214
2688
  const owned = {
@@ -2250,7 +2724,24 @@ var AgentJudge = class {
2250
2724
  if (value === void 0) throw new JudgeError("PROTOCOL", "judge error: invalid JSON body");
2251
2725
  const result = this.read(value, request);
2252
2726
  combined.throwIfAborted();
2253
- return result;
2727
+ for (const [id, answer] of Object.entries(result.answers)) {
2728
+ const question = Object.hasOwn(request.questions, id) ? request.questions[id] : void 0;
2729
+ const faults = question === void 0 ? [] : scanJudgmentTies(question, answer);
2730
+ if (faults.length > 0) throw new JudgeError("PROTOCOL", `judge error: the answer to question ${id} does not fit it\n${(0, _orkestrel_contract.describeFaults)(faults, judgeAnswerContract.schema)}`, { context: {
2731
+ question: id,
2732
+ faults
2733
+ } });
2734
+ }
2735
+ if (result.usage !== void 0) return result;
2736
+ const estimate = estimateTokens(text.text);
2737
+ return {
2738
+ ...result,
2739
+ usage: {
2740
+ prompt: 0,
2741
+ completion: estimate,
2742
+ total: estimate
2743
+ }
2744
+ };
2254
2745
  } catch (error) {
2255
2746
  if (combined.aborted) throw new JudgeAbortError(buildJudgeResult(this.#model, completed), error === combined.reason ? void 0 : { cause: error });
2256
2747
  throw error;
@@ -2259,75 +2750,17 @@ var AgentJudge = class {
2259
2750
  }
2260
2751
  }
2261
2752
  };
2262
- // src/core/providers/validators.ts
2753
+ // src/core/providers/SystemOneJudge.ts
2263
2754
  /**
2264
- * Checks whether a value is a System One response envelope with optional model and usage.
2755
+ * Carries judge questions over the System One protocol and derives answers from server distributions.
2265
2756
  *
2266
2757
  * @remarks
2267
- * Answers remain unknown until checked against their questions. Missing and null usage counts
2268
- * are accepted. Extra members are ignored, and unreadable fields return false.
2758
+ * The caller supplies the server origin and model. Every question travels in one request.
2759
+ * Server measures are ignored; the response model is preserved.
2760
+ * `headers` supplies authentication through the shared judge engine.
2761
+ * The example readings are those of the response Ollama 0.40.0 returned for that request on 2026-10-07.
2269
2762
  *
2270
- * @param value - The unknown response candidate
2271
- * @returns True if the envelope fields have their wire types; false otherwise
2272
- * @example
2273
- * ```ts
2274
- * isSystemOneResponse({ answers: {}, usage: { input_tokens: null } }) // true
2275
- * ```
2276
- */
2277
- function isSystemOneResponse(value) {
2278
- const checked = (0, _orkestrel_contract.attempt)(() => {
2279
- if (!(0, _orkestrel_contract.isRecord)(value)) return false;
2280
- const { model, answers, usage } = value;
2281
- if (!(0, _orkestrel_contract.optionalOf)(_orkestrel_contract.isString)(model) || !(0, _orkestrel_contract.isRecord)(answers)) return false;
2282
- if (usage === void 0) return true;
2283
- if (!(0, _orkestrel_contract.isRecord)(usage)) return false;
2284
- 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);
2285
- });
2286
- return checked.success && checked.value;
2287
- }
2288
- /**
2289
- * Checks whether a value is a System One answer whose type and distribution the wire can read.
2290
- *
2291
- * @remarks
2292
- * The guard checks `type` and the distribution the wire dereferences: the `noul` number for a
2293
- * noul, the `probabilities` map for a choice, and the `probabilities` map or dense array for a
2294
- * score, each probability a finite number in [0, 1]. The `choice`, `score`, `confidence`, and
2295
- * `legend` members are carried unchecked, because the wire never reads them, so a value this
2296
- * guard accepts can hold any value in those members. Distribution sums are not constrained;
2297
- * values are never normalized. A read that throws returns false.
2298
- *
2299
- * @param value - The unknown answer candidate
2300
- * @returns True if the type and the distribution satisfy the wire contract; false otherwise
2301
- * @example
2302
- * ```ts
2303
- * isSystemOneAnswer({ type: 'noul', noul: 0.9 }) // true
2304
- * isSystemOneAnswer({ type: 'score', probabilities: [0.2, 0.8] }) // true
2305
- * isSystemOneAnswer({ type: 'noul', noul: 1.1 }) // false
2306
- * ```
2307
- */
2308
- function isSystemOneAnswer(value) {
2309
- const checked = (0, _orkestrel_contract.attempt)(() => {
2310
- if (!(0, _orkestrel_contract.isRecord)(value)) return false;
2311
- const { type } = value;
2312
- if (type === "noul") return (0, _orkestrel_contract.boundsOf)(0, 1)(value.noul);
2313
- if (type !== "choice" && type !== "score") return false;
2314
- const { probabilities } = value;
2315
- if (type === "score" && (0, _orkestrel_contract.isArray)(probabilities)) return (0, _orkestrel_contract.arrayOf)((0, _orkestrel_contract.boundsOf)(0, 1))(probabilities);
2316
- return (0, _orkestrel_contract.isRecord)(probabilities) && Object.values(probabilities).every((0, _orkestrel_contract.boundsOf)(0, 1));
2317
- });
2318
- return checked.success && checked.value;
2319
- }
2320
- // src/core/providers/SystemOneJudge.ts
2321
- /**
2322
- * Carries judge questions over the System One protocol and derives answers from server distributions.
2323
- *
2324
- * @remarks
2325
- * The caller supplies the server origin and model. Every question travels in one request.
2326
- * Server measures are ignored; the response model is preserved.
2327
- * `headers` supplies authentication through the shared judge engine.
2328
- * The example readings are those of the response Ollama 0.40.0 returned for that request on 2026-10-07.
2329
- *
2330
- * @example Asking a System One server a choice, a noul, and a score
2763
+ * @example Asking a System One server a choice, a noul, and a score
2331
2764
  * ```ts
2332
2765
  * import { measureAnswer, createSystemOneJudge } from '@orkestrel/agent'
2333
2766
  *
@@ -2401,11 +2834,11 @@ var SystemOneJudge = class extends AgentJudge {
2401
2834
  * the message names the question id of an invalid answer
2402
2835
  */
2403
2836
  read(value, request) {
2404
- if (!isSystemOneResponse(value)) throw new JudgeError("PROTOCOL", "judge error: invalid System One response envelope");
2837
+ if (!isSystemOneResponse(value)) throw new JudgeError("PROTOCOL", `judge error: invalid System One response envelope; received ${(0, _orkestrel_contract.preview)(value)}`);
2405
2838
  const entries = Object.entries(request.questions).map(([id, question]) => {
2406
2839
  if (!Object.hasOwn(value.answers, id)) throw new JudgeError("PROTOCOL", `judge error: question ${id} has no System One answer`);
2407
2840
  const wire = value.answers[id];
2408
- if (!isSystemOneAnswer(wire)) throw new JudgeError("PROTOCOL", `judge error: question ${id} has an invalid System One answer`);
2841
+ if (!isSystemOneAnswer(wire)) throw new JudgeError("PROTOCOL", `judge error: question ${id} has an invalid System One answer; received ${(0, _orkestrel_contract.preview)(wire)}`);
2409
2842
  const answer = extractSystemOneAnswer(wire, question);
2410
2843
  if (answer === void 0) throw new JudgeError("PROTOCOL", `judge error: question ${id} has a mismatched or incomplete System One answer`);
2411
2844
  return [id, answer];
@@ -2625,12 +3058,12 @@ var CONVERSATION_RECAP_PREFIX = "[Summary of earlier messages] ";
2625
3058
  // src/core/conversations/errors.ts
2626
3059
  /**
2627
3060
  * Reports a conversation with no {@link ConversationSummaryHandler} to fold its messages with, a
2628
- * `sections` cap below `1`, a `keep` that is not a nonnegative safe integer, a summarizer digest
3061
+ * `sections` cap that is not a positive safe integer, a `keep` that is not a nonnegative safe integer, a summarizer digest
2629
3062
  * whose trimmed text is empty, a judgment or judge request that JSON cannot carry, a stored or
2630
3063
  * selected message whose role is not a conversation role, or a malformed snapshot — thrown by the
2631
3064
  * `compact()` and `add` methods of a {@link ConversationInterface}, its construction, or its
2632
3065
  * judgment store, or returned as the `fault` of an agent selection, carrying the machine-readable
2633
- * `code` `'SUMMARIZER' | 'SECTIONS' | 'JUDGMENT' | 'KEEP' | 'SUMMARY' | 'ROLE' | 'SNAPSHOT'`.
3066
+ * `code` `'SUMMARIZER' | 'SECTIONS' | 'JUDGMENT' | 'KEEP' | 'SUMMARY' | 'ROLE' | 'MESSAGE' | 'SNAPSHOT' | 'ID'`.
2634
3067
  *
2635
3068
  * @remarks
2636
3069
  * Compaction requires a summarizer (it digests the folded slice into a section summary); a
@@ -2638,7 +3071,7 @@ var CONVERSATION_RECAP_PREFIX = "[Summary of earlier messages] ";
2638
3071
  * is a programmer error and throws this with `'SUMMARIZER'`.
2639
3072
  * A `sections` cap (on {@link import('./types.js').ConversationOptions} /
2640
3073
  * {@link import('./types.js').ConversationManagerOptions} /
2641
- * {@link import('./types.js').CompactOptions}) must be `>= 1`, and cannot combine with
3074
+ * {@link import('./types.js').CompactOptions}) must be a positive safe integer, and cannot combine with
2642
3075
  * `cumulative` — either is a programmer error and throws this with `'SECTIONS'`. A `keep` on the same options must be a nonnegative
2643
3076
  * safe integer — any other value is a programmer error and throws this with `'KEEP'`. A judgment
2644
3077
  * record or a judge request that JSON cannot carry, or whose copy fails its guard, is a
@@ -2646,13 +3079,16 @@ var CONVERSATION_RECAP_PREFIX = "[Summary of earlier messages] ";
2646
3079
  * where one exists. A summarizer digest whose trimmed text is empty rejects the `compact()` with
2647
3080
  * `'SUMMARY'`. A stored message plays a `ConversationRole`: `add` refuses any other role with
2648
3081
  * `'ROLE'` before it stores any message of the batch, a hydrating snapshot that holds one throws
2649
- * `'ROLE'`, and an agent's selection that holds one becomes a `'ROLE'` fault. A hydrating snapshot
2650
- * that fails `isConversationSnapshot` or repeats an id throws `'SNAPSHOT'`; a judgment the guard
2651
- * admits but JSON cannot carry still throws `'JUDGMENT'`. Narrow a caught value with
3082
+ * `'ROLE'`, and an agent's selection that holds one becomes a `'ROLE'` fault. `add` refuses an input
3083
+ * whose other members fail `isMessageInput` with `'MESSAGE'`, its `context` holding the `faults`,
3084
+ * before it stores any message of the batch. A hydrating snapshot that fails
3085
+ * `isConversationSnapshot` or repeats an id, and a store row or written snapshot that does, throw
3086
+ * `'SNAPSHOT'`; a judgment the guard admits but JSON cannot carry still throws `'JUDGMENT'`. A
3087
+ * manager's `add` of an id it already holds throws `'ID'`. Narrow a caught value with
2652
3088
  * {@link isConversationError} and branch on `error.code`.
2653
3089
  */
2654
3090
  var ConversationError = class extends Error {
2655
- /** Names the machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer; `'SECTIONS'`: a sub-1 `sections` cap, or a cap combined with `cumulative`; `'JUDGMENT'`: a judgment or judge request that is not JSON; `'KEEP'`: a `keep` that is not a nonnegative safe integer; `'SUMMARY'`: a summarizer digest whose trimmed text is empty; `'ROLE'`: a stored or selected message whose role is not a `ConversationRole`; `'SNAPSHOT'`: a snapshot that fails `isConversationSnapshot` or repeats an id. */
3091
+ /** Names the machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer; `'SECTIONS'`: a `sections` cap that is not a positive safe integer, or a cap combined with `cumulative`; `'JUDGMENT'`: a judgment or judge request that is not JSON; `'KEEP'`: a `keep` that is not a nonnegative safe integer; `'SUMMARY'`: a summarizer digest whose trimmed text is empty; `'ROLE'`: a stored or selected message whose role is not a `ConversationRole`; `'MESSAGE'`: a message input whose other members fail `isMessageInput`; `'SNAPSHOT'`: a snapshot that fails `isConversationSnapshot` or repeats an id; `'ID'`: a manager's `add` of an id it already holds. */
2656
3092
  code;
2657
3093
  /** Holds the values that locate the failure, such as the `question` id of a malformed question, or `undefined` when none were supplied. */
2658
3094
  context;
@@ -2691,11 +3127,66 @@ function isConversationError(value) {
2691
3127
  return (0, _orkestrel_contract.holds)(() => {
2692
3128
  if (!(value instanceof Error && _orkestrel_contract.INTRINSICS.prototype(value) !== Error.prototype && _orkestrel_contract.INTRINSICS.describe(value, Symbol.for("@orkestrel/agent.ConversationError"))?.value === value)) return false;
2693
3129
  const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
2694
- if (!(code === "SUMMARIZER" || code === "SECTIONS" || code === "JUDGMENT" || code === "KEEP" || code === "SUMMARY" || code === "ROLE" || code === "SNAPSHOT")) return false;
3130
+ if (!(code === "SUMMARIZER" || code === "SECTIONS" || code === "JUDGMENT" || code === "KEEP" || code === "SUMMARY" || code === "ROLE" || code === "MESSAGE" || code === "SNAPSHOT" || code === "ID")) return false;
2695
3131
  const context = _orkestrel_contract.INTRINSICS.reflect.read(value, "context");
2696
3132
  return context === void 0 || (0, _orkestrel_contract.isObject)(context) || (0, _orkestrel_contract.isFunction)(context);
2697
3133
  });
2698
3134
  }
3135
+ // src/core/conversations/shapers.ts
3136
+ /**
3137
+ * Describes a stored judgment: exactly one of an answer or a refusal, with its question, sources,
3138
+ * state, model, and storage time.
3139
+ *
3140
+ * @remarks
3141
+ * `isJudgment` is the door a hydrate reads and this shape fills a refusal's faults, because hydrate is
3142
+ * a per-element path. A lockstep test holds that `is` implies the guard.
3143
+ */
3144
+ var judgmentShape = (0, _orkestrel_contract.unionShape)((0, _orkestrel_contract.objectShape)({
3145
+ id: (0, _orkestrel_contract.stringShape)(),
3146
+ question: judgeQuestionShape,
3147
+ model: (0, _orkestrel_contract.stringShape)(),
3148
+ sources: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)()),
3149
+ state: (0, _orkestrel_contract.stringShape)(),
3150
+ time: (0, _orkestrel_contract.numberShape)(),
3151
+ usage: (0, _orkestrel_contract.optionalShape)(tokenUsageShape),
3152
+ answer: judgeAnswerShape
3153
+ }), (0, _orkestrel_contract.objectShape)({
3154
+ id: (0, _orkestrel_contract.stringShape)(),
3155
+ question: judgeQuestionShape,
3156
+ model: (0, _orkestrel_contract.stringShape)(),
3157
+ sources: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)()),
3158
+ state: (0, _orkestrel_contract.stringShape)(),
3159
+ time: (0, _orkestrel_contract.numberShape)(),
3160
+ usage: (0, _orkestrel_contract.optionalShape)(tokenUsageShape),
3161
+ refusal: (0, _orkestrel_contract.objectShape)({ missing: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)()) })
3162
+ }));
3163
+ /** Describes a compacted section: its id, its summary, and the originals it folded. */
3164
+ var sectionShape = (0, _orkestrel_contract.objectShape)({
3165
+ id: (0, _orkestrel_contract.stringShape)(),
3166
+ summary: (0, _orkestrel_contract.stringShape)(),
3167
+ messages: (0, _orkestrel_contract.arrayShape)(messageShape)
3168
+ });
3169
+ /**
3170
+ * Describes a conversation snapshot exactly: an unknown key refuses, so a key a later version adds never
3171
+ * reads as absent.
3172
+ *
3173
+ * @remarks
3174
+ * `isConversationSnapshot` is the door a hydrate and a store read, and this shape fills a refusal's
3175
+ * faults. A lockstep test holds that `is` implies the guard, and on the snapshot fixtures the converse.
3176
+ */
3177
+ var conversationSnapshotShape = (0, _orkestrel_contract.objectShape)({
3178
+ id: (0, _orkestrel_contract.stringShape)(),
3179
+ sections: (0, _orkestrel_contract.arrayShape)(sectionShape),
3180
+ messages: (0, _orkestrel_contract.arrayShape)(messageShape),
3181
+ judgments: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)(judgmentShape))
3182
+ });
3183
+ // src/core/conversations/contracts.ts
3184
+ /** Reads the faults of a refused judgment. */
3185
+ var judgmentContract = (0, _orkestrel_contract.createContract)(judgmentShape);
3186
+ /** Reads the faults of a refused section. */
3187
+ var sectionContract = (0, _orkestrel_contract.createContract)(sectionShape);
3188
+ /** Reads the faults of a refused conversation snapshot. */
3189
+ var conversationSnapshotContract = (0, _orkestrel_contract.createContract)(conversationSnapshotShape);
2699
3190
  // src/core/conversations/validators.ts
2700
3191
  /**
2701
3192
  * Checks whether a value satisfies the domain conversation-message contract.
@@ -2703,8 +3194,9 @@ function isConversationError(value) {
2703
3194
  * @remarks
2704
3195
  * Roles belong to MessageRole, image elements are strings, `thinking` is a string, and `call` is a string on any
2705
3196
  * role, as the flat Message type admits. Tool arguments may carry non-JSON values, as the
2706
- * domain type permits; the message wire contract is narrower. Unreadable fields and untrusted
2707
- * inputs return false.
3197
+ * domain type permits; the message wire contract is narrower. The guard is exact on the message's own
3198
+ * keys: an unknown member, and an own `undefined` optional, return false. Unreadable fields and
3199
+ * untrusted inputs return false.
2708
3200
  *
2709
3201
  * @param value - The unknown message candidate
2710
3202
  * @returns True if the domain message fields are valid; false otherwise
@@ -2725,7 +3217,33 @@ function isMessage(value) {
2725
3217
  if (calls !== void 0 && !(0, _orkestrel_contract.arrayOf)(_orkestrel_tool.isToolCall)(calls)) return false;
2726
3218
  if (call !== void 0 && !(0, _orkestrel_contract.isString)(call)) return false;
2727
3219
  if (images !== void 0 && !(0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(images)) return false;
2728
- return thinking === void 0 || (0, _orkestrel_contract.isString)(thinking);
3220
+ if (thinking !== void 0 && !(0, _orkestrel_contract.isString)(thinking)) return false;
3221
+ return Object.keys(value).length === 3 + (calls === void 0 ? 0 : 1) + (call === void 0 ? 0 : 1) + (images === void 0 ? 0 : 1) + (thinking === void 0 ? 0 : 1);
3222
+ });
3223
+ return checked.success && checked.value;
3224
+ }
3225
+ /**
3226
+ * Checks whether a value is a message input a conversation can store: a string `content`, and `calls`,
3227
+ * `call`, `images`, and `thinking` absent or of their declared types.
3228
+ *
3229
+ * @remarks
3230
+ * A hand guard, because `add` checks every message: it reads the members once and composes no record
3231
+ * guard. The role is `requireRole`'s to refuse, so its text can name the role received. Unknown members
3232
+ * are left behind by the store. An unreadable member returns false.
3233
+ *
3234
+ * @param value - The unknown input candidate
3235
+ * @returns True if the input's members are valid; false otherwise
3236
+ * @example
3237
+ * ```ts
3238
+ * isMessageInput({ role: 'user', content: 'hi' }) // true
3239
+ * isMessageInput({ role: 'user' }) // false
3240
+ * ```
3241
+ */
3242
+ function isMessageInput(value) {
3243
+ const checked = (0, _orkestrel_contract.attempt)(() => {
3244
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
3245
+ const { content, calls, call, images, thinking } = value;
3246
+ return (0, _orkestrel_contract.isString)(content) && (calls === void 0 || (0, _orkestrel_contract.arrayOf)(_orkestrel_tool.isToolCall)(calls)) && (call === void 0 || (0, _orkestrel_contract.isString)(call)) && (images === void 0 || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(images)) && (thinking === void 0 || (0, _orkestrel_contract.isString)(thinking));
2729
3247
  });
2730
3248
  return checked.success && checked.value;
2731
3249
  }
@@ -2733,11 +3251,13 @@ function isMessage(value) {
2733
3251
  * Checks whether a stored judgment carries a valid question and exactly one answer or refusal.
2734
3252
  *
2735
3253
  * @remarks
2736
- * Unknown members, malformed members, and unreadable inputs return false. Answer fields follow
2737
- * the root judge value types; this storage guard does not impose a wire's probability or
2738
- * candidate limits. The `time`, `noul`, and probability numbers must be finite, because a stored
2739
- * record is owned through JSON, which turns `NaN` and the infinities into `null`; so every record
2740
- * this guard accepts hydrates through a `JudgmentManager`.
3254
+ * Unknown members, malformed members, and unreadable inputs return false, the answer's and the
3255
+ * refusal's own included. Answer fields follow the root judge value types; this storage guard does not
3256
+ * impose a wire's probability or candidate limits, and the ties an answer holds to its question run
3257
+ * where a judgment is written, because records written before them can miss them. The `time`, `noul`,
3258
+ * and probability numbers must be finite, because a stored record is owned through JSON, which turns
3259
+ * `NaN` and the infinities into `null`; so every record this guard accepts hydrates through a
3260
+ * `JudgmentManager`.
2741
3261
  *
2742
3262
  * @param value - The unknown stored record
2743
3263
  * @returns True if the record satisfies the judgment contract; false otherwise
@@ -2755,16 +3275,7 @@ var isJudgment = (0, _orkestrel_contract.unionOf)((0, _orkestrel_contract.record
2755
3275
  state: _orkestrel_contract.isString,
2756
3276
  time: _orkestrel_contract.isFiniteNumber,
2757
3277
  usage: _orkestrel_budget.isTokenUsage,
2758
- answer: (0, _orkestrel_contract.andOf)(_orkestrel_contract.isRecord, (0, _orkestrel_contract.unionOf)((0, _orkestrel_contract.objectOf)({
2759
- form: (0, _orkestrel_contract.literalOf)("noul"),
2760
- noul: _orkestrel_contract.isFiniteNumber
2761
- }), (0, _orkestrel_contract.objectOf)({
2762
- form: (0, _orkestrel_contract.literalOf)("score"),
2763
- probabilities: (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isFiniteNumber)
2764
- }), (0, _orkestrel_contract.objectOf)({
2765
- form: (0, _orkestrel_contract.literalOf)("choice"),
2766
- probabilities: (0, _orkestrel_contract.whereOf)(_orkestrel_contract.isRecord, (value) => Object.values(value).every(_orkestrel_contract.isFiniteNumber))
2767
- })))
3278
+ answer: isJudgeAnswer
2768
3279
  }, ["usage"]), (0, _orkestrel_contract.recordOf)({
2769
3280
  id: _orkestrel_contract.isString,
2770
3281
  question: isJudgeQuestion,
@@ -2773,7 +3284,7 @@ var isJudgment = (0, _orkestrel_contract.unionOf)((0, _orkestrel_contract.record
2773
3284
  state: _orkestrel_contract.isString,
2774
3285
  time: _orkestrel_contract.isFiniteNumber,
2775
3286
  usage: _orkestrel_budget.isTokenUsage,
2776
- refusal: (0, _orkestrel_contract.andOf)(_orkestrel_contract.isRecord, (0, _orkestrel_contract.objectOf)({ missing: (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString) }))
3287
+ refusal: (0, _orkestrel_contract.recordOf)({ missing: (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString) })
2777
3288
  }, ["usage"]));
2778
3289
  /**
2779
3290
  * Checks whether an `unknown` is structurally a {@link Section} record — a `string` `id` and
@@ -2785,8 +3296,8 @@ var isJudgment = (0, _orkestrel_contract.unionOf)((0, _orkestrel_contract.record
2785
3296
  * A total guard (it never throws — untrusted input, a throwing getter included, returns
2786
3297
  * `false`). It checks the shape of the section: a record with a `string` `id`, a `string`
2787
3298
  * `summary`, and a dense `messages` array every element of which is a valid {@link Message}
2788
- * record ({@link isMessage}); a sparse `messages` array is refused. Enough to safely impose
2789
- * the {@link Section} type at a storage boundary without a cast.
3299
+ * record ({@link isMessage}), and no other member; a sparse `messages` array is refused. Enough to
3300
+ * safely impose the {@link Section} type at a storage boundary without a cast.
2790
3301
  *
2791
3302
  * @param value - The value to test (one element of a snapshot's `sections` array)
2792
3303
  * @returns True if `value` has the structural shape of a {@link Section}; false otherwise
@@ -2799,11 +3310,11 @@ var isJudgment = (0, _orkestrel_contract.unionOf)((0, _orkestrel_contract.record
2799
3310
  * ```
2800
3311
  */
2801
3312
  function isSection(value) {
2802
- return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.objectOf)({
2803
- id: _orkestrel_contract.isString,
2804
- summary: _orkestrel_contract.isString,
2805
- messages: (0, _orkestrel_contract.arrayOf)(isMessage)
2806
- })(value);
3313
+ return (0, _orkestrel_contract.holds)(() => {
3314
+ if (!(0, _orkestrel_contract.isRecord)(value) || Object.keys(value).length !== 3) return false;
3315
+ const { id, summary, messages } = value;
3316
+ return (0, _orkestrel_contract.isString)(id) && (0, _orkestrel_contract.isString)(summary) && (0, _orkestrel_contract.arrayOf)(isMessage)(messages);
3317
+ });
2807
3318
  }
2808
3319
  /**
2809
3320
  * Narrows an `unknown` to a {@link ConversationSnapshot} — a `string` `id` and valid
@@ -2819,12 +3330,12 @@ function isSection(value) {
2819
3330
  * valid {@link Message} ({@link isMessage}), and optional valid `judgments` — enough to safely
2820
3331
  * impose the
2821
3332
  * {@link ConversationSnapshot} type at a storage boundary without a cast. The structural twin of
2822
- * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. Unknown members are admitted, so a
2823
- * 0.0.29 snapshot that carries a conversation `summary` passes. A malformed blob (a non-record,
2824
- * a missing / non-string `id`, a non-array `sections` / `messages`, or any malformed
2825
- * element) resolves `false`, so a
3333
+ * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. The guard is exact: an unknown member, a
3334
+ * 0.0.35 snapshot's conversation `summary` included, and an own `undefined` `judgments` return
3335
+ * `false`, as a malformed blob (a non-record, a missing / non-string `id`, a non-array `sections` /
3336
+ * `messages`, or any malformed element) does, so a
2826
3337
  * {@link import('./stores/DatabaseConversationStore.js').DatabaseConversationStore}
2827
- * read yields `undefined` rather than a broken conversation. The guard is structural: it admits a
3338
+ * read refuses it rather than seating a broken conversation. The guard is structural: it admits a
2828
3339
  * stored `system` role and a repeated id, which `requireConversationSnapshot` and the
2829
3340
  * {@link import('./Conversation.js').Conversation} constructor then refuse as the stored-data
2830
3341
  * check a conversation runs at hydrate.
@@ -2840,12 +3351,11 @@ function isSection(value) {
2840
3351
  * ```
2841
3352
  */
2842
3353
  function isConversationSnapshot(value) {
2843
- return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.objectOf)({
2844
- id: _orkestrel_contract.isString,
2845
- sections: (0, _orkestrel_contract.arrayOf)(isSection),
2846
- messages: (0, _orkestrel_contract.arrayOf)(isMessage),
2847
- judgments: (0, _orkestrel_contract.optionalOf)((0, _orkestrel_contract.arrayOf)(isJudgment))
2848
- }, ["judgments"])(value);
3354
+ return (0, _orkestrel_contract.holds)(() => {
3355
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
3356
+ const { id, sections, messages, judgments } = value;
3357
+ return Object.keys(value).length === (judgments === void 0 ? 3 : 4) && (0, _orkestrel_contract.isString)(id) && (0, _orkestrel_contract.arrayOf)(isSection)(sections) && (0, _orkestrel_contract.arrayOf)(isMessage)(messages) && (0, _orkestrel_contract.optionalOf)((0, _orkestrel_contract.arrayOf)(isJudgment))(judgments);
3358
+ });
2849
3359
  }
2850
3360
  /**
2851
3361
  * Checks whether a value carries the {@link ConversationInterface} members a run reads: a string
@@ -2873,6 +3383,50 @@ function isConversation(value) {
2873
3383
  }
2874
3384
  // src/core/conversations/helpers.ts
2875
3385
  /**
3386
+ * Reads what a judge result holds for one question, from its own keys alone.
3387
+ *
3388
+ * @param result - The reported answers and refusals
3389
+ * @param id - The question's id
3390
+ * @returns The answer and the refusal, each undefined when the result holds none
3391
+ * @example
3392
+ * ```ts
3393
+ * readJudgeOutcome({ model: 'm', answers: {} }, 'q') // { answer: undefined, refusal: undefined }
3394
+ * ```
3395
+ */
3396
+ function readJudgeOutcome(result, id) {
3397
+ return {
3398
+ answer: Object.hasOwn(result.answers, id) ? result.answers[id] : void 0,
3399
+ refusal: result.refusals !== void 0 && Object.hasOwn(result.refusals, id) ? result.refusals[id] : void 0
3400
+ };
3401
+ }
3402
+ /**
3403
+ * Returns after checking a complete judge result answered or refused every question it was asked, or
3404
+ * throws a coded error naming those it left out.
3405
+ *
3406
+ * @remarks
3407
+ * A cancelled call's partial is not complete, so `JudgmentManager` reads this on a settled result only,
3408
+ * and {@link buildJudgments} stores whatever a partial holds.
3409
+ *
3410
+ * An undefined value is no answer, as {@link buildJudgments} reads it, so a key alone doesn't count.
3411
+ *
3412
+ * @param request - The request whose question keys must be answered
3413
+ * @param result - The result the judge settled
3414
+ * @throws ConversationError Thrown when a question has neither an answer nor a refusal (code `'JUDGMENT'`), naming them under `context.questions`
3415
+ * @example
3416
+ * ```ts
3417
+ * requireJudgeResult({ state: 'text', questions: { a: { form: 'noul' } } }, { model: 'm', answers: {} })
3418
+ * // throws ConversationError ('JUDGMENT')
3419
+ * ```
3420
+ */
3421
+ function requireJudgeResult(request, result) {
3422
+ const left = Object.keys(request.questions).filter((id) => {
3423
+ const { answer, refusal } = readJudgeOutcome(result, id);
3424
+ return answer === void 0 && refusal === void 0;
3425
+ });
3426
+ if (left.length === 0) return;
3427
+ throw new ConversationError("JUDGMENT", `conversation error: the judge left ${left.map((id) => (0, _orkestrel_contract.preview)(id)).join(", ")} neither answered nor refused; a judge must answer or refuse every question it is asked`, { context: { questions: left } });
3428
+ }
3429
+ /**
2876
3430
  * Builds records for answered or refused request keys, attaching usage only for a single question.
2877
3431
  *
2878
3432
  * @param request - The request whose question keys define record order
@@ -2889,8 +3443,7 @@ function isConversation(value) {
2889
3443
  function buildJudgments(request, result, sources, state, model) {
2890
3444
  const single = Object.keys(request.questions).length === 1;
2891
3445
  return Object.entries(request.questions).flatMap(([id, question]) => {
2892
- const answer = Object.hasOwn(result.answers, id) ? result.answers[id] : void 0;
2893
- const refusal = result.refusals !== void 0 && Object.hasOwn(result.refusals, id) ? result.refusals[id] : void 0;
3446
+ const { answer, refusal } = readJudgeOutcome(result, id);
2894
3447
  if (answer === void 0 && refusal === void 0) return [];
2895
3448
  return [{
2896
3449
  id,
@@ -3002,43 +3555,74 @@ function requireRole(role) {
3002
3555
  return stored;
3003
3556
  }
3004
3557
  /**
3558
+ * Returns a message input after checking its role and its members, or throws a coded error.
3559
+ *
3560
+ * @remarks
3561
+ * The role goes first, through {@link requireRole}, so its refusal names what it received. The members
3562
+ * cross {@link isMessageInput}, and `audit` runs only after it refused, so a stored message costs one
3563
+ * guard read. Tool-call arguments must also be JSON values before a message is stored.
3564
+ *
3565
+ * @param input - The message input to check
3566
+ * @param contract - The message input contract, whose `audit` is read only on a refusal
3567
+ * @returns The input, narrowed
3568
+ * @throws ConversationError Thrown when the role is not a `ConversationRole` (code `'ROLE'`) or a member fails `isMessageInput` (code `'MESSAGE'`), with the faults under `context.faults`
3569
+ *
3570
+ * @example
3571
+ * ```ts
3572
+ * requireMessageInput({ role: 'user', content: 'hi' }, messageInputContract) // the input
3573
+ * requireMessageInput({ role: 'user' }, messageInputContract) // throws ConversationError ('MESSAGE')
3574
+ * ```
3575
+ */
3576
+ function requireMessageInput(input, contract) {
3577
+ requireRole((0, _orkestrel_contract.isRecord)(input) ? input.role : void 0);
3578
+ if (isMessageInput(input) && (input.calls === void 0 || input.calls.every((call) => (0, _orkestrel_contract.isJSONValue)(call.arguments)))) return input;
3579
+ const faults = contract.audit(input);
3580
+ throw new ConversationError("MESSAGE", `a message input must carry string content, and calls, call, images, and thinking of their declared types\n${(0, _orkestrel_contract.describeFaults)(faults, contract.schema)}`, { context: { faults } });
3581
+ }
3582
+ /**
3005
3583
  * Returns a conversation snapshot after checking its shape, its stored roles, and its ids, or
3006
3584
  * throws a coded error.
3007
3585
  *
3008
3586
  * @remarks
3009
- * The check reads `id`, `sections`, `messages`, and `judgments` from `value` once each and builds
3010
- * a candidate from those reads: each array member becomes a fresh array holding the same element
3011
- * objects, and a member that is not an array stays as read, so the guard refuses it. The candidate
3012
- * must pass `isConversationSnapshot`, every section message and live message must play a
3013
- * `ConversationRole`, and no id may repeat across section ids, section message ids, and live
3587
+ * The check reads every own member of `value` once and builds a candidate from those reads: each
3588
+ * array member becomes a fresh array holding the same element objects, and a member that is not an
3589
+ * array stays as read, so the guard refuses it. The candidate must pass `isConversationSnapshot`, which
3590
+ * is exact, so an unknown key refuses rather than being dropped; every section message and live message
3591
+ * must play a `ConversationRole`; and no id may repeat across section ids, section message ids, and live
3014
3592
  * message ids. The `'ROLE'` refusal of a `system` message names the row a snapshot written before
3015
3593
  * 0.0.35 can hold, and each `'SNAPSHOT'` refusal asks for what `snapshot()` returned. The candidate
3016
3594
  * is returned, never `value`, so the check and the seating share one read. Element objects are kept
3017
3595
  * by reference, so a host is trusted not to edit a seated message object or to back its members
3018
- * with accessors.
3596
+ * with accessors. The contract's `audit` runs only after the guard refused, to fill `context.faults`.
3019
3597
  *
3020
3598
  * @param value - The snapshot to check, typically a stored read
3021
- * @returns The candidate snapshot, holding `id`, `sections`, `messages`, and `judgments` when present
3022
- * @throws ConversationError Thrown when the value fails `isConversationSnapshot` or repeats an id (code `'SNAPSHOT'`), or holds a message whose role is not a `ConversationRole` (code `'ROLE'`)
3599
+ * @param contract - The snapshot contract, whose `audit` is read only on a refusal
3600
+ * @returns The candidate snapshot
3601
+ * @throws ConversationError Thrown when the value fails `isConversationSnapshot` (code `'SNAPSHOT'`, its faults under `context.faults`), repeats an id (code `'SNAPSHOT'`), or holds a message whose role is not a `ConversationRole` (code `'ROLE'`)
3023
3602
  *
3024
3603
  * @example
3025
3604
  * ```ts
3026
- * requireConversationSnapshot({ id: 'c1', sections: [], messages: [] }) // { id: 'c1', sections: [], messages: [] }
3027
- * requireConversationSnapshot({ id: 'c1', sections: 'nope', messages: [] }) // throws ConversationError ('SNAPSHOT')
3605
+ * requireConversationSnapshot({ id: 'c1', sections: [], messages: [] }, conversationSnapshotContract) // { id: 'c1', sections: [], messages: [] }
3606
+ * requireConversationSnapshot({ id: 'c1', sections: 'nope', messages: [] }, conversationSnapshotContract) // throws ConversationError ('SNAPSHOT')
3028
3607
  * ```
3029
3608
  */
3030
- function requireConversationSnapshot(value) {
3609
+ function requireConversationSnapshot(value, contract) {
3031
3610
  const candidate = (0, _orkestrel_contract.attempt)(() => {
3032
3611
  if (!(0, _orkestrel_contract.isRecord)(value)) return void 0;
3033
- const { id, sections, messages, judgments } = value;
3612
+ const read = { ...value };
3613
+ const { sections, messages, judgments } = read;
3034
3614
  return {
3035
- id,
3615
+ ...read,
3036
3616
  sections: (0, _orkestrel_contract.isArray)(sections) ? Array.from(sections) : sections,
3037
3617
  messages: (0, _orkestrel_contract.isArray)(messages) ? Array.from(messages) : messages,
3038
3618
  ...judgments === void 0 ? {} : { judgments: (0, _orkestrel_contract.isArray)(judgments) ? Array.from(judgments) : judgments }
3039
3619
  };
3040
3620
  });
3041
- if (!candidate.success || !isConversationSnapshot(candidate.value)) throw new ConversationError("SNAPSHOT", "a conversation snapshot must carry a string id, valid sections and messages, and valid judgments when present; pass what snapshot() returned", candidate.success ? void 0 : { cause: candidate.error });
3621
+ if (!candidate.success) throw new ConversationError("SNAPSHOT", "a conversation snapshot must be readable; pass what snapshot() returned", { cause: candidate.error });
3622
+ if (!isConversationSnapshot(candidate.value)) {
3623
+ const faults = contract.audit(candidate.value);
3624
+ throw new ConversationError("SNAPSHOT", `a conversation snapshot must carry a string id, valid sections and messages, valid judgments when present, and no other member; pass what snapshot() returned\n${(0, _orkestrel_contract.describeFaults)(faults, contract.schema)}`, { context: { faults } });
3625
+ }
3042
3626
  const snapshot = candidate.value;
3043
3627
  snapshot.sections.flatMap((section) => section.messages).concat(snapshot.messages).forEach((message) => {
3044
3628
  if (message.role === "system") throw new ConversationError("ROLE", "a conversation snapshot holds a system message; a snapshot written before 0.0.35 can hold a system row; remove it before loading");
@@ -3056,15 +3640,15 @@ function requireConversationSnapshot(value) {
3056
3640
  return snapshot;
3057
3641
  }
3058
3642
  /**
3059
- * Returns a `sections` cap after checking its lower bound, or throws a coded error.
3643
+ * Returns a `sections` cap after checking it is a positive safe integer, or throws a coded error.
3060
3644
  *
3061
3645
  * @remarks
3062
- * A supplied cap must satisfy `>= 1`; fractional caps and positive infinity pass, and `NaN`
3063
- * is refused. An absent cap leaves the sections list unbounded.
3646
+ * A fraction, positive infinity, and `NaN` are refused with `0` and a negative, because a count of
3647
+ * sections is a whole number. An absent cap leaves the sections list unbounded.
3064
3648
  *
3065
3649
  * @param cap - The cap to check, or `undefined` for no cap
3066
3650
  * @returns The cap unchanged, or `undefined` when none was supplied
3067
- * @throws ConversationError Thrown when the cap does not satisfy `>= 1` (code `'SECTIONS'`, including `NaN`)
3651
+ * @throws ConversationError Thrown when the cap is not a positive safe integer (code `'SECTIONS'`)
3068
3652
  *
3069
3653
  * @example
3070
3654
  * ```ts
@@ -3074,7 +3658,7 @@ function requireConversationSnapshot(value) {
3074
3658
  * ```
3075
3659
  */
3076
3660
  function requireSectionsCap(cap) {
3077
- if (cap !== void 0 && !(cap >= 1)) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
3661
+ if (cap !== void 0 && !isPositiveSafeInteger(cap)) throw new ConversationError("SECTIONS", "a sections cap must be a positive safe integer", { context: { sections: cap } });
3078
3662
  return cap;
3079
3663
  }
3080
3664
  /**
@@ -3232,9 +3816,16 @@ var JudgmentManager = class {
3232
3816
  const rendered = typeof state === "string" ? state : JSON.stringify(state);
3233
3817
  const origins = [...sources];
3234
3818
  const entries = (0, _orkestrel_contract.isRecord)(request.questions) ? Object.entries(request.questions) : [];
3819
+ if (entries.length === 0) throw new ConversationError("JUDGMENT", "conversation error: a judge request needs at least one question, keyed by id");
3235
3820
  const checked = entries.map(([id, raw]) => {
3236
3821
  const question = copyJSON(raw);
3237
- if (!isJudgeQuestion(question)) throw new ConversationError("JUDGMENT", `conversation error: question ${id} is malformed`, { context: { question: id } });
3822
+ if (!isJudgeQuestion(question)) {
3823
+ const faults = judgeQuestionContract.audit(question);
3824
+ throw new ConversationError("JUDGMENT", `conversation error: ${describeQuestionRefusal(id, faults, judgeQuestionContract.schema)}`, { context: {
3825
+ question: id,
3826
+ faults
3827
+ } });
3828
+ }
3238
3829
  return [id, question];
3239
3830
  });
3240
3831
  const matched = new Map(checked.flatMap(([id, question]) => {
@@ -3259,6 +3850,7 @@ var JudgmentManager = class {
3259
3850
  });
3260
3851
  try {
3261
3852
  const result = await judge.ask(unmatched, signal);
3853
+ requireJudgeResult(unmatched, result);
3262
3854
  return this.add(buildJudgments(unmatched, result, origins, rendered, judge.model));
3263
3855
  } catch (error) {
3264
3856
  if (isJudgeAbortError(error)) this.add(buildJudgments(unmatched, error.partial, origins, rendered, judge.model));
@@ -3267,10 +3859,19 @@ var JudgmentManager = class {
3267
3859
  }
3268
3860
  #store(input) {
3269
3861
  const copy = copyJSON(input);
3862
+ if ((0, _orkestrel_contract.isRecord)(copy) && copy.answer !== void 0 && !judgeAnswerContract.is(copy.answer)) {
3863
+ const faults = judgeAnswerContract.audit(copy.answer);
3864
+ throw new ConversationError("JUDGMENT", `conversation error: a judgment answer must satisfy its shape\n${(0, _orkestrel_contract.describeFaults)(faults, judgeAnswerContract.schema)}`, { context: { faults } });
3865
+ }
3270
3866
  const judgment = requireJudgment(freezeJSON((0, _orkestrel_contract.isRecord)(copy) ? {
3271
3867
  ...copy,
3272
3868
  time: Date.now()
3273
3869
  } : copy));
3870
+ const faults = judgment.answer === void 0 ? [] : scanJudgmentTies(judgment.question, judgment.answer);
3871
+ if (faults.length > 0) throw new ConversationError("JUDGMENT", `conversation error: judgment ${judgment.id} answers its question wrongly\n${(0, _orkestrel_contract.describeFaults)(faults, judgeAnswerContract.schema)}`, { context: {
3872
+ question: judgment.id,
3873
+ faults
3874
+ } });
3274
3875
  this.#judgments.set(judgment.id, judgment);
3275
3876
  return judgment;
3276
3877
  }
@@ -3355,7 +3956,7 @@ var Conversation = class {
3355
3956
  #pending = /* @__PURE__ */ new Set();
3356
3957
  constructor(options) {
3357
3958
  const supplied = options?.snapshot;
3358
- const snapshot = supplied === void 0 ? void 0 : requireConversationSnapshot(supplied);
3959
+ const snapshot = supplied === void 0 ? void 0 : requireConversationSnapshot(supplied, conversationSnapshotContract);
3359
3960
  this.#judgments = new JudgmentManager(snapshot?.judgments);
3360
3961
  this.#id = snapshot?.id ?? options?.id ?? crypto.randomUUID();
3361
3962
  this.#emitter = new _orkestrel_emitter.Emitter(options);
@@ -3387,10 +3988,10 @@ var Conversation = class {
3387
3988
  }
3388
3989
  add(input) {
3389
3990
  if ((0, _orkestrel_contract.isArray)(input)) {
3390
- input.forEach((one) => requireRole(one.role));
3991
+ input.forEach((one) => requireMessageInput(one, messageInputContract));
3391
3992
  return input.map((one) => this.#create(one));
3392
3993
  }
3393
- return this.#create(input);
3994
+ return this.#create(requireMessageInput(input, messageInputContract));
3394
3995
  }
3395
3996
  message(id) {
3396
3997
  return this.#messages.get(id);
@@ -3499,9 +4100,13 @@ var Conversation = class {
3499
4100
  #create(input) {
3500
4101
  const message = {
3501
4102
  id: crypto.randomUUID(),
3502
- role: requireRole(input.role),
4103
+ role: input.role,
3503
4104
  content: input.content,
3504
- ...input.calls === void 0 ? {} : { calls: input.calls },
4105
+ ...input.calls === void 0 ? {} : { calls: input.calls.map(({ id, name, arguments: arguments_ }) => ({
4106
+ id,
4107
+ name,
4108
+ arguments: arguments_
4109
+ })) },
3505
4110
  ...input.call === void 0 ? {} : { call: input.call },
3506
4111
  ...input.images === void 0 ? {} : { images: input.images },
3507
4112
  ...input.thinking === void 0 ? {} : { thinking: input.thinking }
@@ -3523,7 +4128,8 @@ var Conversation = class {
3523
4128
  * - **Registry.** Conversations live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
3524
4129
  * mints a {@link Conversation} (its `id` from `input` or `crypto.randomUUID()`), flowing the
3525
4130
  * default `#summarize` / `#keep` of the manager in unless the `input` overrides them, and stores
3526
- * it (an already-present `id` overwrites — last write wins). `count` is the map size,
4131
+ * it (an already-present `id` throws a `ConversationError` coded `'ID'`, because replacing a live
4132
+ * conversation would orphan its turns). `count` is the map size,
3527
4133
  * `conversation(id)` looks one up, `conversations()` lists them in insertion order.
3528
4134
  * - **Active pointer.** `active` is the active conversation (the conversation an agent run binds
3529
4135
  * when it names none), `undefined` until the first `add` (which auto-activates it — a registry
@@ -3572,6 +4178,8 @@ var ConversationManager = class {
3572
4178
  return [...this.#conversations.values()];
3573
4179
  }
3574
4180
  add(input) {
4181
+ const held = input?.id ?? input?.snapshot?.id;
4182
+ if (held !== void 0 && this.#conversations.has(held)) throw new ConversationError("ID", `conversation ${(0, _orkestrel_contract.preview)(held)} is already live; pick another id, or remove it first`, { context: { conversation: held } });
3575
4183
  const sections = input?.sections ?? this.#sections;
3576
4184
  const summarize = input?.summarize ?? this.#summarize;
3577
4185
  const conversation = new Conversation({
@@ -3607,7 +4215,8 @@ var ConversationManager = class {
3607
4215
  this.#active = id;
3608
4216
  return registered;
3609
4217
  }
3610
- if (snapshot === void 0 || snapshot.id !== id) return void 0;
4218
+ if (snapshot === void 0) return void 0;
4219
+ if (snapshot.id !== id) throw new ConversationError("SNAPSHOT", `the store answered ${(0, _orkestrel_contract.preview)(id)} with the snapshot of ${(0, _orkestrel_contract.preview)(snapshot.id)}; a store returns only the row of the id it is asked`);
3611
4220
  const conversation = this.add({ snapshot });
3612
4221
  this.#active = conversation.id;
3613
4222
  return conversation;
@@ -3740,8 +4349,9 @@ var MemoryConversationStore = class {
3740
4349
  * the row `{ id: snapshot.id, snapshot }`.
3741
4350
  * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
3742
4351
  * a {@link ConversationSnapshot} ({@link import('../validators.js').isConversationSnapshot} — the
3743
- * total guard for an untrusted storage read), or `undefined` if none is stored, the blob is
3744
- * malformed, or the blob's own `id` is not the requested one.
4352
+ * total guard for an untrusted storage read), or `undefined` only when no row is stored. A row whose
4353
+ * blob is malformed or names another id throws a `ConversationError` coded `'SNAPSHOT'`, so `open`
4354
+ * and `create` stop rather than overwrite it, and `delete` still removes it.
3745
4355
  * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
3746
4356
  *
3747
4357
  * Unlike a session store there is no idle-TTL / eviction — a persisted conversation lives until an
@@ -3780,18 +4390,35 @@ var DatabaseConversationStore = class {
3780
4390
  *
3781
4391
  * @remarks
3782
4392
  * The column is an untrusted storage read, so a blob that fails
3783
- * {@link import('../validators.js').isConversationSnapshot}, or whose own `id` is not `id`,
3784
- * resolves `undefined` rather than a conversation under the wrong key.
4393
+ * {@link import('../validators.js').isConversationSnapshot}, or whose own `id` is not `id`, throws
4394
+ * rather than reading as absent, which would let a later `set` overwrite it.
3785
4395
  *
3786
4396
  * @param id - The conversation id to resolve (the row key)
3787
- * @returns The persisted snapshot, or `undefined` when no row is stored, the blob is malformed,
3788
- * or the blob names another id
4397
+ * @returns The persisted snapshot, or `undefined` when no row is stored
4398
+ * @throws {ConversationError} Thrown when the row's blob is malformed or names another id (code `'SNAPSHOT'`), with the faults under `context.faults`
3789
4399
  */
3790
4400
  async get(id) {
3791
4401
  const row = await this.#table.get(id);
3792
4402
  if (row === void 0) return void 0;
3793
4403
  const snapshot = row.snapshot;
3794
- return isConversationSnapshot(snapshot) && snapshot.id === id ? snapshot : void 0;
4404
+ if (isConversationSnapshot(snapshot)) {
4405
+ if (snapshot.id === id) return snapshot;
4406
+ const faults = [{
4407
+ reason: "type",
4408
+ path: ["id"],
4409
+ expected: "literal",
4410
+ received: (0, _orkestrel_contract.preview)(snapshot.id)
4411
+ }];
4412
+ throw new ConversationError("SNAPSHOT", describeSnapshotRefusal("conversation", id, faults), { context: {
4413
+ id,
4414
+ faults
4415
+ } });
4416
+ }
4417
+ const faults = conversationSnapshotContract.audit(snapshot);
4418
+ throw new ConversationError("SNAPSHOT", describeSnapshotRefusal("conversation", id, faults), { context: {
4419
+ id,
4420
+ faults
4421
+ } });
3795
4422
  }
3796
4423
  /**
3797
4424
  * Inserts or replaces under the snapshot's own `id` (no separate id param) — the row is
@@ -3799,8 +4426,14 @@ var DatabaseConversationStore = class {
3799
4426
  *
3800
4427
  * @param snapshot - The snapshot to store, keyed by its `id`
3801
4428
  * @returns A promise that resolves after the row is written
4429
+ * @throws {ConversationError} Thrown before any write when the snapshot fails the guard `get` reads (code `'SNAPSHOT'`), so no row is written that `get` refuses
3802
4430
  */
3803
4431
  async set(snapshot) {
4432
+ if (!isConversationSnapshot(snapshot)) {
4433
+ const unchecked = snapshot;
4434
+ const faults = conversationSnapshotContract.audit(unchecked);
4435
+ throw new ConversationError("SNAPSHOT", `a conversation store writes only what snapshot() returned\n${(0, _orkestrel_contract.describeFaults)(faults, conversationSnapshotContract.schema)}`, { context: { faults } });
4436
+ }
3804
4437
  await this.#table.set({
3805
4438
  id: snapshot.id,
3806
4439
  snapshot
@@ -3886,7 +4519,7 @@ function createConversation(options) {
3886
4519
  * @remarks
3887
4520
  * Starts empty; `add(input?)` mints a {@link ConversationInterface} (its `id` from the input
3888
4521
  * or a random UUID), flowing the manager's default `summarize` / `keep` in unless the input
3889
- * overrides them, and stores it (an already-present `id` overwrites — last write wins) — and
4522
+ * overrides them, and stores it (an already-present `id` throws, code `'ID'`) — and
3890
4523
  * auto-activates the first one (a registry with conversations always has one `active`); a later
3891
4524
  * `add` leaves `active` unchanged. `switch(id)` re-points `active` (an unknown `id` returns
3892
4525
  * `undefined`, leaving `active` unchanged — lenient, never throws); `conversation(id)` /
@@ -4028,18 +4661,21 @@ var SKILL_SECTION_HEADER = "## Skills";
4028
4661
  // src/core/contexts/errors.ts
4029
4662
  /**
4030
4663
  * Reports a context snapshot that `isMemoSnapshot` refuses, thrown by the construction of a
4031
- * {@link import('./memos/MemoManager.js').MemoManager} before it seats any state, carrying the
4032
- * machine-readable `code` `'SNAPSHOT'`.
4664
+ * {@link import('./memos/MemoManager.js').MemoManager} before it seats any state or by a memo store,
4665
+ * or image files a build has no user turn to carry, carrying the machine-readable `code`
4666
+ * `'SNAPSHOT' | 'IMAGES'`.
4033
4667
  *
4034
4668
  * @remarks
4035
4669
  * A `snapshot` option crosses a foreign boundary (a client, a store, a file), so the manager copies
4036
4670
  * it through JSON and checks the copy: a value JSON cannot carry, a revision missing a field, a
4037
4671
  * non-finite `time`, a sparse `memos` array, a non-string `id`, or a non-array `memos` is a
4038
- * programmer error and throws this with `'SNAPSHOT'`. Narrow a caught value with
4039
- * {@link isContextError} and branch on `error.code`.
4672
+ * programmer error and throws this with `'SNAPSHOT'`, its `context` holding the `faults`. A build
4673
+ * over a conversation that holds turns and no user turn, with image files in the active workspace,
4674
+ * throws `'IMAGES'` rather than dropping them. Narrow a caught value with {@link isContextError} and
4675
+ * branch on `error.code`.
4040
4676
  */
4041
4677
  var ContextError = class extends Error {
4042
- /** Names the machine-readable condition — `'SNAPSHOT'`: a snapshot option that is not a memo snapshot. */
4678
+ /** Names the machine-readable condition — `'SNAPSHOT'`: a snapshot that is not a memo snapshot; `'IMAGES'`: image files with no user turn to carry them. */
4043
4679
  code;
4044
4680
  /** Holds the values that locate the failure, or `undefined` when none were supplied. */
4045
4681
  context;
@@ -4077,7 +4713,8 @@ var ContextError = class extends Error {
4077
4713
  function isContextError(value) {
4078
4714
  return (0, _orkestrel_contract.holds)(() => {
4079
4715
  if (!(value instanceof Error && _orkestrel_contract.INTRINSICS.prototype(value) !== Error.prototype && _orkestrel_contract.INTRINSICS.describe(value, Symbol.for("@orkestrel/agent.ContextError"))?.value === value)) return false;
4080
- if (_orkestrel_contract.INTRINSICS.reflect.read(value, "code") !== "SNAPSHOT") return false;
4716
+ const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
4717
+ if (code !== "SNAPSHOT" && code !== "IMAGES") return false;
4081
4718
  const context = _orkestrel_contract.INTRINSICS.reflect.read(value, "context");
4082
4719
  return context === void 0 || (0, _orkestrel_contract.isObject)(context) || (0, _orkestrel_contract.isFunction)(context);
4083
4720
  });
@@ -4310,13 +4947,14 @@ function attachImages(message, payloads) {
4310
4947
  * @remarks
4311
4948
  * Pure and total: the conversation and its messages are never mutated, and the returned array
4312
4949
  * replaces exactly the one target message with the copy {@link attachImages} builds. Empty
4313
- * data returns the conversation unchanged; a conversation with no user message returns it
4314
- * unchanged too (there is nowhere to attach, so the images are left out). A hole
4315
- * in a sparse conversation is not a user message.
4950
+ * data returns the conversation unchanged. Data with no user message to carry it refuses, an empty
4951
+ * conversation included, because leaving the images out would run without them. A hole in a sparse
4952
+ * conversation is not a user message.
4316
4953
  *
4317
4954
  * @param conversation - The messages to attach into (left unchanged)
4318
4955
  * @param payloads - The base64 image data to attach
4319
4956
  * @returns The conversation with its last user message replaced by the carrying copy
4957
+ * @throws ContextError Thrown when there is data and no user message to carry it (code `'IMAGES'`)
4320
4958
  *
4321
4959
  * @example
4322
4960
  * ```ts
@@ -4327,7 +4965,7 @@ function attachImages(message, payloads) {
4327
4965
  function attachUserImages(conversation, payloads) {
4328
4966
  if (payloads.length === 0) return conversation;
4329
4967
  const target = conversation.findLastIndex((message) => message?.role === "user");
4330
- if (target === -1) return conversation;
4968
+ if (target === -1) throw new ContextError("IMAGES", `the active workspace holds ${payloads.length} image file(s) and the conversation holds no user turn to carry them; add a user message or scope the files out`);
4331
4969
  return conversation.map((message, index) => index === target ? attachImages(message, payloads) : message);
4332
4970
  }
4333
4971
  /**
@@ -4401,16 +5039,70 @@ function intersectKeys(parent, child) {
4401
5039
  function renderIndexEntry(entry) {
4402
5040
  return `- ${entry.name}: ${entry.description}`.replace(/[\n\v\f\r\u0085\u2028\u2029]+/g, " ");
4403
5041
  }
5042
+ // src/core/contexts/shapers.ts
5043
+ /** Describes a stored memo revision exactly. */
5044
+ var memoShape = (0, _orkestrel_contract.objectShape)({
5045
+ id: (0, _orkestrel_contract.stringShape)(),
5046
+ name: (0, _orkestrel_contract.stringShape)(),
5047
+ description: (0, _orkestrel_contract.stringShape)(),
5048
+ content: (0, _orkestrel_contract.stringShape)(),
5049
+ time: (0, _orkestrel_contract.numberShape)(),
5050
+ origin: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.objectShape)({
5051
+ conversation: (0, _orkestrel_contract.stringShape)(),
5052
+ message: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)())
5053
+ }))
5054
+ });
5055
+ /**
5056
+ * Describes a stored memo set exactly: an unknown key refuses.
5057
+ *
5058
+ * @remarks
5059
+ * `isMemoSnapshot` is the door a hydrate and a store read, and this shape fills a refusal's faults. A
5060
+ * lockstep test holds that `is` implies the guard, and on the snapshot fixtures the converse.
5061
+ */
5062
+ var memoSnapshotShape = (0, _orkestrel_contract.objectShape)({
5063
+ id: (0, _orkestrel_contract.stringShape)(),
5064
+ memos: (0, _orkestrel_contract.arrayShape)(memoShape)
5065
+ });
5066
+ // src/core/contexts/contracts.ts
5067
+ /** Reads the faults of a refused memo revision. */
5068
+ var memoContract = (0, _orkestrel_contract.createContract)(memoShape);
5069
+ /** Reads the faults of a refused memo snapshot. */
5070
+ var memoSnapshotContract = (0, _orkestrel_contract.createContract)(memoSnapshotShape);
4404
5071
  // src/core/contexts/validators.ts
4405
5072
  /**
5073
+ * Checks whether a value is a memo's origin: a record with a string `conversation` and an optional
5074
+ * string `message`.
5075
+ *
5076
+ * @remarks
5077
+ * The guard is exact: an unknown member and an own `undefined` `message` return false. Malformed and
5078
+ * unreadable inputs return false.
5079
+ *
5080
+ * @param value - The value to test
5081
+ * @returns True if `value` has the shape of a {@link MemoOrigin}; false otherwise
5082
+ * @example
5083
+ * ```ts
5084
+ * isMemoOrigin({ conversation: 'c1', message: 'm1' }) // true
5085
+ * isMemoOrigin({ conversation: 'c1', extra: true }) // false
5086
+ * ```
5087
+ */
5088
+ function isMemoOrigin(value) {
5089
+ return (0, _orkestrel_contract.holds)(() => {
5090
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
5091
+ const { conversation, message } = value;
5092
+ return (0, _orkestrel_contract.isString)(conversation) && (message === void 0 || (0, _orkestrel_contract.isString)(message)) && Object.keys(value).length === (message === void 0 ? 1 : 2);
5093
+ });
5094
+ }
5095
+ /**
4406
5096
  * Checks whether a value is a stored memo revision: a record with a string `id`, `name`,
4407
5097
  * `description`, and `content`, a finite `time`, and an optional `origin`.
4408
5098
  *
4409
5099
  * @remarks
4410
5100
  * An `origin`, when present, is a record with a string `conversation` and an optional string
4411
- * `message`. Unknown members are admitted. The `time` must be finite, because a stored revision
4412
- * is owned through JSON, which turns `NaN` and the infinities into `null`. Malformed and
4413
- * unreadable inputs, a throwing getter or a revoked proxy included, return false.
5101
+ * `message`. The guard is exact: an unknown member and an own `undefined` optional return false. The
5102
+ * `time` must be finite, because a stored revision is owned through JSON, which turns `NaN` and the
5103
+ * infinities into `null`. Malformed and unreadable inputs, a throwing getter or a revoked proxy
5104
+ * included, return false. Every revision of a hydrate reads this guard, so it counts keys by hand
5105
+ * rather than through a built one, and `memoSnapshotContract` holds it in lockstep.
4414
5106
  *
4415
5107
  * @param value - The value to test
4416
5108
  * @returns True if `value` has the shape of a {@link Memo}; false otherwise
@@ -4423,24 +5115,20 @@ function renderIndexEntry(entry) {
4423
5115
  * isMemo({ id: 'm1', name: 'tone', description: 'Prefers short replies.', content: 'Answer briefly.', time: Number.NaN }) // false
4424
5116
  * ```
4425
5117
  */
4426
- var isMemo = (0, _orkestrel_contract.andOf)(_orkestrel_contract.isRecord, (0, _orkestrel_contract.objectOf)({
4427
- id: _orkestrel_contract.isString,
4428
- name: _orkestrel_contract.isString,
4429
- description: _orkestrel_contract.isString,
4430
- content: _orkestrel_contract.isString,
4431
- time: _orkestrel_contract.isFiniteNumber,
4432
- origin: (0, _orkestrel_contract.andOf)(_orkestrel_contract.isRecord, (0, _orkestrel_contract.objectOf)({
4433
- conversation: _orkestrel_contract.isString,
4434
- message: _orkestrel_contract.isString
4435
- }, ["message"]))
4436
- }, ["origin"]));
5118
+ function isMemo(value) {
5119
+ return (0, _orkestrel_contract.holds)(() => {
5120
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
5121
+ const { id, name, description, content, time, origin } = value;
5122
+ return (0, _orkestrel_contract.isString)(id) && (0, _orkestrel_contract.isString)(name) && (0, _orkestrel_contract.isString)(description) && (0, _orkestrel_contract.isString)(content) && (0, _orkestrel_contract.isFiniteNumber)(time) && (origin === void 0 || isMemoOrigin(origin)) && Object.keys(value).length === (origin === void 0 ? 5 : 6);
5123
+ });
5124
+ }
4437
5125
  /**
4438
5126
  * Checks whether a value is a stored memo set: a record with a string `id` and a dense `memos`
4439
5127
  * array of {@link isMemo} values.
4440
5128
  *
4441
5129
  * @remarks
4442
- * Unknown members are admitted. A sparse `memos` array, a malformed revision, and unreadable
4443
- * inputs, a throwing getter or a revoked proxy included, return false.
5130
+ * The guard is exact: an unknown member returns false, as a sparse `memos` array, a malformed
5131
+ * revision, and unreadable inputs, a throwing getter or a revoked proxy included, do.
4444
5132
  *
4445
5133
  * @param value - The value to test (an opaque storage read)
4446
5134
  * @returns True if `value` has the shape of a {@link MemoSnapshot}; false otherwise
@@ -4454,10 +5142,11 @@ var isMemo = (0, _orkestrel_contract.andOf)(_orkestrel_contract.isRecord, (0, _o
4454
5142
  * ```
4455
5143
  */
4456
5144
  function isMemoSnapshot(value) {
4457
- return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.objectOf)({
4458
- id: _orkestrel_contract.isString,
4459
- memos: (0, _orkestrel_contract.arrayOf)(isMemo)
4460
- })(value);
5145
+ return (0, _orkestrel_contract.holds)(() => {
5146
+ if (!(0, _orkestrel_contract.isRecord)(value) || Object.keys(value).length !== 2) return false;
5147
+ const { id, memos } = value;
5148
+ return (0, _orkestrel_contract.isString)(id) && (0, _orkestrel_contract.arrayOf)(isMemo)(memos);
5149
+ });
4461
5150
  }
4462
5151
  // src/core/contexts/memos/MemoManager.ts
4463
5152
  /**
@@ -4509,7 +5198,10 @@ var MemoManager = class {
4509
5198
  const supplied = options?.snapshot;
4510
5199
  const owned = supplied === void 0 ? void 0 : copyJSON(supplied);
4511
5200
  const snapshot = isMemoSnapshot(owned) ? owned : void 0;
4512
- if (supplied !== void 0 && snapshot === void 0) throw new ContextError("SNAPSHOT", "context error: memo snapshot is malformed; pass what snapshot() returned");
5201
+ if (supplied !== void 0 && snapshot === void 0) {
5202
+ const faults = memoSnapshotContract.audit(owned);
5203
+ throw new ContextError("SNAPSHOT", `context error: memo snapshot is malformed; pass what snapshot() returned\n${(0, _orkestrel_contract.describeFaults)(faults, memoSnapshotContract.schema)}`, { context: { faults } });
5204
+ }
4513
5205
  this.#id = snapshot?.id ?? options?.id ?? crypto.randomUUID();
4514
5206
  this.#emitter = new _orkestrel_emitter.Emitter(options);
4515
5207
  this.#format = options?.format;
@@ -4900,8 +5592,9 @@ var MemoryMemoStore = class {
4900
5592
  * the row `{ id: snapshot.id, snapshot }`.
4901
5593
  * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
4902
5594
  * a {@link MemoSnapshot} ({@link import('../validators.js').isMemoSnapshot} — the total guard for
4903
- * an untrusted storage read), or `undefined` if none is stored, the blob is malformed, or the
4904
- * blob's own `id` is not the requested one.
5595
+ * an untrusted storage read), or `undefined` only when no row is stored. A row whose blob is malformed
5596
+ * or names another id throws a `ContextError` coded `'SNAPSHOT'`, so a host never overwrites it
5597
+ * unawares, and `delete` still removes it.
4905
5598
  * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
4906
5599
  *
4907
5600
  * @example
@@ -4934,18 +5627,35 @@ var DatabaseMemoStore = class {
4934
5627
  *
4935
5628
  * @remarks
4936
5629
  * The column is an untrusted storage read, so a blob that fails
4937
- * {@link import('../validators.js').isMemoSnapshot}, or whose own `id` is not `id`, resolves
4938
- * `undefined` rather than a memo set under the wrong key.
5630
+ * {@link import('../validators.js').isMemoSnapshot}, or whose own `id` is not `id`, throws rather
5631
+ * than reading as absent.
4939
5632
  *
4940
5633
  * @param id - The memo set id to resolve (the row key)
4941
- * @returns The stored snapshot, or `undefined` when no row is stored, the blob is malformed, or
4942
- * the blob names another id
5634
+ * @returns The stored snapshot, or `undefined` when no row is stored
5635
+ * @throws {ContextError} Thrown when the row's blob is malformed or names another id (code `'SNAPSHOT'`), with the faults under `context.faults`
4943
5636
  */
4944
5637
  async get(id) {
4945
5638
  const row = await this.#table.get(id);
4946
5639
  if (row === void 0) return void 0;
4947
5640
  const snapshot = row.snapshot;
4948
- return isMemoSnapshot(snapshot) && snapshot.id === id ? snapshot : void 0;
5641
+ if (isMemoSnapshot(snapshot)) {
5642
+ if (snapshot.id === id) return snapshot;
5643
+ const faults = [{
5644
+ reason: "type",
5645
+ path: ["id"],
5646
+ expected: "literal",
5647
+ received: (0, _orkestrel_contract.preview)(snapshot.id)
5648
+ }];
5649
+ throw new ContextError("SNAPSHOT", describeSnapshotRefusal("memo set", id, faults), { context: {
5650
+ id,
5651
+ faults
5652
+ } });
5653
+ }
5654
+ const faults = memoSnapshotContract.audit(snapshot);
5655
+ throw new ContextError("SNAPSHOT", describeSnapshotRefusal("memo set", id, faults), { context: {
5656
+ id,
5657
+ faults
5658
+ } });
4949
5659
  }
4950
5660
  /**
4951
5661
  * Inserts or replaces under the snapshot's own `id` (no separate id param) — the row is
@@ -4953,8 +5663,14 @@ var DatabaseMemoStore = class {
4953
5663
  *
4954
5664
  * @param snapshot - The snapshot to store, keyed by its `id`
4955
5665
  * @returns A promise that resolves after the row is written
5666
+ * @throws {ContextError} Thrown before any write when the snapshot fails the guard `get` reads (code `'SNAPSHOT'`), so no row is written that `get` refuses
4956
5667
  */
4957
5668
  async set(snapshot) {
5669
+ if (!isMemoSnapshot(snapshot)) {
5670
+ const unchecked = snapshot;
5671
+ const faults = memoSnapshotContract.audit(unchecked);
5672
+ throw new ContextError("SNAPSHOT", `a memo store writes only what snapshot() returned\n${(0, _orkestrel_contract.describeFaults)(faults, memoSnapshotContract.schema)}`, { context: { faults } });
5673
+ }
4958
5674
  await this.#table.set({
4959
5675
  id: snapshot.id,
4960
5676
  snapshot
@@ -5443,10 +6159,6 @@ function createAgentContext(options) {
5443
6159
  */
5444
6160
  var DEFAULT_AGENT_LIMIT = 10;
5445
6161
  /**
5446
- * Names the default zone for an unmatched tool call allowed by an authority.
5447
- */
5448
- var DEFAULT_AUTHORITY_ZONE = "default";
5449
- /**
5450
6162
  * Holds the model-facing texts the loop writes after a tool name in a denial result — the
5451
6163
  * `denied` note for a deny with no reason, an evaluate throw, an invalid decision, or a tool
5452
6164
  * changed after its decision; the `refused` note for an ask the approver refused; and the
@@ -5551,13 +6263,16 @@ function isAgentJobError(value) {
5551
6263
  });
5552
6264
  }
5553
6265
  /**
5554
- * Reports an `ask` on an authority that has no approver or that its approver refuses, a run sharing accounting or a conversation with a live run, a `limit` that is not a nonnegative safe integer, a name absent from a registry pool, or a strict run whose request exceeds its window. Agent construction, the `stream` and `generate` methods, and registry accessors throw synchronously, before returning a handle or promise; a `'WINDOW'` refusal rejects the run's `result` instead, and an `'APPROVER'` refusal rejects the promise the `ask` method of an `Authority` or the `ask` handler of an `Approver` returns. Await the call inside `try`/`catch` to catch it.
6266
+ * Reports an `ask` on an authority that has no approver or that its approver refuses, an authority missing its fallback or a rule's verdict, tools set with no authority, a run sharing accounting or a conversation with a live run, a `limit` that is not a positive safe integer, a name absent from a registry pool, a strict run's answer that fails its schema, or a strict run whose request exceeds its window. Agent construction, the `stream` and `generate` methods, and registry accessors throw synchronously, before returning a handle or promise; a `'WINDOW'` refusal rejects the run's `result` instead, and an `'APPROVER'` refusal rejects the promise the `ask` method of an `Authority` or the `ask` handler of an `Approver` returns. Await the call inside `try`/`catch` to catch it.
5555
6267
  *
5556
6268
  * @remarks
5557
6269
  * `'APPROVER'` reports an `ask` on an `Authority` built with no `ask` handler, or an ask an
5558
6270
  * `Approver` refuses: a call framed while no ask waited for it, a second ask for a waiting call, or
5559
6271
  * arguments it cannot render. The agent loop reads it as an unanswered ask and denies the call. Its
5560
6272
  * `context` names the asked `tool`.
6273
+ * `'AUTHORITY'` reports an `Authority` built with no valid `fallback`, a rule with no valid `verdict`
6274
+ * (its `context` names the `rule` index), and an agent built with `tools` and no `authority`. Each
6275
+ * throws at construction, with the options a host passes to correct it in the message.
5561
6276
  * `'CONCURRENCY'` reports a run started while another run on the same agent is live, when the
5562
6277
  * agent carries a `window` (a shared context budget) or when the run's effective `budget` or
5563
6278
  * `allowance` is an instance a live run holds — the second run's `start()` would replace that
@@ -5566,17 +6281,18 @@ function isAgentJobError(value) {
5566
6281
  * `agent` id, and the `conversation` id for a shared conversation. For concurrent runs, use
5567
6282
  * separate agents, or one agent with no `window`, distinct budget and allowance instances per
5568
6283
  * run, and a distinct conversation per run.
5569
- * `'LIMIT'` reports a construction or per-run `limit` that is not a nonnegative safe integer,
6284
+ * `'LIMIT'` reports a construction or per-run `limit` that is not a positive safe integer,
5570
6285
  * including one a rehydrated job carries into `build`. `'REGISTRY'` reports a rehydration name
5571
- * absent from its registry pool, on `provider` / `tool` / `authority` / `scheduler` / `build`;
5572
- * its `context` names the `category` and the `name`. `'WINDOW'` reports a `strict` run whose
6286
+ * absent from its registry pool, on `provider` / `tool` / `authority` / `scheduler` / `build`,
6287
+ * whose `context` names the `category` and the `name`, and a job that names `tools` and no
6288
+ * `authority` at `build`, whose `context` names the `tools`. `'WINDOW'` reports a `strict` run whose
5573
6289
  * complete provider request is still over the `window` budget, equal to its `max` included, after
5574
6290
  * the one permitted fold or with no fold possible; it settles the run `error` before the provider
5575
6291
  * is called, and its `context` holds the `agent` id, the measured `consumed`, and the window `max`.
5576
6292
  * Narrow a caught value with {@link isAgentError} and branch on `error.code`.
5577
6293
  */
5578
6294
  var AgentError = class extends Error {
5579
- /** Names the machine-readable condition — `'APPROVER'`: an `ask` on an authority that has no approver, or one its approver refuses; `'CONCURRENCY'`: a run sharing accounting or a conversation with a live run; `'LIMIT'`: a `limit` that is not a nonnegative safe integer; `'REGISTRY'`: a rehydration name absent from its registry pool; `'WINDOW'`: a strict run whose complete request is still over its window budget. */
6295
+ /** Names the machine-readable condition — `'APPROVER'`: an `ask` on an authority that has no approver, or one its approver refuses; `'AUTHORITY'`: an authority missing its fallback or a rule's verdict, or tools set with no authority; `'CONCURRENCY'`: a run sharing accounting or a conversation with a live run; `'LIMIT'`: a `limit` that is not a positive safe integer; `'REGISTRY'`: a rehydration name absent from its registry pool; `'WINDOW'`: a strict run whose complete request is still over its window budget. */
5580
6296
  code;
5581
6297
  /** Holds the values that locate the failure, such as the `agent` id of a refused run, or `undefined` when none were supplied. */
5582
6298
  context;
@@ -5615,13 +6331,34 @@ function isAgentError(value) {
5615
6331
  return (0, _orkestrel_contract.holds)(() => {
5616
6332
  if (!(value instanceof Error && _orkestrel_contract.INTRINSICS.prototype(value) !== Error.prototype && _orkestrel_contract.INTRINSICS.describe(value, Symbol.for("@orkestrel/agent.AgentError"))?.value === value)) return false;
5617
6333
  const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
5618
- if (!(code === "APPROVER" || code === "CONCURRENCY" || code === "LIMIT" || code === "REGISTRY" || code === "WINDOW")) return false;
6334
+ if (!(code === "APPROVER" || code === "AUTHORITY" || code === "CONCURRENCY" || code === "LIMIT" || code === "REGISTRY" || code === "SCHEMA" || code === "WINDOW")) return false;
5619
6335
  const context = _orkestrel_contract.INTRINSICS.reflect.read(value, "context");
5620
6336
  return context === void 0 || (0, _orkestrel_contract.isObject)(context) || (0, _orkestrel_contract.isFunction)(context);
5621
6337
  });
5622
6338
  }
5623
6339
  // src/core/agents/shapers.ts
5624
6340
  /**
6341
+ * Describes a durable agent job — the record `AgentRegistry.build` checks exactly, so a misspelled key
6342
+ * refuses instead of running an agent without it.
6343
+ *
6344
+ * @remarks
6345
+ * `children` holds JSON records, because each child crosses this shape at its own `build`. `limit`
6346
+ * reads as a number here and as a positive safe integer at the agent's construction, which owns that
6347
+ * refusal.
6348
+ */
6349
+ var agentJobShape = (0, _orkestrel_contract.objectShape)({
6350
+ provider: (0, _orkestrel_contract.stringShape)(),
6351
+ messages: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)(messageInputShape)),
6352
+ system: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
6353
+ tools: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)())),
6354
+ authority: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
6355
+ scheduler: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
6356
+ limit: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.numberShape)()),
6357
+ timeout: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.numberShape)()),
6358
+ budget: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.numberShape)()),
6359
+ children: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.recordShape)((0, _orkestrel_contract.jsonShape)())))
6360
+ });
6361
+ /**
5625
6362
  * Describes the agent frame wire — one exact object per `AgentFrame` arm, discriminated by
5626
6363
  * `category`.
5627
6364
  *
@@ -5693,72 +6430,10 @@ var agentAnswerShape = (0, _orkestrel_contract.objectShape)({
5693
6430
  var agentFrameContract = (0, _orkestrel_contract.createContract)(agentFrameShape);
5694
6431
  /** Validates and projects agent prompts at the run's `prompt` option and a page's request body. */
5695
6432
  var agentPromptContract = (0, _orkestrel_contract.createContract)(agentPromptShape);
6433
+ /** Checks a durable agent job exactly at `AgentRegistry.build`. */
6434
+ var agentJobContract = (0, _orkestrel_contract.createContract)(agentJobShape);
5696
6435
  /** Validates and projects a page's answers to `ask` frames at a JSON wire boundary. */
5697
6436
  var agentAnswerContract = (0, _orkestrel_contract.createContract)(agentAnswerShape);
5698
- // src/core/agents/Authority.ts
5699
- /**
5700
- * Gates the agent loop's tool calls — the synchronous policy consulted before each call runs,
5701
- * turning one {@link AuthorityContext} into an {@link AuthorityDecision} by walking the ordered
5702
- * rules first-match-wins and falling back to a configurable default, which allows an unmatched
5703
- * call unless its `fallback` decides otherwise, and the approver that answers an `ask` verdict.
5704
- *
5705
- * @remarks
5706
- * - **Ordered, first-match-wins.** `evaluate` walks the configured rules in order and
5707
- * returns the first whose `match(context)` is true as
5708
- * `{ zone, verdict: rule.verdict ?? 'allow', reason }` — a matched rule with no `verdict`
5709
- * allows.
5710
- * - **Fallback.** When no rule matches, `evaluate` returns the configured `fallback`.
5711
- * Default: `{ zone: DEFAULT_AUTHORITY_ZONE, verdict: 'allow' }` (allow-unmatched),
5712
- * so a rules list of denials behaves as a denylist. To make the gate deny-by-default
5713
- * (an allowlist — only matched rules that allow get through), pass a `verdict: 'deny'`
5714
- * `fallback`.
5715
- * - **Consulted before each tool call.** The agent loop calls `evaluate({ call })` for
5716
- * every {@link import('@orkestrel/tool').ToolCall} the model emits; a denied call is fed
5717
- * back to the model as a denial {@link import('@orkestrel/tool').ToolResult} (a `tool`
5718
- * chunk + a tool message) instead of being executed — no tool execution —
5719
- * so the model receives the denial and can react.
5720
- * - **Approver.** `ask` answers an `ask` verdict through the `ask` option's
5721
- * {@link AuthorityHandler}, as a promise for a synchronous or an async handler; a handler
5722
- * throw rejects it. With no handler it rejects with an `AgentError` coded `'APPROVER'`, which
5723
- * the loop reads as an unanswered ask. It never throws synchronously.
5724
- * - **Synchronous.** `evaluate` returns the verdict directly; only `ask` is async.
5725
- * - **Event-free.** A purely functional gate — no Emitter, no events.
5726
- *
5727
- * @example
5728
- * ```ts
5729
- * // A denylist: deny the `delete` tool, allow everything else (default fallback).
5730
- * const authority = new Authority({
5731
- * rules: [{ match: (c) => c.call.name === 'delete', zone: 'restricted', verdict: 'deny' }],
5732
- * })
5733
- * authority.evaluate({ call: { id: '1', name: 'delete', arguments: {} } }) // { zone: 'restricted', verdict: 'deny' }
5734
- * authority.evaluate({ call: { id: '2', name: 'add', arguments: {} } }) // { zone: 'default', verdict: 'allow' }
5735
- * ```
5736
- */
5737
- var Authority = class {
5738
- #rules;
5739
- #fallback;
5740
- #handler;
5741
- constructor(options) {
5742
- this.#rules = options?.rules ?? [];
5743
- this.#fallback = options?.fallback ?? {
5744
- zone: "default",
5745
- verdict: "allow"
5746
- };
5747
- this.#handler = options?.ask;
5748
- }
5749
- evaluate(context) {
5750
- for (const rule of this.#rules) if (rule.match(context)) return {
5751
- zone: rule.zone,
5752
- verdict: rule.verdict ?? "allow",
5753
- ...rule.reason === void 0 ? {} : { reason: rule.reason }
5754
- };
5755
- return this.#fallback;
5756
- }
5757
- async ask(context, decision, signal) {
5758
- if (this.#handler === void 0) throw new AgentError("APPROVER", "an asked call needs an approver and none is set", { context: { tool: context.call.name } });
5759
- return this.#handler(context, decision, signal);
5760
- }
5761
- };
5762
6437
  // src/core/agents/ChannelCell.ts
5763
6438
  /**
5764
6439
  * Holds one value a `Channel` buffers and the cell pushed after it.
@@ -5883,20 +6558,61 @@ var Channel = class {
5883
6558
  });
5884
6559
  }
5885
6560
  };
6561
+ // src/core/agents/validators.ts
6562
+ /**
6563
+ * Checks whether a value is an authority verdict: `allow`, `ask`, or `deny`.
6564
+ *
6565
+ * @param value - The unknown verdict candidate
6566
+ * @returns True if the value names a verdict; false otherwise
6567
+ *
6568
+ * @example
6569
+ * ```ts
6570
+ * isAuthorityVerdict('ask') // true
6571
+ * isAuthorityVerdict('maybe') // false
6572
+ * ```
6573
+ */
6574
+ function isAuthorityVerdict(value) {
6575
+ return value === "allow" || value === "ask" || value === "deny";
6576
+ }
6577
+ /**
6578
+ * Checks whether a value is an authority decision: an object whose `zone` is a string, whose
6579
+ * `verdict` is `allow`, `ask`, or `deny`, and whose `reason` is absent or a string.
6580
+ *
6581
+ * @remarks
6582
+ * Total: each member is read once, unknown members are admitted, and a non-object, a 0.0.34
6583
+ * `{ zone, allowed }` decision, an unknown verdict, a throwing getter, and a revoked or throwing
6584
+ * `Proxy` return false rather than throw.
6585
+ *
6586
+ * @param value - The unknown decision candidate
6587
+ * @returns True if the value satisfies the decision contract; false otherwise
6588
+ *
6589
+ * @example
6590
+ * ```ts
6591
+ * isAuthorityDecision({ zone: 'default', verdict: 'ask', reason: 'confirm' }) // true
6592
+ * isAuthorityDecision({ zone: 'default', allowed: true }) // false
6593
+ * ```
6594
+ */
6595
+ function isAuthorityDecision(value) {
6596
+ return (0, _orkestrel_contract.objectOf)({
6597
+ zone: _orkestrel_contract.isString,
6598
+ verdict: isAuthorityVerdict,
6599
+ reason: _orkestrel_contract.isString
6600
+ }, ["reason"])(value);
6601
+ }
5886
6602
  // src/core/agents/helpers.ts
5887
6603
  /**
5888
6604
  * Projects an unknown value onto a fresh, exact `JSONValue` representation of an
5889
6605
  * {@link AgentResult} — capturing each structural field once through a total boundary,
5890
- * accepting conforming accessors and inherited properties, preserving finite negative and
5891
- * fractional usage counts, dropping extras, and resolving `undefined` for a malformed field, a
5892
- * non-finite usage number, a throwing getter, or a malformed or revoked proxy.
6606
+ * accepting conforming accessors and inherited properties, preserving fractional usage counts,
6607
+ * dropping extras, and resolving `undefined` for a malformed field, a negative or non-finite usage
6608
+ * number, a throwing getter, or a malformed or revoked proxy.
5893
6609
  *
5894
6610
  * @remarks
5895
6611
  * This is a total projection that reads untrusted input. Each structural field is captured once
5896
6612
  * through Contract's sanctioned exception boundary, so conforming accessors and inherited
5897
6613
  * properties are supported while a throwing getter or revoked proxy returns `undefined`.
5898
- * Present usage counts must be finite numbers; negative and fractional values are preserved,
5899
- * not normalized. The result is exact `{ content, thinking?, usage?, partial, stop? }`. A present
6614
+ * Present usage counts must be finite and nonnegative; fractional values are preserved, not
6615
+ * normalized. The result is exact `{ content, thinking?, usage?, partial, stop? }`. A present
5900
6616
  * `stop` is read once, then its `layer`, `id`, `max`, `consumed`, `estimate`, and `usage` once each
5901
6617
  * in that order, `max` and `consumed` only on a `meter` or `budget` arm; the arm is rebuilt
5902
6618
  * exactly, omitting absent readings, and a malformed `stop` refuses the whole value. Extra input
@@ -5963,9 +6679,9 @@ function agentResultToJSON(value) {
5963
6679
  }
5964
6680
  /**
5965
6681
  * Projects an unknown value onto a fresh, exact `TokenUsage` — capturing `prompt`, `completion`, and
5966
- * `total` once each through a total boundary, preserving finite negative and fractional counts,
5967
- * dropping extras, and resolving `undefined` for a non-object, a missing or non-finite count, a
5968
- * throwing getter, or a malformed or revoked proxy.
6682
+ * `total` once each through a total boundary, preserving fractional counts, dropping extras, and
6683
+ * resolving `undefined` for a non-object, a missing, negative, or non-finite count, a throwing getter,
6684
+ * or a malformed or revoked proxy.
5969
6685
  *
5970
6686
  * @remarks
5971
6687
  * The capture runs inside Contract's sanctioned exception boundary, so conforming accessors and
@@ -5974,13 +6690,14 @@ function agentResultToJSON(value) {
5974
6690
  * before it projects, because this function refuses `undefined` like any other non-object.
5975
6691
  *
5976
6692
  * @param value - The unknown value to project
5977
- * @returns A fresh `{ prompt, completion, total }`, or `undefined` when the value is not a finite usage triple
6693
+ * @returns A fresh `{ prompt, completion, total }`, or `undefined` when the value is not a finite nonnegative usage triple
5978
6694
  *
5979
6695
  * @example
5980
6696
  * ```ts
5981
6697
  * import { tokenUsageToJSON } from '@orkestrel/agent'
5982
6698
  *
5983
- * tokenUsageToJSON({ prompt: 2, completion: -1, total: 1.5, cached: 4 }) // { prompt: 2, completion: -1, total: 1.5 }
6699
+ * tokenUsageToJSON({ prompt: 2, completion: 1, total: 1.5, cached: 4 }) // { prompt: 2, completion: 1, total: 1.5 }
6700
+ * tokenUsageToJSON({ prompt: 2, completion: -1, total: 1 }) // undefined
5984
6701
  * tokenUsageToJSON({ prompt: 2, completion: Infinity, total: 3 }) // undefined
5985
6702
  * ```
5986
6703
  */
@@ -5990,7 +6707,7 @@ function tokenUsageToJSON(value) {
5990
6707
  const prompt = Reflect.get(value, "prompt");
5991
6708
  const completion = Reflect.get(value, "completion");
5992
6709
  const total = Reflect.get(value, "total");
5993
- return (0, _orkestrel_contract.isFiniteNumber)(prompt) && (0, _orkestrel_contract.isFiniteNumber)(completion) && (0, _orkestrel_contract.isFiniteNumber)(total) ? {
6710
+ return (0, _orkestrel_budget.isBudgetAmount)(prompt) && (0, _orkestrel_budget.isBudgetAmount)(completion) && (0, _orkestrel_budget.isBudgetAmount)(total) ? {
5994
6711
  prompt,
5995
6712
  completion,
5996
6713
  total
@@ -6099,7 +6816,11 @@ function agentResultToFrame(result) {
6099
6816
  * @example
6100
6817
  * ```ts
6101
6818
  * const approver = createApprover()
6102
- * const authority = createAuthority({ rules, ask: approver.ask })
6819
+ * const authority = createAuthority({
6820
+ * rules,
6821
+ * fallback: { zone: 'default', verdict: 'deny' },
6822
+ * ask: approver.ask,
6823
+ * })
6103
6824
  * const agent = createAgent(provider, { tools, authority })
6104
6825
  * for await (const frame of agentStreamToFrames(agent.stream({ prompt }), approver)) {
6105
6826
  * send(JSON.stringify(frame))
@@ -6175,30 +6896,6 @@ function inferStop(reason, allowance, budget, provider, turn) {
6175
6896
  };
6176
6897
  }
6177
6898
  /**
6178
- * Estimates the context-token footprint of a string — the deterministic `ceil(length / 4)`
6179
- * character heuristic {@link estimateMessages} sums over a conversation's messages (the default
6180
- * context-budget estimator).
6181
- *
6182
- * @remarks
6183
- * Approximates `ceil(length / 4)` (4 characters per token), so the same input always yields the same estimate (no model round-trip).
6184
- * Empty text is `0`. This is a planning heuristic for reasoning about how much a turn's
6185
- * messages cost the next request, not an exact tokenizer count — it never calls a provider,
6186
- * so the agent layer stays provider-agnostic and synchronous where it can be.
6187
- *
6188
- * @param text - The text to estimate (a section summary, a message's content)
6189
- * @returns The estimated token count (`ceil(text.length / 4)`; `0` for empty text)
6190
- *
6191
- * @example
6192
- * ```ts
6193
- * estimateTokens('') // 0
6194
- * estimateTokens('hello') // 2 (ceil(5 / 4))
6195
- * estimateTokens('a'.repeat(40)) // 10
6196
- * ```
6197
- */
6198
- function estimateTokens(text) {
6199
- return Math.ceil(text.length / 4);
6200
- }
6201
- /**
6202
6899
  * Estimates the context-token footprint of a batch of messages — each message's content plus
6203
6900
  * {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD}, a tool-call JSON estimate, a thinking estimate, and
6204
6901
  * {@link import('./constants.js').IMAGE_TOKEN_ESTIMATE} for each attached image. The {@link estimateRequest} message term
@@ -6493,6 +7190,84 @@ function resolveDecision(decision, definition) {
6493
7190
  };
6494
7191
  }
6495
7192
  /**
7193
+ * Checks a run's final answer against its schema: the content must parse as JSON and satisfy the
7194
+ * schema, or the call throws a coded error carrying the faults.
7195
+ *
7196
+ * @remarks
7197
+ * The schema enters through the lenient `schemaToShape`, which widens every keyword it cannot express
7198
+ * and never narrows, because a provider constrains its own output and a false refusal would discard a
7199
+ * good answer. The check is `is`, because `explain` alone passes what `parse` would repair. Each call
7200
+ * compiles once, and a run has one final answer.
7201
+ *
7202
+ * @param content - The final answer's text
7203
+ * @param schema - The run's JSON Schema
7204
+ * @throws {AgentError} Thrown when the content is not JSON, misses the schema, or the schema does not compile (code `'SCHEMA'`), with the faults under `context.faults` for the first two
7205
+ *
7206
+ * @example
7207
+ * ```ts
7208
+ * requireAnswer('{"a":5}', { type: 'object', properties: { a: { type: 'number' } } })
7209
+ * requireAnswer('nope', { type: 'object' }) // throws AgentError ('SCHEMA')
7210
+ * ```
7211
+ */
7212
+ function requireAnswer(content, schema) {
7213
+ const compiled = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.schemaToShape)(schema)));
7214
+ if (!compiled.success) throw new AgentError("SCHEMA", "the run schema does not compile; pass a JSON Schema built from plain values", { cause: compiled.error });
7215
+ const parsed = (0, _orkestrel_contract.parseJSON)(content);
7216
+ const faults = parsed === void 0 ? [{
7217
+ reason: "type",
7218
+ path: [],
7219
+ expected: "json",
7220
+ received: (0, _orkestrel_contract.preview)(content)
7221
+ }] : compiled.value.is(parsed) ? [] : compiled.value.audit(parsed);
7222
+ if (faults.length > 0) throw new AgentError("SCHEMA", `the final answer does not satisfy the run schema; pass strict: false to createAgent to take the raw content from a provider that ignores schemas\n${(0, _orkestrel_contract.describeFaults)(faults, compiled.value.schema)}`, { context: { faults } });
7223
+ }
7224
+ /**
7225
+ * Returns an authority's no-match decision after checking the options carry one, or throws a coded
7226
+ * error naming the two fallbacks a host chooses between.
7227
+ *
7228
+ * @remarks
7229
+ * A rules list says nothing about the calls it does not match, so no fallback is assumed: a denylist
7230
+ * and an allowlist differ only here. The check is {@link isAuthorityDecision}, a total guard, so a
7231
+ * missing options object and a malformed fallback refuse alike.
7232
+ *
7233
+ * @param options - The authority's options as a host passed them
7234
+ * @returns The fallback, narrowed to an {@link AuthorityDecision}
7235
+ * @throws {AgentError} Thrown when the options carry no valid `fallback` (code `'AUTHORITY'`)
7236
+ *
7237
+ * @example
7238
+ * ```ts
7239
+ * requireFallback({ fallback: { zone: 'default', verdict: 'deny' } }) // { zone: 'default', verdict: 'deny' }
7240
+ * ```
7241
+ */
7242
+ function requireFallback(options) {
7243
+ const fallback = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isObject)(options) ? Reflect.get(options, "fallback") : void 0);
7244
+ if (fallback.success && isAuthorityDecision(fallback.value)) return fallback.value;
7245
+ throw new AgentError("AUTHORITY", "an authority needs a fallback for the calls no rule matches; pass fallback: { zone: 'default', verdict: 'deny' } for an allowlist, or fallback: { zone: 'default', verdict: 'allow' } for a denylist");
7246
+ }
7247
+ /**
7248
+ * Returns an authority's rules after checking each one names a verdict, or throws a coded error naming
7249
+ * the first rule that does not.
7250
+ *
7251
+ * @remarks
7252
+ * Absent rules return an empty list. A rule's verdict decides a tool call, so a rule with none is
7253
+ * refused rather than read as an allow. The error's `context` carries the rule's index as `rule`.
7254
+ *
7255
+ * @param rules - The ordered rules, or `undefined` for none
7256
+ * @returns The rules unchanged, or an empty list when none were supplied
7257
+ * @throws {AgentError} Thrown when a rule has no valid `verdict` (code `'AUTHORITY'`)
7258
+ *
7259
+ * @example
7260
+ * ```ts
7261
+ * requireRules([{ match: () => true, zone: 'files', verdict: 'ask' }]).length // 1
7262
+ * ```
7263
+ */
7264
+ function requireRules(rules) {
7265
+ if (rules === void 0) return [];
7266
+ const refused = Array.from(rules).findIndex((rule) => !(0, _orkestrel_contract.objectOf)({ verdict: isAuthorityVerdict })(rule));
7267
+ if (refused !== -1) throw new AgentError("AUTHORITY", `rule ${refused} has no verdict; set verdict: 'allow' | 'ask' | 'deny'`, { context: { rule: refused } });
7268
+ return rules;
7269
+ }
7270
+ /**
6496
7271
  * Consumes a reported usage against a budget over what was already charged, so a turn's total
6497
7272
  * draw is never less than the report and nothing is charged twice.
6498
7273
  *
@@ -6522,26 +7297,25 @@ function chargeUsage(budget, usage, charged) {
6522
7297
  });
6523
7298
  }
6524
7299
  /**
6525
- * Returns a tool-iteration cap after checking it is a nonnegative safe integer, or throws a coded
6526
- * error.
7300
+ * Returns a tool-iteration cap after checking it is a positive safe integer, or throws a coded error.
6527
7301
  *
6528
7302
  * @remarks
6529
- * The check is {@link import('../validators.js').isNonNegativeSafeInteger}, so `0` and `-0`
6530
- * pass while `NaN`, a fraction, a negative, and either infinity are refused. The error's `context`
6531
- * carries the refused value as `limit`.
7303
+ * The check is {@link import('../validators.js').isPositiveSafeInteger}, so `0`, `-0`, `NaN`, a
7304
+ * fraction, a negative, and either infinity are refused, because a run of no turns asks the provider
7305
+ * nothing and answers nothing. The error's `context` carries the refused value as `limit`.
6532
7306
  *
6533
7307
  * @param limit - The tool-iteration cap to check
6534
7308
  * @returns The cap unchanged
6535
- * @throws {AgentError} Thrown when `limit` is not a nonnegative safe integer (code `'LIMIT'`)
7309
+ * @throws {AgentError} Thrown when `limit` is not a positive safe integer (code `'LIMIT'`)
6536
7310
  *
6537
7311
  * @example
6538
7312
  * ```ts
6539
7313
  * requireLimit(3) // 3
6540
- * requireLimit(1.5) // throws AgentError ('LIMIT')
7314
+ * requireLimit(0) // throws AgentError ('LIMIT')
6541
7315
  * ```
6542
7316
  */
6543
7317
  function requireLimit(limit) {
6544
- if (!isNonNegativeSafeInteger(limit)) throw new AgentError("LIMIT", "a limit must be a nonnegative safe integer", { context: { limit } });
7318
+ if (!isPositiveSafeInteger(limit)) throw new AgentError("LIMIT", "a limit must be a positive safe integer, because a run of no turns answers nothing", { context: { limit } });
6545
7319
  return limit;
6546
7320
  }
6547
7321
  /**
@@ -6606,32 +7380,69 @@ function requireConversation(value) {
6606
7380
  });
6607
7381
  }
6608
7382
  /**
6609
- * Renders a tool's output as the string content of its tool message.
7383
+ * Renders a tool's output as the string content of its tool message, or returns `undefined` when JSON
7384
+ * cannot carry it.
6610
7385
  *
6611
7386
  * @remarks
6612
7387
  * A string passes through unchanged and `undefined` renders as `''`. Any other value renders as
6613
7388
  * its `JSON.stringify` text, byte for byte, when JSON can carry it. A value JSON cannot carry (a
6614
- * `BigInt`, a cycle, a symbol, a throwing `toJSON`) renders as `String(value)`, and as `''` when
6615
- * that conversion throws too, so a tool's output never rejects the run that dispatched it.
7389
+ * `BigInt`, a cycle, a symbol, a function, a throwing `toJSON`) returns `undefined`, which
7390
+ * {@link settleToolResult} turns into a tool failure.
6616
7391
  *
6617
7392
  * @param value - The tool's output (`ToolSuccess.value`, which may be `undefined`)
6618
- * @returns The message content
7393
+ * @returns The message content, or `undefined` when JSON cannot carry the value
6619
7394
  *
6620
7395
  * @example
6621
7396
  * ```ts
6622
7397
  * renderToolValue('sunny') // 'sunny'
6623
7398
  * renderToolValue({ count: 2 }) // '{"count":2}'
6624
7399
  * renderToolValue(undefined) // ''
6625
- * renderToolValue(10n) // '10'
7400
+ * renderToolValue(10n) // undefined
6626
7401
  * ```
6627
7402
  */
6628
7403
  function renderToolValue(value) {
6629
7404
  if (typeof value === "string") return value;
6630
7405
  if (value === void 0) return "";
6631
- const rendered = renderJSON(value);
6632
- if (rendered !== void 0) return rendered;
6633
- const fallback = (0, _orkestrel_contract.attempt)(() => String(value));
6634
- return fallback.success ? fallback.value : "";
7406
+ return renderJSON(value);
7407
+ }
7408
+ /**
7409
+ * Settles one dispatched result into the result the loop reports and the content of its tool message,
7410
+ * turning a success whose value JSON cannot carry into a tool failure the model reads.
7411
+ *
7412
+ * @remarks
7413
+ * A failure passes through with its `error` as the content. A success renders through
7414
+ * {@link renderToolValue}. A success that renders to `undefined` becomes the failure arm with the
7415
+ * call's `id` and `name`, whose `error` names the tool and the remedy.
7416
+ *
7417
+ * @param result - The result the tool manager returned for one call
7418
+ * @returns The result to report and the tool message content
7419
+ *
7420
+ * @example
7421
+ * ```ts
7422
+ * settleToolResult({ success: true, id: '1', name: 'read', value: { count: 2 } }).content // '{"count":2}'
7423
+ * settleToolResult({ success: true, id: '1', name: 'read', value: 10n }).outcome.success // false
7424
+ * ```
7425
+ */
7426
+ function settleToolResult(result) {
7427
+ if (!result.success) return {
7428
+ outcome: result,
7429
+ content: result.error
7430
+ };
7431
+ const content = renderToolValue(result.value);
7432
+ if (content !== void 0) return {
7433
+ outcome: result,
7434
+ content
7435
+ };
7436
+ const error = `${(0, _orkestrel_contract.preview)(result.name)} returned a value JSON cannot carry; return strings, numbers, booleans, null, arrays, and plain records`;
7437
+ return {
7438
+ outcome: {
7439
+ success: false,
7440
+ id: result.id,
7441
+ name: result.name,
7442
+ error
7443
+ },
7444
+ content: error
7445
+ };
6635
7446
  }
6636
7447
  /**
6637
7448
  * Returns a registered value or throws when its name is absent.
@@ -6639,7 +7450,7 @@ function renderToolValue(value) {
6639
7450
  * @param category - The registry category named in an error
6640
7451
  * @param name - The requested key
6641
7452
  * @returns The registered value
6642
- * @throws {AgentError} Thrown when the name is absent, with code REGISTRY and a `context` naming the `category` and the `name`.
7453
+ * @throws {AgentError} Thrown when the name is absent, with code REGISTRY, the name encoded and capped in its text because a tool refusal reaches a model, and a `context` naming the `category` and the `name`.
6643
7454
  * @example
6644
7455
  * ```ts
6645
7456
  * requireEntry(new Map([['main', provider]]), 'provider', 'main') // provider
@@ -6647,7 +7458,7 @@ function renderToolValue(value) {
6647
7458
  */
6648
7459
  function requireEntry(pool, category, name) {
6649
7460
  const value = pool.get(name);
6650
- if (value === void 0) throw new AgentError("REGISTRY", `unknown ${category}: ${name}`, { context: {
7461
+ if (value === void 0) throw new AgentError("REGISTRY", `unknown ${category}: ${(0, _orkestrel_contract.preview)(name)}`, { context: {
6651
7462
  category,
6652
7463
  name
6653
7464
  } });
@@ -6670,32 +7481,6 @@ function extractQueueOptions(options) {
6670
7481
  ...timeout === void 0 ? {} : { timeout }
6671
7482
  };
6672
7483
  }
6673
- // src/core/agents/validators.ts
6674
- /**
6675
- * Checks whether a value is an authority decision: an object whose `zone` is a string, whose
6676
- * `verdict` is `allow`, `ask`, or `deny`, and whose `reason` is absent or a string.
6677
- *
6678
- * @remarks
6679
- * Total: each member is read once, unknown members are admitted, and a non-object, a 0.0.34
6680
- * `{ zone, allowed }` decision, an unknown verdict, a throwing getter, and a revoked or throwing
6681
- * `Proxy` return false rather than throw.
6682
- *
6683
- * @param value - The unknown decision candidate
6684
- * @returns True if the value satisfies the decision contract; false otherwise
6685
- *
6686
- * @example
6687
- * ```ts
6688
- * isAuthorityDecision({ zone: 'default', verdict: 'ask', reason: 'confirm' }) // true
6689
- * isAuthorityDecision({ zone: 'default', allowed: true }) // false
6690
- * ```
6691
- */
6692
- function isAuthorityDecision(value) {
6693
- return (0, _orkestrel_contract.objectOf)({
6694
- zone: _orkestrel_contract.isString,
6695
- verdict: (0, _orkestrel_contract.literalOf)("allow", "ask", "deny"),
6696
- reason: _orkestrel_contract.isString
6697
- }, ["reason"])(value);
6698
- }
6699
7484
  // src/core/agents/Agent.ts
6700
7485
  /**
6701
7486
  * Composes a {@link ProviderInterface}, an {@link AgentContext}, and a
@@ -6763,6 +7548,7 @@ var Agent = class {
6763
7548
  #settled = "idle";
6764
7549
  #runs = /* @__PURE__ */ new Map();
6765
7550
  constructor(provider, options) {
7551
+ if (options?.tools !== void 0 && options.authority === void 0) throw new AgentError("AUTHORITY", "tools are set and no authority is; pass authority: createAuthority({ fallback: { zone: 'default', verdict: 'allow' } }) to allow every call, or rules with a deny fallback");
6766
7552
  this.#limit = requireLimit(options?.limit ?? 10);
6767
7553
  this.#id = crypto.randomUUID();
6768
7554
  this.#provider = provider;
@@ -6783,9 +7569,9 @@ var Agent = class {
6783
7569
  this.#allowance = options?.allowance;
6784
7570
  this.#scheduler = options?.scheduler;
6785
7571
  this.#signal = options?.signal;
6786
- this.#authority = options?.authority ?? new Authority();
7572
+ this.#authority = options?.authority;
6787
7573
  this.#window = options?.window;
6788
- this.#strict = options?.strict ?? false;
7574
+ this.#strict = options?.strict ?? true;
6789
7575
  this.#emitter = new _orkestrel_emitter.Emitter(options);
6790
7576
  }
6791
7577
  get id() {
@@ -6805,14 +7591,17 @@ var Agent = class {
6805
7591
  }
6806
7592
  stream(options) {
6807
7593
  const prompt = options?.prompt;
6808
- if (prompt !== void 0 && !agentPromptContract.is(prompt)) throw new _orkestrel_contract.ContractError("Agent: prompt must be an exact AgentPrompt", {
6809
- code: "placement",
6810
- context: {
6811
- path: ["options", "prompt"],
6812
- limit: "AgentPrompt",
6813
- received: (0, _orkestrel_contract.preview)(prompt)
6814
- }
6815
- });
7594
+ if (prompt !== void 0 && !agentPromptContract.is(prompt)) {
7595
+ const faults = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.describeFaults)(agentPromptContract.audit(prompt), agentPromptContract.schema));
7596
+ throw new _orkestrel_contract.ContractError(`Agent: prompt must be an exact AgentPrompt${faults.success ? `\n${faults.value}` : ""}`, {
7597
+ code: "placement",
7598
+ context: {
7599
+ path: ["options", "prompt"],
7600
+ limit: "AgentPrompt",
7601
+ received: (0, _orkestrel_contract.preview)(prompt)
7602
+ }
7603
+ });
7604
+ }
6816
7605
  const limit = options?.limit === void 0 ? this.#limit : requireLimit(options.limit);
6817
7606
  const budget = requireBudget(options?.budget ?? this.#budget, "budget");
6818
7607
  const allowance = requireBudget(options?.allowance ?? this.#allowance, "allowance");
@@ -6900,7 +7689,6 @@ var Agent = class {
6900
7689
  ...schema === void 0 ? {} : { schema }
6901
7690
  };
6902
7691
  this.#window?.clear();
6903
- const cancelled = abort.signal.aborted;
6904
7692
  const turns = [];
6905
7693
  while (turns.length < limit && (turns.at(-1)?.outcome ?? "tools") === "tools") {
6906
7694
  const index = turns.length;
@@ -6975,12 +7763,11 @@ var Agent = class {
6975
7763
  estimate: settled.estimate
6976
7764
  };
6977
7765
  }
6978
- if (definitions === void 0) for (const call of result.tools ?? []) this.#emitter.emit("deny", call, "no tool is advertised in the active scope", void 0);
6979
- else if (result.tools !== void 0 && result.tools.length > 0) {
7766
+ if (result.tools !== void 0 && result.tools.length > 0) {
6980
7767
  const admittedCalls = filterAllowList(names, result.tools, (call) => call.name);
6981
7768
  const authorization = this.#authorize(result.tools, admittedCalls, advertised, held);
6982
7769
  if (authorization.asked.length > 0 && !abort.signal.aborted) yield* this.#approve(tools, authorization, abort.signal);
6983
- if (!abort.signal.aborted) this.#confirm(tools, authorization);
7770
+ if (!abort.signal.aborted) this.#confirm(tools, authorization, advertised);
6984
7771
  if (abort.signal.aborted) {
6985
7772
  turns.push({
6986
7773
  ...settled,
@@ -6995,11 +7782,12 @@ var Agent = class {
6995
7782
  calls: result.tools,
6996
7783
  ...recorded
6997
7784
  });
6998
- const results = await this.#dispatch(tools, result.tools, abort.signal, authorization);
7785
+ const results = await this.#dispatch(tools, result.tools, abort.signal, authorization, advertised);
6999
7786
  const replies = [];
7000
7787
  for (const [position, call] of result.tools.entries()) {
7001
- const outcome = results[position];
7002
- if (outcome === void 0) continue;
7788
+ const dispatched = results[position];
7789
+ if (dispatched === void 0) continue;
7790
+ const { outcome, content } = settleToolResult(dispatched);
7003
7791
  this.#emitter.emit("tool", call, outcome);
7004
7792
  yield {
7005
7793
  category: "tool",
@@ -7008,7 +7796,7 @@ var Agent = class {
7008
7796
  };
7009
7797
  replies.push(conversation.add({
7010
7798
  role: "tool",
7011
- content: outcome.success ? renderToolValue(outcome.value) : outcome.error,
7799
+ content,
7012
7800
  call: call.id
7013
7801
  }));
7014
7802
  }
@@ -7024,6 +7812,13 @@ var Agent = class {
7024
7812
  });
7025
7813
  continue;
7026
7814
  }
7815
+ if (schema !== void 0 && result.truncated !== true && !abort.signal.aborted) {
7816
+ const answered = (0, _orkestrel_contract.attempt)(() => requireAnswer(result.content, schema));
7817
+ if (!answered.success) {
7818
+ if (this.#strict) throw answered.error;
7819
+ this.#emitter.emit("fault", answered.error);
7820
+ }
7821
+ }
7027
7822
  conversation.add({
7028
7823
  role: "assistant",
7029
7824
  content: result.content,
@@ -7047,7 +7842,7 @@ var Agent = class {
7047
7842
  entry.usage,
7048
7843
  ...turns.map((turn) => turn.spent)
7049
7844
  ].reduce((total, one) => one === void 0 ? total : sumUsage(total, one), void 0),
7050
- partial: final?.partial ?? cancelled,
7845
+ partial: final?.partial ?? abort.signal.aborted,
7051
7846
  exhausted: final?.outcome === "tools" && !abort.signal.aborted,
7052
7847
  stop: inferStop(abort.signal.reason, allowance, budget, this.#provider.id, final)
7053
7848
  };
@@ -7059,8 +7854,7 @@ var Agent = class {
7059
7854
  const thinking = surfaced !== void 0 && surfaced.length > 0 ? surfaced : void 0;
7060
7855
  const content = streamed.join("");
7061
7856
  const estimate = estimateTokens(content);
7062
- const reported = provided.success ? provided.value.usage : partial?.usage;
7063
- const usage = reported === void 0 ? void 0 : sanitizeUsage(reported);
7857
+ const usage = provided.success ? provided.value.usage : partial?.usage === void 0 ? void 0 : sanitizeUsage(partial.usage);
7064
7858
  const charged = allowance === budget ? meter?.charged ?? 0 : 0;
7065
7859
  const billed = usage ?? (allowance === budget ? void 0 : {
7066
7860
  prompt: 0,
@@ -7259,6 +8053,11 @@ var Agent = class {
7259
8053
  return (0, _orkestrel_contract.failure)(denial);
7260
8054
  }
7261
8055
  const authority = this.#authority;
8056
+ if (authority === void 0) {
8057
+ const denial = denyCall(call, void 0);
8058
+ this.#emitter.emit("deny", call, "no authority is set", void 0);
8059
+ return (0, _orkestrel_contract.failure)(denial);
8060
+ }
7262
8061
  const evaluated = (0, _orkestrel_contract.attempt)(() => {
7263
8062
  const decision = authority.evaluate({ call });
7264
8063
  if (!(0, _orkestrel_contract.isObject)(decision)) return void 0;
@@ -7290,7 +8089,7 @@ var Agent = class {
7290
8089
  this.#emitter.emit("ask", call, decision);
7291
8090
  if (signal.aborted) return;
7292
8091
  const answer = new Promise((resolve) => {
7293
- const ask = authority.ask;
8092
+ const ask = authority?.ask;
7294
8093
  resolve(ask === void 0 ? {
7295
8094
  approved: false,
7296
8095
  reason: void 0,
@@ -7324,21 +8123,33 @@ var Agent = class {
7324
8123
  }
7325
8124
  }
7326
8125
  }
7327
- #confirm(tools, authorization) {
8126
+ #changed(tools, call, tool, advertised) {
8127
+ const same = (0, _orkestrel_contract.attempt)(() => {
8128
+ const current = tools.tool(call.name);
8129
+ const definition = advertised.find((entry) => entry.name === call.name);
8130
+ return current === tool && current?.annotations?.consequential === true === (definition?.annotations?.consequential === true);
8131
+ });
8132
+ return !same.success || !same.value;
8133
+ }
8134
+ #confirm(tools, authorization, advertised) {
7328
8135
  for (const [index, { call, tool }] of authorization.allowed) {
7329
- if (tools.tool(call.name) === tool) continue;
8136
+ if (!this.#changed(tools, call, tool, advertised)) continue;
7330
8137
  authorization.allowed.delete(index);
7331
8138
  authorization.denials.set(index, denyCall(call, AUTHORITY_NOTES.denied));
7332
8139
  this.#emitter.emit("deny", call, `${call.name} changed after its decision`, void 0);
7333
- this.#confirm(tools, authorization);
8140
+ this.#confirm(tools, authorization, advertised);
7334
8141
  return;
7335
8142
  }
7336
8143
  }
7337
- async #dispatch(tools, calls, signal, authorization) {
8144
+ async #dispatch(tools, calls, signal, authorization, advertised) {
7338
8145
  const { allowed, denials } = authorization;
7339
8146
  const entries = [...allowed].sort(([left], [right]) => left - right);
7340
- const batch = entries.map(([, { call }]) => call);
7341
- const executed = batch.length > 0 ? await tools.execute(batch, { signal }) : [];
8147
+ const started = entries.map(([, { call, tool }]) => {
8148
+ if (!this.#changed(tools, call, tool, advertised)) return tools.execute(call, { signal });
8149
+ this.#emitter.emit("deny", call, `${call.name} changed after its decision`, void 0);
8150
+ return denyCall(call, AUTHORITY_NOTES.denied);
8151
+ });
8152
+ const executed = started.length > 0 ? await Promise.all(started) : [];
7342
8153
  const results = new Map([...denials, ...entries.flatMap(([index], offset) => {
7343
8154
  const result = executed[offset];
7344
8155
  return result === void 0 ? [] : [[index, result]];
@@ -7352,9 +8163,13 @@ var Agent = class {
7352
8163
  const next = await generator.next();
7353
8164
  if (next.done === true) {
7354
8165
  const result = next.value;
7355
- return (0, _orkestrel_contract.attempt)(() => result);
8166
+ return (0, _orkestrel_contract.attempt)(() => requireProviderResult(result));
8167
+ }
8168
+ const delta = readProviderDelta(next.value);
8169
+ if (delta === void 0) {
8170
+ generator.return({ content: "" }).catch(() => {});
8171
+ throw new ProviderError("PROTOCOL", `provider error: a delta must carry a channel of 'content' or 'thinking' and string text; received ${(0, _orkestrel_contract.preview)(next.value)}`);
7356
8172
  }
7357
- const delta = next.value;
7358
8173
  if (delta.channel === "content") {
7359
8174
  streamed.push(delta.text);
7360
8175
  meter?.charge(delta.text);
@@ -7444,6 +8259,67 @@ var AgentFanout = class {
7444
8259
  return settleAgentJob(this.#registry.build(controller.input, controller.signal), this.#partial);
7445
8260
  }
7446
8261
  };
8262
+ // src/core/agents/Authority.ts
8263
+ /**
8264
+ * Gates the agent loop's tool calls — the synchronous policy consulted before each call runs,
8265
+ * turning one {@link AuthorityContext} into an {@link AuthorityDecision} by walking the ordered
8266
+ * rules first-match-wins and falling back to the required `fallback`, and the approver that answers
8267
+ * an `ask` verdict.
8268
+ *
8269
+ * @remarks
8270
+ * - **Refuses at construction.** The constructor throws an `AgentError` coded `'AUTHORITY'` when the
8271
+ * `fallback` is missing or malformed, or when a rule has no valid `verdict`, so a policy with a
8272
+ * hole in it never runs.
8273
+ * - **Ordered, first-match-wins.** `evaluate` walks the configured rules in order and
8274
+ * returns the first whose `match(context)` is true as `{ zone, verdict, reason }`.
8275
+ * - **Fallback.** When no rule matches, `evaluate` returns the configured `fallback`: a
8276
+ * `verdict: 'deny'` fallback makes an allowlist (only matched rules that allow get through), and a
8277
+ * `verdict: 'allow'` fallback makes a denylist.
8278
+ * - **Consulted before each tool call.** The agent loop calls `evaluate({ call })` for
8279
+ * every {@link import('@orkestrel/tool').ToolCall} the model emits; a denied call is fed
8280
+ * back to the model as a denial {@link import('@orkestrel/tool').ToolResult} (a `tool`
8281
+ * chunk + a tool message) instead of being executed — no tool execution —
8282
+ * so the model receives the denial and can react.
8283
+ * - **Approver.** `ask` answers an `ask` verdict through the `ask` option's
8284
+ * {@link AuthorityHandler}, as a promise for a synchronous or an async handler; a handler
8285
+ * throw rejects it. With no handler it rejects with an `AgentError` coded `'APPROVER'`, which
8286
+ * the loop reads as an unanswered ask. It never throws synchronously.
8287
+ * - **Synchronous.** `evaluate` returns the verdict directly; only `ask` is async.
8288
+ * - **Event-free.** A purely functional gate — no Emitter, no events.
8289
+ *
8290
+ * @example
8291
+ * ```ts
8292
+ * // A denylist: deny the `delete` tool, allow everything else.
8293
+ * const authority = new Authority({
8294
+ * rules: [{ match: (c) => c.call.name === 'delete', zone: 'restricted', verdict: 'deny' }],
8295
+ * fallback: { zone: 'default', verdict: 'allow' },
8296
+ * })
8297
+ * authority.evaluate({ call: { id: '1', name: 'delete', arguments: {} } }) // { zone: 'restricted', verdict: 'deny' }
8298
+ * authority.evaluate({ call: { id: '2', name: 'add', arguments: {} } }) // { zone: 'default', verdict: 'allow' }
8299
+ * ```
8300
+ */
8301
+ var Authority = class {
8302
+ #rules;
8303
+ #fallback;
8304
+ #handler;
8305
+ constructor(options) {
8306
+ this.#fallback = requireFallback(options);
8307
+ this.#rules = requireRules(options.rules);
8308
+ this.#handler = options.ask;
8309
+ }
8310
+ evaluate(context) {
8311
+ for (const rule of this.#rules) if (rule.match(context)) return {
8312
+ zone: rule.zone,
8313
+ verdict: rule.verdict,
8314
+ ...rule.reason === void 0 ? {} : { reason: rule.reason }
8315
+ };
8316
+ return this.#fallback;
8317
+ }
8318
+ async ask(context, decision, signal) {
8319
+ if (this.#handler === void 0) throw new AgentError("APPROVER", "an asked call needs an approver and none is set", { context: { tool: context.call.name } });
8320
+ return this.#handler(context, decision, signal);
8321
+ }
8322
+ };
7447
8323
  // src/core/agents/Approver.ts
7448
8324
  /**
7449
8325
  * Holds a server's waiting asks between the run that asked them and the page that answers them — an
@@ -7465,7 +8341,11 @@ var AgentFanout = class {
7465
8341
  * @example
7466
8342
  * ```ts
7467
8343
  * const approver = new Approver()
7468
- * const authority = createAuthority({ rules, ask: approver.ask })
8344
+ * const authority = createAuthority({
8345
+ * rules,
8346
+ * fallback: { zone: 'default', verdict: 'deny' },
8347
+ * ask: approver.ask,
8348
+ * })
7469
8349
  * approver.answer({ id, approved: true }) // true while that ask waits
7470
8350
  * ```
7471
8351
  */
@@ -7547,13 +8427,14 @@ var Approver = class {
7547
8427
  * rehydrated job is a programmer / config error that must fail loudly at build time, never
7548
8428
  * silently resolve to `undefined` and run an agent missing a dependency.
7549
8429
  * - **`build` rehydrates.** Resolve the job's `provider`; assemble a fresh
7550
- * {@link ToolManager} from the `tools` names; rebuild the token `budget` from its
8430
+ * {@link ToolManager} from the `tools` names, which refuses with `code: 'REGISTRY'` when the job
8431
+ * names no `authority`; rebuild the token `budget` from its
7551
8432
  * ceiling (`createTokenBudget({ max })`); resolve the optional `authority` /
7552
8433
  * `scheduler` names; construct the {@link Agent} with `system` / `limit` / `timeout` /
7553
8434
  * the threaded `signal`; seed its context with the job's `messages`; return it. The
7554
8435
  * `signal` is the queue attempt's / runner unit's cancel, so a bounded abort propagates
7555
8436
  * into the agent (which commits a partial — the job's `partial` policy then
7556
- * decides success vs. retry). A job `limit` that is not a nonnegative safe integer throws an
8437
+ * decides success vs. retry). A job `limit` that is not a positive safe integer throws an
7557
8438
  * {@link AgentError} carrying `code: 'LIMIT'` from the agent's construction.
7558
8439
  * - **Event-free.** A pure resolver — no Emitter, no events.
7559
8440
  *
@@ -7591,14 +8472,18 @@ var AgentRegistry = class {
7591
8472
  return requireEntry(this.#schedulers, "scheduler", name);
7592
8473
  }
7593
8474
  build(input, signal) {
8475
+ const faults = agentJobContract.audit(input);
8476
+ if (faults.length > 0) throw new AgentError("REGISTRY", `a job must hold only the declared members of AgentJobInput\n${(0, _orkestrel_contract.describeFaults)(faults, agentJobContract.schema)}`, { context: { faults } });
7594
8477
  const agent = new Agent(this.provider(input.provider), this.#buildOptions(input, signal));
7595
- for (const message of input.messages) agent.context.messages.add(message);
8478
+ for (const message of input.messages ?? []) agent.context.messages.add(message);
7596
8479
  return agent;
7597
8480
  }
7598
8481
  #buildOptions(input, signal) {
8482
+ const names = input.tools ?? [];
8483
+ if (names.length > 0 && input.authority === void 0) throw new AgentError("REGISTRY", "a job that names tools and no authority cannot run them; add authority: 'NAME' to the job and register that NAME under authorities", { context: { tools: names } });
7599
8484
  const budget = input.budget === void 0 ? void 0 : (0, _orkestrel_budget.createTokenBudget)({ max: input.budget });
7600
8485
  return {
7601
- tools: this.#buildToolManager(input.tools),
8486
+ ...names.length === 0 ? {} : { tools: this.#buildToolManager(names) },
7602
8487
  ...input.system === void 0 ? {} : { system: input.system },
7603
8488
  ...input.limit === void 0 ? {} : { limit: input.limit },
7604
8489
  ...input.timeout === void 0 ? {} : { timeout: input.timeout },
@@ -7611,7 +8496,7 @@ var AgentRegistry = class {
7611
8496
  }
7612
8497
  #buildToolManager(names) {
7613
8498
  const manager = new _orkestrel_tool.ToolManager();
7614
- if (names !== void 0) for (const name of names) manager.add(this.tool(name));
8499
+ for (const name of names) manager.add(this.tool(name));
7615
8500
  return manager;
7616
8501
  }
7617
8502
  };
@@ -7693,20 +8578,20 @@ function createChannel() {
7693
8578
  /**
7694
8579
  * Creates a policy gate — an {@link AuthorityInterface} the agent loop consults before
7695
8580
  * each tool call runs, evaluating the ordered rules first-match-wins and falling back
7696
- * to the configured default when none match.
8581
+ * to the required `fallback` when none match.
7697
8582
  *
7698
8583
  * @remarks
7699
- * `rules` are evaluated in order — the first whose `match` is true decides (a matched
7700
- * rule with no `verdict` allows). When no rule matches, the `fallback` decides. Default:
7701
- * `{ zone: DEFAULT_AUTHORITY_ZONE, verdict: 'allow' }` (allow-unmatched — a rules list of
7702
- * denials acts as a denylist). Pass a `verdict: 'deny'` `fallback` to flip the gate to
7703
- * deny-by-default (an allowlist). `ask` answers each `ask` verdict; with none, every ask is
7704
- * refused as unanswered. Wire the result into `createAgent` through `AgentOptions.authority`:
7705
- * a denied call is fed back to the model as a denial `ToolResult` (not executed), so the model
7706
- * can react. Synchronous — `evaluate` returns the verdict directly; only `ask` is async.
8584
+ * `rules` are evaluated in order — the first whose `match` is true decides, and each rule names its
8585
+ * `verdict`. When no rule matches, the `fallback` decides: a `verdict: 'deny'` fallback makes an
8586
+ * allowlist, and a `verdict: 'allow'` fallback makes a denylist. `ask` answers each `ask` verdict;
8587
+ * with none, every ask is refused as unanswered. Wire the result into `createAgent` through
8588
+ * `AgentOptions.authority`: a denied call is fed back to the model as a denial `ToolResult` (not
8589
+ * executed), so the model can react. Synchronous — `evaluate` returns the verdict directly; only
8590
+ * `ask` is async.
7707
8591
  *
7708
- * @param options - Optional `rules` (ordered), `fallback`, and `ask` (see {@link AuthorityOptions})
8592
+ * @param options - The `fallback`, and optional `rules` (ordered) and `ask` (see {@link AuthorityOptions})
7709
8593
  * @returns A working {@link AuthorityInterface}
8594
+ * @throws {AgentError} Thrown when the `fallback` is missing or malformed, or a rule has no valid `verdict` (code `'AUTHORITY'`)
7710
8595
  *
7711
8596
  * @example
7712
8597
  * ```ts
@@ -7715,6 +8600,7 @@ function createChannel() {
7715
8600
  * // Deny the `delete` tool, allow everything else.
7716
8601
  * const authority = createAuthority({
7717
8602
  * rules: [{ match: (c) => c.call.name === 'delete', zone: 'restricted', verdict: 'deny' }],
8603
+ * fallback: { zone: 'default', verdict: 'allow' },
7718
8604
  * })
7719
8605
  * const agent = createAgent(provider, { tools, authority })
7720
8606
  * ```
@@ -7736,7 +8622,11 @@ function createAuthority(options) {
7736
8622
  * @example
7737
8623
  * ```ts
7738
8624
  * const approver = createApprover()
7739
- * const authority = createAuthority({ rules, ask: approver.ask })
8625
+ * const authority = createAuthority({
8626
+ * rules,
8627
+ * fallback: { zone: 'default', verdict: 'deny' },
8628
+ * ask: approver.ask,
8629
+ * })
7740
8630
  * const agent = createAgent(provider, { tools, authority })
7741
8631
  * const stream = agent.stream({ prompt: { content: 'Tidy the notes.' } })
7742
8632
  * for await (const frame of agentStreamToFrames(stream, approver)) {
@@ -7765,16 +8655,29 @@ function createApprover() {
7765
8655
  *
7766
8656
  * @example
7767
8657
  * ```ts
7768
- * import { createAgentRegistry } from '@orkestrel/agent'
8658
+ * import { createAgentRegistry, createAuthority } from '@orkestrel/agent'
7769
8659
  * import type { ProviderInterface } from '@orkestrel/agent'
8660
+ * import { numberShape, objectShape } from '@orkestrel/contract'
7770
8661
  * import { createTool } from '@orkestrel/tool'
7771
8662
  *
7772
8663
  * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
7773
8664
  * const registry = createAgentRegistry({
7774
8665
  * providers: { main: provider },
7775
- * tools: { add: createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }) },
8666
+ * tools: {
8667
+ * add: createTool({
8668
+ * name: 'add',
8669
+ * contract: objectShape({ x: numberShape(), y: numberShape() }),
8670
+ * execute: (a) => Number(a.x) + Number(a.y),
8671
+ * }),
8672
+ * },
8673
+ * authorities: { open: createAuthority({ fallback: { zone: 'default', verdict: 'allow' } }) },
8674
+ * })
8675
+ * const agent = registry.build({
8676
+ * provider: 'main',
8677
+ * messages: [{ role: 'user', content: 'Hi.' }],
8678
+ * tools: ['add'],
8679
+ * authority: 'open',
7776
8680
  * })
7777
- * const agent = registry.build({ provider: 'main', messages: [{ role: 'user', content: 'Hi.' }] })
7778
8681
  * ```
7779
8682
  */
7780
8683
  function createAgentRegistry(options) {
@@ -7903,6 +8806,11 @@ var PLACED_CATEGORIES = Object.freeze(["rule", "correction"]);
7903
8806
  var LEDGER_RULES_KEY = "rules";
7904
8807
  /** Prefixes the key of an owner's record, which the owner's id follows. */
7905
8808
  var LEDGER_OWNER_PREFIX = "owner:";
8809
+ /** Labels recalled conversation text by its origin. */
8810
+ var LEDGER_RECALL_LABELS = Object.freeze({
8811
+ request: "Earlier request:",
8812
+ reply: "Earlier reply:"
8813
+ });
7906
8814
  /**
7907
8815
  * Supplies the wording of every question the ledger asks its judge.
7908
8816
  *
@@ -7999,10 +8907,10 @@ var DETERMINISTIC_JUDGE_ERROR = /invalid or duplicate top logprob token/;
7999
8907
  * carrying the machine-readable `code`.
8000
8908
  *
8001
8909
  * @remarks
8002
- * A threshold, share, capacity, limit, topic, lookup, or gauge outside its bounds is a programmer
8003
- * error and throws this with the matching {@link LedgerErrorCode}. A calibration call that reports
8910
+ * A threshold, share, capacity, limit, topic, lookup, question, or gauge outside its bounds is a
8911
+ * programmer error and throws this with the matching {@link LedgerErrorCode}. A calibration call that reports
8004
8912
  * no prompt usage throws this with `'GAUGE'`. A snapshot option that `isLedgerSnapshot` refuses
8005
- * throws this with `'SNAPSHOT'`. Narrow a caught value with {@link isLedgerError} and
8913
+ * throws this with `'SNAPSHOT'`, its `context` holding the `faults`. Narrow a caught value with {@link isLedgerError} and
8006
8914
  * branch on `error.code`. A selection without an active `respond` call, for a conversation other
8007
8915
  * than the ledger's, or for a request other than that call's request or a ledger note, reports
8008
8916
  * `'REQUEST'` through the selection's fault.
@@ -8010,40 +8918,147 @@ var DETERMINISTIC_JUDGE_ERROR = /invalid or duplicate top logprob token/;
8010
8918
  var LedgerError = class extends Error {
8011
8919
  /** Names the machine-readable condition; the {@link LedgerErrorCode} union describes each code. */
8012
8920
  code;
8921
+ /** Holds the values that locate the failure, such as the `faults` of a refused snapshot, or `undefined` when none were supplied. */
8922
+ context;
8013
8923
  /**
8014
8924
  * Records the failure condition and its description.
8015
8925
  *
8016
8926
  * @param code - The machine-readable condition, exposed as `error.code`
8017
8927
  * @param message - The human-readable description of the failure
8928
+ * @param options - The optional {@link CodedErrorOptions}; `cause` reaches `Error.cause` only when present
8018
8929
  */
8019
- constructor(code, message) {
8020
- super(message);
8930
+ constructor(code, message, options) {
8931
+ super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
8021
8932
  this.name = "LedgerError";
8022
8933
  _orkestrel_contract.INTRINSICS.define(this, Symbol.for("@orkestrel/agent.LedgerError"), { value: this });
8023
8934
  this.code = code;
8935
+ this.context = options?.context;
8024
8936
  }
8025
8937
  };
8026
8938
  /**
8027
- * Narrows an unknown caught value to a {@link LedgerError} through its identity brand and declared members, so a `catch` can
8028
- * branch on its `code`.
8029
- *
8030
- * @param value - The value to test (typically a `catch` binding)
8031
- * @returns True if `value` is a {@link LedgerError}; false otherwise
8032
- *
8939
+ * Narrows an unknown caught value to a {@link LedgerError} through its identity brand and declared members, so a `catch` can
8940
+ * branch on its `code`.
8941
+ *
8942
+ * @param value - The value to test (typically a `catch` binding)
8943
+ * @returns True if `value` is a {@link LedgerError}; false otherwise
8944
+ *
8945
+ * @example
8946
+ * ```ts
8947
+ * isLedgerError(new LedgerError('GAUGE', 'calibration reported no prompt usage')) // true
8948
+ * ```
8949
+ */
8950
+ function isLedgerError(value) {
8951
+ return (0, _orkestrel_contract.holds)(() => {
8952
+ if (!(value instanceof Error && _orkestrel_contract.INTRINSICS.prototype(value) !== Error.prototype && _orkestrel_contract.INTRINSICS.describe(value, Symbol.for("@orkestrel/agent.LedgerError"))?.value === value)) return false;
8953
+ const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
8954
+ if (!(code === "REQUEST" || code === "THRESHOLD" || code === "SHARE" || code === "CAPACITY" || code === "LIMIT" || code === "TOPIC" || code === "LOOKUP" || code === "GAUGE" || code === "REFERENCE" || code === "SNAPSHOT" || code === "QUESTION")) return false;
8955
+ const context = _orkestrel_contract.INTRINSICS.reflect.read(value, "context");
8956
+ return context === void 0 || (0, _orkestrel_contract.isObject)(context) || (0, _orkestrel_contract.isFunction)(context);
8957
+ });
8958
+ }
8959
+ // src/core/ledgers/helpers.ts
8960
+ /**
8961
+ * Digests held messages, lookup call context, stored copies, and sentence receipts using canonical text.
8962
+ * This non-cryptographic checksum detects host edits and makes no security claim.
8963
+ * @param exchanges - The stored exchange entries
8964
+ * @param messages - The conversation holding their messages
8965
+ * @returns The hexadecimal checksum, or undefined when a required message is absent or canonical encoding fails
8966
+ * @example
8967
+ * ```ts
8968
+ * digestLedgerWindow([], []) // a deterministic hexadecimal checksum
8969
+ * ```
8970
+ */
8971
+ function digestLedgerWindow(exchanges, messages) {
8972
+ const byId = new Map(messages.map((message) => [message.id, message]));
8973
+ const held = [...new Set(exchanges.flatMap((entry) => [
8974
+ entry.request,
8975
+ entry.reply,
8976
+ ...entry.lines.map((line) => line.source)
8977
+ ]))].map((id) => byId.get(id));
8978
+ const callers = messages.filter((message) => message.role === "tool" || message.calls !== void 0);
8979
+ const encoded = (0, _orkestrel_contract.attempt)(() => {
8980
+ const values = [...held, ...callers].map((message) => (0, _orkestrel_contract.canonicalStringify)(message));
8981
+ return values.some((value) => value === void 0) ? void 0 : (0, _orkestrel_contract.canonicalStringify)([values, exchanges.map((entry) => [entry.copy ?? null, entry.lines])]);
8982
+ });
8983
+ if (!encoded.success || encoded.value === void 0) return void 0;
8984
+ const hash = [...encoded.value].reduce((prior, character) => {
8985
+ const code = character.codePointAt(0) ?? 0;
8986
+ return {
8987
+ first: Math.imul(prior.first ^ code, 16777619) >>> 0,
8988
+ second: Math.imul(prior.second ^ code, 16777619) >>> 0
8989
+ };
8990
+ }, {
8991
+ first: 2166136261,
8992
+ second: 3335557771
8993
+ });
8994
+ return hash.first.toString(16).padStart(8, "0") + hash.second.toString(16).padStart(8, "0");
8995
+ }
8996
+ /**
8997
+ * Collects stored messages eligible to open the recall offer.
8998
+ * @remarks
8999
+ * Notes and findings without a result do not open the offer. User statements, including quiet
9000
+ * statements available through recall's word route, can open it; this helper does not filter results.
9001
+ * @param input - The messages, findings, and classification
9002
+ * @param requests - The marked request ids
9003
+ * @param notes - The annotation ids to exclude
9004
+ * @returns User statements, lookup findings with results, marked requests, and nonblank call-free replies
8033
9005
  * @example
8034
9006
  * ```ts
8035
- * isLedgerError(new LedgerError('GAUGE', 'calibration reported no prompt usage')) // true
9007
+ * collectRecallMessages(input, new Set(), new Set()) // eligible stored messages
8036
9008
  * ```
8037
9009
  */
8038
- function isLedgerError(value) {
8039
- return (0, _orkestrel_contract.holds)(() => {
8040
- if (!(value instanceof Error && _orkestrel_contract.INTRINSICS.prototype(value) !== Error.prototype && _orkestrel_contract.INTRINSICS.describe(value, Symbol.for("@orkestrel/agent.LedgerError"))?.value === value)) return false;
8041
- const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
8042
- if (!(code === "REQUEST" || code === "THRESHOLD" || code === "SHARE" || code === "CAPACITY" || code === "LIMIT" || code === "TOPIC" || code === "LOOKUP" || code === "GAUGE" || code === "REFERENCE" || code === "SNAPSHOT")) return false;
8043
- return true;
9010
+ function collectRecallMessages(input, requests, notes) {
9011
+ const findings = new Set(input.findings.filter((finding) => finding.result !== void 0).map((finding) => finding.id));
9012
+ return input.messages.filter((message) => !notes.has(message.id) && (requests.has(message.id) || message.role === "assistant" && !hasCalls(message) && message.content.trim() !== "" || message.role === "user" && !hasCalls(message) || message.role === "tool" && findings.has(message.id)));
9013
+ }
9014
+ /**
9015
+ * Checks whether an eligible earlier message is absent from the prompt or incompletely covered.
9016
+ * A successful finding is held when a finding with the same canonical identity and text is fully received.
9017
+ * @param input - The messages, findings, and classification
9018
+ * @param request - The current request id; only messages before it count
9019
+ * @param requests - The marked request ids
9020
+ * @param notes - The annotation ids to exclude
9021
+ * @param messages - The seed tail and held exchange messages; lookup stubs retain their rendered text
9022
+ * @param lines - The receipts held by the window and the request copy together
9023
+ * @returns True if an eligible earlier message is absent or only partly covered; false otherwise
9024
+ * @example
9025
+ * ```ts
9026
+ * hasHiddenRecall(input, request.id, requests, notes, tail, lines) // whether to offer recall
9027
+ * ```
9028
+ */
9029
+ function hasHiddenRecall(input, request, requests, notes, messages, lines) {
9030
+ const start = input.messages.findIndex((message) => message.id === request);
9031
+ const before = new Set(input.messages.slice(0, Math.max(0, start)).map((message) => message.id));
9032
+ const present = new Map(messages.map((message) => [message.id, message]));
9033
+ const receipts = Map.groupBy(lines, (line) => line.source);
9034
+ return collectRecallMessages(input, requests, notes).some((message) => {
9035
+ if (!before.has(message.id)) return false;
9036
+ if (requests.has(message.id) || message.role === "assistant") return !present.has(message.id);
9037
+ if (present.get(message.id)?.content === message.content) return false;
9038
+ const finding = input.findings.find((one) => one.id === message.id);
9039
+ return !(finding === void 0 ? [message.id] : input.findings.filter((one) => before.has(one.id) && matchesLookup(finding, one)).map((one) => one.id)).some((id) => {
9040
+ if (present.get(id)?.content === message.content) return true;
9041
+ const held = new Set((receipts.get(id) ?? []).map((line) => line.sentence));
9042
+ return splitSentences(message.content).every((_, at) => held.has(at));
9043
+ });
8044
9044
  });
8045
9045
  }
8046
- // src/core/ledgers/helpers.ts
9046
+ /**
9047
+ * Checks whether two successful findings carry the same text for the same canonical lookup.
9048
+ * @param left - The finding to compare
9049
+ * @param right - The candidate copy
9050
+ * @returns True for the same finding or equal canonical identities and text; unkeyable findings match only themselves
9051
+ * @example
9052
+ * ```ts
9053
+ * matchesLookup(finding, finding) // true when it has a result
9054
+ * ```
9055
+ */
9056
+ function matchesLookup(left, right) {
9057
+ if (left.result === void 0 || right.result === void 0 || left.text !== right.text) return false;
9058
+ if (left.id === right.id) return true;
9059
+ const identity = identifyLookup(left.name, left.arguments);
9060
+ return identity !== void 0 && identity === identifyLookup(right.name, right.arguments);
9061
+ }
8047
9062
  /**
8048
9063
  * Collects the live message ids placed in records or left without a record.
8049
9064
  * @param projection - The projected records and unplaced ids
@@ -8600,17 +9615,17 @@ function matchEntities(index, text, partial) {
8600
9615
  * Projects the owner records and the rules record from a conversation's messages.
8601
9616
  *
8602
9617
  * @remarks
8603
- * Each line is a sentence of a live message, verbatim except that a sentence that opens with a pronoun opens with its party and a colon. A message is live when it is a user message,
8604
- * or a tool message whose finding is the last finding of its call, and the input neither excludes it nor
8605
- * files it quiet or replaced. A finding with an undefined `result` still replaces the earlier
8606
- * finding of the same call, so the earlier result leaves every record.
9618
+ * Each line is a sentence of a live message, verbatim except that a sentence that opens with a
9619
+ * pronoun opens with its party and a colon. {@link collectLive} retains user messages and the
9620
+ * earliest finding in each call identity's last run of identical text, subject to exclusions,
9621
+ * quiet classification, and supersessions. An undefined result still replaces the earlier finding.
8607
9622
  *
8608
9623
  * A live message joins the owner records its entities name, directly or through a linked lookup
8609
9624
  * argument. A message that names no owner joins where its earlier side of an amendment pair joins,
8610
9625
  * or on the rules record when it is filed as a rule or a correction, and is an orphan otherwise.
8611
9626
  *
8612
- * Every live sentence is retained. Accepted direct edges are listed in `amendments`. An amending message
8613
- * that is itself superseded retains its accepted edge without restoring its text.
9627
+ * Every live sentence is retained. Accepted edges are listed in `amendments`. An amender that is
9628
+ * superseded points to its newest live successor, or retains its endpoint when none exists.
8614
9629
  *
8615
9630
  * A sentence that opens with a pronoun takes the party named in the sentence before it. The reading
8616
9631
  * trusts capitals, so a capitalized word that is no person, such as a carrier named mid-sentence,
@@ -8820,10 +9835,12 @@ function splitTopic(topic) {
8820
9835
  * @remarks
8821
9836
  * The cut keeps at least one entry. When it leaves entries out, its last line reads
8822
9837
  * `N older items not shown; name a narrower topic to narrow the recall`, which
8823
- * {@link matchesCutLine} recognizes.
9838
+ * {@link matchesCutLine} recognizes. Pass `more` when the entries aren't in age order, so the line
9839
+ * doesn't claim an omitted entry is older than a kept one.
8824
9840
  *
8825
9841
  * @param entries - The entries in the order they are kept
8826
9842
  * @param room - The estimate units the joined entries can take
9843
+ * @param kind - What the cut line calls an omitted entry. Default: `older`
8827
9844
  * @returns The kept entries followed by the cut line when any were left out, joined by newlines
8828
9845
  *
8829
9846
  * @example
@@ -8831,7 +9848,7 @@ function splitTopic(topic) {
8831
9848
  * cutListing(['first entry', 'second entry'], 1) // 'first entry\n1 older item not shown; name a narrower topic to narrow the recall'
8832
9849
  * ```
8833
9850
  */
8834
- function cutListing(entries, room) {
9851
+ function cutListing(entries, room, kind = "older") {
8835
9852
  const stop = [...entries.keys()].findIndex((at) => at > 0 && estimateMessages([{
8836
9853
  id: "recall",
8837
9854
  role: "tool",
@@ -8839,7 +9856,7 @@ function cutListing(entries, room) {
8839
9856
  }]) > room);
8840
9857
  const kept = entries.slice(0, stop === -1 ? void 0 : stop);
8841
9858
  const left = entries.length - kept.length;
8842
- return [...kept, ...left > 0 ? [`${left} older item${left === 1 ? "" : "s"} not shown; name a narrower topic to narrow the recall`] : []].join("\n");
9859
+ return [...kept, ...left > 0 ? [`${left} ${kind} item${left === 1 ? "" : "s"} not shown; name a narrower topic to narrow the recall`] : []].join("\n");
8843
9860
  }
8844
9861
  /**
8845
9862
  * Checks whether a line is the cut line {@link cutListing} writes.
@@ -8853,7 +9870,7 @@ function cutListing(entries, room) {
8853
9870
  * ```
8854
9871
  */
8855
9872
  function matchesCutLine(line) {
8856
- return /^\d+ older items? not shown; /.test(line);
9873
+ return /^\d+ (?:older|more) items? not shown; /.test(line);
8857
9874
  }
8858
9875
  /**
8859
9876
  * Renders the tail stub of a lookup result from an earlier request.
@@ -8914,11 +9931,12 @@ function fitSlope(groups) {
8914
9931
  * Collects the ids of the live messages in conversation order.
8915
9932
  *
8916
9933
  * @remarks
8917
- * A message is live when it is a user message, or a tool message whose finding is the last finding of its
8918
- * call, and the input neither excludes it nor files it quiet or replaced. A finding replaces any
8919
- * earlier finding of the same call, an empty one included, so an empty lookup leaves the earlier
8920
- * result out of every record. A finding with no canonical identity stays live as its own source
8921
- * and participates in no replacement.
9934
+ * A message is live when it is a user message or the first unsuperseded finding in its canonical
9935
+ * call identity's last run of identical text, and the input neither excludes it nor files it quiet
9936
+ * or superseded. When the whole run is superseded, its first finding reaches that final filter.
9937
+ * Different text or an undefined result replaces the earlier run, so an empty lookup leaves the
9938
+ * earlier result out of every record. A thrown lookup leaves the earlier result live. A finding
9939
+ * with no canonical identity participates in no replacement.
8922
9940
  *
8923
9941
  * @param input - The messages, findings, and classification to read
8924
9942
  * @returns The live message ids
@@ -8934,7 +9952,14 @@ function collectLive(input) {
8934
9952
  const position = new Map(input.messages.map((message, at) => [message.id, at]));
8935
9953
  const findings = input.findings.filter((finding) => position.has(finding.id));
8936
9954
  const identities = new Map(findings.map((finding) => [finding.id, identifyLookup(finding.name, finding.arguments)]));
8937
- const current = new Set(findings.filter((finding) => finding.result !== void 0 && !findings.some((later) => identities.get(finding.id) !== void 0 && (position.get(later.id) ?? -1) > (position.get(finding.id) ?? -1) && identities.get(later.id) === identities.get(finding.id))).map((finding) => finding.id));
9955
+ const current = new Set([...Map.groupBy(findings, (finding) => identities.get(finding.id) ?? finding).values()].flatMap((group) => {
9956
+ const ordered = group.toSorted((left, right) => (position.get(left.id) ?? -1) - (position.get(right.id) ?? -1));
9957
+ const last = ordered.at(-1);
9958
+ if (last?.result === void 0) return [];
9959
+ const boundary = ordered.findLastIndex((finding) => finding.result === void 0 || finding.text !== last.text);
9960
+ const first = ordered.slice(boundary + 1).find((finding) => (classification.supersessions.get(finding.id) ?? []).length === 0) ?? ordered[boundary + 1];
9961
+ return first === void 0 ? [] : [first.id];
9962
+ }));
8938
9963
  return [...new Map(input.messages.map((message) => [message.id, message])).values()].filter((message) => message.role === "user" || message.role === "tool" && current.has(message.id)).map((message) => message.id).filter((id) => !excluded.has(id) && !classification.quiet.has(id) && (classification.supersessions.get(id) ?? []).length === 0);
8939
9964
  }
8940
9965
  /**
@@ -8989,17 +10014,18 @@ function placeMember(input, links, amended, id, seen, settled) {
8989
10014
  return keys;
8990
10015
  }
8991
10016
  /**
8992
- * Collects accepted direct amendments of live messages.
10017
+ * Collects accepted amendments of live messages, following superseded amenders to live successors.
8993
10018
  *
8994
10019
  * @remarks
8995
10020
  * An accepted edge relates an earlier live message to an amender without claiming which sentences
8996
- * changed. An amender takes effect while live and retains its edge after another message supersedes
8997
- * it, without restoring the superseded text. An excluded or quiet user or tool message has no effect.
10021
+ * changed. A superseded amender points to its newest reachable live successor in conversation order,
10022
+ * with cycles guarded and duplicate endpoints removed. With no live successor, its endpoint stays.
10023
+ * This never restores superseded text. An excluded or quiet user or tool message has no effect.
8998
10024
  *
8999
10025
  * @param input - The exclusions and classification to read
9000
10026
  * @param byId - The messages by id
9001
10027
  * @param live - The live message ids from {@link collectLive}
9002
- * @returns The distinct accepted direct edges in conversation order
10028
+ * @returns The distinct accepted edges in conversation order
9003
10029
  *
9004
10030
  * @example
9005
10031
  * ```ts
@@ -9013,7 +10039,10 @@ function collectAmendments(input, byId, live) {
9013
10039
  const ordered = [...new Set(input.messages.map((message) => message.id))];
9014
10040
  const positions = new Map(ordered.map((id, index) => [id, index]));
9015
10041
  const effective = /* @__PURE__ */ new Set([...live, ...[...byId.values()].filter((message) => message.role !== "assistant" && !excluded.has(message.id) && !classification.quiet.has(message.id) && (classification.supersessions.get(message.id) ?? []).length > 0).map((message) => message.id)]);
9016
- return ordered.filter((id) => living.has(id)).flatMap((earlier) => [...new Set(classification.amendments.get(earlier) ?? [])].filter((later) => effective.has(later) && positions.has(later)).sort((a, b) => (positions.get(a) ?? 0) - (positions.get(b) ?? 0)).map((later) => ({
10042
+ return ordered.filter((id) => living.has(id)).flatMap((earlier) => [...new Set((classification.amendments.get(earlier) ?? []).filter((later) => effective.has(later) && positions.has(later)).map((later) => {
10043
+ const successors = new Set(scanAmendments(classification.supersessions, later, () => true));
10044
+ return ordered.findLast((id) => living.has(id) && successors.has(id)) ?? later;
10045
+ }))].sort((a, b) => (positions.get(a) ?? 0) - (positions.get(b) ?? 0)).map((later) => ({
9017
10046
  earlier,
9018
10047
  later
9019
10048
  })));
@@ -9059,6 +10088,83 @@ function buildLines(input, byId, id, holders, system) {
9059
10088
  }];
9060
10089
  });
9061
10090
  }
10091
+ /**
10092
+ * Encodes a sentence's record placement without comparing rendered text.
10093
+ * @param line - The record key, title, source, and zero-based sentence index
10094
+ * @returns The serialized placement identity
10095
+ * @example
10096
+ * ```ts
10097
+ * buildLedgerLineKey({ key: 'rules', title: 'Rules', source: 'one', sentence: 0 })
10098
+ * ```
10099
+ */
10100
+ function buildLedgerLineKey(line) {
10101
+ return JSON.stringify([
10102
+ line.key,
10103
+ line.title,
10104
+ line.source,
10105
+ line.sentence
10106
+ ]);
10107
+ }
10108
+ /**
10109
+ * Collects the record's sentence receipts not already held by a window.
10110
+ * @param record - The selected record with its original sentence identities
10111
+ * @param held - Placement identities already rendered in the window
10112
+ * @returns The remaining sentences with their record placement
10113
+ * @example
10114
+ * ```ts
10115
+ * collectLedgerLines({ key: 'rules', title: 'Rules', members: [], lines: [] }, new Set()) // []
10116
+ * ```
10117
+ */
10118
+ function collectLedgerLines(record, held) {
10119
+ return record.lines.map((line) => ({
10120
+ ...line,
10121
+ key: record.key,
10122
+ title: record.title
10123
+ })).filter((line) => !held.has(buildLedgerLineKey(line)));
10124
+ }
10125
+ /**
10126
+ * Selects each marked request's unambiguous final call-free reply.
10127
+ * @param messages - Stored conversation messages in order
10128
+ * @param requests - Marked request ids delimiting reply intervals
10129
+ * @param notes - Ledger notes that separate answer passes
10130
+ * @returns Eligible final replies keyed by request id
10131
+ * @example
10132
+ * ```ts
10133
+ * collectLedgerReplies([], new Set(), new Set()).size // 0
10134
+ * ```
10135
+ */
10136
+ function collectLedgerReplies(messages, requests, notes) {
10137
+ const positions = messages.flatMap((message, at) => requests.has(message.id) ? [at] : []);
10138
+ return new Map(positions.flatMap((position, index) => {
10139
+ const request = messages[position];
10140
+ const turns = messages.slice(position + 1, positions[index + 1]);
10141
+ const reply = turns.findLast((message) => message.role === "assistant" && !hasCalls(message));
10142
+ const answers = turns.flatMap((message, at) => message.role === "assistant" && !hasCalls(message) && message.content.trim() !== "" ? [at] : []);
10143
+ const certain = answers.every((at, offset) => offset === 0 || turns.slice((answers[offset - 1] ?? -1) + 1, at).some((message) => notes.has(message.id)));
10144
+ return request !== void 0 && certain && reply !== void 0 && reply.content.trim() !== "" ? [[request.id, reply]] : [];
10145
+ }));
10146
+ }
10147
+ /**
10148
+ * Captures source bytes and direct amendment edges for a held window.
10149
+ * @param messages - Current stored messages
10150
+ * @param sources - Requests, replies, and record sources the window holds
10151
+ * @param amendments - Accepted direct amendment edges from the current projection
10152
+ * @returns Source and edge identities for stale detection
10153
+ * @example
10154
+ * ```ts
10155
+ * collectLedgerEvidence([], new Set(), []).messages.size // 0
10156
+ * ```
10157
+ */
10158
+ function collectLedgerEvidence(messages, sources, amendments) {
10159
+ return {
10160
+ messages: new Set(messages.filter((message) => sources.has(message.id)).map((message) => JSON.stringify(["message", message]))),
10161
+ amendments: new Map(amendments.filter((edge) => sources.has(edge.earlier) || sources.has(edge.later)).map((edge) => [JSON.stringify([
10162
+ "edge",
10163
+ edge.earlier,
10164
+ edge.later
10165
+ ]), { ...edge }]))
10166
+ };
10167
+ }
9062
10168
  // src/core/ledgers/Classifier.ts
9063
10169
  /**
9064
10170
  * Files messages through the conversation's judgment manager and reads their categories and corrections.
@@ -9085,23 +10191,25 @@ var Classifier = class {
9085
10191
  * Asks the judge every question the filing still lacks, in the measured order.
9086
10192
  * @param requests - The request ids, which skip category questions and receive only enabled request-topic questions
9087
10193
  * @param signal - The caller's signal; an abort returns a fault with completed judgments and usage
9088
- * @returns The judgment keys, summed usage, and one attempt per logical question, in asking order; a throw from the assign or entities handler, a scheduler rejection, or a caller abort returns the partial result with `fault`; a judge rejection under a live signal leaves that question undecided and sets no `fault`
10194
+ * @returns The judgment keys, summed usage, and one attempt per logical question, in asking order; a throw from the assign or entities handler, a scheduler rejection, or a caller abort returns the partial result with `fault`; a judge rejection under a live signal leaves that question undecided and sets `failure`
9089
10195
  */
9090
10196
  async classify(requests, signal) {
9091
10197
  const results = [];
9092
10198
  const fault = await this.#gather(requests, signal, results).then(() => void 0, (error) => (0, _orkestrel_contract.isError)(error) ? error : new Error((0, _orkestrel_contract.errorToMessage)(error)));
9093
10199
  const usage = results.reduce((total, result) => result.usage === void 0 ? total : sumUsage(total, result.usage), void 0);
10200
+ const failure = results.find((result) => result.failure !== void 0)?.failure;
9094
10201
  return {
9095
10202
  judgments: results.flatMap((result) => result.judgments),
9096
10203
  attempts: results.flatMap((result) => result.attempts),
9097
10204
  ...fault === void 0 ? {} : { fault },
10205
+ ...failure === void 0 ? {} : { failure },
9098
10206
  ...usage === void 0 ? {} : { usage }
9099
10207
  };
9100
10208
  }
9101
10209
  /**
9102
10210
  * Reads the category a message is filed under.
9103
10211
  * @param id - The message id
9104
- * @returns The assigned category, the recorded category at the cutoff, or `undefined`
10212
+ * @returns The assigned category, the recorded category at the cutoff, or `undefined`; a quiet winner suppressed by the user literal floor remains undecided
9105
10213
  */
9106
10214
  category(id) {
9107
10215
  const message = this.#options.conversation.message(id);
@@ -9110,25 +10218,29 @@ var Classifier = class {
9110
10218
  if (assigned !== void 0) return assigned;
9111
10219
  const answer = this.#readCategories(id);
9112
10220
  const cutoff = this.#options.thresholds.category;
9113
- return LEDGER_CATEGORIES.flatMap((category) => {
10221
+ const winner = LEDGER_CATEGORIES.flatMap((category) => {
9114
10222
  const probability = answer?.probabilities[category];
9115
10223
  return probability !== void 0 && probability >= cutoff ? [{
9116
10224
  category,
9117
10225
  probability
9118
10226
  }] : [];
9119
10227
  }).reduce((best, candidate) => best === void 0 || candidate.probability > best.probability ? candidate : best, void 0)?.category;
10228
+ return winner !== void 0 && QUIET_CATEGORIES.includes(winner) && !this.quiet(id) ? void 0 : winner;
9120
10229
  }
9121
10230
  /**
9122
10231
  * Reports whether a message files under a quiet category.
9123
10232
  * @param id - The message id
9124
- * @returns True if the assigned or recorded category is quiet; false otherwise.
10233
+ * @returns True if the assigned category is quiet or the recorded quiet weight reaches the cutoff, except for a user message carrying an id or number; false otherwise
9125
10234
  */
9126
10235
  quiet(id) {
9127
10236
  const message = this.#options.conversation.message(id);
9128
10237
  if (message === void 0) return false;
9129
10238
  const assigned = this.#options.assign(message);
9130
10239
  if (assigned !== void 0) return QUIET_CATEGORIES.includes(assigned);
9131
- return this.#weigh(id, QUIET_CATEGORIES) >= this.#options.thresholds.category;
10240
+ if (this.#weigh(id, QUIET_CATEGORIES) < this.#options.thresholds.category) return false;
10241
+ if (message.role !== "user") return true;
10242
+ const literals = extractLiterals(message.content);
10243
+ return literals.ids.size + literals.numbers.size === 0;
9132
10244
  }
9133
10245
  /**
9134
10246
  * Reports whether a message files under a decisive category.
@@ -9167,7 +10279,7 @@ var Classifier = class {
9167
10279
  if (head !== "amends" && head !== "supersedes" || !(0, _orkestrel_contract.isString)(earlier) || !(0, _orkestrel_contract.isString)(later)) return [];
9168
10280
  const before = this.#options.conversation.message(earlier);
9169
10281
  const after = this.#options.conversation.message(later);
9170
- if (before === void 0 || after === void 0 || (this.#readNoul(this.#buildPair(head, earlier, later)) ?? 0) < this.#options.thresholds[head]) return [];
10282
+ if (before === void 0 || after === void 0 || (this.#readNoul(this.#buildPair(head, earlier, later)) ?? 0) < this.#options.thresholds[head] || this.quiet(later)) return [];
9171
10283
  return head === "amends" && !this.#sharesLiteral(extractLiterals(before.content), extractLiterals(after.content)) ? [] : [{
9172
10284
  head,
9173
10285
  earlier,
@@ -9402,86 +10514,26 @@ var Classifier = class {
9402
10514
  judgments: [],
9403
10515
  attempts
9404
10516
  });
9405
- if (isJudgeError(error) && error.code === "QUESTION" || DETERMINISTIC_JUDGE_ERROR.test(renderCauseChain(error))) this.#failed.add(fingerprint);
9406
10517
  if (signal.aborted) throw error;
10518
+ const failure = (0, _orkestrel_contract.isError)(error) ? error : new Error((0, _orkestrel_contract.errorToMessage)(error));
10519
+ if (this.#options.strict === false && (isJudgeError(error) && error.code === "QUESTION" || DETERMINISTIC_JUDGE_ERROR.test(renderCauseChain(error)))) this.#failed.add(fingerprint);
9407
10520
  return {
9408
10521
  judgments: [],
9409
- attempts: []
10522
+ attempts: [],
10523
+ failure
9410
10524
  };
9411
10525
  }
9412
10526
  }
9413
10527
  };
9414
- // src/core/ledgers/validators.ts
9415
- /**
9416
- * Checks whether a value is finite and lies in the interval (0, 1].
9417
- * @param value - The value to inspect
9418
- * @returns True if the value is a valid ledger fraction; false otherwise
9419
- * @example
9420
- * ```ts
9421
- * isFraction(0.8) // true
9422
- * ```
9423
- */
9424
- function isFraction(value) {
9425
- return (0, _orkestrel_contract.isFiniteNumber)(value) && value > 0 && value <= 1;
9426
- }
9427
- /**
9428
- * Checks whether a value is a positive safe integer.
9429
- * @param value - The value to inspect
9430
- * @returns True if the value is a positive safe integer; false otherwise
9431
- * @example
9432
- * ```ts
9433
- * isPositiveSafeInteger(4096) // true
9434
- * ```
9435
- */
9436
- function isPositiveSafeInteger(value) {
9437
- return (0, _orkestrel_contract.isFiniteNumber)(value) && Number.isSafeInteger(value) && value > 0;
9438
- }
9439
- /**
9440
- * Checks whether a value is a ledger snapshot whose marked ids name messages of its own conversation.
9441
- *
9442
- * @remarks
9443
- * `conversation` must pass `isConversationSnapshot`, and `requests`, `notes`, and `failures` must be
9444
- * dense arrays of strings. Each request and note id must name a `user` message of
9445
- * `conversation.messages` and each failure id a `tool` message, so an id only a section original
9446
- * holds is refused. A `gauge`, when present, must hold a finite `scale` greater than 0 and a finite
9447
- * `overhead` of at least 0. Unknown members are admitted. Unreadable members and untrusted inputs
9448
- * return false. The guard is structural: it admits a conversation holding a stored `system` role
9449
- * or a repeated id, which `requireConversationSnapshot` and the `Conversation` constructor then
9450
- * refuse as the stored-data check a ledger's hydrate runs.
9451
- *
9452
- * @param value - The value to inspect, such as a parsed checkpoint
9453
- * @returns True if the value has the shape of a ledger snapshot whose marked ids name messages of its own conversation; false otherwise
9454
- * @example
9455
- * ```ts
9456
- * const conversation = { id: 'c1', sections: [], messages: [{ id: 'm1', role: 'user', content: 'Check FX-111.' }] }
9457
- * isLedgerSnapshot({ conversation, requests: ['m1'], notes: [], failures: [] }) // true
9458
- * isLedgerSnapshot({ conversation, requests: [], notes: [], failures: ['m1'] }) // false
9459
- * ```
9460
- */
9461
- function isLedgerSnapshot(value) {
9462
- return (0, _orkestrel_contract.holds)(() => {
9463
- if (!(0, _orkestrel_contract.isRecord)(value)) return false;
9464
- const { conversation, requests, notes, failures, gauge } = value;
9465
- if (!isConversationSnapshot(conversation)) return false;
9466
- const isIds = (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString);
9467
- if (!isIds(requests) || !isIds(notes) || !isIds(failures)) return false;
9468
- if (gauge !== void 0) {
9469
- if (!(0, _orkestrel_contract.isRecord)(gauge)) return false;
9470
- const { scale, overhead } = gauge;
9471
- if (!(0, _orkestrel_contract.isFiniteNumber)(scale) || scale <= 0 || !(0, _orkestrel_contract.isFiniteNumber)(overhead) || overhead < 0) return false;
9472
- }
9473
- const roles = new Map(conversation.messages.map((message) => [message.id, message.role]));
9474
- return requests.every((id) => roles.get(id) === "user") && notes.every((id) => roles.get(id) === "user") && failures.every((id) => roles.get(id) === "tool");
9475
- });
9476
- }
9477
10528
  // src/core/ledgers/Gauge.ts
9478
10529
  /**
9479
10530
  * Prices prompts in tokens from a measured scale and overhead cost, and measures the room a request
9480
10531
  * has left.
9481
10532
  *
9482
10533
  * @remarks
9483
- * `observe` rescales from the first call of each finished request with the overhead cost taken out,
9484
- * and keeps that request's calls for the marginal rate and the longest final completion for the reply
10534
+ * `observe` rescales from the first call only when `spared` is absent, zero, or at least `overhead`,
10535
+ * with `max(0, overhead - spared)` taken out. A partially spared call keeps the existing scale.
10536
+ * It keeps that request's calls for the marginal rate and the longest final completion for the reply
9485
10537
  * reserve after subtracting its thinking. `overhead` never changes after construction.
9486
10538
  * {@link GaugeOptions} defines the measured use, reply reserve, and recall-room formulas.
9487
10539
  * {@link LedgerOptions} defines the ledger's plan budget and recall close rule.
@@ -9539,7 +10591,7 @@ var Gauge = class {
9539
10591
  }
9540
10592
  remainder(calls) {
9541
10593
  const call = calls.at(-1);
9542
- const used = (0, _orkestrel_contract.isFiniteNumber)(call?.prompt) ? call.prompt + (call.completion ?? 0) - (this.#replay === "none" ? call.thinking ?? 0 : 0) : this.#overhead + this.#scale * (call?.estimate ?? 0);
10594
+ const used = (0, _orkestrel_contract.isFiniteNumber)(call?.prompt) ? call.prompt + (call.completion ?? 0) - (this.#replay === "none" ? call.thinking ?? 0 : 0) : Math.max(0, this.#overhead - (call?.spared ?? 0)) + this.#scale * (call?.estimate ?? 0);
9543
10595
  return Math.max(0, this.#capacity - used);
9544
10596
  }
9545
10597
  reserve(calls, longest) {
@@ -9559,8 +10611,8 @@ var Gauge = class {
9559
10611
  }
9560
10612
  observe(calls, reply) {
9561
10613
  const first = calls[0];
9562
- if (first !== void 0 && (0, _orkestrel_contract.isFiniteNumber)(first.prompt) && first.estimate > 0) {
9563
- const priced = first.prompt - this.#overhead;
10614
+ if (first !== void 0 && (0, _orkestrel_contract.isFiniteNumber)(first.prompt) && first.estimate > 0 && ((first.spared ?? 0) === 0 || (first.spared ?? 0) >= this.#overhead)) {
10615
+ const priced = first.prompt - Math.max(0, this.#overhead - (first.spared ?? 0));
9564
10616
  if (priced > 0) this.#scale = priced / first.estimate;
9565
10617
  }
9566
10618
  this.#history = [...this.#history, [...calls]];
@@ -9734,8 +10786,12 @@ var LedgerPass = class {
9734
10786
  var LedgerRequest = class {
9735
10787
  #content;
9736
10788
  #limit;
10789
+ #faults = /* @__PURE__ */ new Set();
9737
10790
  #message;
9738
10791
  #plan;
10792
+ #scope;
10793
+ #draft;
10794
+ #sent;
9739
10795
  #passes = [];
9740
10796
  #attempts = [];
9741
10797
  #admitted = /* @__PURE__ */ new Set();
@@ -9770,6 +10826,16 @@ var LedgerRequest = class {
9770
10826
  get plan() {
9771
10827
  return this.#plan;
9772
10828
  }
10829
+ get scope() {
10830
+ return this.#scope;
10831
+ }
10832
+ apply(scope) {
10833
+ this.#scope = scope;
10834
+ }
10835
+ /** Returns the copy and receipts adopted by the last provider turn. */
10836
+ get sent() {
10837
+ return this.#sent;
10838
+ }
9773
10839
  /**
9774
10840
  * Returns the selection the current pass's run adopted last.
9775
10841
  *
@@ -9825,6 +10891,8 @@ var LedgerRequest = class {
9825
10891
  this.#passes.at(-1)?.adopt(selection, boundary);
9826
10892
  }
9827
10893
  observe(call, position) {
10894
+ const draft = this.#draft;
10895
+ this.#sent = draft !== void 0 && this.selection?.messages.some((message) => message.id === draft.copy.id && message.content === draft.copy.content) ? draft : void 0;
9828
10896
  this.#passes.at(-1)?.observe(call, position);
9829
10897
  }
9830
10898
  spend(usage) {
@@ -9839,8 +10907,15 @@ var LedgerRequest = class {
9839
10907
  if (message?.thinking === void 0) return;
9840
10908
  pass.think(computeThinking(message, completion));
9841
10909
  }
9842
- file(attempts, plan) {
10910
+ fault(error) {
10911
+ this.#faults.add(error);
10912
+ }
10913
+ faulted(error) {
10914
+ return this.#faults.has(error);
10915
+ }
10916
+ file(attempts, plan, draft) {
9843
10917
  this.#attempts = [...this.#attempts, ...attempts];
10918
+ this.#draft = draft;
9844
10919
  if (plan !== void 0) this.#plan = plan;
9845
10920
  }
9846
10921
  admit(name, args) {
@@ -9971,6 +11046,156 @@ var LedgerScan = class {
9971
11046
  return this.#positions.get(id);
9972
11047
  }
9973
11048
  };
11049
+ // src/core/ledgers/shapers.ts
11050
+ /** Describes the minimal serialized evidence of a nonempty exchange window. */
11051
+ var ledgerWindowShape = (0, _orkestrel_contract.objectShape)({
11052
+ digest: (0, _orkestrel_contract.stringShape)(),
11053
+ exchanges: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
11054
+ request: (0, _orkestrel_contract.stringShape)(),
11055
+ copy: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
11056
+ reply: (0, _orkestrel_contract.stringShape)(),
11057
+ lines: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
11058
+ key: (0, _orkestrel_contract.stringShape)(),
11059
+ title: (0, _orkestrel_contract.stringShape)(),
11060
+ source: (0, _orkestrel_contract.stringShape)(),
11061
+ sentence: (0, _orkestrel_contract.numberShape)({
11062
+ integer: true,
11063
+ min: 0,
11064
+ max: Number.MAX_SAFE_INTEGER
11065
+ })
11066
+ }))
11067
+ }), { min: 1 }),
11068
+ amendments: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
11069
+ earlier: (0, _orkestrel_contract.stringShape)(),
11070
+ later: (0, _orkestrel_contract.stringShape)()
11071
+ }))
11072
+ });
11073
+ /**
11074
+ * Describes a ledger snapshot exactly: an unknown key refuses, so a key a later version adds never reads
11075
+ * as absent.
11076
+ *
11077
+ * @remarks
11078
+ * `isLedgerSnapshot` is the door a hydrate reads, and this shape fills a refusal's faults. What the shape
11079
+ * cannot state, that each marked id names a message of the snapshot's own conversation, is the guard's
11080
+ * alone.
11081
+ */
11082
+ var ledgerSnapshotShape = (0, _orkestrel_contract.objectShape)({
11083
+ conversation: conversationSnapshotShape,
11084
+ requests: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)()),
11085
+ notes: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)()),
11086
+ failures: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)()),
11087
+ gauge: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.objectShape)({
11088
+ scale: (0, _orkestrel_contract.numberShape)(),
11089
+ overhead: (0, _orkestrel_contract.numberShape)()
11090
+ })),
11091
+ window: (0, _orkestrel_contract.optionalShape)(ledgerWindowShape)
11092
+ });
11093
+ // src/core/ledgers/contracts.ts
11094
+ /** Reads structural faults of a serialized exchange window. */
11095
+ var ledgerWindowContract = (0, _orkestrel_contract.createContract)(ledgerWindowShape);
11096
+ /** Reads structural faults of a ledger snapshot, including its optional held exchange window. */
11097
+ var ledgerSnapshotContract = (0, _orkestrel_contract.createContract)(ledgerSnapshotShape);
11098
+ // src/core/ledgers/validators.ts
11099
+ /**
11100
+ * Checks whether a value has the exact shape of a nonempty exchange window.
11101
+ * @param value - The untrusted window
11102
+ * @returns True if the window and its members are well shaped; false otherwise
11103
+ * @example
11104
+ * ```ts
11105
+ * isLedgerWindowSnapshot({ digest: 'checksum', exchanges: [{ request: 'request', reply: 'reply', lines: [] }], amendments: [] }) // true
11106
+ * ```
11107
+ */
11108
+ function isLedgerWindowSnapshot(value) {
11109
+ return (0, _orkestrel_contract.holds)(() => {
11110
+ if (!(0, _orkestrel_contract.isRecord)(value) || Object.keys(value).length !== 3 || !(0, _orkestrel_contract.isString)(value.digest)) return false;
11111
+ const { exchanges, amendments } = value;
11112
+ if (!(0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isRecord)(exchanges) || exchanges.length === 0 || !(0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isRecord)(amendments)) return false;
11113
+ return exchanges.every((entry) => {
11114
+ const { request, reply, copy, lines } = entry;
11115
+ return Object.keys(entry).length === 3 + Number(copy !== void 0) && (0, _orkestrel_contract.isString)(request) && (0, _orkestrel_contract.isString)(reply) && (copy === void 0 || (0, _orkestrel_contract.isString)(copy)) && (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isRecord)(lines) && lines.every((line) => Object.keys(line).length === 4 && (0, _orkestrel_contract.isString)(line.key) && (0, _orkestrel_contract.isString)(line.title) && (0, _orkestrel_contract.isString)(line.source) && isNonNegativeSafeInteger(line.sentence));
11116
+ }) && amendments.every((edge) => Object.keys(edge).length === 2 && (0, _orkestrel_contract.isString)(edge.earlier) && (0, _orkestrel_contract.isString)(edge.later));
11117
+ });
11118
+ }
11119
+ /**
11120
+ * Checks whether a value is finite and lies in the interval (0, 1].
11121
+ * @param value - The value to inspect
11122
+ * @returns True if the value is a valid ledger fraction; false otherwise
11123
+ * @example
11124
+ * ```ts
11125
+ * isFraction(0.8) // true
11126
+ * ```
11127
+ */
11128
+ function isFraction(value) {
11129
+ return (0, _orkestrel_contract.isFiniteNumber)(value) && value > 0 && value <= 1;
11130
+ }
11131
+ /**
11132
+ * Checks whether a value holds the wording a ledger files with: a choice `category` question whose
11133
+ * criteria describe every category, a non-empty `topic` string, and noul `amends` and `supersedes`
11134
+ * questions.
11135
+ *
11136
+ * @remarks
11137
+ * A ledger asks these on every message, so a hole in them would otherwise surface as filing that quietly
11138
+ * stays undecided. Total: an unreadable member returns false.
11139
+ *
11140
+ * @param value - The value to inspect
11141
+ * @returns True if the value holds the wording the ledger's questions read; false otherwise
11142
+ * @example
11143
+ * ```ts
11144
+ * isLedgerQuestion(LEDGER_QUESTIONS) // true
11145
+ * isLedgerQuestion({ topic: 'Does it concern this?' }) // false
11146
+ * ```
11147
+ */
11148
+ function isLedgerQuestion(value) {
11149
+ return (0, _orkestrel_contract.holds)(() => {
11150
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
11151
+ const { category, topic, amends, supersedes } = value;
11152
+ if (!isJudgeQuestion(category) || category.form !== "choice") return false;
11153
+ const { criteria } = category;
11154
+ return LEDGER_CATEGORIES.every((one) => isJudgeEntry(criteria[one])) && (0, _orkestrel_contract.isString)(topic) && topic.trim() !== "" && isJudgeQuestion(amends) && amends.form === "noul" && isJudgeQuestion(supersedes) && supersedes.form === "noul";
11155
+ });
11156
+ }
11157
+ /**
11158
+ * Checks whether a value is a ledger snapshot whose marked ids name messages of its own conversation.
11159
+ *
11160
+ * @remarks
11161
+ * `conversation` must pass `isConversationSnapshot`, and `requests`, `notes`, and `failures` must be
11162
+ * dense arrays of strings. Each request and note id must name a `user` message of
11163
+ * `conversation.messages` and each failure id a `tool` message, so an id only a section original
11164
+ * holds is refused. A `gauge`, when present, must hold a finite `scale` greater than 0 and a finite
11165
+ * `overhead` of at least 0. The guard is exact: an unknown member and an own `undefined` `gauge` return
11166
+ * false, at the snapshot and in its `gauge`. Unreadable members and untrusted inputs return false. The
11167
+ * guard is structural: it admits a conversation holding a stored `system` role
11168
+ * or a repeated id, which `requireConversationSnapshot` and the `Conversation` constructor then
11169
+ * refuse as the stored-data check a ledger's hydrate runs.
11170
+ * A present `window` must be nonempty and exactly shaped. This guard does not audit its references
11171
+ * or sentence ranges. Hydration discards the window when its digest disagrees or cannot be computed.
11172
+ *
11173
+ * @param value - The value to inspect, such as a parsed checkpoint
11174
+ * @returns True if the value has the shape of a ledger snapshot whose marked ids name messages of its own conversation; false otherwise
11175
+ * @example
11176
+ * ```ts
11177
+ * const conversation = { id: 'c1', sections: [], messages: [{ id: 'm1', role: 'user', content: 'Check FX-111.' }] }
11178
+ * isLedgerSnapshot({ conversation, requests: ['m1'], notes: [], failures: [] }) // true
11179
+ * isLedgerSnapshot({ conversation, requests: [], notes: [], failures: ['m1'] }) // false
11180
+ * ```
11181
+ */
11182
+ function isLedgerSnapshot(value) {
11183
+ return (0, _orkestrel_contract.holds)(() => {
11184
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
11185
+ const { conversation, requests, notes, failures, gauge, window } = value;
11186
+ if (Object.keys(value).length !== 4 + Number(gauge !== void 0) + Number(window !== void 0)) return false;
11187
+ if (!isConversationSnapshot(conversation)) return false;
11188
+ const isIds = (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString);
11189
+ if (!isIds(requests) || !isIds(notes) || !isIds(failures)) return false;
11190
+ if (gauge !== void 0) {
11191
+ if (!(0, _orkestrel_contract.isRecord)(gauge) || Object.keys(gauge).length !== 2) return false;
11192
+ const { scale, overhead } = gauge;
11193
+ if (!(0, _orkestrel_contract.isFiniteNumber)(scale) || scale <= 0 || !(0, _orkestrel_contract.isFiniteNumber)(overhead) || overhead < 0) return false;
11194
+ }
11195
+ const roles = new Map(conversation.messages.map((message) => [message.id, message.role]));
11196
+ return requests.every((id) => roles.get(id) === "user") && notes.every((id) => roles.get(id) === "user") && failures.every((id) => roles.get(id) === "tool") && (window === void 0 || isLedgerWindowSnapshot(window));
11197
+ });
11198
+ }
9974
11199
  // src/core/ledgers/Ledger.ts
9975
11200
  /**
9976
11201
  * Serves one conversation through classified records, a bounded copy of each request turn that carries them, and a final answer pass.
@@ -9998,13 +11223,16 @@ var Ledger = class {
9998
11223
  #scans = /* @__PURE__ */ new Set();
9999
11224
  #calibrations = /* @__PURE__ */ new Set();
10000
11225
  #gauge;
11226
+ #window = [];
11227
+ #evidence = collectLedgerEvidence([], /* @__PURE__ */ new Set(), []);
10001
11228
  /**
10002
11229
  * Composes the conversation, classifier, tools, and agent.
10003
11230
  * @param provider - The provider that serves the conversation
10004
11231
  * @param options - The filing policy, capacity, and request bounds
10005
11232
  * @throws {LedgerError} Thrown when a threshold, share, capacity, limit, topic, lookup, or gauge is invalid
10006
11233
  * @throws {LedgerError} Thrown when `predict` is not a nonnegative safe integer less than `capacity` (code `'CAPACITY'`)
10007
- * @throws {LedgerError} Thrown when `snapshot` is malformed or marks an id its conversation does not hold (code `'SNAPSHOT'`)
11234
+ * @throws {LedgerError} Thrown when `questions` fails `isLedgerQuestion` (code `'QUESTION'`)
11235
+ * @throws {LedgerError} Thrown when `snapshot` is malformed or marks an id its conversation does not hold (code `'SNAPSHOT'`, its faults under `context.faults`)
10008
11236
  * @throws {ConversationError} Thrown when `isLedgerSnapshot` admits `snapshot` but its conversation holds a `system` message (code `'ROLE'`) or repeats an id (code `'SNAPSHOT'`)
10009
11237
  */
10010
11238
  constructor(provider, options) {
@@ -10021,13 +11249,19 @@ var Ledger = class {
10021
11249
  for (const value of Object.values(options.share ?? {})) if (!isFraction(value)) throw new LedgerError("SHARE", "shares must be finite and in (0, 1]");
10022
11250
  if (!isPositiveSafeInteger(options.capacity)) throw new LedgerError("CAPACITY", "capacity must be a positive safe integer");
10023
11251
  this.#predict = resolvePredict(options.predict, options.capacity);
10024
- for (const value of [options.recall?.limit, options.agent?.limit]) if (value !== void 0 && !isNonNegativeSafeInteger(value)) throw new LedgerError("LIMIT", "limits must be nonnegative safe integers");
11252
+ if (options.recall?.limit !== void 0 && !isNonNegativeSafeInteger(options.recall.limit)) throw new LedgerError("LIMIT", "a recall limit must be a nonnegative safe integer");
11253
+ if (options.agent?.limit !== void 0 && !isPositiveSafeInteger(options.agent.limit)) throw new LedgerError("LIMIT", "an agent limit must be a positive safe integer");
10025
11254
  const topics = Array.from(options.topics, (topic) => topic.name);
10026
11255
  if (topics.some((name) => name.trim() === "") || new Set(topics).size !== topics.length) throw new LedgerError("TOPIC", "topic names must be nonempty and unique");
10027
11256
  const lookups = ["recall", ...Array.from(options.lookups ?? [], (lookup) => lookup.tool.name)];
10028
11257
  if (new Set(lookups).size !== lookups.length) throw new LedgerError("LOOKUP", "lookup names must be unique and cannot be recall");
11258
+ if (!isLedgerQuestion(options.questions)) throw new LedgerError("QUESTION", "questions must hold a choice category question whose criteria describe every category, a non-empty topic string, and noul amends and supersedes questions; pass the LEDGER_QUESTIONS constant for the measured wording");
10029
11259
  const snapshot = options.snapshot;
10030
- if (snapshot !== void 0 && !isLedgerSnapshot(snapshot)) throw new LedgerError("SNAPSHOT", "snapshot must be well formed and mark only messages of its conversation; pass what snapshot() returned");
11260
+ if (snapshot !== void 0 && !isLedgerSnapshot(snapshot)) {
11261
+ const candidate = options.snapshot;
11262
+ const faults = ledgerSnapshotContract.audit(candidate);
11263
+ throw new LedgerError("SNAPSHOT", `snapshot must be well formed and mark only messages of its conversation; pass what snapshot() returned${faults.length === 0 ? "" : `\n${(0, _orkestrel_contract.describeFaults)(faults, ledgerSnapshotContract.schema)}`}`, { context: { faults } });
11264
+ }
10031
11265
  this.#provider = provider;
10032
11266
  this.#replay = provider.replay ?? "none";
10033
11267
  this.#options = options;
@@ -10058,7 +11292,8 @@ var Ledger = class {
10058
11292
  return matchEntities(scan?.index ?? buildLedgerIndex(collectRegistry(this.#readLookups(this.#conversation.messages()))), text, partial);
10059
11293
  },
10060
11294
  ...options.correction === void 0 ? {} : { correction: options.correction },
10061
- ...options.agent?.scheduler === void 0 ? {} : { scheduler: options.agent.scheduler }
11295
+ ...options.agent?.scheduler === void 0 ? {} : { scheduler: options.agent.scheduler },
11296
+ ...options.agent?.strict === void 0 ? {} : { strict: options.agent.strict }
10062
11297
  });
10063
11298
  const tools = (0, _orkestrel_tool.createToolManager)();
10064
11299
  for (const lookup of options.lookups ?? []) tools.add((0, _orkestrel_tool.createTool)({
@@ -10072,15 +11307,11 @@ var Ledger = class {
10072
11307
  }));
10073
11308
  tools.add((0, _orkestrel_tool.createTool)({
10074
11309
  name: "recall",
10075
- description: `Recall what the full conversation record holds on a topic: an owner name, an id, or one of the topics ${renderTopicNames(options.topics)}. Returns source lines, newest first.`,
10076
- parameters: {
10077
- type: "object",
10078
- properties: { topic: {
10079
- type: "string",
10080
- description: "An owner name, an id, or a topic"
10081
- } },
10082
- required: ["topic"]
10083
- },
11310
+ description: `Find something said earlier that isn't in the messages you can see, by an owner name, an id, or one of the topics ${renderTopicNames(options.topics)}. Returns statements and lookup findings first, then earlier requests and replies labeled by origin; newest first within each group.`,
11311
+ contract: (0, _orkestrel_contract.objectShape)({ topic: (0, _orkestrel_contract.stringShape)({
11312
+ min: 1,
11313
+ description: "An owner name, an id, or a topic"
11314
+ }) }),
10084
11315
  execute: (args) => this.#recall(args)
10085
11316
  }));
10086
11317
  this.#agent = createAgent(provider, {
@@ -10088,8 +11319,19 @@ var Ledger = class {
10088
11319
  conversations,
10089
11320
  system: options.system,
10090
11321
  tools,
11322
+ authority: createAuthority({
11323
+ rules: [{
11324
+ match: (context) => lookups.includes(context.call.name),
11325
+ zone: "ledger",
11326
+ verdict: "allow"
11327
+ }],
11328
+ fallback: {
11329
+ zone: "ledger",
11330
+ verdict: "deny",
11331
+ reason: "the ledger advertises no such tool"
11332
+ }
11333
+ }),
10091
11334
  limit: options.agent?.limit ?? 8,
10092
- strict: false,
10093
11335
  select: (conversation, request, signal) => this.#select(conversation, request, signal)
10094
11336
  });
10095
11337
  this.#agent.emitter.on("tool", (_call, result) => {
@@ -10104,6 +11346,7 @@ var Ledger = class {
10104
11346
  if (isLedgerError(selection.fault) && selection.fault.code === "REQUEST") return;
10105
11347
  this.#current()?.adopt(selection, this.#conversation.messages().length);
10106
11348
  });
11349
+ this.#agent.emitter.on("fault", (error) => this.#current()?.fault(error));
10107
11350
  this.#agent.emitter.on("turn", () => this.#observeTurn());
10108
11351
  this.#agent.emitter.on("usage", (usage) => this.#current()?.spend(usage));
10109
11352
  }
@@ -10148,8 +11391,8 @@ var Ledger = class {
10148
11391
  }
10149
11392
  }
10150
11393
  /**
10151
- * Serializes the conversation, the marked ids, and the gauge into a snapshot a later ledger hydrates from.
10152
- * @returns The snapshot, each id list in conversation order and `gauge` present after the ledger holds one
11394
+ * Serializes the conversation, marked ids, gauge, and completed exchange window for a later ledger.
11395
+ * @returns The snapshot, each id list in conversation order, with a gauge and window only when held
10153
11396
  * @throws {AgentError} Thrown when a request or calibration is active (code `'CONCURRENCY'`)
10154
11397
  */
10155
11398
  snapshot() {
@@ -10158,18 +11401,37 @@ var Ledger = class {
10158
11401
  const conversation = this.#conversation.snapshot();
10159
11402
  const ids = conversation.messages.map((message) => message.id);
10160
11403
  const gauge = this.gauge;
11404
+ const window = {
11405
+ exchanges: this.#window.map((entry) => ({
11406
+ request: entry.request.id,
11407
+ ...entry.request.content === this.#conversation.message(entry.request.id)?.content ? {} : { copy: entry.request.content },
11408
+ reply: entry.reply.id,
11409
+ lines: entry.lines.map(({ key, title, source, sentence }) => ({
11410
+ key,
11411
+ title,
11412
+ source,
11413
+ sentence
11414
+ }))
11415
+ })),
11416
+ amendments: [...this.#evidence.amendments.values()].map((edge) => ({ ...edge }))
11417
+ };
11418
+ const digest = window.exchanges.length === 0 ? void 0 : digestLedgerWindow(window.exchanges, conversation.messages);
10161
11419
  return {
10162
11420
  conversation,
10163
11421
  requests: ids.filter((id) => this.#requests.has(id)),
10164
11422
  notes: ids.filter((id) => this.#annotations.has(id)),
10165
11423
  failures: ids.filter((id) => this.#results.get(id)?.success === false),
10166
- ...gauge === void 0 ? {} : { gauge }
11424
+ ...gauge === void 0 ? {} : { gauge },
11425
+ ...window.exchanges.length === 0 || digest === void 0 ? {} : { window: {
11426
+ ...window,
11427
+ digest
11428
+ } }
10167
11429
  };
10168
11430
  }
10169
11431
  /**
10170
11432
  * Appends a request and serves it, recovering an unfinished or rejected first pass with one answer pass.
10171
11433
  * A calibration call that reports no prompt usage, or a prompt usage of 0 or less, rejects with `LedgerError` code `'GAUGE'`; a provider error during calibration rejects with that error.
10172
- * When the answer pass rejects, `respond` rejects with that error. Every rejection leaves the ledger free for a later call.
11434
+ * When the answer pass rejects, `respond` rejects with that error, and under the default `strict` a judge failure or another selection failure rejects it too. A rejection belongs to its own call: the ledger stays free, and the next call asks the judge again about whatever is still unanswered.
10173
11435
  * @param content - The request text
10174
11436
  * @param signal - The caller's cancellation signal; an abort during calibration rejects with its reason
10175
11437
  * @returns The final pass, the usage of every pass, and the filing attempts of this request alone
@@ -10181,16 +11443,20 @@ var Ledger = class {
10181
11443
  if (this.#busy()) throw new AgentError("CONCURRENCY", "a ledger request is already active");
10182
11444
  const caller = (0, _orkestrel_abort.createAbort)({ ...signal === void 0 ? {} : { signal } }).signal;
10183
11445
  const request = new LedgerRequest(content, this.#options.recall?.limit);
11446
+ const scope = this.#agent.context.scope;
10184
11447
  this.#live.add(request);
10185
11448
  try {
10186
11449
  if (this.#gauge === void 0) await this.#measureGauge(caller);
10187
11450
  const entered = request.enter(this.#conversation);
10188
11451
  this.#requests.add(entered.id);
10189
- const first = await this.#generatePass(request, caller, this.#options.think).catch(() => ({
10190
- content: "",
10191
- partial: true,
10192
- ...request.usage === void 0 ? {} : { usage: request.usage }
10193
- }));
11452
+ const first = await this.#generatePass(request, caller, this.#options.think).catch((error) => {
11453
+ if (request.faulted(error)) throw error;
11454
+ return {
11455
+ content: "",
11456
+ partial: true,
11457
+ ...request.usage === void 0 ? {} : { usage: request.usage }
11458
+ };
11459
+ });
10194
11460
  const passes = [first];
10195
11461
  if (!caller.aborted && (first.partial || first.content.trim() === "")) {
10196
11462
  const digest = this.#buildDigest(request, entered);
@@ -10203,7 +11469,8 @@ var Ledger = class {
10203
11469
  content: LEDGER_NOTES.cue
10204
11470
  }).id);
10205
11471
  const previous = this.#agent.context.scope;
10206
- this.#agent.context.apply(createScope({
11472
+ const answer = createScope({
11473
+ ...previous,
10207
11474
  name: "answer",
10208
11475
  tools: [],
10209
11476
  select: async (conversation, message, runSignal) => {
@@ -10214,11 +11481,12 @@ var Ledger = class {
10214
11481
  messages: selection.messages.filter((one) => one.role !== "tool" && !hasCalls(one))
10215
11482
  };
10216
11483
  }
10217
- }));
11484
+ });
11485
+ this.#agent.context.apply(answer);
10218
11486
  try {
10219
11487
  passes.push(await this.#generatePass(request, caller, false));
10220
11488
  } finally {
10221
- this.#agent.context.apply(previous);
11489
+ if (this.#agent.context.scope === answer) this.#agent.context.apply(previous);
10222
11490
  }
10223
11491
  }
10224
11492
  const last = passes.at(-1) ?? first;
@@ -10237,24 +11505,22 @@ var Ledger = class {
10237
11505
  ...last.stop === void 0 ? {} : { stop: last.stop }
10238
11506
  };
10239
11507
  } finally {
11508
+ if (request.scope !== void 0 && this.#agent.context.scope === request.scope) this.#agent.context.apply(scope);
10240
11509
  this.#flush();
11510
+ this.#commitExchange(request);
10241
11511
  this.#live.delete(request);
10242
11512
  }
10243
11513
  }
10244
11514
  async #measureGauge(signal) {
10245
11515
  try {
10246
- const messages = stripThinking([
10247
- ...this.#agent.context.build({
10248
- messages: [],
10249
- judgments: []
10250
- }),
10251
- ...this.#conversation.view(),
10252
- {
11516
+ const messages = stripThinking(this.#agent.context.build({
11517
+ messages: [...this.#conversation.view(), {
10253
11518
  id: "calibration",
10254
11519
  role: "user",
10255
11520
  content: ""
10256
- }
10257
- ], this.#replay).slice(0, -1);
11521
+ }],
11522
+ judgments: []
11523
+ }), this.#replay).slice(0, -1);
10258
11524
  const priced = await this.#provider.generate(messages, signal, this.#agent.context.tools.definitions(), { think: false });
10259
11525
  const bare = await this.#provider.generate(messages, signal, void 0, { think: false });
10260
11526
  if (!(0, _orkestrel_contract.isFiniteNumber)(priced.usage?.prompt) || priced.usage.prompt <= 0 || !(0, _orkestrel_contract.isFiniteNumber)(bare.usage?.prompt) || bare.usage.prompt <= 0) throw new LedgerError("GAUGE", "calibration requires prompt usage from both calls");
@@ -10304,6 +11570,37 @@ var Ledger = class {
10304
11570
  for (const id of snapshot.requests) this.#requests.add(id);
10305
11571
  for (const id of snapshot.notes) this.#annotations.add(id);
10306
11572
  const messages = this.#conversation.messages();
11573
+ if (snapshot.window !== void 0 && digestLedgerWindow(snapshot.window.exchanges, messages) === snapshot.window.digest) {
11574
+ const window = snapshot.window.exchanges.map((entry) => {
11575
+ const request = this.#conversation.message(entry.request);
11576
+ const reply = this.#conversation.message(entry.reply);
11577
+ if (request === void 0 || reply === void 0) throw new LedgerError("SNAPSHOT", "window pair must belong to its conversation");
11578
+ const lines = entry.lines.map((line) => {
11579
+ const source = this.#conversation.message(line.source);
11580
+ if (source === void 0) throw new LedgerError("SNAPSHOT", "window source must belong to its conversation");
11581
+ return {
11582
+ ...line,
11583
+ text: splitSentences(source.content)[line.sentence] ?? "",
11584
+ topics: [],
11585
+ role: source.role
11586
+ };
11587
+ });
11588
+ return {
11589
+ request: {
11590
+ ...request,
11591
+ content: entry.copy ?? request.content
11592
+ },
11593
+ reply,
11594
+ lines
11595
+ };
11596
+ });
11597
+ this.#window = window;
11598
+ this.#evidence = collectLedgerEvidence(messages, new Set(this.#window.flatMap((entry) => [
11599
+ entry.request.id,
11600
+ entry.reply.id,
11601
+ ...entry.lines.map((line) => line.source)
11602
+ ])), snapshot.window.amendments);
11603
+ }
10307
11604
  const groups = collectToolGroups(messages);
10308
11605
  for (const message of messages) {
10309
11606
  if (!snapshot.failures.includes(message.id)) continue;
@@ -10431,22 +11728,29 @@ var Ledger = class {
10431
11728
  return scan;
10432
11729
  }
10433
11730
  #file(classified, request, selected, message) {
10434
- const { attempts, ...filed } = classified;
11731
+ const { attempts, failure, ...filed } = classified;
10435
11732
  const planned = (0, _orkestrel_contract.attempt)(() => {
10436
11733
  if (filed.fault !== void 0) throw filed.fault;
11734
+ const { draft, ...plan } = this.#plan(selected, this.#rescan());
10437
11735
  return {
10438
- ...this.#plan(selected, this.#rescan()),
10439
- ...filed
11736
+ draft,
11737
+ selection: {
11738
+ ...plan,
11739
+ ...filed,
11740
+ ...failure === void 0 ? {} : { fault: failure }
11741
+ }
10440
11742
  };
10441
11743
  });
10442
- request.file(attempts, planned.success ? planned.value : void 0);
11744
+ request.file(attempts, planned.success ? planned.value.selection : void 0, planned.success ? planned.value.draft : void 0);
10443
11745
  if (!planned.success) return this.#refuse(planned.error, filed, this.#conversation);
11746
+ const selection = planned.value.selection;
10444
11747
  return this.#annotations.has(message.id) ? {
10445
- ...planned.value,
10446
- messages: [...planned.value.messages, ...this.#collectAfter(selected)]
10447
- } : planned.value;
11748
+ ...selection,
11749
+ messages: [...selection.messages, ...this.#collectAfter(selected)]
11750
+ } : selection;
10448
11751
  }
10449
11752
  #refuse(error, filed, conversation) {
11753
+ if (conversation === this.#conversation && !(isLedgerError(error) && error.code === "REQUEST")) this.#current()?.file([]);
10450
11754
  return {
10451
11755
  ...filed,
10452
11756
  messages: conversation.view(),
@@ -10473,7 +11777,11 @@ var Ledger = class {
10473
11777
  topics: [...near]
10474
11778
  });
10475
11779
  const total = Math.max(0, (this.#options.capacity - this.#predict) * this.#share.prompt - (this.#gauge?.overhead ?? 0)) / (1 + LEDGER_SCALE_DRIFT);
10476
- const tail = this.#selectTail(request, total * this.#share.tail, input, scan);
11780
+ const built = this.#agent.context.build({
11781
+ messages: [request],
11782
+ judgments: []
11783
+ });
11784
+ const tail = this.#selectTail(request, total, input, scan);
10477
11785
  const tailIds = new Set(tail.map((message) => message.id));
10478
11786
  const scoped = records.some((record) => record.key !== LEDGER_RULES_KEY);
10479
11787
  const held = /* @__PURE__ */ new Set([...projection.records.filter((record) => scoped || record.key === "rules").flatMap((record) => record.members), ...records.length > 0 ? projection.records.flatMap((record) => record.members.filter((id) => this.#conversation.message(id)?.role === "user" && this.#classifier.category(id) === "rule" && this.#classifier.decisive(id))) : []]);
@@ -10521,25 +11829,121 @@ var Ledger = class {
10521
11829
  source: unit.source,
10522
11830
  record: this.#buildUnitRecord(unit.source, unit.category, input, projection, held)
10523
11831
  }));
10524
- const trimmed = this.#trim(steps, records, mapped, new Set(ordered.map((unit) => unit.source)), request, tail, total, projection.amendments, scan);
10525
- const shown = /* @__PURE__ */ new Set([...trimmed.included, ...records.flatMap((record) => record.members.filter((source) => {
10526
- const full = record.lines.filter((line) => line.source === source);
10527
- const visible = trimmed.records.find((one) => one.key === record.key)?.lines.filter((line) => line.source === source) ?? [];
10528
- return full.length > 0 && full.length === visible.length;
10529
- }))]);
10530
- return { messages: [...tail.slice(0, -1).map((message) => message.role === "tool" ? this.#renderTailStub(message, scan, shown) : message), buildRequestMessage(request, trimmed.rendered)] };
10531
- }
10532
- #trim(steps, records, mapped, included, request, tail, total, amendments, scan) {
10533
- const built = this.#agent.context.build({
10534
- messages: [request],
10535
- judgments: []
11832
+ const trimmed = this.#trim(steps, records, mapped, new Set(ordered.map((unit) => unit.source)), built, tail, total, projection.amendments, scan);
11833
+ const replies = collectLedgerReplies(scan.messages, this.#requests, this.#annotations);
11834
+ const sources = new Set(this.#window.flatMap((entry) => [
11835
+ entry.request.id,
11836
+ entry.reply.id,
11837
+ ...entry.lines.map((line) => line.source)
11838
+ ]));
11839
+ const evidence = collectLedgerEvidence(scan.messages, sources, projection.amendments);
11840
+ const prefix = this.#window.flatMap((entry) => entry.lines);
11841
+ const prior = buildLedgerRenderContext(scan.messages, prefix, projection.amendments);
11842
+ const combined = buildLedgerRenderContext(scan.messages, [...prefix, ...trimmed.rendered.lines], projection.amendments);
11843
+ const stale = evidence.messages.size !== this.#evidence.messages.size || [...evidence.messages].some((receipt) => !this.#evidence.messages.has(receipt)) || evidence.amendments.size !== this.#evidence.amendments.size || [...evidence.amendments.keys()].some((receipt) => !this.#evidence.amendments.has(receipt)) || this.#window.some((entry) => replies.get(entry.request.id)?.id !== entry.reply.id || entry.lines.some((line) => !live.has(line.source))) || projection.amendments.some((edge) => prior.first.has(edge.earlier) && prior.complete.has(edge.later) !== combined.complete.has(edge.later) || prior.first.has(edge.later) && combined.complete.has(edge.later) && !prior.first.has(edge.earlier) && combined.first.has(edge.earlier));
11844
+ const last = stale ? this.#window.at(-1) : void 0;
11845
+ const retained = last === void 0 || replies.get(last.request.id)?.id !== last.reply.id ? void 0 : this.#conversation.message(last.request.id);
11846
+ if (stale) this.#window = [];
11847
+ const [previous, reply] = this.#window.length === 0 ? this.#selectExchange(request, input) : [];
11848
+ const window = this.#window.length > 0 ? this.#window : previous !== void 0 && reply !== void 0 ? [{
11849
+ request: previous,
11850
+ reply,
11851
+ lines: []
11852
+ }] : retained !== void 0 && last !== void 0 ? [{
11853
+ request: retained,
11854
+ reply: last.reply,
11855
+ lines: []
11856
+ }] : [];
11857
+ if (this.#window.length === 0) {
11858
+ const derived = collectLedgerEvidence(scan.messages, new Set(window.flatMap((entry) => [entry.request.id, entry.reply.id])), projection.amendments);
11859
+ this.#window = window;
11860
+ this.#evidence = derived;
11861
+ }
11862
+ const carried = window.flatMap((entry) => entry.lines);
11863
+ const delta = this.#render(trimmed.records, mapped, trimmed.included, projection.amendments, scan, new Set(carried.map(buildLedgerLineKey)), carried);
11864
+ const newest = window.at(-1);
11865
+ const plain = newest === void 0 ? void 0 : this.#conversation.message(newest.request.id);
11866
+ const fallback = newest === void 0 || plain === void 0 ? [] : [{
11867
+ request: plain,
11868
+ reply: newest.reply,
11869
+ lines: []
11870
+ }];
11871
+ const empty = {
11872
+ window: [],
11873
+ rendered: trimmed.rendered
11874
+ };
11875
+ const rungs = [
11876
+ {
11877
+ window,
11878
+ rendered: delta
11879
+ },
11880
+ {
11881
+ window: fallback,
11882
+ rendered: trimmed.rendered
11883
+ },
11884
+ empty
11885
+ ];
11886
+ const probes = rungs.map((rung) => [...built.slice(0, -1), ...stripThinking([
11887
+ ...tail.slice(0, -1),
11888
+ ...rung.window.flatMap((entry) => [entry.request, entry.reply]),
11889
+ buildRequestMessage(built.at(-1) ?? request, rung.rendered.text)
11890
+ ], this.#replay)]);
11891
+ const names = this.#agent.context.scope?.tools;
11892
+ const definitions = this.#agent.context.tools.definitions();
11893
+ const offered = rungs.map((rung) => hasHiddenRecall(input, request.id, this.#requests, this.#annotations, [...tail.slice(0, -1), ...rung.window.flatMap((entry) => [entry.request, entry.reply])], [...rung.window.flatMap((entry) => entry.lines), ...rung.rendered.lines]));
11894
+ const longest = this.#findLongest();
11895
+ const closed = probes.map((messages, at) => {
11896
+ const tools = definitions.filter((tool) => (names === void 0 || names.includes(tool.name)) && (tool.name !== "recall" || offered[at]));
11897
+ const calls = [{
11898
+ estimate: estimateMessages(messages),
11899
+ tools: tools.length,
11900
+ spared: this.#spared(tools)
11901
+ }];
11902
+ return offered[at] === true && this.#gauge !== void 0 && this.#gauge.remainder(calls) - this.#predict < 2 * this.#gauge.reserve(calls, longest);
10536
11903
  });
11904
+ const prices = probes.map((messages) => this.#price(messages));
11905
+ const admitted = rungs.find((_rung, at) => (prices[at] ?? Infinity) <= total && (closed[2] === true || closed[at] === false)) ?? empty;
11906
+ if (offered[rungs.indexOf(admitted)] === false && (names === void 0 || names.includes("recall"))) {
11907
+ const tools = definitions.filter((tool) => tool.name !== "recall").map((tool) => tool.name);
11908
+ const narrowed = this.#agent.context.scope?.narrow({ tools }) ?? createScope({
11909
+ name: "ledger",
11910
+ tools
11911
+ });
11912
+ this.#current()?.apply(narrowed);
11913
+ this.#agent.context.apply(narrowed);
11914
+ }
11915
+ const copy = buildRequestMessage(request, admitted.rendered.text);
11916
+ const kept = admitted.window.flatMap((entry) => entry.lines);
11917
+ const origins = /* @__PURE__ */ new Set([
11918
+ request.id,
11919
+ ...admitted.window.flatMap((entry) => [entry.request.id, entry.reply.id]),
11920
+ ...kept.map((line) => line.source),
11921
+ ...admitted.rendered.lines.map((line) => line.source)
11922
+ ]);
11923
+ const draft = {
11924
+ copy,
11925
+ lines: admitted.rendered.lines,
11926
+ evidence: collectLedgerEvidence(scan.messages, origins, projection.amendments),
11927
+ window: admitted.window
11928
+ };
11929
+ const shown = buildLedgerRenderContext(scan.messages, [...kept, ...admitted.rendered.lines], []).complete;
11930
+ return {
11931
+ draft,
11932
+ messages: [
11933
+ ...tail.slice(0, -1).map((message) => message.role === "tool" ? this.#renderTailStub(message, scan, shown) : message),
11934
+ ...admitted.window.flatMap((entry) => [entry.request, entry.reply]),
11935
+ copy
11936
+ ]
11937
+ };
11938
+ }
11939
+ #trim(steps, records, mapped, included, built, tail, total, amendments, scan) {
10537
11940
  const head = built.slice(0, -1);
10538
- const priced = built.at(-1) ?? request;
11941
+ const priced = built.at(-1);
11942
+ if (priced === void 0) throw new Error("a built plan holds its request");
10539
11943
  const found = findLedgerCut(0, steps.length, (count) => {
10540
11944
  const cut = buildLedgerCut(steps.slice(0, count), records, included);
10541
11945
  const rendered = this.#render(cut.records, mapped, cut.included, amendments, scan);
10542
- const copy = buildRequestMessage(priced, rendered);
11946
+ const copy = buildRequestMessage(priced, rendered.text);
10543
11947
  return this.#price([...head, ...stripThinking([...tail.slice(0, -1), copy], this.#replay)]) <= total ? {
10544
11948
  ...cut,
10545
11949
  rendered
@@ -10564,36 +11968,48 @@ var Ledger = class {
10564
11968
  const live = collectProjectionIds(projection);
10565
11969
  return scanAmendments(input.classification.amendments, source, (id) => id === source || !held.has(id) && live.has(id)).flatMap((id) => this.#projectLines(input, id));
10566
11970
  }
10567
- #render(records, mapped, included, amendments, scan) {
10568
- const entries = mapped.filter((unit) => included.has(unit.source)).flatMap(({ record: unit }) => [...Map.groupBy(unit.lines, (line) => line.source)].map(([source, lines]) => ({
11971
+ #render(records, mapped, included, amendments, scan, held = /* @__PURE__ */ new Set(), prefix = []) {
11972
+ const population = records.map((record) => ({
11973
+ ...record,
11974
+ lines: collectLedgerLines(record, held)
11975
+ }));
11976
+ const entries = mapped.filter((unit) => included.has(unit.source)).flatMap(({ record: unit }) => [...Map.groupBy(collectLedgerLines(unit, /* @__PURE__ */ new Set()), (line) => line.source)].map(([source, lines]) => ({
10569
11977
  unit,
10570
11978
  source,
10571
11979
  lines
10572
11980
  })));
10573
11981
  const first = new Map(entries.toReversed().map((entry) => [entry.source, entry]));
10574
- const unique = entries.filter((entry) => first.get(entry.source) === entry);
10575
- const record = records.find((candidate) => candidate.key === "rules" && candidate.lines.length > 0);
10576
- const ownerRecords = records.filter((candidate) => candidate.key !== "rules" && candidate.lines.length > 0);
10577
- const context = buildLedgerRenderContext(this.#conversation.messages(), [
11982
+ const unique = entries.filter((entry) => first.get(entry.source) === entry).map((entry) => ({
11983
+ ...entry,
11984
+ lines: entry.lines.filter((line) => !prefix.some((prior) => prior.source === line.source && prior.sentence === line.sentence))
11985
+ })).filter((entry) => entry.lines.length > 0);
11986
+ const record = population.find((candidate) => candidate.key === "rules" && candidate.lines.length > 0);
11987
+ const ownerRecords = population.filter((candidate) => candidate.key !== "rules" && candidate.lines.length > 0);
11988
+ const lines = [
10578
11989
  ...ownerRecords.flatMap((candidate) => candidate.lines),
10579
11990
  ...unique.filter((entry) => entry.unit.key !== LEDGER_RULES_KEY).flatMap((entry) => entry.lines),
10580
11991
  ...record?.lines ?? [],
10581
11992
  ...unique.filter((entry) => entry.unit.key === LEDGER_RULES_KEY).flatMap((entry) => entry.lines)
10582
- ], amendments);
11993
+ ];
11994
+ const context = buildLedgerRenderContext(scan.messages, [...prefix, ...lines], amendments);
10583
11995
  const owners = ownerRecords.map((owner) => renderLedgerPinned(owner, context)).join("\n\n");
10584
- const rendered = unique.map(({ unit, source, lines }) => {
11996
+ const rendered = unique.map(({ unit, source, lines: retained }) => {
10585
11997
  const sentences = splitSentences(this.#conversation.message(source)?.content ?? "");
10586
11998
  return {
10587
11999
  rules: unit.key === LEDGER_RULES_KEY,
10588
- texts: unit.key === "rules" && source === unit.members[0] ? lines.map((line) => this.#renderSource(source, sentences, [line], context, scan)) : [this.#renderSource(source, sentences, lines, context, scan)]
12000
+ texts: unit.key === "rules" && source === unit.members[0] ? retained.map((line) => this.#renderSource(source, sentences, [line], context, scan)) : [this.#renderSource(source, sentences, retained, context, scan)]
10589
12001
  };
10590
12002
  });
10591
12003
  const pinned = rendered.filter((one) => !one.rules).flatMap((one) => one.texts);
10592
12004
  const rules = rendered.filter((one) => one.rules).flatMap((one) => one.texts);
10593
12005
  const body = [owners, pinned.join("\n")].filter((text) => text !== "").join("\n\n");
10594
- return [body === "" ? "" : `## Records\n${body}`, record === void 0 ? rules.length === 0 ? "" : ["## Rules", ...rules].join("\n") : [renderLedgerRecord(record, context), ...rules].join("\n")].filter((text) => text !== "").join("\n\n");
12006
+ return {
12007
+ text: [body === "" ? "" : `## Records\n${body}`, record === void 0 ? rules.length === 0 ? "" : ["## Rules", ...rules].join("\n") : [renderLedgerRecord(record, context), ...rules].join("\n")].filter((part) => part !== "").join("\n\n"),
12008
+ lines
12009
+ };
10595
12010
  }
10596
- #selectTail(request, cap, input, scan) {
12011
+ #selectTail(request, total, input, scan) {
12012
+ const cap = total * this.#share.tail;
10597
12013
  const first = input.messages.findIndex((message) => this.#requests.has(message.id));
10598
12014
  const seed = input.messages.slice(0, first < 0 ? input.messages.findIndex((message) => message.id === request.id) : first);
10599
12015
  const groups = collectToolGroups(seed).flatMap((group) => {
@@ -10624,6 +12040,29 @@ var Ledger = class {
10624
12040
  const stop = exchanges.findIndex((_exchange, index) => this.#price(stripThinking([...exchanges.slice(0, index + 1).toReversed().flat(), request], this.#replay)) > cap);
10625
12041
  return [...exchanges.slice(0, stop === -1 ? exchanges.length : stop).toReversed().flat(), request];
10626
12042
  }
12043
+ #selectExchange(request, input) {
12044
+ const before = input.messages.slice(0, input.messages.findIndex((message) => message.id === request.id));
12045
+ const previous = before.findLast((message) => this.#requests.has(message.id));
12046
+ const reply = previous === void 0 ? void 0 : collectLedgerReplies(before, this.#requests, this.#annotations).get(previous.id);
12047
+ return previous !== void 0 && reply !== void 0 ? [previous, reply] : [];
12048
+ }
12049
+ #commitExchange(request) {
12050
+ const draft = request.sent;
12051
+ const message = request.message;
12052
+ if (message === void 0) return;
12053
+ const reply = collectLedgerReplies(this.#conversation.messages(), this.#requests, this.#annotations).get(message.id);
12054
+ if (reply === void 0) return;
12055
+ this.#window = [...draft?.window ?? this.#window, {
12056
+ request: draft?.copy ?? message,
12057
+ reply,
12058
+ lines: draft?.lines ?? []
12059
+ }];
12060
+ const evidence = draft?.evidence ?? this.#evidence;
12061
+ this.#evidence = {
12062
+ messages: /* @__PURE__ */ new Set([...evidence.messages, ...collectLedgerEvidence([message, reply], /* @__PURE__ */ new Set([message.id, reply.id]), []).messages]),
12063
+ amendments: evidence.amendments
12064
+ };
12065
+ }
10627
12066
  #renderTailStub(message, scan, shown) {
10628
12067
  const group = scan.group(message.id);
10629
12068
  const call = group === void 0 ? void 0 : findLedgerCall(group, message);
@@ -10632,7 +12071,7 @@ var Ledger = class {
10632
12071
  const visible = renderStub(call?.name ?? "tool", call?.arguments ?? {}, "shown");
10633
12072
  return {
10634
12073
  ...message,
10635
- content: renderStub(call?.name ?? "tool", call?.arguments ?? {}, this.#results.get(message.id)?.success === false ? "failed" : finding?.result === void 0 ? "empty" : shown === void 0 ? hidden.length > visible.length ? "hidden" : "shown" : shown.has(message.id) ? "shown" : "hidden")
12074
+ content: renderStub(call?.name ?? "tool", call?.arguments ?? {}, this.#results.get(message.id)?.success === false ? "failed" : finding?.result === void 0 ? "empty" : shown === void 0 ? hidden.length > visible.length ? "hidden" : "shown" : scan.findings.some((other) => shown.has(other.id) && matchesLookup(finding, other)) ? "shown" : "hidden")
10636
12075
  };
10637
12076
  }
10638
12077
  #observeTurn() {
@@ -10643,11 +12082,19 @@ var Ledger = class {
10643
12082
  const additions = this.#conversation.messages().slice(boundary);
10644
12083
  const messages = this.#agent.context.build(request.selection);
10645
12084
  const names = this.#agent.context.scope?.tools;
12085
+ const tools = this.#agent.context.tools.definitions().filter((tool) => names === void 0 || names.includes(tool.name));
10646
12086
  request.observe({
10647
12087
  estimate: estimateMessages(stripThinking([...messages, ...additions], this.#replay)),
10648
- tools: this.#agent.context.tools.definitions().filter((tool) => names === void 0 || names.includes(tool.name)).length
12088
+ tools: tools.length,
12089
+ spared: this.#spared(tools)
10649
12090
  }, this.#conversation.messages().length);
10650
12091
  }
12092
+ #spared(tools) {
12093
+ const overhead = this.#gauge?.overhead ?? 0;
12094
+ if (tools.length === 0) return overhead;
12095
+ const missing = this.#agent.context.tools.definitions().filter((tool) => !tools.some((one) => one.name === tool.name));
12096
+ return missing.length === 0 ? 0 : Math.min(overhead, estimateTokens(JSON.stringify(missing)) * (this.#gauge?.scale ?? 1));
12097
+ }
10651
12098
  #findLongest() {
10652
12099
  return this.#conversation.messages().filter((message) => message.role === "assistant").reduce((longest, message) => message.content.length > longest.length ? message.content : longest, "");
10653
12100
  }
@@ -10700,11 +12147,12 @@ var Ledger = class {
10700
12147
  const source = listable.get(id);
10701
12148
  if (source === void 0) return [];
10702
12149
  const lookup = scan.finding(id);
10703
- const text = lookup === void 0 ? source.content : `${renderLookupCall(lookup.name, lookup.arguments)}: ${source.content}`;
12150
+ const content = source.content.replace(/(^|[\n\v\f\r\u0085\u2028\u2029])(?=Earlier (?:request|reply):)/g, "$1 ");
12151
+ const text = lookup === void 0 ? content : `${renderLookupCall(lookup.name, lookup.arguments)}: ${content}`;
10704
12152
  return [{
10705
12153
  id,
10706
12154
  text,
10707
- stored: lookup === void 0 ? [] : [[splitResultLines(text)[0] ?? "", splitResultLines(source.content)[0] ?? ""]]
12155
+ stored: lookup === void 0 ? [] : [[splitResultLines(text)[0] ?? "", splitResultLines(content)[0] ?? ""]]
10708
12156
  }];
10709
12157
  });
10710
12158
  return {
@@ -10717,7 +12165,14 @@ var Ledger = class {
10717
12165
  listed: [],
10718
12166
  recalled: /* @__PURE__ */ new Map()
10719
12167
  });
10720
- const result = gathered.listed.length === 0 ? `nothing on "${topic}"; recall ${guidance}` : cutListing(gathered.listed, room);
12168
+ const earlier = input.messages.slice(0, Math.max(0, start)).reverse().flatMap((message) => {
12169
+ if (gathered.done.has(message.id) || this.#annotations.has(message.id)) return [];
12170
+ const label = this.#requests.has(message.id) ? LEDGER_RECALL_LABELS.request : message.role === "assistant" && !hasCalls(message) && message.content.trim() !== "" ? LEDGER_RECALL_LABELS.reply : void 0;
12171
+ if (label === void 0 || !searches.some((search) => search.words.length > 0 && search.words.every((word) => message.content.toLowerCase().includes(word)))) return [];
12172
+ return [`${label} ${message.content.replace(/[\n\v\f\r\u0085\u2028\u2029]+/g, "\n ")}`];
12173
+ });
12174
+ const listed = [...gathered.listed, ...earlier];
12175
+ const result = listed.length === 0 ? `nothing earlier on "${topic}"; answer from the messages you can see` : cutListing(listed, room, earlier.length > 0 ? "more" : "older");
10721
12176
  request?.list(result, gathered.recalled);
10722
12177
  return result;
10723
12178
  } finally {
@@ -10730,7 +12185,7 @@ var Ledger = class {
10730
12185
  const result = this.#results.get(message.id);
10731
12186
  if (message.role !== "tool" || result?.success !== true) return [];
10732
12187
  const recalled = result.name === "recall";
10733
- if (recalled && /^nothing on /.test(message.content)) return [];
12188
+ if (recalled && /^nothing earlier on /.test(message.content)) return [];
10734
12189
  const stored = recalled ? request.lines(message.content) : void 0;
10735
12190
  return splitResultLines(message.content).filter((line) => !recalled || !matchesCutLine(line)).map((line) => stored?.get(line) ?? line);
10736
12191
  }))];
@@ -10774,7 +12229,6 @@ exports.ConversationError = ConversationError;
10774
12229
  exports.ConversationManager = ConversationManager;
10775
12230
  exports.DECISIVE_CATEGORIES = DECISIVE_CATEGORIES;
10776
12231
  exports.DEFAULT_AGENT_LIMIT = DEFAULT_AGENT_LIMIT;
10777
- exports.DEFAULT_AUTHORITY_ZONE = DEFAULT_AUTHORITY_ZONE;
10778
12232
  exports.DEFAULT_CONVERSATION_KEEP = DEFAULT_CONVERSATION_KEEP;
10779
12233
  exports.DEFAULT_LEDGER_LIMIT = DEFAULT_LEDGER_LIMIT;
10780
12234
  exports.DEFAULT_LEDGER_SHARE = DEFAULT_LEDGER_SHARE;
@@ -10796,6 +12250,7 @@ exports.LEDGER_CATEGORIES = LEDGER_CATEGORIES;
10796
12250
  exports.LEDGER_NOTES = LEDGER_NOTES;
10797
12251
  exports.LEDGER_OWNER_PREFIX = LEDGER_OWNER_PREFIX;
10798
12252
  exports.LEDGER_QUESTIONS = LEDGER_QUESTIONS;
12253
+ exports.LEDGER_RECALL_LABELS = LEDGER_RECALL_LABELS;
10799
12254
  exports.LEDGER_RULES_KEY = LEDGER_RULES_KEY;
10800
12255
  exports.LEDGER_SCALE_DRIFT = LEDGER_SCALE_DRIFT;
10801
12256
  exports.LEDGER_SLICE = LEDGER_SLICE;
@@ -10839,6 +12294,8 @@ exports.agentAnswerShape = agentAnswerShape;
10839
12294
  exports.agentChunkToFrame = agentChunkToFrame;
10840
12295
  exports.agentFrameContract = agentFrameContract;
10841
12296
  exports.agentFrameShape = agentFrameShape;
12297
+ exports.agentJobContract = agentJobContract;
12298
+ exports.agentJobShape = agentJobShape;
10842
12299
  exports.agentPromptContract = agentPromptContract;
10843
12300
  exports.agentPromptShape = agentPromptShape;
10844
12301
  exports.agentResultToFrame = agentResultToFrame;
@@ -10852,6 +12309,7 @@ exports.buildJudgeResult = buildJudgeResult;
10852
12309
  exports.buildJudgments = buildJudgments;
10853
12310
  exports.buildLedgerCut = buildLedgerCut;
10854
12311
  exports.buildLedgerIndex = buildLedgerIndex;
12312
+ exports.buildLedgerLineKey = buildLedgerLineKey;
10855
12313
  exports.buildLedgerRenderContext = buildLedgerRenderContext;
10856
12314
  exports.buildLines = buildLines;
10857
12315
  exports.buildProviderHeaders = buildProviderHeaders;
@@ -10866,12 +12324,18 @@ exports.chargeUsage = chargeUsage;
10866
12324
  exports.collectAmendments = collectAmendments;
10867
12325
  exports.collectExchanges = collectExchanges;
10868
12326
  exports.collectImageData = collectImageData;
12327
+ exports.collectLedgerEvidence = collectLedgerEvidence;
12328
+ exports.collectLedgerLines = collectLedgerLines;
12329
+ exports.collectLedgerReplies = collectLedgerReplies;
10869
12330
  exports.collectLive = collectLive;
10870
12331
  exports.collectNames = collectNames;
10871
12332
  exports.collectProjectionIds = collectProjectionIds;
12333
+ exports.collectRecallMessages = collectRecallMessages;
10872
12334
  exports.collectRegistry = collectRegistry;
10873
12335
  exports.collectToolGroups = collectToolGroups;
10874
12336
  exports.computeThinking = computeThinking;
12337
+ exports.conversationSnapshotContract = conversationSnapshotContract;
12338
+ exports.conversationSnapshotShape = conversationSnapshotShape;
10875
12339
  exports.copyJSON = copyJSON;
10876
12340
  exports.createAgent = createAgent;
10877
12341
  exports.createAgentContext = createAgentContext;
@@ -10900,7 +12364,10 @@ exports.createSystemOneJudge = createSystemOneJudge;
10900
12364
  exports.createThinkSplitter = createThinkSplitter;
10901
12365
  exports.cutListing = cutListing;
10902
12366
  exports.denyCall = denyCall;
12367
+ exports.describeQuestionRefusal = describeQuestionRefusal;
10903
12368
  exports.describeRelayRefusal = describeRelayRefusal;
12369
+ exports.describeSnapshotRefusal = describeSnapshotRefusal;
12370
+ exports.digestLedgerWindow = digestLedgerWindow;
10904
12371
  exports.estimateMessages = estimateMessages;
10905
12372
  exports.estimateRequest = estimateRequest;
10906
12373
  exports.estimateTokens = estimateTokens;
@@ -10916,44 +12383,70 @@ exports.fitSlope = fitSlope;
10916
12383
  exports.freezeJSON = freezeJSON;
10917
12384
  exports.handleAgentQueueJob = handleAgentQueueJob;
10918
12385
  exports.hasCalls = hasCalls;
12386
+ exports.hasHiddenRecall = hasHiddenRecall;
10919
12387
  exports.identifyLookup = identifyLookup;
10920
12388
  exports.inferStop = inferStop;
10921
12389
  exports.intersectKeys = intersectKeys;
10922
12390
  exports.isAgentError = isAgentError;
10923
12391
  exports.isAgentJobError = isAgentJobError;
10924
12392
  exports.isAuthorityDecision = isAuthorityDecision;
12393
+ exports.isAuthorityVerdict = isAuthorityVerdict;
10925
12394
  exports.isContextError = isContextError;
10926
12395
  exports.isConversation = isConversation;
10927
12396
  exports.isConversationError = isConversationError;
10928
12397
  exports.isConversationSnapshot = isConversationSnapshot;
10929
12398
  exports.isFraction = isFraction;
10930
12399
  exports.isJudgeAbortError = isJudgeAbortError;
12400
+ exports.isJudgeAnswer = isJudgeAnswer;
10931
12401
  exports.isJudgeEntry = isJudgeEntry;
10932
12402
  exports.isJudgeError = isJudgeError;
10933
12403
  exports.isJudgeQuestion = isJudgeQuestion;
10934
12404
  exports.isJudgment = isJudgment;
10935
12405
  exports.isLedgerError = isLedgerError;
12406
+ exports.isLedgerQuestion = isLedgerQuestion;
10936
12407
  exports.isLedgerSnapshot = isLedgerSnapshot;
12408
+ exports.isLedgerWindowSnapshot = isLedgerWindowSnapshot;
10937
12409
  exports.isMemo = isMemo;
12410
+ exports.isMemoOrigin = isMemoOrigin;
10938
12411
  exports.isMemoSnapshot = isMemoSnapshot;
10939
12412
  exports.isMessage = isMessage;
12413
+ exports.isMessageInput = isMessageInput;
10940
12414
  exports.isNonNegativeSafeInteger = isNonNegativeSafeInteger;
10941
12415
  exports.isPositiveSafeInteger = isPositiveSafeInteger;
10942
12416
  exports.isProviderAbortError = isProviderAbortError;
10943
12417
  exports.isProviderError = isProviderError;
12418
+ exports.isProviderResult = isProviderResult;
10944
12419
  exports.isSection = isSection;
10945
12420
  exports.isSystemOneAnswer = isSystemOneAnswer;
10946
12421
  exports.isSystemOneResponse = isSystemOneResponse;
10947
12422
  exports.joinThinking = joinThinking;
12423
+ exports.judgeAnswerContract = judgeAnswerContract;
12424
+ exports.judgeAnswerShape = judgeAnswerShape;
12425
+ exports.judgeEntryShape = judgeEntryShape;
12426
+ exports.judgeQuestionContract = judgeQuestionContract;
12427
+ exports.judgeQuestionShape = judgeQuestionShape;
12428
+ exports.judgmentContract = judgmentContract;
12429
+ exports.judgmentShape = judgmentShape;
12430
+ exports.ledgerSnapshotContract = ledgerSnapshotContract;
12431
+ exports.ledgerSnapshotShape = ledgerSnapshotShape;
12432
+ exports.ledgerWindowContract = ledgerWindowContract;
12433
+ exports.ledgerWindowShape = ledgerWindowShape;
10948
12434
  exports.linkOwners = linkOwners;
10949
12435
  exports.matchEntities = matchEntities;
10950
12436
  exports.matchesCutLine = matchesCutLine;
10951
12437
  exports.matchesJSON = matchesJSON;
10952
12438
  exports.matchesJudgment = matchesJudgment;
12439
+ exports.matchesLookup = matchesLookup;
10953
12440
  exports.measureAnswer = measureAnswer;
10954
12441
  exports.measureMessage = measureMessage;
10955
12442
  exports.measureRequest = measureRequest;
12443
+ exports.memoContract = memoContract;
12444
+ exports.memoShape = memoShape;
12445
+ exports.memoSnapshotContract = memoSnapshotContract;
12446
+ exports.memoSnapshotShape = memoSnapshotShape;
10956
12447
  exports.messageContract = messageContract;
12448
+ exports.messageInputContract = messageInputContract;
12449
+ exports.messageInputShape = messageInputShape;
10957
12450
  exports.messageShape = messageShape;
10958
12451
  exports.normalizeArgument = normalizeArgument;
10959
12452
  exports.normalizeWord = normalizeWord;
@@ -10968,6 +12461,8 @@ exports.rankLedgerCut = rankLedgerCut;
10968
12461
  exports.readBodyText = readBodyText;
10969
12462
  exports.readChunks = readChunks;
10970
12463
  exports.readFailure = readFailure;
12464
+ exports.readJudgeOutcome = readJudgeOutcome;
12465
+ exports.readProviderDelta = readProviderDelta;
10971
12466
  exports.relayFrameContract = relayFrameContract;
10972
12467
  exports.relayFrameShape = relayFrameShape;
10973
12468
  exports.releaseReader = releaseReader;
@@ -10986,15 +12481,21 @@ exports.renderStub = renderStub;
10986
12481
  exports.renderToolValue = renderToolValue;
10987
12482
  exports.renderTopicNames = renderTopicNames;
10988
12483
  exports.requestToRelay = requestToRelay;
12484
+ exports.requireAnswer = requireAnswer;
10989
12485
  exports.requireBudget = requireBudget;
10990
12486
  exports.requireConversation = requireConversation;
10991
12487
  exports.requireConversationSnapshot = requireConversationSnapshot;
10992
12488
  exports.requireCumulative = requireCumulative;
10993
12489
  exports.requireEntry = requireEntry;
12490
+ exports.requireFallback = requireFallback;
12491
+ exports.requireJudgeResult = requireJudgeResult;
10994
12492
  exports.requireJudgment = requireJudgment;
10995
12493
  exports.requireKeep = requireKeep;
10996
12494
  exports.requireLimit = requireLimit;
12495
+ exports.requireMessageInput = requireMessageInput;
12496
+ exports.requireProviderResult = requireProviderResult;
10997
12497
  exports.requireRole = requireRole;
12498
+ exports.requireRules = requireRules;
10998
12499
  exports.requireSectionsCap = requireSectionsCap;
10999
12500
  exports.requireSummary = requireSummary;
11000
12501
  exports.resolveDecision = resolveDecision;
@@ -11002,8 +12503,13 @@ exports.resolvePredict = resolvePredict;
11002
12503
  exports.sanitizeToken = sanitizeToken;
11003
12504
  exports.sanitizeUsage = sanitizeUsage;
11004
12505
  exports.scanAmendments = scanAmendments;
12506
+ exports.scanJudgmentTies = scanJudgmentTies;
12507
+ exports.scanProbability = scanProbability;
12508
+ exports.sectionContract = sectionContract;
12509
+ exports.sectionShape = sectionShape;
11005
12510
  exports.selectRecords = selectRecords;
11006
12511
  exports.settleAgentJob = settleAgentJob;
12512
+ exports.settleToolResult = settleToolResult;
11007
12513
  exports.splitResultLines = splitResultLines;
11008
12514
  exports.splitSentences = splitSentences;
11009
12515
  exports.splitTopic = splitTopic;