@wrongstack/core 0.306.4 → 0.307.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/chronicle/index.js +957 -953
  2. package/dist/chronicle/metrics-ingest.d.ts +55 -0
  3. package/dist/chronicle/metrics-schema.d.ts +86 -0
  4. package/dist/chronicle/metrics-store.d.ts +5 -120
  5. package/dist/chronicle/project-server.js +1083 -1084
  6. package/dist/chronicle/sqlite-journal-quota.d.ts +39 -0
  7. package/dist/chronicle/sqlite-journal-schema.d.ts +25 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +12 -177
  9. package/dist/coordination/agents/index.js +1738 -1690
  10. package/dist/coordination/agents/project-agent-capture.d.ts +48 -0
  11. package/dist/coordination/agents/project-agent-identity.d.ts +3 -129
  12. package/dist/coordination/agents/project-agent-roster.d.ts +17 -0
  13. package/dist/coordination/index.js +6011 -5894
  14. package/dist/coordination/mail-tools.d.ts +1 -1
  15. package/dist/coordination/mailbox-http-actor-query.d.ts +16 -0
  16. package/dist/coordination/mailbox-http-router.d.ts +0 -48
  17. package/dist/coordination/mailbox-http-sse.d.ts +12 -0
  18. package/dist/coordination/mailbox-message-types.d.ts +71 -0
  19. package/dist/coordination/mailbox-predicates.d.ts +13 -0
  20. package/dist/coordination/mailbox-project-server.js +1 -1
  21. package/dist/coordination/mailbox-session-sync.d.ts +8 -0
  22. package/dist/coordination/mailbox-types.d.ts +6 -793
  23. package/dist/core/agent-loop-context.d.ts +28 -0
  24. package/dist/core/agent-loop-detector.d.ts +25 -0
  25. package/dist/core/fallback-doctor.d.ts +72 -0
  26. package/dist/core/index.d.ts +1 -0
  27. package/dist/core/index.js +1159 -888
  28. package/dist/defaults/index.js +3579 -3545
  29. package/dist/execution/autonomy-brain-llm.d.ts +55 -0
  30. package/dist/execution/autonomy-brain.d.ts +3 -156
  31. package/dist/execution/compaction-budget.d.ts +38 -0
  32. package/dist/execution/compaction-core.d.ts +3 -170
  33. package/dist/execution/compaction-elision.d.ts +58 -0
  34. package/dist/execution/council-orchestrator-helpers.d.ts +39 -0
  35. package/dist/execution/council-orchestrator.d.ts +3 -42
  36. package/dist/execution/council-response-parser.d.ts +56 -0
  37. package/dist/execution/index.js +2415 -2443
  38. package/dist/execution/prompt-enhancer.js +11 -3
  39. package/dist/execution/tool-executor-guard.d.ts +18 -0
  40. package/dist/execution/tool-executor-runner.d.ts +12 -0
  41. package/dist/execution/tool-executor.d.ts +0 -75
  42. package/dist/goal/index.js +19 -6
  43. package/dist/hq/index.js +8 -2
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +7813 -7292
  46. package/dist/infrastructure/index.js +3 -1
  47. package/dist/kernel/events/agent-events.d.ts +2 -0
  48. package/dist/kernel/events/provider-events.d.ts +13 -0
  49. package/dist/plugin/index.js +2590 -2561
  50. package/dist/plugins/auto-review-config.d.ts +53 -0
  51. package/dist/plugins/auto-review-git.d.ts +19 -0
  52. package/dist/plugins/auto-review-plugin.d.ts +2 -140
  53. package/dist/plugins/review-finding-verification.d.ts +7 -0
  54. package/dist/prompts/index.d.ts +1 -0
  55. package/dist/prompts/prompt-journal.d.ts +94 -0
  56. package/dist/security/index.js +627 -652
  57. package/dist/security/permission-explain.d.ts +19 -0
  58. package/dist/security/permission-policy.d.ts +0 -101
  59. package/dist/security/yolo-risk.d.ts +5 -4
  60. package/dist/session-catalog/index.js +220 -212
  61. package/dist/session-catalog/project-server.js +227 -219
  62. package/dist/session-catalog/store-rebuild.d.ts +9 -0
  63. package/dist/session-catalog/store-schema.d.ts +48 -0
  64. package/dist/session-catalog/store.d.ts +0 -17
  65. package/dist/storage/director-state.d.ts +6 -0
  66. package/dist/storage/file-session-writer.d.ts +2 -58
  67. package/dist/storage/index.d.ts +2 -1
  68. package/dist/storage/index.js +2057 -1925
  69. package/dist/storage/orphan-lock-cleaner.d.ts +15 -0
  70. package/dist/storage/session-recovery.d.ts +9 -0
  71. package/dist/storage/session-store/rename-session.d.ts +16 -0
  72. package/dist/storage/session-store/resume-session.d.ts +26 -0
  73. package/dist/storage/session-store/session-store-clear.d.ts +11 -0
  74. package/dist/storage/session-store/session-store-index.d.ts +13 -0
  75. package/dist/storage/session-store.d.ts +1 -129
  76. package/dist/storage/session-summary-tracker.d.ts +39 -0
  77. package/dist/storage/session-write-buffer.d.ts +43 -0
  78. package/dist/storage/session-writer/session-writer-flush.d.ts +4 -0
  79. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +28 -0
  80. package/dist/tools/fallback-agent-model-assign-tool.d.ts +18 -0
  81. package/dist/tools/fallback-leader-model-set-tool.d.ts +18 -0
  82. package/dist/tools/fallback-manage-tools.d.ts +8 -45
  83. package/dist/tools/fallback-profile-manage-tool.d.ts +16 -0
  84. package/dist/tools/fallback-provider-key-set-tool.d.ts +17 -0
  85. package/dist/tools/fallback-provider-manage-tool.d.ts +26 -0
  86. package/dist/tools/index.js +5070 -5071
  87. package/dist/types/config/root.d.ts +7 -2
  88. package/dist/types/default-config.d.ts +1 -1
  89. package/dist/types/index.d.ts +54 -55
  90. package/dist/types/index.js +953 -932
  91. package/dist/types/runtime-capability-manifest.d.ts +5 -5
  92. package/dist/types/session-markers.d.ts +12 -0
  93. package/dist/types/session-markers.js +19 -0
  94. package/dist/worktree/worktree-manager.d.ts +1 -1
  95. package/instructions/coordination/subagent-baseline.md +2 -2
  96. package/instructions/llm/prompt-enhancer.md +2 -1
  97. package/instructions/sections/tool/common-patterns.md +21 -0
  98. package/instructions/sections/tool/delegation-compact.md +3 -1
  99. package/instructions/sections/tool/delegation-full.md +5 -1
  100. package/instructions/system-lite.md +28 -3
  101. package/instructions/system-pro.md +37 -8
  102. package/instructions/system.md +4 -2
  103. package/package.json +5 -5
@@ -1158,7 +1158,10 @@ var MAILBOX_TYPE_PROPERTIES = {
1158
1158
  }
1159
1159
  };
1160
1160
 
1161
- // src/coordination/mailbox-types.ts
1161
+ // src/coordination/mailbox-predicates.ts
1162
+ var SESSION_RECIPIENT_PREFIX = "@session:";
1163
+
1164
+ // src/coordination/mailbox-session-sync.ts
1162
1165
  function isAffectedBySessionAffinity(message) {
1163
1166
  return message.sessionAffinity !== void 0;
1164
1167
  }
@@ -1189,7 +1192,6 @@ async function acceptMailboxMessageForSession(message, currentSessionId, ctx) {
1189
1192
  if (ctx?.allowUnscoped === true) return true;
1190
1193
  return false;
1191
1194
  }
1192
- var SESSION_RECIPIENT_PREFIX = "@session:";
1193
1195
 
1194
1196
  // src/coordination/sqlite-mailbox.ts
1195
1197
  var SQLITE_MAILBOX_FILE = "_mailbox.sqlite";
@@ -5661,406 +5663,820 @@ function resolveEventSessionId(ctx) {
5661
5663
  return session?.id ?? "";
5662
5664
  }
5663
5665
 
