zidane 6.5.1 → 6.5.3

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 (110) hide show
  1. package/dist/{acp-o-LQAjFw.js → acp-CIpD8UIw.js} +4 -4
  2. package/dist/{acp-o-LQAjFw.js.map → acp-CIpD8UIw.js.map} +1 -1
  3. package/dist/acp-cli.js +5 -5
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.d.ts.map +1 -1
  6. package/dist/acp.js +1 -1
  7. package/dist/{agent-BVNkC1h4.js → agent-8kfGIDb_.js} +755 -586
  8. package/dist/agent-8kfGIDb_.js.map +1 -0
  9. package/dist/agent.d.ts +1 -1
  10. package/dist/agent.js +1 -1
  11. package/dist/{anthropic-Dov2mC5e.js → anthropic-_Hn1YiWr.js} +2 -2
  12. package/dist/{anthropic-Dov2mC5e.js.map → anthropic-_Hn1YiWr.js.map} +1 -1
  13. package/dist/{auth-B8ZuBcKC.js → auth-CS_e8ng1.js} +6 -5
  14. package/dist/{auth-B8ZuBcKC.js.map → auth-CS_e8ng1.js.map} +1 -1
  15. package/dist/chat/pure.d.ts +4 -4
  16. package/dist/chat/pure.js +3 -3
  17. package/dist/chat.d.ts +124 -10
  18. package/dist/chat.d.ts.map +1 -1
  19. package/dist/chat.js +6 -6
  20. package/dist/{context-breakdown-BLF10tOO.js → context-segments-1mcqSCM3.js} +110 -2
  21. package/dist/context-segments-1mcqSCM3.js.map +1 -0
  22. package/dist/{context-transport-D-daWOmp.js → context-transport-Bco9Wo0b.js} +3 -3
  23. package/dist/{context-transport-D-daWOmp.js.map → context-transport-Bco9Wo0b.js.map} +1 -1
  24. package/dist/contexts/daytona.d.ts +1 -1
  25. package/dist/contexts/daytona.d.ts.map +1 -1
  26. package/dist/contexts/daytona.js +5 -2
  27. package/dist/contexts/daytona.js.map +1 -1
  28. package/dist/contexts/e2b.d.ts +1 -1
  29. package/dist/eval.d.ts +1 -1
  30. package/dist/extensions.d.ts +1 -1
  31. package/dist/extensions.js +1 -1
  32. package/dist/headless.d.ts +1 -1
  33. package/dist/headless.js +1 -1
  34. package/dist/{index-CWBSrqI6.d.ts → index-BKyWhILQ.d.ts} +179 -7
  35. package/dist/index-BKyWhILQ.d.ts.map +1 -0
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +7 -7
  38. package/dist/{login-CR3fDHXh.js → login-D3CKhgbI.js} +2 -2
  39. package/dist/{login-CR3fDHXh.js.map → login-D3CKhgbI.js.map} +1 -1
  40. package/dist/{mcp-C6IkXNOS.js → mcp-D9fqOQo5.js} +3 -3
  41. package/dist/{mcp-C6IkXNOS.js.map → mcp-D9fqOQo5.js.map} +1 -1
  42. package/dist/mcp.d.ts +1 -1
  43. package/dist/mcp.js +1 -1
  44. package/dist/{oauth-bkEOybqm.js → oauth-BYjlFR-J.js} +49 -10
  45. package/dist/oauth-BYjlFR-J.js.map +1 -0
  46. package/dist/output/stream-json.d.ts +1 -1
  47. package/dist/output/terminal.d.ts +1 -1
  48. package/dist/plugins.d.ts +1 -1
  49. package/dist/presets.d.ts +1 -1
  50. package/dist/presets.js +1 -1
  51. package/dist/providers/anthropic.d.ts +1 -1
  52. package/dist/providers/anthropic.js +1 -1
  53. package/dist/providers/arcee.d.ts +1 -1
  54. package/dist/providers/baseten.d.ts +1 -1
  55. package/dist/providers/cerebras.d.ts +1 -1
  56. package/dist/providers/local.d.ts +1 -1
  57. package/dist/providers/openai-compat.d.ts +1 -1
  58. package/dist/providers/openai.d.ts +1 -1
  59. package/dist/providers/openai.js +1 -1
  60. package/dist/providers/openrouter.d.ts +1 -1
  61. package/dist/providers/xai.d.ts +1 -1
  62. package/dist/providers/xai.js +68 -1
  63. package/dist/providers/xai.js.map +1 -0
  64. package/dist/{providers-B6mFe7OD.js → providers-VypZh7rD.js} +3 -3
  65. package/dist/{providers-B6mFe7OD.js.map → providers-VypZh7rD.js.map} +1 -1
  66. package/dist/providers.d.ts +2 -2
  67. package/dist/providers.js +4 -4
  68. package/dist/restate.d.ts +1 -1
  69. package/dist/restate.js +1 -1
  70. package/dist/rolldown-runtime-BhDjJH2R.js +15 -0
  71. package/dist/session/sqlite.d.ts +1 -1
  72. package/dist/session.d.ts +1 -1
  73. package/dist/skills.d.ts +1 -1
  74. package/dist/{stdio-DzUUkkhS.js → stdio-DPpBDPx5.js} +13 -4
  75. package/dist/{stdio-DzUUkkhS.js.map → stdio-DPpBDPx5.js.map} +1 -1
  76. package/dist/{stdio-loader-BILyUGmi.js → stdio-loader-CI4sPFve.js} +11 -6
  77. package/dist/stdio-loader-CI4sPFve.js.map +1 -0
  78. package/dist/{tool-formatters-gFqQbzPY.d.ts → tool-formatters-DMr62Hpy.d.ts} +3 -3
  79. package/dist/{tool-formatters-gFqQbzPY.d.ts.map → tool-formatters-DMr62Hpy.d.ts.map} +1 -1
  80. package/dist/tools/fetch-url.d.ts +1 -1
  81. package/dist/tools/web-search.d.ts +1 -1
  82. package/dist/tools.d.ts +1 -1
  83. package/dist/tools.js +1 -1
  84. package/dist/{transcript-anchors-qCs3wZpu.d.ts → transcript-anchors-BHihk9nf.d.ts} +2 -2
  85. package/dist/{transcript-anchors-qCs3wZpu.d.ts.map → transcript-anchors-BHihk9nf.d.ts.map} +1 -1
  86. package/dist/{transcript-anchors-DEIr0tJC.js → transcript-anchors-uU_KZLsp.js} +161 -36
  87. package/dist/transcript-anchors-uU_KZLsp.js.map +1 -0
  88. package/dist/tui.d.ts +74 -3
  89. package/dist/tui.d.ts.map +1 -1
  90. package/dist/tui.js +823 -81
  91. package/dist/tui.js.map +1 -1
  92. package/dist/{turn-operations-M9-jUyDd.d.ts → turn-operations-BgqVUjtl.d.ts} +3 -3
  93. package/dist/{turn-operations-M9-jUyDd.d.ts.map → turn-operations-BgqVUjtl.d.ts.map} +1 -1
  94. package/dist/{turn-operations-C3IijXMW.js → turn-operations-iGu5J08j.js} +2 -2
  95. package/dist/{turn-operations-C3IijXMW.js.map → turn-operations-iGu5J08j.js.map} +1 -1
  96. package/dist/types.d.ts +1 -1
  97. package/dist/{xai-Du6Wfnlb.js → xai-CfmI5Y5k.js} +10 -67
  98. package/dist/xai-CfmI5Y5k.js.map +1 -0
  99. package/docs/CHAT.md +42 -1
  100. package/docs/INTERACTIONS.md +39 -0
  101. package/docs/RESTATE.md +7 -0
  102. package/docs/TUI.md +28 -2
  103. package/package.json +18 -17
  104. package/dist/agent-BVNkC1h4.js.map +0 -1
  105. package/dist/context-breakdown-BLF10tOO.js.map +0 -1
  106. package/dist/index-CWBSrqI6.d.ts.map +0 -1
  107. package/dist/oauth-bkEOybqm.js.map +0 -1
  108. package/dist/stdio-loader-BILyUGmi.js.map +0 -1
  109. package/dist/transcript-anchors-DEIr0tJC.js.map +0 -1
  110. package/dist/xai-Du6Wfnlb.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { B as rewriteMessagesToWire, H as toWireName, I as augmentMcpDoubleUnderscoreAliases, L as buildAliasMaps, M as buildPromptMessage, N as canonicalizePrompt, R as buildSanitizedWireAliases, V as toCanonicalName, z as rewriteContentToCanonical } from "./openai-compat-CnHeo4Lz.js";
2
2
  import { a as isWireTurn, c as toolOutputByteLength, i as documentBlockMarker, l as toolResultToText, o as sanitizeToolResultMetadata, s as toolOutputBudgetByteLength, t as DEFAULT_AGENT_CLOCK } from "./types-BIarq1qE.js";
3
- import { a as styleReplacementForVia, i as stripLineNumberPrefixes, n as describeVia, r as resolveOldString, t as buildContextBreakdown } from "./context-breakdown-BLF10tOO.js";
3
+ import { a as stripLineNumberPrefixes, i as resolveOldString, n as buildContextBreakdown, o as styleReplacementForVia, r as describeVia, t as buildContextSegments } from "./context-segments-1mcqSCM3.js";
4
4
  import { n as estimateTokens, r as utf8ByteLength } from "./utils-ngQzYzZD.js";
5
5
  import { a as formatDuration, c as previewLine, o as formatTaskStatus, s as formatTaskSummary } from "./format-BF745pLL.js";
6
6
  import { i as parseGitLsFiles, r as GIT_LS_FILES_COMMAND } from "./glob-DluQFSw1.js";
@@ -13,7 +13,7 @@ import { n as writeFileAtomicAsync, t as writeFileAtomic } from "./atomic-write-
13
13
  import { p as resolveStorageDirs, r as loadAgentPlugin } from "./plugins-2PMTVcp-.js";
14
14
  import { r as createLogger, t as consoleSink } from "./logger-DBX9uYOw.js";
15
15
  import { n as reconcileImageMediaType } from "./media-sniff-UKijEaiQ.js";
