@sema-agent/core 7.23.5 → 7.24.0

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/agents/list-agents-tool.d.ts +7 -4
  3. package/dist/agents/list-agents-tool.js +12 -1
  4. package/dist/agents/observer.js +5 -8
  5. package/dist/agents/subagent.js +2 -2
  6. package/dist/agents/team.js +9 -0
  7. package/dist/brain/degrading.js +8 -9
  8. package/dist/brain/route-adjudicator.d.ts +25 -12
  9. package/dist/brain/route-adjudicator.js +89 -23
  10. package/dist/brain/route-conformance.js +9 -0
  11. package/dist/brain/stream-engine.js +11 -9
  12. package/dist/core/arg-summary.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +42 -1
  14. package/dist/core/auto-compaction.js +21 -1
  15. package/dist/core/auto-mode.js +2 -8
  16. package/dist/core/exec-gate.js +3 -5
  17. package/dist/core/file-history-store.js +11 -10
  18. package/dist/core/hooks.d.ts +7 -0
  19. package/dist/core/mcp.js +7 -5
  20. package/dist/core/memory-engine/distiller.d.ts +7 -4
  21. package/dist/core/memory-engine/distiller.js +11 -3
  22. package/dist/core/memory.js +1 -0
  23. package/dist/core/runner/assemble-result.js +12 -19
  24. package/dist/core/runner/contracts.d.ts +1 -0
  25. package/dist/core/runner/prepare-file-history.js +19 -0
  26. package/dist/core/runner/run-recovery-lanes.js +4 -20
  27. package/dist/core/runner/run-settle-and-teardown.js +2 -16
  28. package/dist/core/runner/run-turn-boundary.js +4 -20
  29. package/dist/core/runner/runtask.js +3 -5
  30. package/dist/core/runner/stream-settle-backstop.js +5 -5
  31. package/dist/core/runner/tool-end-body.js +5 -4
  32. package/dist/core/runner/tool-output-projection.js +3 -2
  33. package/dist/core/runner/turn-attachments.d.ts +0 -20
  34. package/dist/core/runner/turn-attachments.js +41 -36
  35. package/dist/core/session-store.js +2 -4
  36. package/dist/core/session.d.ts +4 -1
  37. package/dist/core/session.js +2 -1
  38. package/dist/core/task-event.d.ts +19 -9
  39. package/dist/core/task-registry-shared.js +1 -0
  40. package/dist/core/thrown-value.d.ts +24 -13
  41. package/dist/core/thrown-value.js +13 -1
  42. package/dist/core/tool-errors.d.ts +6 -0
  43. package/dist/core/tool-errors.js +6 -14
  44. package/dist/core/tools.js +8 -9
  45. package/dist/engine/llm/types.d.ts +4 -4
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/orchestration/workflow.js +44 -23
  49. package/dist/stores/file/file-history-store.js +12 -11
  50. package/package.json +1 -1
  51. package/test/export-surface.snapshot.json +7 -3
  52. package/wire-changes/core@7.24.0.json +249 -0
@@ -1,7 +1,7 @@
1
1
  import { mintSystemReminder } from "../reminder-mint.js";
2
2
  import { DEFAULT_COMPACTION_SETTINGS } from "../../internal/harness.js";
3
3
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
4
- import { DEFAULT_COMPACTION_INSTRUCTIONS, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill } from "../auto-compaction.js";
4
+ import { DEFAULT_COMPACTION_INSTRUCTIONS, isCompactionManualCancel, maybeCompact, mintCompactedEvent, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill } from "../auto-compaction.js";
5
5
  import { emitTrace } from "../trace.js";
6
6
  import { redactThenCut } from "../../agents/subagent-steps.js";
7
7
  import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, attachmentEnvelopeTags, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, isIntactSource, rebaseCadenceWindows, renderAgentListingFrame, renderMcpDroppedTools, renderMcpInstructionsDelta, selectMcpDroppedBatch, renderSkillsListingFrame, renderToolsDelta } from "./turn-attachments.js";
@@ -643,22 +643,7 @@ export function createTurnBoundary(input) {
643
643
  }
644
644
  recordCompactionReuse(runner, prepared, comp);
645
645
  if (comp.compacted) {
646
- queue.push({
647
- type: "compacted",
648
- trigger: passTrigger,
649
- tokensBefore: comp.tokensBefore ?? 0,
650
- ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
651
- ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
652
- ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
653
- ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
654
- ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
655
- ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
656
- ...(comp.modelFallback ? { modelFallback: true } : {}),
657
- ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
658
- ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
659
- ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
660
- ...ident(),
661
- });
646
+ queue.push(mintCompactedEvent(comp, passTrigger, ident()));
662
647
  if (!forceManual && recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
663
648
  compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
664
649
  queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
@@ -674,9 +659,8 @@ export function createTurnBoundary(input) {
674
659
  rs.attach.attachState.postCompactPending = true;
675
660
  rebaseCadenceWindows(rs.attach.attachState, rs.counters.cadenceTurns);
676
661
  }
677
- if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
678
- const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
679
- rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
662
+ if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON && comp.postTriggerTokens !== undefined) {
663
+ rs.counters.compactionFloor = Math.ceil(comp.postTriggerTokens * COMPACTION_REGROWTH_FACTOR);
680
664
  }
681
665
  }