5664
- // src/core/continue-to-next-iteration.ts
5665
- function parseContinueDirective(text) {
5666
- const LINE_MARKERS = /^\s*\[(continue|next step|proceed|done)\]\s*$/gim;
5667
- const tail = text.length <= DIRECTIVE_SCAN_WINDOW ? text : text.slice(text.length - DIRECTIVE_SCAN_WINDOW);
5668
- let match;
5669
- let lastDirective = "none";
5670
- while ((match = LINE_MARKERS.exec(tail)) !== null) {
5671
- const value = (match[1] ?? "").toLowerCase();
5672
- if (value === "continue" || value === "next step" || value === "proceed") {
5673
- lastDirective = "continue";
5674
- } else if (value === "done") {
5675
- lastDirective = "stop";
5676
- }
5666
+ // src/core/agent-loop-context.ts
5667
+ function createAgentLoopContextManager(a, handlers) {
5668
+ let _lastEmittedMsgCount = -1;
5669
+ let _lastEmittedToolCount = -1;
5670
+ let _lastEmittedRevision = -1;
5671
+ let _lastEmittedMaxContext = -1;
5672
+ let _lastPreFlightMsgCount = -1;
5673
+ let _lastPreFlightToolCount = -1;
5674
+ let _lastPreFlightRevision = -1;
5675
+ let _lastCompactionMsgCount = -1;
5676
+ let _lastCompactionRevision = -1;
5677
+ let _lastCompactionToolsRef = null;
5678
+ let _lastCompactionSystemRef = null;
5679
+ let _lastCompactionRequestTokens;
5680
+ let _lastCompactionMaxContext = -1;
5681
+ let _lastCompactionWasNoop = false;
5682
+ let _cachedSysRef = null;
5683
+ let _cachedToolsRef = null;
5684
+ let _cachedOverheadTokens = 0;
5685
+ const calibrationKey = (model = a.ctx.model) => `${a.ctx.provider?.id ?? "unknown"}/${model}`;
5686
+ function currentMaxContext() {
5687
+ const metaLimit = a.ctx.meta?.["effectiveMaxContext"];
5688
+ const providerMax = a.ctx.provider.capabilities.maxContext;
5689
+ return typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : 2e5;
5677
5690
  }
5678
- return lastDirective;
5679
- }
5680
- var DIRECTIVE_SCAN_WINDOW = 2048;
5681
- var META_KEY2 = "_autonomousContinue";
5682
- function consumeAutonomousContinue(ctx) {
5683
- const val = ctx.meta[META_KEY2] === true;
5684
- delete ctx.meta[META_KEY2];
5685
- return val;
5686
- }
5687
-
5688
- // src/core/iteration-limit.ts
5689
- function requestLimitExtension(opts) {
5690
- const { events, sessionId, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
5691
- return new Promise((resolve10) => {
5692
- let resolved = false;
5693
- const timerFired = () => {
5694
- if (!resolved) {
5695
- resolved = true;
5696
- resolve10(0);
5697
- }
5698
- };
5699
- const timer = setTimeout(timerFired, timeoutMs);
5700
- timer.unref();
5701
- const deny = () => {
5702
- if (!resolved) {
5703
- resolved = true;
5704
- clearTimeout(timer);
5705
- resolve10(0);
5706
- }
5691
+ async function refreshProviderContextLimit(provider, model, opts = {}) {
5692
+ a.ctx.meta ??= {};
5693
+ const routeKey = `${provider.id}/${model}`;
5694
+ const previousRouteKey = a.ctx.meta["contextLimitRouteKey"];
5695
+ const routeChanged = previousRouteKey !== void 0 && previousRouteKey !== routeKey;
5696
+ const previousMaxContext = currentMaxContext();
5697
+ if (routeChanged) {
5698
+ delete a.ctx.meta["providerOverflowMaxContext"];
5699
+ delete a.ctx.meta["contextLimitBaseline"];
5700
+ delete a.ctx.meta["providerLimitEffectiveMaxContext"];
5701
+ delete a.ctx.meta["effectiveMaxContext"];
5702
+ delete a.ctx.meta["effectiveMaxContextSource"];
5703
+ delete a.ctx.meta["contextLimitLastEmittedMaxContext"];
5704
+ delete a.ctx.meta["contextLimitLastEmittedSource"];
5705
+ }
5706
+ a.ctx.meta["contextLimitRouteKey"] = routeKey;
5707
+ const routeMeta = a.ctx.meta["effectiveMaxContext"];
5708
+ const routeProviderCap = provider.capabilities.maxContext;
5709
+ const currentEffective = typeof routeMeta === "number" && routeMeta > 0 ? routeMeta : typeof routeProviderCap === "number" && routeProviderCap > 0 ? routeProviderCap : 2e5;
5710
+ const emitEffectiveLimit = (effective2, source) => {
5711
+ const lastEmitted = a.ctx.meta["contextLimitLastEmittedMaxContext"];
5712
+ const lastEmittedSource = a.ctx.meta["contextLimitLastEmittedSource"];
5713
+ if (lastEmitted === effective2 && lastEmittedSource === source) return;
5714
+ const eventPrevious = typeof lastEmitted === "number" && Number.isFinite(lastEmitted) && lastEmitted > 0 ? Math.floor(lastEmitted) : previousMaxContext;
5715
+ a.ctx.meta["contextLimitLastEmittedMaxContext"] = effective2;
5716
+ a.ctx.meta["contextLimitLastEmittedSource"] = source;
5717
+ a.events.emit("ctx.max_context", {
5718
+ sessionId: resolveEventSessionId(a.ctx),
5719
+ providerId: provider.id,
5720
+ modelId: model,
5721
+ maxContext: effective2,
5722
+ ...eventPrevious > 0 ? { previousMaxContext: eventPrevious } : {},
5723
+ source,
5724
+ decreased: eventPrevious > 0 && effective2 < eventPrevious
5725
+ });
5707
5726
  };
5708
- const grant = (extra) => {
5709
- if (!resolved) {
5710
- resolved = true;
5711
- clearTimeout(timer);
5712
- resolve10(Math.max(0, extra));
5727
+ const emitNativeAfterRouteChange = () => {
5728
+ if (routeChanged && currentEffective > 0) {
5729
+ a.ctx.meta["effectiveMaxContext"] = currentEffective;
5730
+ a.ctx.meta["effectiveMaxContextSource"] = "configured";
5731
+ emitEffectiveLimit(currentEffective, "configured");
5713
5732
  }
5714
5733
  };
5715
- events.emit("iteration.limit_reached", {
5716
- sessionId,
5717
- currentIterations,
5718
- currentLimit,
5719
- grant,
5720
- deny
5721
- });
5722
- if (autoExtend) {
5723
- setImmediate(() => {
5724
- if (!resolved) {
5725
- resolved = true;
5726
- clearTimeout(timer);
5727
- resolve10(100);
5728
- }
5729
- });
5734
+ const refresh = provider.refreshContextLimit;
5735
+ if (opts.probe === false || !refresh) {
5736
+ emitNativeAfterRouteChange();
5737
+ return;
5730
5738
  }
5731
- });
5732
- }
5733
-
5734
- // src/core/next-steps-slot.ts
5735
- var SLOT_KEY = "nextsteps.pending";
5736
- var MAX_PENDING_NEXT_STEPS = 4;
5737
- var NEXT_STEPS_TAG_RE = /<nextsteps\b[^>]*>/i;
5738
- function writePendingNextSteps(ctx, steps) {
5739
- const cleaned = [];
5740
- for (const step of steps) {
5741
- const text = typeof step?.text === "string" ? step.text.replace(/\s+/g, " ").trim() : "";
5742
- if (!text) continue;
5743
- cleaned.push(step.auto === true && cleaned.length === 0 ? { text, auto: true } : { text });
5744
- if (cleaned.length >= MAX_PENDING_NEXT_STEPS) break;
5745
- }
5746
- if (cleaned.length === 0) {
5747
- clearPendingNextSteps(ctx);
5748
- return;
5749
- }
5750
- ctx.meta[SLOT_KEY] = cleaned;
5751
- }
5752
- function readPendingNextSteps(ctx) {
5753
- const raw = ctx.meta[SLOT_KEY];
5754
- if (!Array.isArray(raw) || raw.length === 0) return void 0;
5755
- const steps = raw.filter(
5756
- (s) => typeof s?.text === "string" && s.text.length > 0
5757
- );
5758
- return steps.length > 0 ? steps : void 0;
5759
- }
5760
- function clearPendingNextSteps(ctx) {
5761
- delete ctx.meta[SLOT_KEY];
5762
- }
5763
- function hasNextStepsTag(text) {
5764
- return NEXT_STEPS_TAG_RE.test(text);
5765
- }
5766
- function renderNextStepsBlock(steps) {
5767
- const lines = steps.map((step, i) => {
5768
- const auto = i === 0 && step.auto === true ? ' auto="true"' : "";
5769
- return `${i + 1}. ${step.text}${auto}`;
5770
- });
5771
- return `<nextsteps>
5772
- ${lines.join("\n")}
5773
- </nextsteps>`;
5774
- }
5775
- function endsTurn(stopReason) {
5776
- return stopReason !== "tool_use" && stopReason !== "tool_call";
5777
- }
5778
- function maybeAppendPendingNextSteps(ctx, res) {
5779
- if (ctx.agentId !== "leader") return res;
5780
- if (!endsTurn(res.stopReason)) return res;
5781
- if (ctx.signal.aborted) return res;
5782
- const steps = readPendingNextSteps(ctx);
5783
- if (!steps) return res;
5784
- if (hasOpenTodos(ctx.todos)) {
5785
- clearPendingNextSteps(ctx);
5786
- return res;
5787
- }
5788
- const lastTextIndex = res.content.findLastIndex(isTextBlock);
5789
- if (lastTextIndex >= 0) {
5790
- const block = res.content[lastTextIndex];
5791
- if (block && isTextBlock(block) && hasNextStepsTag(block.text)) {
5792
- clearPendingNextSteps(ctx);
5793
- return res;
5739
+ let discovered;
5740
+ try {
5741
+ discovered = await refresh.call(provider, model, { signal: a.ctx.signal });
5742
+ } catch {
5743
+ emitNativeAfterRouteChange();
5744
+ return;
5745
+ }
5746
+ const providerLimit = discovered?.maxContext;
5747
+ if (typeof providerLimit !== "number" || !Number.isFinite(providerLimit) || providerLimit <= 0) {
5748
+ emitNativeAfterRouteChange();
5749
+ return;
5750
+ }
5751
+ const providerLimitFloored = Math.floor(providerLimit);
5752
+ const existingBaseline = a.ctx.meta["contextLimitBaseline"];
5753
+ const lastApplied = a.ctx.meta["providerLimitEffectiveMaxContext"];
5754
+ const existingOverflow = a.ctx.meta["providerOverflowMaxContext"];
5755
+ const effectiveChangedOutsideProbe = typeof lastApplied === "number" && Number.isFinite(lastApplied) && currentEffective !== Math.floor(lastApplied) && currentEffective !== existingOverflow;
5756
+ const baseline = effectiveChangedOutsideProbe || typeof existingBaseline !== "number" || !Number.isFinite(existingBaseline) || existingBaseline <= 0 ? currentEffective : Math.floor(existingBaseline);
5757
+ a.ctx.meta["contextLimitBaseline"] = baseline;
5758
+ const overflowLimit = a.ctx.meta["providerOverflowMaxContext"];
5759
+ const overflowFloored = typeof overflowLimit === "number" && Number.isFinite(overflowLimit) && overflowLimit > 0 ? Math.floor(overflowLimit) : void 0;
5760
+ const ceilings = [baseline, providerLimitFloored];
5761
+ if (overflowFloored !== void 0) ceilings.push(overflowFloored);
5762
+ const effective = Math.min(...ceilings.filter((limit) => limit > 0));
5763
+ let effectiveSource;
5764
+ if (overflowFloored !== void 0 && effective === overflowFloored) {
5765
+ effectiveSource = "provider_overflow";
5766
+ } else if (effective === providerLimitFloored) {
5767
+ effectiveSource = "provider";
5768
+ } else {
5769
+ effectiveSource = "configured";
5794
5770
  }
5771
+ a.ctx.meta["effectiveMaxContext"] = effective;
5772
+ a.ctx.meta["providerLimitEffectiveMaxContext"] = effective;
5773
+ a.ctx.meta["effectiveMaxContextSource"] = effectiveSource;
5774
+ emitEffectiveLimit(effective, effectiveSource);
5795
5775
  }
5796
- const rendered = renderNextStepsBlock(steps);
5797
- const content = [...res.content];
5798
- const target = lastTextIndex >= 0 ? content[lastTextIndex] : void 0;
5799
- if (target && isTextBlock(target)) {
5800
- const separator = target.text.trim() ? "\n\n" : "";
5801
- content[lastTextIndex] = { ...target, text: `${target.text}${separator}${rendered}` };
5802
- } else {
5803
- content.push({ type: "text", text: rendered });
5776
+ function systemAndToolsOverhead() {
5777
+ const sysRef = a.ctx.systemPrompt;
5778
+ const toolsRef = a.ctx.tools;
5779
+ if (sysRef === _cachedSysRef && toolsRef === _cachedToolsRef && _cachedOverheadTokens > 0) {
5780
+ return _cachedOverheadTokens;
5781
+ }
5782
+ const breakdown = estimateRequestTokens([], sysRef, toolsRef ?? [], calibrationKey());
5783
+ _cachedSysRef = sysRef;
5784
+ _cachedToolsRef = toolsRef;
5785
+ _cachedOverheadTokens = breakdown.systemPrompt + breakdown.tools;
5786
+ return _cachedOverheadTokens;
5804
5787
  }
5805
- clearPendingNextSteps(ctx);
5806
- return { ...res, content };
5807
- }
5808
-
5809
- // src/core/streaming-response-builder.ts
5810
- import { randomUUID as randomUUID4 } from "node:crypto";
5811
-
5812
- // src/utils/json-repair.ts
5813
- function completePartialObject(s) {
5814
- if (!s.trim().startsWith("{")) return s;
5815
- if (tryParse(s).ok) return s;
5816
- return repairTruncated(s);
5817
- }
5818
- function repairTruncated(s) {
5819
- const stack = [];
5820
- let inString = false;
5821
- let escaped = false;
5822
- let sawKey = false;
5823
- let prevSig = "";
5824
- let contentEnd = 0;
5825
- let stringBraceDepth = 0;
5826
- for (let i = 0; i < s.length; i++) {
5827
- const ch = expectDefined(s[i]);
5828
- if (inString) {
5829
- contentEnd = i + 1;
5830
- if (escaped) {
5831
- escaped = false;
5832
- continue;
5833
- }
5834
- if (ch === "\\") {
5835
- escaped = true;
5836
- continue;
5837
- }
5838
- if (ch === '"') {
5839
- inString = false;
5840
- prevSig = '"';
5841
- stringBraceDepth = 0;
5842
- continue;
5788
+ function stashRequestTokens(req) {
5789
+ const preFlight = estimateRequestTokens(
5790
+ req.messages,
5791
+ req.system,
5792
+ req.tools ?? [],
5793
+ calibrationKey(req.model)
5794
+ );
5795
+ a.ctx.lastRequestTokens = preFlight.total;
5796
+ _lastPreFlightMsgCount = req.messages.length;
5797
+ _lastPreFlightToolCount = (req.tools ?? []).length;
5798
+ _lastPreFlightRevision = a.ctx.state.revision;
5799
+ _cachedSysRef = req.system;
5800
+ _cachedToolsRef = req.tools ?? [];
5801
+ _cachedOverheadTokens = preFlight.systemPrompt + preFlight.tools;
5802
+ a.ctx.meta["lastRequestTokensAt"] = {
5803
+ msgCount: req.messages.length,
5804
+ toolCount: (req.tools ?? []).length,
5805
+ revision: a.ctx.state.revision
5806
+ };
5807
+ return preFlight;
5808
+ }
5809
+ function refreshContextRequestTokenStash(opts = {}) {
5810
+ const msgCount = a.ctx.messages.length;
5811
+ const toolCount = (a.ctx.tools ?? []).length;
5812
+ const revision = a.ctx.state.revision;
5813
+ const stashed = a.ctx.lastRequestTokens;
5814
+ const stashedAt = a.ctx.meta?.["lastRequestTokensAt"];
5815
+ if (!opts.force && typeof stashed === "number" && stashed > 0 && typeof stashedAt === "object" && stashedAt !== null) {
5816
+ const meta = stashedAt;
5817
+ if (meta.msgCount === msgCount && meta.toolCount === toolCount && meta.revision === revision && _lastPreFlightRevision === revision) {
5818
+ return stashed;
5843
5819
  }
5844
- if (ch === "{") stringBraceDepth++;
5845
- else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
5846
- continue;
5847
- }
5848
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
5849
- contentEnd = i + 1;
5850
- if (ch === '"') {
5851
- inString = true;
5852
- sawKey = true;
5853
- stringBraceDepth = 0;
5854
- prevSig = '"';
5855
- } else if (ch === "{" || ch === "[") {
5856
- stack.push(ch);
5857
- prevSig = ch;
5858
- } else if (ch === "}" || ch === "]") {
5859
- stack.pop();
5860
- prevSig = ch;
5861
- } else {
5862
- prevSig = ch;
5863
5820
  }
5821
+ const refreshed = estimateMessageTokens(a.ctx.messages) + systemAndToolsOverhead();
5822
+ a.ctx.lastRequestTokens = refreshed;
5823
+ _lastPreFlightMsgCount = msgCount;
5824
+ _lastPreFlightToolCount = toolCount;
5825
+ _lastPreFlightRevision = revision;
5826
+ a.ctx.meta["lastRequestTokensAt"] = { msgCount, toolCount, revision };
5827
+ return refreshed;
5864
5828
  }
5865
- if (!sawKey && !inString) return s;
5866
- let result = s.slice(0, contentEnd);
5867
- if (inString) {
5868
- if (escaped) {
5869
- result = result.slice(0, -1);
5870
- } else if (endsWithInvalidEscape(result)) {
5871
- result = result.slice(0, -2);
5829
+ async function compactContextIfNeeded() {
5830
+ const msgCount = a.ctx.messages.length;
5831
+ const maxContext = currentMaxContext();
5832
+ const revision = a.ctx.state.revision;
5833
+ const toolsRef = a.ctx.tools;
5834
+ const systemRef = a.ctx.systemPrompt;
5835
+ const requestTokens = a.ctx.lastRequestTokens;
5836
+ if (_lastCompactionMsgCount === msgCount && _lastCompactionRevision === revision && _lastCompactionToolsRef === toolsRef && _lastCompactionSystemRef === systemRef && _lastCompactionRequestTokens === requestTokens && _lastCompactionWasNoop && _lastCompactionMaxContext === maxContext && maxContext > 0) {
5837
+ return false;
5872
5838
  }
5873
- if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
5874
- result += '"';
5875
- } else if (prevSig === ":") {
5876
- result += "null";
5839
+ const beforeMessages = a.ctx.messages;
5840
+ const beforeMsgCount = a.ctx.messages.length;
5841
+ const beforeRevision = a.ctx.state.revision;
5842
+ await a.pipelines.contextWindow.run(a.ctx);
5843
+ _lastCompactionMsgCount = a.ctx.messages.length;
5844
+ _lastCompactionRevision = a.ctx.state.revision;
5845
+ _lastCompactionToolsRef = a.ctx.tools;
5846
+ _lastCompactionSystemRef = a.ctx.systemPrompt;
5847
+ _lastCompactionMaxContext = maxContext;
5848
+ const changed = a.ctx.state.revision !== beforeRevision || a.ctx.messages !== beforeMessages || a.ctx.messages.length !== beforeMsgCount;
5849
+ const tokens = refreshContextRequestTokenStash({ force: changed });
5850
+ _lastCompactionRequestTokens = tokens;
5851
+ const load = maxContext > 0 ? tokens / maxContext : 0;
5852
+ _lastCompactionWasNoop = tokens > 0 && load < 0.5;
5853
+ if (changed) {
5854
+ _lastEmittedMsgCount = -1;
5855
+ _lastEmittedToolCount = -1;
5856
+ _lastEmittedMaxContext = -1;
5857
+ }
5858
+ return changed;
5877
5859
  }
5878
- for (let k = stack.length - 1; k >= 0; k--) {
5879
- result += stack[k] === "{" ? "}" : "]";
5860
+ async function buildRequestWithPreflightCompaction(opts) {
5861
+ let prepared = await handlers.response.buildAndRunRequestPipeline(opts);
5862
+ let req = prepared.request;
5863
+ let preFlight = stashRequestTokens(req);
5864
+ let probedRouteKey = `${prepared.provider.id}/${req.model}`;
5865
+ await refreshProviderContextLimit(prepared.provider, req.model);
5866
+ if (await compactContextIfNeeded()) {
5867
+ prepared = await handlers.response.buildAndRunRequestPipeline(opts);
5868
+ req = prepared.request;
5869
+ preFlight = stashRequestTokens(req);
5870
+ const rebuiltRouteKey = `${prepared.provider.id}/${req.model}`;
5871
+ if (rebuiltRouteKey !== probedRouteKey) {
5872
+ probedRouteKey = rebuiltRouteKey;
5873
+ await refreshProviderContextLimit(prepared.provider, req.model);
5874
+ if (await compactContextIfNeeded()) {
5875
+ prepared = await handlers.response.buildAndRunRequestPipeline(opts);
5876
+ req = prepared.request;
5877
+ preFlight = stashRequestTokens(req);
5878
+ }
5879
+ }
5880
+ }
5881
+ return { req, provider: prepared.provider, preFlight };
5880
5882
  }
5881
- if (!tryParse(result).ok) {
5882
- const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
5883
- if (tryParse(patched).ok) result = patched;
5883
+ function emitContextPct() {
5884
+ const msgCount = a.ctx.messages.length;
5885
+ const toolCount = (a.ctx.tools ?? []).length;
5886
+ const maxContext = currentMaxContext();
5887
+ const revision = a.ctx.state.revision;
5888
+ if (msgCount === _lastEmittedMsgCount && toolCount === _lastEmittedToolCount && revision === _lastEmittedRevision && maxContext === _lastEmittedMaxContext && maxContext > 0) {
5889
+ return;
5890
+ }
5891
+ _lastEmittedMsgCount = msgCount;
5892
+ _lastEmittedToolCount = toolCount;
5893
+ _lastEmittedRevision = revision;
5894
+ _lastEmittedMaxContext = maxContext;
5895
+ if (msgCount !== _lastPreFlightMsgCount || toolCount !== _lastPreFlightToolCount || revision !== _lastPreFlightRevision) {
5896
+ refreshContextRequestTokenStash({ force: true });
5897
+ }
5898
+ let total;
5899
+ const anchored = realAnchoredInputTokens(
5900
+ a.ctx.messages,
5901
+ a.ctx.lastRealInputTokens,
5902
+ typeof a.ctx.meta?.["realAnchorMsgCount"] === "number" ? a.ctx.meta["realAnchorMsgCount"] : void 0
5903
+ );
5904
+ const stashed = a.ctx.lastRequestTokens;
5905
+ if (anchored !== null) {
5906
+ total = anchored;
5907
+ } else if (typeof stashed === "number" && stashed > 0) {
5908
+ const cal = getCalibrationState(calibrationKey());
5909
+ total = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
5910
+ } else {
5911
+ const raw = refreshContextRequestTokenStash({ force: true });
5912
+ const cal = getCalibrationState(calibrationKey());
5913
+ total = cal.calibrated ? Math.round(raw * Math.min(1.5, Math.max(0.5, cal.ratio))) : raw;
5914
+ }
5915
+ const rawLoad = maxContext > 0 ? total / maxContext : 0;
5916
+ const load = Math.max(0, Math.min(1, rawLoad));
5917
+ a.events.emit("ctx.pct", {
5918
+ sessionId: resolveEventSessionId(a.ctx),
5919
+ load,
5920
+ rawLoad,
5921
+ tokens: total,
5922
+ maxContext
5923
+ });
5884
5924
  }
5885
- return result;
5925
+ return {
5926
+ refreshProviderContextLimit,
5927
+ compactContextIfNeeded,
5928
+ stashRequestTokens,
5929
+ refreshContextRequestTokenStash,
5930
+ buildRequestWithPreflightCompaction,
5931
+ emitContextPct,
5932
+ currentMaxContext,
5933
+ calibrationKey,
5934
+ get lastPreFlightMsgCount() {
5935
+ return _lastPreFlightMsgCount;
5936
+ }
5937
+ };
5886
5938
  }
5887
- var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
5888
- function endsWithInvalidEscape(str) {
5889
- const last = str[str.length - 1];
5890
- if (str[str.length - 2] !== "\\" || last === void 0) return false;
5891
- if (VALID_ESCAPE.has(last)) return false;
5892
- let backslashes = 0;
5893
- for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
5894
- return backslashes % 2 === 1;
5939
+
5940
+ // src/core/agent-loop-detector.ts
5941
+ function iterationFingerprint(blocks) {
5942
+ const toolUses = blocks.filter(isToolUseBlock);
5943
+ const texts = blocks.filter(isTextBlock);
5944
+ const toolNameSet = Array.from(new Set(toolUses.map((u) => u.name))).sort();
5945
+ const firstInputHash = toolUses[0] ? hashSmall(stableStringify(toolUses[0].input ?? {})) : "";
5946
+ const textBlob = texts.map((t2) => t2.text).join("").slice(0, 512);
5947
+ const hasContent = toolNameSet.length > 0 || textBlob.length > 0;
5948
+ if (!hasContent) return "__empty__";
5949
+ return [
5950
+ `tools=${toolNameSet.join("+") || "-"}`,
5951
+ `in0=${firstInputHash}`,
5952
+ `txt=${textBlob}`
5953
+ ].join("\n");
5895
5954
  }
5896
- function tryParse(s) {
5897
- try {
5898
- return { ok: true, value: JSON.parse(s) };
5899
- } catch {
5900
- return { ok: false };
5955
+ function stableStringify(value) {
5956
+ return JSON.stringify(canonicalize(value));
5957
+ }
5958
+ function canonicalize(value) {
5959
+ if (Array.isArray(value)) return value.map(canonicalize);
5960
+ if (value && typeof value === "object") {
5961
+ const src = value;
5962
+ const out = {};
5963
+ for (const k of Object.keys(src).sort()) out[k] = canonicalize(src[k]);
5964
+ return out;
5901
5965
  }
5966
+ return value;
5902
5967
  }
5903
-
5904
- // src/core/streaming-response-builder.ts
5905
- var STREAM_DRAIN_TIMEOUT_MS = 500;
5906
- function buildResponse(state) {
5907
- const content = [];
5908
- for (const b of state.blockOrder) {
5909
- if (b.kind === "text") {
5910
- const txt = state.textBuffers[b.idx] ?? "";
5911
- if (txt) content.push({ type: "text", text: txt });
5912
- } else if (b.kind === "thinking") {
5913
- const t2 = state.thinking[b.idx];
5914
- if (!t2) continue;
5915
- if (!t2.textBuf && !t2.signature) continue;
5916
- const block = { type: "thinking", thinking: t2.textBuf };
5917
- if (t2.signature) block.signature = t2.signature;
5918
- if (t2.providerMeta && Object.keys(t2.providerMeta).length > 0) {
5919
- block.providerMeta = t2.providerMeta;
5968
+ function hashSmall(s) {
5969
+ let h = 2166136261;
5970
+ for (let i = 0; i < s.length; i++) {
5971
+ h ^= s.charCodeAt(i);
5972
+ h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
5973
+ }
5974
+ return h.toString(36);
5975
+ }
5976
+ var AgentLoopDetector = class {
5977
+ constructor(a) {
5978
+ this.a = a;
5979
+ }
5980
+ a;
5981
+ lastToolSignature = "";
5982
+ toolLoopCount = 0;
5983
+ iterationSteerDone = false;
5984
+ recentCallKeys = [];
5985
+ steeredCallKeys = /* @__PURE__ */ new Set();
5986
+ checkIteration(iterationIndex, content, toolUses, queueSteer) {
5987
+ const loopCfg = this.a.loopDetection;
5988
+ if (loopCfg.mode === "off") return { cut: false };
5989
+ const sig = iterationFingerprint(content);
5990
+ if (sig !== "__empty__") {
5991
+ if (sig === this.lastToolSignature) {
5992
+ this.toolLoopCount++;
5993
+ } else {
5994
+ this.lastToolSignature = sig;
5995
+ this.toolLoopCount = 1;
5996
+ this.iterationSteerDone = false;
5997
+ }
5998
+ const names = toolUses.map((t2) => t2.name).join(", ");
5999
+ const hasText2 = content.some(isTextBlock);
6000
+ const kind = toolUses.length > 0 && hasText2 ? "mixed" : toolUses.length > 0 ? "tool" : "message";
6001
+ const observationRepeat = loopCfg.mode === "steer-then-cut" && toolUses.length > 0 && toolUses.every((use) => {
6002
+ const tool = this.a.tools.get(use.name);
6003
+ return tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0);
6004
+ });
6005
+ const repeatMultiplier = observationRepeat ? 2 : 1;
6006
+ const detail = kind === "tool" ? `"${names}" called with effectively identical inputs ${this.toolLoopCount} times in a row` : kind === "mixed" ? `"${names}" + same text repeated ${this.toolLoopCount} times in a row` : `same assistant text repeated ${this.toolLoopCount} times in a row`;
6007
+ const cutAt = (loopCfg.mode === "cut" ? loopCfg.steerThreshold : loopCfg.cutThreshold) * repeatMultiplier;
6008
+ if (this.toolLoopCount >= cutAt) {
6009
+ this.a.logger.warn(`Loop detected: ${detail} \u2014 stopping to prevent infinite loop.`);
6010
+ this.a.events.emit("tool.loop_detected", {
6011
+ sessionId: resolveEventSessionId(this.a.ctx),
6012
+ ctx: this.a.ctx,
6013
+ tools: names,
6014
+ repeatCount: this.toolLoopCount,
6015
+ iteration: iterationIndex,
6016
+ kind,
6017
+ action: "cut",
6018
+ scope: "iteration"
6019
+ });
6020
+ const summary = kind === "message" ? `[Loop detected: same assistant message repeated ${this.toolLoopCount}\xD7 \u2014 stopping to prevent infinite repetition.]` : `[Loop detected: ${detail} \u2014 stopping to prevent infinite repetition.]`;
6021
+ return { cut: true, cutSummary: summary };
6022
+ }
6023
+ if (loopCfg.mode === "steer-then-cut" && this.toolLoopCount >= loopCfg.steerThreshold * repeatMultiplier && !this.iterationSteerDone) {
6024
+ this.iterationSteerDone = true;
6025
+ this.a.logger.warn(`Loop detected: ${detail} \u2014 steering the model to change approach.`);
6026
+ this.a.events.emit("tool.loop_detected", {
6027
+ sessionId: resolveEventSessionId(this.a.ctx),
6028
+ ctx: this.a.ctx,
6029
+ tools: names,
6030
+ repeatCount: this.toolLoopCount,
6031
+ iteration: iterationIndex,
6032
+ kind,
6033
+ action: "steer",
6034
+ scope: "iteration"
6035
+ });
6036
+ queueSteer(
6037
+ `[loop-detector] Your last ${this.toolLoopCount} responses were effectively identical (${detail}). This approach is not working. Change strategy: use a different tool, different arguments, or a different plan \u2014 or explain what is blocking you and stop. Repeating the same response ${cutAt - this.toolLoopCount} more time(s) will terminate the turn.`
6038
+ );
5920
6039
  }
5921
- content.push(block);
5922
6040
  } else {
5923
- const tb = state.tools.get(b.id);
5924
- if (tb) {
5925
- const block = {
5926
- type: "tool_use",
5927
- id: b.id,
5928
- name: tb.name,
5929
- input: tb.input ?? {}
5930
- };
5931
- if (tb.providerMeta && Object.keys(tb.providerMeta).length > 0) {
5932
- block.providerMeta = tb.providerMeta;
5933
- }
5934
- content.push(block);
5935
- }
6041
+ this.lastToolSignature = "";
6042
+ this.toolLoopCount = 0;
6043
+ this.iterationSteerDone = false;
6044
+ }
6045
+ if (loopCfg.mode === "steer-then-cut") {
6046
+ for (const u of toolUses) {
6047
+ const key = `${u.name}:${hashSmall(stableStringify(u.input ?? {}))}`;
6048
+ this.recentCallKeys.push(key);
6049
+ if (this.recentCallKeys.length > loopCfg.windowSize) this.recentCallKeys.shift();
6050
+ if (this.steeredCallKeys.has(key)) continue;
6051
+ let count = 0;
6052
+ for (const k of this.recentCallKeys) if (k === key) count++;
6053
+ const tool = this.a.tools.get(u.name);
6054
+ const observationThreshold = tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0) ? loopCfg.callRepeatThreshold * 2 : loopCfg.callRepeatThreshold;
6055
+ if (count < observationThreshold) continue;
6056
+ this.steeredCallKeys.add(key);
6057
+ const preview = JSON.stringify(u.input ?? {}).slice(0, 160);
6058
+ this.a.logger.warn(
6059
+ `Loop detected: "${u.name}" called with identical arguments ${count}\xD7 within the last ${loopCfg.windowSize} tool calls \u2014 steering the model to change approach.`
6060
+ );
6061
+ this.a.events.emit("tool.loop_detected", {
6062
+ sessionId: resolveEventSessionId(this.a.ctx),
6063
+ ctx: this.a.ctx,
6064
+ tools: u.name,
6065
+ repeatCount: count,
6066
+ iteration: iterationIndex,
6067
+ kind: "tool",
6068
+ action: "steer",
6069
+ scope: "call"
6070
+ });
6071
+ queueSteer(
6072
+ `[loop-detector] You have called ${u.name}(${preview}) ${count} times with identical arguments within the last ${loopCfg.windowSize} tool calls. The result will not change. Do not repeat this call \u2014 use what you already know, try a different approach, or explain the blocker.`
6073
+ );
6074
+ }
5936
6075
  }
6076
+ return { cut: false };
5937
6077
  }
5938
- if (content.length === 0) content.push({ type: "text", text: "" });
5939
- return { content, stopReason: state.stopReason, usage: state.usage, model: state.model };
5940
- }
5941
- function createStreamingState(model) {
5942
- return {
5943
- model,
5944
- stopReason: "end_turn",
5945
- usage: { input: 0, output: 0 },
5946
- textBuffers: [],
5947
- currentTextIndex: -1,
5948
- tools: /* @__PURE__ */ new Map(),
5949
- thinking: [],
5950
- currentThinkingIndex: -1,
5951
- blockOrder: []
5952
- };
6078
+ };
6079
+
6080
+ // src/core/continue-to-next-iteration.ts
6081
+ function parseContinueDirective(text) {
6082
+ const LINE_MARKERS = /^\s*\[(continue|next step|proceed|done)\]\s*$/gim;
6083
+ const tail = text.length <= DIRECTIVE_SCAN_WINDOW ? text : text.slice(text.length - DIRECTIVE_SCAN_WINDOW);
6084
+ let match;
6085
+ let lastDirective = "none";
6086
+ while ((match = LINE_MARKERS.exec(tail)) !== null) {
6087
+ const value = (match[1] ?? "").toLowerCase();
6088
+ if (value === "continue" || value === "next step" || value === "proceed") {
6089
+ lastDirective = "continue";
6090
+ } else if (value === "done") {
6091
+ lastDirective = "stop";
6092
+ }
6093
+ }
6094
+ return lastDirective;
5953
6095
  }
5954
- function handleMessageStart(state, model) {
5955
- state.model = model;
6096
+ var DIRECTIVE_SCAN_WINDOW = 2048;
6097
+ var META_KEY2 = "_autonomousContinue";
6098
+ function consumeAutonomousContinue(ctx) {
6099
+ const val = ctx.meta[META_KEY2] === true;
6100
+ delete ctx.meta[META_KEY2];
6101
+ return val;
5956
6102
  }
5957
- function handleContentBlockStart(state, ev) {
5958
- const kind = ev.kind ?? "text";
5959
- if (kind === "text") {
5960
- state.currentTextIndex = state.textBuffers.length;
5961
- state.textBuffers.push("");
5962
- state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
5963
- } else if (kind === "tool_use") {
5964
- const id = ev.id ?? randomUUID4();
5965
- state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
5966
- state.blockOrder.push({ kind: "tool", id });
5967
- state.currentTextIndex = -1;
5968
- } else if (kind === "thinking") {
5969
- state.currentThinkingIndex = state.thinking.length;
5970
- state.thinking.push({
5971
- textBuf: "",
5972
- ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
6103
+
6104
+ // src/core/iteration-limit.ts
6105
+ function requestLimitExtension(opts) {
6106
+ const { events, sessionId, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
6107
+ return new Promise((resolve10) => {
6108
+ let resolved = false;
6109
+ const timerFired = () => {
6110
+ if (!resolved) {
6111
+ resolved = true;
6112
+ resolve10(0);
6113
+ }
6114
+ };
6115
+ const timer = setTimeout(timerFired, timeoutMs);
6116
+ timer.unref();
6117
+ const deny = () => {
6118
+ if (!resolved) {
6119
+ resolved = true;
6120
+ clearTimeout(timer);
6121
+ resolve10(0);
6122
+ }
6123
+ };
6124
+ const grant = (extra) => {
6125
+ if (!resolved) {
6126
+ resolved = true;
6127
+ clearTimeout(timer);
6128
+ resolve10(Math.max(0, extra));
6129
+ }
6130
+ };
6131
+ events.emit("iteration.limit_reached", {
6132
+ sessionId,
6133
+ currentIterations,
6134
+ currentLimit,
6135
+ grant,
6136
+ deny
5973
6137
  });
5974
- state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
5975
- state.currentTextIndex = -1;
5976
- }
5977
- }
5978
- function handleContentBlockStop(state, ev) {
5979
- void state;
5980
- void ev;
6138
+ if (autoExtend) {
6139
+ setImmediate(() => {
6140
+ if (!resolved) {
6141
+ resolved = true;
6142
+ clearTimeout(timer);
6143
+ resolve10(100);
6144
+ }
6145
+ });
6146
+ }
6147
+ });
5981
6148
  }
5982
- function handleTextDelta(state, text) {
5983
- if (state.currentTextIndex === -1) {
5984
- state.currentTextIndex = state.textBuffers.length;
5985
- state.textBuffers.push("");
5986
- state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
6149
+
6150
+ // src/core/next-steps-slot.ts
6151
+ var SLOT_KEY = "nextsteps.pending";
6152
+ var MAX_PENDING_NEXT_STEPS = 4;
6153
+ var NEXT_STEPS_TAG_RE = /<nextsteps\b[^>]*>/i;
6154
+ function writePendingNextSteps(ctx, steps) {
6155
+ const cleaned = [];
6156
+ for (const step of steps) {
6157
+ const text = typeof step?.text === "string" ? step.text.replace(/\s+/g, " ").trim() : "";
6158
+ if (!text) continue;
6159
+ cleaned.push(step.auto === true && cleaned.length === 0 ? { text, auto: true } : { text });
6160
+ if (cleaned.length >= MAX_PENDING_NEXT_STEPS) break;
5987
6161
  }
5988
- state.textBuffers[state.currentTextIndex] = (state.textBuffers[state.currentTextIndex] ?? "") + text;
5989
- }
5990
- function handleToolUseStart(state, ev) {
5991
- state.currentTextIndex = -1;
5992
- state.tools.set(ev.id, { name: ev.name, partial: "" });
5993
- state.blockOrder.push({ kind: "tool", id: ev.id });
6162
+ if (cleaned.length === 0) {
6163
+ clearPendingNextSteps(ctx);
6164
+ return;
6165
+ }
6166
+ ctx.meta[SLOT_KEY] = cleaned;
5994
6167
  }
5995
- function handleToolUseInputDelta(state, ev) {
5996
- const t2 = state.tools.get(ev.id);
5997
- if (t2) t2.partial += ev.partial;
6168
+ function readPendingNextSteps(ctx) {
6169
+ const raw = ctx.meta[SLOT_KEY];
6170
+ if (!Array.isArray(raw) || raw.length === 0) return void 0;
6171
+ const steps = raw.filter(
6172
+ (s) => typeof s?.text === "string" && s.text.length > 0
6173
+ );
6174
+ return steps.length > 0 ? steps : void 0;
5998
6175
  }
5999
- function safeJsonOrRaw(s) {
6000
- if (!s) return {};
6001
- try {
6002
- return JSON.parse(s);
6003
- } catch {
6004
- const repaired = completePartialObject(s);
6005
- try {
6006
- return JSON.parse(repaired);
6007
- } catch {
6008
- return { _raw: repaired };
6009
- }
6010
- }
6176
+ function clearPendingNextSteps(ctx) {
6177
+ delete ctx.meta[SLOT_KEY];
6011
6178
  }
6012
- function handleToolUseStop(state, ev) {
6013
- const t2 = state.tools.get(ev.id);
6014
- if (t2) {
6015
- t2.input = ev.input !== void 0 ? ev.input : safeJsonOrRaw(t2.partial);
6016
- if (ev.providerMeta) t2.providerMeta = ev.providerMeta;
6017
- }
6018
- state.currentTextIndex = -1;
6179
+ function hasNextStepsTag(text) {
6180
+ return NEXT_STEPS_TAG_RE.test(text);
6019
6181
  }
6020
- function handleThinkingStart(state, ev) {
6021
- state.currentThinkingIndex = state.thinking.length;
6022
- state.thinking.push({
6023
- textBuf: "",
6024
- ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
6182
+ function renderNextStepsBlock(steps) {
6183
+ const lines = steps.map((step, i) => {
6184
+ const auto = i === 0 && step.auto === true ? ' auto="true"' : "";
6185
+ return `${i + 1}. ${step.text}${auto}`;
6025
6186
  });
6026
- state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
6027
- state.currentTextIndex = -1;
6187
+ return `<nextsteps>
6188
+ ${lines.join("\n")}
6189
+ </nextsteps>`;
6028
6190
  }
6029
- function handleThinkingDelta(state, text) {
6030
- if (state.currentThinkingIndex === -1) {
6031
- handleThinkingStart(state, {});
6032
- }
6033
- const t2 = state.thinking[state.currentThinkingIndex];
6034
- if (t2) t2.textBuf += text;
6191
+ function endsTurn(stopReason) {
6192
+ return stopReason !== "tool_use" && stopReason !== "tool_call";
6035
6193
  }
6036
- function handleThinkingSignature(state, signature) {
6037
- if (state.currentThinkingIndex === -1) {
6038
- handleThinkingStart(state, {});
6194
+ function maybeAppendPendingNextSteps(ctx, res) {
6195
+ if (ctx.agentId !== "leader") return res;
6196
+ if (!endsTurn(res.stopReason)) return res;
6197
+ if (ctx.signal.aborted) return res;
6198
+ const steps = readPendingNextSteps(ctx);
6199
+ if (!steps) return res;
6200
+ if (hasOpenTodos(ctx.todos)) {
6201
+ clearPendingNextSteps(ctx);
6202
+ return res;
6039
6203
  }
6040
- const t2 = state.thinking[state.currentThinkingIndex];
6041
- if (t2) t2.signature = signature;
6042
- }
6043
- function handleThinkingStop(state) {
6044
- state.currentThinkingIndex = -1;
6204
+ const lastTextIndex = res.content.findLastIndex(isTextBlock);
6205
+ if (lastTextIndex >= 0) {
6206
+ const block = res.content[lastTextIndex];
6207
+ if (block && isTextBlock(block) && hasNextStepsTag(block.text)) {
6208
+ clearPendingNextSteps(ctx);
6209
+ return res;
6210
+ }
6211
+ }
6212
+ const rendered = renderNextStepsBlock(steps);
6213
+ const content = [...res.content];
6214
+ const target = lastTextIndex >= 0 ? content[lastTextIndex] : void 0;
6215
+ if (target && isTextBlock(target)) {
6216
+ const separator = target.text.trim() ? "\n\n" : "";
6217
+ content[lastTextIndex] = { ...target, text: `${target.text}${separator}${rendered}` };
6218
+ } else {
6219
+ content.push({ type: "text", text: rendered });
6220
+ }
6221
+ clearPendingNextSteps(ctx);
6222
+ return { ...res, content };
6045
6223
  }
6046
- function handleMessageStop(state, ev) {
6047
- state.stopReason = ev.stopReason ?? "end_turn";
6048
- state.usage = ev.usage ?? { input: 0, output: 0 };
6224
+
6225
+ // src/core/streaming-response-builder.ts
6226
+ import { randomUUID as randomUUID4 } from "node:crypto";
6227
+
6228
+ // src/utils/json-repair.ts
6229
+ function completePartialObject(s) {
6230
+ if (!s.trim().startsWith("{")) return s;
6231
+ if (tryParse(s).ok) return s;
6232
+ return repairTruncated(s);
6049
6233
  }
6050
- async function streamProviderToResponse(provider, req, signal, ctx, events, logger) {
6051
- const state = createStreamingState(req.model);
6052
- const sessionId = resolveEventSessionId(ctx);
6053
- logger.debug("Stream started", { providerId: provider.id, model: req.model });
6054
- const TEXT_BATCH_SIZE = 4;
6055
- let pendingText = "";
6056
- let pendingCount = 0;
6057
- const flushText = () => {
6058
- if (pendingCount > 0) {
6059
- events.emit("provider.text_delta", { sessionId, ctx, text: pendingText });
6060
- pendingText = "";
6061
- pendingCount = 0;
6062
- }
6063
- };
6234
+ function repairTruncated(s) {
6235
+ const stack = [];
6236
+ let inString = false;
6237
+ let escaped = false;
6238
+ let sawKey = false;
6239
+ let prevSig = "";
6240
+ let contentEnd = 0;
6241
+ let stringBraceDepth = 0;
6242
+ for (let i = 0; i < s.length; i++) {
6243
+ const ch = expectDefined(s[i]);
6244
+ if (inString) {
6245
+ contentEnd = i + 1;
6246
+ if (escaped) {
6247
+ escaped = false;
6248
+ continue;
6249
+ }
6250
+ if (ch === "\\") {
6251
+ escaped = true;
6252
+ continue;
6253
+ }
6254
+ if (ch === '"') {
6255
+ inString = false;
6256
+ prevSig = '"';
6257
+ stringBraceDepth = 0;
6258
+ continue;
6259
+ }
6260
+ if (ch === "{") stringBraceDepth++;
6261
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
6262
+ continue;
6263
+ }
6264
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
6265
+ contentEnd = i + 1;
6266
+ if (ch === '"') {
6267
+ inString = true;
6268
+ sawKey = true;
6269
+ stringBraceDepth = 0;
6270
+ prevSig = '"';
6271
+ } else if (ch === "{" || ch === "[") {
6272
+ stack.push(ch);
6273
+ prevSig = ch;
6274
+ } else if (ch === "}" || ch === "]") {
6275
+ stack.pop();
6276
+ prevSig = ch;
6277
+ } else {
6278
+ prevSig = ch;
6279
+ }
6280
+ }
6281
+ if (!sawKey && !inString) return s;
6282
+ let result = s.slice(0, contentEnd);
6283
+ if (inString) {
6284
+ if (escaped) {
6285
+ result = result.slice(0, -1);
6286
+ } else if (endsWithInvalidEscape(result)) {
6287
+ result = result.slice(0, -2);
6288
+ }
6289
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
6290
+ result += '"';
6291
+ } else if (prevSig === ":") {
6292
+ result += "null";
6293
+ }
6294
+ for (let k = stack.length - 1; k >= 0; k--) {
6295
+ result += stack[k] === "{" ? "}" : "]";
6296
+ }
6297
+ if (!tryParse(result).ok) {
6298
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
6299
+ if (tryParse(patched).ok) result = patched;
6300
+ }
6301
+ return result;
6302
+ }
6303
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
6304
+ function endsWithInvalidEscape(str) {
6305
+ const last = str[str.length - 1];
6306
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
6307
+ if (VALID_ESCAPE.has(last)) return false;
6308
+ let backslashes = 0;
6309
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
6310
+ return backslashes % 2 === 1;
6311
+ }
6312
+ function tryParse(s) {
6313
+ try {
6314
+ return { ok: true, value: JSON.parse(s) };
6315
+ } catch {
6316
+ return { ok: false };
6317
+ }
6318
+ }
6319
+
6320
+ // src/core/streaming-response-builder.ts
6321
+ var STREAM_DRAIN_TIMEOUT_MS = 500;
6322
+ function buildResponse(state) {
6323
+ const content = [];
6324
+ for (const b of state.blockOrder) {
6325
+ if (b.kind === "text") {
6326
+ const txt = state.textBuffers[b.idx] ?? "";
6327
+ if (txt) content.push({ type: "text", text: txt });
6328
+ } else if (b.kind === "thinking") {
6329
+ const t2 = state.thinking[b.idx];
6330
+ if (!t2) continue;
6331
+ if (!t2.textBuf && !t2.signature) continue;
6332
+ const block = { type: "thinking", thinking: t2.textBuf };
6333
+ if (t2.signature) block.signature = t2.signature;
6334
+ if (t2.providerMeta && Object.keys(t2.providerMeta).length > 0) {
6335
+ block.providerMeta = t2.providerMeta;
6336
+ }
6337
+ content.push(block);
6338
+ } else {
6339
+ const tb = state.tools.get(b.id);
6340
+ if (tb) {
6341
+ const block = {
6342
+ type: "tool_use",
6343
+ id: b.id,
6344
+ name: tb.name,
6345
+ input: tb.input ?? {}
6346
+ };
6347
+ if (tb.providerMeta && Object.keys(tb.providerMeta).length > 0) {
6348
+ block.providerMeta = tb.providerMeta;
6349
+ }
6350
+ content.push(block);
6351
+ }
6352
+ }
6353
+ }
6354
+ if (content.length === 0) content.push({ type: "text", text: "" });
6355
+ return { content, stopReason: state.stopReason, usage: state.usage, model: state.model };
6356
+ }
6357
+ function createStreamingState(model) {
6358
+ return {
6359
+ model,
6360
+ stopReason: "end_turn",
6361
+ usage: { input: 0, output: 0 },
6362
+ textBuffers: [],
6363
+ currentTextIndex: -1,
6364
+ tools: /* @__PURE__ */ new Map(),
6365
+ thinking: [],
6366
+ currentThinkingIndex: -1,
6367
+ blockOrder: []
6368
+ };
6369
+ }
6370
+ function handleMessageStart(state, model) {
6371
+ state.model = model;
6372
+ }
6373
+ function handleContentBlockStart(state, ev) {
6374
+ const kind = ev.kind ?? "text";
6375
+ if (kind === "text") {
6376
+ state.currentTextIndex = state.textBuffers.length;
6377
+ state.textBuffers.push("");
6378
+ state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
6379
+ } else if (kind === "tool_use") {
6380
+ const id = ev.id ?? randomUUID4();
6381
+ state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
6382
+ state.blockOrder.push({ kind: "tool", id });
6383
+ state.currentTextIndex = -1;
6384
+ } else if (kind === "thinking") {
6385
+ state.currentThinkingIndex = state.thinking.length;
6386
+ state.thinking.push({
6387
+ textBuf: "",
6388
+ ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
6389
+ });
6390
+ state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
6391
+ state.currentTextIndex = -1;
6392
+ }
6393
+ }
6394
+ function handleContentBlockStop(state, ev) {
6395
+ void state;
6396
+ void ev;
6397
+ }
6398
+ function handleTextDelta(state, text) {
6399
+ if (state.currentTextIndex === -1) {
6400
+ state.currentTextIndex = state.textBuffers.length;
6401
+ state.textBuffers.push("");
6402
+ state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
6403
+ }
6404
+ state.textBuffers[state.currentTextIndex] = (state.textBuffers[state.currentTextIndex] ?? "") + text;
6405
+ }
6406
+ function handleToolUseStart(state, ev) {
6407
+ state.currentTextIndex = -1;
6408
+ state.tools.set(ev.id, { name: ev.name, partial: "" });
6409
+ state.blockOrder.push({ kind: "tool", id: ev.id });
6410
+ }
6411
+ function handleToolUseInputDelta(state, ev) {
6412
+ const t2 = state.tools.get(ev.id);
6413
+ if (t2) t2.partial += ev.partial;
6414
+ }
6415
+ function safeJsonOrRaw(s) {
6416
+ if (!s) return {};
6417
+ try {
6418
+ return JSON.parse(s);
6419
+ } catch {
6420
+ const repaired = completePartialObject(s);
6421
+ try {
6422
+ return JSON.parse(repaired);
6423
+ } catch {
6424
+ return { _raw: repaired };
6425
+ }
6426
+ }
6427
+ }
6428
+ function handleToolUseStop(state, ev) {
6429
+ const t2 = state.tools.get(ev.id);
6430
+ if (t2) {
6431
+ t2.input = ev.input !== void 0 ? ev.input : safeJsonOrRaw(t2.partial);
6432
+ if (ev.providerMeta) t2.providerMeta = ev.providerMeta;
6433
+ }
6434
+ state.currentTextIndex = -1;
6435
+ }
6436
+ function handleThinkingStart(state, ev) {
6437
+ state.currentThinkingIndex = state.thinking.length;
6438
+ state.thinking.push({
6439
+ textBuf: "",
6440
+ ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
6441
+ });
6442
+ state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
6443
+ state.currentTextIndex = -1;
6444
+ }
6445
+ function handleThinkingDelta(state, text) {
6446
+ if (state.currentThinkingIndex === -1) {
6447
+ handleThinkingStart(state, {});
6448
+ }
6449
+ const t2 = state.thinking[state.currentThinkingIndex];
6450
+ if (t2) t2.textBuf += text;
6451
+ }
6452
+ function handleThinkingSignature(state, signature) {
6453
+ if (state.currentThinkingIndex === -1) {
6454
+ handleThinkingStart(state, {});
6455
+ }
6456
+ const t2 = state.thinking[state.currentThinkingIndex];
6457
+ if (t2) t2.signature = signature;
6458
+ }
6459
+ function handleThinkingStop(state) {
6460
+ state.currentThinkingIndex = -1;
6461
+ }
6462
+ function handleMessageStop(state, ev) {
6463
+ state.stopReason = ev.stopReason ?? "end_turn";
6464
+ state.usage = ev.usage ?? { input: 0, output: 0 };
6465
+ }
6466
+ async function streamProviderToResponse(provider, req, signal, ctx, events, logger) {
6467
+ const state = createStreamingState(req.model);
6468
+ const sessionId = resolveEventSessionId(ctx);
6469
+ logger.debug("Stream started", { providerId: provider.id, model: req.model });
6470
+ const TEXT_BATCH_SIZE = 4;
6471
+ let pendingText = "";
6472
+ let pendingCount = 0;
6473
+ const flushText = () => {
6474
+ if (pendingCount > 0) {
6475
+ events.emit("provider.text_delta", { sessionId, ctx, text: pendingText });
6476
+ pendingText = "";
6477
+ pendingCount = 0;
6478
+ }
6479
+ };
6064
6480
  const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
6065
6481
  try {
6066
6482
  for (; ; ) {
@@ -6529,413 +6945,122 @@ async function runProviderWithRetry(opts) {
6529
6945
  signal.removeEventListener("abort", onAbort);
6530
6946
  };
6531
6947
  const onAbort = () => {
6532
- if (settled) return;
6533
- settled = true;
6534
- cleanup();
6535
- reject(new Error("aborted"));
6536
- };
6537
- const t2 = setTimeout(() => {
6538
- if (settled) return;
6539
- settled = true;
6540
- cleanup();
6541
- resolve10();
6542
- }, delay2);
6543
- if (signal.aborted) {
6544
- onAbort();
6545
- return;
6546
- }
6547
- signal.addEventListener("abort", onAbort);
6548
- });
6549
- attempt++;
6550
- }
6551
- }
6552
- }
6553
- function elapsedMs(startedNs) {
6554
- return Number(process.hrtime.bigint() - startedNs) / 1e6;
6555
- }
6556
-
6557
- // src/core/queued-messages.ts
6558
- var META_KEY3 = "_queuedMessagesAwareness";
6559
- var MAX_ITEMS = 20;
6560
- var MAX_PREVIEW_CHARS = 500;
6561
- function read(ctx) {
6562
- const raw = ctx.meta[META_KEY3];
6563
- if (!raw || typeof raw !== "object") return void 0;
6564
- const s = raw;
6565
- return Array.isArray(s.items) && Array.isArray(s.seen) ? s : void 0;
6566
- }
6567
- function sameList(a, b) {
6568
- return a.length === b.length && a.every((v, i) => v === b[i]);
6569
- }
6570
- function toPreview(text) {
6571
- const trimmed = text.trim();
6572
- if (trimmed.length <= MAX_PREVIEW_CHARS) return trimmed;
6573
- return `${trimmed.slice(0, MAX_PREVIEW_CHARS - 1)}\u2026`;
6574
- }
6575
- function setQueuedMessagesSnapshot(ctx, texts) {
6576
- const items = texts.map(toPreview).filter((t2) => t2.length > 0).slice(0, MAX_ITEMS);
6577
- const prev = read(ctx);
6578
- if (items.length === 0 && (!prev || prev.seen.length === 0)) {
6579
- if (prev) delete ctx.meta[META_KEY3];
6580
- return;
6581
- }
6582
- ctx.meta[META_KEY3] = { items, seen: prev?.seen ?? [] };
6583
- }
6584
- function consumeQueuedMessagesUpdate(ctx) {
6585
- const s = read(ctx);
6586
- if (!s) return null;
6587
- if (sameList(s.items, s.seen)) return null;
6588
- if (s.items.length === 0) {
6589
- delete ctx.meta[META_KEY3];
6590
- return null;
6591
- }
6592
- ctx.meta[META_KEY3] = { items: s.items, seen: s.items };
6593
- return s.items;
6594
- }
6595
- function buildQueuedMessagesBlock(items) {
6596
- const body = items.map((t2, i) => `${i + 1}. ${t2}`).join("\n");
6597
- return [
6598
- "[QUEUED MESSAGES \u2014 the user typed these while you were working. They are",
6599
- "waiting in the input queue and each will be delivered as its own turn",
6600
- "after you finish, so do NOT answer or act on them as new tasks now. Read",
6601
- "them as a heads-up: if one changes what you should be doing right now",
6602
- "(new constraint, invalidated approach), adapt your current work; if one",
6603
- "is just an FYI, fold it in. The queue may still change before delivery.",
6604
- "This notice replaces any earlier one \u2014 the queue now holds exactly:",
6605
- "",
6606
- body,
6607
- "]"
6608
- ].join("\n");
6609
- }
6610
-
6611
- // src/core/request-provider-binding.ts
6612
- var requestProviders = /* @__PURE__ */ new WeakMap();
6613
- function bindRequestProvider(request, provider) {
6614
- requestProviders.set(request, provider);
6615
- }
6616
- function providerBoundToRequest(request) {
6617
- return requestProviders.get(request);
6618
- }
6619
-
6620
- // src/core/agent-loop.ts
6621
- function toError(err) {
6622
- return err instanceof Error ? err : new Error(String(err));
6623
- }
6624
- function signalAbortReason(signal) {
6625
- const r = signal.reason;
6626
- if (r instanceof Error) return r.message || r.name;
6627
- if (typeof r === "string" && r.length > 0) return r;
6628
- return "aborted";
6629
- }
6630
- function createAgentLoopHandler(a, handlers) {
6631
- const checkMailbox = attachMailboxChecker(a);
6632
- const fleetPulseCfg = (() => {
6633
- try {
6634
- return typeof a.container?.has === "function" && a.container.has(TOKENS.ConfigStore) ? a.container.resolve(TOKENS.ConfigStore).get().fleet?.pulse : void 0;
6635
- } catch {
6636
- return void 0;
6637
- }
6638
- })();
6639
- const getFleetPulse = attachFleetPulse(a, fleetPulseCfg);
6640
- const pulseEveryN = Math.max(1, fleetPulseCfg?.everyNIterations ?? 5);
6641
- const backgroundCoordination = () => a.ctx.meta["coordinationContextMode"] === "background";
6642
- async function refreshProviderContextLimit(provider, model, opts = {}) {
6643
- a.ctx.meta ??= {};
6644
- const routeKey = `${provider.id}/${model}`;
6645
- const previousRouteKey = a.ctx.meta["contextLimitRouteKey"];
6646
- const routeChanged = previousRouteKey !== void 0 && previousRouteKey !== routeKey;
6647
- const previousMaxContext = currentMaxContext();
6648
- if (routeChanged) {
6649
- delete a.ctx.meta["providerOverflowMaxContext"];
6650
- delete a.ctx.meta["contextLimitBaseline"];
6651
- delete a.ctx.meta["providerLimitEffectiveMaxContext"];
6652
- delete a.ctx.meta["effectiveMaxContext"];
6653
- delete a.ctx.meta["effectiveMaxContextSource"];
6654
- delete a.ctx.meta["contextLimitLastEmittedMaxContext"];
6655
- delete a.ctx.meta["contextLimitLastEmittedSource"];
6656
- }
6657
- a.ctx.meta["contextLimitRouteKey"] = routeKey;
6658
- const routeMeta = a.ctx.meta["effectiveMaxContext"];
6659
- const routeProviderCap = provider.capabilities.maxContext;
6660
- const currentEffective = typeof routeMeta === "number" && routeMeta > 0 ? routeMeta : typeof routeProviderCap === "number" && routeProviderCap > 0 ? routeProviderCap : 2e5;
6661
- const emitEffectiveLimit = (effective2, source) => {
6662
- const lastEmitted = a.ctx.meta["contextLimitLastEmittedMaxContext"];
6663
- const lastEmittedSource = a.ctx.meta["contextLimitLastEmittedSource"];
6664
- if (lastEmitted === effective2 && lastEmittedSource === source) return;
6665
- const eventPrevious = typeof lastEmitted === "number" && Number.isFinite(lastEmitted) && lastEmitted > 0 ? Math.floor(lastEmitted) : previousMaxContext;
6666
- a.ctx.meta["contextLimitLastEmittedMaxContext"] = effective2;
6667
- a.ctx.meta["contextLimitLastEmittedSource"] = source;
6668
- a.events.emit("ctx.max_context", {
6669
- sessionId: resolveEventSessionId(a.ctx),
6670
- providerId: provider.id,
6671
- modelId: model,
6672
- maxContext: effective2,
6673
- ...eventPrevious > 0 ? { previousMaxContext: eventPrevious } : {},
6674
- source,
6675
- decreased: eventPrevious > 0 && effective2 < eventPrevious
6676
- });
6677
- };
6678
- const emitNativeAfterRouteChange = () => {
6679
- if (routeChanged && currentEffective > 0) {
6680
- a.ctx.meta["effectiveMaxContext"] = currentEffective;
6681
- a.ctx.meta["effectiveMaxContextSource"] = "configured";
6682
- emitEffectiveLimit(currentEffective, "configured");
6683
- }
6684
- };
6685
- const refresh = provider.refreshContextLimit;
6686
- if (opts.probe === false || !refresh) {
6687
- emitNativeAfterRouteChange();
6688
- return;
6689
- }
6690
- let discovered;
6691
- try {
6692
- discovered = await refresh.call(provider, model, { signal: a.ctx.signal });
6693
- } catch {
6694
- emitNativeAfterRouteChange();
6695
- return;
6696
- }
6697
- const providerLimit = discovered?.maxContext;
6698
- if (typeof providerLimit !== "number" || !Number.isFinite(providerLimit) || providerLimit <= 0) {
6699
- emitNativeAfterRouteChange();
6700
- return;
6701
- }
6702
- const providerLimitFloored = Math.floor(providerLimit);
6703
- const existingBaseline = a.ctx.meta["contextLimitBaseline"];
6704
- const lastApplied = a.ctx.meta["providerLimitEffectiveMaxContext"];
6705
- const existingOverflow = a.ctx.meta["providerOverflowMaxContext"];
6706
- const effectiveChangedOutsideProbe = typeof lastApplied === "number" && Number.isFinite(lastApplied) && currentEffective !== Math.floor(lastApplied) && currentEffective !== existingOverflow;
6707
- const baseline = effectiveChangedOutsideProbe || typeof existingBaseline !== "number" || !Number.isFinite(existingBaseline) || existingBaseline <= 0 ? currentEffective : Math.floor(existingBaseline);
6708
- a.ctx.meta["contextLimitBaseline"] = baseline;
6709
- const overflowLimit = a.ctx.meta["providerOverflowMaxContext"];
6710
- const overflowFloored = typeof overflowLimit === "number" && Number.isFinite(overflowLimit) && overflowLimit > 0 ? Math.floor(overflowLimit) : void 0;
6711
- const ceilings = [baseline, providerLimitFloored];
6712
- if (overflowFloored !== void 0) ceilings.push(overflowFloored);
6713
- const effective = Math.min(...ceilings.filter((limit) => limit > 0));
6714
- let effectiveSource;
6715
- if (overflowFloored !== void 0 && effective === overflowFloored) {
6716
- effectiveSource = "provider_overflow";
6717
- } else if (effective === providerLimitFloored) {
6718
- effectiveSource = "provider";
6719
- } else {
6720
- effectiveSource = "configured";
6721
- }
6722
- a.ctx.meta["effectiveMaxContext"] = effective;
6723
- a.ctx.meta["providerLimitEffectiveMaxContext"] = effective;
6724
- a.ctx.meta["effectiveMaxContextSource"] = effectiveSource;
6725
- emitEffectiveLimit(effective, effectiveSource);
6726
- }
6727
- async function compactContextIfNeeded() {
6728
- const msgCount = a.ctx.messages.length;
6729
- const maxContext = currentMaxContext();
6730
- const revision = a.ctx.state.revision;
6731
- const toolsRef = a.ctx.tools;
6732
- const systemRef = a.ctx.systemPrompt;
6733
- const requestTokens = a.ctx.lastRequestTokens;
6734
- if (_lastCompactionMsgCount === msgCount && _lastCompactionRevision === revision && _lastCompactionToolsRef === toolsRef && _lastCompactionSystemRef === systemRef && _lastCompactionRequestTokens === requestTokens && _lastCompactionWasNoop && _lastCompactionMaxContext === maxContext && maxContext > 0) {
6735
- return false;
6736
- }
6737
- const beforeMessages = a.ctx.messages;
6738
- const beforeMsgCount = a.ctx.messages.length;
6739
- const beforeRevision = a.ctx.state.revision;
6740
- await a.pipelines.contextWindow.run(a.ctx);
6741
- _lastCompactionMsgCount = a.ctx.messages.length;
6742
- _lastCompactionRevision = a.ctx.state.revision;
6743
- _lastCompactionToolsRef = a.ctx.tools;
6744
- _lastCompactionSystemRef = a.ctx.systemPrompt;
6745
- _lastCompactionMaxContext = maxContext;
6746
- const changed = a.ctx.state.revision !== beforeRevision || a.ctx.messages !== beforeMessages || a.ctx.messages.length !== beforeMsgCount;
6747
- const tokens = refreshContextRequestTokenStash({ force: changed });
6748
- _lastCompactionRequestTokens = tokens;
6749
- const load = maxContext > 0 ? tokens / maxContext : 0;
6750
- _lastCompactionWasNoop = tokens > 0 && load < 0.5;
6751
- if (changed) {
6752
- _lastEmittedMsgCount = -1;
6753
- _lastEmittedToolCount = -1;
6754
- _lastEmittedMaxContext = -1;
6755
- }
6756
- return changed;
6757
- }
6758
- const calibrationKey = (model = a.ctx.model) => `${a.ctx.provider?.id ?? "unknown"}/${model}`;
6759
- let _cachedSysRef = null;
6760
- let _cachedToolsRef = null;
6761
- let _cachedOverheadTokens = 0;
6762
- function systemAndToolsOverhead() {
6763
- const sysRef = a.ctx.systemPrompt;
6764
- const toolsRef = a.ctx.tools;
6765
- if (sysRef === _cachedSysRef && toolsRef === _cachedToolsRef && _cachedOverheadTokens > 0) {
6766
- return _cachedOverheadTokens;
6767
- }
6768
- const breakdown = estimateRequestTokens([], sysRef, toolsRef ?? [], calibrationKey());
6769
- _cachedSysRef = sysRef;
6770
- _cachedToolsRef = toolsRef;
6771
- _cachedOverheadTokens = breakdown.systemPrompt + breakdown.tools;
6772
- return _cachedOverheadTokens;
6773
- }
6774
- function stashRequestTokens(req) {
6775
- const preFlight = estimateRequestTokens(
6776
- req.messages,
6777
- req.system,
6778
- req.tools ?? [],
6779
- calibrationKey(req.model)
6780
- );
6781
- a.ctx.lastRequestTokens = preFlight.total;
6782
- _lastPreFlightMsgCount = req.messages.length;
6783
- _lastPreFlightToolCount = (req.tools ?? []).length;
6784
- _lastPreFlightRevision = a.ctx.state.revision;
6785
- _cachedSysRef = req.system;
6786
- _cachedToolsRef = req.tools ?? [];
6787
- _cachedOverheadTokens = preFlight.systemPrompt + preFlight.tools;
6788
- a.ctx.meta["lastRequestTokensAt"] = {
6789
- msgCount: req.messages.length,
6790
- toolCount: (req.tools ?? []).length,
6791
- revision: a.ctx.state.revision
6792
- };
6793
- return preFlight;
6794
- }
6795
- function refreshContextRequestTokenStash(opts = {}) {
6796
- const msgCount = a.ctx.messages.length;
6797
- const toolCount = (a.ctx.tools ?? []).length;
6798
- const revision = a.ctx.state.revision;
6799
- const stashed = a.ctx.lastRequestTokens;
6800
- const stashedAt = a.ctx.meta?.["lastRequestTokensAt"];
6801
- if (!opts.force && typeof stashed === "number" && stashed > 0 && typeof stashedAt === "object" && stashedAt !== null) {
6802
- const meta = stashedAt;
6803
- if (meta.msgCount === msgCount && meta.toolCount === toolCount && meta.revision === revision && _lastPreFlightRevision === revision) {
6804
- return stashed;
6805
- }
6806
- }
6807
- const refreshed = estimateMessageTokens(a.ctx.messages) + systemAndToolsOverhead();
6808
- a.ctx.lastRequestTokens = refreshed;
6809
- _lastPreFlightMsgCount = msgCount;
6810
- _lastPreFlightToolCount = toolCount;
6811
- _lastPreFlightRevision = revision;
6812
- a.ctx.meta["lastRequestTokensAt"] = { msgCount, toolCount, revision };
6813
- return refreshed;
6814
- }
6815
- async function buildRequestWithPreflightCompaction(opts) {
6816
- let prepared = await handlers.response.buildAndRunRequestPipeline(opts);
6817
- let req = prepared.request;
6818
- let preFlight = stashRequestTokens(req);
6819
- let probedRouteKey = `${prepared.provider.id}/${req.model}`;
6820
- await refreshProviderContextLimit(prepared.provider, req.model);
6821
- if (await compactContextIfNeeded()) {
6822
- prepared = await handlers.response.buildAndRunRequestPipeline(opts);
6823
- req = prepared.request;
6824
- preFlight = stashRequestTokens(req);
6825
- const rebuiltRouteKey = `${prepared.provider.id}/${req.model}`;
6826
- if (rebuiltRouteKey !== probedRouteKey) {
6827
- probedRouteKey = rebuiltRouteKey;
6828
- await refreshProviderContextLimit(prepared.provider, req.model);
6829
- if (await compactContextIfNeeded()) {
6830
- prepared = await handlers.response.buildAndRunRequestPipeline(opts);
6831
- req = prepared.request;
6832
- preFlight = stashRequestTokens(req);
6948
+ if (settled) return;
6949
+ settled = true;
6950
+ cleanup();
6951
+ reject(new Error("aborted"));
6952
+ };
6953
+ const t2 = setTimeout(() => {
6954
+ if (settled) return;
6955
+ settled = true;
6956
+ cleanup();
6957
+ resolve10();
6958
+ }, delay2);
6959
+ if (signal.aborted) {
6960
+ onAbort();
6961
+ return;
6833
6962
  }
6834
- }
6963
+ signal.addEventListener("abort", onAbort);
6964
+ });
6965
+ attempt++;
6835
6966
  }
6836
- return { req, provider: prepared.provider, preFlight };
6837
6967
  }
6838
- function emitContextPct() {
6839
- const msgCount = a.ctx.messages.length;
6840
- const toolCount = (a.ctx.tools ?? []).length;
6841
- const maxContext = currentMaxContext();
6842
- const revision = a.ctx.state.revision;
6843
- if (msgCount === _lastEmittedMsgCount && toolCount === _lastEmittedToolCount && revision === _lastEmittedRevision && maxContext === _lastEmittedMaxContext && maxContext > 0) {
6844
- return;
6845
- }
6846
- _lastEmittedMsgCount = msgCount;
6847
- _lastEmittedToolCount = toolCount;
6848
- _lastEmittedRevision = revision;
6849
- _lastEmittedMaxContext = maxContext;
6850
- if (msgCount !== _lastPreFlightMsgCount || toolCount !== _lastPreFlightToolCount || revision !== _lastPreFlightRevision) {
6851
- refreshContextRequestTokenStash({ force: true });
6852
- }
6853
- let total;
6854
- const anchored = realAnchoredInputTokens(
6855
- a.ctx.messages,
6856
- a.ctx.lastRealInputTokens,
6857
- typeof a.ctx.meta?.["realAnchorMsgCount"] === "number" ? a.ctx.meta["realAnchorMsgCount"] : void 0
6858
- );
6859
- const stashed = a.ctx.lastRequestTokens;
6860
- if (anchored !== null) {
6861
- total = anchored;
6862
- } else if (typeof stashed === "number" && stashed > 0) {
6863
- const cal = getCalibrationState(calibrationKey());
6864
- total = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
6865
- } else {
6866
- const raw = refreshContextRequestTokenStash({ force: true });
6867
- const cal = getCalibrationState(calibrationKey());
6868
- total = cal.calibrated ? Math.round(raw * Math.min(1.5, Math.max(0.5, cal.ratio))) : raw;
6869
- }
6870
- const rawLoad = maxContext > 0 ? total / maxContext : 0;
6871
- const load = Math.max(0, Math.min(1, rawLoad));
6872
- a.events.emit("ctx.pct", {
6873
- sessionId: resolveEventSessionId(a.ctx),
6874
- load,
6875
- rawLoad,
6876
- tokens: total,
6877
- maxContext
6878
- });
6968
+ }
6969
+ function elapsedMs(startedNs) {
6970
+ return Number(process.hrtime.bigint() - startedNs) / 1e6;
6971
+ }
6972
+
6973
+ // src/core/queued-messages.ts
6974
+ var META_KEY3 = "_queuedMessagesAwareness";
6975
+ var MAX_ITEMS = 20;
6976
+ var MAX_PREVIEW_CHARS = 500;
6977
+ function read(ctx) {
6978
+ const raw = ctx.meta[META_KEY3];
6979
+ if (!raw || typeof raw !== "object") return void 0;
6980
+ const s = raw;
6981
+ return Array.isArray(s.items) && Array.isArray(s.seen) ? s : void 0;
6982
+ }
6983
+ function sameList(a, b) {
6984
+ return a.length === b.length && a.every((v, i) => v === b[i]);
6985
+ }
6986
+ function toPreview(text) {
6987
+ const trimmed = text.trim();
6988
+ if (trimmed.length <= MAX_PREVIEW_CHARS) return trimmed;
6989
+ return `${trimmed.slice(0, MAX_PREVIEW_CHARS - 1)}\u2026`;
6990
+ }
6991
+ function setQueuedMessagesSnapshot(ctx, texts) {
6992
+ const items = texts.map(toPreview).filter((t2) => t2.length > 0).slice(0, MAX_ITEMS);
6993
+ const prev = read(ctx);
6994
+ if (items.length === 0 && (!prev || prev.seen.length === 0)) {
6995
+ if (prev) delete ctx.meta[META_KEY3];
6996
+ return;
6879
6997
  }
6880
- function currentMaxContext() {
6881
- const metaLimit = a.ctx.meta?.["effectiveMaxContext"];
6882
- const providerMax = a.ctx.provider.capabilities.maxContext;
6883
- return typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : 2e5;
6998
+ ctx.meta[META_KEY3] = { items, seen: prev?.seen ?? [] };
6999
+ }
7000
+ function consumeQueuedMessagesUpdate(ctx) {
7001
+ const s = read(ctx);
7002
+ if (!s) return null;
7003
+ if (sameList(s.items, s.seen)) return null;
7004
+ if (s.items.length === 0) {
7005
+ delete ctx.meta[META_KEY3];
7006
+ return null;
6884
7007
  }
6885
- let _lastEmittedMsgCount = -1;
6886
- let _lastEmittedToolCount = -1;
6887
- let _lastEmittedRevision = -1;
6888
- let _lastEmittedMaxContext = -1;
6889
- let _lastPreFlightMsgCount = -1;
6890
- let _lastPreFlightToolCount = -1;
6891
- let _lastPreFlightRevision = -1;
6892
- let _lastCompactionMsgCount = -1;
6893
- let _lastCompactionRevision = -1;
6894
- let _lastCompactionToolsRef = null;
6895
- let _lastCompactionSystemRef = null;
6896
- let _lastCompactionRequestTokens;
6897
- let _lastCompactionMaxContext = -1;
6898
- let _lastCompactionWasNoop = false;
7008
+ ctx.meta[META_KEY3] = { items: s.items, seen: s.items };
7009
+ return s.items;
7010
+ }
7011
+ function buildQueuedMessagesBlock(items) {
7012
+ const body = items.map((t2, i) => `${i + 1}. ${t2}`).join("\n");
7013
+ return [
7014
+ "[QUEUED MESSAGES \u2014 the user typed these while you were working. They are",
7015
+ "waiting in the input queue and each will be delivered as its own turn",
7016
+ "after you finish, so do NOT answer or act on them as new tasks now. Read",
7017
+ "them as a heads-up: if one changes what you should be doing right now",
7018
+ "(new constraint, invalidated approach), adapt your current work; if one",
7019
+ "is just an FYI, fold it in. The queue may still change before delivery.",
7020
+ "This notice replaces any earlier one \u2014 the queue now holds exactly:",
7021
+ "",
7022
+ body,
7023
+ "]"
7024
+ ].join("\n");
7025
+ }
7026
+
7027
+ // src/core/request-provider-binding.ts
7028
+ var requestProviders = /* @__PURE__ */ new WeakMap();
7029
+ function bindRequestProvider(request, provider) {
7030
+ requestProviders.set(request, provider);
7031
+ }
7032
+ function providerBoundToRequest(request) {
7033
+ return requestProviders.get(request);
7034
+ }
7035
+
7036
+ // src/core/agent-loop.ts
7037
+ function toError(err) {
7038
+ return err instanceof Error ? err : new Error(String(err));
7039
+ }
7040
+ function signalAbortReason(signal) {
7041
+ const r = signal.reason;
7042
+ if (r instanceof Error) return r.message || r.name;
7043
+ if (typeof r === "string" && r.length > 0) return r;
7044
+ return "aborted";
7045
+ }
7046
+ function createAgentLoopHandler(a, handlers) {
7047
+ const checkMailbox = attachMailboxChecker(a);
7048
+ const fleetPulseCfg = (() => {
7049
+ try {
7050
+ return typeof a.container?.has === "function" && a.container.has(TOKENS.ConfigStore) ? a.container.resolve(TOKENS.ConfigStore).get().fleet?.pulse : void 0;
7051
+ } catch {
7052
+ return void 0;
7053
+ }
7054
+ })();
7055
+ const getFleetPulse = attachFleetPulse(a, fleetPulseCfg);
7056
+ const pulseEveryN = Math.max(1, fleetPulseCfg?.everyNIterations ?? 5);
7057
+ const backgroundCoordination = () => a.ctx.meta["coordinationContextMode"] === "background";
7058
+ const loopContext = createAgentLoopContextManager(a, handlers);
6899
7059
  function foldBlockIntoConversation(block) {
6900
7060
  if (!a.ctx.state.appendBlockToLastUserMessage(block)) {
6901
7061
  a.ctx.state.appendMessage({ role: "user", content: [block], origin: "runtime" });
6902
7062
  }
6903
7063
  }
6904
- function iterationFingerprint(blocks) {
6905
- const toolUses = blocks.filter(isToolUseBlock);
6906
- const texts = blocks.filter(isTextBlock);
6907
- const toolNameSet = Array.from(new Set(toolUses.map((u) => u.name))).sort();
6908
- const firstInputHash = toolUses[0] ? hashSmall(stableStringify(toolUses[0].input ?? {})) : "";
6909
- const textBlob = texts.map((t2) => t2.text).join("").slice(0, 512);
6910
- const hasContent = toolNameSet.length > 0 || textBlob.length > 0;
6911
- if (!hasContent) return "__empty__";
6912
- return [
6913
- `tools=${toolNameSet.join("+") || "-"}`,
6914
- `in0=${firstInputHash}`,
6915
- `txt=${textBlob}`
6916
- ].join("\n");
6917
- }
6918
- function stableStringify(value) {
6919
- return JSON.stringify(canonicalize(value));
6920
- }
6921
- function canonicalize(value) {
6922
- if (Array.isArray(value)) return value.map(canonicalize);
6923
- if (value && typeof value === "object") {
6924
- const src = value;
6925
- const out = {};
6926
- for (const k of Object.keys(src).sort()) out[k] = canonicalize(src[k]);
6927
- return out;
6928
- }
6929
- return value;
6930
- }
6931
- function hashSmall(s) {
6932
- let h = 2166136261;
6933
- for (let i = 0; i < s.length; i++) {
6934
- h ^= s.charCodeAt(i);
6935
- h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
6936
- }
6937
- return h.toString(36);
6938
- }
6939
7064
  function injectPendingBtwNotes(onMailboxBlock) {
6940
7065
  const notes = consumeBtwNotes(a.ctx);
6941
7066
  if (notes.length === 0) return;
@@ -7015,15 +7140,10 @@ function createAgentLoopHandler(a, handlers) {
7015
7140
  a.ctx.state.replaceMessages(cleaned.messages);
7016
7141
  a.ctx.lastRealInputTokens = void 0;
7017
7142
  delete a.ctx.meta["realAnchorMsgCount"];
7018
- refreshContextRequestTokenStash({ force: true });
7143
+ loopContext.refreshContextRequestTokenStash({ force: true });
7019
7144
  }
7020
7145
  }
7021
- const loopCfg = a.loopDetection;
7022
- let lastToolSignature = "";
7023
- let toolLoopCount = 0;
7024
- let iterationSteerDone = false;
7025
- const recentCallKeys = [];
7026
- const steeredCallKeys = /* @__PURE__ */ new Set();
7146
+ const loopDetector = new AgentLoopDetector(a);
7027
7147
  let pendingLoopSteer = null;
7028
7148
  let todoReconcileSteers = 0;
7029
7149
  function queueLoopSteer(text) {
@@ -7130,7 +7250,7 @@ ${text}` : text;
7130
7250
  req,
7131
7251
  provider: requestProvider,
7132
7252
  preFlight
7133
- } = await buildRequestWithPreflightCompaction(opts);
7253
+ } = await loopContext.buildRequestWithPreflightCompaction(opts);
7134
7254
  await a.ctx.session.append({
7135
7255
  type: "llm_request",
7136
7256
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7143,7 +7263,7 @@ ${text}` : text;
7143
7263
  let res;
7144
7264
  try {
7145
7265
  res = await customRunner(a.ctx, req);
7146
- const key = calibrationKey(req.model);
7266
+ const key = loopContext.calibrationKey(req.model);
7147
7267
  const cal = getCalibrationState(key);
7148
7268
  const calibratedTotal = cal.calibrated ? Math.round(preFlight.total * Math.min(1.5, Math.max(0.5, cal.ratio))) : preFlight.total;
7149
7269
  const realInputTokens = effectiveInputTokens(res.usage);
@@ -7151,10 +7271,10 @@ ${text}` : text;
7151
7271
  const previousRealInput = a.ctx.lastRealInputTokens;
7152
7272
  const previousAnchorMsgCount = typeof a.ctx.meta?.["realAnchorMsgCount"] === "number" ? a.ctx.meta["realAnchorMsgCount"] : void 0;
7153
7273
  const anchorIsPlausible = realInputTokens >= calibratedTotal * 0.5;
7154
- const anchorAdvanced = previousRealInput === void 0 || realInputTokens > previousRealInput || previousAnchorMsgCount !== void 0 && _lastPreFlightMsgCount < previousAnchorMsgCount;
7274
+ const anchorAdvanced = previousRealInput === void 0 || realInputTokens > previousRealInput || previousAnchorMsgCount !== void 0 && loopContext.lastPreFlightMsgCount < previousAnchorMsgCount;
7155
7275
  if (realInputTokens > 0 && anchorIsPlausible && anchorAdvanced) {
7156
7276
  a.ctx.lastRealInputTokens = realInputTokens;
7157
- a.ctx.meta["realAnchorMsgCount"] = _lastPreFlightMsgCount;
7277
+ a.ctx.meta["realAnchorMsgCount"] = loopContext.lastPreFlightMsgCount;
7158
7278
  }
7159
7279
  recoveryRetries = 0;
7160
7280
  } catch (err) {
@@ -7244,7 +7364,9 @@ ${text}` : text;
7244
7364
  clearEvaluatedMailboxBlocks();
7245
7365
  const responseProvider = providerBoundToRequest(req) ?? requestProvider;
7246
7366
  const responseResult = await handlers.response.processResponse(res, req, responseProvider);
7247
- await refreshProviderContextLimit(responseProvider, req.model, { probe: false });
7367
+ await loopContext.refreshProviderContextLimit(responseProvider, req.model, {
7368
+ probe: false
7369
+ });
7248
7370
  if (responseResult.finalText) {
7249
7371
  a.ctx.meta["lastAgentOutput"] = responseResult.finalText;
7250
7372
  }
@@ -7267,103 +7389,18 @@ ${text}` : text;
7267
7389
  }
7268
7390
  finalText = responseResult.finalText;
7269
7391
  const toolUses = res.content.filter(isToolUseBlock);
7270
- if (loopCfg.mode !== "off") {
7271
- const sig = iterationFingerprint(res.content);
7272
- if (sig !== "__empty__") {
7273
- if (sig === lastToolSignature) {
7274
- toolLoopCount++;
7275
- } else {
7276
- lastToolSignature = sig;
7277
- toolLoopCount = 1;
7278
- iterationSteerDone = false;
7279
- }
7280
- const names = toolUses.map((t2) => t2.name).join(", ");
7281
- const hasText2 = res.content.some(isTextBlock);
7282
- const kind = toolUses.length > 0 && hasText2 ? "mixed" : toolUses.length > 0 ? "tool" : "message";
7283
- const observationRepeat = loopCfg.mode === "steer-then-cut" && toolUses.length > 0 && toolUses.every((use) => {
7284
- const tool = a.tools.get(use.name);
7285
- return tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0);
7286
- });
7287
- const repeatMultiplier = observationRepeat ? 2 : 1;
7288
- const detail = kind === "tool" ? `"${names}" called with effectively identical inputs ${toolLoopCount} times in a row` : kind === "mixed" ? `"${names}" + same text repeated ${toolLoopCount} times in a row` : `same assistant text repeated ${toolLoopCount} times in a row`;
7289
- const cutAt = (loopCfg.mode === "cut" ? loopCfg.steerThreshold : loopCfg.cutThreshold) * repeatMultiplier;
7290
- if (toolLoopCount >= cutAt) {
7291
- a.logger.warn(`Loop detected: ${detail} \u2014 stopping to prevent infinite loop.`);
7292
- a.events.emit("tool.loop_detected", {
7293
- sessionId: resolveEventSessionId(a.ctx),
7294
- ctx: a.ctx,
7295
- tools: names,
7296
- repeatCount: toolLoopCount,
7297
- iteration: i,
7298
- kind,
7299
- action: "cut",
7300
- scope: "iteration"
7301
- });
7302
- const summary = kind === "message" ? `[Loop detected: same assistant message repeated ${toolLoopCount}\xD7 \u2014 stopping to prevent infinite repetition.]` : `[Loop detected: ${detail} \u2014 stopping to prevent infinite repetition.]`;
7303
- return {
7304
- status: "max_iterations",
7305
- iterations,
7306
- finalText: finalText || summary,
7307
- delegateSummaries
7308
- };
7309
- }
7310
- if (loopCfg.mode === "steer-then-cut" && toolLoopCount >= loopCfg.steerThreshold * repeatMultiplier && !iterationSteerDone) {
7311
- iterationSteerDone = true;
7312
- a.logger.warn(`Loop detected: ${detail} \u2014 steering the model to change approach.`);
7313
- a.events.emit("tool.loop_detected", {
7314
- sessionId: resolveEventSessionId(a.ctx),
7315
- ctx: a.ctx,
7316
- tools: names,
7317
- repeatCount: toolLoopCount,
7318
- iteration: i,
7319
- kind,
7320
- action: "steer",
7321
- scope: "iteration"
7322
- });
7323
- queueLoopSteer(
7324
- `[loop-detector] Your last ${toolLoopCount} responses were effectively identical (${detail}). This approach is not working. Change strategy: use a different tool, different arguments, or a different plan \u2014 or explain what is blocking you and stop. Repeating the same response ${cutAt - toolLoopCount} more time(s) will terminate the turn.`
7325
- );
7326
- }
7327
- } else {
7328
- lastToolSignature = "";
7329
- toolLoopCount = 0;
7330
- iterationSteerDone = false;
7331
- }
7332
- if (loopCfg.mode === "steer-then-cut") {
7333
- for (const u of toolUses) {
7334
- const key = `${u.name}:${hashSmall(stableStringify(u.input ?? {}))}`;
7335
- recentCallKeys.push(key);
7336
- if (recentCallKeys.length > loopCfg.windowSize) recentCallKeys.shift();
7337
- if (steeredCallKeys.has(key)) continue;
7338
- let count = 0;
7339
- for (const k of recentCallKeys) if (k === key) count++;
7340
- const tool = a.tools.get(u.name);
7341
- const observationThreshold = tool?.mutating === false && (tool.riskTier === "safe" || (tool.capabilities?.length ?? 0) > 0) ? loopCfg.callRepeatThreshold * 2 : loopCfg.callRepeatThreshold;
7342
- if (count < observationThreshold) continue;
7343
- steeredCallKeys.add(key);
7344
- const preview2 = JSON.stringify(u.input ?? {}).slice(0, 160);
7345
- a.logger.warn(
7346
- `Loop detected: "${u.name}" called with identical arguments ${count}\xD7 within the last ${loopCfg.windowSize} tool calls \u2014 steering the model to change approach.`
7347
- );
7348
- a.events.emit("tool.loop_detected", {
7349
- sessionId: resolveEventSessionId(a.ctx),
7350
- ctx: a.ctx,
7351
- tools: u.name,
7352
- repeatCount: count,
7353
- iteration: i,
7354
- kind: "tool",
7355
- action: "steer",
7356
- scope: "call"
7357
- });
7358
- queueLoopSteer(
7359
- `[loop-detector] You have called ${u.name}(${preview2}) ${count} times with identical arguments within the last ${loopCfg.windowSize} tool calls. The result will not change. Do not repeat this call \u2014 use what you already know, try a different approach, or explain the blocker.`
7360
- );
7361
- }
7362
- }
7392
+ const loopCheck = loopDetector.checkIteration(i, res.content, toolUses, queueLoopSteer);
7393
+ if (loopCheck.cut) {
7394
+ return {
7395
+ status: "max_iterations",
7396
+ iterations,
7397
+ finalText: finalText || loopCheck.cutSummary || "",
7398
+ delegateSummaries
7399
+ };
7363
7400
  }
7364
7401
  if (toolUses.length === 0) {
7365
- await compactContextIfNeeded();
7366
- emitContextPct();
7402
+ await loopContext.compactContextIfNeeded();
7403
+ loopContext.emitContextPct();
7367
7404
  a.events.emit("iteration.completed", {
7368
7405
  sessionId: resolveEventSessionId(a.ctx),
7369
7406
  ctx: a.ctx,
@@ -7401,8 +7438,8 @@ ${text}` : text;
7401
7438
  throw toolErr;
7402
7439
  }
7403
7440
  if (autonomousContinue && consumeAutonomousContinue(a.ctx)) {
7404
- await compactContextIfNeeded();
7405
- emitContextPct();
7441
+ await loopContext.compactContextIfNeeded();
7442
+ loopContext.emitContextPct();
7406
7443
  a.events.emit("iteration.completed", {
7407
7444
  sessionId: resolveEventSessionId(a.ctx),
7408
7445
  ctx: a.ctx,
@@ -7411,8 +7448,8 @@ ${text}` : text;
7411
7448
  await a.extensions.runAfterIteration(a.ctx, i);
7412
7449
  continue;
7413
7450
  }
7414
- await compactContextIfNeeded();
7415
- emitContextPct();
7451
+ await loopContext.compactContextIfNeeded();
7452
+ loopContext.emitContextPct();
7416
7453
  a.events.emit("iteration.completed", {
7417
7454
  sessionId: resolveEventSessionId(a.ctx),
7418
7455
  ctx: a.ctx,
@@ -7611,6 +7648,9 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7611
7648
  "codebase-incoming-calls",
7612
7649
  "codebase-outgoing-calls",
7613
7650
  "codebase-index",
7651
+ "codebase-skeleton",
7652
+ "codebase-repo-map",
7653
+ "codebase-impact-analysis",
7614
7654
  "dead-code-scan",
7615
7655
  "diff",
7616
7656
  "json",
@@ -7621,7 +7661,7 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7621
7661
  id: "filesystem.write",
7622
7662
  pack: "development",
7623
7663
  exposure: "direct",
7624
- tools: ["write", "edit", "replace", "patch"]
7664
+ tools: ["write", "edit", "replace", "patch", "codebase-ast-replace"]
7625
7665
  },
7626
7666
  {
7627
7667
  id: "execution.shell",
@@ -7633,7 +7673,7 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7633
7673
  id: "verification.run",
7634
7674
  pack: "development",
7635
7675
  exposure: "direct",
7636
- tools: ["lint", "format", "typecheck", "test", "e2e_plan"]
7676
+ tools: ["lint", "format", "typecheck", "test", "e2e_plan", "codebase-targeted-test"]
7637
7677
  },
7638
7678
  {
7639
7679
  id: "version-control.manage",
@@ -7770,7 +7810,13 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7770
7810
  id: "quality.analyze",
7771
7811
  pack: "development",
7772
7812
  exposure: "on-demand",
7773
- tools: ["dead_code_scan", "detect_duplicate_code", "secret_scanner_test", "error_lens_history"]
7813
+ tools: [
7814
+ "dead_code_scan",
7815
+ "detect_duplicate_code",
7816
+ "secret_scanner_test",
7817
+ "error_lens_history",
7818
+ "security-ast-scan"
7819
+ ]
7774
7820
  },
7775
7821
  {
7776
7822
  id: "release.manage",
@@ -7788,7 +7834,7 @@ var RUNTIME_CAPABILITY_MANIFEST = [
7788
7834
  id: "tools.discover",
7789
7835
  pack: "admin",
7790
7836
  exposure: "direct",
7791
- tools: ["tool_search", "tool_use", "tool_help", "batch_tool_use"]
7837
+ tools: ["tool_search", "tool_use", "tool_help", "batch_tool_use", "clarify"]
7792
7838
  },
7793
7839
  {
7794
7840
  id: "runtime.admin",
@@ -8228,7 +8274,7 @@ function createAgentResponseHandler(a) {
8228
8274
  ...repaired.report
8229
8275
  });
8230
8276
  a.logger.warn(
8231
- `Repaired context tool adjacency: removed ${repaired.report.removedToolUses.length} tool_use block(s), ${repaired.report.removedToolResults.length} tool_result block(s), ${repaired.report.removedMessages} empty message(s)`
8277
+ `Repaired context tool adjacency: removed ${repaired.report.removedToolUses.length} tool_use block(s), ${repaired.report.removedToolResults.length} tool_result block(s), ${repaired.report.removedMessages} empty message(s)` + formatAdjacencyRepairIds(repaired.report.removedToolUses, repaired.report.removedToolResults)
8232
8278
  );
8233
8279
  }
8234
8280
  }
@@ -8348,6 +8394,10 @@ function createAgentResponseHandler(a) {
8348
8394
  }
8349
8395
  return { buildAndRunRequestPipeline, processResponse };
8350
8396
  }
8397
+ function formatAdjacencyRepairIds(toolUses, toolResults) {
8398
+ const ids = [.../* @__PURE__ */ new Set([...toolUses, ...toolResults])].slice(0, 8);
8399
+ return ids.length > 0 ? `; affected tool ids: ${ids.join(", ")}${toolUses.length + toolResults.length > ids.length ? ", \u2026" : ""}` : "";
8400
+ }
8351
8401
 
8352
8402
  // src/utils/sage-output-block.ts
8353
8403
  var SAGE_INJECTOR_HEADINGS = /* @__PURE__ */ new Set([
@@ -8919,7 +8969,7 @@ var Agent = class {
8919
8969
  this.maxIterations = init.maxIterations ?? DEFAULT_MAX_ITERATIONS;
8920
8970
  this.executionStrategy = init.executionStrategy ?? "smart";
8921
8971
  this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
8922
- this.autoExtendLimit = init.autoExtendLimit ?? true;
8972
+ this.autoExtendLimit = init.autoExtendLimit ?? false;
8923
8973
  this.loopDetection = resolveLoopDetection(init.loopDetection);
8924
8974
  this.autonomousContinue = init.autonomousContinue ?? false;
8925
8975
  this.refreshSystemPrompt = init.refreshSystemPrompt ?? false;
@@ -9286,9 +9336,6 @@ function resolveContinuation(input) {
9286
9336
  return { source: "open", text, label: "\u25B6 Continue \u2192 (no pending task \u2014 choosing next step)" };
9287
9337
  }
9288
9338
 
9289
- // src/core/fallback-model.ts
9290
- import { randomUUID as randomUUID6 } from "node:crypto";
9291
-
9292
9339
  // src/core/model-availability-calendar.ts
9293
9340
  function logicalCalendarTarget(providerId, model) {
9294
9341
  if (providerId !== "omniroute") return { providerId, model };
@@ -9776,7 +9823,206 @@ function dedupeChain(entries, current) {
9776
9823
  );
9777
9824
  }
9778
9825
 
9826
+ // src/core/fallback-doctor.ts
9827
+ function diagnoseFallbackConfig(config, tracker, opts) {
9828
+ const mgr = new FallbackProfileManager(config, { statusTracker: tracker });
9829
+ const primary = { providerId: config.provider, model: config.model };
9830
+ const activeProfile = mgr.activeProfileName();
9831
+ const explicitChain = Object.freeze([...config.fallbackModels ?? []]);
9832
+ const effectiveCandidates = mgr.resolveCandidates(primary, {
9833
+ fallbackProfile: activeProfile
9834
+ });
9835
+ const effectiveOrder = Object.freeze(
9836
+ effectiveCandidates.map((c) => `${c.providerId}/${c.model}`)
9837
+ );
9838
+ const autoEnabled = config.fallbackAuto !== false;
9839
+ const warnings = [];
9840
+ const providersInChain = /* @__PURE__ */ new Set();
9841
+ for (const entry of effectiveCandidates) {
9842
+ providersInChain.add(entry.providerId);
9843
+ }
9844
+ if (effectiveCandidates.length === 0) {
9845
+ warnings.push({
9846
+ code: "EMPTY_CHAIN",
9847
+ severity: "critical",
9848
+ message: "The effective fallback chain is empty. Any rate limit or outage on the primary model will immediately fail without recovery.",
9849
+ recommendation: "Add fallback models using /fallback add <provider/model> or enable smart defaults with /fallback auto on."
9850
+ });
9851
+ }
9852
+ if (effectiveCandidates.length > 0) {
9853
+ const onlyPrimaryProvider = providersInChain.size === 1 && providersInChain.has(primary.providerId);
9854
+ if (onlyPrimaryProvider) {
9855
+ warnings.push({
9856
+ code: "SIBLING_QUARANTINE_RISK",
9857
+ severity: "warning",
9858
+ message: `All fallback models belong to the same provider ("${primary.providerId}"). An account-level quota or token exhaustion will quarantine all sibling models at once.`,
9859
+ recommendation: "Add at least one cross-provider model (e.g. OpenAI, Anthropic, or Google) to your fallback profile."
9860
+ });
9861
+ }
9862
+ }
9863
+ for (const entry of effectiveCandidates) {
9864
+ const health = mgr.checkProvider(entry.providerId);
9865
+ if (!health.usable) {
9866
+ warnings.push({
9867
+ code: "PROVIDER_UNUSABLE",
9868
+ severity: "critical",
9869
+ target: `${entry.providerId}/${entry.model}`,
9870
+ message: `Provider "${entry.providerId}" is missing an API key or baseUrl in configuration.`,
9871
+ recommendation: `Configure API credentials for "${entry.providerId}" or remove it from the fallback chain.`
9872
+ });
9873
+ }
9874
+ if (tracker && !tracker.isAvailable(entry.providerId, entry.model)) {
9875
+ const status2 = tracker.getStatus(entry.providerId, entry.model);
9876
+ const remainingMs = status2?.stateExpiresAt ? Math.max(0, status2.stateExpiresAt - Date.now()) : 0;
9877
+ const remainingSec = Math.round(remainingMs / 1e3);
9878
+ warnings.push({
9879
+ code: "MODEL_QUARANTINED",
9880
+ severity: "warning",
9881
+ target: `${entry.providerId}/${entry.model}`,
9882
+ message: `Model is currently in quarantine (waiting room) due to ${status2?.lastErrorKind ?? "failures"}.${remainingSec > 0 ? ` Resets in ${remainingSec}s.` : ""}`,
9883
+ recommendation: "Model will be automatically re-admitted once cooldown/reset expires."
9884
+ });
9885
+ }
9886
+ const cal = evaluateModelCalendar(
9887
+ config.modelAvailabilitySchedule,
9888
+ entry.providerId,
9889
+ entry.model
9890
+ );
9891
+ if (!cal.allowed) {
9892
+ warnings.push({
9893
+ code: "CALENDAR_BLOCKED",
9894
+ severity: "warning",
9895
+ target: `${entry.providerId}/${entry.model}`,
9896
+ message: `Model is blocked by the model availability calendar (${cal.rule?.label ?? "schedule restriction"}).`
9897
+ });
9898
+ }
9899
+ if (opts?.modelContextLimitMap) {
9900
+ const primaryLimit = opts.modelContextLimitMap[`${primary.providerId}/${primary.model}`] ?? opts.modelContextLimitMap[primary.model];
9901
+ const entryLimit = opts.modelContextLimitMap[`${entry.providerId}/${entry.model}`] ?? opts.modelContextLimitMap[entry.model];
9902
+ if (typeof primaryLimit === "number" && typeof entryLimit === "number" && entryLimit > 0 && primaryLimit > 0 && entryLimit < primaryLimit) {
9903
+ warnings.push({
9904
+ code: "CONTEXT_WINDOW_WARNING",
9905
+ severity: "warning",
9906
+ target: `${entry.providerId}/${entry.model}`,
9907
+ message: `Context window downgrade: primary model has ${primaryLimit.toLocaleString()} tokens capacity, but fallback candidate has only ${entryLimit.toLocaleString()} tokens. Long sessions will fail failover or require aggressive compaction.`,
9908
+ recommendation: `Ensure fallback models have equal or greater context capacity (>= ${primaryLimit.toLocaleString()} tokens) or keep sessions below ${entryLimit.toLocaleString()} tokens.`
9909
+ });
9910
+ }
9911
+ }
9912
+ }
9913
+ const uniqueProviders = Object.freeze([...providersInChain]);
9914
+ const crossProviderCount = uniqueProviders.filter((p) => p !== primary.providerId).length;
9915
+ const hasCritical = warnings.some((w) => w.severity === "critical");
9916
+ const hasWarning = warnings.some((w) => w.severity === "warning");
9917
+ const status = hasCritical ? "critical" : hasWarning ? "warning" : "healthy";
9918
+ return Object.freeze({
9919
+ primary: Object.freeze(primary),
9920
+ activeProfile,
9921
+ explicitChain,
9922
+ effectiveOrder,
9923
+ autoEnabled,
9924
+ status,
9925
+ warnings: Object.freeze(warnings),
9926
+ crossProviderCount,
9927
+ uniqueProviders
9928
+ });
9929
+ }
9930
+ function simulateFallbackFailover(config, opts = {}) {
9931
+ const errorKind = opts.errorKind ?? "rate_limit";
9932
+ const isEligible = isFallbackWorthy(errorKind);
9933
+ const primary = { providerId: config.provider, model: config.model };
9934
+ if (!isEligible) {
9935
+ return Object.freeze({
9936
+ triggeringError: errorKind,
9937
+ isFallbackEligible: false,
9938
+ primary: Object.freeze(primary),
9939
+ steps: Object.freeze([]),
9940
+ finalTarget: null,
9941
+ summary: `Error kind "${errorKind}" is not fallback-worthy (request-shaped error, e.g. context_overflow/auth). The agent loop will not attempt fallback.`
9942
+ });
9943
+ }
9944
+ const mgr = new FallbackProfileManager(config, { statusTracker: opts.statusTracker });
9945
+ const candidates = mgr.resolveCandidates(primary);
9946
+ const steps = [];
9947
+ let finalTarget = null;
9948
+ for (let i = 0; i < candidates.length; i++) {
9949
+ const entry = candidates[i];
9950
+ const key = `${entry.providerId}/${entry.model}`;
9951
+ const providerSwitched = entry.providerId !== primary.providerId;
9952
+ const cal = evaluateModelCalendar(
9953
+ config.modelAvailabilitySchedule,
9954
+ entry.providerId,
9955
+ entry.model
9956
+ );
9957
+ if (!cal.allowed) {
9958
+ steps.push({
9959
+ index: i + 1,
9960
+ providerId: entry.providerId,
9961
+ model: entry.model,
9962
+ skipped: true,
9963
+ reason: `Blocked by model availability calendar (${cal.rule?.label ?? "schedule"})`,
9964
+ providerSwitched
9965
+ });
9966
+ continue;
9967
+ }
9968
+ if (opts.statusTracker && !opts.statusTracker.isAvailable(entry.providerId, entry.model)) {
9969
+ steps.push({
9970
+ index: i + 1,
9971
+ providerId: entry.providerId,
9972
+ model: entry.model,
9973
+ skipped: true,
9974
+ reason: "Quarantined in status tracker waiting room",
9975
+ providerSwitched
9976
+ });
9977
+ continue;
9978
+ }
9979
+ const contextLimit = opts.modelContextLimitMap?.[key];
9980
+ if (typeof opts.currentTokens === "number" && opts.currentTokens > 0 && typeof contextLimit === "number" && contextLimit > 0 && opts.currentTokens > contextLimit) {
9981
+ steps.push({
9982
+ index: i + 1,
9983
+ providerId: entry.providerId,
9984
+ model: entry.model,
9985
+ skipped: true,
9986
+ reason: `Context pre-filter: request tokens (${opts.currentTokens}) exceeds model window (${contextLimit})`,
9987
+ providerSwitched
9988
+ });
9989
+ continue;
9990
+ }
9991
+ const health = mgr.checkProvider(entry.providerId);
9992
+ if (!health.usable) {
9993
+ steps.push({
9994
+ index: i + 1,
9995
+ providerId: entry.providerId,
9996
+ model: entry.model,
9997
+ skipped: true,
9998
+ reason: `Cannot construct provider "${entry.providerId}" (missing API key/endpoint)`,
9999
+ providerSwitched
10000
+ });
10001
+ continue;
10002
+ }
10003
+ steps.push({
10004
+ index: i + 1,
10005
+ providerId: entry.providerId,
10006
+ model: entry.model,
10007
+ skipped: false,
10008
+ providerSwitched
10009
+ });
10010
+ finalTarget = { providerId: entry.providerId, model: entry.model };
10011
+ break;
10012
+ }
10013
+ const summary = finalTarget ? `Failover succeeded: will rotate from ${primary.providerId}/${primary.model} to ${finalTarget.providerId}/${finalTarget.model}.` : "Failover failed: no usable fallback candidate could be reached.";
10014
+ return Object.freeze({
10015
+ triggeringError: errorKind,
10016
+ isFallbackEligible: true,
10017
+ primary: Object.freeze(primary),
10018
+ steps: Object.freeze(steps),
10019
+ finalTarget: finalTarget ? Object.freeze(finalTarget) : null,
10020
+ summary
10021
+ });
10022
+ }
10023
+
9779
10024
  // src/core/fallback-model.ts
10025
+ import { randomUUID as randomUUID6 } from "node:crypto";
9780
10026
  function fallbackProfileChain(config, profileName) {
9781
10027
  if (!profileName) return [];
9782
10028
  const mgr = new FallbackProfileManager(config);
@@ -9911,7 +10157,25 @@ function createFallbackModelExtension(deps) {
9911
10157
  if (!evaluateModelCalendar(cfg.modelAvailabilitySchedule, primary.providerId, primary.model).allowed || deps.statusTracker && !deps.statusTracker.isAvailable(primary.providerId, primary.model))
9912
10158
  return;
9913
10159
  try {
9914
- ctx.provider = await deps.buildProvider(primary.providerId, primary.model);
10160
+ const primaryProvider = await deps.buildProvider(primary.providerId, primary.model);
10161
+ const currentTokens = ctx.lastRequestTokens;
10162
+ const maxContext = maxContextOf(primaryProvider);
10163
+ if (maxContext > 0 && typeof currentTokens === "number" && currentTokens > maxContext) {
10164
+ deps.events.emit("provider.primary_probe_context_blocked", {
10165
+ sessionId: resolveEventSessionId(ctx),
10166
+ providerId: primary.providerId,
10167
+ model: primary.model,
10168
+ currentTokens,
10169
+ maxContext,
10170
+ timestamp: now()
10171
+ });
10172
+ deps.logger?.warn(
10173
+ `fallback-model: deferring primary probe "${primary.providerId}/${primary.model}" \u2014 context (${currentTokens}) exceeds target window (${maxContext})`
10174
+ );
10175
+ markPrimaryFailure(cfg);
10176
+ return;
10177
+ }
10178
+ ctx.provider = primaryProvider;
9915
10179
  ctx.model = primary.model;
9916
10180
  await deps.onModelSwitch?.(primary.providerId, primary.model);
9917
10181
  primaryBlockedUntil = 0;
@@ -10025,9 +10289,10 @@ function createFallbackModelExtension(deps) {
10025
10289
  const status = shouldFallback(firstErr_);
10026
10290
  if (status === null) throw firstErr_;
10027
10291
  let gateRequestId;
10028
- if (deps.fallbackGate && usableChain.length > 0) {
10292
+ const configuredGateSeconds = cfg.fallbackGateSeconds ?? deps.fallbackGateSeconds;
10293
+ if (configuredGateSeconds !== 0 && deps.fallbackGate && usableChain.length > 0) {
10029
10294
  gateRequestId = randomUUID6();
10030
- const autoSwitchSeconds = Math.max(1, deps.fallbackGateSeconds ?? 7);
10295
+ const autoSwitchSeconds = Math.max(1, configuredGateSeconds ?? 7);
10031
10296
  const gateCandidates = usableChain.map((e) => ({
10032
10297
  providerId: e.providerId,
10033
10298
  model: e.model
@@ -10854,6 +11119,7 @@ function shellGuidanceBlock(shell, detail) {
10854
11119
  var MAX_ENVIRONMENT_CACHE_ENTRIES = 16;
10855
11120
  async function buildEnvironment(ctx, env) {
10856
11121
  const modelCapabilities = env.modelCapabilities;
11122
+ const today = env.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10857
11123
  const cacheKey = [
10858
11124
  ctx.projectRoot,
10859
11125
  ctx.provider ?? "",
@@ -10862,7 +11128,9 @@ async function buildEnvironment(ctx, env) {
10862
11128
  modelCapabilities?.supportsTools ? 1 : 0,
10863
11129
  modelCapabilities?.supportsVision ? 1 : 0,
10864
11130
  modelCapabilities?.supportsReasoning ? 1 : 0,
10865
- env.skillCache ?? ""
11131
+ env.skillCache ?? "",
11132
+ today,
11133
+ env.modeId ?? ""
10866
11134
  ].join("\0");
10867
11135
  const cached = env.envCacheByRoot.get(cacheKey);
10868
11136
  if (cached) {
@@ -10870,7 +11138,6 @@ async function buildEnvironment(ctx, env) {
10870
11138
  env.envCacheByRoot.set(cacheKey, cached);
10871
11139
  return cached;
10872
11140
  }
10873
- const today = env.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10874
11141
  const platform2 = `${os3.platform()} ${os3.release()}`;
10875
11142
  const effShell = effectiveShell(os3.platform(), process.env["WRONGSTACK_SHELL"]);
10876
11143
  const shell = effShell === "posix" ? process.env.SHELL ?? process.env.ComSpec ?? "unknown" : SHELL_DISPLAY[effShell];
@@ -11716,7 +11983,9 @@ ${peers}
11716
11983
  const tpl = tplCtx ?? this.templateContext(ctx);
11717
11984
  const section = (key, vars = {}) => instructionSection(instructions, key, vars, tpl);
11718
11985
  const tier = this.tier;
11719
- if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.tier === tier) {
11986
+ const subagent = ctx.subagent === true;
11987
+ const maxContextTokens = this.modelCapabilities()?.maxContextTokens ?? 0;
11988
+ if (this._toolsUsageCache?.toolsRef === tools && this._toolsUsageCache?.tier === tier && this._toolsUsageCache?.subagent === subagent && this._toolsUsageCache?.maxContextTokens === maxContextTokens) {
11720
11989
  return this._toolsUsageCache.text;
11721
11990
  }
11722
11991
  const byCat = /* @__PURE__ */ new Map();
@@ -11843,7 +12112,7 @@ ${hint.trim()}`);
11843
12112
  }
11844
12113
  }
11845
12114
  const text = lines.join("\n");
11846
- this._toolsUsageCache = { toolsRef: tools, tier, text };
12115
+ this._toolsUsageCache = { toolsRef: tools, tier, subagent, maxContextTokens, text };
11847
12116
  return text;
11848
12117
  }
11849
12118
  renderOnlineAgents(agents) {
@@ -11906,6 +12175,7 @@ export {
11906
12175
  createDefaultPipelines,
11907
12176
  createFallbackModelExtension,
11908
12177
  detectContinueIntent,
12178
+ diagnoseFallbackConfig,
11909
12179
  effectiveFallbackChain,
11910
12180
  fallbackProfileChain,
11911
12181
  formatModelRef,
@@ -11924,6 +12194,7 @@ export {
11924
12194
  runtimeFallbackChain,
11925
12195
  setBtwNote,
11926
12196
  setQueuedMessagesSnapshot,
12197
+ simulateFallbackFailover,
11927
12198
  smartDefaultFallbackChain,
11928
12199
  wrapAsState,
11929
12200
  writePendingNextSteps