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