zidane 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,15 +3,15 @@ import {
3
3
  createSkillActivationState,
4
4
  installAllowedToolsGate,
5
5
  interpolateShellCommands,
6
- resolveSkills,
6
+ resolveSkillsWithCleanup,
7
7
  validateResourcePath
8
- } from "./chunk-TPXPVEH6.js";
8
+ } from "./chunk-J4ZOSNSH.js";
9
9
  import {
10
10
  createProcessContext
11
- } from "./chunk-IUBBVF53.js";
11
+ } from "./chunk-UD25QF3H.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";
@@ -197,6 +197,17 @@ function getReadState(session) {
197
197
  }
198
198
  return map;
199
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
+ }
200
211
  function hashContent(text) {
201
212
  let h = 2166136261;
202
213
  for (let i = 0; i < text.length; i++) {
@@ -356,6 +367,18 @@ var glob = {
356
367
  }
357
368
  };
358
369
 
370
+ // src/tools/shell-quote.ts
371
+ var SAFE_TOKEN_RE = /^[\w@%+=:,./-]+$/;
372
+ var SINGLE_QUOTE_RE = /'/g;
373
+ function shellQuote(arg) {
374
+ if (SAFE_TOKEN_RE.test(arg))
375
+ return arg;
376
+ return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
377
+ }
378
+ function alwaysQuote(arg) {
379
+ return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
380
+ }
381
+
359
382
  // src/tools/grep.ts
360
383
  var DEFAULT_HEAD_LIMIT = 250;
361
384
  var DEFAULT_OUTPUT_MODE = "files_with_matches";
@@ -431,11 +454,6 @@ async function runViaRipgrep(input, ctx) {
431
454
  }
432
455
  return formatPaginated(result.stdout, input);
433
456
  }