682
666
  }
@@ -6,7 +6,7 @@ import { createRapidRefillState } from "../auto-compaction.js";
6
6
  import { boundInputHashOf } from "../canonical-json.js";
7
7
  import { emitTrace } from "../trace.js";
8
8
  import { emitTaskOutcome } from "../task-outcome.js";
9
- import { authCarrierFingerprint, normalizeBaseUrl } from "../../brain/route-adjudicator.js";
9
+ import { authCarrierFingerprint, displayUrlOf, routeKeyOf } from "../../brain/route-adjudicator.js";
10
10
  import { runWithReasoningWireFacts } from "../../brain/status-sink.js";
11
11
  import { expandTiers } from "../roles.js";
12
12
  import { screenSwappableDeps } from "../swappable-deps.js";
@@ -208,10 +208,8 @@ export class Runner {
208
208
  const prior = this.deps.models?.[name];
209
209
  if (!prior)
210
210
  continue;
211
- const from = normalizeBaseUrl(prior.baseUrl);
212
- const to = normalizeBaseUrl(nextModel.baseUrl);
213
- if (from !== to && authCarrierFingerprint(prior.headers) === authCarrierFingerprint(nextModel.headers)) {
214
- movedEntries.push({ modelId: name, from, to });
211
+ if (routeKeyOf(prior.baseUrl) !== routeKeyOf(nextModel.baseUrl) && authCarrierFingerprint(prior.headers) === authCarrierFingerprint(nextModel.headers)) {
212
+ movedEntries.push({ modelId: name, from: displayUrlOf(prior.baseUrl), to: displayUrlOf(nextModel.baseUrl) });
215
213
  }
216
214
  }
217
215
  }
@@ -6,7 +6,7 @@ import { createSafeNotifier } from "../safe-notify.js";
6
6
  import { settleTeardownLeg } from "./teardown-bounded.js";
7
7
  import { emitTrace } from "../trace.js";
8
8
  import { deliverDelegationLifecycle } from "../types.js";
