zidane 5.3.0 → 5.3.2

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 (68) hide show
  1. package/README.md +2 -0
  2. package/dist/{agent-CYpPKn5Z.d.ts → agent-BXRCCHeq.d.ts} +557 -5
  3. package/dist/agent-BXRCCHeq.d.ts.map +1 -0
  4. package/dist/chat.d.ts +310 -6
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +2 -2
  7. package/dist/{errors-COmsomd5.js → errors-Byb0F8B9.js} +44 -2
  8. package/dist/errors-Byb0F8B9.js.map +1 -0
  9. package/dist/{index-D-cTScN3.d.ts → index-BPk8-Slm.d.ts} +81 -10
  10. package/dist/index-BPk8-Slm.d.ts.map +1 -0
  11. package/dist/{index-Cc-q1hLT.d.ts → index-CT5_p-3P.d.ts} +2 -2
  12. package/dist/{index-Cc-q1hLT.d.ts.map → index-CT5_p-3P.d.ts.map} +1 -1
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +10 -10
  15. package/dist/{interpolate-BhmHKD6x.js → interpolate-ERgZUxgg.js} +2 -2
  16. package/dist/{interpolate-BhmHKD6x.js.map → interpolate-ERgZUxgg.js.map} +1 -1
  17. package/dist/{login-BXVt5wuA.js → login-DrBZ15G7.js} +3 -3
  18. package/dist/{login-BXVt5wuA.js.map → login-DrBZ15G7.js.map} +1 -1
  19. package/dist/{mcp-B1psg7jf.js → mcp-DhmmJfxK.js} +16 -3
  20. package/dist/mcp-DhmmJfxK.js.map +1 -0
  21. package/dist/mcp.d.ts +1 -1
  22. package/dist/mcp.js +1 -1
  23. package/dist/{messages-DsbMYNmt.js → messages-D0xT979U.js} +631 -68
  24. package/dist/messages-D0xT979U.js.map +1 -0
  25. package/dist/{presets-tvD28pCu.js → presets-0_IRJAYF.js} +29 -10
  26. package/dist/presets-0_IRJAYF.js.map +1 -0
  27. package/dist/presets.d.ts +2 -2
  28. package/dist/presets.js +1 -1
  29. package/dist/{providers-v1Rn2rqG.js → providers-x3LZByR5.js} +38 -6
  30. package/dist/providers-x3LZByR5.js.map +1 -0
  31. package/dist/providers.d.ts +2 -2
  32. package/dist/providers.js +3 -3
  33. package/dist/session/sqlite.d.ts +1 -1
  34. package/dist/session/sqlite.js +1 -1
  35. package/dist/{session-DOJgRXvF.js → session-BHZwxmfr.js} +2 -2
  36. package/dist/{session-DOJgRXvF.js.map → session-BHZwxmfr.js.map} +1 -1
  37. package/dist/session.d.ts +1 -1
  38. package/dist/session.js +2 -2
  39. package/dist/skills.d.ts +2 -2
  40. package/dist/skills.js +1 -1
  41. package/dist/{tools-CMVruxF0.js → tools-CCsL5SCO.js} +516 -140
  42. package/dist/tools-CCsL5SCO.js.map +1 -0
  43. package/dist/tools.d.ts +3 -3
  44. package/dist/tools.js +2 -2
  45. package/dist/{transcript-anchors-eyhlGeBI.d.ts → transcript-anchors-DSk8LlWt.d.ts} +28 -4
  46. package/dist/transcript-anchors-DSk8LlWt.d.ts.map +1 -0
  47. package/dist/tui.d.ts +29 -3
  48. package/dist/tui.d.ts.map +1 -1
  49. package/dist/tui.js +365 -80
  50. package/dist/tui.js.map +1 -1
  51. package/dist/{turn-operations-Y7e15gJf.js → turn-operations-CutZin8X.js} +678 -33
  52. package/dist/turn-operations-CutZin8X.js.map +1 -0
  53. package/dist/types-IcokUOyC.js.map +1 -1
  54. package/dist/types.d.ts +2 -2
  55. package/dist/types.js +1 -1
  56. package/docs/ARCHITECTURE.md +1 -1
  57. package/docs/SKILL.md +23 -1
  58. package/package.json +1 -1
  59. package/dist/agent-CYpPKn5Z.d.ts.map +0 -1
  60. package/dist/errors-COmsomd5.js.map +0 -1
  61. package/dist/index-D-cTScN3.d.ts.map +0 -1
  62. package/dist/mcp-B1psg7jf.js.map +0 -1
  63. package/dist/messages-DsbMYNmt.js.map +0 -1
  64. package/dist/presets-tvD28pCu.js.map +0 -1
  65. package/dist/providers-v1Rn2rqG.js.map +0 -1
  66. package/dist/tools-CMVruxF0.js.map +0 -1
  67. package/dist/transcript-anchors-eyhlGeBI.d.ts.map +0 -1
  68. package/dist/turn-operations-Y7e15gJf.js.map +0 -1
@@ -1,9 +1,9 @@
1
1
  import { n as createProcessContext } from "./contexts-BwiHIr2w.js";
2
- import { c as toTypedError, o as errorMessage, r as AgentProviderError, t as AgentAbortedError } from "./errors-COmsomd5.js";
2
+ import { a as AgentToolPairingError, l as toTypedError, r as AgentProviderError, s as errorMessage, t as AgentAbortedError } from "./errors-Byb0F8B9.js";
3
3
  import { t as toolOutputByteLength } from "./types-IcokUOyC.js";
4
- import { i as sanitizeOrphanedToolCalls } from "./messages-DsbMYNmt.js";
5
- import { t as connectMcpServers } from "./mcp-B1psg7jf.js";
6
- import { _ as validateResourcePath, b as createSkillActivationState, d as escapeXml, n as resolveSkills, p as installAllowedToolsGate, t as interpolateShellCommands, u as buildCatalog } from "./interpolate-BhmHKD6x.js";
4
+ import { a as detectTurnInterruption, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureToolResultPairing, s as filterUnresolvedToolUses } from "./messages-D0xT979U.js";
5
+ import { t as connectMcpServers } from "./mcp-DhmmJfxK.js";
6
+ import { _ as validateResourcePath, b as createSkillActivationState, d as escapeXml, n as resolveSkills, p as installAllowedToolsGate, t as interpolateShellCommands, u as buildCatalog } from "./interpolate-ERgZUxgg.js";
7
7
  import { n as formatTokenUsage, t as flattenTurns } from "./stats-DgOvY7wd.js";
8
8
  import { createHooks } from "hookable";
9
9
  import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
