zidane 3.1.0 → 3.1.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.
@@ -8,10 +8,10 @@ import {
8
8
  } from "./chunk-TPXPVEH6.js";
9
9
  import {
10
10
  createProcessContext
11
- } from "./chunk-2EQT4EHD.js";
11
+ } from "./chunk-IUBBVF53.js";
12
12
  import {
13
13
  connectMcpServers
14
- } from "./chunk-R74LQKAM.js";
14
+ } from "./chunk-7H34OFDA.js";
15
15
  import {
16
16
  toolOutputByteLength
17
17
  } from "./chunk-JH6IAAFA.js";
@@ -36,6 +36,154 @@ function countExactMatches(haystack, needle) {
36
36
  }
37
37
  return count;
38
38
  }
39
+ var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
40
+ var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
41
+ var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
42
+ var RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
43
+ function normalizeQuotes(str) {
44
+ return str.replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'").replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'").replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"').replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"');
45
+ }
46
+ var DESANITIZATIONS = [
47
+ ["<fnr>", "<function_results>"],
48
+ ["<n>", "<name>"],
49
+ ["</n>", "</name>"],
50
+ ["<o>", "<output>"],
51
+ ["</o>", "</output>"],
52
+ ["<e>", "<error>"],
53
+ ["</e>", "</error>"],
54
+ ["<s>", "<system>"],
55
+ ["</s>", "</system>"],
56
+ ["<r>", "<result>"],
57
+ ["</r>", "</result>"],
58
+ ["< META_START >", "<META_START>"],
59
+ ["< META_END >", "<META_END>"],
60
+ ["< EOT >", "<EOT>"],
61
+ ["< META >", "<META>"],
62
+ ["< SOS >", "<SOS>"],
63
+ ["\n\nH:", "\n\nHuman:"],
64
+ ["\n\nA:", "\n\nAssistant:"]
65
+ ];
66
+ function desanitize(s) {
67
+ let out = s;
68
+ for (const [from, to] of DESANITIZATIONS)
69
+ out = out.replaceAll(from, to);
70
+ return out;
71
+ }
72
+ function resolveOldString(haystack, needle) {
73
+ const exact = countExactMatches(haystack, needle);
74
+ if (exact > 0)
75
+ return { actual: needle, occurrences: exact, via: "exact" };
76
+ const normNeedle = normalizeQuotes(needle);
77
+ const normFile = normalizeQuotes(haystack);
78
+ if (normNeedle !== needle || normFile !== haystack) {
79
+ const idx = normFile.indexOf(normNeedle);
80
+ if (idx !== -1) {
81
+ const actual = haystack.slice(idx, idx + normNeedle.length);
82
+ let occ = 0;
83
+ let cursor = 0;
84
+ while (true) {
85
+ const next = normFile.indexOf(normNeedle, cursor);
86
+ if (next === -1)
87
+ break;
88
+ occ++;
89
+ cursor = next + normNeedle.length;
90
+ }
91
+ return { actual, occurrences: occ, via: "quotes" };
92
+ }
93
+ }
94
+ const desan = desanitize(needle);
95
+ if (desan !== needle) {
96
+ const desanCount = countExactMatches(haystack, desan);
97
+ if (desanCount > 0)
98
+ return { actual: desan, occurrences: desanCount, via: "desanitize" };
99
+ }
100
+ const combo = desanitize(normNeedle);
101
+ if (combo !== needle) {
102
+ const idx = normFile.indexOf(combo);
103
+ if (idx !== -1) {
104
+ const actual = haystack.slice(idx, idx + combo.length);
105
+ let occ = 0;
106
+ let cursor = 0;
107
+ while (true) {
108
+ const next = normFile.indexOf(combo, cursor);
109
+ if (next === -1)
110
+ break;
111
+ occ++;
112
+ cursor = next + combo.length;
113
+ }
114
+ return { actual, occurrences: occ, via: "quotes+desanitize" };
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+ function preserveQuoteStyle(actual, replacement) {
120
+ const hasDouble = actual.includes(LEFT_DOUBLE_CURLY_QUOTE) || actual.includes(RIGHT_DOUBLE_CURLY_QUOTE);
121
+ const hasSingle = actual.includes(LEFT_SINGLE_CURLY_QUOTE) || actual.includes(RIGHT_SINGLE_CURLY_QUOTE);
122
+ if (!hasDouble && !hasSingle)
123
+ return replacement;
124
+ let out = replacement;
125
+ if (hasDouble)
126
+ out = applyCurly(out, '"', LEFT_DOUBLE_CURLY_QUOTE, RIGHT_DOUBLE_CURLY_QUOTE, false);
127
+ if (hasSingle)
128
+ out = applyCurly(out, "'", LEFT_SINGLE_CURLY_QUOTE, RIGHT_SINGLE_CURLY_QUOTE, true);
129
+ return out;
130
+ }
131
+ function applyCurly(s, straight, left, right, contractionAware) {
132
+ const chars = [...s];
133
+ const result = [];
134
+ for (let i = 0; i < chars.length; i++) {
135
+ if (chars[i] !== straight) {
136
+ result.push(chars[i]);
137
+ continue;
138
+ }
139
+ if (contractionAware) {
140
+ const prev = i > 0 ? chars[i - 1] : "";
141
+ const next = i < chars.length - 1 ? chars[i + 1] : "";
142
+ if (/\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
143
+ result.push(right);
144
+ continue;
145
+ }
146
+ }
147
+ result.push(isOpeningContext(chars, i) ? left : right);
148
+ }
149
+ return result.join("");
150
+ }
151
+ function isOpeningContext(chars, i) {
152
+ if (i === 0)
153
+ return true;
154
+ const prev = chars[i - 1];
155
+ return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "\u2014" || prev === "\u2013";
156
+ }
157
+
158
+ // src/tools/path-suggest.ts
159
+ async function findSimilarFile(execution, handle, path) {
160
+ const slash = path.lastIndexOf("/");
161
+ const dir = slash === -1 ? "." : path.slice(0, slash) || "/";
162
+ const target = slash === -1 ? path : path.slice(slash + 1);
163
+ const dot = target.lastIndexOf(".");
164
+ const targetBase = dot === -1 ? target : target.slice(0, dot);
165
+ if (targetBase.length === 0)
166
+ return null;
167
+ let entries;
168
+ try {
169
+ entries = await execution.listFiles(handle, dir);
170
+ } catch {
171
+ return null;
172
+ }
173
+ for (const entry of entries) {
174
+ if (entry === target)
175
+ continue;
176
+ const entryDot = entry.lastIndexOf(".");
177
+ const entryBase = entryDot === -1 ? entry : entry.slice(0, entryDot);
178
+ if (entryBase === targetBase)
179
+ return entry;
180
+ }
181
+ return null;
182
+ }
183
+ async function suggestionFor(execution, handle, path) {
184
+ const sibling = await findSimilarFile(execution, handle, path);
185
+ return sibling ? ` Did you mean ${sibling}?` : "";
186
+ }
39
187
 
40
188
  // src/tools/read-state.ts
41
189
  var STATE = /* @__PURE__ */ new WeakMap();
@@ -49,6 +197,17 @@ function getReadState(session) {
49
197
  }
50
198
  return map;
51
199
  }
200
+ var TOOL_DEDUP_STATE = /* @__PURE__ */ new WeakMap();
201
+ function getToolDedupState(session) {
202
+ if (!session)
203
+ return void 0;
204
+ let map = TOOL_DEDUP_STATE.get(session);
205
+ if (!map) {
206
+ map = /* @__PURE__ */ new Map();
207
+ TOOL_DEDUP_STATE.set(session, map);
208
+ }
209
+ return map;
210
+ }
52
211
  function hashContent(text) {
53
212
  let h = 2166136261;
54
213
  for (let i = 0; i < text.length; i++) {
@@ -87,7 +246,8 @@ var edit = {
87
246
  try {
88
247
  original = await ctx.execution.readFile(ctx.handle, target);
89
248
  } catch {
90
- return `Edit error: file not found: ${target}`;
249
+ const hint = await suggestionFor(ctx.execution, ctx.handle, target);
250
+ return `Edit error: file not found: ${target}.${hint}`;
91
251
  }
92
252
  if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
93
253
  const readState = getReadState(ctx.session);
@@ -98,14 +258,18 @@ var edit = {
98
258
  if (prior.contentHash !== hashContent(original))
99
259
  return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
100
260
  }
101
- const occurrences = countExactMatches(original, find);
102
- if (occurrences === 0) {
261
+ const match = resolveOldString(original, find);
262
+ if (!match) {
103
263
  const preview = nearestMatchPreview(original, find);
104
264
  return preview ? `Edit error: old_string not found in ${target}. Closest match in the file: ${preview}` : `Edit error: old_string not found in ${target}.`;
105
265
  }
266
+ const { actual, occurrences, via } = match;
106
267
  if (occurrences > 1 && !replaceAll)
107
268
  return `Edit error: old_string appears ${occurrences} times in ${target}. Pass replace_all=true or expand old_string for uniqueness.`;
108
- const updated = replaceAll ? original.split(find).join(replacement) : original.replace(find, replacement);
269
+ let styledReplacement = via === "desanitize" || via === "quotes+desanitize" ? desanitize(replacement) : replacement;
270
+ if (via === "quotes" || via === "quotes+desanitize")
271
+ styledReplacement = preserveQuoteStyle(actual, styledReplacement);
272
+ const updated = replaceAll ? original.split(actual).join(styledReplacement) : original.replace(actual, styledReplacement);
109
273
  if (updated === original)
110
274
  return `Edit error: replacement produced no change in ${target}.`;
111
275
  await ctx.execution.writeFile(ctx.handle, target, updated);
@@ -479,7 +643,8 @@ var multiEdit = {
479
643
  try {
480
644
  current = await ctx.execution.readFile(ctx.handle, target);
481
645
  } catch {
482
- return `multi_edit error: file not found: ${target}`;
646
+ const hint = await suggestionFor(ctx.execution, ctx.handle, target);
647
+ return `multi_edit error: file not found: ${target}.${hint}`;
483
648
  }
484
649
  if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
485
650
  const readState = getReadState(ctx.session);
@@ -502,12 +667,16 @@ var multiEdit = {
502
667
  return `multi_edit error: edit #${i + 1} has empty old_string. Use write_file to fully replace a file.`;
503
668
  if (find === replacement)
504
669
  return `multi_edit error: edit #${i + 1} old_string and new_string are identical.`;
505
- const occurrences = countExactMatches(current, find);
506
- if (occurrences === 0)
670
+ const match = resolveOldString(current, find);
671
+ if (!match)
507
672
  return `multi_edit error: edit #${i + 1} old_string not found in ${target}.`;
673
+ const { actual, occurrences, via } = match;
508
674
  if (occurrences > 1 && !replaceAll)
509
675
  return `multi_edit error: edit #${i + 1} old_string appears ${occurrences} times. Pass replace_all=true on this edit or expand old_string for uniqueness.`;
510
- current = replaceAll ? current.split(find).join(replacement) : current.replace(find, replacement);
676
+ let styledReplacement = via === "desanitize" || via === "quotes+desanitize" ? desanitize(replacement) : replacement;
677
+ if (via === "quotes" || via === "quotes+desanitize")
678
+ styledReplacement = preserveQuoteStyle(actual, styledReplacement);
679
+ current = replaceAll ? current.split(actual).join(styledReplacement) : current.replace(actual, styledReplacement);
511
680
  applied += occurrences;
512
681
  }
513
682
  await ctx.execution.writeFile(ctx.handle, target, current);
@@ -524,10 +693,51 @@ var multiEdit = {
524
693
  };
525
694
 
526
695
  // src/tools/read-file.ts
696
+ import { Buffer as Buffer2 } from "buffer";
697
+
698
+ // src/tools/binary-read.ts
527
699
  import { Buffer } from "buffer";
700
+ function imageMediaTypeFor(path) {
701
+ const dot = path.lastIndexOf(".");
702
+ if (dot === -1)
703
+ return void 0;
704
+ const ext = path.slice(dot + 1).toLowerCase();
705
+ switch (ext) {
706
+ case "png":
707
+ return "image/png";
708
+ case "jpg":
709
+ case "jpeg":
710
+ return "image/jpeg";
711
+ case "gif":
712
+ return "image/gif";
713
+ case "webp":
714
+ return "image/webp";
715
+ default:
716
+ return void 0;
717
+ }
718
+ }
719
+ async function readFileAsBase64(execution, handle, path) {
720
+ if (execution.readFileBinary) {
721
+ const bytes = await execution.readFileBinary(handle, path);
722
+ const b642 = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
723
+ return { base64: b642, byteLength: bytes.byteLength };
724
+ }
725
+ const cmd = `base64 < ${shellQuote2(path)}`;
726
+ const result = await execution.exec(handle, cmd);
727
+ if (result.exitCode !== 0)
728
+ throw new Error(`base64 read failed: ${result.stderr || `exit ${result.exitCode}`}`);
729
+ const b64 = result.stdout.replace(/\s+/g, "");
730
+ return { base64: b64, byteLength: Math.floor(b64.length * 3 / 4) };
731
+ }
732
+ function shellQuote2(s) {
733
+ return `'${s.replace(/'/g, "'\\''")}'`;
734
+ }
735
+
736
+ // src/tools/read-file.ts
528
737
  var DEFAULT_LINE_LIMIT = 2e3;
529
738
  var DEFAULT_BYTE_CAP = 65536;
530
739
  var BINARY_PROBE_BYTES = 8e3;
740
+ var DEFAULT_IMAGE_BYTE_CAP = 5 * 1024 * 1024;
531
741
  var readFile = {
532
742
  spec: {
533
743
  name: "read_file",
@@ -544,13 +754,33 @@ var readFile = {
544
754
  }
545
755
  },
546
756
  async execute({ path, offset, limit, maxBytes }, ctx) {
757
+ const imgMedia = imageMediaTypeFor(path);
758
+ if (imgMedia) {
759
+ const sizeCap = maxBytes !== void 0 ? normalizeInteger(maxBytes, DEFAULT_IMAGE_BYTE_CAP) : DEFAULT_IMAGE_BYTE_CAP;
760
+ try {
761
+ const { base64, byteLength } = await readFileAsBase64(ctx.execution, ctx.handle, path);
762
+ if (sizeCap > 0 && byteLength > sizeCap) {
763
+ return `[image too large to inline: ${path}, ${byteLength} bytes (cap ${sizeCap}). Raise maxBytes, or use shell to inspect.]`;
764
+ }
765
+ const content = [
766
+ { type: "text", text: `Image: ${path} (${byteLength} bytes, ${imgMedia})` },
767
+ { type: "image", mediaType: imgMedia, data: base64 }
768
+ ];
769
+ return content;
770
+ } catch (err) {
771
+ const msg = err instanceof Error ? err.message : String(err);
772
+ const hint = await suggestionFor(ctx.execution, ctx.handle, path);
773
+ return `Image read failed: ${path} \u2014 ${msg}.${hint}`;
774
+ }
775
+ }
547
776
  let raw;
548
777
  try {
549
778
  raw = await ctx.execution.readFile(ctx.handle, path);
550
779
  } catch {
551
- return `File not found: ${path}`;
780
+ const hint = await suggestionFor(ctx.execution, ctx.handle, path);
781
+ return `File not found: ${path}.${hint}`;
552
782
  }
553
- const totalBytes = Buffer.byteLength(raw);
783
+ const totalBytes = Buffer2.byteLength(raw);
554
784
  const dedupEnabled = ctx.behavior?.dedupReads !== false;
555
785
  const readState = dedupEnabled ? getReadState(ctx.session) : void 0;
556
786
  const absKey = `${ctx.handle.cwd}::${path}`;
@@ -580,7 +810,7 @@ var readFile = {
580
810
  if (maxBytesN > 0) {
581
811
  const truncatedSlice = [];
582
812
  for (const line of slice) {
583
- const lineBytes = Buffer.byteLength(line) + 1;
813
+ const lineBytes = Buffer2.byteLength(line) + 1;
584
814
  if (bytesUsed + lineBytes > maxBytesN && truncatedSlice.length > 0) {
585
815
  bytesCut = true;
586
816
  break;
@@ -597,14 +827,14 @@ var readFile = {
597
827
  }
598
828
  let midLineCut = false;
599
829
  if (maxBytesN > 0 && slice.length > 0) {
600
- const bodyBytes = Buffer.byteLength(slice.join("\n"));
830
+ const bodyBytes = Buffer2.byteLength(slice.join("\n"));
601
831
  if (bodyBytes > maxBytesN) {
602
832
  const lastIdx = slice.length - 1;
603
833
  const lastLine = slice[lastIdx];
604
- const otherBytes = lastIdx > 0 ? Buffer.byteLength(slice.slice(0, lastIdx).join("\n")) + 1 : 0;
834
+ const otherBytes = lastIdx > 0 ? Buffer2.byteLength(slice.slice(0, lastIdx).join("\n")) + 1 : 0;
605
835
  const budgetForLast = Math.max(0, maxBytesN - otherBytes);
606
836
  let cut = Math.min(lastLine.length, budgetForLast);
607
- while (cut > 0 && Buffer.byteLength(lastLine.slice(0, cut)) > budgetForLast)
837
+ while (cut > 0 && Buffer2.byteLength(lastLine.slice(0, cut)) > budgetForLast)
608
838
  cut--;
609
839
  slice[lastIdx] = lastLine.slice(0, cut);
610
840
  midLineCut = true;
@@ -666,7 +896,38 @@ function looksBinary(text) {
666
896
  }
667
897
 
668
898
  // src/tools/shell.ts
669
- import { Buffer as Buffer2 } from "buffer";
899
+ import { Buffer as Buffer3 } from "buffer";
900
+
901
+ // src/tools/shell-semantics.ts
902
+ var DEFAULT_SEMANTIC = (exitCode) => ({
903
+ isError: exitCode !== 0,
904
+ message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
905
+ });
906
+ var COMMAND_SEMANTICS = /* @__PURE__ */ new Map([
907
+ // grep / ripgrep: 0 = matches, 1 = no matches, ≥2 = error.
908
+ ["grep", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "No matches found" : void 0 })],
909
+ ["rg", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "No matches found" : void 0 })],
910
+ // diff: 0 = identical, 1 = differ, ≥2 = error.
911
+ ["diff", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Files differ" : void 0 })],
912
+ // find: 0 = ok, 1 = some dirs inaccessible (warning), ≥2 = error.
913
+ ["find", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Some directories were inaccessible" : void 0 })],
914
+ // test / [: 0 = condition true, 1 = condition false, ≥2 = error.
915
+ ["test", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Condition is false" : void 0 })],
916
+ ["[", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Condition is false" : void 0 })]
917
+ ]);
918
+ function interpretShellResult(command, exitCode) {
919
+ const base = extractTrailingCommand(command);
920
+ const semantic = COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC;
921
+ return semantic(exitCode);
922
+ }
923
+ function extractTrailingCommand(command) {
924
+ const segments = command.split(/\|\||&&|[;|\n]/);
925
+ const last = segments[segments.length - 1]?.trim() ?? command;
926
+ const tokens = last.split(/\s+/).filter((t) => !/^[A-Z_]\w*=/i.test(t));
927
+ return tokens[0] ?? "";
928
+ }
929
+
930
+ // src/tools/shell.ts
670
931
  var DEFAULT_MAX_OUTPUT_BYTES = 8192;
671
932
  var shell = {
672
933
  spec: {
@@ -686,10 +947,19 @@ var shell = {
686
947
  const execOpts = {};
687
948
  if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0)
688
949
  execOpts.timeout = Math.max(1, Math.ceil(timeout / 1e3));
689
- const result = await ctx.execution.exec(ctx.handle, command, execOpts);
950
+ const cmd = command;
951
+ const result = await ctx.execution.exec(ctx.handle, cmd, execOpts);
690
952
  const cap = normalizeCap(maxOutputBytes);
953
+ const semantic = interpretShellResult(cmd, result.exitCode);
691
954
  if (result.exitCode === 0)
692
955
  return truncateTail(result.stdout || "(no output)", cap);
956
+ if (!semantic.isError) {
957
+ const body = (result.stdout || result.stderr || "").trim();
958
+ const tail = truncateTail(body, cap);
959
+ const footer = semantic.message ? `
960
+ (${semantic.message})` : "";
961
+ return tail.length > 0 ? `${tail}${footer}` : semantic.message ?? "(no output)";
962
+ }
693
963
  const combined = `${result.stdout}
694
964
  ${result.stderr}`.trim();
695
965
  return `Exit code ${result.exitCode}
@@ -706,21 +976,21 @@ function normalizeCap(value) {
706
976
  function truncateTail(text, cap) {
707
977
  if (cap === 0)
708
978
  return text;
709
- const totalBytes = Buffer2.byteLength(text);
979
+ const totalBytes = Buffer3.byteLength(text);
710
980
  if (totalBytes <= cap)
711
981
  return text;
712
982
  let bytes = 0;
713
983
  let charIdx = text.length;
714
984
  while (charIdx > 0) {
715
985
  const ch = text[charIdx - 1];
716
- const chBytes = Buffer2.byteLength(ch);
986
+ const chBytes = Buffer3.byteLength(ch);
717
987
  if (bytes + chBytes > cap)
718
988
  break;
719
989
  bytes += chBytes;
720
990
  charIdx--;
721
991
  }
722
992
  const tail = text.slice(charIdx);
723
- const droppedBytes = totalBytes - Buffer2.byteLength(tail);
993
+ const droppedBytes = totalBytes - Buffer3.byteLength(tail);
724
994
  return `\u2026(${droppedBytes} bytes truncated from head)\u2026
725
995
  ${tail}`;
726
996
  }
@@ -1020,6 +1290,59 @@ function rewriteMessagesToWire(messages, maps) {
1020
1290
  return messages.map((msg) => ({ ...msg, content: rewriteContentToWire(msg.content, maps) }));
1021
1291
  }
1022
1292
 
1293
+ // src/dedup-tools.ts
1294
+ function installDedupToolsGate(hooks, getDedupTools, getSession) {
1295
+ const pending = /* @__PURE__ */ new Map();
1296
+ function pendingKey(callId, name) {
1297
+ return `${callId}::${name}`;
1298
+ }
1299
+ function gateHandler(ctx) {
1300
+ if (ctx.block || ctx.result !== void 0)
1301
+ return;
1302
+ const dedupTools = getDedupTools();
1303
+ const hasher = dedupTools?.[ctx.name];
1304
+ if (!hasher)
1305
+ return;
1306
+ const session = getSession();
1307
+ const state = getToolDedupState(session);
1308
+ if (!state)
1309
+ return;
1310
+ let hash;
1311
+ try {
1312
+ hash = hasher(ctx.input);
1313
+ } catch {
1314
+ return;
1315
+ }
1316
+ if (typeof hash !== "string" || hash.length === 0)
1317
+ return;
1318
+ const prior = state.get(ctx.name);
1319
+ if (prior && prior.hash === hash) {
1320
+ ctx.result = prior.result;
1321
+ return;
1322
+ }
1323
+ pending.set(pendingKey(ctx.callId, ctx.name), hash);
1324
+ }
1325
+ function afterHandler(ctx) {
1326
+ const key = pendingKey(ctx.callId, ctx.name);
1327
+ const hash = pending.get(key);
1328
+ if (hash === void 0)
1329
+ return;
1330
+ pending.delete(key);
1331
+ const session = getSession();
1332
+ const state = getToolDedupState(session);
1333
+ if (!state)
1334
+ return;
1335
+ state.set(ctx.name, { hash, result: ctx.result });
1336
+ }
1337
+ const unregisterGate = hooks.hook("tool:gate", gateHandler);
1338
+ const unregisterAfter = hooks.hook("tool:after", afterHandler);
1339
+ return function uninstall() {
1340
+ unregisterGate();
1341
+ unregisterAfter();
1342
+ pending.clear();
1343
+ };
1344
+ }
1345
+
1023
1346
  // src/tools/validation.ts
1024
1347
  var TRUE_STRINGS = /* @__PURE__ */ new Set(["true", "True", "TRUE", "1", "yes", "Yes", "YES"]);
1025
1348
  var FALSE_STRINGS = /* @__PURE__ */ new Set(["false", "False", "FALSE", "0", "no", "No", "NO"]);
@@ -1175,6 +1498,24 @@ function formatValue(value) {
1175
1498
 
1176
1499
  // src/loop.ts
1177
1500
  var IMAGE_OMITTED_MARKER = "[image omitted \u2014 model does not support vision]";
1501
+ function applyThinkingDecay(baseBudget, decay, turn) {
1502
+ if (typeof baseBudget !== "number" || baseBudget <= 0)
1503
+ return baseBudget;
1504
+ if (!decay)
1505
+ return baseBudget;
1506
+ let raw;
1507
+ if (typeof decay === "function") {
1508
+ raw = decay(turn, baseBudget);
1509
+ } else {
1510
+ if (turn <= decay.afterTurn)
1511
+ return baseBudget;
1512
+ const k = turn - decay.afterTurn;
1513
+ raw = Math.max(decay.floor, baseBudget * decay.factor ** k);
1514
+ }
1515
+ if (Number.isNaN(raw) || raw <= 0)
1516
+ return 0;
1517
+ return Math.round(Math.min(baseBudget, raw));
1518
+ }
1178
1519
  function turnsToMessages(turns) {
1179
1520
  return turns.filter((t) => t.role !== "system").map((t) => ({ role: t.role, content: t.content }));
1180
1521
  }
@@ -1330,6 +1671,7 @@ async function executeTurn(ctx, turn) {
1330
1671
  const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
1331
1672
  sanitizedMessages = applyTailCompaction(sanitizedMessages, threshold, keep);
1332
1673
  }
1674
+ const effectiveThinkingBudget = applyThinkingDecay(ctx.thinkingBudget, ctx.thinkingDecay, turn);
1333
1675
  const streamOptions = {
1334
1676
  model: ctx.model,
1335
1677
  system: ctx.system,
@@ -1337,13 +1679,22 @@ async function executeTurn(ctx, turn) {
1337
1679
  messages: sanitizedMessages,
1338
1680
  maxTokens: ctx.maxTokens ?? 16384,
1339
1681
  thinking: ctx.thinking,
1340
- thinkingBudget: ctx.thinkingBudget,
1682
+ thinkingBudget: effectiveThinkingBudget,
1341
1683
  cache: ctx.cache ?? true,
1342
1684
  signal: ctx.signal
1343
1685
  };
1344
1686
  const transformCtx = { messages: streamOptions.messages };
1345
1687
  await ctx.hooks.callHook("context:transform", transformCtx);
1346
1688
  streamOptions.messages = transformCtx.messages;
1689
+ const systemCtx = {
1690
+ system: streamOptions.system,
1691
+ messages: streamOptions.messages,
1692
+ turn,
1693
+ turnId,
1694
+ ...ctx.session ? { session: ctx.session } : {}
1695
+ };
1696
+ await ctx.hooks.callHook("system:transform", systemCtx);
1697
+ streamOptions.system = systemCtx.system;
1347
1698
  await ctx.hooks.callHook("turn:before", { turn, turnId, options: streamOptions });
1348
1699
  let currentText = "";
1349
1700
  let currentThinking = "";
@@ -1376,7 +1727,13 @@ async function executeTurn(ctx, turn) {
1376
1727
  createdAt: Date.now()
1377
1728
  };
1378
1729
  ctx.turns.push(errorTurn);
1379
- await ctx.hooks.callHook("turn:after", { turn, turnId, usage: errorUsage, message: errorTurn });
1730
+ await ctx.hooks.callHook("turn:after", {
1731
+ turn,
1732
+ turnId,
1733
+ usage: errorUsage,
1734
+ message: errorTurn,
1735
+ toolCounts: { turn: Object.freeze({}), run: Object.freeze({ ...ctx.runToolCounts }) }
1736
+ });
1380
1737
  throw wrapProviderError(err, ctx);
1381
1738
  }
1382
1739
  if (currentText) {
@@ -1399,7 +1756,16 @@ async function executeTurn(ctx, turn) {
1399
1756
  createdAt: Date.now()
1400
1757
  };
1401
1758
  ctx.turns.push(assistantTurn);
1402
- await ctx.hooks.callHook("turn:after", { turn, turnId, usage: result.usage, message: assistantTurn });
1759
+ const turnCounts = {};
1760
+ for (const tc of canonicalToolCalls)
1761
+ turnCounts[tc.name] = (turnCounts[tc.name] ?? 0) + 1;
1762
+ await ctx.hooks.callHook("turn:after", {
1763
+ turn,
1764
+ turnId,
1765
+ usage: result.usage,
1766
+ message: assistantTurn,
1767
+ toolCounts: { turn: Object.freeze(turnCounts), run: Object.freeze({ ...ctx.runToolCounts }) }
1768
+ });
1403
1769
  if (result.done) {
1404
1770
  if (ctx.schema && !ctx.signal.aborted) {
1405
1771
  const outputSpec = {
@@ -1456,6 +1822,17 @@ async function executeTurn(ctx, turn) {
1456
1822
  }
1457
1823
  return { ended: true, turnId, usage: result.usage };
1458
1824
  }
1825
+ if (canonicalToolCalls.length === 0 && result.usage.finishReason === "pause") {
1826
+ const continueMsg = ctx.provider.userMessage("Please continue.");
1827
+ ctx.turns.push({
1828
+ id: await ctx.generateTurnId(),
1829
+ runId: ctx.runId,
1830
+ role: continueMsg.role,
1831
+ content: continueMsg.content,
1832
+ createdAt: Date.now()
1833
+ });
1834
+ return { ended: false, turnId, usage: result.usage };
1835
+ }
1459
1836
  const toolResults = ctx.toolExecution === "parallel" ? await executeToolsParallel(ctx, canonicalToolCalls, turnId) : await executeToolsSequential(ctx, canonicalToolCalls, turnId);
1460
1837
  const toolResultMsg = ctx.provider.toolResultsMessage(toolResults);
1461
1838
  ctx.turns.push({
@@ -1501,6 +1878,7 @@ async function executeSingleTool(ctx, call, turnId) {
1501
1878
  const toolDef = ctx.tools[call.name];
1502
1879
  const callId = call.id;
1503
1880
  const displayName = toWireName(call.name, ctx.aliasMaps);
1881
+ const runToolCounts = Object.freeze({ ...ctx.runToolCounts });
1504
1882
  const gateCtx = {
1505
1883
  turnId,
1506
1884
  callId,
@@ -1508,12 +1886,27 @@ async function executeSingleTool(ctx, call, turnId) {
1508
1886
  displayName,
1509
1887
  input: call.input,
1510
1888
  block: false,
1511
- reason: "Tool execution was blocked"
1889
+ reason: "Tool execution was blocked",
1890
+ runToolCounts
1512
1891
  };
1513
1892
  await ctx.hooks.callHook("tool:gate", gateCtx);
1514
1893
  if (gateCtx.block) {
1515
1894
  return { result: { id: callId, content: `Blocked: ${gateCtx.reason}` } };
1516
1895
  }
1896
+ ctx.runToolCounts[call.name] = (ctx.runToolCounts[call.name] ?? 0) + 1;
1897
+ if (gateCtx.result !== void 0) {
1898
+ const substitute = await emitToolResult(ctx, {
1899
+ turnId,
1900
+ callId,
1901
+ name: call.name,
1902
+ displayName,
1903
+ input: gateCtx.input,
1904
+ output: gateCtx.result,
1905
+ isError: false,
1906
+ runToolCounts
1907
+ });
1908
+ return { result: { id: callId, content: substitute } };
1909
+ }
1517
1910
  let effectiveInput = gateCtx.input;
1518
1911
  if (!toolDef) {
1519
1912
  const unknownCtx = {
@@ -1571,6 +1964,7 @@ async function executeSingleTool(ctx, call, turnId) {
1571
1964
  name: call.name,
1572
1965
  displayName,
1573
1966
  input: effectiveInput,
1967
+ runToolCounts,
1574
1968
  ...coercions ? { coercions } : {}
1575
1969
  });
1576
1970
  let output;
@@ -1610,12 +2004,29 @@ async function executeSingleTool(ctx, call, turnId) {
1610
2004
  output = errorCtx.result ?? `Tool error: ${error.message}`;
1611
2005
  isError = true;
1612
2006
  }
1613
- const transformCtx = {
2007
+ const finalOutput = await emitToolResult(ctx, {
1614
2008
  turnId,
1615
2009
  callId,
1616
2010
  name: call.name,
1617
2011
  displayName,
1618
2012
  input: effectiveInput,
2013
+ output,
2014
+ isError,
2015
+ runToolCounts,
2016
+ ...coercions ? { coercions } : {}
2017
+ });
2018
+ return { result: { id: callId, content: finalOutput } };
2019
+ }
2020
+ async function emitToolResult(ctx, params) {
2021
+ const { turnId, callId, name, displayName, input, runToolCounts, coercions } = params;
2022
+ let output = params.output;
2023
+ let isError = params.isError;
2024
+ const transformCtx = {
2025
+ turnId,
2026
+ callId,
2027
+ name,
2028
+ displayName,
2029
+ input,
1619
2030
  result: output,
1620
2031
  isError,
1621
2032
  outputBytes: toolOutputByteLength(output),
@@ -1628,14 +2039,15 @@ async function executeSingleTool(ctx, call, turnId) {
1628
2039
  await ctx.hooks.callHook("tool:after", {
1629
2040
  turnId,
1630
2041
  callId,
1631
- name: call.name,
2042
+ name,
1632
2043
  displayName,
1633
- input: effectiveInput,
2044
+ input,
1634
2045
  result: output,
1635
2046
  outputBytes: toolOutputByteLength(output),
2047
+ runToolCounts,
1636
2048
  ...coercions ? { coercions } : {}
1637
2049
  });
1638
- return { result: { id: callId, content: output } };
2050
+ return output;
1639
2051
  }
1640
2052
  async function executeToolsSequential(ctx, toolCalls, turnId) {
1641
2053
  const results = [];
@@ -1744,6 +2156,81 @@ function buildPromptMessage(provider, parts) {
1744
2156
  return defaultPromptMessage(parts);
1745
2157
  }
1746
2158
 
2159
+ // src/tool-budgets.ts
2160
+ function installToolBudgetsGate(hooks, getToolBudgets, enqueueSteer) {
2161
+ const steeredOnce = /* @__PURE__ */ new Set();
2162
+ const approvedCounts = {};
2163
+ async function gateHandler(ctx) {
2164
+ if (ctx.block || ctx.result !== void 0)
2165
+ return;
2166
+ const toolBudgets = getToolBudgets();
2167
+ const budget = toolBudgets?.[ctx.name];
2168
+ if (!budget)
2169
+ return;
2170
+ const max = budget.max;
2171
+ if (typeof max !== "number" || max <= 0)
2172
+ return;
2173
+ const count = approvedCounts[ctx.name] ?? 0;
2174
+ if (count < max) {
2175
+ approvedCounts[ctx.name] = count + 1;
2176
+ return;
2177
+ }
2178
+ const onExceed = budget.onExceed ?? "steer";
2179
+ let mode;
2180
+ let message;
2181
+ if (typeof onExceed === "function") {
2182
+ try {
2183
+ const out = onExceed({ tool: ctx.name, count, max });
2184
+ mode = out.mode;
2185
+ message = out.message;
2186
+ } catch {
2187
+ mode = "steer";
2188
+ message = defaultSteerMessage(ctx.name, count, max);
2189
+ }
2190
+ } else if (onExceed === "block") {
2191
+ mode = "block";
2192
+ message = defaultBlockMessage(ctx.name, max);
2193
+ } else {
2194
+ mode = "steer";
2195
+ message = defaultSteerMessage(ctx.name, count, max);
2196
+ }
2197
+ if (mode === "block") {
2198
+ ctx.block = true;
2199
+ ctx.reason = message;
2200
+ await hooks.callHook("tool-budget:exceeded", {
2201
+ tool: ctx.name,
2202
+ count,
2203
+ max,
2204
+ turnId: ctx.turnId,
2205
+ mode: "block"
2206
+ });
2207
+ return;
2208
+ }
2209
+ if (!steeredOnce.has(ctx.name)) {
2210
+ steeredOnce.add(ctx.name);
2211
+ enqueueSteer(message);
2212
+ await hooks.callHook("tool-budget:exceeded", {
2213
+ tool: ctx.name,
2214
+ count,
2215
+ max,
2216
+ turnId: ctx.turnId,
2217
+ mode: "steer"
2218
+ });
2219
+ }
2220
+ }
2221
+ const unregister = hooks.hook("tool:gate", gateHandler);
2222
+ return function uninstall() {
2223
+ unregister();
2224
+ steeredOnce.clear();
2225
+ };
2226
+ }
2227
+ function defaultSteerMessage(tool, count, max) {
2228
+ return `[Tool budget reached: '${tool}' has been called ${count} times this run (cap: ${max}). Avoid calling it again unless strictly necessary; commit to a result and move on.]`;
2229
+ }
2230
+ function defaultBlockMessage(tool, max) {
2231
+ return `Tool '${tool}' has reached its per-run budget of ${max} calls; further invocations are refused.`;
2232
+ }
2233
+
1747
2234
  // src/agent.ts
1748
2235
  var HOOK_EVENT_NAMES = [
1749
2236
  "system:before",
@@ -1762,6 +2249,7 @@ var HOOK_EVENT_NAMES = [
1762
2249
  "validation:reject",
1763
2250
  "validation:coerce",
1764
2251
  "context:transform",
2252
+ "system:transform",
1765
2253
  "steer:inject",
1766
2254
  "spawn:before",
1767
2255
  "spawn:complete",
@@ -1790,6 +2278,7 @@ var HOOK_EVENT_NAMES = [
1790
2278
  "usage",
1791
2279
  "output",
1792
2280
  "budget:exceeded",
2281
+ "tool-budget:exceeded",
1793
2282
  "agent:abort",
1794
2283
  "agent:done",
1795
2284
  "session:start",
@@ -1813,7 +2302,12 @@ function resolveBehavior(agentBehavior, runBehavior) {
1813
2302
  toolOutputBudget: runBehavior?.toolOutputBudget ?? agentBehavior?.toolOutputBudget,
1814
2303
  compactStrategy: runBehavior?.compactStrategy ?? agentBehavior?.compactStrategy ?? "off",
1815
2304
  compactThreshold: runBehavior?.compactThreshold ?? agentBehavior?.compactThreshold,
1816
- compactKeepTurns: runBehavior?.compactKeepTurns ?? agentBehavior?.compactKeepTurns
2305
+ compactKeepTurns: runBehavior?.compactKeepTurns ?? agentBehavior?.compactKeepTurns,
2306
+ thinkingDecay: runBehavior?.thinkingDecay ?? agentBehavior?.thinkingDecay,
2307
+ dedupReads: runBehavior?.dedupReads ?? agentBehavior?.dedupReads,
2308
+ dedupTools: runBehavior?.dedupTools ?? agentBehavior?.dedupTools,
2309
+ requireReadBeforeEdit: runBehavior?.requireReadBeforeEdit ?? agentBehavior?.requireReadBeforeEdit,
2310
+ toolBudgets: runBehavior?.toolBudgets ?? agentBehavior?.toolBudgets
1817
2311
  };
1818
2312
  }
1819
2313
  function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, mcpConnector, eager }) {
@@ -1936,7 +2430,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
1936
2430
  }
1937
2431
  const thinking = options.thinking ?? "off";
1938
2432
  const model = options.model ?? provider.meta.defaultModel;
1939
- const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns } = resolveBehavior(agentBehavior, options.behavior);
2433
+ const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
2434
+ const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets } = resolvedBehavior;
1940
2435
  let system = options.system || agentSystem || "You are a helpful assistant.";
1941
2436
  if (skillsCatalog) {
1942
2437
  system = `${system}
@@ -2036,6 +2531,16 @@ ${skillsCatalog}`;
2036
2531
  await hooks.callHook("session:end", { sessionId: session.id, runId, status, turnRange: [runTurnStart, turns.length - 1] });
2037
2532
  }
2038
2533
  const uninstallAllowedToolsGate = installAllowedToolsGate(hooks, skillActivationState);
2534
+ const uninstallToolBudgets = installToolBudgetsGate(
2535
+ hooks,
2536
+ () => toolBudgets,
2537
+ (msg) => steeringQueue.push(msg)
2538
+ );
2539
+ const uninstallDedupTools = installDedupToolsGate(
2540
+ hooks,
2541
+ () => dedupTools,
2542
+ () => session ?? void 0
2543
+ );
2039
2544
  const runStartMs = Date.now();
2040
2545
  const runDepth = typeof options.depth === "number" ? options.depth : 0;
2041
2546
  try {
@@ -2048,7 +2553,10 @@ ${skillsCatalog}`;
2048
2553
  agentToolAliases: toolAliases,
2049
2554
  agentMcpServers: mcpServers,
2050
2555
  agentSkills,
2051
- agentBehavior,
2556
+ // Forward the resolved view (agent + run merged) so per-run overrides
2557
+ // of `dedupReads` / `requireReadBeforeEdit` / etc. are visible to
2558
+ // tools via `ToolContext.behavior`.
2559
+ agentBehavior: resolvedBehavior,
2052
2560
  tools,
2053
2561
  formattedTools,
2054
2562
  aliasMaps,
@@ -2075,7 +2583,9 @@ ${skillsCatalog}`;
2075
2583
  compactStrategy,
2076
2584
  compactThreshold,
2077
2585
  compactKeepTurns,
2078
- runStartMs
2586
+ ...thinkingDecay !== void 0 ? { thinkingDecay } : {},
2587
+ runStartMs,
2588
+ runToolCounts: {}
2079
2589
  });
2080
2590
  const finalStats = {
2081
2591
  ...stats,
@@ -2117,6 +2627,8 @@ ${skillsCatalog}`;
2117
2627
  } finally {
2118
2628
  await deactivateAllSkills();
2119
2629
  uninstallAllowedToolsGate();
2630
+ uninstallDedupTools();
2631
+ uninstallToolBudgets();
2120
2632
  unregisterSpawnHook();
2121
2633
  unregisterSessionSync?.();
2122
2634
  for (const unregister of perRunUnregisters)
@@ -2519,7 +3031,7 @@ function createSpawnTool(options = {}) {
2519
3031
  var spawn = createSpawnTool();
2520
3032
 
2521
3033
  // src/tools/write-file.ts
2522
- import { Buffer as Buffer3 } from "buffer";
3034
+ import { Buffer as Buffer4 } from "buffer";
2523
3035
  var writeFile = {
2524
3036
  spec: {
2525
3037
  name: "write_file",
@@ -2541,7 +3053,7 @@ var writeFile = {
2541
3053
  existing = await ctx.execution.readFile(ctx.handle, targetPath);
2542
3054
  } catch {
2543
3055
  }
2544
- const bytes = Buffer3.byteLength(targetContent);
3056
+ const bytes = Buffer4.byteLength(targetContent);
2545
3057
  if (existing === targetContent)
2546
3058
  return `No change needed: ${targetPath} already at target state (${bytes} bytes).`;
2547
3059
  await ctx.execution.writeFile(ctx.handle, targetPath, targetContent);