zidane 5.6.11 → 5.6.13

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 (64) hide show
  1. package/README.md +19 -2
  2. package/dist/{agent-C9AKTU_V.d.ts → agent-ClkpElCZ.d.ts} +540 -55
  3. package/dist/agent-ClkpElCZ.d.ts.map +1 -0
  4. package/dist/chat.d.ts +47 -17
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +3 -3
  7. package/dist/{index-6f4T7Gc0.d.ts → index-CTDMMdIy.d.ts} +348 -3
  8. package/dist/index-CTDMMdIy.d.ts.map +1 -0
  9. package/dist/{index-DPN7TcXK.d.ts → index-v3Tzobqr.d.ts} +2 -2
  10. package/dist/{index-DPN7TcXK.d.ts.map → index-v3Tzobqr.d.ts.map} +1 -1
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +169 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/{login-BindcfKi.js → login-DS3sf6b5.js} +4 -4
  15. package/dist/{login-BindcfKi.js.map → login-DS3sf6b5.js.map} +1 -1
  16. package/dist/{mcp-0jRkIV0g.js → mcp-DGeB7-3D.js} +13 -2
  17. package/dist/mcp-DGeB7-3D.js.map +1 -0
  18. package/dist/mcp.d.ts +1 -1
  19. package/dist/mcp.js +1 -1
  20. package/dist/{messages-BfmXLDT4.js → messages-Dym8S_YH.js} +303 -8
  21. package/dist/messages-Dym8S_YH.js.map +1 -0
  22. package/dist/{presets-CmzMeWg2.js → presets-CZXS_87d.js} +2 -2
  23. package/dist/{presets-CmzMeWg2.js.map → presets-CZXS_87d.js.map} +1 -1
  24. package/dist/presets.d.ts +2 -2
  25. package/dist/presets.js +1 -1
  26. package/dist/{providers-C_ahnRBS.js → providers-beXyD9W9.js} +137 -21
  27. package/dist/providers-beXyD9W9.js.map +1 -0
  28. package/dist/providers.d.ts +2 -2
  29. package/dist/providers.js +3 -3
  30. package/dist/restate.d.ts +1 -1
  31. package/dist/session/sqlite.d.ts +1 -1
  32. package/dist/{session-PUzXZlG6.js → session-BRIsmBSY.js} +5 -2
  33. package/dist/session-BRIsmBSY.js.map +1 -0
  34. package/dist/session.d.ts +2 -2
  35. package/dist/session.js +3 -3
  36. package/dist/skills.d.ts +2 -2
  37. package/dist/{tools-CxOfTt3R.js → tools-DE9pR_NG.js} +515 -116
  38. package/dist/tools-DE9pR_NG.js.map +1 -0
  39. package/dist/tools.d.ts +3 -3
  40. package/dist/tools.js +1 -1
  41. package/dist/{transcript-anchors-DDCHSDdX.d.ts → transcript-anchors-D0TR6djV.d.ts} +4 -4
  42. package/dist/transcript-anchors-D0TR6djV.d.ts.map +1 -0
  43. package/dist/tui.d.ts +2 -2
  44. package/dist/tui.d.ts.map +1 -1
  45. package/dist/tui.js +12 -8
  46. package/dist/tui.js.map +1 -1
  47. package/dist/{turn-operations-CxE8BBau.js → turn-operations-6Yls2HuG.js} +907 -42
  48. package/dist/turn-operations-6Yls2HuG.js.map +1 -0
  49. package/dist/types-oKPBdCmL.js.map +1 -1
  50. package/dist/types.d.ts +3 -3
  51. package/docs/ARCHITECTURE.md +101 -20
  52. package/docs/CHAT.md +27 -5
  53. package/docs/RESTATE.md +1 -1
  54. package/docs/SKILL.md +39 -3
  55. package/package.json +5 -2
  56. package/dist/agent-C9AKTU_V.d.ts.map +0 -1
  57. package/dist/index-6f4T7Gc0.d.ts.map +0 -1
  58. package/dist/mcp-0jRkIV0g.js.map +0 -1
  59. package/dist/messages-BfmXLDT4.js.map +0 -1
  60. package/dist/providers-C_ahnRBS.js.map +0 -1
  61. package/dist/session-PUzXZlG6.js.map +0 -1
  62. package/dist/tools-CxOfTt3R.js.map +0 -1
  63. package/dist/transcript-anchors-DDCHSDdX.d.ts.map +0 -1
  64. package/dist/turn-operations-CxE8BBau.js.map +0 -1
@@ -1,14 +1,14 @@
1
1
  import { n as createProcessContext } from "./contexts-BOtMvzli.js";
2
2
  import { c as errorMessage, i as AgentProviderError, n as AgentBudgetExceededError, o as AgentToolPairingError, t as AgentAbortedError, u as toTypedError } from "./errors-DdZXnyXE.js";
3
3
  import { n as toolOutputByteLength, t as DEFAULT_AGENT_CLOCK } from "./types-oKPBdCmL.js";