9
- import { asThrownError, thrownText } from "../thrown-value.js";
9
+ import { asThrownError, readMember, thrownText } from "../thrown-value.js";
10
10
  export function streamSettleBackstop(input) {
11
11
  const { spec, resume, queue, taskIdRef, entryTracer, runStartedAt, live, runner, sessions, locallyClaimedTokens, parentConstraintRegistry, suspendedEnvReaps } = input;
12
12
  const onRunRejected = async (err) => {
@@ -57,11 +57,11 @@ export function streamSettleBackstop(input) {
57
57
  }
58
58
  const remoteEnvFailure = (() => {
59
59
  let cur = err;
60
- for (let depth = 0; cur && typeof cur === "object" && depth < 8; depth++) {
61
- const note = cur.remoteEnvFailure;
60
+ for (let depth = 0; cur !== null && cur !== undefined && typeof cur === "object" && depth < 8; depth++) {
61
+ const note = readMember(cur, "remoteEnvFailure");
62
62
  if (note !== undefined)
63
63
  return [note];
64
- cur = cur.cause;
64
+ cur = readMember(cur, "cause");
65
65
  }
66
66
  return undefined;
67
67
  })();
@@ -79,7 +79,7 @@ export function streamSettleBackstop(input) {
79
79
  return observed !== undefined && observed.length > 0 ? { editedFiles: observed } : {};
80
80
  })(),
81
81
  ...(() => {
82
- const hinted = err.retryAfterMs;
82
+ const hinted = readMember(err, "retryAfterMs");
83
83
  return code === "memory.admission_required" && typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0
84
84
  ? { retryAfterMs: hinted }
85
85
  : {};
@@ -1,4 +1,5 @@
1
1
  import {} from "../../internal/harness.js";
2
+ import { readMember } from "../thrown-value.js";
2
3
  import { MCP_DELIVERY_VERDICTS } from "../mcp-failure.js";
3
4
  import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
4
5
  import { toolOutputFrom } from "./tool-output-projection.js";
@@ -35,11 +36,11 @@ export function batchResponseDigest(result) {
35
36
  export function toolEndBodyFrom(result, isError, structured, gate, gatedCallIdOfRun) {
36
37
  const o = toolOutputFrom(result);
37
38
  const st = structured(result);
38
- const det = isError ? result?.details : undefined;
39
- const codeRaw = det?.code;
40
- const kindRaw = det?.errorKind;
39
+ const det = isError ? readMember(result, "details") : undefined;
40
+ const codeRaw = readMember(det, "code");
41
+ const kindRaw = readMember(det, "errorKind");
41
42
  const code = typeof codeRaw === "string" ? codeRaw : typeof kindRaw === "string" ? kindRaw : undefined;
42
- const deliveredRaw = det?.delivered;
43
+ const deliveredRaw = readMember(det, "delivered");
43
44
  const delivered = typeof deliveredRaw === "string" && MCP_DELIVERY_VERDICTS.includes(deliveredRaw) ? deliveredRaw : undefined;
44
45
  const gatedCallId = code === "gate.parked" ? gatedCallIdOfRun : undefined;
45
46
  return {
@@ -1,4 +1,5 @@
1
1
  import { engineCardTypes } from "../tool-registry.js";
2
+ import { readMember } from "../thrown-value.js";
2
3
  const MAX_TOOL_END_OUTPUT_CHARS = 16384;
3
4
  const safeJsonLength = (v) => {
4
5
  try {
@@ -62,10 +63,10 @@ export function createStructuredProjector(opts = {}) {
62
63
  const allowed = new Set([...CC_DETAIL_TYPES, ...(opts.declaredCards ?? [])]);
63
64
  const announced = new Set();
64
65
  return (result) => {
65
- const details = result !== null && typeof result === "object" ? result.details : undefined;
66
+ const details = readMember(result, "details");
66
67
  if (details === null || typeof details !== "object")
67
68
  return undefined;
68
- const t = details.type;
69
+ const t = readMember(details, "type");
69
70
  if (typeof t !== "string")
70
71
  return undefined;
71
72
  if (!allowed.has(t)) {
@@ -346,26 +346,6 @@ export declare const INSTRUCTIONS_CHANGE_BYTE_CAP = 512;
346
346
  * `steering_injected` echo are all keyed on that wordlist.
347
347
  */
348
348
  export declare function createAttachmentState(): AttachmentState;
349
- /**
350
- * H4 — thin-projection reducer over a tool's RAW `result.details` (NOT the wire card projector
351
- * projection and its 4×16K caps: a huge todo list must shrink into a bounded snapshot here, never
352
- * disarm the reminder by being dropped wholesale). Only the first-party discriminants `todo` /
353
- * `task` / `task-list` are recognized — every other card type (subagent `agent`/`fork`/`task-output`,
354
- * bash, fs, mcp, …) is structurally ignored, which is also the child-isolation contract: a subagent's
355
- * own todo state never reaches the parent's onToolEnd, and its delegation-card types don't parse here.
356
- *
357
- * task #51 F3 (窗口复位/投影摄入拆分): this reducer is PROJECTION INGEST ONLY — it
358
- * never touches the write windows. CC's window anchor is the tool_use block's PRESENCE in the
359
- * transcript (qFm/BFm match the block by name, :480217-480219/:480253-480254, with no execution-
360
- * outcome check), so the run loop stamps the windows on INITIATION via {@link stampWriteAnchor}
361
- * (inside {@link advanceCadenceClock}'s message walk); ingest stays success-only — a failed write
362
- * has no trustworthy `details`, exactly like CC's reminder body rendering from the authoritative
363
- * store rather than from tool results.
364
- *
365
- * Idempotent (same event twice ⇒ same state) and commutative across DISTINCT task ids (F10); two
366
- * whole-list writes (TodoWrite / TaskList) racing in one batch are last-write-wins by nature — the
367
- * later list IS the truer snapshot, documented rather than fought.
368
- */
369
349
  export declare function reduceToolEnd(state: AttachmentState, details: unknown): void;
370
350
  /** task #51 F2/F3 — which reminder window a tool_use block resets. Resolved by the run loop (alias-aware:
371
351
  * ToolSpec.aliases over the mounted roster; retired-name normalization is gone); the producers
@@ -1,4 +1,5 @@
1
1
  import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
2
+ import { readMember } from "../thrown-value.js";
2
3
  import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
3
4
  import { protocolOf } from "../protocol-table.js";
4
5
  import { renderSkillLinesWithin, renderSkillsBlock } from "./synthetic-tools.js";
@@ -24,6 +25,7 @@ export const CHANGED_FILES_MAX = 20;
24
25
  export const CHANGED_FILES_MTIME_EPS_MS = 2000;
25
26
  export const ATTACHMENT_BYTE_CAP = 8 * 1024;
26
27
  const PROJECTION_ITEMS_MAX = 50;
28
+ const PROJECTION_SCAN_MAX = 10_000;
27
29
  const PROJECTION_CONTENT_MAX = 80;
28
30
  const LISTING_FRAME_FLOOR_BYTES = 768;
29
31
  const DATE_CHANGE_MAX_BYTES = 160;
@@ -72,52 +74,55 @@ export function createAttachmentState() {
72
74
  postCompactPending: false,
73
75
  };
74
76
  }
77
+ function readListLength(value) {
78
+ const n = value !== null && typeof value === "object" ? readMember(value, "length") : undefined;
79
+ return typeof n === "number" && Number.isInteger(n) && n >= 0 ? Math.min(n, PROJECTION_SCAN_MAX) : undefined;
80
+ }
81
+ function readTaskEntry(raw) {
82
+ const id = readMember(raw, "id");
83
+ if (typeof id !== "string")
84
+ return undefined;
85
+ const subject = readMember(raw, "subject");
86
+ const status = readMember(raw, "status");
87
+ return { id, item: { content: typeof subject === "string" ? subject.slice(0, PROJECTION_CONTENT_MAX) : "", status: typeof status === "string" ? status : "pending" } };
88
+ }
75
89
  export function reduceToolEnd(state, details) {
76
- if (details === null || typeof details !== "object")
77
- return;
78
- const d = details;
79
- if (d.type === "todo") {
80
- const newTodos = d.newTodos;
81
- if (!Array.isArray(newTodos))
90
+ const type = readMember(details, "type");
91
+ if (type === "todo") {
92
+ const newTodos = readMember(details, "newTodos");
93
+ const count = readListLength(newTodos);
94
+ if (count === undefined)
82
95
  return;
83
- const shaped = newTodos.flatMap((t) => {
84
- const item = t;
85
- return item && typeof item === "object" && typeof item.content === "string" && typeof item.status === "string"
86
- ? [{ content: item.content.slice(0, PROJECTION_CONTENT_MAX), status: item.status }]
87
- : [];
88
- });
96
+ const shaped = [];
97
+ for (let i = 0; i < count; i++) {
98
+ const item = readMember(newTodos, String(i));
99
+ const content = readMember(item, "content");
100
+ const status = readMember(item, "status");
101
+ if (typeof content === "string" && typeof status === "string")
102
+ shaped.push({ content: content.slice(0, PROJECTION_CONTENT_MAX), status });
103
+ }
89
104
  const allDone = shaped.length > 0 && shaped.every((t) => t.status === "completed");
90
105
  state.todo = allDone ? { items: [] } : { items: shaped.slice(0, PROJECTION_ITEMS_MAX) };
91
106
  }
92
- else if (d.type === "task") {
107
+ else if (type === "task") {
93
108
  const map = state.taskItems ?? (state.taskItems = new Map());
94
- const deleted = d.deleted;
109
+ const deleted = readMember(details, "deleted");
95
110
  if (typeof deleted === "string")
96
111
  map.delete(deleted);
97
- const task = d.task;
98
- if (task !== null && typeof task === "object") {
99
- const t = task;
100
- if (typeof t.id === "string") {
101
- map.set(t.id, {
102
- content: typeof t.subject === "string" ? t.subject.slice(0, PROJECTION_CONTENT_MAX) : "",
103
- status: typeof t.status === "string" ? t.status : "pending",
104
- });
105
- }
106
- }
107
- }
108
- else if (d.type === "task-list") {
109
- const tasks = d.tasks;
110
- if (!Array.isArray(tasks))
112
+ const entry = readTaskEntry(readMember(details, "task"));
113
+ if (entry !== undefined)
114
+ map.set(entry.id, entry.item);
115
+ }
116
+ else if (type === "task-list") {
117
+ const tasks = readMember(details, "tasks");
118
+ const count = readListLength(tasks);
119
+ if (count === undefined)
111
120
  return;
112
121
  const map = new Map();
113
- for (const raw of tasks) {
114
- const t = raw;
115
- if (t && typeof t === "object" && typeof t.id === "string") {
116
- map.set(t.id, {
117
- content: typeof t.subject === "string" ? t.subject.slice(0, PROJECTION_CONTENT_MAX) : "",
118
- status: typeof t.status === "string" ? t.status : "pending",
119
- });
120
- }
122
+ for (let i = 0; i < count; i++) {
123
+ const entry = readTaskEntry(readMember(tasks, String(i)));
124
+ if (entry !== undefined)
125
+ map.set(entry.id, entry.item);
121
126
  }
122
127
  state.taskItems = map;
123
128
  }
@@ -1,5 +1,6 @@
1
1
  import { InMemorySessionRepo, SessionError, uuidv7, } from "../internal/harness.js";
2
2
  import { isSessionConflict } from "./session.js";
3
+ import { readMember } from "./thrown-value.js";
3
4
  const DAY_MS = 24 * 60 * 60 * 1000;
4
5
  import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
5
6
  export { SESSION_DEFAULT_TTL_DAYS };
@@ -285,8 +286,5 @@ async function settledForkOptions(source) {
285
286
  return {};
286
287
  }
287
288
  function isNotFound(err) {
288
- return (typeof err === "object" &&
289
- err !== null &&
290
- "code" in err &&
291
- err.code === "not_found");
289
+ return readMember(err, "code") === "not_found";
292
290
  }
@@ -8,7 +8,10 @@ export type { SessionWriteOptions } from "../internal/harness.js";
8
8
  import type { Session } from "../internal/harness.js";
9
9
  import type { SessionTreeEntry } from "../internal/harness.js";
10
10
  export type { SessionPlacementRecord } from "../internal/harness.js";
11
- /** True when `err` is a session optimistic-lock conflict (a concurrent writer won the branch leaf). */
11
+ /** True when `err` is a session optimistic-lock conflict (a concurrent writer won the branch leaf). Both
12
+ * halves read what a HOST `SessionStorage` threw, so both are guarded — unreadable is not a conflict,
13
+ * never a raise inside the classify-and-rethrow guard. The class test is KEPT: widening the verdict to
14
+ * any value carrying the word would arm the CAS retry loop on a foreign error (KL-942b). */
12
15
  export declare function isSessionConflict(err: unknown): boolean;
13
16
  /**
14
17
  * Subagent transcript persistence — the ACQUIRE-form placement declaration a trusted spawner passes
@@ -3,8 +3,9 @@ export { InMemorySessionRepo, InMemorySessionStorage, uuidv7, validateEntriesFor
3
3
  export { BaseSessionStorage, leafIdAfterEntry, } from "../internal/harness.js";
4
4
  export { SessionError } from "../internal/harness.js";
5
5
  import { SessionError } from "../internal/harness.js";
6
+ import { readMember, thrownIs } from "./thrown-value.js";
6
7
  export function isSessionConflict(err) {
7
- return err instanceof SessionError && err.code === "conflict";
8
+ return thrownIs(err, SessionError) && readMember(err, "code") === "conflict";
8
9
  }
9
10
  export function hasSessionFork(store) {
10
11
  return typeof store.fork === "function";
@@ -592,16 +592,23 @@ export type TaskEvent = ({
592
592
  type: "compacted";
593
593
  trigger: "auto" | "manual" | "forced";
594
594
  tokensBefore: number;
595
- /** TB telemetry B4: post-compaction context size in the TRIGGER-GATE coordinate
596
- * (`postTriggerTokens` = estimate + anchor-less overhead). Pairs with {@link triggerTokensBefore}
597
- * — NOT with `tokensBefore` (which is the CC `pre_tokens` value in prepareCompaction's coordinate,
598
- * lacking the overhead term; subtracting across the two coordinates would mis-state the delta,
599
- * the audit-B-1 / P-13 class). Absent when the post measurement failed (best-effort, never fails a
600
- * successful compaction). */
595
+ /** How much context this compaction FREED, as the engine measured it: one structural sum over
596
+ * the pre-messages minus the same sum over the post-messages, inside the pass. **The only
597
+ * subtractable number on this event**, and what a context meter should render. Clamped ≥ 0 at
598
+ * the mint, so `0` means "freed nothing", never "could not tell". Absent ⇒ the post
599
+ * measurement failed and no delta exists — never a stand-in. */
600
+ freedTokens?: number;
601
+ /** DIAGNOSTIC, NOT subtractable. Post-compaction size in the TRIGGER-GATE coordinate
602
+ * (structural estimate + the fixed prompt overhead). {@link triggerTokensBefore} is not its
603
+ * matching pre-value: the gate reads a usage-ANCHORED figure whenever the provider's last
604
+ * usage is fresh, while this is always structural + overhead, so the difference mixes
605
+ * coordinates and goes NEGATIVE on ordinary auto passes (3037 − 4591 = −1554, measured on a
606
+ * 4k window). Use {@link freedTokens}. Absent when the post measurement failed. */
601
607
  tokensAfter?: number;
602
- /** TB telemetry B4: pre-compaction size in the SAME trigger-gate coordinate as {@link tokensAfter}
603
- * (`triggerTokens` — the exact quantity the compaction gate compared). `triggerTokensBefore −
604
- * tokensAfter` is the self-consistent freed delta. Absent on the legacy emitter. */
608
+ /** DIAGNOSTIC, NOT subtractable. The quantity the gate compared against its threshold —
609
+ * usage-anchored when the anchor is fresh, structural + overhead when it is not. It answers
610
+ * WHAT tripped the trigger, a different question from how much was freed; left un-clamped and
611
+ * un-paired rather than made to look subtractable. Absent on the legacy emitter. */
605
612
  triggerTokensBefore?: number;
606
613
  /** TB telemetry B4: wall time of the whole compaction pass (summary call included) — what the
607
614
  * boundary pause actually cost. Absent on legacy emitters. */
@@ -823,6 +830,9 @@ export type TaskEvent = ({
823
830
  * Live/ephemeral — NOT persisted (a resumable trace re-derives usage from the result), like `status`.
824
831
  * Maps to CC `SDKTaskProgress` (camelCase here → snake_case `task_id`/`total_tokens`/`tool_uses`/`duration_ms`
825
832
  * at the SDK/service projection, the same convention as `toolCallId`→`tool_use_id`).
833
+ * `taskId` is the child RUN's id (`spec.taskId ?? sessionId`), NOT the registry handle: the
834
+ * background lanes launch with no explicit `spec.taskId`, so it equals the child's session id and
835
+ * hence its `transcriptId`. Both domains are tabled in `docs/INTEGRATION-CORE.md` §1.6a.
826
836
  */
827
837
  type: "task_progress";
828
838
  taskId: string;
@@ -227,6 +227,7 @@ export function formatWorkflowRun(run) {
227
227
  : {}),
228
228
  ...(run.journalSkips !== undefined && run.journalSkips > 0 ? { journal_skipped: run.journalSkips } : {}),
229
229
  ...(run.status === "failed" && run.error ? { error: delimitUntrusted("workflow error", boundedRedactedSummary(run.error, 500)) } : {}),
230
+ ...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
230
231
  ...(run.status === "completed" && run.result !== undefined
231
232
  ? { result: delimitUntrusted("workflow result", run.resultFull ?? run.result) }
232
233
  : {}),
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Is this an Error, including one minted in ANOTHER REALM?
3
3
  *
4
- * `instanceof` compares against THIS realm's `Error.prototype`, so a genuine `Error` thrown by a script the
5
- * engine ran inside `vm.createContext` — a workflow script is exactly that — fails the test and would be
4
+ * `instanceof` compares against THIS realm's `Error.prototype`, so a genuine `Error` minted outside it — by
5
+ * a script the engine ran through a deployment-supplied runner, or a duplicated module instance — fails and would be
6
6
  * described as an anonymous object, losing the message that is the entire point of reporting it. The brand
7
7
  * check is the realm-independent one: an object with the `[[ErrorData]]` internal slot answers `[object Error]`
8
8
  * no matter where it was made, and a proxy forwards it. A plain object can forge the tag through
@@ -13,21 +13,28 @@
13
13
  * proxy traps, and the tag is a getter that can throw.
14
14
  */
15
15
  export declare function isErrorLike(value: unknown): boolean;
16
+ /** `value instanceof ctor`, TOTAL — the class half of {@link readMember}'s rule. The prototype-chain walk
17
+ * runs two proxy traps and a revoked proxy throws on both, while every caller tests a value someone else
18
+ * threw inside the handler that decides how a failure settles. Unrecognizable is not that class, and
19
+ * {@link isErrorLike} is this same rule for `Error`. */
20
+ export declare function thrownIs<T>(value: unknown, ctor: abstract new (...args: never[]) => T): value is T;
21
+ /** The NAME a thrown value carries (`Error.prototype.name` and its subclasses' overrides), read
22
+ * realm-independently and guarded — `undefined` when the value is not error-like or its `name` is not a
23
+ * string. Callers that compare against a name CONSTANT need exactly this: the comparison is on the WORD
24
+ * precisely because the value crossed a seam, so binding the test to this realm's `Error.prototype`
25
+ * defeats it. Whether an EMPTY name folds to `undefined` is policy and stays at the call site. */
26
+ export declare function thrownName(err: unknown): string | undefined;
16
27
  /**
17
28
  * Turning a value that was THROWN at us into text — the ONE rule, for the whole engine.
18
29
  *
19
- * The idiom this replaces is `err instanceof Error ? err.message : String(err)` (and its template-literal
20
- * twin, `` `${err}` ``). Both spell a PARTIAL function on the non-Error arm: `String` calls the value's own
21
- * `toString` / `Symbol.toPrimitive`, so it throws on a value that has neither (a null-prototype object) and
22
- * on one whose own hook throws. The thrower is not ours — a deployment hook, a plugin callback, a model-
23
- * supplied tool body may reject with any value at all — so the rendering step was the one step that could
24
- * destroy the report it exists to produce. Where the render sat inside a promise rejection handler, the
25
- * resulting `TypeError` was an unhandled rejection and took the whole process down: a caller's choice of
26
- * rejection value decided whether the engine stayed alive.
30
+ * The idiom it replaces — `err instanceof Error ? err.message : String(err)` — is PARTIAL on its non-Error
31
+ * arm: `String` calls the value's own `toString` / `Symbol.toPrimitive`, so it throws on a value that has
32
+ * neither and on one whose hook throws. The thrower is not ours (a deployment hook, a plugin callback, a
33
+ * model-supplied tool body may reject with anything), so the rendering step was the one step that could
34
+ * destroy the report it exists to produce — and inside a promise rejection handler it took the process down.
27
35
  *
28
- * Totality is therefore the contract, and it is met the same way the engine's structural describer already
29
- * meets it: the value is never asked to say what it is. Three arms, in this order, chosen so that the
30
- * dominant shapes keep their EXACT previous bytes and only the shapes that used to be useless or fatal move:
36
+ * TOTALITY is therefore the contract, met the way the structural describer meets it: the value is never
37
+ * asked to say what it is. Three arms, ordered so the dominant shapes keep their EXACT previous bytes:
31
38
  * - a thrown STRING is its own message (a bare `throw "…"`), returned verbatim — the describer would quote
32
39
  * it, which is right when rejecting a config value and wrong when relaying a message someone threw;
33
40
  * - an ERROR hands over `message` verbatim — no cap, no one-lining, no quoting. This is the arm nearly all
@@ -90,6 +97,10 @@ export declare function asThrownError(err: unknown): Error;
90
97
  * is `undefined` for BOTH "absent" and "unreadable" — deliberately, because a caller that must tell those
91
98
  * apart is asking a question an untrusted value is not obliged to answer; what it does instead is fall back
92
99
  * to describing the value, which {@link thrownText} already does totally.
100
+ *
101
+ * The read walks the PROTOTYPE CHAIN deliberately — `name` lives on `Error.prototype`. `"__proto__"` is
102
+ * the one key where that differs from an own read, and its only caller is {@link readOwnMembers}, which
103
+ * passes it only when it IS an own key (a data property shadows the accessor; both readings agree).
93
104
  */
94
105
  export declare function readMember(value: unknown, key: string): unknown;
95
106
  /** The PLURAL of {@link readMember}: read a value's own enumerable members, once each, into an inert
@@ -9,6 +9,18 @@ export function isErrorLike(value) {
9
9
  return false;
10
10
  }
11
11
  }
12
+ export function thrownIs(value, ctor) {
13
+ try {
14
+ return value instanceof ctor;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export function thrownName(err) {
21
+ const name = isErrorLike(err) ? readMember(err, "name") : undefined;
22
+ return typeof name === "string" ? name : undefined;
23
+ }
12
24
  export function thrownText(err) {
13
25
  try {
14
26
  if (typeof err === "string")
@@ -66,7 +78,7 @@ export function readOwnMembers(value) {
66
78
  }
67
79
  const out = {};
68
80
  for (const key of keys)
69
- out[key] = readMember(value, key);
81
+ Object.defineProperty(out, key, { value: readMember(value, key), enumerable: true, writable: true, configurable: true });
70
82
  return out;
71
83
  }
72
84
  export function thrownMarksOf(err) {
@@ -33,6 +33,12 @@ export declare function truncateError(s: string): string;
33
33
  * budget of one character. */
34
34
  export declare const OUTPUT_BUDGET_MIN_CHARS = 512;
35
35
  export declare function clipWithFilePointer(s: string, limit: number, fullOutputPath?: string): string;
36
+ /**
37
+ * Format a thrown tool error into model-facing text (design/64 §11.1/§11.2): a shell/exec error becomes
38
+ * `Exit code N` + stderr + stdout; a plain Error becomes its message (+ any captured streams); anything
39
+ * else is stringified. The result is truncated to {@link MAX_ERROR_CHARS}. An AbortError is reported as an
40
+ * interruption.
41
+ */
36
42
  export declare function formatToolError(error: unknown): string;
37
43
  /**
38
44
  * A typed, read-side fold of a `TaskResult.errorCode` into the failure CLASS it belongs to (design/80
@@ -2,7 +2,7 @@ import { Value } from "typebox/value";
2
2
  const MAX_ERROR_CHARS = 10_000;
3
3
  const HALF = MAX_ERROR_CHARS / 2;
4
4
  import { isHighSurrogate, isLowSurrogate, sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js";
5
- import { readMember, thrownText } from "./thrown-value.js";
5
+ import { isErrorLike, readMember, thrownName, thrownText } from "./thrown-value.js";
6
6
  export function truncateError(s) {
7
7
  if (s.length <= MAX_ERROR_CHARS)
8
8
  return s;
@@ -39,18 +39,10 @@ function isShellLike(e) {
39
39
  return false;
40
40
  }
41
41
  }
42
- function isErrorInstance(v) {
43
- try {
44
- return v instanceof Error;
45
- }
46
- catch {
47
- return false;
48
- }
49
- }
50
42
  export function formatToolError(error) {
51
43
  const parts = [];
52
- const name = readMember(error, "name");
53
- if (isErrorInstance(error) && name === "AbortError")
44
+ const name = thrownName(error);
45
+ if (name === "AbortError")
54
46
  return "Interrupted";
55
47
  const message = readMember(error, "message");
56
48
  const messageText = typeof message === "string" ? message : undefined;
@@ -60,15 +52,15 @@ export function formatToolError(error) {
60
52
  const stdout = readMember(error, "stdout");
61
53
  if (typeof exitCode === "number")
62
54
  parts.push(`Exit code ${exitCode}`);
63
- if (isErrorInstance(error) && messageText !== undefined && messageText !== "")
55
+ if (isErrorLike(error) && messageText !== undefined && messageText !== "")
64
56
  parts.push(messageText);
65
57
  if (typeof stderr === "string" && stderr.trim())
66
58
  parts.push(stderr);
67
59
  if (typeof stdout === "string" && stdout.trim())
68
60
  parts.push(stdout);
69
61
  }
70
- else if (isErrorInstance(error)) {
71
- parts.push((messageText ?? "") || (typeof name === "string" ? name : "") || "Error");
62
+ else if (isErrorLike(error)) {
63
+ parts.push((messageText ?? "") || (name ?? "") || "Error");
72
64
  }
73
65
  else {
74
66
  parts.push(thrownText(error));
@@ -6,7 +6,7 @@ function normalizeContent(ret) {
6
6
  if (typeof ret === "string") {
7
7
  return { content: [{ type: "text", text: ret }], details: undefined, terminate: false, isError: false };
8
8
  }
9
- const raw = ret.content;
9
+ const raw = readMember(ret, "content");
10
10
  if (typeof raw !== "string" && !Array.isArray(raw)) {
11
11
  return {
12
12
  content: [{ type: "text", text: "Tool error: content must be a string or an array of content blocks." }],
@@ -16,7 +16,7 @@ function normalizeContent(ret) {
16
16
  };
17
17
  }
18
18
  const content = typeof raw === "string" ? [{ type: "text", text: raw }] : raw;
19
- return { content, details: ret.details, terminate: ret.terminate ?? false, isError: ret.isError === true };
19
+ return { content, details: readMember(ret, "details"), terminate: readMember(ret, "terminate") === true, isError: readMember(ret, "isError") === true };
20
20
  }
21
21
  function isEmptyToolContent(content) {
22
22
  if (content.length === 0)
@@ -108,11 +108,15 @@ export function defineTool(spec, options) {
108
108
  isError: true,
109
109
  };
110
110
  }
111
- let ret;
112
111
  try {
113
112
  const baseCtx = { toolCallId, signal };
114
113
  const ctx = options?.enrichCtx ? stampCallIdentity(options.enrichCtx(baseCtx), toolCallId, signal) : baseCtx;
115
- ret = await spec.execute(params, ctx);
114
+ const ret = await spec.execute(params, ctx);
115
+ const { content, details, terminate, isError } = normalizeContent(ret);
116
+ const guarded = isEmptyToolContent(content)
117
+ ? [{ type: "text", text: isError ? `(${spec.name} reported an error with no further output)` : `(${spec.name} completed with no output)` }]
118
+ : content;
119
+ return { content: guarded, details, terminate, ...(isError ? { isError: true } : {}) };
116
120
  }
117
121
  catch (err) {
118
122
  const wrapped = new Error(formatToolError(err));
@@ -124,11 +128,6 @@ export function defineTool(spec, options) {
124
128
  wrapped.errorKind = errorKind;
125
129
  throw wrapped;
126
130
  }
127
- const { content, details, terminate, isError } = normalizeContent(ret);
128
- const guarded = isEmptyToolContent(content)
129
- ? [{ type: "text", text: isError ? `(${spec.name} reported an error with no further output)` : `(${spec.name} completed with no output)` }]
130
- : content;
131
- return { content: guarded, details, terminate, ...(isError ? { isError: true } : {}) };
132
131
  },
133
132
  };
134
133
  if (spec.contract) {
@@ -911,14 +911,14 @@ export interface RoutePairingConfig {
911
911
  export type RoutePairingPosture = "per-model" | "paired" | "unpinned" | "keyless";
912
912
  /** Machine codes an adjudication refusal carries (minted here, forwarded verbatim by consumers). */
913
913
  export type RouteRefusalCode = "route.credential_mismatch" | "route.credential_missing";
914
- /** The structured account of a refusal — enough for a caller to render the two halves and the fix. */
914
+ /** The structured account of a refusal — the two halves and the fix, every URL a DISPLAY spelling. */
915
915
  export interface RouteRefusalDetail {
916
916
  modelId: string;
917
- /** The entry's declared URL half (`Model.baseUrl`, normalized). */
917
+ /** The entry's declared URL half (`Model.baseUrl`), via `displayUrlOf` — NOT route identity. */
918
918
  entryBaseUrl: string;
919
- /** The root the request would actually target (normalized). */
919
+ /** The root the request would actually target, rendered (use `routeKeyOf` for identity). */
920
920
  requestBaseUrl: string;
921
- /** The deployment's declared root the credential is paired with (mismatch refusals). */
921
+ /** The declared root the credential is paired with (mismatch refusals), rendered. */
922
922
  configBaseUrl?: string;
923
923
  /** One-line configuration fix. */
924
924
  fixHint: string;
package/dist/index.d.ts CHANGED
@@ -325,7 +325,7 @@ export { createCircuitBreakerBrain, type CircuitBreakerOptions, type BreakerStat
325
325
  export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, type DegradingBrainOptions, type DegradeReason, type DegradationInfo, } from "./brain/degrading.js";
326
326
  export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
327
327
  export { WAITING_FIRST_TOKEN_AFTER_MS, WAITING_FIRST_TOKEN_EVERY_MS } from "./brain/stream-engine.js";
328
- export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, normalizeBaseUrl, hasAuthCarrier, type RoutePairingStatus, } from "./brain/route-adjudicator.js";
328
+ export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, routeKeyOf, displayUrlOf, resolveRequestRoot, hasAuthCarrier, type RoutePairingStatus, } from "./brain/route-adjudicator.js";
329
329
  export type { RouteAdjudication, RouteCredential, RouteCredentialSource, RoutePairingConfig, RoutePairingPosture, RouteRefusalCode, RouteRefusalDetail, } from "./internal/llm.js";
330
330
  export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS, type RouteAdjudicationVector } from "./brain/route-conformance.js";
331
331
  export { type BrainTimeoutConfig } from "./brain/timeout.js";