16
- import { f as wrapDiscoveredMcpTools, i as deriveMcpToolMeta, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-C6IkXNOS.js";
16
+ import { f as wrapDiscoveredMcpTools, i as deriveMcpToolMeta, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-D9fqOQo5.js";
17
17
  import { appendStaticSection, renderSystemForWire } from "./prompt.js";
18
18
  import { a as detectTurnInterruption, c as ensureToolResultPairing, l as filterUnresolvedToolUses, m as resolveContentRefsInMessages, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, p as remintDuplicateToolCallIds, s as ensureEndsWithUserMessage } from "./messages-C_H9soi5.js";
19
19
  import { a as getExtensionSessionState, c as setExtensionSessionState, l as withExtensionSession, n as currentExtensionSession, r as deleteExtensionSessionState, s as resumeChainRunIds } from "./session-VpLrNaFQ.js";
@@ -10542,6 +10542,134 @@ function textForPersistence(output) {
10542
10542
  return parts.join("\n");
10543
10543
  }
10544
10544
  //#endregion
10545
+ //#region src/loop-recovery.ts
10546
+ /**
10547
+ * Operation labels used by the provider-stream watchdogs.
10548
+ *
10549
+ * Single source of truth for the strings so `withTimeout` callers and the
10550
+ * failure classifier can never drift: the classifier matches on exactly the
10551
+ * label the watchdog emits.
10552
+ */
10553
+ const PROVIDER_WATCHDOG_OPERATIONS = {
10554
+ "first response": "provider first response",
10555
+ "total stream": "provider total stream",
10556
+ "stream inactivity": "provider stream inactivity"
10557
+ };
10558
+ /** Watchdog operation names that count as provider-stream failures. */
10559
+ const PROVIDER_WATCHDOG_OPERATION_SET = new Set(Object.values(PROVIDER_WATCHDOG_OPERATIONS));
10560
+ const RETRY_DEFAULTS = {
10561
+ maxAttempts: 3,
10562
+ initialDelayMs: 1e3,
10563
+ maxDelayMs: 3e4
10564
+ };
10565
+ /**
10566
+ * Normalize a user-supplied `RetryConfig` (any field may be missing or invalid)
10567
+ * into a fully-populated config the retry loop can use without per-field guards.
10568
+ *
10569
+ * Invalid values (non-finite, < 1 for `maxAttempts`, < 0 for delays) fall back
10570
+ * to defaults rather than throwing — this is a behavior knob, not a validation
10571
+ * boundary, and a bad value here shouldn't crash a run.
10572
+ */
10573
+ function resolveRetryConfig(cfg) {
10574
+ return {
10575
+ maxAttempts: Number.isFinite(cfg?.maxAttempts) && cfg.maxAttempts >= 1 ? Math.floor(cfg.maxAttempts) : RETRY_DEFAULTS.maxAttempts,
10576
+ initialDelayMs: Number.isFinite(cfg?.initialDelayMs) && cfg.initialDelayMs >= 0 ? cfg.initialDelayMs : RETRY_DEFAULTS.initialDelayMs,
10577
+ maxDelayMs: Number.isFinite(cfg?.maxDelayMs) && cfg.maxDelayMs >= 0 ? cfg.maxDelayMs : RETRY_DEFAULTS.maxDelayMs
10578
+ };
10579
+ }
10580
+ /**
10581
+ * Classify a failed stream attempt.
10582
+ *
10583
+ * Retry eligibility covers two sources: an error the provider's own classifier
10584
+ * marked retryable, and a provider-stream watchdog timeout. The latter is
10585
+ * retryable BY CONSTRUCTION for the first-response watchdog — zero observable
10586
+ * events arrived, so re-issuing is at least as safe as the mid-stream
10587
+ * `overloaded_error` retries the classifier opts into. The total-stream and
10588
+ * inactivity watchdogs ride the same carve-out but lean on `cleanSlate` for
10589
+ * their safety.
10590
+ *
10591
+ * Fallback excludes two classifications:
10592
+ * - `context_exceeded` — owned by overflow recovery; the transcript is the
10593
+ * problem, not the provider.
10594
+ * - `tool_pairing_corruption` — transcript-level corruption fails identically
10595
+ * on every provider.
10596
+ *
10597
+ * Everything else (rate limit, overload, 5xx, auth/billing, even unclassified
10598
+ * network failures) is provider-side: a sibling target may well succeed, and a
10599
+ * genuinely broken request just exhausts the chain and surfaces the last error.
10600
+ */
10601
+ function assessStreamFailure(input) {
10602
+ const { err, runAborted, classifyError, progress, attempt, retry, hasNextTarget } = input;
10603
+ const wasAborted = runAborted || err instanceof Error && err.name === "AbortError";
10604
+ const classified = !wasAborted ? classifyError?.(err) ?? null : null;
10605
+ const cleanSlate = progress.text === "" && progress.thinking === "" && !progress.serverToolActivity;
10606
+ const isRetryable = classified?.kind === "provider_error" && classified.retryable === true;
10607
+ const isStreamWatchdogTimeout = !wasAborted && err instanceof OperationTimeoutError && PROVIDER_WATCHDOG_OPERATION_SET.has(err.operation);
10608
+ const canRetry = !wasAborted && (isRetryable || isStreamWatchdogTimeout) && cleanSlate && attempt < retry.maxAttempts;
10609
+ const retryAfterMs = canRetry ? extractRetryAfterMs(err) : void 0;
10610
+ return {
10611
+ wasAborted,
10612
+ classified,
10613
+ cleanSlate,
10614
+ retryAfterMs,
10615
+ canRetry,
10616
+ preferFallbackOverRetry: retryAfterMs !== void 0 && retryAfterMs > 15e3 && hasNextTarget,
10617
+ canRecoverOverflow: !wasAborted && classified?.kind === "context_exceeded" && cleanSlate,
10618
+ canFallback: !wasAborted && cleanSlate && hasNextTarget && classified?.kind !== "context_exceeded" && classified?.kind !== "tool_pairing_corruption"
10619
+ };
10620
+ }
10621
+ /**
10622
+ * Backoff delay for the next attempt.
10623
+ *
10624
+ * A server-declared `retry-after` wins (capped at `maxDelayMs`); otherwise
10625
+ * full-jitter exponential backoff — a uniform draw from `[0, capped)` rather
10626
+ * than a fixed `capped`, so a fleet of clients retrying the same overloaded
10627
+ * endpoint spreads out instead of stampeding in lockstep.
10628
+ */
10629
+ function computeRetryDelayMs(attempt, cfg, retryAfterMs) {
10630
+ if (retryAfterMs !== void 0 && retryAfterMs >= 0) return Math.min(retryAfterMs, cfg.maxDelayMs);
10631
+ const exponential = cfg.initialDelayMs * 2 ** (attempt - 1);
10632
+ const capped = Math.min(exponential, cfg.maxDelayMs);
10633
+ return Math.floor(Math.random() * capped);
10634
+ }
10635
+ /**
10636
+ * Extract a `retry-after` value (in milliseconds) from an error's `.headers`, if any.
10637
+ *
10638
+ * Supports both `retry-after-ms` (non-standard but Stainless-emitted; already in ms,
10639
+ * always integer) and the standard `retry-after` header (integer seconds, RFC 7231).
10640
+ * Returns `undefined` when neither is present, non-numeric, or negative.
10641
+ *
10642
+ * `parseInt` over `parseFloat` here matches the actual contract — Stainless emits
10643
+ * `retry-after-ms` as an integer and RFC 7231 specifies a non-negative integer for
10644
+ * the seconds form. The HTTP-date form of `retry-after` is deliberately unsupported
10645
+ * (Anthropic + OpenAI both emit numeric seconds; supporting dates invites clock-skew
10646
+ * concerns for negligible benefit).
10647
+ */
10648
+ function extractRetryAfterMs(err) {
10649
+ if (!err || typeof err !== "object") return void 0;
10650
+ const headers = err.headers;
10651
+ if (!headers || typeof headers !== "object") return void 0;
10652
+ const get = (key) => {
10653
+ const h = headers;
10654
+ if (typeof h.get === "function") {
10655
+ const v = h.get(key);
10656
+ return typeof v === "string" ? v : void 0;
10657
+ }
10658
+ const v = h[key];
10659
+ return typeof v === "string" ? v : void 0;
10660
+ };
10661
+ const ms = get("retry-after-ms");
10662
+ if (ms !== void 0) {
10663
+ const parsed = Number.parseInt(ms, 10);
10664
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
10665
+ }
10666
+ const seconds = get("retry-after");
10667
+ if (seconds !== void 0) {
10668
+ const parsed = Number.parseInt(seconds, 10);
10669
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed * 1e3;
10670
+ }
10671
+ }
10672
+ //#endregion
10545
10673
  //#region src/loop.ts
10546
10674
  const DEFAULT_PROVIDER_STREAM_START_TIMEOUT_MS = 300 * 1e3;
10547
10675
  const DEFAULT_PROVIDER_STREAM_TIMEOUT_MS = 1800 * 1e3;
@@ -10553,7 +10681,7 @@ const DEFAULT_PROVIDER_STREAM_TIMEOUT_MS = 1800 * 1e3;
10553
10681
  */
10554
10682
  const DEFAULT_HOOK_TIMEOUT_MS = 300 * 1e3;
10555
10683
  function hookTimeoutMsFor(ctx) {
10556
- return resolveTimeoutMs(ctx.hookTimeoutMs, DEFAULT_HOOK_TIMEOUT_MS);
10684
+ return resolveTimeoutMs(ctx.behavior.hookTimeoutMs, DEFAULT_HOOK_TIMEOUT_MS);
10557
10685
  }
10558
10686
  /**
10559
10687
  * Dispatch an already-invoked hook promise under {@link LoopContext.hookTimeoutMs},
@@ -11349,12 +11477,12 @@ function applyPairingRepair(ctx, messages, turnId) {
11349
11477
  const repairs = [];
11350
11478
  const repaired = ensureToolResultPairing(messages, { onRepair: (repair) => repairs.push(repair) });
11351
11479
  if (repairs.length === 0) return repaired;
11352
- if (ctx.strictToolPairing) throw new AgentToolPairingError({
11480
+ if (ctx.behavior.strictToolPairing) throw new AgentToolPairingError({
11353
11481
  message: `Tool pairing corruption detected (${repairs.length} repair${repairs.length === 1 ? "" : "s"}); strict mode is on so the request was not sent.`,
11354
11482
  ...ctx.providerName ? { provider: ctx.providerName } : {},
11355
11483
  repairs
11356
11484
  });
11357
- const rawLimit = ctx.maxPairingRepairsPerTurn;
11485
+ const rawLimit = ctx.behavior.maxPairingRepairsPerTurn;
11358
11486
  const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : Number.POSITIVE_INFINITY;
11359
11487
  if (repairs.length > limit) throw new AgentToolPairingError({
11360
11488
  message: `Pairing-repair storm: ${repairs.length} repairs in a single turn (limit ${limit}). The transcript is systematically corrupt (likely duplicate tool_use ids from the provider); continuing would loop without forward progress.`,
@@ -11420,19 +11548,19 @@ async function runLoop(ctx) {
11420
11548
  let totalCacheCreation = 0;
11421
11549
  const turnUsages = [];
11422
11550
  const startTime = Date.now();
11423
- const maxTurns = ctx.maxTurns ?? Number.POSITIVE_INFINITY;
11551
+ const maxTurns = ctx.behavior.maxTurns ?? Number.POSITIVE_INFINITY;
11424
11552
  let turnsCompleted = 0;
11425
- const pauseCap = ctx.maxConsecutivePauseTurns ?? 0;
11553
+ const pauseCap = ctx.behavior.maxConsecutivePauseTurns ?? 0;
11426
11554
  let consecutiveEmptyPauseTurns = 0;
11427
- const maxConsecutiveLengthResumes = ctx.maxConsecutiveLengthResumes ?? 8;
11555
+ const maxConsecutiveLengthResumes = ctx.behavior.maxConsecutiveLengthResumes ?? 8;
11428
11556
  let consecutiveLengthResumes = 0;
11429
11557
  const maxConsecutiveEmptyLengthResumes = 2;
11430
11558
  let consecutiveEmptyLengthResumes = 0;
11431
- const maxConsecutiveRefusalResumes = ctx.maxConsecutiveRefusalResumes ?? 3;
11559
+ const maxConsecutiveRefusalResumes = ctx.behavior.maxConsecutiveRefusalResumes ?? 3;
11432
11560
  let consecutiveRefusalResumes = 0;
11433
- const wallDeadlineActive = typeof ctx.maxWallMs === "number" && Number.isFinite(ctx.maxWallMs) && ctx.maxWallMs > 0;
11561
+ const wallDeadlineActive = typeof ctx.behavior.maxWallMs === "number" && Number.isFinite(ctx.behavior.maxWallMs) && ctx.behavior.maxWallMs > 0;
11434
11562
  const wallStartMs = wallDeadlineActive ? await ctx.clock.now() : 0;
11435
- const maxTurnsWarnAt = Number.isFinite(maxTurns) && typeof ctx.maxTurnsWarning === "number" && Number.isFinite(ctx.maxTurnsWarning) && ctx.maxTurnsWarning > 0 && ctx.maxTurnsWarning < maxTurns ? Math.floor(ctx.maxTurnsWarning) : void 0;
11563
+ const maxTurnsWarnAt = Number.isFinite(maxTurns) && typeof ctx.behavior.maxTurnsWarning === "number" && Number.isFinite(ctx.behavior.maxTurnsWarning) && ctx.behavior.maxTurnsWarning > 0 && ctx.behavior.maxTurnsWarning < maxTurns ? Math.floor(ctx.behavior.maxTurnsWarning) : void 0;
11436
11564
  let maxTurnsWarned = false;
11437
11565
  const ttft = { mark: void 0 };
11438
11566
  const markTtft = () => {
@@ -11504,9 +11632,9 @@ async function runLoop(ctx) {
11504
11632
  break;
11505
11633
  }
11506
11634
  const breach = checkRunBudget({
11507
- maxCostUsd: ctx.maxCostUsd,
11508
- maxTotalTokens: ctx.maxTotalTokens,
11509
- maxWallMs: ctx.maxWallMs,
11635
+ maxCostUsd: ctx.behavior.maxCostUsd,
11636
+ maxTotalTokens: ctx.behavior.maxTotalTokens,
11637
+ maxWallMs: ctx.behavior.maxWallMs,
11510
11638
  cost: turnUsages.reduce((s, t) => s + (t.cost ?? 0), 0),
11511
11639
  tokens: totalIn + totalOut,
11512
11640
  elapsedMs: wallDeadlineActive ? await ctx.clock.now() - wallStartMs : 0
@@ -11542,7 +11670,7 @@ async function runLoop(ctx) {
11542
11670
  if (!result.ended && !maxTurnsWarned && maxTurnsWarnAt !== void 0 && maxTurns - (turn + 1) <= maxTurnsWarnAt) {
11543
11671
  maxTurnsWarned = true;
11544
11672
  const remaining = maxTurns - (turn + 1);
11545
- const warning = formatMaxTurnsWarning(ctx.maxTurnsWarningMessage, {
11673
+ const warning = formatMaxTurnsWarning(ctx.behavior.maxTurnsWarningMessage, {
11546
11674
  remaining,
11547
11675
  maxTurns
11548
11676
  });
@@ -11584,7 +11712,7 @@ async function runLoop(ctx) {
11584
11712
  }
11585
11713
  }
11586
11714
  let exhaustedOutput;
11587
- if (ctx.schema && !ctx.signal.aborted && turnsCompleted > 0) {
11715
+ if (ctx.behavior.schema && !ctx.signal.aborted && turnsCompleted > 0) {
11588
11716
  const enforcement = await enforceSchemaOutput(ctx, ctx.clock.randomUUID());
11589
11717
  exhaustedOutput = enforcement.output;
11590
11718
  totalIn += enforcement.usage.input;
@@ -11715,7 +11843,7 @@ async function enforceSchemaOutput(ctx, turnId) {
11715
11843
  const outputSpec = {
11716
11844
  name: "__output__",
11717
11845
  description: "Return the final structured output matching the required schema.",
11718
- inputSchema: ctx.schema
11846
+ inputSchema: ctx.behavior.schema
11719
11847
  };
11720
11848
  const schemaMessages = ensureEndsWithUserMessage(applyPairingRepair(ctx, rewriteMessagesToWire(await resolveContentRefsForWire(ctx, turnsToMessages(applyCompactSummaryCutoff(ctx.turns))), ctx.aliasMaps), turnId), ctx.provider);
11721
11849
  let schemaResult;
@@ -11725,8 +11853,8 @@ async function enforceSchemaOutput(ctx, turnId) {
11725
11853
  system: ctx.system,
11726
11854
  tools: ctx.provider.formatTools([outputSpec]),
11727
11855
  messages: schemaMessages,
11728
- maxTokens: ctx.maxTokens,
11729
- ...ctx.modelCatalog ? { modelCatalog: ctx.modelCatalog } : {},
11856
+ maxTokens: ctx.behavior.maxTokens,
11857
+ ...ctx.behavior.modelCatalog ? { modelCatalog: ctx.behavior.modelCatalog } : {},
11730
11858
  toolChoice: {
11731
11859
  type: "tool",
11732
11860
  name: "__output__"
@@ -11743,7 +11871,7 @@ async function enforceSchemaOutput(ctx, turnId) {
11743
11871
  const output = schemaResult.toolCalls.find((tc) => tc.name === "__output__")?.input;
11744
11872
  if (output) await ctx.hooks.callHook("output", {
11745
11873
  output,
11746
- schema: ctx.schema
11874
+ schema: ctx.behavior.schema
11747
11875
  });
11748
11876
  const schemaTurn = {
11749
11877
  id: ctx.clock.randomUUID(),
@@ -11820,17 +11948,11 @@ function wrapProviderError(err, ctx) {
11820
11948
  * classifier so the retry carve-out can't silently detach from the errors
11821
11949
  * it targets when a label is renamed.
11822
11950
  */
11823
- const PROVIDER_WATCHDOG_OPERATIONS = {
11824
- "first response": "provider first response",
11825
- "total stream": "provider total stream",
11826
- "stream inactivity": "provider stream inactivity"
11827
- };
11828
- const PROVIDER_WATCHDOG_OPERATION_SET = new Set(Object.values(PROVIDER_WATCHDOG_OPERATIONS));
11829
11951
  function providerWatchdogTimeouts(ctx) {
11830
11952
  return {
11831
- startTimeoutMs: resolveTimeoutMs(ctx.providerStreamStartTimeoutMs, DEFAULT_PROVIDER_STREAM_START_TIMEOUT_MS, ctx.maxWallMs),
11832
- totalTimeoutMs: resolveTimeoutMs(ctx.providerStreamTimeoutMs, DEFAULT_PROVIDER_STREAM_TIMEOUT_MS, ctx.maxWallMs),
11833
- idleTimeoutMs: resolveTimeoutMs(ctx.providerStreamIdleTimeoutMs, 0, ctx.maxWallMs)
11953
+ startTimeoutMs: resolveTimeoutMs(ctx.behavior.providerStreamStartTimeoutMs, DEFAULT_PROVIDER_STREAM_START_TIMEOUT_MS, ctx.behavior.maxWallMs),
11954
+ totalTimeoutMs: resolveTimeoutMs(ctx.behavior.providerStreamTimeoutMs, DEFAULT_PROVIDER_STREAM_TIMEOUT_MS, ctx.behavior.maxWallMs),
11955
+ idleTimeoutMs: resolveTimeoutMs(ctx.behavior.providerStreamIdleTimeoutMs, 0, ctx.behavior.maxWallMs)
11834
11956
  };
11835
11957
  }
11836
11958
  function stripInvalidAnthropicThinking(content) {
@@ -12140,11 +12262,6 @@ function extractStreamErrorMeta(err) {
12140
12262
  return out;
12141
12263
  }
12142
12264
  /** Defaults applied when `behavior.retry` is absent or has missing fields. */
12143
- const RETRY_DEFAULTS = {
12144
- maxAttempts: 3,
12145
- initialDelayMs: 1e3,
12146
- maxDelayMs: 3e4
12147
- };
12148
12265
  /**
12149
12266
  * First-response watchdog cap for RETRY attempts (attempt ≥ 2). The first
12150
12267
  * attempt gets the full `providerStreamStartTimeoutMs` window (large-prompt
@@ -12277,7 +12394,7 @@ async function maybeAutoCompact(ctx, turn, lastTurnUsage, state) {
12277
12394
  keepTurns: cfg.keepTurns,
12278
12395
  signal: ctx.signal,
12279
12396
  onProgress,
12280
- ...ctx.providerStreamTimeoutMs !== void 0 ? { providerStreamTimeoutMs: ctx.providerStreamTimeoutMs } : {}
12397
+ ...ctx.behavior.providerStreamTimeoutMs !== void 0 ? { providerStreamTimeoutMs: ctx.behavior.providerStreamTimeoutMs } : {}
12281
12398
  });
12282
12399
  const summaryTurn = summaryToTurn({
12283
12400
  summary: result.summary,
@@ -12310,8 +12427,8 @@ async function maybeAutoCompact(ctx, turn, lastTurnUsage, state) {
12310
12427
  }
12311
12428
  ctx.turns.push(summaryTurn, ...restorationTurns);
12312
12429
  await ctx.persistTurns?.();
12313
- if (cfg.releaseReplacedTurns && ctx.persistDir && result.summarizedTurnIds.length > 0) try {
12314
- await releaseReplacedTurnContent(ctx.turns, new Set(result.summarizedTurnIds), { persistDir: ctx.persistDir });
12430
+ if (cfg.releaseReplacedTurns && ctx.behavior.persistDir && result.summarizedTurnIds.length > 0) try {
12431
+ await releaseReplacedTurnContent(ctx.turns, new Set(result.summarizedTurnIds), { persistDir: ctx.behavior.persistDir });
12315
12432
  } catch (err) {
12316
12433
  if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/loop] post-compact turn release failed: ${errorMessage(err)}\n`);
12317
12434
  }
@@ -12348,87 +12465,6 @@ async function maybeAutoCompact(ctx, turn, lastTurnUsage, state) {
12348
12465
  }
12349
12466
  }
12350
12467
  /**
12351
- * Normalize a user-supplied `RetryConfig` (any field may be missing or invalid)
12352
- * into a fully-populated config the retry loop can use without per-field guards.
12353
- *
12354
- * Invalid values (non-finite, < 1 for `maxAttempts`, < 0 for delays) fall back
12355
- * to defaults rather than throwing — this is a behavior knob, not a validation
12356
- * boundary, and a bad value here shouldn't crash a run.
12357
- */
12358
- function resolveRetryConfig(cfg) {
12359
- return {
12360
- maxAttempts: Number.isFinite(cfg?.maxAttempts) && cfg.maxAttempts >= 1 ? Math.floor(cfg.maxAttempts) : RETRY_DEFAULTS.maxAttempts,
12361
- initialDelayMs: Number.isFinite(cfg?.initialDelayMs) && cfg.initialDelayMs >= 0 ? cfg.initialDelayMs : RETRY_DEFAULTS.initialDelayMs,
12362
- maxDelayMs: Number.isFinite(cfg?.maxDelayMs) && cfg.maxDelayMs >= 0 ? cfg.maxDelayMs : RETRY_DEFAULTS.maxDelayMs
12363
- };
12364
- }
12365
- /**
12366
- * Compute the backoff delay for the upcoming retry.
12367
- *
12368
- * Strategy:
12369
- * - If the server returned `retry-after` / `retry-after-ms`, honor it (capped at `maxDelayMs`).
12370
- * - Otherwise: `initialDelayMs * 2^(attempt-1)`, capped at `maxDelayMs`, then "full jitter"
12371
- * (`Math.random() * computed`). Full jitter scatters retries from concurrent clients
12372
- * across the whole window — empirically better than fixed exponential under thundering-herd
12373
- * (per AWS Architecture Blog's analysis of EBO+jitter strategies).
12374
- *
12375
- * `attempt` is the 1-indexed number of the attempt that just *failed*, so the first
12376
- * post-failure delay uses `attempt=1` → `initialDelayMs * 1` before jitter.
12377
- */
12378
- function computeRetryDelayMs(attempt, cfg, retryAfterMs) {
12379
- if (retryAfterMs !== void 0 && retryAfterMs >= 0) return Math.min(retryAfterMs, cfg.maxDelayMs);
12380
- const exponential = cfg.initialDelayMs * 2 ** (attempt - 1);
12381
- const capped = Math.min(exponential, cfg.maxDelayMs);
12382
- return Math.floor(Math.random() * capped);
12383
- }
12384
- /**
12385
- * Extract a `retry-after` value (in milliseconds) from an error's `.headers`, if any.
12386
- *
12387
- * Supports both `retry-after-ms` (non-standard but Stainless-emitted; already in ms,
12388
- * always integer) and the standard `retry-after` header (integer seconds, RFC 7231).
12389
- * Returns `undefined` when neither is present, non-numeric, or negative.
12390
- *
12391
- * `parseInt` over `parseFloat` here matches the actual contract — Stainless emits
12392
- * `retry-after-ms` as an integer and RFC 7231 specifies a non-negative integer for
12393
- * the seconds form. The HTTP-date form of `retry-after` is deliberately unsupported
12394
- * (Anthropic + OpenAI both emit numeric seconds; supporting dates invites clock-skew
12395
- * concerns for negligible benefit).
12396
- */
12397
- /**
12398
- * Server-declared `retry-after` above this is treated as "not a transient
12399
- * blip" by the fallback machinery: burst rate limits announce seconds,
12400
- * quota/usage exhaustion announces minutes-to-hours. A run holding more
12401
- * fallback targets fails over immediately instead of burning its retry
12402
- * budget against a wall that won't move within the wait window; a
12403
- * single-target run still waits (capped at `maxDelayMs`) because waiting
12404
- * is its only option.
12405
- */
12406
- const LONG_RETRY_AFTER_FALLBACK_MS = 15e3;
12407
- function extractRetryAfterMs(err) {
12408
- if (!err || typeof err !== "object") return void 0;
12409
- const headers = err.headers;
12410
- if (!headers || typeof headers !== "object") return void 0;
12411
- const get = (key) => {
12412
- const h = headers;
12413
- if (typeof h.get === "function") {
12414
- const v = h.get(key);
12415
- return typeof v === "string" ? v : void 0;
12416
- }
12417
- const v = h[key];
12418
- return typeof v === "string" ? v : void 0;
12419
- };
12420
- const ms = get("retry-after-ms");
12421
- if (ms !== void 0) {
12422
- const parsed = Number.parseInt(ms, 10);
12423
- if (Number.isFinite(parsed) && parsed >= 0) return parsed;
12424
- }
12425
- const seconds = get("retry-after");
12426
- if (seconds !== void 0) {
12427
- const parsed = Number.parseInt(seconds, 10);
12428
- if (Number.isFinite(parsed) && parsed >= 0) return parsed * 1e3;
12429
- }
12430
- }
12431
- /**
12432
12468
  * `setTimeout` that rejects if the given signal aborts before the timer fires.
12433
12469
  *
12434
12470
  * Resolves with `undefined` on normal completion. Rejects with the signal's
@@ -12451,11 +12487,60 @@ function abortableSleep(ms, signal) {
12451
12487
  signal.addEventListener("abort", onAbort, { once: true });
12452
12488
  });
12453
12489
  }
12490
+ /**
12491
+ * Advance the run to the next provider+model target and rebind the prepared
12492
+ * request to it.
12493
+ *
12494
+ * Caller contract: only invoke when `assessStreamFailure` returned
12495
+ * `canFallback` — that verdict guarantees an untried target exists, so the
12496
+ * index arithmetic here needs no bounds check of its own.
12497
+ *
12498
+ * Run-sticky: `activeTargetIndex` only advances, so later turns of this run
12499
+ * stay on the fallback target. The next `run()` resolves a fresh target list
12500
+ * starting from the primary.
12501
+ */
12502
+ async function advanceToFallbackTarget(ctx, streamOptions, turnId, err) {
12503
+ const targets = ctx.targets;
12504
+ const fromIndex = ctx.activeTargetIndex ?? 0;
12505
+ const nextIndex = fromIndex + 1;
12506
+ const from = targets[fromIndex];
12507
+ const to = targets[nextIndex];
12508
+ ctx.activeTargetIndex = nextIndex;
12509
+ ctx.provider = to.provider;
12510
+ ctx.model = to.model;
12511
+ ctx.providerName = to.provider.name;
12512
+ if (ctx.rebuildFormattedTools) ctx.formattedTools = ctx.rebuildFormattedTools(to.provider);
12513
+ (ctx.targetFailures ??= []).push({
12514
+ provider: from.provider.name,
12515
+ model: from.model,
12516
+ err
12517
+ });
12518
+ if (ctx.autoCompact) {
12519
+ const nextWindow = to.contextWindow ?? (ctx.autoCompact.contextWindowFromProviderMeta ? to.provider.meta.contextWindow : void 0);
12520
+ if (nextWindow !== void 0 && Number.isFinite(nextWindow) && nextWindow > 0) ctx.autoCompact.contextWindow = nextWindow;
12521
+ }
12522
+ streamOptions.model = to.model;
12523
+ streamOptions.tools = ctx.formattedTools;
12524
+ streamOptions.messages = sanitizeStoredToolResults(to.provider, streamOptions.messages);
12525
+ await settledHook(ctx, "hook model:fallback", ctx.hooks.callHook("model:fallback", {
12526
+ turnId,
12527
+ from: {
12528
+ provider: from.provider.name,
12529
+ model: from.model
12530
+ },
12531
+ to: {
12532
+ provider: to.provider.name,
12533
+ model: to.model
12534
+ },
12535
+ targetIndex: nextIndex,
12536
+ err
12537
+ }));
12538
+ }
12454
12539
  async function executeTurn(ctx, turn, priorUsage) {
12455
12540
  const turnId = ctx.clock.randomUUID();
12456
12541
  let canonicalMessages = turnsToMessages(applyCompactSummaryCutoff(ctx.turns));
12457
12542
  canonicalMessages = await resolveContentRefsForWire(ctx, canonicalMessages);
12458
- if (ctx.elideStaleReads === true || ctx.elideStaleReads === "stub") {
12543
+ if (ctx.behavior.elideStaleReads === true || ctx.behavior.elideStaleReads === "stub") {
12459
12544
  /**
12460
12545
  * Deliberately UNQUANTIZED, unlike tail compaction and media age-out below.
12461
12546
  *
@@ -12483,30 +12568,30 @@ async function executeTurn(ctx, turn, priorUsage) {
12483
12568
  let sanitizedMessages = sanitizeStoredToolResults(ctx.provider, wireMessages);
12484
12569
  let callMapsMemo;
12485
12570
  const callMaps = () => callMapsMemo ??= collectCallMaps(canonicalMessages);
12486
- if (ctx.compactStrategy === "tail") {
12487
- const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
12488
- const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
12571
+ if (ctx.behavior.compactStrategy === "tail") {
12572
+ const threshold = typeof ctx.behavior.compactThreshold === "number" && ctx.behavior.compactThreshold > 0 ? ctx.behavior.compactThreshold : 131072;
12573
+ const keep = typeof ctx.behavior.compactKeepTurns === "number" && ctx.behavior.compactKeepTurns >= 0 ? ctx.behavior.compactKeepTurns : 4;
12489
12574
  const { readPathByCallId, toolNameByCallId } = callMaps();
12490
12575
  const compacted = applyTailCompaction(sanitizedMessages, threshold, keep, {
12491
12576
  readPathByCallId,
12492
12577
  toolNameByCallId,
12493
12578
  chunkTurns: 8,
12494
- persistElideMinBytes: ctx.persistDir ? PERSISTENCE_PREVIEW_BYTES : 0
12579
+ persistElideMinBytes: ctx.behavior.persistDir ? PERSISTENCE_PREVIEW_BYTES : 0
12495
12580
  });
12496
12581
  sanitizedMessages = compacted.messages;
12497
12582
  markReadStateForElidedPaths(ctx, ctx.handle.cwd, compacted.elidedReadPaths);
12498
- if (ctx.persistDir && compacted.persistableElided.length > 0) sanitizedMessages = await persistElidedToolResults(sanitizedMessages, compacted.persistableElided, {
12499
- persistDir: ctx.persistDir,
12500
- maxBytes: ctx.persistMaxBytes,
12583
+ if (ctx.behavior.persistDir && compacted.persistableElided.length > 0) sanitizedMessages = await persistElidedToolResults(sanitizedMessages, compacted.persistableElided, {
12584
+ persistDir: ctx.behavior.persistDir,
12585
+ maxBytes: ctx.behavior.persistMaxBytes,
12501
12586
  writeBlob: executionBlobWriter(ctx)
12502
12587
  });
12503
- } else if (typeof ctx.compactStrategy === "function") {
12504
- const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
12505
- const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
12588
+ } else if (typeof ctx.behavior.compactStrategy === "function") {
12589
+ const threshold = typeof ctx.behavior.compactThreshold === "number" && ctx.behavior.compactThreshold > 0 ? ctx.behavior.compactThreshold : 131072;
12590
+ const keep = typeof ctx.behavior.compactKeepTurns === "number" && ctx.behavior.compactKeepTurns >= 0 ? ctx.behavior.compactKeepTurns : 4;
12506
12591
  let totalBytes = 0;
12507
12592
  for (const msg of sanitizedMessages) for (const block of msg.content) if (block.type === "tool_result") totalBytes += memoizedToolResultBudgetBytes(block);
12508
12593
  try {
12509
- const compacted = await ctx.compactStrategy(sanitizedMessages, {
12594
+ const compacted = await ctx.behavior.compactStrategy(sanitizedMessages, {
12510
12595
  threshold,
12511
12596
  keepTurns: keep,
12512
12597
  totalBytes
@@ -12516,9 +12601,9 @@ async function executeTurn(ctx, turn, priorUsage) {
12516
12601
  console.error("[zidane] compactStrategy function threw:", err);
12517
12602
  }
12518
12603
  }
12519
- if (typeof ctx.mediaKeepTurns === "number" && ctx.mediaKeepTurns >= 0 && ctx.tools.read_file) {
12604
+ if (typeof ctx.behavior.mediaKeepTurns === "number" && ctx.behavior.mediaKeepTurns >= 0 && ctx.tools.read_file) {
12520
12605
  const { readPathByCallId } = callMaps();
12521
- const aged = applyMediaAgeout(sanitizedMessages, ctx.mediaKeepTurns, {
12606
+ const aged = applyMediaAgeout(sanitizedMessages, ctx.behavior.mediaKeepTurns, {
12522
12607
  chunkTurns: 8,
12523
12608
  readPathByCallId,
12524
12609
  readFileToolName: toWireName("read_file", ctx.aliasMaps),
@@ -12527,22 +12612,22 @@ async function executeTurn(ctx, turn, priorUsage) {
12527
12612
  sanitizedMessages = aged.messages;
12528
12613
  markReadStateForElidedPaths(ctx, ctx.handle.cwd, aged.elidedReadPaths);
12529
12614
  }
12530
- const effectiveThinkingBudget = ctx.thinking === "adaptive" && ctx.provider.meta.capabilities?.adaptiveBudgetIsEnvelope === true ? ctx.thinkingBudget : applyThinkingDecay(ctx.thinkingBudget, ctx.thinkingDecay, turn + 1);
12615
+ const effectiveThinkingBudget = ctx.thinking === "adaptive" && ctx.provider.meta.capabilities?.adaptiveBudgetIsEnvelope === true ? ctx.behavior.thinkingBudget : applyThinkingDecay(ctx.behavior.thinkingBudget, ctx.behavior.thinkingDecay, turn + 1);
12531
12616
  const formattedTools = ctx.rebuildFormattedTools ? ctx.rebuildFormattedTools(ctx.provider) : ctx.formattedTools;
12532
12617
  const streamOptions = {
12533
12618
  model: ctx.model,
12534
12619
  system: ctx.system,
12535
12620
  tools: formattedTools,
12536
12621
  messages: sanitizedMessages,
12537
- maxTokens: ctx.maxTokens,
12538
- ...ctx.modelCatalog ? { modelCatalog: ctx.modelCatalog } : {},
12622
+ maxTokens: ctx.behavior.maxTokens,
12623
+ ...ctx.behavior.modelCatalog ? { modelCatalog: ctx.behavior.modelCatalog } : {},
12539
12624
  thinking: ctx.thinking,
12540
12625
  thinkingBudget: effectiveThinkingBudget,
12541
- ...ctx.taskBudget !== void 0 ? { taskBudget: ctx.taskBudget } : {},
12542
- ...ctx.effortHint !== void 0 ? { effortHint: ctx.effortHint } : {},
12626
+ ...ctx.behavior.taskBudget !== void 0 ? { taskBudget: ctx.behavior.taskBudget } : {},
12627
+ ...ctx.behavior.effortHint !== void 0 ? { effortHint: ctx.behavior.effortHint } : {},
12543
12628
  ...ctx.modelOptions ? { modelOptions: ctx.modelOptions } : {},
12544
- cache: ctx.cache ?? true,
12545
- ...ctx.cacheTtl !== void 0 ? { cacheTtl: ctx.cacheTtl } : {},
12629
+ cache: ctx.behavior.cache ?? true,
12630
+ ...ctx.behavior.cacheTtl !== void 0 ? { cacheTtl: ctx.behavior.cacheTtl } : {},
12546
12631
  signal: ctx.signal
12547
12632
  };
12548
12633
  const transformCtx = { messages: streamOptions.messages };
@@ -12585,7 +12670,7 @@ async function executeTurn(ctx, turn, priorUsage) {
12585
12670
  if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/loop] stream hook handler threw: ${errorMessage(err)}\n`);
12586
12671
  });
12587
12672
  };
12588
- const retryCfg = resolveRetryConfig(ctx.retry);
12673
+ const retryCfg = resolveRetryConfig(ctx.behavior.retry);
12589
12674
  let result;
12590
12675
  let attempt = 0;
12591
12676
  let overflowRecoveries = 0;
@@ -12641,17 +12726,23 @@ async function executeTurn(ctx, turn, priorUsage) {
12641
12726
  break;
12642
12727
  } catch (caught) {
12643
12728
  let terminalErr = caught;
12644
- let wasAborted = ctx.signal.aborted || caught instanceof Error && caught.name === "AbortError";
12645
- const classified = !wasAborted ? ctx.provider.classifyError?.(caught) : null;
12646
- const isRetryable = classified?.kind === "provider_error" && classified.retryable === true;
12647
- const cleanSlate = currentText === "" && currentThinking === "" && !serverToolActivity;
12648
- const isStreamWatchdogTimeout = !wasAborted && caught instanceof OperationTimeoutError && PROVIDER_WATCHDOG_OPERATION_SET.has(caught.operation);
12649
- const canRetry = !wasAborted && (isRetryable || isStreamWatchdogTimeout) && cleanSlate && attempt < retryCfg.maxAttempts;
12650
- const retryAfterMs = canRetry ? extractRetryAfterMs(caught) : void 0;
12651
- const hasNextTarget = ctx.targets !== void 0 && (ctx.activeTargetIndex ?? 0) + 1 < ctx.targets.length;
12652
- if (canRetry && !(retryAfterMs !== void 0 && retryAfterMs > LONG_RETRY_AFTER_FALLBACK_MS && hasNextTarget)) {
12729
+ const failure = assessStreamFailure({
12730
+ err: caught,
12731
+ runAborted: ctx.signal.aborted,
12732
+ classifyError: ctx.provider.classifyError?.bind(ctx.provider),
12733
+ progress: {
12734
+ text: currentText,
12735
+ thinking: currentThinking,
12736
+ serverToolActivity
12737
+ },
12738
+ attempt,
12739
+ retry: retryCfg,
12740
+ hasNextTarget: ctx.targets !== void 0 && (ctx.activeTargetIndex ?? 0) + 1 < ctx.targets.length
12741
+ });
12742
+ let wasAborted = failure.wasAborted;
12743
+ if (failure.canRetry && !failure.preferFallbackOverRetry) {
12653
12744
  const meta = extractStreamErrorMeta(caught);
12654
- const delayMs = computeRetryDelayMs(attempt, retryCfg, retryAfterMs);
12745
+ const delayMs = computeRetryDelayMs(attempt, retryCfg, failure.retryAfterMs);
12655
12746
  await settledHook(ctx, "hook stream:retry", ctx.hooks.callHook("stream:retry", {
12656
12747
  turnId,
12657
12748
  attempt,
@@ -12671,7 +12762,7 @@ async function executeTurn(ctx, turn, priorUsage) {
12671
12762
  wasAborted = true;
12672
12763
  }
12673
12764
  }
12674
- if (!wasAborted && classified?.kind === "context_exceeded" && cleanSlate && overflowRecoveries < CONTEXT_OVERFLOW_KEEP_TURNS.length) {
12765
+ if (!wasAborted && failure.canRecoverOverflow && overflowRecoveries < CONTEXT_OVERFLOW_KEEP_TURNS.length) {
12675
12766
  const { readPathByCallId, toolNameByCallId } = callMaps();
12676
12767
  let recoveredMessages;
12677
12768
  const elidedReadPaths = [];
@@ -12708,45 +12799,10 @@ async function executeTurn(ctx, turn, priorUsage) {
12708
12799
  continue;
12709
12800
  }
12710
12801
  }
12711
- if (!wasAborted && cleanSlate && ctx.targets && classified?.kind !== "context_exceeded" && classified?.kind !== "tool_pairing_corruption") {
12712
- const fromIndex = ctx.activeTargetIndex ?? 0;
12713
- const nextIndex = fromIndex + 1;
12714
- if (nextIndex < ctx.targets.length) {
12715
- const from = ctx.targets[fromIndex];
12716
- const to = ctx.targets[nextIndex];
12717
- ctx.activeTargetIndex = nextIndex;
12718
- ctx.provider = to.provider;
12719
- ctx.model = to.model;
12720
- ctx.providerName = to.provider.name;
12721
- if (ctx.rebuildFormattedTools) ctx.formattedTools = ctx.rebuildFormattedTools(to.provider);
12722
- (ctx.targetFailures ??= []).push({
12723
- provider: from.provider.name,
12724
- model: from.model,
12725
- err: caught
12726
- });
12727
- if (ctx.autoCompact) {
12728
- const nextWindow = to.contextWindow ?? (ctx.autoCompact.contextWindowFromProviderMeta ? to.provider.meta.contextWindow : void 0);
12729
- if (typeof nextWindow === "number" && Number.isFinite(nextWindow) && nextWindow > 0) ctx.autoCompact.contextWindow = nextWindow;
12730
- }
12731
- streamOptions.model = to.model;
12732
- streamOptions.tools = ctx.formattedTools;
12733
- streamOptions.messages = sanitizeStoredToolResults(to.provider, streamOptions.messages);
12734
- attempt = 0;
12735
- await settledHook(ctx, "hook model:fallback", ctx.hooks.callHook("model:fallback", {
12736
- turnId,
12737
- from: {
12738
- provider: from.provider.name,
12739
- model: from.model
12740
- },
12741
- to: {
12742
- provider: to.provider.name,
12743
- model: to.model
12744
- },
12745
- targetIndex: nextIndex,
12746
- err: caught
12747
- }));
12748
- continue;
12749
- }
12802
+ if (!wasAborted && failure.canFallback) {
12803
+ await advanceToFallbackTarget(ctx, streamOptions, turnId, caught);
12804
+ attempt = 0;
12805
+ continue;
12750
12806
  }
12751
12807
  await streamHookChain;
12752
12808
  const errorUsage = {
@@ -12801,7 +12857,7 @@ async function executeTurn(ctx, turn, priorUsage) {
12801
12857
  name: toCanonicalName(tc.name, ctx.aliasMaps)
12802
12858
  }));
12803
12859
  let canonicalContent = rewriteContentToCanonical(result.assistantMessage?.content ?? [], ctx.aliasMaps);
12804
- if (canonicalToolCalls.length > 0 && !ctx.strictToolPairing) {
12860
+ if (canonicalToolCalls.length > 0 && !ctx.behavior.strictToolPairing) {
12805
12861
  const priorIds = /* @__PURE__ */ new Set();
12806
12862
  for (const t of ctx.turns) for (const block of t.content) if (block.type === "tool_call") priorIds.add(block.id);
12807
12863
  const reminted = remintDuplicateToolCallIds(canonicalToolCalls, canonicalContent, priorIds);
@@ -12843,7 +12899,7 @@ async function executeTurn(ctx, turn, priorUsage) {
12843
12899
  },
12844
12900
  cumulativeUsage: buildCumulativeUsage(priorUsage, result.usage)
12845
12901
  }));
12846
- if (ctx.resumeOnMaxTokens && (ctx.maxConsecutiveLengthResumes ?? 8) > 0 && result.usage.finishReason === "length" && canonicalToolCalls.length === 0 && !ctx.signal.aborted) {
12902
+ if (ctx.behavior.resumeOnMaxTokens && (ctx.behavior.maxConsecutiveLengthResumes ?? 8) > 0 && result.usage.finishReason === "length" && canonicalToolCalls.length === 0 && !ctx.signal.aborted) {
12847
12903
  const continueMsg = ctx.provider.userMessage("Please continue.");
12848
12904
  ctx.turns.push({
12849
12905
  id: await ctx.generateTurnId(),
@@ -12860,7 +12916,7 @@ async function executeTurn(ctx, turn, priorUsage) {
12860
12916
  ...currentText.length === 0 ? { lengthEmpty: true } : {}
12861
12917
  };
12862
12918
  }
12863
- if (result.usage.finishReason === "content-filter" && canonicalToolCalls.length === 0 && !ctx.signal.aborted && (ctx.maxConsecutiveRefusalResumes ?? 3) > 0) {
12919
+ if (result.usage.finishReason === "content-filter" && canonicalToolCalls.length === 0 && !ctx.signal.aborted && (ctx.behavior.maxConsecutiveRefusalResumes ?? 3) > 0) {
12864
12920
  const continueMsg = ctx.provider.userMessage("The previous response was interrupted by the provider (refusal stop). Please continue with the task.");
12865
12921
  ctx.turns.push({
12866
12922
  id: await ctx.generateTurnId(),
@@ -12878,7 +12934,7 @@ async function executeTurn(ctx, turn, priorUsage) {
12878
12934
  };
12879
12935
  }
12880
12936
  if (result.done) {
12881
- if (ctx.schema && !ctx.signal.aborted) {
12937
+ if (ctx.behavior.schema && !ctx.signal.aborted) {
12882
12938
  const enforcement = await enforceSchemaOutput(ctx, turnId);
12883
12939
  const sumOptional = (a, b) => ({
12884
12940
  has: a !== void 0 || b !== void 0,
@@ -12927,7 +12983,7 @@ async function executeTurn(ctx, turn, priorUsage) {
12927
12983
  };
12928
12984
  }
12929
12985
  const toolResults = await executeToolBatch(ctx, canonicalToolCalls, turnId);
12930
- if (ctx.elideStaleReads === "notice") appendStaleReadNotices(ctx.turns, canonicalToolCalls, toolResults);
12986
+ if (ctx.behavior.elideStaleReads === "notice") appendStaleReadNotices(ctx.turns, canonicalToolCalls, toolResults);
12931
12987
  const toolResultMsg = ctx.provider.toolResultsMessage(toolResults);
12932
12988
  const toolResultsTurn = {
12933
12989
  id: ctx.clock.randomUUID(),
@@ -12943,8 +12999,8 @@ async function executeTurn(ctx, turn, priorUsage) {
12943
12999
  message: toolResultsTurn,
12944
13000
  results: toolResults
12945
13001
  }));
12946
- if (typeof ctx.toolOutputBudget === "number" && ctx.toolOutputBudget > 0) {
12947
- const excludeSet = ctx.toolOutputBudgetExcludeTools && ctx.toolOutputBudgetExcludeTools.length > 0 ? new Set(ctx.toolOutputBudgetExcludeTools) : void 0;
13002
+ if (typeof ctx.behavior.toolOutputBudget === "number" && ctx.behavior.toolOutputBudget > 0) {
13003
+ const excludeSet = ctx.behavior.toolOutputBudgetExcludeTools && ctx.behavior.toolOutputBudgetExcludeTools.length > 0 ? new Set(ctx.behavior.toolOutputBudgetExcludeTools) : void 0;
12948
13004
  const nameById = excludeSet ? new Map(canonicalToolCalls.map((c) => [c.id, c.name])) : void 0;
12949
13005
  const totalBytes = toolResults.reduce((sum, r) => {
12950
13006
  if (excludeSet && nameById) {
@@ -12953,8 +13009,8 @@ async function executeTurn(ctx, turn, priorUsage) {
12953
13009
  }
12954
13010
  return sum + toolOutputBudgetByteLength(r.content);
12955
13011
  }, 0);
12956
- if (totalBytes > ctx.toolOutputBudget) {
12957
- const warning = `[Tool output budget exceeded: ${totalBytes} text-equivalent bytes returned in this turn (cap: ${ctx.toolOutputBudget}). Summarize the salient findings before calling more tools.]`;
13012
+ if (totalBytes > ctx.behavior.toolOutputBudget) {
13013
+ const warning = `[Tool output budget exceeded: ${totalBytes} text-equivalent bytes returned in this turn (cap: ${ctx.behavior.toolOutputBudget}). Summarize the salient findings before calling more tools.]`;
12958
13014
  const userMsg = ctx.provider.userMessage(warning);
12959
13015
  ctx.turns.push({
12960
13016
  id: await ctx.generateTurnId(),
@@ -12967,7 +13023,7 @@ async function executeTurn(ctx, turn, priorUsage) {
12967
13023
  turn,
12968
13024
  turnId,
12969
13025
  bytes: totalBytes,
12970
- budget: ctx.toolOutputBudget
13026
+ budget: ctx.behavior.toolOutputBudget
12971
13027
  });
12972
13028
  }
12973
13029
  }
@@ -13042,7 +13098,7 @@ function sanitizeToolOutputForProvider(provider, output) {
13042
13098
  * Empty object when the agent has no live connection.
13043
13099
  */
13044
13100
  function mcpConnectionFor(ctx) {
13045
- const connection = ctx.agentMcpConnection?.();
13101
+ const connection = ctx.inheritance.mcpConnection?.();
13046
13102
  if (!connection) return {};
13047
13103
  return { mcpConnection: nonOwningMcpView(connection) };
13048
13104
  }
@@ -13265,14 +13321,14 @@ async function runSingleToolDispatch(ctx, call, turnId, fixed) {
13265
13321
  execution: ctx.execution,
13266
13322
  handle: ctx.handle,
13267
13323
  hooks: ctx.hooks,
13268
- tools: ctx.agentTools,
13269
- ...ctx.agentName !== void 0 ? { name: ctx.agentName } : {},
13270
- ...ctx.agentSystem !== void 0 ? { system: ctx.agentSystem } : {},
13271
- ...ctx.agentToolAliases !== void 0 ? { toolAliases: ctx.agentToolAliases } : {},
13272
- ...ctx.agentMcpServers !== void 0 ? { mcpServers: ctx.agentMcpServers } : {},
13324
+ tools: ctx.inheritance.tools,
13325
+ ...ctx.inheritance.name !== void 0 ? { name: ctx.inheritance.name } : {},
13326
+ ...ctx.inheritance.system !== void 0 ? { system: ctx.inheritance.system } : {},
13327
+ ...ctx.inheritance.toolAliases !== void 0 ? { toolAliases: ctx.inheritance.toolAliases } : {},
13328
+ ...ctx.inheritance.mcpServers !== void 0 ? { mcpServers: ctx.inheritance.mcpServers } : {},
13273
13329
  ...mcpConnectionFor(ctx),
13274
- ...ctx.agentSkills !== void 0 ? { skills: ctx.agentSkills } : {},
13275
- ...ctx.agentBehavior !== void 0 ? { behavior: ctx.agentBehavior } : {},
13330
+ ...ctx.inheritance.skills !== void 0 ? { skills: ctx.inheritance.skills } : {},
13331
+ behavior: ctx.behavior,
13276
13332
  turnId,
13277
13333
  callId,
13278
13334
  runId: ctx.runId,
@@ -13335,7 +13391,7 @@ async function runSingleToolDispatch(ctx, call, turnId, fixed) {
13335
13391
  isError = true;
13336
13392
  } else {
13337
13393
  const error = err instanceof Error ? err : new Error(String(err));
13338
- if (ctx.shouldRethrowToolError?.(error)) {
13394
+ if (ctx.behavior.shouldRethrowToolError?.(error)) {
13339
13395
  ctx.toolState?.discard(callId);
13340
13396
  throw error;
13341
13397
  }
@@ -13493,8 +13549,8 @@ async function emitToolResult(ctx, params) {
13493
13549
  const metadata = sanitizedMetadata.metadata;
13494
13550
  if (sanitizedMetadata.warnings.length > 0 && process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/loop] sanitized tool metadata for ${name}/${callId}: ${sanitizedMetadata.warnings.join("; ")}\n`);
13495
13551
  const finalOutcome = isError && outcome && outcome !== "failed" ? "failed" : outcome;
13496
- const threshold = ctx.persistThreshold;
13497
- const persistDir = ctx.persistDir;
13552
+ const threshold = ctx.behavior.persistThreshold;
13553
+ const persistDir = ctx.behavior.persistDir;
13498
13554
  if (threshold && threshold > 0 && persistDir) {
13499
13555
  const outcome = await maybePersistToolResult({
13500
13556
  toolName: name,
@@ -13502,8 +13558,8 @@ async function emitToolResult(ctx, params) {
13502
13558
  output,
13503
13559
  threshold,
13504
13560
  persistDir,
13505
- ...ctx.persistExcludeTools ? { excludeTools: ctx.persistExcludeTools } : {},
13506
- ...typeof ctx.persistMaxBytes === "number" ? { maxBytes: ctx.persistMaxBytes } : {},
13561
+ ...ctx.behavior.persistExcludeTools ? { excludeTools: ctx.behavior.persistExcludeTools } : {},
13562
+ ...typeof ctx.behavior.persistMaxBytes === "number" ? { maxBytes: ctx.behavior.persistMaxBytes } : {},
13507
13563
  writeBlob: executionBlobWriter(ctx)
13508
13564
  });
13509
13565
  if (outcome.kind === "persisted") output = outcome.output;
@@ -13593,13 +13649,12 @@ function resolveConcurrencySafe(def, input) {
13593
13649
  * AND carries the shell-cascade signal — siblings see one signal source.
13594
13650
  */
13595
13651
  async function executeToolBatch(ctx, toolCalls, turnId) {
13596
- if (ctx.toolBatchExecutor) return executeCustomToolBatch(ctx, toolCalls, turnId, ctx.toolBatchExecutor);
13652
+ if (ctx.behavior.toolBatchExecutor && !ctx.suppressBatchExecutor) return executeCustomToolBatch(ctx, toolCalls, turnId, ctx.behavior.toolBatchExecutor);
13597
13653
  return executeNativeToolBatch(ctx, toolCalls, turnId);
13598
13654
  }
13599
- async function executeCustomToolBatch(ctx, toolCalls, turnId, executor) {
13600
- if (toolCalls.length === 0) return [];
13655
+ function createToolBatchHarness(ctx, toolCalls, turnId, options = {}) {
13601
13656
  const N = toolCalls.length;
13602
- const maxConcurrent = Math.max(1, ctx.maxConcurrentTools ?? DEFAULT_MAX_CONCURRENT_TOOLS);
13657
+ const maxConcurrent = Math.max(1, ctx.behavior.maxConcurrentTools ?? DEFAULT_MAX_CONCURRENT_TOOLS);
13603
13658
  const safe = Array.from({ length: N });
13604
13659
  for (let i = 0; i < N; i++) safe[i] = resolveConcurrencySafe(ctx.tools[toolCalls[i].name], toolCalls[i].input);
13605
13660
  const siblingAbort = new AbortController();
@@ -13612,14 +13667,20 @@ async function executeCustomToolBatch(ctx, toolCalls, turnId, executor) {
13612
13667
  const childCtx = {
13613
13668
  ...ctx,
13614
13669
  signal: siblingAbort.signal,
13615
- toolBatchExecutor: void 0
13670
+ ...options.suppressBatchExecutor ? { suppressBatchExecutor: true } : {}
13616
13671
  };
13672
+ /**
13673
+ * Distinguish a shell-cascade kill from a user-issued abort so the model sees
13674
+ * actionable text. When BOTH the parent signal and the sibling signal are
13675
+ * aborted, the parent wins — user-issued aborts take precedence (the model is
13676
+ * being interrupted by the human, not by a sibling's failure).
13677
+ */
13617
13678
  const cancelMessage = () => {
13618
13679
  if (ctx.signal.aborted) return INTERRUPT_MESSAGE_FOR_TOOL_USE;
13619
13680
  if (siblingAbort.signal.reason === SHELL_CASCADE_REASON) return SHELL_CASCADE_CANCEL_MESSAGE;
13620
13681
  return INTERRUPT_MESSAGE_FOR_TOOL_USE;
13621
13682
  };
13622
- const execute = async (index) => {
13683
+ const settle = async (index) => {
13623
13684
  const call = toolCalls[index];
13624
13685
  try {
13625
13686
  const { result } = await executeSingleTool(childCtx, call, turnId);
@@ -13627,7 +13688,7 @@ async function executeCustomToolBatch(ctx, toolCalls, turnId, executor) {
13627
13688
  if (result.isError && !isUserCancel && call.name === SHELL_TOOL_NAME && !siblingAbort.signal.aborted) siblingAbort.abort(SHELL_CASCADE_REASON);
13628
13689
  return result;
13629
13690
  } catch (err) {
13630
- if (ctx.shouldRethrowToolError?.(err)) throw err;
13691
+ if (ctx.behavior.shouldRethrowToolError?.(err)) throw err;
13631
13692
  const isAbort = siblingAbort.signal.aborted || ctx.signal.aborted || err instanceof Error && err.name === "AbortError";
13632
13693
  return {
13633
13694
  id: call.id,
@@ -13636,6 +13697,25 @@ async function executeCustomToolBatch(ctx, toolCalls, turnId, executor) {
13636
13697
  };
13637
13698
  }
13638
13699
  };
13700
+ return {
13701
+ childCtx,
13702
+ siblingAbort,
13703
+ safe,
13704
+ maxConcurrent,
13705
+ cancelMessage,
13706
+ settle,
13707
+ dispose: () => {
13708
+ if (parentAbortListener) {
13709
+ ctx.signal.removeEventListener("abort", parentAbortListener);
13710
+ parentAbortListener = void 0;
13711
+ }
13712
+ }
13713
+ };
13714
+ }
13715
+ async function executeCustomToolBatch(ctx, toolCalls, turnId, executor) {
13716
+ if (toolCalls.length === 0) return [];
13717
+ const harness = createToolBatchHarness(ctx, toolCalls, turnId, { suppressBatchExecutor: true });
13718
+ const { childCtx, siblingAbort, safe, maxConcurrent, cancelMessage } = harness;
13639
13719
  const executeDurable = async (indices, all) => {
13640
13720
  const starts = [];
13641
13721
  const results = /* @__PURE__ */ new Map();
@@ -13700,7 +13780,7 @@ async function executeCustomToolBatch(ctx, toolCalls, turnId, executor) {
13700
13780
  signal: ctx.signal,
13701
13781
  steeringQueue: ctx.steeringQueue,
13702
13782
  isConcurrencySafe: (index) => safe[index] === true,
13703
- execute,
13783
+ execute: harness.settle,
13704
13784
  canExecuteDurably: (index) => typeof ctx.tools[toolCalls[index].name]?.durableExecute === "function",
13705
13785
  executeDurable,
13706
13786
  interruptedResult: (index) => ({
@@ -13714,7 +13794,7 @@ async function executeCustomToolBatch(ctx, toolCalls, turnId, executor) {
13714
13794
  isError: true
13715
13795
  })
13716
13796
  };
13717
- const timeoutMs = typeof ctx.toolBatchTimeoutMs === "number" && Number.isFinite(ctx.toolBatchTimeoutMs) && ctx.toolBatchTimeoutMs > 0 ? ctx.toolBatchTimeoutMs : 0;
13797
+ const timeoutMs = typeof ctx.behavior.toolBatchTimeoutMs === "number" && Number.isFinite(ctx.behavior.toolBatchTimeoutMs) && ctx.behavior.toolBatchTimeoutMs > 0 ? ctx.behavior.toolBatchTimeoutMs : 0;
13718
13798
  try {
13719
13799
  if (timeoutMs <= 0) return await executor(executionCtx);
13720
13800
  return await withTimeout$1(executor(executionCtx), {
@@ -13731,7 +13811,7 @@ async function executeCustomToolBatch(ctx, toolCalls, turnId, executor) {
13731
13811
  isError: true
13732
13812
  }));
13733
13813
  } finally {
13734
- if (parentAbortListener) ctx.signal.removeEventListener("abort", parentAbortListener);
13814
+ harness.dispose();
13735
13815
  }
13736
13816
  }
13737
13817
  async function startDurableToolExecution(ctx, call, turnId, index) {
@@ -13866,14 +13946,14 @@ async function startDurableToolExecution(ctx, call, turnId, index) {
13866
13946
  execution: ctx.execution,
13867
13947
  handle: ctx.handle,
13868
13948
  hooks: ctx.hooks,
13869
- tools: ctx.agentTools,
13870
- ...ctx.agentName !== void 0 ? { name: ctx.agentName } : {},
13871
- ...ctx.agentSystem !== void 0 ? { system: ctx.agentSystem } : {},
13872
- ...ctx.agentToolAliases !== void 0 ? { toolAliases: ctx.agentToolAliases } : {},
13873
- ...ctx.agentMcpServers !== void 0 ? { mcpServers: ctx.agentMcpServers } : {},
13949
+ tools: ctx.inheritance.tools,
13950
+ ...ctx.inheritance.name !== void 0 ? { name: ctx.inheritance.name } : {},
13951
+ ...ctx.inheritance.system !== void 0 ? { system: ctx.inheritance.system } : {},
13952
+ ...ctx.inheritance.toolAliases !== void 0 ? { toolAliases: ctx.inheritance.toolAliases } : {},
13953
+ ...ctx.inheritance.mcpServers !== void 0 ? { mcpServers: ctx.inheritance.mcpServers } : {},
13874
13954
  ...mcpConnectionFor(ctx),
13875
- ...ctx.agentSkills !== void 0 ? { skills: ctx.agentSkills } : {},
13876
- ...ctx.agentBehavior !== void 0 ? { behavior: ctx.agentBehavior } : {},
13955
+ ...ctx.inheritance.skills !== void 0 ? { skills: ctx.inheritance.skills } : {},
13956
+ behavior: ctx.behavior,
13877
13957
  turnId,
13878
13958
  callId,
13879
13959
  runId: ctx.runId,
@@ -13930,7 +14010,7 @@ async function completeDurableToolExecution(ctx, start, output) {
13930
14010
  };
13931
14011
  }
13932
14012
  async function failDurableToolExecution(ctx, start, err, cancelMessage) {
13933
- if (ctx.shouldRethrowToolError?.(err)) {
14013
+ if (ctx.behavior.shouldRethrowToolError?.(err)) {
13934
14014
  ctx.toolState?.discard(start.callId);
13935
14015
  throw err;
13936
14016
  }
@@ -14013,49 +14093,14 @@ function errorFromSerializedFailure(failure) {
14013
14093
  async function executeNativeToolBatch(ctx, toolCalls, turnId) {
14014
14094
  if (toolCalls.length === 0) return [];
14015
14095
  const N = toolCalls.length;
14016
- const maxConcurrent = Math.max(1, ctx.maxConcurrentTools ?? DEFAULT_MAX_CONCURRENT_TOOLS);
14017
14096
  const results = Array.from({ length: N });
14018
- const safe = Array.from({ length: N });
14019
- for (let i = 0; i < N; i++) safe[i] = resolveConcurrencySafe(ctx.tools[toolCalls[i].name], toolCalls[i].input);
14020
- const siblingAbort = new AbortController();
14021
- let parentAbortListener;
14022
- if (ctx.signal.aborted) siblingAbort.abort(ctx.signal.reason ?? "parent-aborted");
14023
- else {
14024
- parentAbortListener = () => siblingAbort.abort(ctx.signal.reason ?? "parent-aborted");
14025
- ctx.signal.addEventListener("abort", parentAbortListener, { once: true });
14026
- }
14027
- const childCtx = {
14028
- ...ctx,
14029
- signal: siblingAbort.signal
14030
- };
14097
+ const harness = createToolBatchHarness(ctx, toolCalls, turnId);
14098
+ const { safe, maxConcurrent } = harness;
14031
14099
  /** Indices currently in flight. Tracked for fleet-safety + cap checks. */
14032
14100
  const inFlight = /* @__PURE__ */ new Map();
14033
- /**
14034
- * Distinguish a shell-cascade kill from a user-issued abort so the
14035
- * model sees actionable text. When BOTH the parent signal and the
14036
- * sibling signal are aborted, the parent wins — user-issued aborts
14037
- * take precedence (the model is being interrupted by the human, not
14038
- * by a sibling's failure).
14039
- */
14040
- const cancelMessage = () => {
14041
- if (ctx.signal.aborted) return INTERRUPT_MESSAGE_FOR_TOOL_USE;
14042
- if (siblingAbort.signal.reason === SHELL_CASCADE_REASON) return SHELL_CASCADE_CANCEL_MESSAGE;
14043
- return INTERRUPT_MESSAGE_FOR_TOOL_USE;
14044
- };
14045
14101
  const dispatch = (index) => {
14046
- const call = toolCalls[index];
14047
- return executeSingleTool(childCtx, call, turnId).then(({ result }) => {
14102
+ return harness.settle(index).then((result) => {
14048
14103
  results[index] = result;
14049
- const isUserCancel = typeof result.content === "string" && result.content === "[Tool call cancelled by user]";
14050
- if (result.isError && !isUserCancel && call.name === SHELL_TOOL_NAME && !siblingAbort.signal.aborted) siblingAbort.abort(SHELL_CASCADE_REASON);
14051
- }, (err) => {
14052
- if (ctx.shouldRethrowToolError?.(err)) throw err;
14053
- const isAbort = siblingAbort.signal.aborted || ctx.signal.aborted || err instanceof Error && err.name === "AbortError";
14054
- results[index] = {
14055
- id: call.id,
14056
- content: isAbort ? cancelMessage() : `Error: ${errorMessage(err)}`,
14057
- isError: true
14058
- };
14059
14104
  }).finally(() => {
14060
14105
  inFlight.delete(index);
14061
14106
  });
@@ -14115,7 +14160,7 @@ async function executeNativeToolBatch(ctx, toolCalls, turnId) {
14115
14160
  await drain();
14116
14161
  return results;
14117
14162
  } finally {
14118
- if (parentAbortListener) ctx.signal.removeEventListener("abort", parentAbortListener);
14163
+ harness.dispose();
14119
14164
  }
14120
14165
  }
14121
14166
  //#endregion
@@ -15076,6 +15121,257 @@ function buildMcpToolNameSeparatorAliases(mcpToolNames, servers, separator) {
15076
15121
  return Object.keys(aliases).length > 0 ? aliases : void 0;
15077
15122
  }
15078
15123
  /**
15124
+ * Partition the registry into eager vs lazy buckets and build the per-run
15125
+ * unlock bookkeeping the loop reads when rebuilding the wire-level tool list.
15126
+ *
15127
+ * Native (non-MCP) tools are always eager; `tool_search` itself must be eager
15128
+ * too (the discovery path can't gate on its own surfacing). The `unlocked` set
15129
+ * seeds with every eager canonical tool name and grows as the model surfaces
15130
+ * lazy tools through `tool_search`.
15131
+ *
15132
+ * Aliases are passed through so lazy entries carry their wire-level display
15133
+ * name — what the catalog and `tool_search` results show to the model — while
15134
+ * keeping the canonical name on the entry for unlock-set membership and
15135
+ * dispatch-map lookup.
15136
+ */
15137
+ function buildRunDisclosureState(input) {
15138
+ const { tools: baseTools, mcpToolNames, mcpServers, toolDisclosure, toolSearch, toolAliases, hasRunToolOverride } = input;
15139
+ const disclosure = partitionToolDisclosure(baseTools, mcpToolNames, mcpServers, toolDisclosure, toolAliases);
15140
+ const unlocked = new Set(disclosure.eagerCanonicalNames);
15141
+ const initialUnlocked = new Set(disclosure.eagerCanonicalNames);
15142
+ const dynamicUnlockOrder = [];
15143
+ const dynamicUnlockSeen = /* @__PURE__ */ new Set();
15144
+ const recordUnlock = (canonical) => {
15145
+ if (initialUnlocked.has(canonical)) return;
15146
+ if (dynamicUnlockSeen.has(canonical)) return;
15147
+ dynamicUnlockSeen.add(canonical);
15148
+ unlocked.add(canonical);
15149
+ dynamicUnlockOrder.push(canonical);
15150
+ };
15151
+ const hostDefinedToolSearch = !!baseTools.tool_search;
15152
+ const injectedToolSearch = !hasRunToolOverride && (toolDisclosure === "lazy" || disclosure.lazyEntries.length > 0) && toolSearch?.tool !== false && !hostDefinedToolSearch;
15153
+ let tools = baseTools;
15154
+ if (injectedToolSearch) {
15155
+ const toolSearchTool = createToolSearchTool({
15156
+ catalog: disclosure.lazyEntries,
15157
+ unlocked,
15158
+ addUnlock: recordUnlock,
15159
+ ...toolSearch?.limit !== void 0 ? { defaultLimit: toolSearch.limit } : {}
15160
+ });
15161
+ tools = {
15162
+ ...baseTools,
15163
+ [toolSearchTool.spec.name]: toolSearchTool
15164
+ };
15165
+ unlocked.add(toolSearchTool.spec.name);
15166
+ initialUnlocked.add(toolSearchTool.spec.name);
15167
+ }
15168
+ return {
15169
+ disclosure,
15170
+ unlocked,
15171
+ initialUnlocked,
15172
+ dynamicUnlockOrder,
15173
+ recordUnlock,
15174
+ tools,
15175
+ injectedToolSearch,
15176
+ hostDefinedToolSearch
15177
+ };
15178
+ }
15179
+ /**
15180
+ * Choose the prior turns a run starts from, or `undefined` for a fresh start.
15181
+ *
15182
+ * Two resume flavors:
15183
+ * - `resumeRunId` (child-run resume): seed ONLY the referenced prior run's
15184
+ * lineage. Used by the spawn tool to continue a stopped/failed subagent —
15185
+ * the parent's conversation (and every other child's) stays invisible,
15186
+ * mirroring how a fresh child starts from just its task prompt. Allowed
15187
+ * WITH `parentRunId` set (unlike a full-session resume).
15188
+ * - full-session resume: the historical top-level path, on re-runs
15189
+ * (`runs > 0`) or promptless runs (session turns already contain the user
15190
+ * message). **Excluded for child agents**: when the spawn tool runs with
15191
+ * `persist: true`, the child shares the parent's session for *storage* but
15192
+ * its conversation must start fresh from the task prompt. Otherwise the
15193
+ * child would pull in the parent's in-flight assistant turn — including the
15194
+ * parent's pending `spawn` tool_use — and every provider (Anthropic loudly,
15195
+ * OpenAI implicitly) rejects a history where a `tool_use` lacks its
15196
+ * matching `tool_result`.
15197
+ */
15198
+ function selectSeedTurns(input) {
15199
+ const { session, options, resumeFilteredTurns } = input;
15200
+ if (options.resumeRunId && session) {
15201
+ const chain = resumeChainRunIds(session.runs, options.resumeRunId);
15202
+ return filterUnresolvedToolUses(session.turns.filter((t) => t.runId !== void 0 && chain.has(t.runId)));
15203
+ }
15204
+ if (!(session && session.turns.length > 0 && (session.runs.length > 0 || !options.prompt) && !options.parentRunId && !options.resumeRunId)) return void 0;
15205
+ const childRunIds = childRunIdSet(session);
15206
+ const resumed = childRunIds.size === 0 ? session.turns : session.turns.filter((t) => !t.runId || !childRunIds.has(t.runId));
15207
+ return resumeFilteredTurns && resumed === session.turns ? resumeFilteredTurns : filterUnresolvedToolUses(resumed);
15208
+ }
15209
+ /**
15210
+ * Replay every resolved `tool_search` from a resumed history into the per-run
15211
+ * `unlocked` set.
15212
+ *
15213
+ * Without this seeding a resumed conversation shows the model "These tools are
15214
+ * now callable" in the prior tool_result, but the next turn's `formattedTools`
15215
+ * and the `tool:gate` hook still treat the tools as locked — every subsequent
15216
+ * `tool_use` is rejected with "load via tool_search first", which the model
15217
+ * can't recover from without a fresh `tool_search` round-trip.
15218
+ *
15219
+ * Only replays calls whose result the model actually consumed: a `tool_call`
15220
+ * without a matching successful `tool_result` is either an unresolved pair
15221
+ * `filterUnresolvedToolUses` already stripped or a structural error — either
15222
+ * way, unlocking from it would be speculative.
15223
+ */
15224
+ function replayToolSearchUnlocks(seededTurns, lazyEntries, unlocked, limit, recordUnlock) {
15225
+ const resolvedCallIds = /* @__PURE__ */ new Set();
15226
+ for (const turn of seededTurns) for (const block of turn.content) if (block.type === "tool_result" && !block.isError) resolvedCallIds.add(block.callId);
15227
+ for (const turn of seededTurns) for (const block of turn.content) {
15228
+ if (block.type !== "tool_call") continue;
15229
+ if (block.name !== "tool_search") continue;
15230
+ if (!resolvedCallIds.has(block.id)) continue;
15231
+ applyToolSearchToUnlocked(lazyEntries, block.input, unlocked, limit, recordUnlock);
15232
+ }
15233
+ }
15234
+ /**
15235
+ * Reconcile + drain pending background-task notifications.
15236
+ *
15237
+ * Each exit becomes a `<task-notification>` text block prepended to the user
15238
+ * turn so the model sees task completions on its very next prompt, without
15239
+ * polling. Two sources, merged by taskId:
15240
+ *
15241
+ * 1. PUSH — the agent-scoped queue populated by `background:exit` since the
15242
+ * last `run()` (in-process contexts fire `onExit`).
15243
+ * 2. PULL — a `listBackground` sweep for terminated tasks the model hasn't
15244
+ * been told about. This is what makes remote / durable contexts work:
15245
+ * they have no legal way to push from a timer into the host (journal
15246
+ * rules), so the loop polls at the run boundary instead.
15247
+ * `endedAt - startedAt` reproduces `durationMs` without depending on
15248
+ * sweep timing.
15249
+ *
15250
+ * The `background:reconcile` hook fires on the merged list INSIDE the run()
15251
+ * await chain — the journalable seam for durable hosts (see the hook's doc).
15252
+ * Entries suppressed earlier (`shell_kill`, `read_file`, `wait_task`) are
15253
+ * absent from both sources via the `notified` latch. See
15254
+ * `docs/RUN_IN_BACKGROUND.md` §Completion notification.
15255
+ *
15256
+ * Durable hosts wrap the live `listBackground` poll in `journalReconcile` so it
15257
+ * records as one journaled side effect (replay returns the recorded snapshot,
15258
+ * keeping `exits.length` — and therefore the conditional hook's journal
15259
+ * position — stable across attempts). Everything downstream of the snapshot is
15260
+ * pure over journaled / replayed inputs (session-turn acks, duration
15261
+ * derivation), so journaling the poll is enough to make the whole reconcile
15262
+ * deterministic.
15263
+ */
15264
+ async function drainTaskNotifications(input) {
15265
+ const { state, executionContext, executionHandle, behavior, journalReconcile, session, turns, hooks } = input;
15266
+ const reconciled = new Map(state.pending);
15267
+ state.pending.clear();
15268
+ if (executionContext.listBackground && executionHandle && typeof behavior.tasksDir === "string" && behavior.tasksDir.length > 0) try {
15269
+ const listBackground = executionContext.listBackground;
15270
+ const handle = executionHandle;
15271
+ const entries = journalReconcile ? await journalReconcile(() => listBackground(handle)) : await listBackground(handle);
15272
+ const acks = entries.some((e) => e.status !== "running") ? collectTaskAcksFromTurns(session ? session.turns : turns) : void 0;
15273
+ for (const entry of entries) {
15274
+ if (entry.status === "running" || state.notified.has(entry.taskId) || reconciled.has(entry.taskId)) continue;
15275
+ if (acks) {
15276
+ if (acks.taskIds.has(entry.taskId)) continue;
15277
+ const readAt = acks.outputPaths.get(resolve(entry.outputPath));
15278
+ if (readAt !== void 0 && entry.endedAt !== void 0 && readAt >= entry.endedAt + READ_ACK_CLOCK_SKEW_MARGIN_MS) continue;
15279
+ }
15280
+ reconciled.set(entry.taskId, {
15281
+ taskId: entry.taskId,
15282
+ status: entry.status,
15283
+ exitCode: entry.exitCode ?? 0,
15284
+ ...entry.signal ? { signal: entry.signal } : {},
15285
+ outputPath: entry.outputPath,
15286
+ durationMs: Math.max(0, (entry.endedAt ?? entry.startedAt) - entry.startedAt),
15287
+ command: entry.command
15288
+ });
15289
+ }
15290
+ } catch (err) {
15291
+ if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/agent] background reconcile failed: ${err instanceof Error ? err.message : String(err)}\n`);
15292
+ }
15293
+ const reconcileCtx = { exits: [...reconciled.values()] };
15294
+ if (reconcileCtx.exits.length > 0) await hooks.callHook("background:reconcile", reconcileCtx);
15295
+ for (const taskId of reconciled.keys()) {
15296
+ state.notified.add(taskId);
15297
+ state.pendingStalls.delete(taskId);
15298
+ }
15299
+ const drained = [];
15300
+ for (const notif of reconcileCtx.exits) drained.push({
15301
+ type: "text",
15302
+ text: renderTaskNotificationXml(notif)
15303
+ });
15304
+ if (state.pendingStalls.size > 0) {
15305
+ for (const stall of state.pendingStalls.values()) drained.push({
15306
+ type: "text",
15307
+ text: renderTaskStallXml(stall)
15308
+ });
15309
+ state.pendingStalls.clear();
15310
+ }
15311
+ return drained;
15312
+ }
15313
+ /**
15314
+ * Assemble the tool registry a single run advertises.
15315
+ *
15316
+ * Pipeline: resolve the base set (run override > agent + MCP) → derive the
15317
+ * wire-name aliases → inject the skills tools → apply extension catalog
15318
+ * transforms → index by spec name → rewrite the built-in `shell` for this
15319
+ * run's capabilities.
15320
+ */
15321
+ function buildRunToolRegistry(input) {
15322
+ const { runOverrideTools, sourceTools, mcpConnection, mcpServers, mcpToolNameSeparator, toolAliases, resolvedSkills, skillsConfig, skillActivationState, hooks, toolCatalogTransforms, executionContext, behavior } = input;
15323
+ const runBaseTools = runOverrideTools !== void 0 ? runOverrideTools : mcpConnection ? {
15324
+ ...sourceTools,
15325
+ ...mcpConnection.tools
15326
+ } : sourceTools;
15327
+ const mcpToolNames = runOverrideTools === void 0 && mcpConnection ? new Set(Object.keys(mcpConnection.tools)) : /* @__PURE__ */ new Set();
15328
+ const generatedMcpToolAliases = buildMcpToolNameSeparatorAliases(mcpToolNames, mcpServers, mcpToolNameSeparator);
15329
+ const baseToolAliases = generatedMcpToolAliases ? {
15330
+ ...generatedMcpToolAliases,
15331
+ ...toolAliases ?? {}
15332
+ } : toolAliases;
15333
+ const sanitizedWireAliases = buildSanitizedWireAliases(mcpToolNames, baseToolAliases);
15334
+ const effectiveToolAliases = sanitizedWireAliases ? {
15335
+ ...baseToolAliases ?? {},
15336
+ ...sanitizedWireAliases
15337
+ } : baseToolAliases;
15338
+ const transformedToolCatalog = applyToolCatalogTransforms(runOverrideTools === void 0 && !!resolvedSkills && resolvedSkills.length > 0 && skillsConfig?.tool !== false ? {
15339
+ skills_use: createSkillsUseTool({
15340
+ catalog: resolvedSkills,
15341
+ state: skillActivationState,
15342
+ hooks,
15343
+ allowShellInterpolation: skillsConfig?.allowShellInterpolation,
15344
+ approveShellInterpolation: skillsConfig?.approveShellInterpolation
15345
+ }),
15346
+ skills_read: createSkillsReadTool({
15347
+ catalog: resolvedSkills,
15348
+ state: skillActivationState
15349
+ }),
15350
+ skills_run_script: createSkillsRunScriptTool({
15351
+ catalog: resolvedSkills,
15352
+ state: skillActivationState,
15353
+ scriptTimeoutMs: skillsConfig?.scriptTimeoutMs
15354
+ }),
15355
+ ...runBaseTools
15356
+ } : runBaseTools, toolCatalogTransforms, "run", mcpToolNames);
15357
+ const tools = {};
15358
+ for (const tool of Object.values(transformedToolCatalog)) tools[tool.spec.name] = tool;
15359
+ if (tools.shell === shell) {
15360
+ const detachedTasks = resolveDetachedTasksCapability(executionContext);
15361
+ const bgDisabled = behavior.disableBackgroundTasks === true || behavior.disableBackgroundTasks === "non-durable" && detachedTasks !== "durable";
15362
+ tools.shell = createShellTool({
15363
+ allowBackground: typeof behavior.tasksDir === "string" && behavior.tasksDir.length > 0 && !bgDisabled && detachedTasks !== "none",
15364
+ registeredCanonicals: new Set(Object.keys(tools)),
15365
+ ...effectiveToolAliases ? { toolAliases: effectiveToolAliases } : {}
15366
+ });
15367
+ }
15368
+ return {
15369
+ tools,
15370
+ mcpToolNames,
15371
+ toolAliases: effectiveToolAliases
15372
+ };
15373
+ }
15374
+ /**
15079
15375
  * Partition a tool registry into eager and lazy buckets.
15080
15376
  *
15081
15377
  * Native + skill tools always end up in the eager bucket — only MCP tools
@@ -15656,6 +15952,7 @@ function createAgent(rawOptions) {
15656
15952
  const externalSignal = options.signal;
15657
15953
  running = true;
15658
15954
  toolCancels.clearPending();
15955
+ const perRunUnregisters = [];
15659
15956
  try {
15660
15957
  abortController = new AbortController();
15661
15958
  idlePromise = new Promise((resolve) => {
@@ -15702,10 +15999,9 @@ function createAgent(rawOptions) {
15702
15999
  externalSignal.addEventListener("abort", externalAbortListener, { once: true });
15703
16000
  }
15704
16001
  const childrenStats = [];
15705
- const unregisterSpawnHook = hooks.hook("spawn:complete", (ctx) => {
16002
+ perRunUnregisters.push(hooks.hook("spawn:complete", (ctx) => {
15706
16003
  childrenStats.push(ctx);
15707
- });
15708
- const perRunUnregisters = [];
16004
+ }));
15709
16005
  if (options.hooks) for (const [event, handler] of Object.entries(options.hooks)) {
15710
16006
  if (!isKnownHookEvent(event)) throw new Error(`Unknown hook event "${event}" passed to run(). See AgentHooks for valid events.`);
15711
16007
  const handlerList = Array.isArray(handler) ? handler : [handler];
@@ -15741,86 +16037,37 @@ function createAgent(rawOptions) {
15741
16037
  }
15742
16038
  const thinking = options.thinking ?? "off";
15743
16039
  const model = targets[0].model;
15744
- if (runProvider.meta?.clearsContextServerSide) resolvedBehavior.dedupReads = false;
15745
- const { maxConcurrentTools, toolBatchExecutor, toolBatchTimeoutMs, shouldRethrowToolError, maxTurns, maxTurnsWarning, maxTurnsWarningMessage, maxCostUsd, maxTotalTokens, maxWallMs, providerStreamStartTimeoutMs, providerStreamTimeoutMs, providerStreamIdleTimeoutMs, maxTokens, modelCatalog, retry, thinkingBudget, taskBudget, effortHint, thinkingOverrunPolicy, modelOptions: behaviorModelOptions, schema, cache, cacheTtl, toolOutputBudget, toolOutputBudgetExcludeTools, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, writeFileExistingPolicy, textToolCallRescue, unknownToolRecovery, progressNudges, dedupTools, toolBudgets, repeatGuard, elideStaleReads, mediaKeepTurns, autoCompact, toolDisclosure, mcpToolNameSeparator, toolSearch, surfaceMcpInstructions, persistThreshold, persistExcludeTools, persistDir, persistMaxBytes, strictToolPairing, maxPairingRepairsPerTurn, maxConsecutivePauseTurns, maxConsecutiveRefusalResumes, resumeOnMaxTokens, maxConsecutiveLengthResumes, persistTurns } = resolvedBehavior;
16040
+ if (runProvider.meta.clearsContextServerSide) resolvedBehavior.dedupReads = false;
16041
+ Object.freeze(resolvedBehavior);
16042
+ const { thinkingOverrunPolicy, thinkingBudget, modelOptions: behaviorModelOptions, compactKeepTurns, writeFileExistingPolicy, textToolCallRescue, unknownToolRecovery, progressNudges, dedupTools, toolBudgets, repeatGuard, autoCompact, toolDisclosure, mcpToolNameSeparator, toolSearch, surfaceMcpInstructions, persistTurns } = resolvedBehavior;
15746
16043
  const modelOptions = options.modelOptions ?? behaviorModelOptions;
15747
16044
  let system = options.system || agentSystem || "You are a helpful assistant.";
15748
16045
  const baseSystemForBreakdown = renderSystemForWire(system);
15749
16046
  if (skillsCatalog) system = appendStaticSection(system, skillsCatalog);
15750
- const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? {
15751
- ...sourceTools,
15752
- ...mcpConnection.tools
15753
- } : sourceTools;
15754
- const mcpToolNames = options.tools === void 0 && mcpConnection ? new Set(Object.keys(mcpConnection.tools)) : /* @__PURE__ */ new Set();
15755
- const generatedMcpToolAliases = buildMcpToolNameSeparatorAliases(mcpToolNames, mcpServers, mcpToolNameSeparator);
15756
- const baseToolAliases = generatedMcpToolAliases ? {
15757
- ...generatedMcpToolAliases,
15758
- ...toolAliases ?? {}
15759
- } : toolAliases;
15760
- const sanitizedWireAliases = buildSanitizedWireAliases(mcpToolNames, baseToolAliases);
15761
- const effectiveToolAliases = sanitizedWireAliases ? {
15762
- ...baseToolAliases ?? {},
15763
- ...sanitizedWireAliases
15764
- } : baseToolAliases;
15765
- const transformedToolCatalog = applyToolCatalogTransforms(options.tools === void 0 && !!resolvedSkills && resolvedSkills.length > 0 && skillsConfig?.tool !== false ? {
15766
- skills_use: createSkillsUseTool({
15767
- catalog: resolvedSkills,
15768
- state: skillActivationState,
15769
- hooks,
15770
- allowShellInterpolation: skillsConfig?.allowShellInterpolation,
15771
- approveShellInterpolation: skillsConfig?.approveShellInterpolation
15772
- }),
15773
- skills_read: createSkillsReadTool({
15774
- catalog: resolvedSkills,
15775
- state: skillActivationState
15776
- }),
15777
- skills_run_script: createSkillsRunScriptTool({
15778
- catalog: resolvedSkills,
15779
- state: skillActivationState,
15780
- scriptTimeoutMs: skillsConfig?.scriptTimeoutMs
15781
- }),
15782
- ...runBaseTools
15783
- } : runBaseTools, toolCatalogTransforms, "run", mcpToolNames);
15784
- const toolsPreSearch = {};
15785
- for (const tool of Object.values(transformedToolCatalog)) toolsPreSearch[tool.spec.name] = tool;
15786
- if (toolsPreSearch.shell === shell) {
15787
- const detachedTasks = resolveDetachedTasksCapability(executionContext);
15788
- const bgDisabled = resolvedBehavior?.disableBackgroundTasks === true || resolvedBehavior?.disableBackgroundTasks === "non-durable" && detachedTasks !== "durable";
15789
- toolsPreSearch.shell = createShellTool({
15790
- allowBackground: typeof resolvedBehavior?.tasksDir === "string" && resolvedBehavior.tasksDir.length > 0 && !bgDisabled && detachedTasks !== "none",
15791
- registeredCanonicals: new Set(Object.keys(toolsPreSearch)),
15792
- ...effectiveToolAliases ? { toolAliases: effectiveToolAliases } : {}
15793
- });
15794
- }
15795
- const disclosure = partitionToolDisclosure(toolsPreSearch, mcpToolNames, mcpServers, toolDisclosure, effectiveToolAliases);
15796
- const unlocked = new Set(disclosure.eagerCanonicalNames);
15797
- const initialUnlocked = new Set(disclosure.eagerCanonicalNames);
15798
- const dynamicUnlockOrder = [];
15799
- const dynamicUnlockSeen = /* @__PURE__ */ new Set();
15800
- function recordDynamicUnlock(canonical) {
15801
- if (initialUnlocked.has(canonical)) return;
15802
- if (dynamicUnlockSeen.has(canonical)) return;
15803
- dynamicUnlockSeen.add(canonical);
15804
- unlocked.add(canonical);
15805
- dynamicUnlockOrder.push(canonical);
15806
- }
15807
- const hostDefinedToolSearch = !!toolsPreSearch.tool_search;
15808
- const shouldInjectToolSearch = options.tools === void 0 && (toolDisclosure === "lazy" || disclosure.lazyEntries.length > 0) && toolSearch?.tool !== false && !hostDefinedToolSearch;
15809
- let tools = toolsPreSearch;
15810
- if (shouldInjectToolSearch) {
15811
- const toolSearchTool = createToolSearchTool({
15812
- catalog: disclosure.lazyEntries,
15813
- unlocked,
15814
- addUnlock: recordDynamicUnlock,
15815
- ...toolSearch?.limit !== void 0 ? { defaultLimit: toolSearch.limit } : {}
15816
- });
15817
- tools = {
15818
- ...toolsPreSearch,
15819
- [toolSearchTool.spec.name]: toolSearchTool
15820
- };
15821
- unlocked.add(toolSearchTool.spec.name);
15822
- initialUnlocked.add(toolSearchTool.spec.name);
15823
- }
16047
+ const { tools: toolsPreSearch, mcpToolNames, toolAliases: effectiveToolAliases } = buildRunToolRegistry({
16048
+ runOverrideTools: options.tools,
16049
+ sourceTools,
16050
+ mcpConnection,
16051
+ mcpServers,
16052
+ mcpToolNameSeparator,
16053
+ toolAliases,
16054
+ resolvedSkills,
16055
+ skillsConfig,
16056
+ skillActivationState,
16057
+ hooks,
16058
+ toolCatalogTransforms,
16059
+ executionContext,
16060
+ behavior: resolvedBehavior
16061
+ });
16062
+ const { disclosure, unlocked, initialUnlocked, dynamicUnlockOrder, recordUnlock: recordDynamicUnlock, tools, injectedToolSearch: shouldInjectToolSearch, hostDefinedToolSearch } = buildRunDisclosureState({
16063
+ tools: toolsPreSearch,
16064
+ mcpToolNames,
16065
+ mcpServers,
16066
+ toolDisclosure,
16067
+ toolSearch,
16068
+ toolAliases: effectiveToolAliases,
16069
+ hasRunToolOverride: options.tools !== void 0
16070
+ });
15824
16071
  const discoveryToolName = shouldInjectToolSearch ? "tool_search" : hostDefinedToolSearch ? effectiveToolAliases?.tool_search ?? "tool_search" : null;
15825
16072
  let searchableCatalogText;
15826
16073
  if (disclosure.lazyEntries.length > 0) {
@@ -15905,76 +16152,31 @@ function createAgent(rawOptions) {
15905
16152
  });
15906
16153
  }
15907
16154
  const turns = [];
15908
- const isResume = session && session.turns.length > 0 && (session.runs.length > 0 || !options.prompt) && !options.parentRunId && !options.resumeRunId;
15909
- let seededTurns;
15910
- if (options.resumeRunId && session) {
15911
- const chain = resumeChainRunIds(session.runs, options.resumeRunId);
15912
- seededTurns = filterUnresolvedToolUses(session.turns.filter((t) => t.runId !== void 0 && chain.has(t.runId)));
15913
- } else if (isResume) {
15914
- const childRunIds = childRunIdSet(session);
15915
- const resumed = childRunIds.size === 0 ? session.turns : session.turns.filter((t) => !t.runId || !childRunIds.has(t.runId));
15916
- seededTurns = resumeFilteredTurns && resumed === session.turns ? resumeFilteredTurns : filterUnresolvedToolUses(resumed);
15917
- }
16155
+ const seededTurns = selectSeedTurns({
16156
+ session,
16157
+ options,
16158
+ resumeFilteredTurns
16159
+ });
15918
16160
  if (seededTurns) {
15919
16161
  turns.push(...seededTurns);
15920
- if (shouldInjectToolSearch && disclosure.lazyEntries.length > 0) {
15921
- const resolvedCallIds = /* @__PURE__ */ new Set();
15922
- for (const turn of seededTurns) for (const block of turn.content) if (block.type === "tool_result" && !block.isError) resolvedCallIds.add(block.callId);
15923
- for (const turn of seededTurns) for (const block of turn.content) {
15924
- if (block.type !== "tool_call") continue;
15925
- if (block.name !== "tool_search") continue;
15926
- if (!resolvedCallIds.has(block.id)) continue;
15927
- applyToolSearchToUnlocked(disclosure.lazyEntries, block.input, unlocked, toolSearch?.limit, recordDynamicUnlock);
15928
- }
15929
- }
16162
+ if (shouldInjectToolSearch && disclosure.lazyEntries.length > 0) replayToolSearchUnlocks(seededTurns, disclosure.lazyEntries, unlocked, toolSearch?.limit, recordDynamicUnlock);
15930
16163
  }
15931
16164
  const runTurnStart = turns.length;
15932
16165
  if (options.system) await hooks.callHook("system:before", { system: options.system });
15933
- const reconciled = new Map(pendingTaskNotifications);
15934
- pendingTaskNotifications.clear();
15935
- if (executionContext.listBackground && executionHandle && typeof resolvedBehavior?.tasksDir === "string" && resolvedBehavior.tasksDir.length > 0) try {
15936
- const listBackground = executionContext.listBackground;
15937
- const handle = executionHandle;
15938
- const entries = options.journalReconcile ? await options.journalReconcile(() => listBackground(handle)) : await listBackground(handle);
15939
- const acks = entries.some((e) => e.status !== "running") ? collectTaskAcksFromTurns(session ? session.turns : turns) : void 0;
15940
- for (const entry of entries) {
15941
- if (entry.status === "running" || notifiedTaskIds.has(entry.taskId) || reconciled.has(entry.taskId)) continue;
15942
- if (acks) {
15943
- if (acks.taskIds.has(entry.taskId)) continue;
15944
- const readAt = acks.outputPaths.get(resolve(entry.outputPath));
15945
- if (readAt !== void 0 && entry.endedAt !== void 0 && readAt >= entry.endedAt + READ_ACK_CLOCK_SKEW_MARGIN_MS) continue;
15946
- }
15947
- reconciled.set(entry.taskId, {
15948
- taskId: entry.taskId,
15949
- status: entry.status,
15950
- exitCode: entry.exitCode ?? 0,
15951
- ...entry.signal ? { signal: entry.signal } : {},
15952
- outputPath: entry.outputPath,
15953
- durationMs: Math.max(0, (entry.endedAt ?? entry.startedAt) - entry.startedAt),
15954
- command: entry.command
15955
- });
15956
- }
15957
- } catch (err) {
15958
- if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/agent] background reconcile failed: ${err instanceof Error ? err.message : String(err)}\n`);
15959
- }
15960
- const reconcileCtx = { exits: [...reconciled.values()] };
15961
- if (reconcileCtx.exits.length > 0) await hooks.callHook("background:reconcile", reconcileCtx);
15962
- for (const taskId of reconciled.keys()) {
15963
- notifiedTaskIds.add(taskId);
15964
- pendingStallNotifications.delete(taskId);
15965
- }
15966
- const drainedNotifications = [];
15967
- for (const notif of reconcileCtx.exits) drainedNotifications.push({
15968
- type: "text",
15969
- text: renderTaskNotificationXml(notif)
16166
+ const drainedNotifications = await drainTaskNotifications({
16167
+ state: {
16168
+ pending: pendingTaskNotifications,
16169
+ pendingStalls: pendingStallNotifications,
16170
+ notified: notifiedTaskIds
16171
+ },
16172
+ executionContext,
16173
+ executionHandle,
16174
+ behavior: resolvedBehavior,
16175
+ journalReconcile: options.journalReconcile,
16176
+ session,
16177
+ turns,
16178
+ hooks
15970
16179
  });
15971
- if (pendingStallNotifications.size > 0) {
15972
- for (const stall of pendingStallNotifications.values()) drainedNotifications.push({
15973
- type: "text",
15974
- text: renderTaskStallXml(stall)
15975
- });
15976
- pendingStallNotifications.clear();
15977
- }
15978
16180
  let lastPersistedTurnCount = turns.length;
15979
16181
  const promptParts = canonicalizePrompt(options.prompt);
15980
16182
  if (promptParts || drainedNotifications.length > 0) {
@@ -16021,8 +16223,10 @@ function createAgent(rawOptions) {
16021
16223
  });
16022
16224
  }
16023
16225
  const perTurnSync = session && persistTurns !== false;
16024
- const unregisterTurnSync = perTurnSync ? hooks.hook("turn:after", persistPendingTurns) : void 0;
16025
- const unregisterToolResultsSync = perTurnSync ? hooks.hook("tool-results:after", persistPendingTurns) : void 0;
16226
+ if (perTurnSync) {
16227
+ perRunUnregisters.push(hooks.hook("turn:after", persistPendingTurns));
16228
+ perRunUnregisters.push(hooks.hook("tool-results:after", persistPendingTurns));
16229
+ }
16026
16230
  async function flushTurns(opts = {}) {
16027
16231
  if (!session) return;
16028
16232
  if (opts.failureFallback) {
@@ -16067,7 +16271,7 @@ function createAgent(rawOptions) {
16067
16271
  const run = session?.runs.find((r) => r.id === runId);
16068
16272
  if (run) run.endedAt = await clock.now();
16069
16273
  }
16070
- const uninstallAllowedToolsGate = installAllowedToolsGate(hooks, skillActivationState);
16274
+ perRunUnregisters.push(installAllowedToolsGate(hooks, skillActivationState));
16071
16275
  const getExecutionHandle = () => executionHandle ?? void 0;
16072
16276
  perRunUnregisters.push(installWriteFileExistingPolicy(hooks, writeFileExistingPolicy, {
16073
16277
  execution: executionContext,
@@ -16078,10 +16282,10 @@ function createAgent(rawOptions) {
16078
16282
  advertisedToolNames,
16079
16283
  enqueueNudge: (message) => nonInterruptingNudgeQueue.push(message)
16080
16284
  }));
16081
- const uninstallToolBudgets = installToolBudgetsGate(hooks, () => toolBudgets, (msg) => nonInterruptingNudgeQueue.push(msg));
16082
- const uninstallRepeatGuard = installRepeatGuard(hooks, () => repeatGuard, () => abortController?.abort());
16083
- const uninstallDedupTools = installDedupToolsGate(hooks, () => dedupTools, () => session ?? void 0);
16084
- const uninstallLazyDisclosureHydration = installLazyDisclosureHydrationGate(hooks, {
16285
+ perRunUnregisters.push(installToolBudgetsGate(hooks, () => toolBudgets, (msg) => nonInterruptingNudgeQueue.push(msg)));
16286
+ perRunUnregisters.push(installRepeatGuard(hooks, () => repeatGuard, () => abortController?.abort()));
16287
+ perRunUnregisters.push(installDedupToolsGate(hooks, () => dedupTools, () => session ?? void 0));
16288
+ perRunUnregisters.push(installLazyDisclosureHydrationGate(hooks, {
16085
16289
  enabled: shouldInjectToolSearch && disclosure.lazyEntries.length > 0,
16086
16290
  session: session ?? void 0,
16087
16291
  lazyCanonicalNames: disclosure.lazyCanonicalNames,
@@ -16089,20 +16293,20 @@ function createAgent(rawOptions) {
16089
16293
  unlocked,
16090
16294
  recordUnlock: recordDynamicUnlock,
16091
16295
  defaultLimit: toolSearch?.limit
16092
- });
16093
- const uninstallLazyDisclosureGate = installLazyDisclosureGate(hooks, disclosure.lazyCanonicalNames, unlocked, discoveryToolName);
16094
- const uninstallReadBeforeEditHydration = installReadBeforeEditHydrationGate(hooks, resolvedBehavior.requireReadBeforeEdit === true, {
16296
+ }));
16297
+ perRunUnregisters.push(installLazyDisclosureGate(hooks, disclosure.lazyCanonicalNames, unlocked, discoveryToolName));
16298
+ perRunUnregisters.push(installReadBeforeEditHydrationGate(hooks, resolvedBehavior.requireReadBeforeEdit === true, {
16095
16299
  session: session ?? void 0,
16096
16300
  getHandle: () => executionHandle,
16097
16301
  execution: executionContext,
16098
16302
  aliasMaps,
16099
16303
  defaultLineNumbers: resolvedBehavior.readLineNumbers ?? true
16100
- });
16101
- const uninstallSkillsActivationHydration = installSkillsActivationHydrationGate(hooks, {
16304
+ }));
16305
+ perRunUnregisters.push(installSkillsActivationHydrationGate(hooks, {
16102
16306
  session: session ?? void 0,
16103
16307
  getResolvedSkills: () => resolvedSkills,
16104
16308
  state: skillActivationState
16105
- });
16309
+ }));
16106
16310
  perRunUnregisters.push(installThinkingOverrunPolicy(hooks, thinkingOverrunPolicy, {
16107
16311
  baseBudget: thinkingBudget,
16108
16312
  enqueueNudge: (message) => nonInterruptingNudgeQueue.push(message)
@@ -16121,25 +16325,100 @@ function createAgent(rawOptions) {
16121
16325
  turnUsage: []
16122
16326
  };
16123
16327
  const wallStartMs = Date.now();
16124
- const providerContextWindow = typeof runProvider.meta.contextWindow === "number" ? runProvider.meta.contextWindow : void 0;
16328
+ const providerContextWindow = runProvider.meta.contextWindow;
16125
16329
  const resolvedAutoCompact = resolveAutoCompact(autoCompact, {
16126
16330
  ...providerContextWindow !== void 0 ? { fallbackContextWindow: providerContextWindow } : {},
16127
16331
  ...compactKeepTurns !== void 0 ? { compactKeepTurns } : {}
16128
16332
  });
16333
+ /**
16334
+ * Loop-view stats synthesized from the live usage mirror.
16335
+ *
16336
+ * Used by every exit that did NOT get a clean `AgentStats` back from
16337
+ * `runLoop` (mid-stream abort, budget breach, hard failure): the mirror
16338
+ * carries the usage of every turn that completed before the throw, so the
16339
+ * persisted record and `agent:done` report real spend instead of zeros.
16340
+ * Wall time comes from `wallStartMs` (a real clock) rather than the
16341
+ * possibly-journaled `runStartedAt`, matching `runLoop`'s own accounting.
16342
+ */
16343
+ function mirroredLoopStats() {
16344
+ return {
16345
+ totalIn: usageMirror.totalIn,
16346
+ totalOut: usageMirror.totalOut,
16347
+ totalCacheRead: usageMirror.totalCacheRead,
16348
+ totalCacheCreation: usageMirror.totalCacheCreation,
16349
+ turns: usageMirror.turns,
16350
+ elapsed: Date.now() - wallStartMs,
16351
+ turnUsage: usageMirror.turnUsage,
16352
+ ...usageMirror.cost > 0 ? { cost: usageMirror.cost } : {}
16353
+ };
16354
+ }
16355
+ /**
16356
+ * The single exit path for a run. Every terminal branch — completed,
16357
+ * clean-break abort, mid-stream abort, budget breach, hard error — routes
16358
+ * through here so all five report identically: children folded into the
16359
+ * returned totals, session ledger written from the loop-only view, run row
16360
+ * stamped, `session:end` fired, then `agent:done` with the final stats.
16361
+ *
16362
+ * Callers that must propagate an error still `throw` after awaiting this;
16363
+ * the finalization itself is outcome-independent.
16364
+ */
16365
+ async function finishRun(disposition) {
16366
+ const { kind, loopStats, parentCost } = disposition;
16367
+ let childrenIn = 0;
16368
+ let childrenOut = 0;
16369
+ let childrenCost = 0;
16370
+ let childrenCacheRead = 0;
16371
+ let childrenCacheCreation = 0;
16372
+ for (const c of childrenStats) {
16373
+ childrenIn += c.stats.totalIn;
16374
+ childrenOut += c.stats.totalOut;
16375
+ childrenCost += c.stats.cost ?? 0;
16376
+ childrenCacheRead += c.stats.totalCacheRead;
16377
+ childrenCacheCreation += c.stats.totalCacheCreation;
16378
+ }
16379
+ const cumulativeCost = parentCost + childrenCost;
16380
+ const finalStats = {
16381
+ ...loopStats,
16382
+ totalIn: loopStats.totalIn + childrenIn,
16383
+ totalOut: loopStats.totalOut + childrenOut,
16384
+ totalCacheRead: loopStats.totalCacheRead + childrenCacheRead,
16385
+ totalCacheCreation: loopStats.totalCacheCreation + childrenCacheCreation,
16386
+ ...cumulativeCost > 0 ? { cost: cumulativeCost } : {},
16387
+ children: childrenStats.length > 0 ? childrenStats : void 0
16388
+ };
16389
+ if (session) {
16390
+ const ledger = {
16391
+ turns: loopStats.turns,
16392
+ tokensIn: loopStats.totalIn,
16393
+ tokensOut: loopStats.totalOut,
16394
+ turnUsage: loopStats.turnUsage,
16395
+ ...parentCost > 0 ? { cost: parentCost } : {}
16396
+ };
16397
+ if (disposition.kind === "completed") session.completeRun(runId, ledger);
16398
+ else if (disposition.kind === "aborted") session.abortRun(runId, ledger);
16399
+ else session.errorRun(runId, disposition.failure, ledger);
16400
+ }
16401
+ await stampSessionRunEnd();
16402
+ await finalizeSession(kind);
16403
+ await settleWithinTimeout("hook agent:done", hooks.callHook("agent:done", finalStats), hookTimeoutMs);
16404
+ return finalStats;
16405
+ }
16129
16406
  try {
16130
16407
  await toolStateController.hydrate();
16131
16408
  const stats = await runLoop({
16132
16409
  provider: runProvider,
16133
16410
  targets,
16134
16411
  hooks,
16135
- agentName,
16136
- agentSystem,
16137
- agentTools: sourceTools,
16138
- agentToolAliases: effectiveToolAliases,
16139
- agentMcpServers: allMcpServers.length > 0 ? allMcpServers : void 0,
16140
- agentMcpConnection: () => mcpConnection,
16141
- agentSkills,
16142
- agentBehavior: resolvedBehavior,
16412
+ behavior: resolvedBehavior,
16413
+ inheritance: {
16414
+ ...agentName !== void 0 ? { name: agentName } : {},
16415
+ ...agentSystem !== void 0 ? { system: agentSystem } : {},
16416
+ tools: sourceTools,
16417
+ ...effectiveToolAliases !== void 0 ? { toolAliases: effectiveToolAliases } : {},
16418
+ ...allMcpServers.length > 0 ? { mcpServers: allMcpServers } : {},
16419
+ mcpConnection: () => mcpConnection,
16420
+ ...agentSkills !== void 0 ? { skills: agentSkills } : {}
16421
+ },
16143
16422
  tools,
16144
16423
  formattedTools,
16145
16424
  rebuildFormattedTools: buildFormattedTools,
@@ -16147,10 +16426,6 @@ function createAgent(rawOptions) {
16147
16426
  model,
16148
16427
  system,
16149
16428
  thinking,
16150
- ...maxConcurrentTools !== void 0 ? { maxConcurrentTools } : {},
16151
- ...toolBatchExecutor !== void 0 ? { toolBatchExecutor } : {},
16152
- ...toolBatchTimeoutMs !== void 0 ? { toolBatchTimeoutMs } : {},
16153
- ...shouldRethrowToolError !== void 0 ? { shouldRethrowToolError } : {},
16154
16429
  signal: abortController.signal,
16155
16430
  execution: executionContext,
16156
16431
  handle: executionHandle,
@@ -16161,53 +16436,16 @@ function createAgent(rawOptions) {
16161
16436
  runId,
16162
16437
  generateTurnId: mintTurnId,
16163
16438
  clock,
16164
- maxTurns,
16165
- ...maxTurnsWarning !== void 0 ? { maxTurnsWarning } : {},
16166
- ...maxTurnsWarningMessage !== void 0 ? { maxTurnsWarningMessage } : {},
16167
- ...maxCostUsd !== void 0 ? { maxCostUsd } : {},
16168
- ...maxTotalTokens !== void 0 ? { maxTotalTokens } : {},
16169
- ...maxWallMs !== void 0 ? { maxWallMs } : {},
16170
- ...providerStreamStartTimeoutMs !== void 0 ? { providerStreamStartTimeoutMs } : {},
16171
- ...providerStreamTimeoutMs !== void 0 ? { providerStreamTimeoutMs } : {},
16172
- ...providerStreamIdleTimeoutMs !== void 0 ? { providerStreamIdleTimeoutMs } : {},
16173
- ...resolvedBehavior.hookTimeoutMs !== void 0 ? { hookTimeoutMs: resolvedBehavior.hookTimeoutMs } : {},
16174
- ...retry !== void 0 ? { retry } : {},
16175
- maxTokens,
16176
- ...modelCatalog !== void 0 ? { modelCatalog } : {},
16439
+ ...modelOptions ? { modelOptions } : {},
16440
+ ...resolvedAutoCompact ? { autoCompact: resolvedAutoCompact } : {},
16177
16441
  ...session ? { session } : {},
16178
16442
  ...agentReadState ? { readState: agentReadState } : {},
16179
16443
  toolState: toolStateController,
16180
16444
  ...options.parentRunId ? { parentRunId: options.parentRunId } : {},
16181
16445
  ...options.parentCallId ? { parentCallId: options.parentCallId } : {},
16182
16446
  depth: runDepth,
16183
- thinkingBudget,
16184
- ...taskBudget !== void 0 ? { taskBudget } : {},
16185
- ...effortHint !== void 0 ? { effortHint } : {},
16186
- ...modelOptions ? { modelOptions } : {},
16187
- schema,
16188
- cache,
16189
- ...cacheTtl !== void 0 ? { cacheTtl } : {},
16190
- toolOutputBudget,
16191
- ...toolOutputBudgetExcludeTools !== void 0 ? { toolOutputBudgetExcludeTools } : {},
16192
- compactStrategy,
16193
- compactThreshold,
16194
- compactKeepTurns,
16195
- ...elideStaleReads !== void 0 ? { elideStaleReads } : {},
16196
- ...mediaKeepTurns !== void 0 ? { mediaKeepTurns } : {},
16197
- ...resolvedAutoCompact ? { autoCompact: resolvedAutoCompact } : {},
16198
16447
  ...perTurnSync ? { persistTurns: persistPendingTurns } : {},
16199
16448
  getActiveSkills: () => skillActivationState.active(),
16200
- ...thinkingDecay !== void 0 ? { thinkingDecay } : {},
16201
- ...persistThreshold !== void 0 ? { persistThreshold } : {},
16202
- ...persistExcludeTools !== void 0 ? { persistExcludeTools } : {},
16203
- ...persistDir !== void 0 ? { persistDir } : {},
16204
- ...persistMaxBytes !== void 0 ? { persistMaxBytes } : {},
16205
- ...strictToolPairing ? { strictToolPairing: true } : {},
16206
- ...maxPairingRepairsPerTurn !== void 0 ? { maxPairingRepairsPerTurn } : {},
16207
- ...maxConsecutivePauseTurns !== void 0 ? { maxConsecutivePauseTurns } : {},
16208
- ...maxConsecutiveRefusalResumes !== void 0 ? { maxConsecutiveRefusalResumes } : {},
16209
- ...resumeOnMaxTokens !== void 0 ? { resumeOnMaxTokens } : {},
16210
- ...maxConsecutiveLengthResumes !== void 0 ? { maxConsecutiveLengthResumes } : {},
16211
16449
  providerName: runProvider.name,
16212
16450
  runStartMs,
16213
16451
  runToolCounts: {},
@@ -16215,53 +16453,12 @@ function createAgent(rawOptions) {
16215
16453
  usageMirror
16216
16454
  });
16217
16455
  const parentTurnCost = stats.turnUsage?.reduce((sum, t) => sum + (t.cost ?? 0), 0) ?? 0;
16218
- let childrenIn = 0;
16219
- let childrenOut = 0;
16220
- let childrenCost = 0;
16221
- let childrenCacheRead = 0;
16222
- let childrenCacheCreation = 0;
16223
- for (const c of childrenStats) {
16224
- childrenIn += c.stats.totalIn;
16225
- childrenOut += c.stats.totalOut;
16226
- childrenCost += c.stats.cost ?? 0;
16227
- childrenCacheRead += c.stats.totalCacheRead;
16228
- childrenCacheCreation += c.stats.totalCacheCreation;
16229
- }
16230
- const cumulativeCost = parentTurnCost + childrenCost;
16231
- const finalStats = {
16232
- ...stats,
16233
- totalIn: stats.totalIn + childrenIn,
16234
- totalOut: stats.totalOut + childrenOut,
16235
- totalCacheRead: stats.totalCacheRead + childrenCacheRead,
16236
- totalCacheCreation: stats.totalCacheCreation + childrenCacheCreation,
16237
- ...cumulativeCost > 0 ? { cost: cumulativeCost } : {},
16238
- children: childrenStats.length > 0 ? childrenStats : void 0
16239
- };
16240
16456
  await flushTurns();
16241
- if (abortController.signal.aborted) {
16242
- session?.abortRun(runId, {
16243
- turns: stats.turns,
16244
- tokensIn: stats.totalIn,
16245
- tokensOut: stats.totalOut,
16246
- turnUsage: stats.turnUsage,
16247
- cost: parentTurnCost > 0 ? parentTurnCost : void 0
16248
- });
16249
- await stampSessionRunEnd();
16250
- await finalizeSession("aborted");
16251
- await settleWithinTimeout("hook agent:done", hooks.callHook("agent:done", finalStats), hookTimeoutMs);
16252
- return finalStats;
16253
- }
16254
- session?.completeRun(runId, {
16255
- turns: stats.turns,
16256
- tokensIn: stats.totalIn,
16257
- tokensOut: stats.totalOut,
16258
- turnUsage: stats.turnUsage,
16259
- cost: parentTurnCost > 0 ? parentTurnCost : void 0
16457
+ return await finishRun({
16458
+ kind: abortController.signal.aborted ? "aborted" : "completed",
16459
+ loopStats: stats,
16460
+ parentCost: parentTurnCost
16260
16461
  });
16261
- await stampSessionRunEnd();
16262
- await finalizeSession("completed");
16263
- await settleWithinTimeout("hook agent:done", hooks.callHook("agent:done", finalStats), hookTimeoutMs);
16264
- return finalStats;
16265
16462
  } catch (err) {
16266
16463
  await flushTurns({ failureFallback: true });
16267
16464
  if (abortController.signal.aborted) {
@@ -16270,84 +16467,39 @@ function createAgent(rawOptions) {
16270
16467
  ...runId ? { runId } : {},
16271
16468
  ...typeof abortReason === "string" && abortReason.length > 0 ? { reason: abortReason } : {}
16272
16469
  });
16273
- session?.abortRun(runId, {
16274
- turns: usageMirror.turns,
16275
- tokensIn: usageMirror.totalIn,
16276
- tokensOut: usageMirror.totalOut,
16277
- turnUsage: usageMirror.turnUsage,
16278
- cost: usageMirror.cost > 0 ? usageMirror.cost : void 0
16470
+ return await finishRun({
16471
+ kind: "aborted",
16472
+ loopStats: mirroredLoopStats(),
16473
+ parentCost: usageMirror.cost
16279
16474
  });
16280
- await stampSessionRunEnd();
16281
- await finalizeSession("aborted");
16282
- const stats = {
16283
- totalIn: usageMirror.totalIn,
16284
- totalOut: usageMirror.totalOut,
16285
- totalCacheRead: usageMirror.totalCacheRead,
16286
- totalCacheCreation: usageMirror.totalCacheCreation,
16287
- turns: usageMirror.turns,
16288
- elapsed: Date.now() - wallStartMs,
16289
- turnUsage: usageMirror.turnUsage,
16290
- ...usageMirror.cost > 0 ? { cost: usageMirror.cost } : {}
16291
- };
16292
- await settleWithinTimeout("hook agent:done", hooks.callHook("agent:done", stats), hookTimeoutMs);
16293
- return stats;
16294
16475
  }
16295
16476
  if (err instanceof AgentBudgetExceededError) {
16296
- session?.abortRun(runId, {
16297
- turns: usageMirror.turns,
16298
- tokensIn: usageMirror.totalIn,
16299
- tokensOut: usageMirror.totalOut,
16300
- turnUsage: usageMirror.turnUsage,
16301
- cost: usageMirror.cost > 0 ? usageMirror.cost : void 0
16477
+ await finishRun({
16478
+ kind: "aborted",
16479
+ loopStats: mirroredLoopStats(),
16480
+ parentCost: usageMirror.cost
16302
16481
  });
16303
- await stampSessionRunEnd();
16304
- await finalizeSession("aborted");
16305
- await settleWithinTimeout("hook agent:done", hooks.callHook("agent:done", {
16306
- totalIn: usageMirror.totalIn,
16307
- totalOut: usageMirror.totalOut,
16308
- totalCacheRead: usageMirror.totalCacheRead,
16309
- totalCacheCreation: usageMirror.totalCacheCreation,
16310
- turns: usageMirror.turns,
16311
- elapsed: Date.now() - wallStartMs,
16312
- turnUsage: usageMirror.turnUsage,
16313
- ...usageMirror.cost > 0 ? { cost: usageMirror.cost } : {}
16314
- }), hookTimeoutMs);
16315
16482
  throw err;
16316
16483
  }
16317
- session?.errorRun(runId, errorMessage(err));
16318
- await stampSessionRunEnd();
16319
- await finalizeSession("error");
16320
- await settleWithinTimeout("hook agent:done", hooks.callHook("agent:done", {
16321
- totalIn: usageMirror.totalIn,
16322
- totalOut: usageMirror.totalOut,
16323
- totalCacheRead: usageMirror.totalCacheRead,
16324
- totalCacheCreation: usageMirror.totalCacheCreation,
16325
- turns: usageMirror.turns,
16326
- elapsed: Date.now() - wallStartMs,
16327
- turnUsage: usageMirror.turnUsage,
16328
- ...usageMirror.cost > 0 ? { cost: usageMirror.cost } : {}
16329
- }), hookTimeoutMs);
16484
+ await finishRun({
16485
+ kind: "error",
16486
+ loopStats: mirroredLoopStats(),
16487
+ parentCost: usageMirror.cost,
16488
+ failure: errorMessage(err)
16489
+ });
16330
16490
  throw err;
16331
16491
  } finally {
16332
16492
  await deactivateAllSkills();
16333
- uninstallAllowedToolsGate();
16334
- uninstallDedupTools();
16335
- uninstallRepeatGuard();
16336
- uninstallToolBudgets();
16337
- uninstallLazyDisclosureHydration();
16338
- uninstallLazyDisclosureGate();
16339
- uninstallReadBeforeEditHydration();
16340
- uninstallSkillsActivationHydration();
16341
- unregisterSpawnHook();
16342
- unregisterTurnSync?.();
16343
- unregisterToolResultsSync?.();
16344
16493
  for (const unregister of perRunUnregisters) unregister();
16494
+ perRunUnregisters.length = 0;
16345
16495
  steeringQueue.length = 0;
16346
16496
  nonInterruptingNudgeQueue.length = 0;
16347
16497
  followUpQueue.length = 0;
16348
16498
  resetRunScope();
16349
16499
  }
16350
16500
  } finally {
16501
+ for (const unregister of perRunUnregisters) unregister();
16502
+ perRunUnregisters.length = 0;
16351
16503
  resetRunScope();
16352
16504
  if (externalSignal && externalAbortListener) externalSignal.removeEventListener("abort", externalAbortListener);
16353
16505
  }
@@ -16680,6 +16832,22 @@ function createAgent(rawOptions) {
16680
16832
  wireTools
16681
16833
  });
16682
16834
  }
16835
+ async function getContextSegments(opts) {
16836
+ if (destroyed) return null;
16837
+ const assembly = lastContextAssembly ?? await buildPreRunAssembly();
16838
+ if (!assembly) return null;
16839
+ const rulesBlock = opts?.rulesBlock ?? assembly.rulesBlock;
16840
+ return buildContextSegments({
16841
+ system: assembly.system,
16842
+ baseSystem: assembly.baseSystem,
16843
+ ...rulesBlock ? { rulesBlock } : {},
16844
+ ...opts?.rulesFiles?.length ? { rulesFiles: opts.rulesFiles } : {},
16845
+ ...opts?.systemPath !== void 0 ? { systemPath: opts.systemPath } : {},
16846
+ ...assembly.skillsCatalog ? { skillsCatalog: assembly.skillsCatalog } : {},
16847
+ ...assembly.mcpInstructions ? { mcpInstructions: assembly.mcpInstructions } : {},
16848
+ ...assembly.subagentDefs ? { subagentDefs: assembly.subagentDefs } : {}
16849
+ });
16850
+ }
16683
16851
  async function getContextBreakdown(opts) {
16684
16852
  if (destroyed) return null;
16685
16853
  const assembly = lastContextAssembly ?? await buildPreRunAssembly(opts?.model);
@@ -16793,10 +16961,11 @@ function createAgent(rawOptions) {
16793
16961
  },
16794
16962
  meta: Object.freeze({ ...provider.meta }),
16795
16963
  getContextBreakdown,
16964
+ getContextSegments,
16796
16965
  [Symbol.asyncDispose]: destroy
16797
16966
  };
16798
16967
  }
16799
16968
  //#endregion
16800
16969
  export { installExtensionFromSpec as $, groupBindings as $t, selectFilesFromSession as A, defaultRepeatGuardNormalize as An, createSkillsReadTool as At, buildUpToCompactPrompt as B, matchModelEntries as Bt, resolveLegacySessionDir as C, resolveExtensionConfigValue as Cn, waitTask as Ct, resolveTasksDir as D, OUTPUT_RESERVE_TOKENS as Dn, reserveChildLabel as Dt, resolveSessionDir as E, AUTO_COMPACT_MIN_GROWTH_FRACTION as En, isSpawnTool as Et, TRAILER as F, hashContent as Fn, readFile$1 as Ft, summaryToTurn as G, edit as Gt, anchorPreviewFor as H, createInteractionTool as Ht, buildCompactPrompt as I, hydrateReadStateFromSession as In, looksBinary as It, CompactPromptTooLongError as J, KEYBINDING_DEFS as Jt, truncateHeadForPtlRetry as K, validateToolArgs as Kt, buildFromCompactPrompt as L, hydrateReadStateFromSessionAsync as Ln, multiEdit as Lt, compactConversation as M, normalizeShellCommand as Mn, createShellTool as Mt, BASE_INSTRUCTIONS as N, stableStringify as Nn, shell as Nt, buildPostCompactAttachments as O, effectiveContextWindow as On, createSkillsUseTool as Ot, NO_TOOLS_PREAMBLE as P, getReadState as Pn, tailTruncate as Pt, fetchExtensionSource as Q, formatBindingForDisplay as Qt, buildFullCompactPrompt as R, readStateKey as Rn, createModelSearchTool as Rt, resolveLegacyPersistDir as S, resolveExtensionConfig as Sn, WAIT_TASK_TIMED_OUT_PREFIX as St, resolvePersistDir as T, toWireConfigSchema as Tn, createSpawnTool as Tt, sliceForCompaction as U, grep as Ut, ANCHOR_PREVIEW_MAX_CHARS as V, listFiles as Vt, stripImagesFromTurns as W, glob as Wt, createExtensionPromptBridge as X, KEYBINDING_KEY_COL_WIDTH as Xt, EMPTY_EXTENSION_REGISTRY as Y, KEYBINDING_DEF_BY_ACTION as Yt, ExtensionInstallError as Z, ensureKeybindingsFile as Zt, buildPersistedStub as _, defineExtension as _n, composePresets as _t, stagedToolState as a, readKeybindingsRaw as an, createExtensionHud as at, migrateSessionArtifactDirs as b, isValidExtensionConfigItem as bn, basic_default as bt, INTERRUPT_MESSAGE_FOR_TOOL_USE as c, registerExtensionHostModules as cn, buildExtensionRegistry as ct, TOOL_USE_SKIPPED_MESSAGE as d, discoverExtensions as dn, filterExtensionRegistry as dt, keybindingsPath as en, listExtensionCandidates as et, applyCompactSummaryCutoff as f, inlineLoadedExtension as fn, filterRegistryPlugins as ft, PERSISTENCE_PREVIEW_BYTES as g, createDebugLog as gn, createWorkspaceSurface as gt, PERSISTED_STUB_PREFIX as h, scanExtensionDirectories as hn, createExtensionHostRuntimeBridge as ht, isToolStateOps as i, readKeybindings as in, updateInstalledExtension as it, selectRecentFiles as j, defaultRepeatGuardTracked as jn, shellKill as jt, selectFilesFromReadState as k, shouldAutoCompact as kn, createSkillsRunScriptTool as kt, SHELL_CASCADE_CANCEL_MESSAGE as l, ENTRY_CANDIDATES as ln, composeExtensionPreset as lt, renderStaleReadNotice as m, parseExtensionScanPathEnv as mn, partitionExtensionCatalog as mt, DEFAULT_MAX_VALUE_BYTES as n, mergeKeybindings as nn, readExtensionInstallProvenance as nt, normalizeProviders as o, resolveExtensionKeybindings as on, discoverAndBuildExtensionRegistry as ot, collectNewlyStaleReadPaths as p, isExtensionDefinition as pn, mergeContributions as pt, CompactInvalidInputError as q, DEFAULT_KEYBINDINGS as qt, createToolStateController as r, parseBindingSpec as rn, removeInstalledExtension as rt, resolveModelTargets as s, stripJsonComments as sn, buildExtensionPreset as st, createAgent as t, matchesBinding as tn, parseExtensionInstallSource as tt, TOOL_USE_CANCELLED_MESSAGE as u, defaultExtensionScanPaths as un, disposeExtensionRegistry as ut, cleanupPersistedSession as v, clampExtensionConfigNumber as vn, definePreset as vt, resolveMcpWarningsDir as w, sanitizeExtensionConfigActionPatch as wn, createToolSearchTool as wt, rehomePersistedStubs as x, isValidExtensionConfigValue as xn, writeFile$1 as xt, maybePersistToolResult as y, coerceExtensionConfigInput as yn, basicTools as yt, buildTailCompactPrompt as z, resolveReadStateMap as zn, formatModelLine as zt };
16801
16970
 
16802
- //# sourceMappingURL=agent-BVNkC1h4.js.map
16971
+ //# sourceMappingURL=agent-8kfGIDb_.js.map