@@ -107,8 +107,14 @@ function rewriteMessagesToWire(messages, maps) {
107
107
  const STATE = /* @__PURE__ */ new WeakMap();
108
108
  /**
109
109
  * Get or lazily create the per-session read-state map. Returns `undefined`
110
- * when no session is provided — tools should treat that as "no dedup, no
111
- * guard": the state has nowhere to live, so every read is fresh.
110
+ * when no session is provided.
111
+ *
112
+ * Most tool callers should prefer {@link resolveReadStateMap}, which
113
+ * additionally honors an explicit `ctx.readState` (the path
114
+ * `spawn`'s `shareReadState: true` uses to forward the parent's map
115
+ * into a sessionless child). Use this helper directly only when the
116
+ * Session-keyed map is exactly what you want and you don't need to
117
+ * accept an override.
112
118
  */
113
119
  function getReadState(session) {
114
120
  if (!session) return void 0;
@@ -119,19 +125,38 @@ function getReadState(session) {
119
125
  }
120
126
  return map;
121
127
  }
122
- const TOOL_DEDUP_STATE = /* @__PURE__ */ new WeakMap();
123
128
  /**
124
- * Get or lazily create the per-session tool-dedup map. Returns `undefined`
125
- * when no session is provided middleware should treat that as "no dedup".
129
+ * Resolve the active read-state map from a tool context. An explicit
130
+ * `ctx.readState` wins (used by `spawn`'s `shareReadState` opt-in to
131
+ * forward the parent's map into a sessionless child); otherwise the
132
+ * usual `Session`-keyed lookup applies.
133
+ *
134
+ * Tools should call this helper instead of `getReadState(ctx.session)`
135
+ * directly so the `shareReadState` plumbing is honored uniformly.
126
136
  */
127
- function getToolDedupState(session) {
128
- if (!session) return void 0;
129
- let map = TOOL_DEDUP_STATE.get(session);
130
- if (!map) {
131
- map = /* @__PURE__ */ new Map();
132
- TOOL_DEDUP_STATE.set(session, map);
133
- }
134
- return map;
137
+ function resolveReadStateMap(ctx) {
138
+ return ctx.readState ?? getReadState(ctx.session);
139
+ }
140
+ /**
141
+ * Canonical read-state key for a `(cwd, path)` pair.
142
+ *
143
+ * `ctx.execution.readFile(handle, path)` resolves the model-provided
144
+ * path against `handle.cwd` before touching disk — so `src/App.tsx`,
145
+ * `./src/App.tsx`, and `/abs/cwd/src/App.tsx` all read the **same**
146
+ * bytes. The read-state map MUST mirror that resolution: without it,
147
+ * a model that reads `src/App.tsx` and then edits `./src/App.tsx`
148
+ * trips the `requireReadBeforeEdit` gate for a file it demonstrably
149
+ * already saw (the gate's "has not been read in this session" message
150
+ * fires on a stale key).
151
+ *
152
+ * `node:path`'s `resolve(cwd, path)` short-circuits when `path` is
153
+ * already absolute, so absolute and relative shapes converge on the
154
+ * same canonical form. We don't `realpath()` symlinks — the file IS
155
+ * the path the model addressed, not the realpath behind it; the host's
156
+ * execution context decides how to dereference.
157
+ */
158
+ function readStateKey(cwd, path) {
159
+ return resolve(cwd, path);
135
160
  }
136
161
  /**
137
162
  * FNV-1a 32-bit hash, hex-encoded. Fast, non-cryptographic — we only need
@@ -147,6 +172,20 @@ function hashContent(text) {
147
172
  }
148
173
  return h.toString(16).padStart(8, "0");
149
174
  }
175
+ const TOOL_DEDUP_STATE = /* @__PURE__ */ new WeakMap();
176
+ /**
177
+ * Get or lazily create the per-session tool-dedup map. Returns `undefined`
178
+ * when no session is provided — middleware should treat that as "no dedup".
179
+ */
180
+ function getToolDedupState(session) {
181
+ if (!session) return void 0;
182
+ let map = TOOL_DEDUP_STATE.get(session);
183
+ if (!map) {
184
+ map = /* @__PURE__ */ new Map();
185
+ TOOL_DEDUP_STATE.set(session, map);
186
+ }
187
+ return map;
188
+ }
150
189
  //#endregion
151
190
  //#region src/dedup-tools.ts
152
191
  /**
@@ -480,6 +519,7 @@ function validateToolArgs(input, schema) {
480
519
  };
481
520
  let coerced;
482
521
  const coercions = [];
522
+ let droppedItems;
483
523
  for (const [key, value] of Object.entries(input)) {
484
524
  const propSchema = properties[key];
485
525
  if (!propSchema?.type) continue;
@@ -494,11 +534,138 @@ function validateToolArgs(input, schema) {
494
534
  coerced[key] = outcome.value;
495
535
  coercions.push(key);
496
536
  }
537
+ const arrayValue = outcome.changed ? outcome.value : value;
538
+ if (propSchema.type === "array" && propSchema.items && Array.isArray(arrayValue)) {
539
+ const itemOutcome = validateArrayItems(arrayValue, propSchema);
540
+ if (itemOutcome.error) return {
541
+ valid: false,
542
+ error: `Field "${key}": ${itemOutcome.error}`
543
+ };
544
+ if (itemOutcome.changed || itemOutcome.dropped.length > 0 || itemOutcome.truncated) {
545
+ if (!coerced) coerced = { ...input };
546
+ coerced[key] = itemOutcome.items;
547
+ if (!coercions.includes(key)) coercions.push(key);
548
+ }
549
+ if (itemOutcome.dropped.length > 0) {
550
+ if (!droppedItems) droppedItems = {};
551
+ droppedItems[key] = itemOutcome.dropped;
552
+ }
553
+ }
497
554
  }
498
555
  return {
499
556
  valid: true,
500
557
  coercedInput: coerced ?? input,
501
- coercions
558
+ coercions,
559
+ ...droppedItems ? { droppedItems } : {}
560
+ };
561
+ }
562
+ function validateArrayItems(items, schema) {
563
+ if (schema.minItems !== void 0 && items.length < schema.minItems) return {
564
+ items,
565
+ changed: false,
566
+ truncated: false,
567
+ dropped: [],
568
+ error: `expected at least ${schema.minItems} item${schema.minItems === 1 ? "" : "s"}, got ${items.length}`
569
+ };
570
+ const itemSchema = schema.items;
571
+ if (!itemSchema) return {
572
+ items,
573
+ changed: false,
574
+ truncated: false,
575
+ dropped: []
576
+ };
577
+ const out = [];
578
+ const outOriginalIdx = [];
579
+ const dropped = [];
580
+ let changed = false;
581
+ for (let i = 0; i < items.length; i++) {
582
+ const item = items[i];
583
+ const v = validateOneItem(item, itemSchema);
584
+ if (v.dropped) {
585
+ dropped.push(i);
586
+ changed = true;
587
+ continue;
588
+ }
589
+ if (v.changed) changed = true;
590
+ out.push(v.value);
591
+ outOriginalIdx.push(i);
592
+ }
593
+ let truncated = false;
594
+ if (schema.maxItems !== void 0 && out.length > schema.maxItems) {
595
+ for (let i = schema.maxItems; i < out.length; i++) dropped.push(outOriginalIdx[i]);
596
+ out.length = schema.maxItems;
597
+ truncated = true;
598
+ changed = true;
599
+ }
600
+ dropped.sort((a, b) => a - b);
601
+ return {
602
+ items: out,
603
+ changed,
604
+ truncated,
605
+ dropped
606
+ };
607
+ }
608
+ function validateOneItem(item, schema) {
609
+ if (schema.type === "object") {
610
+ if (!item || typeof item !== "object" || Array.isArray(item)) return {
611
+ value: item,
612
+ changed: false,
613
+ dropped: true
614
+ };
615
+ const obj = item;
616
+ const required = schema.required ?? [];
617
+ for (const field of required) {
618
+ const v = obj[field];
619
+ if (v === void 0 || v === null) return {
620
+ value: item,
621
+ changed: false,
622
+ dropped: true
623
+ };
624
+ }
625
+ const properties = schema.properties ?? {};
626
+ let coercedItem;
627
+ for (const [key, value] of Object.entries(obj)) {
628
+ const subSchema = properties[key];
629
+ if (!subSchema?.type) continue;
630
+ if (value === void 0 || value === null) continue;
631
+ const outcome = coerceValue(value, subSchema);
632
+ if (outcome.error) return {
633
+ value: item,
634
+ changed: false,
635
+ dropped: true
636
+ };
637
+ if (outcome.changed) {
638
+ if (!coercedItem) coercedItem = { ...obj };
639
+ coercedItem[key] = outcome.value;
640
+ }
641
+ }
642
+ return coercedItem ? {
643
+ value: coercedItem,
644
+ changed: true,
645
+ dropped: false
646
+ } : {
647
+ value: item,
648
+ changed: false,
649
+ dropped: false
650
+ };
651
+ }
652
+ if (schema.type) {
653
+ const outcome = coerceValue(item, schema);
654
+ if (outcome.error) return {
655
+ value: item,
656
+ changed: false,
657
+ dropped: true
658
+ };
659
+ return {
660
+ value: outcome.value,
661
+ changed: outcome.changed,
662
+ dropped: false
663
+ };
664
+ }
665
+ return {
666
+ value: item,
667
+ changed: false,
668
+ dropped: false
502
669
  };
503
670
  }
504
671
  function coerceValue(value, schema) {
@@ -628,6 +795,30 @@ function formatValue(value) {
628
795
  //#region src/loop.ts
629
796
  const IMAGE_OMITTED_MARKER = "[image omitted — model does not support vision]";
630
797
  /**
798
+ * Canonical tool_result text emitted when a tool call is interrupted by the
799
+ * user mid-flight (Esc / Ctrl-C / external `AbortSignal`). Mirrors Claude
800
+ * Code's `INTERRUPT_MESSAGE_FOR_TOOL_USE` so downstream consumers can pattern
801
+ * match a single string across both harnesses. Always paired with
802
+ * `isError: true` on the wire — the model treats it as a failed call rather
803
+ * than a successful tool response.
804
+ */
805
+ const INTERRUPT_MESSAGE_FOR_TOOL_USE = "[Request interrupted by user for tool use]";
806
+ /**
807
+ * Canonical tool_result text emitted when a tool call is skipped because a
808
+ * sibling sequential call errored or a steering message arrived between
809
+ * iterations of {@link executeToolsSequential}. Distinguished from
810
+ * {@link INTERRUPT_MESSAGE_FOR_TOOL_USE} so consumers can distinguish "user
811
+ * cancelled" from "framework superseded".
812
+ */
813
+ const TOOL_USE_SKIPPED_MESSAGE = "[Tool use skipped — superseded by user message]";
814
+ /**
815
+ * Canonical tool_result text emitted when the loop catches a sequential
816
+ * sibling that threw and synthesizes follow-up results for the remaining
817
+ * queued calls. Distinct from {@link TOOL_USE_SKIPPED_MESSAGE} so telemetry
818
+ * can split "skipped by user steering" from "skipped after error".
819
+ */
820
+ const TOOL_USE_AFTER_ERROR_MESSAGE = "[Tool use skipped — previous tool call in batch threw]";
821
+ /**
631
822
  * Compute the effective thinking budget for a given run-relative turn, given
632
823
  * the configured decay schedule. Pure helper — exported for tests and so
633
824
  * downstream tooling can preview decay curves without spinning up the loop.
@@ -915,19 +1106,47 @@ function applyStaleReadElision(messages) {
915
1106
  }
916
1107
  /**
917
1108
  * Drop read-state entries for paths whose reads got elided. Keys are
918
- * `${cwd}::${path}` (see `src/tools/read-file.ts`); we don't know the
919
- * cwd in this context, so we match by `::<path>` suffix. Safe because
920
- * paths inside the read-state map come from a single session bound to
921
- * one cwd in practice; an accidental cross-cwd collision would also
922
- * have to share an identical relative path, in which case force-fresh
923
- * is the correct behavior anyway.
1109
+ * canonical absolute paths produced by `readStateKey(cwd, path)` (see
1110
+ * `src/tools/read-state.ts`); the loop has `ctx.handle.cwd` in scope,
1111
+ * so we re-derive each elided path's canonical key and delete by
1112
+ * direct lookup. No suffix matching, no cross-cwd ambiguity.
1113
+ *
1114
+ * Resolves the map via `resolveReadStateMap` so a child agent running
1115
+ * with a parent's shared map (via `shareReadState`) invalidates the
1116
+ * shared entries too — otherwise a child's `read → edit → re-read`
1117
+ * would dedup-hit on the now-stale parent entry.
924
1118
  */
925
- function invalidateReadStateForElidedPaths(session, elidedPaths) {
926
- if (!session || elidedPaths.length === 0) return;
927
- const readState = getReadState(session);
1119
+ function invalidateReadStateForElidedPaths(ctx, cwd, elidedPaths) {
1120
+ if (elidedPaths.length === 0) return;
1121
+ const readState = resolveReadStateMap(ctx);
928
1122
  if (!readState || readState.size === 0) return;
929
- const suffixes = elidedPaths.map((p) => `::${p}`);
930
- for (const key of [...readState.keys()]) if (suffixes.some((s) => key.endsWith(s))) readState.delete(key);
1123
+ for (const p of elidedPaths) readState.delete(readStateKey(cwd, p));
1124
+ }
1125
+ /**
1126
+ * Run {@link ensureToolResultPairing} with the loop's hook + strict-mode
1127
+ * context plugged in. Centralized so the pre-send path and the schema-
1128
+ * enforcement path share identical telemetry + throw semantics.
1129
+ *
1130
+ * The captured `repairs` array is what `AgentToolPairingError` carries on
1131
+ * strict-mode throws, and it's what `pairing:repair` fires from. Hook
1132
+ * notifications run AFTER the pass completes so they don't interfere with
1133
+ * the synchronous walk — and we drop them on the floor in strict mode (the
1134
+ * throw is more informative than a fire-and-forget log).
1135
+ */
1136
+ function applyPairingRepair(ctx, messages, turnId) {
1137
+ const repairs = [];
1138
+ const repaired = ensureToolResultPairing(messages, { onRepair: (repair) => repairs.push(repair) });
1139
+ if (repairs.length === 0) return repaired;
1140
+ if (ctx.strictToolPairing) throw new AgentToolPairingError({
1141
+ message: `Tool pairing corruption detected (${repairs.length} repair${repairs.length === 1 ? "" : "s"}); strict mode is on so the request was not sent.`,
1142
+ ...ctx.providerName ? { provider: ctx.providerName } : {},
1143
+ repairs
1144
+ });
1145
+ for (const repair of repairs) ctx.hooks.callHook("pairing:repair", {
1146
+ ...repair,
1147
+ turnId
1148
+ });
1149
+ return repaired;
931
1150
  }
932
1151
  function sanitizeStoredToolResults(provider, messages) {
933
1152
  if (provider.meta.capabilities?.vision !== false) return messages;
@@ -1060,13 +1279,34 @@ function wrapProviderError(err, ctx) {
1060
1279
  cause: err
1061
1280
  });
1062
1281
  }
1282
+ /** Max bytes of provider error text inlined into the assistant turn placeholder. */
1283
+ const ERROR_PLACEHOLDER_MAX = 280;
1284
+ /**
1285
+ * Build the assistant-turn placeholder text when the provider throws before
1286
+ * streaming any output. Inlines the underlying error message (truncated and
1287
+ * stripped of stack-like newlines) so the persisted turn carries a useful
1288
+ * diagnostic — the human reading the transcript sees the failure mode
1289
+ * without having to attach a debugger, and `tool_search` schema rejections
1290
+ * become self-explanatory.
1291
+ *
1292
+ * The bracketed `[✗ Streaming failed: ...]` shape preserves the prior
1293
+ * format that hosts may pattern-match on while adding the new payload
1294
+ * suffix. Falls back to the original generic placeholder when no message
1295
+ * can be extracted.
1296
+ */
1297
+ function buildStreamErrorPlaceholder(err) {
1298
+ const raw = errorMessage(err).trim();
1299
+ if (raw.length === 0) return "[✗ Streaming failed before any output.]";
1300
+ const oneLine = raw.replace(/\s+/g, " ");
1301
+ return `[✗ Streaming failed before any output: ${oneLine.length > ERROR_PLACEHOLDER_MAX ? `${oneLine.slice(0, ERROR_PLACEHOLDER_MAX - 1).trimEnd()}…` : oneLine}]`;
1302
+ }
1063
1303
  async function executeTurn(ctx, turn) {
1064
1304
  const turnId = await ctx.generateTurnId();
1065
1305
  let canonicalMessages = turnsToMessages(applyCompactSummaryCutoff(ctx.turns));
1066
1306
  if (ctx.elideStaleReads === true) {
1067
1307
  const elision = applyStaleReadElision(canonicalMessages);
1068
1308
  canonicalMessages = elision.messages;
1069
- invalidateReadStateForElidedPaths(ctx.session, elision.elidedPaths);
1309
+ invalidateReadStateForElidedPaths(ctx, ctx.handle.cwd, elision.elidedPaths);
1070
1310
  }
1071
1311
  const wireMessages = rewriteMessagesToWire(canonicalMessages, ctx.aliasMaps);
1072
1312
  let sanitizedMessages = sanitizeStoredToolResults(ctx.provider, wireMessages);
@@ -1091,7 +1331,7 @@ async function executeTurn(ctx, turn) {
1091
1331
  const transformCtx = { messages: streamOptions.messages };
1092
1332
  await ctx.hooks.callHook("context:transform", transformCtx);
1093
1333
  streamOptions.messages = transformCtx.messages;
1094
- streamOptions.messages = sanitizeOrphanedToolCalls(streamOptions.messages);
1334
+ streamOptions.messages = applyPairingRepair(ctx, streamOptions.messages, turnId);
1095
1335
  const systemCtx = {
1096
1336
  system: streamOptions.system,
1097
1337
  messages: streamOptions.messages,
@@ -1137,12 +1377,13 @@ async function executeTurn(ctx, turn) {
1137
1377
  input: 0,
1138
1378
  output: 0
1139
1379
  };
1380
+ const placeholderText = wasAborted ? "[⏹ Streaming was aborted.]" : buildStreamErrorPlaceholder(err);
1140
1381
  const errorContent = currentText ? [{
1141
1382
  type: "text",
1142
1383
  text: currentText
1143
1384
  }] : [{
1144
1385
  type: "text",
1145
- text: wasAborted ? "[⏹ Streaming was aborted.]" : "[✗ Streaming failed before any output.]"
1386
+ text: placeholderText
1146
1387
  }];
1147
1388
  const errorTurn = {
1148
1389
  id: turnId,
@@ -1153,6 +1394,10 @@ async function executeTurn(ctx, turn) {
1153
1394
  createdAt: Date.now()
1154
1395
  };
1155
1396
  ctx.turns.push(errorTurn);
1397
+ if (!wasAborted) await ctx.hooks.callHook("stream:error", {
1398
+ err,
1399
+ turnId
1400
+ });
1156
1401
  await ctx.hooks.callHook("turn:after", {
1157
1402
  turn,
1158
1403
  turnId,
@@ -1205,7 +1450,7 @@ async function executeTurn(ctx, turn) {
1205
1450
  description: "Return the final structured output matching the required schema.",
1206
1451
  inputSchema: ctx.schema
1207
1452
  };
1208
- const schemaMessages = sanitizeOrphanedToolCalls(rewriteMessagesToWire(turnsToMessages(applyCompactSummaryCutoff(ctx.turns)), ctx.aliasMaps));
1453
+ const schemaMessages = applyPairingRepair(ctx, rewriteMessagesToWire(turnsToMessages(applyCompactSummaryCutoff(ctx.turns)), ctx.aliasMaps), turnId);
1209
1454
  let schemaResult;
1210
1455
  try {
1211
1456
  schemaResult = await ctx.provider.stream({
@@ -1335,30 +1580,69 @@ function stripImagesForNonVision(provider, output) {
1335
1580
  if (provider.meta.capabilities?.vision !== false) return output;
1336
1581
  return output.map((b) => b.type === "image" ? IMAGE_OMITTED_MARKER : b.text).join("\n");
1337
1582
  }
1583
+ /**
1584
+ * Build the per-call base for every `tool:*` hook ctx (and the
1585
+ * matching shape for `mcp:tool:*`). Centralized so the `runId` /
1586
+ * `parentRunId` / `depth` identity fields land uniformly on every
1587
+ * event the loop fires — without one helper they drift across ~14
1588
+ * inline construction sites. The returned object IS the
1589
+ * {@link ToolHookContext} canonical shape; specialized hook payloads
1590
+ * (`gateCtx`, `transformCtx`, etc.) spread it and append their own
1591
+ * fields.
1592
+ */
1593
+ function buildToolHookBase(ctx, turnId, callId, name, displayName, input) {
1594
+ return {
1595
+ turnId,
1596
+ callId,
1597
+ name,
1598
+ displayName,
1599
+ input,
1600
+ ...ctx.runId !== void 0 ? { runId: ctx.runId } : {},
1601
+ ...ctx.parentRunId !== void 0 ? { parentRunId: ctx.parentRunId } : {},
1602
+ ...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
1603
+ };
1604
+ }
1338
1605
  async function executeSingleTool(ctx, call, turnId) {
1339
1606
  const toolDef = ctx.tools[call.name];
1340
1607
  const callId = call.id;
1341
1608
  const displayName = toWireName(call.name, ctx.aliasMaps);
1342
1609
  const runToolCounts = Object.freeze({ ...ctx.runToolCounts });
1343
1610
  const gateCtx = {
1344
- turnId,
1345
- callId,
1346
- name: call.name,
1347
- displayName,
1348
- input: call.input,
1611
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, call.input),
1349
1612
  block: false,
1350
1613
  reason: "Tool execution was blocked",
1351
1614
  runToolCounts
1352
1615
  };
1353
1616
  await ctx.hooks.callHook("tool:gate", gateCtx);
1354
- if (gateCtx.block) return { result: {
1355
- id: callId,
1356
- content: `Blocked: ${gateCtx.reason}`
1357
- } };
1617
+ if (gateCtx.block) {
1618
+ await fireDispatched(ctx, {
1619
+ turnId,
1620
+ callId,
1621
+ name: call.name,
1622
+ displayName,
1623
+ input: gateCtx.input,
1624
+ outcome: "gate-block",
1625
+ reason: gateCtx.reason,
1626
+ runToolCounts
1627
+ });
1628
+ return { result: {
1629
+ id: callId,
1630
+ content: `Blocked: ${gateCtx.reason}`,
1631
+ isError: true
1632
+ } };
1633
+ }
1358
1634
  ctx.runToolCounts[call.name] = (ctx.runToolCounts[call.name] ?? 0) + 1;
1359
- if (gateCtx.result !== void 0) return { result: {
1360
- id: callId,
1361
- content: await emitToolResult(ctx, {
1635
+ if (gateCtx.result !== void 0) {
1636
+ await fireDispatched(ctx, {
1637
+ turnId,
1638
+ callId,
1639
+ name: call.name,
1640
+ displayName,
1641
+ input: gateCtx.input,
1642
+ outcome: "gate-substitute",
1643
+ runToolCounts
1644
+ });
1645
+ const emitted = await emitToolResult(ctx, {
1362
1646
  turnId,
1363
1647
  callId,
1364
1648
  name: call.name,
@@ -1367,69 +1651,84 @@ async function executeSingleTool(ctx, call, turnId) {
1367
1651
  output: gateCtx.result,
1368
1652
  isError: false,
1369
1653
  runToolCounts
1370
- })
1371
- } };
1654
+ });
1655
+ return { result: {
1656
+ id: callId,
1657
+ content: emitted.output,
1658
+ ...emitted.isError ? { isError: true } : {}
1659
+ } };
1660
+ }
1372
1661
  let effectiveInput = gateCtx.input;
1373
1662
  if (!toolDef) {
1374
1663
  const unknownCtx = {
1375
- turnId,
1376
- callId,
1377
- name: call.name,
1378
- displayName,
1379
- input: effectiveInput,
1664
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1380
1665
  suppressError: false
1381
1666
  };
1382
1667
  await ctx.hooks.callHook("tool:unknown", unknownCtx);
1383
1668
  const content = unknownCtx.result ?? `Tool error: Unknown tool: ${call.name}`;
1669
+ const isError = unknownCtx.result === void 0;
1384
1670
  if (!unknownCtx.suppressError) {
1385
1671
  const err = /* @__PURE__ */ new Error(`Unknown tool: ${call.name}`);
1386
1672
  await ctx.hooks.callHook("tool:error", {
1387
- turnId,
1388
- callId,
1389
- name: call.name,
1390
- displayName,
1391
- input: effectiveInput,
1673
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1392
1674
  error: err
1393
1675
  });
1394
1676
  }
1677
+ await fireDispatched(ctx, {
1678
+ turnId,
1679
+ callId,
1680
+ name: call.name,
1681
+ displayName,
1682
+ input: effectiveInput,
1683
+ outcome: "unknown",
1684
+ runToolCounts
1685
+ });
1395
1686
  return { result: {
1396
1687
  id: callId,
1397
- content
1688
+ content,
1689
+ ...isError ? { isError: true } : {}
1398
1690
  } };
1399
1691
  }
1400
1692
  const validation = validateToolArgs(effectiveInput, toolDef.spec.inputSchema);
1401
1693
  if (!validation.valid) {
1402
1694
  await ctx.hooks.callHook("validation:reject", {
1695
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1696
+ reason: validation.error ?? "invalid input",
1697
+ schema: toolDef.spec.inputSchema
1698
+ });
1699
+ await fireDispatched(ctx, {
1403
1700
  turnId,
1404
1701
  callId,
1405
1702
  name: call.name,
1406
1703
  displayName,
1407
1704
  input: effectiveInput,
1408
- reason: validation.error ?? "invalid input",
1409
- schema: toolDef.spec.inputSchema
1705
+ outcome: "invalid-input",
1706
+ runToolCounts
1410
1707
  });
1411
1708
  return { result: {
1412
1709
  id: callId,
1413
- content: `Validation error: ${validation.error}`
1710
+ content: `Validation error: ${validation.error}`,
1711
+ isError: true
1414
1712
  } };
1415
1713
  }
1416
1714
  effectiveInput = validation.coercedInput ?? effectiveInput;
1417
1715
  const coercions = validation.coercions && validation.coercions.length > 0 ? validation.coercions : void 0;
1418
1716
  if (coercions) await ctx.hooks.callHook("validation:coerce", {
1419
- turnId,
1420
- callId,
1421
- name: call.name,
1422
- displayName,
1423
- input: effectiveInput,
1717
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1424
1718
  coercions,
1425
1719
  schema: toolDef.spec.inputSchema
1426
1720
  });
1427
- await ctx.hooks.callHook("tool:before", {
1721
+ await fireDispatched(ctx, {
1428
1722
  turnId,
1429
1723
  callId,
1430
1724
  name: call.name,
1431
1725
  displayName,
1432
1726
  input: effectiveInput,
1727
+ outcome: "execute",
1728
+ runToolCounts
1729
+ });
1730
+ await ctx.hooks.callHook("tool:before", {
1731
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1433
1732
  runToolCounts,
1434
1733
  ...coercions ? { coercions } : {}
1435
1734
  });
@@ -1452,55 +1751,79 @@ async function executeSingleTool(ctx, call, turnId) {
1452
1751
  turnId,
1453
1752
  callId,
1454
1753
  runId: ctx.runId,
1754
+ ...ctx.parentRunId !== void 0 ? { parentRunId: ctx.parentRunId } : {},
1455
1755
  ...ctx.session ? { session: ctx.session } : {},
1756
+ ...ctx.readState ? { readState: ctx.readState } : {},
1456
1757
  ...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
1457
1758
  };
1458
1759
  output = await toolDef.execute(effectiveInput, toolCtx);
1459
1760
  } catch (err) {
1460
1761
  const error = err instanceof Error ? err : new Error(String(err));
1461
1762
  const errorCtx = {
1462
- turnId,
1463
- callId,
1464
- name: call.name,
1465
- displayName,
1466
- input: effectiveInput,
1763
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
1467
1764
  error
1468
1765
  };
1469
1766
  await ctx.hooks.callHook("tool:error", errorCtx);
1470
1767
  output = errorCtx.result ?? `Tool error: ${error.message}`;
1471
1768
  isError = true;
1472
1769
  }
1770
+ const emitted = await emitToolResult(ctx, {
1771
+ turnId,
1772
+ callId,
1773
+ name: call.name,
1774
+ displayName,
1775
+ input: effectiveInput,
1776
+ output,
1777
+ isError,
1778
+ runToolCounts,
1779
+ ...coercions ? { coercions } : {}
1780
+ });
1473
1781
  return { result: {
1474
1782
  id: callId,
1475
- content: await emitToolResult(ctx, {
1476
- turnId,
1477
- callId,
1478
- name: call.name,
1479
- displayName,
1480
- input: effectiveInput,
1481
- output,
1482
- isError,
1483
- runToolCounts,
1484
- ...coercions ? { coercions } : {}
1485
- })
1783
+ content: emitted.output,
1784
+ ...emitted.isError ? { isError: true } : {}
1486
1785
  } };
1487
1786
  }
1488
1787
  /**
1788
+ * Fire `tool:dispatched` with the resolved path discriminator. Every code
1789
+ * path in {@link executeSingleTool} that produces a tool_result must call
1790
+ * this exactly once — that contract is what makes `tool:dispatched` ↔
1791
+ * `tool:after` a guaranteed symmetric pairing for live-event consumers
1792
+ * (the chat layer, SDK consumers reconstructing wire history from events,
1793
+ * tracing spans that want to record refused calls separately from
1794
+ * executed ones).
1795
+ *
1796
+ * Helper exists to centralize the optional-field plumbing for `reason`
1797
+ * (only set on `gate-block`) without sprinkling spread-conditional logic
1798
+ * across five call sites.
1799
+ */
1800
+ async function fireDispatched(ctx, params) {
1801
+ const { turnId, callId, name, displayName, input, outcome, reason, runToolCounts } = params;
1802
+ await ctx.hooks.callHook("tool:dispatched", {
1803
+ ...buildToolHookBase(ctx, turnId, callId, name, displayName, input),
1804
+ outcome,
1805
+ runToolCounts,
1806
+ ...reason !== void 0 ? { reason } : {}
1807
+ });
1808
+ }
1809
+ /**
1489
1810
  * Shared post-output emission: fire `tool:transform` (mutate-allowed), strip
1490
1811
  * images for non-vision providers, fire `tool:after`. Used by both the
1491
1812
  * gate-substitute (Z20) and post-execute paths so they stay byte-for-byte
1492
1813
  * identical from the consumer's perspective.
1814
+ *
1815
+ * Returns both the (possibly transformed) output and the final `isError`
1816
+ * flag — `tool:transform` listeners can flip the flag in either direction
1817
+ * (e.g. rewrite a structured error response to a graceful retry hint), and
1818
+ * the caller needs the post-transform value to populate `ToolResult.isError`
1819
+ * on the wire.
1493
1820
  */
1494
1821
  async function emitToolResult(ctx, params) {
1495
1822
  const { turnId, callId, name, displayName, input, runToolCounts, coercions } = params;
1496
1823
  let output = params.output;
1497
1824
  let isError = params.isError;
1498
1825
  const transformCtx = {
1499
- turnId,
1500
- callId,
1501
- name,
1502
- displayName,
1503
- input,
1826
+ ...buildToolHookBase(ctx, turnId, callId, name, displayName, input),
1504
1827
  result: output,
1505
1828
  isError,
1506
1829
  outputBytes: toolOutputByteLength(output),
@@ -1525,17 +1848,16 @@ async function emitToolResult(ctx, params) {
1525
1848
  }
1526
1849
  output = stripImagesForNonVision(ctx.provider, output);
1527
1850
  await ctx.hooks.callHook("tool:after", {
1528
- turnId,
1529
- callId,
1530
- name,
1531
- displayName,
1532
- input,
1851
+ ...buildToolHookBase(ctx, turnId, callId, name, displayName, input),
1533
1852
  result: output,
1534
1853
  outputBytes: toolOutputByteLength(output),
1535
1854
  runToolCounts,
1536
1855
  ...coercions ? { coercions } : {}
1537
1856
  });
1538
- return output;
1857
+ return {
1858
+ output,
1859
+ isError
1860
+ };
1539
1861
  }
1540
1862
  async function executeToolsSequential(ctx, toolCalls, turnId) {
1541
1863
  const results = [];
@@ -1544,14 +1866,16 @@ async function executeToolsSequential(ctx, toolCalls, turnId) {
1544
1866
  if (ctx.signal.aborted) {
1545
1867
  for (let j = i; j < toolCalls.length; j++) results.push({
1546
1868
  id: toolCalls[j].id,
1547
- content: "Aborted: run was cancelled"
1869
+ content: INTERRUPT_MESSAGE_FOR_TOOL_USE,
1870
+ isError: true
1548
1871
  });
1549
1872
  return results;
1550
1873
  }
1551
1874
  if (ctx.steeringQueue.length > 0) {
1552
1875
  for (let j = i; j < toolCalls.length; j++) results.push({
1553
1876
  id: toolCalls[j].id,
1554
- content: "Skipped: steering message received"
1877
+ content: TOOL_USE_SKIPPED_MESSAGE,
1878
+ isError: true
1555
1879
  });
1556
1880
  return results;
1557
1881
  }
@@ -1561,11 +1885,13 @@ async function executeToolsSequential(ctx, toolCalls, turnId) {
1561
1885
  } catch (err) {
1562
1886
  results.push({
1563
1887
  id: call.id,
1564
- content: `Error: ${errorMessage(err)}`
1888
+ content: `Error: ${errorMessage(err)}`,
1889
+ isError: true
1565
1890
  });
1566
1891
  for (let j = i + 1; j < toolCalls.length; j++) results.push({
1567
1892
  id: toolCalls[j].id,
1568
- content: "Skipped: previous tool call threw"
1893
+ content: TOOL_USE_AFTER_ERROR_MESSAGE,
1894
+ isError: true
1569
1895
  });
1570
1896
  return results;
1571
1897
  }
@@ -1576,9 +1902,12 @@ async function executeToolsParallel(ctx, toolCalls, turnId) {
1576
1902
  const executions = toolCalls.map((call) => executeSingleTool(ctx, call, turnId));
1577
1903
  return (await Promise.allSettled(executions)).map((s, i) => {
1578
1904
  if (s.status === "fulfilled") return s.value.result;
1905
+ const reason = s.reason;
1906
+ const isAbort = ctx.signal.aborted || reason instanceof Error && reason.name === "AbortError";
1579
1907
  return {
1580
1908
  id: toolCalls[i].id,
1581
- content: `Error: ${s.reason instanceof Error ? s.reason.message : String(s.reason)}`
1909
+ content: isAbort ? INTERRUPT_MESSAGE_FOR_TOOL_USE : `Error: ${reason instanceof Error ? reason.message : String(reason)}`,
1910
+ isError: true
1582
1911
  };
1583
1912
  });
1584
1913
  }
@@ -2157,8 +2486,10 @@ const HOOK_EVENT_SET = new Set([
2157
2486
  "stream:text",
2158
2487
  "stream:end",
2159
2488
  "stream:thinking",
2489
+ "stream:error",
2160
2490
  "oauth:refresh",
2161
2491
  "tool:gate",
2492
+ "tool:dispatched",
2162
2493
  "tool:before",
2163
2494
  "tool:after",
2164
2495
  "tool:error",
@@ -2175,8 +2506,10 @@ const HOOK_EVENT_SET = new Set([
2175
2506
  "child:stream:text",
2176
2507
  "child:stream:thinking",
2177
2508
  "child:stream:end",
2509
+ "child:stream:error",
2178
2510
  "child:tool:gate",
2179
2511
  "child:mcp:tool:gate",
2512
+ "child:tool:dispatched",
2180
2513
  "child:tool:before",
2181
2514
  "child:tool:after",
2182
2515
  "child:tool:transform",
@@ -2205,6 +2538,7 @@ const HOOK_EVENT_SET = new Set([
2205
2538
  "output",
2206
2539
  "budget:exceeded",
2207
2540
  "tool-budget:exceeded",
2541
+ "pairing:repair",
2208
2542
  "agent:abort",
2209
2543
  "agent:done",
2210
2544
  "session:start",
@@ -2224,6 +2558,16 @@ function isKnownHookEvent(event) {
2224
2558
  * loop from leaving the persisted session with an orphan tool_use — which
2225
2559
  * Anthropic rejects on resume.
2226
2560
  *
2561
+ * Each synthetic result carries the shared
2562
+ * {@link SYNTHETIC_TOOL_RESULT_PLACEHOLDER} text (so consumers can pattern-
2563
+ * match the same way they would on a live pre-send repair) and
2564
+ * `isError: true` so the model treats it as a failed call.
2565
+ *
2566
+ * Fires `pairing:repair` (mode `orphan-tool-use-append`) for each synthetic
2567
+ * result, mirroring the wire-level repair pass's observability so postmortem
2568
+ * dashboards see the same telemetry shape regardless of where the orphan
2569
+ * was detected.
2570
+ *
2227
2571
  * No-op when:
2228
2572
  * - The trailing turn isn't an assistant turn (already closed, or session
2229
2573
  * ends with the seeded user prompt).
@@ -2231,7 +2575,7 @@ function isKnownHookEvent(event) {
2231
2575
  * - All tool_use ids are already answered by a tool_result somewhere later
2232
2576
  * in the conversation (defensive — shouldn't happen but cheap to check).
2233
2577
  */
2234
- function synthesizeMissingToolResults(turns, syntheticTurnId, runId, provider) {
2578
+ async function synthesizeMissingToolResults(turns, syntheticTurnId, runId, provider, hooks) {
2235
2579
  if (turns.length === 0) return;
2236
2580
  const last = turns[turns.length - 1];
2237
2581
  if (last.role !== "assistant") return;
@@ -2244,7 +2588,8 @@ function synthesizeMissingToolResults(turns, syntheticTurnId, runId, provider) {
2244
2588
  if (dangling.length === 0) return;
2245
2589
  const results = dangling.map((id) => ({
2246
2590
  id,
2247
- content: "Aborted: run failed before tool execution completed"
2591
+ content: SYNTHETIC_TOOL_RESULT_PLACEHOLDER,
2592
+ isError: true
2248
2593
  }));
2249
2594
  const msg = provider.toolResultsMessage(results);
2250
2595
  turns.push({
@@ -2254,6 +2599,11 @@ function synthesizeMissingToolResults(turns, syntheticTurnId, runId, provider) {
2254
2599
  content: msg.content,
2255
2600
  createdAt: Date.now()
2256
2601
  });
2602
+ for (const callId of dangling) await hooks.callHook("pairing:repair", {
2603
+ mode: "orphan-tool-use-append",
2604
+ callId,
2605
+ messageIndex: turns.length - 2
2606
+ });
2257
2607
  }
2258
2608
  function resolveBehavior(agentBehavior, runBehavior) {
2259
2609
  return {
@@ -2278,7 +2628,8 @@ function resolveBehavior(agentBehavior, runBehavior) {
2278
2628
  toolSearch: runBehavior?.toolSearch ?? agentBehavior?.toolSearch,
2279
2629
  persistThreshold: runBehavior?.persistThreshold ?? agentBehavior?.persistThreshold,
2280
2630
  persistExcludeTools: runBehavior?.persistExcludeTools ?? agentBehavior?.persistExcludeTools,
2281
- persistDir: runBehavior?.persistDir ?? agentBehavior?.persistDir
2631
+ persistDir: runBehavior?.persistDir ?? agentBehavior?.persistDir,
2632
+ strictToolPairing: runBehavior?.strictToolPairing ?? agentBehavior?.strictToolPairing ?? false
2282
2633
  };
2283
2634
  }
2284
2635
  /**
@@ -2366,7 +2717,7 @@ function buildSearchableCatalog(entries, options) {
2366
2717
  }
2367
2718
  const serverNames = [...byServer.keys()].sort();
2368
2719
  const parts = [];
2369
- if (options.discoveryToolName) parts.push("The following tools are available but their input schemas are NOT loaded in your context.", `Call the \`${options.discoveryToolName}\` tool to load schemas before invoking them. Surfaced tools persist for the rest of the run.`, "");
2720
+ if (options.discoveryToolName) parts.push("The following tools are available but their input schemas are NOT loaded in your context.", `Call the \`${options.discoveryToolName}\` tool to load schemas for any tool below that you have not already surfaced. Surfaced tools persist for the rest of the run.`, "");
2370
2721
  parts.push("<searchable_tools>");
2371
2722
  for (const server of serverNames) {
2372
2723
  parts.push(` <server name="${escapeXml(server)}">`);
@@ -2425,7 +2776,7 @@ function initialRunCounter(session) {
2425
2776
  for (const t of session.turns) consider(t.runId);
2426
2777
  return max;
2427
2778
  }
2428
- function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, mcpConnector, eager, hooks: initialHooks }) {
2779
+ function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, readState: agentReadState, skills: agentSkills, mcpConnector, eager, hooks: initialHooks }) {
2429
2780
  const hooks = createHooks();
2430
2781
  const executionContext = execution ?? createProcessContext();
2431
2782
  const sourceTools = agentTools ?? {};
@@ -2494,9 +2845,14 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2494
2845
  if (running) throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
2495
2846
  const hasSessionTurns = session && session.turns.length > 0;
2496
2847
  if (!options.prompt && !hasSessionTurns) throw new Error("prompt is required when no session with existing turns is provided");
2497
- if (!options.prompt && hasSessionTurns) {
2498
- const lastTurn = session.turns.at(-1);
2499
- if (lastTurn && lastTurn.role !== "user") throw new Error("cannot resume without prompt: last session turn must be a user message");
2848
+ let resumeFilteredTurns;
2849
+ if (hasSessionTurns) resumeFilteredTurns = filterUnresolvedToolUses(session.turns);
2850
+ if (!options.prompt && resumeFilteredTurns) {
2851
+ const lastTurn = resumeFilteredTurns.at(-1);
2852
+ if (lastTurn && lastTurn.role !== "user") {
2853
+ const detail = detectTurnInterruption(resumeFilteredTurns) === "completed" ? "last turn is a completed assistant message" : "last turn is mid-stream assistant content";
2854
+ throw new Error(`cannot resume without prompt: ${detail}. Pass a prompt to agent.run({ prompt: … }).`);
2855
+ }
2500
2856
  }
2501
2857
  let externalAbortListener;
2502
2858
  const externalSignal = options.signal;
@@ -2561,7 +2917,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2561
2917
  const thinking = options.thinking ?? "off";
2562
2918
  const model = options.model ?? provider.meta.defaultModel;
2563
2919
  const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
2564
- const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, persistThreshold, persistExcludeTools, persistDir } = resolvedBehavior;
2920
+ const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, persistThreshold, persistExcludeTools, persistDir, strictToolPairing } = resolvedBehavior;
2565
2921
  let system = options.system || agentSystem || "You are a helpful assistant.";
2566
2922
  if (skillsCatalog) system = `${system}\n\n${skillsCatalog}`;
2567
2923
  const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? {
@@ -2626,7 +2982,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2626
2982
  if (isResume) {
2627
2983
  const childRunIds = new Set(session.runs.filter((r) => (r.depth ?? 0) > 0).map((r) => r.id));
2628
2984
  const resumed = childRunIds.size === 0 ? session.turns : session.turns.filter((t) => !t.runId || !childRunIds.has(t.runId));
2629
- turns.push(...resumed);
2985
+ const filteredForRuntime = resumeFilteredTurns && resumed === session.turns ? resumeFilteredTurns : filterUnresolvedToolUses(resumed);
2986
+ turns.push(...filteredForRuntime);
2630
2987
  }
2631
2988
  const runTurnStart = turns.length;
2632
2989
  if (options.system) await hooks.callHook("system:before", { system: options.system });
@@ -2669,7 +3026,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2669
3026
  const unregisterToolResultsSync = session ? hooks.hook("tool-results:after", persistPendingTurns) : void 0;
2670
3027
  async function flushTurns(opts = {}) {
2671
3028
  if (!session) return;
2672
- if (opts.failureFallback) synthesizeMissingToolResults(turns, await session.generateTurnId?.() ?? crypto.randomUUID(), runId, provider);
3029
+ if (opts.failureFallback) await synthesizeMissingToolResults(turns, await session.generateTurnId?.() ?? crypto.randomUUID(), runId, provider, hooks);
2673
3030
  const remaining = turns.slice(lastPersistedTurnCount);
2674
3031
  if (remaining.length > 0) {
2675
3032
  await session.appendTurns(remaining);
@@ -2735,6 +3092,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2735
3092
  maxTurns,
2736
3093
  maxTokens,
2737
3094
  ...session ? { session } : {},
3095
+ ...agentReadState ? { readState: agentReadState } : {},
3096
+ ...options.parentRunId ? { parentRunId: options.parentRunId } : {},
2738
3097
  depth: runDepth,
2739
3098
  thinkingBudget,
2740
3099
  schema,
@@ -2748,6 +3107,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2748
3107
  ...persistThreshold !== void 0 ? { persistThreshold } : {},
2749
3108
  ...persistExcludeTools !== void 0 ? { persistExcludeTools } : {},
2750
3109
  ...persistDir !== void 0 ? { persistDir } : {},
3110
+ ...strictToolPairing ? { strictToolPairing: true } : {},
3111
+ providerName: provider.name,
2751
3112
  runStartMs,
2752
3113
  runToolCounts: {}
2753
3114
  });
@@ -3296,12 +3657,14 @@ const edit = {
3296
3657
  } catch {
3297
3658
  return `Edit error: file not found: ${target}.${await suggestionFor(ctx.execution, ctx.handle, target)}`;
3298
3659
  }
3299
- if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
3300
- const readState = getReadState(ctx.session);
3301
- const absKey = `${ctx.handle.cwd}::${target}`;
3302
- const prior = readState?.get(absKey);
3303
- if (!prior) return `Edit error: ${target} has not been read in this session. Call read_file first so the edit applies against the current contents.`;
3304
- if (prior.contentHash !== hashContent(original)) return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
3660
+ if (ctx.behavior?.requireReadBeforeEdit) {
3661
+ const readState = resolveReadStateMap(ctx);
3662
+ if (readState) {
3663
+ const absKey = readStateKey(ctx.handle.cwd, target);
3664
+ const prior = readState.get(absKey);
3665
+ if (!prior) return `Edit error: ${target} has not been read in this session. Call read_file first so the edit applies against the current contents. (Reads inside a \`spawn\` subagent with \`persist: false\` and without \`shareReadState: true\` do NOT propagate to the parent — re-read in the calling context.)`;
3666
+ if (prior.contentHash !== hashContent(original)) return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
3667
+ }
3305
3668
  }
3306
3669
  const match = resolveOldString(original, find);
3307
3670
  if (!match) {
@@ -3314,11 +3677,11 @@ const edit = {
3314
3677
  const updated = replaceAll ? original.split(actual).join(styledReplacement) : original.replace(actual, styledReplacement);
3315
3678
  if (updated === original) return `Edit error: replacement produced no change in ${target}.`;
3316
3679
  await ctx.execution.writeFile(ctx.handle, target, updated);
3317
- if (ctx.session) {
3318
- const readState = getReadState(ctx.session);
3319
- const absKey = `${ctx.handle.cwd}::${target}`;
3320
- const prior = readState?.get(absKey);
3321
- if (readState && prior) readState.set(absKey, {
3680
+ const readState = resolveReadStateMap(ctx);
3681
+ if (readState) {
3682
+ const absKey = readStateKey(ctx.handle.cwd, target);
3683
+ const prior = readState.get(absKey);
3684
+ if (prior) readState.set(absKey, {
3322
3685
  ...prior,
3323
3686
  contentHash: hashContent(updated),
3324
3687
  mtimeMs: Date.now()
@@ -3757,12 +4120,14 @@ const multiEdit = {
3757
4120
  } catch {
3758
4121
  return `multi_edit error: file not found: ${target}.${await suggestionFor(ctx.execution, ctx.handle, target)}`;
3759
4122
  }
3760
- if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
3761
- const readState = getReadState(ctx.session);
3762
- const absKey = `${ctx.handle.cwd}::${target}`;
3763
- const prior = readState?.get(absKey);
3764
- if (!prior) return `multi_edit error: ${target} has not been read in this session. Call read_file first so the edits apply against the current contents.`;
3765
- if (prior.contentHash !== hashContent(current)) return `multi_edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
4123
+ if (ctx.behavior?.requireReadBeforeEdit) {
4124
+ const readState = resolveReadStateMap(ctx);
4125
+ if (readState) {
4126
+ const absKey = readStateKey(ctx.handle.cwd, target);
4127
+ const prior = readState.get(absKey);
4128
+ if (!prior) return `multi_edit error: ${target} has not been read in this session. Call read_file first so the edits apply against the current contents. (Reads inside a \`spawn\` subagent with \`persist: false\` and without \`shareReadState: true\` do NOT propagate to the parent — re-read in the calling context.)`;
4129
+ if (prior.contentHash !== hashContent(current)) return `multi_edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
4130
+ }
3766
4131
  }
3767
4132
  let totalReplacements = 0;
3768
4133
  for (let i = 0; i < steps.length; i++) {
@@ -3782,11 +4147,11 @@ const multiEdit = {
3782
4147
  totalReplacements += occurrences;
3783
4148
  }
3784
4149
  await ctx.execution.writeFile(ctx.handle, target, current);
3785
- if (ctx.session) {
3786
- const readState = getReadState(ctx.session);
3787
- const absKey = `${ctx.handle.cwd}::${target}`;
3788
- const prior = readState?.get(absKey);
3789
- if (readState && prior) readState.set(absKey, {
4150
+ const readState = resolveReadStateMap(ctx);
4151
+ if (readState) {
4152
+ const absKey = readStateKey(ctx.handle.cwd, target);
4153
+ const prior = readState.get(absKey);
4154
+ if (prior) readState.set(absKey, {
3790
4155
  ...prior,
3791
4156
  contentHash: hashContent(current),
3792
4157
  mtimeMs: Date.now()
@@ -3936,14 +4301,16 @@ const readFile$1 = {
3936
4301
  return `File not found: ${path}.${await suggestionFor(ctx.execution, ctx.handle, path)}`;
3937
4302
  }
3938
4303
  const totalBytes = Buffer.byteLength(raw);
3939
- const readState = ctx.behavior?.dedupReads !== false ? getReadState(ctx.session) : void 0;
3940
- const absKey = `${ctx.handle.cwd}::${path}`;
4304
+ const dedupEnabled = ctx.behavior?.dedupReads !== false;
4305
+ const gateEnabled = ctx.behavior?.requireReadBeforeEdit === true;
4306
+ const readState = dedupEnabled || gateEnabled ? resolveReadStateMap(ctx) : void 0;
4307
+ const absKey = readStateKey(ctx.handle.cwd, path);
3941
4308
  const offsetForKey = normalizeInteger(offset, 1);
3942
4309
  const limitForKey = normalizeInteger(limit, DEFAULT_LINE_LIMIT);
3943
4310
  const maxBytesForKey = normalizeInteger(maxBytes, DEFAULT_BYTE_CAP);
3944
4311
  const showLineNumbers = typeof lineNumbers === "boolean" ? lineNumbers : ctx.behavior?.readLineNumbers ?? true;
3945
4312
  const currentHash = readState ? hashContent(raw) : "";
3946
- if (readState) {
4313
+ if (dedupEnabled && readState) {
3947
4314
  const prior = readState.get(absKey);
3948
4315
  if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey && prior.lineNumbers === showLineNumbers) return `File ${path} unchanged since the previous read in this session — the prior result is still current.`;
3949
4316
  }
@@ -4161,6 +4528,8 @@ const BUBBLED_EVENTS = [
4161
4528
  "stream:text",
4162
4529
  "stream:thinking",
4163
4530
  "stream:end",
4531
+ "stream:error",
4532
+ "tool:dispatched",
4164
4533
  "tool:before",
4165
4534
  "tool:after",
4166
4535
  "tool:error",
@@ -4175,6 +4544,8 @@ const CHILD_EVENT_NAME = {
4175
4544
  "stream:text": "child:stream:text",
4176
4545
  "stream:thinking": "child:stream:thinking",
4177
4546
  "stream:end": "child:stream:end",
4547
+ "stream:error": "child:stream:error",
4548
+ "tool:dispatched": "child:tool:dispatched",
4178
4549
  "tool:before": "child:tool:before",
4179
4550
  "tool:after": "child:tool:after",
4180
4551
  "tool:error": "child:tool:error",
@@ -4385,18 +4756,23 @@ function createSpawnTool(options = {}) {
4385
4756
  let result = "";
4386
4757
  let unbubble;
4387
4758
  try {
4388
- const agent = createAgent({
4759
+ const parentPreset = {
4389
4760
  ...ctx.name !== void 0 ? { name: ctx.name } : {},
4390
4761
  ...ctx.system !== void 0 ? { system: ctx.system } : {},
4391
4762
  tools: ctx.tools,
4392
4763
  ...ctx.toolAliases !== void 0 ? { toolAliases: ctx.toolAliases } : {},
4393
4764
  ...ctx.mcpServers !== void 0 ? { mcpServers: ctx.mcpServers } : {},
4394
4765
  ...ctx.skills !== void 0 ? { skills: ctx.skills } : {},
4395
- ...ctx.behavior !== void 0 ? { behavior: ctx.behavior } : {},
4766
+ ...ctx.behavior !== void 0 ? { behavior: ctx.behavior } : {}
4767
+ };
4768
+ const sharedReadState = options.shareReadState ? resolveReadStateMap(ctx) : void 0;
4769
+ const agent = createAgent({
4770
+ ...parentPreset,
4396
4771
  ...options.preset,
4397
4772
  provider: ctx.provider,
4398
4773
  execution: ctx.execution,
4399
- ...options.persist && ctx.session ? { session: ctx.session } : {}
4774
+ ...options.persist && ctx.session ? { session: ctx.session } : {},
4775
+ ...sharedReadState ? { readState: sharedReadState } : {}
4400
4776
  });
4401
4777
  if (forwardHooks) {
4402
4778
  const unregisterEnricher = agent.hooks.hook("tool:before", async (toolCtx) => {
@@ -4571,6 +4947,6 @@ const writeFile$1 = {
4571
4947
  }
4572
4948
  };
4573
4949
  //#endregion
4574
- export { maybePersistToolResult as C, cleanupPersistedSession as S, getReadState as T, createSkillsReadTool as _, multiEdit as a, PERSISTENCE_PREVIEW_BYTES as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, glob as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shell as r, createInteractionTool as s, writeFile$1 as t, edit as u, validateToolArgs as v, resolvePersistDir as w, buildPersistedStub as x, PERSISTED_STUB_PREFIX as y };
4950
+ export { readStateKey as A, PERSISTENCE_PREVIEW_BYTES as C, resolvePersistDir as D, maybePersistToolResult as E, getReadState as O, PERSISTED_STUB_PREFIX as S, cleanupPersistedSession as T, createSkillsReadTool as _, multiEdit as a, TOOL_USE_SKIPPED_MESSAGE as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, resolveReadStateMap as j, hashContent as k, glob as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shell as r, createInteractionTool as s, writeFile$1 as t, edit as u, INTERRUPT_MESSAGE_FOR_TOOL_USE as v, buildPersistedStub as w, validateToolArgs as x, TOOL_USE_AFTER_ERROR_MESSAGE as y };
4575
4951
 
4576
- //# sourceMappingURL=tools-CMVruxF0.js.map
4952
+ //# sourceMappingURL=tools-CCsL5SCO.js.map