434
- function shellQuote(arg) {
435
- if (/^[\w@%+=:,./-]+$/.test(arg))
436
- return arg;
437
- return `'${arg.replace(/'/g, "'\\''")}'`;
438
- }
439
457
  async function runInProcess(input, ctx) {
440
458
  const mode = input.output_mode ?? DEFAULT_OUTPUT_MODE;
441
459
  const flags = `${input["-i"] ? "i" : ""}${input.multiline ? "s" : ""}${mode !== "content" ? "" : "g"}`;
@@ -684,6 +702,33 @@ var multiEdit = {
684
702
  // src/tools/read-file.ts
685
703
  import { Buffer as Buffer2 } from "buffer";
686
704
 
705
+ // src/tools/binary-detect.ts
706
+ var SNIFF_BYTES = 8192;
707
+ var REPLACEMENT_RATIO_THRESHOLD = 0.01;
708
+ var REPLACEMENT_MIN_COUNT = 5;
709
+ function containsNullByte(text, sniffBytes = SNIFF_BYTES) {
710
+ const sample = text.length > sniffBytes ? text.slice(0, sniffBytes) : text;
711
+ for (let i = 0; i < sample.length; i++) {
712
+ if (sample.charCodeAt(i) === 0)
713
+ return true;
714
+ }
715
+ return false;
716
+ }
717
+ function looksBinary(text, sniffBytes = SNIFF_BYTES) {
718
+ const sample = text.length > sniffBytes ? text.slice(0, sniffBytes) : text;
719
+ if (sample.length === 0)
720
+ return false;
721
+ let replacementCount = 0;
722
+ for (let i = 0; i < sample.length; i++) {
723
+ const code = sample.charCodeAt(i);
724
+ if (code === 0)
725
+ return true;
726
+ if (code === 65533)
727
+ replacementCount++;
728
+ }
729
+ return replacementCount >= REPLACEMENT_MIN_COUNT && replacementCount / sample.length > REPLACEMENT_RATIO_THRESHOLD;
730
+ }
731
+
687
732
  // src/tools/binary-read.ts
688
733
  import { Buffer } from "buffer";
689
734
  function imageMediaTypeFor(path) {
@@ -711,21 +756,27 @@ async function readFileAsBase64(execution, handle, path) {
711
756
  const b642 = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
712
757
  return { base64: b642, byteLength: bytes.byteLength };
713
758
  }
714
- const cmd = `base64 < ${shellQuote2(path)}`;
759
+ const cmd = `base64 < ${alwaysQuote(path)}`;
715
760
  const result = await execution.exec(handle, cmd);
716
761
  if (result.exitCode !== 0)
717
762
  throw new Error(`base64 read failed: ${result.stderr || `exit ${result.exitCode}`}`);
718
763
  const b64 = result.stdout.replace(/\s+/g, "");
719
- return { base64: b64, byteLength: Math.floor(b64.length * 3 / 4) };
764
+ return { base64: b64, byteLength: decodedBase64ByteLength(b64) };
720
765
  }
721
- function shellQuote2(s) {
722
- return `'${s.replace(/'/g, "'\\''")}'`;
766
+ function decodedBase64ByteLength(b64) {
767
+ if (b64.length === 0)
768
+ return 0;
769
+ let pad = 0;
770
+ if (b64.endsWith("=="))
771
+ pad = 2;
772
+ else if (b64.endsWith("="))
773
+ pad = 1;
774
+ return Math.max(0, b64.length * 3 / 4 - pad);
723
775
  }
724
776
 
725
777
  // src/tools/read-file.ts
726
778
  var DEFAULT_LINE_LIMIT = 2e3;
727
779
  var DEFAULT_BYTE_CAP = 65536;
728
- var BINARY_PROBE_BYTES = 8e3;
729
780
  var DEFAULT_IMAGE_BYTE_CAP = 5 * 1024 * 1024;
730
781
  var readFile = {
731
782
  spec: {
@@ -868,21 +919,6 @@ function normalizeInteger(value, fallback) {
868
919
  return fallback;
869
920
  return Math.floor(value);
870
921
  }
871
- var REPLACEMENT_RATIO_THRESHOLD = 0.01;
872
- var REPLACEMENT_MIN_COUNT = 5;
873
- function looksBinary(text) {
874
- const sample = text.length > BINARY_PROBE_BYTES ? text.slice(0, BINARY_PROBE_BYTES) : text;
875
- if (sample.includes("\0"))
876
- return true;
877
- if (sample.length === 0)
878
- return false;
879
- let replacementCount = 0;
880
- for (let i = 0; i < sample.length; i++) {
881
- if (sample.charCodeAt(i) === 65533)
882
- replacementCount++;
883
- }
884
- return replacementCount >= REPLACEMENT_MIN_COUNT && replacementCount / sample.length > REPLACEMENT_RATIO_THRESHOLD;
885
- }
886
922
 
887
923
  // src/tools/shell.ts
888
924
  import { Buffer as Buffer3 } from "buffer";
@@ -985,15 +1021,6 @@ ${tail}`;
985
1021
  }
986
1022
 
987
1023
  // src/tools/skills-read.ts
988
- var SNIFF_BYTES = 8192;
989
- function looksBinary2(text) {
990
- const len = Math.min(text.length, SNIFF_BYTES);
991
- for (let i = 0; i < len; i++) {
992
- if (text.charCodeAt(i) === 0)
993
- return true;
994
- }
995
- return false;
996
- }
997
1024
  function createSkillsReadTool(options) {
998
1025
  const byName = new Map(options.catalog.map((s) => [s.name, s]));
999
1026
  return {
@@ -1038,7 +1065,7 @@ function createSkillsReadTool(options) {
1038
1065
  const message = err instanceof Error ? err.message : String(err);
1039
1066
  return `Error reading "${relPath}" in skill "${skillName}": ${message}`;
1040
1067
  }
1041
- if (looksBinary2(content)) {
1068
+ if (containsNullByte(content)) {
1042
1069
  return JSON.stringify({
1043
1070
  kind: "binary-unsupported",
1044
1071
  path: validated.absolutePath,
@@ -1051,12 +1078,8 @@ function createSkillsReadTool(options) {
1051
1078
  }
1052
1079
 
1053
1080
  // src/tools/skills-run-script.ts
1054
- var SINGLE_QUOTE_RE = /'/g;
1055
1081
  var ABS_WINDOWS_RE = /^[a-z]:[\\/]/i;
1056
1082
  var COLLAPSE_SLASHES_RE = /\/+/g;
1057
- function quoteShellArg(arg) {
1058
- return `'${arg.replace(SINGLE_QUOTE_RE, `'\\''`)}'`;
1059
- }
1060
1083
  function createSkillsRunScriptTool(options) {
1061
1084
  const byName = new Map(options.catalog.map((s) => [s.name, s]));
1062
1085
  const timeoutMs = options.scriptTimeoutMs ?? 6e4;
@@ -1103,7 +1126,7 @@ function createSkillsRunScriptTool(options) {
1103
1126
  const validated = validateResourcePath(joinedPath, skill.baseDir);
1104
1127
  if (!validated.valid)
1105
1128
  return `Error: ${validated.error}`;
1106
- const cmd = [validated.absolutePath, ...args].map(quoteShellArg).join(" ");
1129
+ const cmd = [validated.absolutePath, ...args].map(alwaysQuote).join(" ");
1107
1130
  try {
1108
1131
  const result = await ctx.execution.exec(ctx.handle, cmd, {
1109
1132
  timeout: Math.max(1, Math.round(timeoutMs / 1e3))
@@ -1279,6 +1302,59 @@ function rewriteMessagesToWire(messages, maps) {
1279
1302
  return messages.map((msg) => ({ ...msg, content: rewriteContentToWire(msg.content, maps) }));
1280
1303
  }
1281
1304
 
1305
+ // src/dedup-tools.ts
1306
+ function installDedupToolsGate(hooks, getDedupTools, getSession) {
1307
+ const pending = /* @__PURE__ */ new Map();
1308
+ function pendingKey(callId, name) {
1309
+ return `${callId}::${name}`;
1310
+ }
1311
+ function gateHandler(ctx) {
1312
+ if (ctx.block || ctx.result !== void 0)
1313
+ return;
1314
+ const dedupTools = getDedupTools();
1315
+ const hasher = dedupTools?.[ctx.name];
1316
+ if (!hasher)
1317
+ return;
1318
+ const session = getSession();
1319
+ const state = getToolDedupState(session);
1320
+ if (!state)
1321
+ return;
1322
+ let hash;
1323
+ try {
1324
+ hash = hasher(ctx.input);
1325
+ } catch {
1326
+ return;
1327
+ }
1328
+ if (typeof hash !== "string" || hash.length === 0)
1329
+ return;
1330
+ const prior = state.get(ctx.name);
1331
+ if (prior && prior.hash === hash) {
1332
+ ctx.result = prior.result;
1333
+ return;
1334
+ }
1335
+ pending.set(pendingKey(ctx.callId, ctx.name), hash);
1336
+ }
1337
+ function afterHandler(ctx) {
1338
+ const key = pendingKey(ctx.callId, ctx.name);
1339
+ const hash = pending.get(key);
1340
+ if (hash === void 0)
1341
+ return;
1342
+ pending.delete(key);
1343
+ const session = getSession();
1344
+ const state = getToolDedupState(session);
1345
+ if (!state)
1346
+ return;
1347
+ state.set(ctx.name, { hash, result: ctx.result });
1348
+ }
1349
+ const unregisterGate = hooks.hook("tool:gate", gateHandler);
1350
+ const unregisterAfter = hooks.hook("tool:after", afterHandler);
1351
+ return function uninstall() {
1352
+ unregisterGate();
1353
+ unregisterAfter();
1354
+ pending.clear();
1355
+ };
1356
+ }
1357
+
1282
1358
  // src/tools/validation.ts
1283
1359
  var TRUE_STRINGS = /* @__PURE__ */ new Set(["true", "True", "TRUE", "1", "yes", "Yes", "YES"]);
1284
1360
  var FALSE_STRINGS = /* @__PURE__ */ new Set(["false", "False", "FALSE", "0", "no", "No", "NO"]);
@@ -1434,6 +1510,24 @@ function formatValue(value) {
1434
1510
 
1435
1511
  // src/loop.ts
1436
1512
  var IMAGE_OMITTED_MARKER = "[image omitted \u2014 model does not support vision]";
1513
+ function applyThinkingDecay(baseBudget, decay, turn) {
1514
+ if (typeof baseBudget !== "number" || baseBudget <= 0)
1515
+ return baseBudget;
1516
+ if (!decay)
1517
+ return baseBudget;
1518
+ let raw;
1519
+ if (typeof decay === "function") {
1520
+ raw = decay(turn, baseBudget);
1521
+ } else {
1522
+ if (turn <= decay.afterTurn)
1523
+ return baseBudget;
1524
+ const k = turn - decay.afterTurn;
1525
+ raw = Math.max(decay.floor, baseBudget * decay.factor ** k);
1526
+ }
1527
+ if (Number.isNaN(raw) || raw <= 0)
1528
+ return 0;
1529
+ return Math.round(Math.min(baseBudget, raw));
1530
+ }
1437
1531
  function turnsToMessages(turns) {
1438
1532
  return turns.filter((t) => t.role !== "system").map((t) => ({ role: t.role, content: t.content }));
1439
1533
  }
@@ -1589,6 +1683,7 @@ async function executeTurn(ctx, turn) {
1589
1683
  const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
1590
1684
  sanitizedMessages = applyTailCompaction(sanitizedMessages, threshold, keep);
1591
1685
  }
1686
+ const effectiveThinkingBudget = applyThinkingDecay(ctx.thinkingBudget, ctx.thinkingDecay, turn);
1592
1687
  const streamOptions = {
1593
1688
  model: ctx.model,
1594
1689
  system: ctx.system,
@@ -1596,13 +1691,22 @@ async function executeTurn(ctx, turn) {
1596
1691
  messages: sanitizedMessages,
1597
1692
  maxTokens: ctx.maxTokens ?? 16384,
1598
1693
  thinking: ctx.thinking,
1599
- thinkingBudget: ctx.thinkingBudget,
1694
+ thinkingBudget: effectiveThinkingBudget,
1600
1695
  cache: ctx.cache ?? true,
1601
1696
  signal: ctx.signal
1602
1697
  };
1603
1698
  const transformCtx = { messages: streamOptions.messages };
1604
1699
  await ctx.hooks.callHook("context:transform", transformCtx);
1605
1700
  streamOptions.messages = transformCtx.messages;
1701
+ const systemCtx = {
1702
+ system: streamOptions.system,
1703
+ messages: streamOptions.messages,
1704
+ turn,
1705
+ turnId,
1706
+ ...ctx.session ? { session: ctx.session } : {}
1707
+ };
1708
+ await ctx.hooks.callHook("system:transform", systemCtx);
1709
+ streamOptions.system = systemCtx.system;
1606
1710
  await ctx.hooks.callHook("turn:before", { turn, turnId, options: streamOptions });
1607
1711
  let currentText = "";
1608
1712
  let currentThinking = "";
@@ -1626,16 +1730,23 @@ async function executeTurn(ctx, turn) {
1626
1730
  );
1627
1731
  } catch (err) {
1628
1732
  const errorUsage = { input: 0, output: 0 };
1733
+ const errorContent = currentText ? [{ type: "text", text: currentText }] : [{ type: "text", text: "[provider error before any output]" }];
1629
1734
  const errorTurn = {
1630
1735
  id: turnId,
1631
1736
  runId: ctx.runId,
1632
1737
  role: "assistant",
1633
- content: currentText ? [{ type: "text", text: currentText }] : [],
1738
+ content: errorContent,
1634
1739
  usage: errorUsage,
1635
1740
  createdAt: Date.now()
1636
1741
  };
1637
1742
  ctx.turns.push(errorTurn);
1638
- await ctx.hooks.callHook("turn:after", { turn, turnId, usage: errorUsage, message: errorTurn });
1743
+ await ctx.hooks.callHook("turn:after", {
1744
+ turn,
1745
+ turnId,
1746
+ usage: errorUsage,
1747
+ message: errorTurn,
1748
+ toolCounts: { turn: Object.freeze({}), run: Object.freeze({ ...ctx.runToolCounts }) }
1749
+ });
1639
1750
  throw wrapProviderError(err, ctx);
1640
1751
  }
1641
1752
  if (currentText) {
@@ -1658,7 +1769,16 @@ async function executeTurn(ctx, turn) {
1658
1769
  createdAt: Date.now()
1659
1770
  };
1660
1771
  ctx.turns.push(assistantTurn);
1661
- await ctx.hooks.callHook("turn:after", { turn, turnId, usage: result.usage, message: assistantTurn });
1772
+ const turnCounts = {};
1773
+ for (const tc of canonicalToolCalls)
1774
+ turnCounts[tc.name] = (turnCounts[tc.name] ?? 0) + 1;
1775
+ await ctx.hooks.callHook("turn:after", {
1776
+ turn,
1777
+ turnId,
1778
+ usage: result.usage,
1779
+ message: assistantTurn,
1780
+ toolCounts: { turn: Object.freeze(turnCounts), run: Object.freeze({ ...ctx.runToolCounts }) }
1781
+ });
1662
1782
  if (result.done) {
1663
1783
  if (ctx.schema && !ctx.signal.aborted) {
1664
1784
  const outputSpec = {
@@ -1771,6 +1891,7 @@ async function executeSingleTool(ctx, call, turnId) {
1771
1891
  const toolDef = ctx.tools[call.name];
1772
1892
  const callId = call.id;
1773
1893
  const displayName = toWireName(call.name, ctx.aliasMaps);
1894
+ const runToolCounts = Object.freeze({ ...ctx.runToolCounts });
1774
1895
  const gateCtx = {
1775
1896
  turnId,
1776
1897
  callId,
@@ -1778,12 +1899,27 @@ async function executeSingleTool(ctx, call, turnId) {
1778
1899
  displayName,
1779
1900
  input: call.input,
1780
1901
  block: false,
1781
- reason: "Tool execution was blocked"
1902
+ reason: "Tool execution was blocked",
1903
+ runToolCounts
1782
1904
  };
1783
1905
  await ctx.hooks.callHook("tool:gate", gateCtx);
1784
1906
  if (gateCtx.block) {
1785
1907
  return { result: { id: callId, content: `Blocked: ${gateCtx.reason}` } };
1786
1908
  }
1909
+ ctx.runToolCounts[call.name] = (ctx.runToolCounts[call.name] ?? 0) + 1;
1910
+ if (gateCtx.result !== void 0) {
1911
+ const substitute = await emitToolResult(ctx, {
1912
+ turnId,
1913
+ callId,
1914
+ name: call.name,
1915
+ displayName,
1916
+ input: gateCtx.input,
1917
+ output: gateCtx.result,
1918
+ isError: false,
1919
+ runToolCounts
1920
+ });
1921
+ return { result: { id: callId, content: substitute } };
1922
+ }
1787
1923
  let effectiveInput = gateCtx.input;
1788
1924
  if (!toolDef) {
1789
1925
  const unknownCtx = {
@@ -1841,6 +1977,7 @@ async function executeSingleTool(ctx, call, turnId) {
1841
1977
  name: call.name,
1842
1978
  displayName,
1843
1979
  input: effectiveInput,
1980
+ runToolCounts,
1844
1981
  ...coercions ? { coercions } : {}
1845
1982
  });
1846
1983
  let output;
@@ -1880,12 +2017,29 @@ async function executeSingleTool(ctx, call, turnId) {
1880
2017
  output = errorCtx.result ?? `Tool error: ${error.message}`;
1881
2018
  isError = true;
1882
2019
  }
1883
- const transformCtx = {
2020
+ const finalOutput = await emitToolResult(ctx, {
1884
2021
  turnId,
1885
2022
  callId,
1886
2023
  name: call.name,
1887
2024
  displayName,
1888
2025
  input: effectiveInput,
2026
+ output,
2027
+ isError,
2028
+ runToolCounts,
2029
+ ...coercions ? { coercions } : {}
2030
+ });
2031
+ return { result: { id: callId, content: finalOutput } };
2032
+ }
2033
+ async function emitToolResult(ctx, params) {
2034
+ const { turnId, callId, name, displayName, input, runToolCounts, coercions } = params;
2035
+ let output = params.output;
2036
+ let isError = params.isError;
2037
+ const transformCtx = {
2038
+ turnId,
2039
+ callId,
2040
+ name,
2041
+ displayName,
2042
+ input,
1889
2043
  result: output,
1890
2044
  isError,
1891
2045
  outputBytes: toolOutputByteLength(output),
@@ -1898,14 +2052,15 @@ async function executeSingleTool(ctx, call, turnId) {
1898
2052
  await ctx.hooks.callHook("tool:after", {
1899
2053
  turnId,
1900
2054
  callId,
1901
- name: call.name,
2055
+ name,
1902
2056
  displayName,
1903
- input: effectiveInput,
2057
+ input,
1904
2058
  result: output,
1905
2059
  outputBytes: toolOutputByteLength(output),
2060
+ runToolCounts,
1906
2061
  ...coercions ? { coercions } : {}
1907
2062
  });
1908
- return { result: { id: callId, content: output } };
2063
+ return output;
1909
2064
  }
1910
2065
  async function executeToolsSequential(ctx, toolCalls, turnId) {
1911
2066
  const results = [];
@@ -1913,28 +2068,10 @@ async function executeToolsSequential(ctx, toolCalls, turnId) {
1913
2068
  if (ctx.signal.aborted)
1914
2069
  break;
1915
2070
  if (ctx.steeringQueue.length > 0) {
1916
- const steerMsg = ctx.steeringQueue.shift();
1917
- await ctx.hooks.callHook("steer:inject", { message: steerMsg });
1918
- for (const skipped of toolCalls.slice(toolCalls.indexOf(call))) {
1919
- results.push({ id: skipped.id, content: "Skipped: steering message received" });
1920
- }
1921
- const toolResultMsg = ctx.provider.toolResultsMessage(results);
1922
- ctx.turns.push({
1923
- id: await ctx.generateTurnId(),
1924
- runId: ctx.runId,
1925
- role: toolResultMsg.role,
1926
- content: toolResultMsg.content,
1927
- createdAt: Date.now()
1928
- });
1929
- const steerUserMsg = ctx.provider.userMessage(steerMsg);
1930
- ctx.turns.push({
1931
- id: await ctx.generateTurnId(),
1932
- runId: ctx.runId,
1933
- role: steerUserMsg.role,
1934
- content: steerUserMsg.content,
1935
- createdAt: Date.now()
1936
- });
1937
- return [];
2071
+ const fromIdx = toolCalls.indexOf(call);
2072
+ for (let i = fromIdx; i < toolCalls.length; i++)
2073
+ results.push({ id: toolCalls[i].id, content: "Skipped: steering message received" });
2074
+ return results;
1938
2075
  }
1939
2076
  const { result } = await executeSingleTool(ctx, call, turnId);
1940
2077
  results.push(result);
@@ -1978,6 +2115,15 @@ function canonicalizePrompt(prompt, legacyImages) {
1978
2115
  }
1979
2116
  if (prompt.length === 0)
1980
2117
  return void 0;
2118
+ for (const part of prompt) {
2119
+ if (!part || typeof part !== "object" || typeof part.type !== "string") {
2120
+ throw new Error("Invalid PromptPart: each part must be an object with a `type` field.");
2121
+ }
2122
+ const type = part.type;
2123
+ if (type !== "text" && type !== "image" && type !== "document") {
2124
+ throw new Error(`Invalid PromptPart type "${type}". Expected "text" | "image" | "document".`);
2125
+ }
2126
+ }
1981
2127
  const hasMeaningfulPart = prompt.some((part) => part.type === "text" && part.text.length > 0 || part.type === "image" || part.type === "document");
1982
2128
  if (!hasMeaningfulPart)
1983
2129
  return void 0;
@@ -2014,6 +2160,81 @@ function buildPromptMessage(provider, parts) {
2014
2160
  return defaultPromptMessage(parts);
2015
2161
  }
2016
2162
 
2163
+ // src/tool-budgets.ts
2164
+ function installToolBudgetsGate(hooks, getToolBudgets, enqueueSteer) {
2165
+ const steeredOnce = /* @__PURE__ */ new Set();
2166
+ const approvedCounts = {};
2167
+ async function gateHandler(ctx) {
2168
+ if (ctx.block || ctx.result !== void 0)
2169
+ return;
2170
+ const toolBudgets = getToolBudgets();
2171
+ const budget = toolBudgets?.[ctx.name];
2172
+ if (!budget)
2173
+ return;
2174
+ const max = budget.max;
2175
+ if (typeof max !== "number" || max <= 0)
2176
+ return;
2177
+ const count = approvedCounts[ctx.name] ?? 0;
2178
+ if (count < max) {
2179
+ approvedCounts[ctx.name] = count + 1;
2180
+ return;
2181
+ }
2182
+ const onExceed = budget.onExceed ?? "steer";
2183
+ let mode;
2184
+ let message;
2185
+ if (typeof onExceed === "function") {
2186
+ try {
2187
+ const out = onExceed({ tool: ctx.name, count, max });
2188
+ mode = out.mode;
2189
+ message = out.message;
2190
+ } catch {
2191
+ mode = "steer";
2192
+ message = defaultSteerMessage(ctx.name, count, max);
2193
+ }
2194
+ } else if (onExceed === "block") {
2195
+ mode = "block";
2196
+ message = defaultBlockMessage(ctx.name, max);
2197
+ } else {
2198
+ mode = "steer";
2199
+ message = defaultSteerMessage(ctx.name, count, max);
2200
+ }
2201
+ if (mode === "block") {
2202
+ ctx.block = true;
2203
+ ctx.reason = message;
2204
+ await hooks.callHook("tool-budget:exceeded", {
2205
+ tool: ctx.name,
2206
+ count,
2207
+ max,
2208
+ turnId: ctx.turnId,
2209
+ mode: "block"
2210
+ });
2211
+ return;
2212
+ }
2213
+ if (!steeredOnce.has(ctx.name)) {
2214
+ steeredOnce.add(ctx.name);
2215
+ enqueueSteer(message);
2216
+ await hooks.callHook("tool-budget:exceeded", {
2217
+ tool: ctx.name,
2218
+ count,
2219
+ max,
2220
+ turnId: ctx.turnId,
2221
+ mode: "steer"
2222
+ });
2223
+ }
2224
+ }
2225
+ const unregister = hooks.hook("tool:gate", gateHandler);
2226
+ return function uninstall() {
2227
+ unregister();
2228
+ steeredOnce.clear();
2229
+ };
2230
+ }
2231
+ function defaultSteerMessage(tool, count, max) {
2232
+ 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.]`;
2233
+ }
2234
+ function defaultBlockMessage(tool, max) {
2235
+ return `Tool '${tool}' has reached its per-run budget of ${max} calls; further invocations are refused.`;
2236
+ }
2237
+
2017
2238
  // src/agent.ts
2018
2239
  var HOOK_EVENT_NAMES = [
2019
2240
  "system:before",
@@ -2032,6 +2253,7 @@ var HOOK_EVENT_NAMES = [
2032
2253
  "validation:reject",
2033
2254
  "validation:coerce",
2034
2255
  "context:transform",
2256
+ "system:transform",
2035
2257
  "steer:inject",
2036
2258
  "spawn:before",
2037
2259
  "spawn:complete",
@@ -2060,6 +2282,7 @@ var HOOK_EVENT_NAMES = [
2060
2282
  "usage",
2061
2283
  "output",
2062
2284
  "budget:exceeded",
2285
+ "tool-budget:exceeded",
2063
2286
  "agent:abort",
2064
2287
  "agent:done",
2065
2288
  "session:start",
@@ -2083,7 +2306,12 @@ function resolveBehavior(agentBehavior, runBehavior) {
2083
2306
  toolOutputBudget: runBehavior?.toolOutputBudget ?? agentBehavior?.toolOutputBudget,
2084
2307
  compactStrategy: runBehavior?.compactStrategy ?? agentBehavior?.compactStrategy ?? "off",
2085
2308
  compactThreshold: runBehavior?.compactThreshold ?? agentBehavior?.compactThreshold,
2086
- compactKeepTurns: runBehavior?.compactKeepTurns ?? agentBehavior?.compactKeepTurns
2309
+ compactKeepTurns: runBehavior?.compactKeepTurns ?? agentBehavior?.compactKeepTurns,
2310
+ thinkingDecay: runBehavior?.thinkingDecay ?? agentBehavior?.thinkingDecay,
2311
+ dedupReads: runBehavior?.dedupReads ?? agentBehavior?.dedupReads,
2312
+ dedupTools: runBehavior?.dedupTools ?? agentBehavior?.dedupTools,
2313
+ requireReadBeforeEdit: runBehavior?.requireReadBeforeEdit ?? agentBehavior?.requireReadBeforeEdit,
2314
+ toolBudgets: runBehavior?.toolBudgets ?? agentBehavior?.toolBudgets
2087
2315
  };
2088
2316
  }
2089
2317
  function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, mcpConnector, eager }) {
@@ -2107,6 +2335,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2107
2335
  const skillsDisabled = skillsEnabledValue === false || Array.isArray(skillsEnabledValue) && skillsEnabledValue.length === 0;
2108
2336
  let resolvedSkills = null;
2109
2337
  let skillsCatalog = null;
2338
+ let skillsCleanup = () => {
2339
+ };
2110
2340
  const skillActivationState = createSkillActivationState({
2111
2341
  maxActive: skillsConfig?.maxActive
2112
2342
  });
@@ -2174,7 +2404,9 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2174
2404
  await warmup();
2175
2405
  }
2176
2406
  if (!skillsDisabled && skillsConfig && !resolvedSkills) {
2177
- resolvedSkills = await resolveSkills(skillsConfig);
2407
+ const bundle = await resolveSkillsWithCleanup(skillsConfig);
2408
+ resolvedSkills = bundle.skills;
2409
+ skillsCleanup = bundle.cleanup;
2178
2410
  await hooks.callHook("skills:resolve", { skills: resolvedSkills });
2179
2411
  const skillsToolRegistered = skillsConfig?.tool !== false && resolvedSkills.length > 0;
2180
2412
  const catalogCtx = {
@@ -2206,7 +2438,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
2206
2438
  }
2207
2439
  const thinking = options.thinking ?? "off";
2208
2440
  const model = options.model ?? provider.meta.defaultModel;
2209
- const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns } = resolveBehavior(agentBehavior, options.behavior);
2441
+ const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
2442
+ const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets } = resolvedBehavior;
2210
2443
  let system = options.system || agentSystem || "You are a helpful assistant.";
2211
2444
  if (skillsCatalog) {
2212
2445
  system = `${system}
@@ -2306,6 +2539,16 @@ ${skillsCatalog}`;
2306
2539
  await hooks.callHook("session:end", { sessionId: session.id, runId, status, turnRange: [runTurnStart, turns.length - 1] });
2307
2540
  }
2308
2541
  const uninstallAllowedToolsGate = installAllowedToolsGate(hooks, skillActivationState);
2542
+ const uninstallToolBudgets = installToolBudgetsGate(
2543
+ hooks,
2544
+ () => toolBudgets,
2545
+ (msg) => steeringQueue.push(msg)
2546
+ );
2547
+ const uninstallDedupTools = installDedupToolsGate(
2548
+ hooks,
2549
+ () => dedupTools,
2550
+ () => session ?? void 0
2551
+ );
2309
2552
  const runStartMs = Date.now();
2310
2553
  const runDepth = typeof options.depth === "number" ? options.depth : 0;
2311
2554
  try {
@@ -2318,7 +2561,10 @@ ${skillsCatalog}`;
2318
2561
  agentToolAliases: toolAliases,
2319
2562
  agentMcpServers: mcpServers,
2320
2563
  agentSkills,
2321
- agentBehavior,
2564
+ // Forward the resolved view (agent + run merged) so per-run overrides
2565
+ // of `dedupReads` / `requireReadBeforeEdit` / etc. are visible to
2566
+ // tools via `ToolContext.behavior`.
2567
+ agentBehavior: resolvedBehavior,
2322
2568
  tools,
2323
2569
  formattedTools,
2324
2570
  aliasMaps,
@@ -2345,7 +2591,9 @@ ${skillsCatalog}`;
2345
2591
  compactStrategy,
2346
2592
  compactThreshold,
2347
2593
  compactKeepTurns,
2348
- runStartMs
2594
+ ...thinkingDecay !== void 0 ? { thinkingDecay } : {},
2595
+ runStartMs,
2596
+ runToolCounts: {}
2349
2597
  });
2350
2598
  const finalStats = {
2351
2599
  ...stats,
@@ -2387,6 +2635,8 @@ ${skillsCatalog}`;
2387
2635
  } finally {
2388
2636
  await deactivateAllSkills();
2389
2637
  uninstallAllowedToolsGate();
2638
+ uninstallDedupTools();
2639
+ uninstallToolBudgets();
2390
2640
  unregisterSpawnHook();
2391
2641
  unregisterSessionSync?.();
2392
2642
  for (const unregister of perRunUnregisters)
@@ -2413,6 +2663,11 @@ ${skillsCatalog}`;
2413
2663
  return idlePromise ?? Promise.resolve();
2414
2664
  }
2415
2665
  async function reset() {
2666
+ if (running) {
2667
+ throw new Error(
2668
+ "Cannot reset() while the agent is running. Call `agent.abort()` and `await agent.waitForIdle()` first."
2669
+ );
2670
+ }
2416
2671
  conversationTurns = [];
2417
2672
  steeringQueue.length = 0;
2418
2673
  followUpQueue.length = 0;
@@ -2501,6 +2756,9 @@ ${skillsCatalog}`;
2501
2756
  await executionContext.destroy(executionHandle);
2502
2757
  executionHandle = null;
2503
2758
  }
2759
+ skillsCleanup();
2760
+ skillsCleanup = () => {
2761
+ };
2504
2762
  }
2505
2763
  if (eager && allMcpServers.length > 0) {
2506
2764
  void warmup().catch(() => {