4
- import { a as detectTurnInterruption, c as filterUnresolvedToolUses, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-BfmXLDT4.js";
5
- import { t as connectMcpServers } from "./mcp-0jRkIV0g.js";
4
+ import { E as appendStaticSection, a as detectTurnInterruption, c as filterUnresolvedToolUses, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureEndsWithUserMessage, s as ensureToolResultPairing } from "./messages-Dym8S_YH.js";
5
+ import { t as connectMcpServers } from "./mcp-DGeB7-3D.js";
6
6
  import { _ as validateResourcePath, b as createSkillActivationState, d as escapeXml, n as resolveSkills, p as installAllowedToolsGate, t as interpolateShellCommands, u as buildCatalog } from "./interpolate-DM1UcKeQ.js";
7
7
  import { n as formatTokenUsage, t as flattenTurns } from "./stats-Lc3zL3RM.js";
8
8
  import { dirname, isAbsolute, join, resolve } from "node:path";
9
9
  import { createHooks } from "hookable";
10
10
  import { homedir } from "node:os";
11
- import { mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
11
+ import { glob, mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
12
12
  import { Buffer } from "node:buffer";
13
13
  //#region src/aliasing.ts
14
14
  /**
@@ -60,6 +60,45 @@ function toCanonicalName(wire, maps) {
60
60
  return maps.canonicalByAlias.get(wire) ?? wire;
61
61
  }
62
62
  /**
63
+ * Augment {@link AliasMaps} with INBOUND-ONLY aliases for the Claude Code
64
+ * `mcp__server__tool` double-underscore naming convention. For every MCP
65
+ * tool registered with the canonical `mcp_<server>_<tool>` form, add a
66
+ * `canonicalByAlias` entry mapping the double-underscore variant to the
67
+ * same canonical name. Outbound (`aliasByCanonical`) is left untouched —
68
+ * the model still sees the single-underscore canonical on the wire, so
69
+ * the tools-array byte count (and the provider prompt cache) is
70
+ * unaffected.
71
+ *
72
+ * Why: SDK consumers and tool descriptions across the ecosystem
73
+ * inconsistently reference one form or the other (Anthropic's docs use
74
+ * double, zidane uses single). A Claude-Code-trained model emitting
75
+ * `mcp__supabase__apply_migration` against a server zidane registered as
76
+ * `mcp_supabase_apply_migration` would otherwise trip the `tool:unknown`
77
+ * path and waste a turn on the correction.
78
+ *
79
+ * Idempotent: a canonical that already has a double-underscore alias
80
+ * mapped (host's explicit `toolAliases` got there first) is left alone.
81
+ * Tools whose canonical name DOESN'T start with `mcp_` (or that contain
82
+ * no inner separator) are skipped — we don't want to invent aliases for
83
+ * non-MCP tools or malformed names.
84
+ *
85
+ * Mutates `maps.canonicalByAlias` in place and returns the same object
86
+ * for chaining.
87
+ */
88
+ function augmentMcpDoubleUnderscoreAliases(maps, canonicalNames) {
89
+ for (const canonical of canonicalNames) {
90
+ if (!canonical.startsWith("mcp_")) continue;
91
+ const tail = canonical.slice(4);
92
+ const sep = tail.indexOf("_");
93
+ if (sep <= 0 || sep >= tail.length - 1) continue;
94
+ const doubleForm = `mcp__${tail.slice(0, sep)}__${tail.slice(sep + 1)}`;
95
+ if (doubleForm === canonical) continue;
96
+ if (maps.canonicalByAlias.has(doubleForm)) continue;
97
+ maps.canonicalByAlias.set(doubleForm, canonical);
98
+ }
99
+ return maps;
100
+ }
101
+ /**
63
102
  * Rewrite `tool_call` block names in a content array from canonical → wire for outbound
64
103
  * messages sent to the provider. Mutation is non-destructive (returns a new array).
65
104
  */
@@ -326,36 +365,86 @@ function installDedupToolsGate(hooks, getDedupTools, getSession) {
326
365
  function pendingKey(callId, name) {
327
366
  return `${callId}::${name}`;
328
367
  }
368
+ /**
369
+ * Resolve the legacy hasher-only form vs the new config-object form to
370
+ * the same internal shape. Centralised so the gate/after handlers stay
371
+ * single-branch. Returns `null` when the tool has no dedup config.
372
+ */
373
+ function resolveConfig(name) {
374
+ const raw = getDedupTools()?.[name];
375
+ if (!raw) return null;
376
+ if (typeof raw === "function") return {
377
+ hasher: raw,
378
+ mode: "replay",
379
+ threshold: Infinity,
380
+ reason: void 0
381
+ };
382
+ const threshold = (typeof raw.threshold === "number" && Number.isFinite(raw.threshold) ? Math.max(2, Math.floor(raw.threshold)) : void 0) ?? (raw.mode === "block-after" ? 4 : Infinity);
383
+ return {
384
+ hasher: raw.hasher,
385
+ mode: raw.mode ?? "replay",
386
+ threshold,
387
+ reason: raw.reason
388
+ };
389
+ }
390
+ function formatReason(reason, toolName, input, count) {
391
+ if (typeof reason === "string") return reason;
392
+ if (typeof reason === "function") try {
393
+ const out = reason(input, count);
394
+ if (typeof out === "string" && out.length > 0) return out;
395
+ } catch {}
396
+ return `Identical \`${toolName}\` call repeated ${count} times — break the loop by changing your approach or moving on to a different step.`;
397
+ }
329
398
  function gateHandler(ctx) {
330
399
  if (ctx.block || ctx.result !== void 0) return;
331
- const hasher = getDedupTools()?.[ctx.name];
332
- if (!hasher) return;
400
+ const config = resolveConfig(ctx.name);
401
+ if (!config) return;
333
402
  const state = getToolDedupState(getSession());
334
403
  if (!state) return;
335
404
  let hash;
336
405
  try {
337
- hash = hasher(ctx.input);
406
+ hash = config.hasher(ctx.input);
338
407
  } catch {
339
408
  return;
340
409
  }
341
410
  if (typeof hash !== "string" || hash.length === 0) return;
342
411
  const prior = state.get(ctx.name);
343
412
  if (prior && prior.hash === hash) {
413
+ const priorRepeats = prior.repeats ?? 0;
414
+ const count = priorRepeats + 2;
415
+ if (config.mode === "block-after" && count >= config.threshold) {
416
+ ctx.block = true;
417
+ ctx.reason = formatReason(config.reason, ctx.name, ctx.input, count);
418
+ state.set(ctx.name, {
419
+ hash,
420
+ result: prior.result,
421
+ repeats: priorRepeats + 1
422
+ });
423
+ return;
424
+ }
344
425
  ctx.result = prior.result;
426
+ pending.set(pendingKey(ctx.callId, ctx.name), {
427
+ hash,
428
+ repeats: priorRepeats + 1
429
+ });
345
430
  return;
346
431
  }
347
- pending.set(pendingKey(ctx.callId, ctx.name), hash);
432
+ pending.set(pendingKey(ctx.callId, ctx.name), {
433
+ hash,
434
+ repeats: 0
435
+ });
348
436
  }
349
437
  function afterHandler(ctx) {
350
438
  const key = pendingKey(ctx.callId, ctx.name);
351
- const hash = pending.get(key);
352
- if (hash === void 0) return;
439
+ const entry = pending.get(key);
440
+ if (entry === void 0) return;
353
441
  pending.delete(key);
354
442
  const state = getToolDedupState(getSession());
355
443
  if (!state) return;
356
444
  state.set(ctx.name, {
357
- hash,
358
- result: ctx.result
445
+ hash: entry.hash,
446
+ result: ctx.result,
447
+ repeats: entry.repeats
359
448
  });
360
449
  }
361
450
  const unregisterGate = hooks.hook("tool:gate", gateHandler);
@@ -1412,6 +1501,8 @@ async function runLoop(ctx) {
1412
1501
  const startTime = Date.now();
1413
1502
  const maxTurns = ctx.maxTurns ?? Number.POSITIVE_INFINITY;
1414
1503
  let turnsCompleted = 0;
1504
+ const pauseCap = ctx.maxConsecutivePauseTurns ?? 5;
1505
+ let consecutiveEmptyPauseTurns = 0;
1415
1506
  const ttft = { mark: void 0 };
1416
1507
  const markTtft = () => {
1417
1508
  if (ttft.mark === void 0) ttft.mark = Date.now() - ctx.runStartMs;
@@ -1446,6 +1537,10 @@ async function runLoop(ctx) {
1446
1537
  totalIn,
1447
1538
  totalOut
1448
1539
  });
1540
+ if (result.pauseEmpty) {
1541
+ consecutiveEmptyPauseTurns += 1;
1542
+ if (pauseCap > 0 && consecutiveEmptyPauseTurns >= pauseCap) break;
1543
+ } else consecutiveEmptyPauseTurns = 0;
1449
1544
  if (ctx.signal.aborted) {
1450
1545
  await ctx.hooks.callHook("agent:abort", {});
1451
1546
  break;
@@ -1621,6 +1716,106 @@ function extractStreamErrorMeta(err) {
1621
1716
  }
1622
1717
  return out;
1623
1718
  }
1719
+ /** Defaults applied when `behavior.retry` is absent or has missing fields. */
1720
+ const RETRY_DEFAULTS = {
1721
+ maxAttempts: 3,
1722
+ initialDelayMs: 1e3,
1723
+ maxDelayMs: 3e4
1724
+ };
1725
+ /**
1726
+ * Normalize a user-supplied `RetryConfig` (any field may be missing or invalid)
1727
+ * into a fully-populated config the retry loop can use without per-field guards.
1728
+ *
1729
+ * Invalid values (non-finite, < 1 for `maxAttempts`, < 0 for delays) fall back
1730
+ * to defaults rather than throwing — this is a behavior knob, not a validation
1731
+ * boundary, and a bad value here shouldn't crash a run.
1732
+ */
1733
+ function resolveRetryConfig(cfg) {
1734
+ return {
1735
+ maxAttempts: Number.isFinite(cfg?.maxAttempts) && cfg.maxAttempts >= 1 ? Math.floor(cfg.maxAttempts) : RETRY_DEFAULTS.maxAttempts,
1736
+ initialDelayMs: Number.isFinite(cfg?.initialDelayMs) && cfg.initialDelayMs >= 0 ? cfg.initialDelayMs : RETRY_DEFAULTS.initialDelayMs,
1737
+ maxDelayMs: Number.isFinite(cfg?.maxDelayMs) && cfg.maxDelayMs >= 0 ? cfg.maxDelayMs : RETRY_DEFAULTS.maxDelayMs
1738
+ };
1739
+ }
1740
+ /**
1741
+ * Compute the backoff delay for the upcoming retry.
1742
+ *
1743
+ * Strategy:
1744
+ * - If the server returned `retry-after` / `retry-after-ms`, honor it (capped at `maxDelayMs`).
1745
+ * - Otherwise: `initialDelayMs * 2^(attempt-1)`, capped at `maxDelayMs`, then "full jitter"
1746
+ * (`Math.random() * computed`). Full jitter scatters retries from concurrent clients
1747
+ * across the whole window — empirically better than fixed exponential under thundering-herd
1748
+ * (per AWS Architecture Blog's analysis of EBO+jitter strategies).
1749
+ *
1750
+ * `attempt` is the 1-indexed number of the attempt that just *failed*, so the first
1751
+ * post-failure delay uses `attempt=1` → `initialDelayMs * 1` before jitter.
1752
+ */
1753
+ function computeRetryDelayMs(attempt, cfg, retryAfterMs) {
1754
+ if (retryAfterMs !== void 0 && retryAfterMs >= 0) return Math.min(retryAfterMs, cfg.maxDelayMs);
1755
+ const exponential = cfg.initialDelayMs * 2 ** (attempt - 1);
1756
+ const capped = Math.min(exponential, cfg.maxDelayMs);
1757
+ return Math.floor(Math.random() * capped);
1758
+ }
1759
+ /**
1760
+ * Extract a `retry-after` value (in milliseconds) from an error's `.headers`, if any.
1761
+ *
1762
+ * Supports both `retry-after-ms` (non-standard but Stainless-emitted; already in ms,
1763
+ * always integer) and the standard `retry-after` header (integer seconds, RFC 7231).
1764
+ * Returns `undefined` when neither is present, non-numeric, or negative.
1765
+ *
1766
+ * `parseInt` over `parseFloat` here matches the actual contract — Stainless emits
1767
+ * `retry-after-ms` as an integer and RFC 7231 specifies a non-negative integer for
1768
+ * the seconds form. The HTTP-date form of `retry-after` is deliberately unsupported
1769
+ * (Anthropic + OpenAI both emit numeric seconds; supporting dates invites clock-skew
1770
+ * concerns for negligible benefit).
1771
+ */
1772
+ function extractRetryAfterMs(err) {
1773
+ if (!err || typeof err !== "object") return void 0;
1774
+ const headers = err.headers;
1775
+ if (!headers || typeof headers !== "object") return void 0;
1776
+ const get = (key) => {
1777
+ const h = headers;
1778
+ if (typeof h.get === "function") {
1779
+ const v = h.get(key);
1780
+ return typeof v === "string" ? v : void 0;
1781
+ }
1782
+ const v = h[key];
1783
+ return typeof v === "string" ? v : void 0;
1784
+ };
1785
+ const ms = get("retry-after-ms");
1786
+ if (ms !== void 0) {
1787
+ const parsed = Number.parseInt(ms, 10);
1788
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
1789
+ }
1790
+ const seconds = get("retry-after");
1791
+ if (seconds !== void 0) {
1792
+ const parsed = Number.parseInt(seconds, 10);
1793
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed * 1e3;
1794
+ }
1795
+ }
1796
+ /**
1797
+ * `setTimeout` that rejects if the given signal aborts before the timer fires.
1798
+ *
1799
+ * Resolves with `undefined` on normal completion. Rejects with the signal's
1800
+ * `reason` (or a generic AbortError when no reason is set) on abort. Cleans
1801
+ * up its `abort` listener in both paths so callers don't leak handlers.
1802
+ */
1803
+ function abortableSleep(ms, signal) {
1804
+ if (signal.aborted) return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1805
+ return new Promise((resolve, reject) => {
1806
+ let timer;
1807
+ const onAbort = () => {
1808
+ clearTimeout(timer);
1809
+ signal.removeEventListener("abort", onAbort);
1810
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1811
+ };
1812
+ timer = setTimeout(() => {
1813
+ signal.removeEventListener("abort", onAbort);
1814
+ resolve();
1815
+ }, ms);
1816
+ signal.addEventListener("abort", onAbort, { once: true });
1817
+ });
1818
+ }
1624
1819
  async function executeTurn(ctx, turn, priorUsage) {
1625
1820
  const turnId = await ctx.generateTurnId();
1626
1821
  let canonicalMessages = turnsToMessages(applyCompactSummaryCutoff(ctx.turns));
@@ -1635,6 +1830,21 @@ async function executeTurn(ctx, turn, priorUsage) {
1635
1830
  const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
1636
1831
  const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
1637
1832
  sanitizedMessages = applyTailCompaction(sanitizedMessages, threshold, keep);
1833
+ } else if (typeof ctx.compactStrategy === "function") {
1834
+ const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
1835
+ const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
1836
+ let totalBytes = 0;
1837
+ for (const msg of sanitizedMessages) for (const block of msg.content) if (block.type === "tool_result") totalBytes += toolOutputByteLength(block.output);
1838
+ try {
1839
+ const compacted = await ctx.compactStrategy(sanitizedMessages, {
1840
+ threshold,
1841
+ keepTurns: keep,
1842
+ totalBytes
1843
+ });
1844
+ if (Array.isArray(compacted)) sanitizedMessages = compacted;
1845
+ } catch (err) {
1846
+ console.error("[zidane] compactStrategy function threw:", err);
1847
+ }
1638
1848
  }
1639
1849
  const effectiveThinkingBudget = applyThinkingDecay(ctx.thinkingBudget, ctx.thinkingDecay, turn);
1640
1850
  const formattedTools = ctx.rebuildFormattedTools ? ctx.rebuildFormattedTools() : ctx.formattedTools;
@@ -1668,6 +1878,8 @@ async function executeTurn(ctx, turn, priorUsage) {
1668
1878
  turnId,
1669
1879
  options: streamOptions
1670
1880
  });
1881
+ streamOptions.messages = applyPairingRepair(ctx, streamOptions.messages, turnId);
1882
+ streamOptions.messages = ensureEndsWithUserMessage(streamOptions.messages, ctx.provider);
1671
1883
  let currentText = "";
1672
1884
  let currentThinking = "";
1673
1885
  const streamStartedAt = Date.now();
@@ -1679,74 +1891,104 @@ async function executeTurn(ctx, turn, priorUsage) {
1679
1891
  turnId,
1680
1892
  startedAt: streamStartedAt
1681
1893
  });
1894
+ const retryCfg = resolveRetryConfig(ctx.retry);
1682
1895
  let result;
1683
- try {
1684
- result = await ctx.provider.stream(streamOptions, {
1685
- onText(delta) {
1686
- markTurnTtft();
1687
- currentText += delta;
1688
- ctx.hooks.callHook("stream:text", {
1689
- delta,
1690
- text: currentText,
1691
- turnId
1896
+ let attempt = 0;
1897
+ while (true) {
1898
+ attempt += 1;
1899
+ try {
1900
+ result = await ctx.provider.stream(streamOptions, {
1901
+ onText(delta) {
1902
+ markTurnTtft();
1903
+ currentText += delta;
1904
+ ctx.hooks.callHook("stream:text", {
1905
+ delta,
1906
+ text: currentText,
1907
+ turnId
1908
+ });
1909
+ },
1910
+ onThinking(delta) {
1911
+ markTurnTtft();
1912
+ currentThinking += delta;
1913
+ ctx.hooks.callHook("stream:thinking", {
1914
+ delta,
1915
+ thinking: currentThinking,
1916
+ turnId
1917
+ });
1918
+ },
1919
+ onOAuthRefresh(refreshCtx) {
1920
+ return ctx.hooks.callHook("oauth:refresh", refreshCtx);
1921
+ }
1922
+ });
1923
+ break;
1924
+ } catch (caught) {
1925
+ let terminalErr = caught;
1926
+ let wasAborted = ctx.signal.aborted || caught instanceof Error && caught.name === "AbortError";
1927
+ const classified = !wasAborted ? ctx.provider.classifyError?.(caught) : null;
1928
+ const isRetryable = classified?.kind === "provider_error" && classified.retryable === true;
1929
+ if (!wasAborted && isRetryable && currentText === "" && currentThinking === "" && attempt < retryCfg.maxAttempts) {
1930
+ const meta = extractStreamErrorMeta(caught);
1931
+ const delayMs = computeRetryDelayMs(attempt, retryCfg, extractRetryAfterMs(caught));
1932
+ await ctx.hooks.callHook("stream:retry", {
1933
+ turnId,
1934
+ attempt,
1935
+ nextAttempt: attempt + 1,
1936
+ delayMs,
1937
+ err: caught,
1938
+ ...meta
1692
1939
  });
1693
- },
1694
- onThinking(delta) {
1695
- markTurnTtft();
1696
- currentThinking += delta;
1697
- ctx.hooks.callHook("stream:thinking", {
1698
- delta,
1699
- thinking: currentThinking,
1700
- turnId
1940
+ try {
1941
+ await abortableSleep(delayMs, ctx.signal);
1942
+ continue;
1943
+ } catch {
1944
+ const abortErr = /* @__PURE__ */ new Error("Agent run aborted");
1945
+ abortErr.name = "AbortError";
1946
+ terminalErr = abortErr;
1947
+ wasAborted = true;
1948
+ }
1949
+ }
1950
+ const errorUsage = {
1951
+ input: 0,
1952
+ output: 0
1953
+ };
1954
+ const placeholderText = wasAborted ? "[⏹ Streaming was aborted.]" : buildStreamErrorPlaceholder(terminalErr);
1955
+ const errorContent = currentText ? [{
1956
+ type: "text",
1957
+ text: currentText
1958
+ }] : [{
1959
+ type: "text",
1960
+ text: placeholderText
1961
+ }];
1962
+ const errorTurn = {
1963
+ id: turnId,
1964
+ runId: ctx.runId,
1965
+ role: "assistant",
1966
+ content: errorContent,
1967
+ usage: errorUsage,
1968
+ createdAt: await ctx.clock.now()
1969
+ };
1970
+ ctx.turns.push(errorTurn);
1971
+ if (!wasAborted) {
1972
+ const meta = extractStreamErrorMeta(terminalErr);
1973
+ await ctx.hooks.callHook("stream:error", {
1974
+ err: terminalErr,
1975
+ turnId,
1976
+ ...meta
1701
1977
  });
1702
- },
1703
- onOAuthRefresh(refreshCtx) {
1704
- return ctx.hooks.callHook("oauth:refresh", refreshCtx);
1705
1978
  }
1706
- });
1707
- } catch (err) {
1708
- const wasAborted = ctx.signal.aborted || err instanceof Error && err.name === "AbortError";
1709
- const errorUsage = {
1710
- input: 0,
1711
- output: 0
1712
- };
1713
- const placeholderText = wasAborted ? "[⏹ Streaming was aborted.]" : buildStreamErrorPlaceholder(err);
1714
- const errorContent = currentText ? [{
1715
- type: "text",
1716
- text: currentText
1717
- }] : [{
1718
- type: "text",
1719
- text: placeholderText
1720
- }];
1721
- const errorTurn = {
1722
- id: turnId,
1723
- runId: ctx.runId,
1724
- role: "assistant",
1725
- content: errorContent,
1726
- usage: errorUsage,
1727
- createdAt: await ctx.clock.now()
1728
- };
1729
- ctx.turns.push(errorTurn);
1730
- if (!wasAborted) {
1731
- const meta = extractStreamErrorMeta(err);
1732
- await ctx.hooks.callHook("stream:error", {
1733
- err,
1979
+ await ctx.hooks.callHook("turn:after", {
1980
+ turn,
1734
1981
  turnId,
1735
- ...meta
1982
+ usage: errorUsage,
1983
+ message: errorTurn,
1984
+ toolCounts: {
1985
+ turn: Object.freeze({}),
1986
+ run: Object.freeze({ ...ctx.runToolCounts })
1987
+ },
1988
+ cumulativeUsage: buildCumulativeUsage(priorUsage, errorUsage)
1736
1989
  });
1990
+ throw wrapProviderError(terminalErr, ctx);
1737
1991
  }
1738
- await ctx.hooks.callHook("turn:after", {
1739
- turn,
1740
- turnId,
1741
- usage: errorUsage,
1742
- message: errorTurn,
1743
- toolCounts: {
1744
- turn: Object.freeze({}),
1745
- run: Object.freeze({ ...ctx.runToolCounts })
1746
- },
1747
- cumulativeUsage: buildCumulativeUsage(priorUsage, errorUsage)
1748
- });
1749
- throw wrapProviderError(err, ctx);
1750
1992
  }
1751
1993
  if (currentText) await ctx.hooks.callHook("stream:end", {
1752
1994
  text: currentText,
@@ -1844,6 +2086,7 @@ async function executeTurn(ctx, turn, priorUsage) {
1844
2086
  usage: result.usage
1845
2087
  };
1846
2088
  }
2089
+ const finishedEmptyPause = canonicalToolCalls.length === 0 && result.usage.finishReason === "pause" && currentText.length === 0 && currentThinking.length === 0;
1847
2090
  if (canonicalToolCalls.length === 0 && result.usage.finishReason === "pause") {
1848
2091
  const continueMsg = ctx.provider.userMessage("Please continue.");
1849
2092
  ctx.turns.push({
@@ -1856,7 +2099,8 @@ async function executeTurn(ctx, turn, priorUsage) {
1856
2099
  return {
1857
2100
  ended: false,
1858
2101
  turnId,
1859
- usage: result.usage
2102
+ usage: result.usage,
2103
+ ...finishedEmptyPause ? { pauseEmpty: true } : {}
1860
2104
  };
1861
2105
  }
1862
2106
  const toolResults = await executeToolBatch(ctx, canonicalToolCalls, turnId);
@@ -2032,6 +2276,11 @@ async function runSingleToolDispatch(ctx, call, turnId, fixed) {
2032
2276
  }
2033
2277
  let effectiveInput = gateCtx.input;
2034
2278
  if (!toolDef) {
2279
+ await ctx.hooks.callHook("tool:before", {
2280
+ ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
2281
+ runToolCounts,
2282
+ unknown: true
2283
+ });
2035
2284
  const unknownCtx = {
2036
2285
  ...buildToolHookBase(ctx, turnId, callId, call.name, displayName, effectiveInput),
2037
2286
  suppressError: false
@@ -3431,9 +3680,12 @@ function selectToolSearchMatches(catalog, input, defaultLimit = DEFAULT_LIMIT$1)
3431
3680
  * normal "tool not callable" error on its next attempt, which is the
3432
3681
  * correct response when a tool genuinely no longer exists.
3433
3682
  */
3434
- function applyToolSearchToUnlocked(catalog, input, unlocked, defaultLimit) {
3683
+ function applyToolSearchToUnlocked(catalog, input, unlocked, defaultLimit, addUnlock) {
3435
3684
  const { shown } = selectToolSearchMatches(catalog, input, defaultLimit ?? DEFAULT_LIMIT$1);
3436
- for (const entry of shown) unlocked.add(entry.canonicalName);
3685
+ for (const entry of shown) {
3686
+ unlocked.add(entry.canonicalName);
3687
+ addUnlock?.(entry.canonicalName);
3688
+ }
3437
3689
  }
3438
3690
  /**
3439
3691
  * Factory for `tool_search`. Auto-injected by the agent when
@@ -3476,7 +3728,10 @@ function createToolSearchTool(options) {
3476
3728
  if (ctx.signal?.aborted) return "<tool_search_results matches=\"0\" aborted=\"true\">Run aborted.</tool_search_results>";
3477
3729
  if (options.catalog.length === 0) return "<tool_search_results matches=\"0\">No lazy tools registered for this run.</tool_search_results>";
3478
3730
  const { shown, total, truncated, misses, query, server } = selectToolSearchMatches(options.catalog, input, defaultLimit);
3479
- for (const entry of shown) options.unlocked.add(entry.canonicalName);
3731
+ for (const entry of shown) {
3732
+ options.unlocked.add(entry.canonicalName);
3733
+ options.addUnlock?.(entry.canonicalName);
3734
+ }
3480
3735
  const parts = [];
3481
3736
  const queryAttr = query ? ` query="${escapeXml(query)}"` : "";
3482
3737
  const serverAttr = server ? ` server="${escapeXml(server)}"` : "";
@@ -3540,6 +3795,7 @@ const HOOK_EVENT_SET = new Set([
3540
3795
  "stream:end",
3541
3796
  "stream:thinking",
3542
3797
  "stream:error",
3798
+ "stream:retry",
3543
3799
  "oauth:refresh",
3544
3800
  "tool:gate",
3545
3801
  "tool:dispatched",
@@ -3673,6 +3929,7 @@ function resolveBehavior(agentBehavior, runBehavior) {
3673
3929
  maxConcurrentTools: runBehavior?.maxConcurrentTools ?? agentBehavior?.maxConcurrentTools,
3674
3930
  maxTurns: runBehavior?.maxTurns ?? agentBehavior?.maxTurns,
3675
3931
  maxCostUsd: runBehavior?.maxCostUsd ?? agentBehavior?.maxCostUsd,
3932
+ retry: runBehavior?.retry ?? agentBehavior?.retry,
3676
3933
  maxTotalTokens: runBehavior?.maxTotalTokens ?? agentBehavior?.maxTotalTokens,
3677
3934
  maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens,
3678
3935
  thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
@@ -3692,13 +3949,15 @@ function resolveBehavior(agentBehavior, runBehavior) {
3692
3949
  elideStaleReads: runBehavior?.elideStaleReads ?? agentBehavior?.elideStaleReads,
3693
3950
  toolDisclosure: runBehavior?.toolDisclosure ?? agentBehavior?.toolDisclosure ?? "eager",
3694
3951
  toolSearch: runBehavior?.toolSearch ?? agentBehavior?.toolSearch,
3952
+ surfaceMcpInstructions: runBehavior?.surfaceMcpInstructions ?? agentBehavior?.surfaceMcpInstructions ?? true,
3695
3953
  persistThreshold: runBehavior?.persistThreshold ?? agentBehavior?.persistThreshold,
3696
3954
  persistExcludeTools: runBehavior?.persistExcludeTools ?? agentBehavior?.persistExcludeTools,
3697
3955
  persistDir: runBehavior?.persistDir ?? agentBehavior?.persistDir,
3698
3956
  persistMaxBytes: runBehavior?.persistMaxBytes ?? agentBehavior?.persistMaxBytes,
3699
3957
  tasksDir: runBehavior?.tasksDir ?? agentBehavior?.tasksDir,
3700
3958
  disableBackgroundTasks: runBehavior?.disableBackgroundTasks ?? agentBehavior?.disableBackgroundTasks,
3701
- strictToolPairing: runBehavior?.strictToolPairing ?? agentBehavior?.strictToolPairing ?? false
3959
+ strictToolPairing: runBehavior?.strictToolPairing ?? agentBehavior?.strictToolPairing ?? false,
3960
+ maxConsecutivePauseTurns: runBehavior?.maxConsecutivePauseTurns ?? agentBehavior?.maxConsecutivePauseTurns
3702
3961
  };
3703
3962
  }
3704
3963
  /**
@@ -3821,6 +4080,40 @@ function installLazyDisclosureGate(hooks, lazyCanonicalNames, unlocked, discover
3821
4080
  });
3822
4081
  }
3823
4082
  /**
4083
+ * Render the per-server MCP `instructions` payloads into a single
4084
+ * `# MCP Server Instructions` section. Matches the shape the Claude Code
4085
+ * SDK emits so eval traces stay comparable.
4086
+ *
4087
+ * Empty / whitespace-only entries are filtered out upstream (see
4088
+ * `bootstrapServer`); this function only renders what survived. Callers
4089
+ * that pass an empty map get an empty string back — no stub heading.
4090
+ *
4091
+ * Output shape:
4092
+ *
4093
+ * # MCP Server Instructions
4094
+ *
4095
+ * ## supabase
4096
+ * The project is provisioned. Use `apply_migration` directly.
4097
+ *
4098
+ * ## linear
4099
+ * …
4100
+ *
4101
+ * Server names are emitted in Map iteration order (config order), which
4102
+ * is byte-stable across runs.
4103
+ */
4104
+ function renderMcpInstructionsSection(instructions) {
4105
+ if (instructions.size === 0) return "";
4106
+ const parts = ["# MCP Server Instructions", ""];
4107
+ let first = true;
4108
+ for (const [name, body] of instructions) {
4109
+ if (!first) parts.push("");
4110
+ first = false;
4111
+ parts.push(`## ${name}`);
4112
+ parts.push(body.trim());
4113
+ }
4114
+ return parts.join("\n");
4115
+ }
4116
+ /**
3824
4117
  * Pick the next safe value for `runCounter` so `run_${++counter}` mints
3825
4118
  * an id that doesn't collide with any runId already referenced by the
3826
4119
  * session — whether the runId lives in `session.runs` or only in
@@ -4007,9 +4300,9 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4007
4300
  const thinking = options.thinking ?? "off";
4008
4301
  const model = options.model ?? provider.meta.defaultModel;
4009
4302
  const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
4010
- const { maxConcurrentTools, maxTurns, maxCostUsd, maxTotalTokens, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, toolOutputBudgetExcludeTools, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, persistThreshold, persistExcludeTools, persistDir, persistMaxBytes, strictToolPairing } = resolvedBehavior;
4303
+ const { maxConcurrentTools, maxTurns, maxCostUsd, maxTotalTokens, maxTokens, retry, thinkingBudget, schema, cache, toolOutputBudget, toolOutputBudgetExcludeTools, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch, surfaceMcpInstructions, persistThreshold, persistExcludeTools, persistDir, persistMaxBytes, strictToolPairing, maxConsecutivePauseTurns } = resolvedBehavior;
4011
4304
  let system = options.system || agentSystem || "You are a helpful assistant.";
4012
- if (skillsCatalog) system = `${system}\n\n${skillsCatalog}`;
4305
+ if (skillsCatalog) system = appendStaticSection(system, skillsCatalog);
4013
4306
  const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? {
4014
4307
  ...sourceTools,
4015
4308
  ...mcpConnection.tools
@@ -4041,6 +4334,16 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4041
4334
  });
4042
4335
  const disclosure = partitionToolDisclosure(toolsPreSearch, mcpToolNames, mcpServers, toolDisclosure, toolAliases);
4043
4336
  const unlocked = new Set(disclosure.eagerCanonicalNames);
4337
+ const initialUnlocked = new Set(disclosure.eagerCanonicalNames);
4338
+ const dynamicUnlockOrder = [];
4339
+ const dynamicUnlockSeen = /* @__PURE__ */ new Set();
4340
+ function recordDynamicUnlock(canonical) {
4341
+ if (initialUnlocked.has(canonical)) return;
4342
+ if (dynamicUnlockSeen.has(canonical)) return;
4343
+ dynamicUnlockSeen.add(canonical);
4344
+ unlocked.add(canonical);
4345
+ dynamicUnlockOrder.push(canonical);
4346
+ }
4044
4347
  const hostDefinedToolSearch = !!toolsPreSearch.tool_search;
4045
4348
  const shouldInjectToolSearch = disclosure.lazyEntries.length > 0 && toolSearch?.tool !== false && !hostDefinedToolSearch;
4046
4349
  let tools = toolsPreSearch;
@@ -4048,6 +4351,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4048
4351
  const toolSearchTool = createToolSearchTool({
4049
4352
  catalog: disclosure.lazyEntries,
4050
4353
  unlocked,
4354
+ addUnlock: recordDynamicUnlock,
4051
4355
  ...toolSearch?.limit !== void 0 ? { defaultLimit: toolSearch.limit } : {}
4052
4356
  });
4053
4357
  tools = {
@@ -4055,14 +4359,29 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4055
4359
  [toolSearchTool.spec.name]: toolSearchTool
4056
4360
  };
4057
4361
  unlocked.add(toolSearchTool.spec.name);
4362
+ initialUnlocked.add(toolSearchTool.spec.name);
4058
4363
  }
4059
4364
  const discoveryToolName = shouldInjectToolSearch ? "tool_search" : hostDefinedToolSearch ? toolAliases?.tool_search ?? "tool_search" : null;
4060
- if (disclosure.lazyEntries.length > 0) system = `${system}\n\n${buildSearchableCatalog(disclosure.lazyEntries, { discoveryToolName })}`;
4365
+ if (disclosure.lazyEntries.length > 0) system = appendStaticSection(system, buildSearchableCatalog(disclosure.lazyEntries, { discoveryToolName }));
4366
+ if (surfaceMcpInstructions && mcpConnection?.instructions && mcpConnection.instructions.size > 0) {
4367
+ const section = renderMcpInstructionsSection(mcpConnection.instructions);
4368
+ if (section.length > 0) system = appendStaticSection(system, section);
4369
+ }
4061
4370
  const aliasMaps = buildAliasMaps(toolAliases, Object.keys(tools));
4371
+ augmentMcpDoubleUnderscoreAliases(aliasMaps, Object.keys(tools));
4062
4372
  function buildFormattedTools() {
4063
4373
  const specs = [];
4064
4374
  for (const t of Object.values(tools)) {
4065
- if (!unlocked.has(t.spec.name)) continue;
4375
+ if (!initialUnlocked.has(t.spec.name)) continue;
4376
+ specs.push({
4377
+ name: aliasMaps.aliasByCanonical.get(t.spec.name) ?? t.spec.name,
4378
+ description: t.spec.description || "",
4379
+ inputSchema: t.spec.inputSchema
4380
+ });
4381
+ }
4382
+ for (const canonical of dynamicUnlockOrder) {
4383
+ const t = tools[canonical];
4384
+ if (!t) continue;
4066
4385
  specs.push({
4067
4386
  name: aliasMaps.aliasByCanonical.get(t.spec.name) ?? t.spec.name,
4068
4387
  description: t.spec.description || "",
@@ -4085,7 +4404,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4085
4404
  if (block.type !== "tool_call") continue;
4086
4405
  if (block.name !== "tool_search") continue;
4087
4406
  if (!resolvedCallIds.has(block.id)) continue;
4088
- applyToolSearchToUnlocked(disclosure.lazyEntries, block.input, unlocked, toolSearch?.limit);
4407
+ applyToolSearchToUnlocked(disclosure.lazyEntries, block.input, unlocked, toolSearch?.limit, recordDynamicUnlock);
4089
4408
  }
4090
4409
  }
4091
4410
  }
@@ -4206,6 +4525,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4206
4525
  maxTurns,
4207
4526
  ...maxCostUsd !== void 0 ? { maxCostUsd } : {},
4208
4527
  ...maxTotalTokens !== void 0 ? { maxTotalTokens } : {},
4528
+ ...retry !== void 0 ? { retry } : {},
4209
4529
  maxTokens,
4210
4530
  ...session ? { session } : {},
4211
4531
  ...agentReadState ? { readState: agentReadState } : {},
@@ -4226,6 +4546,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4226
4546
  ...persistDir !== void 0 ? { persistDir } : {},
4227
4547
  ...persistMaxBytes !== void 0 ? { persistMaxBytes } : {},
4228
4548
  ...strictToolPairing ? { strictToolPairing: true } : {},
4549
+ ...maxConsecutivePauseTurns !== void 0 ? { maxConsecutivePauseTurns } : {},
4229
4550
  providerName: provider.name,
4230
4551
  runStartMs,
4231
4552
  runToolCounts: {},
@@ -4523,7 +4844,8 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
4523
4844
  get activeSkills() {
4524
4845
  return skillActivationState.active();
4525
4846
  },
4526
- meta: Object.freeze({ ...provider.meta })
4847
+ meta: Object.freeze({ ...provider.meta }),
4848
+ [Symbol.asyncDispose]: destroy
4527
4849
  };
4528
4850
  }
4529
4851
  //#endregion
@@ -4799,7 +5121,7 @@ const edit = {
4799
5121
  properties: {
4800
5122
  path: {
4801
5123
  type: "string",
4802
- description: "Relative file path."
5124
+ description: "File path (relative to the execution-context cwd, or absolute)."
4803
5125
  },
4804
5126
  old_string: {
4805
5127
  type: "string",
@@ -4904,12 +5226,19 @@ function sharedPrefixLength(a, b) {
4904
5226
  /**
4905
5227
  * Glob-pattern file matching.
4906
5228
  *
4907
- * Uses Bun's native `Bun.Glob` engine when running in the in-process execution
4908
- * context. For non-process contexts (docker, sandbox), falls back to running
4909
- * the pattern through a shell `find` invocation so the match is executed
4910
- * wherever the context lives.
5229
+ * Uses the Node `fs/promises` `glob` API in the in-process execution
5230
+ * context (Bun ≥1.3 re-implements the same module-level entry, so the
5231
+ * same code path serves both runtimes). For non-process contexts
5232
+ * (docker, sandbox), falls back to running the pattern through a shell
5233
+ * `find` invocation so the match is executed wherever the context lives.
5234
+ *
5235
+ * Behavioral note: Node's `fs.glob` yields directories alongside files
5236
+ * and Bun's port doesn't yet accept `withFileTypes`, so we post-filter
5237
+ * with `stat` to keep the historical files-only contract.
4911
5238
  *
4912
- * Results are capped at 1000 entries to keep model input bounded.
5239
+ * Results are capped at 1000 entries to keep model input bounded. The
5240
+ * cap applies after the directory filter, so dirs don't consume the
5241
+ * file budget.
4913
5242
  *
4914
5243
  * By default each row carries `<path>\t<size>\t<mtime>` metadata so the
4915
5244
  * model can rank "what changed recently" without a follow-up `read_file`.
@@ -4921,10 +5250,14 @@ function sharedPrefixLength(a, b) {
4921
5250
  const DEFAULT_LIMIT = 1e3;
4922
5251
  const SAFE_GLOB_PATTERN_RE = /^[\w./*?[\]{}!,^@+-]+$/;
4923
5252
  async function globInProcess(pattern, cwd, limit) {
4924
- const glob = new Bun.Glob(pattern);
4925
5253
  const results = [];
4926
- for await (const file of glob.scan({ cwd })) {
4927
- results.push(file);
5254
+ for await (const rel of glob(pattern, { cwd })) {
5255
+ try {
5256
+ if (!(await stat(resolve(cwd, rel))).isFile()) continue;
5257
+ } catch {
5258
+ continue;
5259
+ }
5260
+ results.push(rel);
4928
5261
  if (results.length >= limit) break;
4929
5262
  }
4930
5263
  return results.sort();
@@ -4936,7 +5269,7 @@ async function globViaShell(pattern, ctx, limit) {
4936
5269
  if (result.exitCode !== 0 && !result.stdout) return [];
4937
5270
  return result.stdout.split("\n").filter((line) => line.length > 0);
4938
5271
  }
4939
- const glob = {
5272
+ const glob$1 = {
4940
5273
  isConcurrencySafe: true,
4941
5274
  spec: {
4942
5275
  name: "glob",
@@ -4986,10 +5319,10 @@ const glob = {
4986
5319
  /**
4987
5320
  * Search file contents by regex.
4988
5321
  *
4989
- * Wraps ripgrep (`rg`) when available, falls back to an in-process Bun.Glob +
4990
- * regex implementation when running in the in-process execution context. For
4991
- * non-process contexts without `rg`, returns a clear hint rather than silently
4992
- * doing nothing.
5322
+ * Wraps ripgrep (`rg`) when available, falls back to an in-process
5323
+ * `fs/promises` `glob` + regex implementation when running in the
5324
+ * in-process execution context. For non-process contexts without `rg`,
5325
+ * returns a clear hint rather than silently doing nothing.
4993
5326
  *
4994
5327
  * The tool surface mirrors Claude Code's `Grep` so models authored against the
4995
5328
  * Anthropic tool surface need no relearning. Output modes:
@@ -5006,7 +5339,7 @@ const grep = {
5006
5339
  isConcurrencySafe: true,
5007
5340
  spec: {
5008
5341
  name: "grep",
5009
- description: "Search file contents by regex. Returns matching paths (default), match content, or per-file counts. Backed by ripgrep when available with a Bun.Glob fallback for in-process runs.",
5342
+ description: "Search file contents by regex. Returns matching paths (default), match content, or per-file counts. Backed by ripgrep when available with an in-process glob fallback for in-process runs.",
5010
5343
  inputSchema: {
5011
5344
  type: "object",
5012
5345
  properties: {
@@ -5190,13 +5523,12 @@ async function enumerateFiles(input, ctx) {
5190
5523
  if ((await ctx.execution.exec(ctx.handle, `test -f ${shellQuote(input.path)} && echo file || echo dir`)).stdout.trim() === "file") return [input.path];
5191
5524
  } catch {}
5192
5525
  const pattern = input.glob ?? "**/*";
5193
- const glob = new Bun.Glob(pattern);
5194
- const out = [];
5195
5526
  const scanRoot = root === "." ? cwd : `${cwd.replace(/\/$/, "")}/${root.replace(/^\.\//, "")}`;
5196
- for await (const file of glob.scan({
5197
- cwd: scanRoot,
5198
- onlyFiles: true
5199
- })) out.push(root === "." ? file : `${root.replace(/\/$/, "")}/${file}`);
5527
+ const prefix = root === "." ? "" : `${root.replace(/\/$/, "")}/`;
5528
+ const out = [];
5529
+ for await (const rel of glob(pattern, { cwd: scanRoot })) try {
5530
+ if ((await stat(resolve(scanRoot, rel))).isFile()) out.push(prefix ? `${prefix}${rel}` : rel);
5531
+ } catch {}
5200
5532
  return out.sort();
5201
5533
  }
5202
5534
  function formatPaginated(text, input) {
@@ -5242,12 +5574,12 @@ const listFiles = {
5242
5574
  isConcurrencySafe: true,
5243
5575
  spec: {
5244
5576
  name: "list_files",
5245
- description: "List files and directories at the given path (relative to project root).",
5577
+ description: "List the immediate entries (files and subdirectories) at `path`. Returns a newline-separated list, or `(empty directory)` when the directory exists but is empty. Returns `Directory not found: <path>` for a missing path. Non-recursive — use `glob` for pattern matching across nested directories.",
5246
5578
  inputSchema: {
5247
5579
  type: "object",
5248
5580
  properties: { path: {
5249
5581
  type: "string",
5250
- description: "Relative directory path (default: \".\")"
5582
+ description: "Directory path (relative to the execution-context cwd, or absolute). Defaults to `.`."
5251
5583
  } },
5252
5584
  required: []
5253
5585
  }
@@ -5294,7 +5626,7 @@ const multiEdit = {
5294
5626
  properties: {
5295
5627
  path: {
5296
5628
  type: "string",
5297
- description: "Relative file path."
5629
+ description: "File path (relative to the execution-context cwd, or absolute)."
5298
5630
  },
5299
5631
  edits: {
5300
5632
  type: "array",
@@ -5504,7 +5836,7 @@ const readFile$1 = {
5504
5836
  properties: {
5505
5837
  path: {
5506
5838
  type: "string",
5507
- description: "Relative file path."
5839
+ description: "File path (relative to the execution-context cwd, or absolute)."
5508
5840
  },
5509
5841
  offset: {
5510
5842
  type: "integer",
@@ -5720,6 +6052,62 @@ function extractText(message) {
5720
6052
  return "";
5721
6053
  }
5722
6054
  /**
6055
+ * Read-only tool whitelist applied when a subagent preset has
6056
+ * `readonly: true` and no explicit `tools` list. Intentionally narrow —
6057
+ * only obviously non-mutating built-ins. Hosts wanting a different
6058
+ * read-only profile (e.g. include `glob` for a code-search agent) should
6059
+ * pass an explicit `tools` array on the subagent def.
6060
+ */
6061
+ const READONLY_TOOL_DEFAULTS = [
6062
+ "read_file",
6063
+ "grep",
6064
+ "glob",
6065
+ "list_files"
6066
+ ];
6067
+ /**
6068
+ * Apply a subagent preset's tool filter to the parent's tool registry.
6069
+ * Always a strict subset of the input — the child agent never gains
6070
+ * tools the parent doesn't have. Names that don't match a parent tool
6071
+ * are silently dropped (matches MCP's lenient `enabledTools` behavior).
6072
+ *
6073
+ * Precedence: `def.tools` (explicit list) > `def.readonly: true`
6074
+ * (built-in read-only set) > unfiltered.
6075
+ */
6076
+ function filterToolsForSubagent(parentTools, def) {
6077
+ if (!parentTools) return parentTools;
6078
+ const explicit = def.tools;
6079
+ if (explicit && explicit.length > 0) {
6080
+ const wanted = new Set(explicit);
6081
+ const filtered = {};
6082
+ for (const [registryKey, t] of Object.entries(parentTools)) if (wanted.has(t.spec.name)) filtered[registryKey] = t;
6083
+ return filtered;
6084
+ }
6085
+ if (def.readonly) {
6086
+ const wanted = new Set(READONLY_TOOL_DEFAULTS);
6087
+ const filtered = {};
6088
+ for (const [registryKey, t] of Object.entries(parentTools)) if (wanted.has(t.spec.name)) filtered[registryKey] = t;
6089
+ return filtered;
6090
+ }
6091
+ return parentTools;
6092
+ }
6093
+ /**
6094
+ * Render the per-type descriptions into a single schema-field
6095
+ * description string the model reads when choosing a `subagent_type`.
6096
+ * Falls back to a generic line when the host didn't supply any per-type
6097
+ * descriptions.
6098
+ */
6099
+ function buildSubagentTypeDescription(registry) {
6100
+ const lines = [];
6101
+ let hasAny = false;
6102
+ for (const [key, def] of Object.entries(registry)) if (def.description) {
6103
+ lines.push(`- "${key}": ${def.description}`);
6104
+ hasAny = true;
6105
+ } else lines.push(`- "${key}"`);
6106
+ lines.push("- \"general-purpose\": no specialization; uses the spawn tool's default config.");
6107
+ if (!hasAny) return `Optional subagent preset. One of: ${lines.map((l) => l.replace(/^- /, "").replace(/:.*$/, "")).join(", ")}.`;
6108
+ return `Optional subagent preset that overlays the spawn tool's defaults.\n${lines.join("\n")}`;
6109
+ }
6110
+ /**
5723
6111
  * Race `task` (an already-running child `agent.run()` promise) against a
5724
6112
  * timer. Does NOT race against the parent abort signal — the child agent
5725
6113
  * already observes the same signal internally and handles its own aborted
@@ -5856,6 +6244,8 @@ function createSpawnTool(options = {}) {
5856
6244
  turns: 0,
5857
6245
  elapsed: 0
5858
6246
  };
6247
+ const subagentRegistry = options.subagents;
6248
+ const subagentTypeKeys = !!subagentRegistry && Object.keys(subagentRegistry).length > 0 ? [...new Set([...Object.keys(subagentRegistry), "general-purpose"])] : [];
5859
6249
  return {
5860
6250
  get children() {
5861
6251
  return localChildren;
@@ -5877,7 +6267,12 @@ function createSpawnTool(options = {}) {
5877
6267
  system: {
5878
6268
  type: "string",
5879
6269
  description: "Optional system prompt override for this specific sub-agent."
5880
- }
6270
+ },
6271
+ ...subagentTypeKeys.length > 0 ? { subagent_type: {
6272
+ type: "string",
6273
+ enum: subagentTypeKeys,
6274
+ description: buildSubagentTypeDescription(subagentRegistry)
6275
+ } } : {}
5881
6276
  },
5882
6277
  required: ["task"]
5883
6278
  }
@@ -5885,6 +6280,8 @@ function createSpawnTool(options = {}) {
5885
6280
  async execute(input, ctx) {
5886
6281
  const task = input.task;
5887
6282
  const systemOverride = input.system;
6283
+ const requestedSubagentType = typeof input.subagent_type === "string" ? input.subagent_type : void 0;
6284
+ const subagentDef = requestedSubagentType && subagentRegistry ? subagentRegistry[requestedSubagentType] ?? void 0 : void 0;
5888
6285
  const parentDepth = ctx.depth ?? 0;
5889
6286
  const childDepth = parentDepth + 1;
5890
6287
  if (childDepth > maxDepth) return `Cannot spawn: maxDepth=${maxDepth} reached (parent depth=${parentDepth}). Deepen the cap with createSpawnTool({ maxDepth }).`;
@@ -5905,10 +6302,11 @@ function createSpawnTool(options = {}) {
5905
6302
  let result = "";
5906
6303
  let unbubble;
5907
6304
  try {
6305
+ const filteredTools = subagentDef ? filterToolsForSubagent(ctx.tools, subagentDef) : ctx.tools;
5908
6306
  const parentPreset = {
5909
6307
  ...ctx.name !== void 0 ? { name: ctx.name } : {},
5910
6308
  ...ctx.system !== void 0 ? { system: ctx.system } : {},
5911
- tools: ctx.tools,
6309
+ tools: filteredTools,
5912
6310
  ...ctx.toolAliases !== void 0 ? { toolAliases: ctx.toolAliases } : {},
5913
6311
  ...ctx.mcpServers !== void 0 ? { mcpServers: ctx.mcpServers } : {},
5914
6312
  ...ctx.skills !== void 0 ? { skills: ctx.skills } : {},
@@ -5948,10 +6346,11 @@ function createSpawnTool(options = {}) {
5948
6346
  };
5949
6347
  await ctx.hooks.callHook("spawn:before", spawnHookCtx);
5950
6348
  const propagatedTracing = Object.keys(spawnHookCtx.tracingContext).length > 0 ? Object.freeze({ ...spawnHookCtx.tracingContext }) : void 0;
6349
+ const effectiveSystem = systemOverride ?? subagentDef?.system ?? options.system;
5951
6350
  const runPromise = agent.run({
5952
6351
  prompt: task,
5953
6352
  model: options.model,
5954
- system: systemOverride ?? options.system,
6353
+ system: effectiveSystem,
5955
6354
  thinking: options.thinking,
5956
6355
  signal: ctx.signal,
5957
6356
  depth: childDepth,
@@ -6088,17 +6487,17 @@ function createSpawnTool(options = {}) {
6088
6487
  const writeFile$1 = {
6089
6488
  spec: {
6090
6489
  name: "write_file",
6091
- description: "Write content to a file (creates parent directories). Returns Created / Updated / \"No change needed\" so the model can detect no-ops without a separate read.",
6490
+ description: "Write `content` to `path`, creating any missing parent directories. Overwrites existing files in full; prefer `edit` / `multi_edit` for surgical changes when you only want to alter part of a file. Returns one of: `Created <path> (N bytes)` (file did not exist), `Updated <path> (N bytes)` (content differed), or `No change needed: <path> already at target state (N bytes)` — so the model can detect no-ops without a separate `read_file`.",
6092
6491
  inputSchema: {
6093
6492
  type: "object",
6094
6493
  properties: {
6095
6494
  path: {
6096
6495
  type: "string",
6097
- description: "Relative file path."
6496
+ description: "File path (relative to the execution-context cwd, or absolute)."
6098
6497
  },
6099
6498
  content: {
6100
6499
  type: "string",
6101
- description: "File content."
6500
+ description: "Complete file content. Overwrites any existing file at `path`."
6102
6501
  }
6103
6502
  },
6104
6503
  required: ["path", "content"]
@@ -6118,6 +6517,6 @@ const writeFile$1 = {
6118
6517
  }
6119
6518
  };
6120
6519
  //#endregion
6121
- export { resolvePersistDir as A, formatTaskStatus as B, TOOL_USE_SKIPPED_MESSAGE as C, buildPersistedStub as D, PERSISTENCE_PREVIEW_BYTES as E, resolveReadStateMap as F, previewLine as H, ageString as I, compactPath as L, getReadState as M, hashContent as N, cleanupPersistedSession as O, readStateKey as P, fmtTokens as R, TOOL_USE_CANCELLED_MESSAGE as S, PERSISTED_STUB_PREFIX as T, shortId as U, formatTaskSummary as V, createSkillsReadTool as _, multiEdit as a, INTERRUPT_MESSAGE_FOR_TOOL_USE as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, resolveTasksDir as j, maybePersistToolResult as k, glob as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shellKill as r, createInteractionTool as s, writeFile$1 as t, edit as u, createShellTool as v, validateToolArgs as w, SHELL_CASCADE_CANCEL_MESSAGE as x, shell as y, formatDuration as z };
6520
+ export { resolvePersistDir as A, formatTaskStatus as B, TOOL_USE_SKIPPED_MESSAGE as C, buildPersistedStub as D, PERSISTENCE_PREVIEW_BYTES as E, resolveReadStateMap as F, previewLine as H, ageString as I, compactPath as L, getReadState as M, hashContent as N, cleanupPersistedSession as O, readStateKey as P, fmtTokens as R, TOOL_USE_CANCELLED_MESSAGE as S, PERSISTED_STUB_PREFIX as T, shortId as U, formatTaskSummary as V, createSkillsReadTool as _, multiEdit as a, INTERRUPT_MESSAGE_FOR_TOOL_USE as b, grep as c, resolveOldString as d, styleReplacementForVia as f, createSkillsRunScriptTool as g, createSkillsUseTool as h, readFile$1 as i, resolveTasksDir as j, maybePersistToolResult as k, glob$1 as l, createToolSearchTool as m, createSpawnTool as n, listFiles as o, createAgent as p, shellKill as r, createInteractionTool as s, writeFile$1 as t, edit as u, createShellTool as v, validateToolArgs as w, SHELL_CASCADE_CANCEL_MESSAGE as x, shell as y, formatDuration as z };
6122
6521
 
6123
- //# sourceMappingURL=tools-CxOfTt3R.js.map
6522
+ //# sourceMappingURL=tools-DE9pR_NG.js.map