@probelabs/probe 0.6.0-rc294 → 0.6.0-rc296

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 (31) hide show
  1. package/README.md +7 -0
  2. package/bin/binaries/{probe-v0.6.0-rc294-aarch64-apple-darwin.tar.gz → probe-v0.6.0-rc296-aarch64-apple-darwin.tar.gz} +0 -0
  3. package/bin/binaries/{probe-v0.6.0-rc294-aarch64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc296-aarch64-unknown-linux-musl.tar.gz} +0 -0
  4. package/bin/binaries/{probe-v0.6.0-rc294-x86_64-apple-darwin.tar.gz → probe-v0.6.0-rc296-x86_64-apple-darwin.tar.gz} +0 -0
  5. package/bin/binaries/{probe-v0.6.0-rc294-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc296-x86_64-pc-windows-msvc.zip} +0 -0
  6. package/bin/binaries/{probe-v0.6.0-rc294-x86_64-unknown-linux-musl.tar.gz → probe-v0.6.0-rc296-x86_64-unknown-linux-musl.tar.gz} +0 -0
  7. package/build/agent/ProbeAgent.d.ts +10 -0
  8. package/build/agent/ProbeAgent.js +868 -29
  9. package/build/agent/mcp/client.js +81 -4
  10. package/build/agent/mcp/xmlBridge.js +11 -0
  11. package/build/agent/otelLogBridge.js +184 -0
  12. package/build/agent/simpleTelemetry.js +8 -0
  13. package/build/delegate.js +75 -6
  14. package/build/index.js +6 -2
  15. package/build/tools/common.js +84 -11
  16. package/build/tools/vercel.js +78 -18
  17. package/cjs/agent/ProbeAgent.cjs +1004 -48
  18. package/cjs/agent/simpleTelemetry.cjs +112 -0
  19. package/cjs/index.cjs +1116 -48
  20. package/index.d.ts +26 -0
  21. package/package.json +1 -1
  22. package/src/agent/ProbeAgent.d.ts +10 -0
  23. package/src/agent/ProbeAgent.js +868 -29
  24. package/src/agent/mcp/client.js +81 -4
  25. package/src/agent/mcp/xmlBridge.js +11 -0
  26. package/src/agent/otelLogBridge.js +184 -0
  27. package/src/agent/simpleTelemetry.js +8 -0
  28. package/src/delegate.js +75 -6
  29. package/src/index.js +6 -2
  30. package/src/tools/common.js +84 -11
  31. package/src/tools/vercel.js +78 -18
@@ -26106,8 +26106,15 @@ async function delegate({
26106
26106
  // Optional per-instance manager, falls back to default singleton
26107
26107
  concurrencyLimiter = null,
26108
26108
  // Optional global AI concurrency limiter
26109
- parentAbortSignal = null
26109
+ parentAbortSignal = null,
26110
26110
  // Optional AbortSignal from parent to cancel this delegation
26111
+ // Timeout settings inherited from parent agent
26112
+ timeoutBehavior = void 0,
26113
+ requestTimeout = void 0,
26114
+ gracefulTimeoutBonusSteps = void 0,
26115
+ // Subagent lifecycle callbacks for graceful stop coordination
26116
+ onSubagentCreated = null,
26117
+ onSubagentCompleted = null
26111
26118
  }) {
26112
26119
  if (!task || typeof task !== "string") {
26113
26120
  throw new Error("Task parameter is required and must be a string");
@@ -26190,12 +26197,38 @@ async function delegate({
26190
26197
  // Inherit from parent
26191
26198
  mcpConfigPath,
26192
26199
  // Inherit from parent
26193
- concurrencyLimiter
26200
+ concurrencyLimiter,
26194
26201
  // Inherit global AI concurrency limiter
26202
+ // Inherit timeout behavior from parent — subagent gets its own graceful wind-down
26203
+ // so it can produce partial results instead of being hard-killed by the external timer.
26204
+ // The external delegate timeout (capped to parent's remaining budget) is the hard limit;
26205
+ // maxOperationTimeout on the subagent is set slightly shorter so its own wind-down
26206
+ // fires before the external kill.
26207
+ maxOperationTimeout: Math.max(1e4, timeout * 1e3 - 15e3),
26208
+ // 15s before external kill
26209
+ timeoutBehavior: timeoutBehavior || "graceful",
26210
+ requestTimeout,
26211
+ gracefulTimeoutBonusSteps: gracefulTimeoutBonusSteps ?? 2
26212
+ // fewer steps for subagents
26195
26213
  });
26214
+ if (onSubagentCreated) {
26215
+ onSubagentCreated(sessionId, subagent);
26216
+ }
26196
26217
  if (debug) {
26197
26218
  console.error(`[DELEGATE] Created subagent with session ${sessionId}`);
26198
26219
  console.error(`[DELEGATE] Subagent config: promptType=${promptType}, enableDelegate=false, maxIterations=${remainingIterations}`);
26220
+ console.error(`[DELEGATE] Timeout inheritance: externalTimeout=${timeout}s, maxOperationTimeout=${Math.max(1e4, timeout * 1e3 - 15e3)}ms, behavior=${timeoutBehavior || "graceful"}, bonusSteps=${gracefulTimeoutBonusSteps ?? 2}`);
26221
+ }
26222
+ if (tracer) {
26223
+ tracer.addEvent("delegation.subagent_created", {
26224
+ "delegation.session_id": sessionId,
26225
+ "delegation.parent_session_id": parentSessionId,
26226
+ "delegation.external_timeout_s": timeout,
26227
+ "delegation.internal_timeout_ms": Math.max(1e4, timeout * 1e3 - 15e3),
26228
+ "delegation.timeout_behavior": timeoutBehavior || "graceful",
26229
+ "delegation.bonus_steps": gracefulTimeoutBonusSteps ?? 2,
26230
+ "delegation.max_iterations": remainingIterations
26231
+ });
26199
26232
  }
26200
26233
  const timeoutPromise = new Promise((_, reject2) => {
26201
26234
  timeoutId = setTimeout(() => {
@@ -26204,6 +26237,7 @@ async function delegate({
26204
26237
  }, timeout * 1e3);
26205
26238
  });
26206
26239
  let parentAbortHandler;
26240
+ let parentAbortHardCancelId = null;
26207
26241
  const parentAbortPromise = new Promise((_, reject2) => {
26208
26242
  if (parentAbortSignal) {
26209
26243
  if (parentAbortSignal.aborted) {
@@ -26212,8 +26246,31 @@ async function delegate({
26212
26246
  return;
26213
26247
  }
26214
26248
  parentAbortHandler = () => {
26215
- subagent.cancel();
26216
- reject2(new Error("Delegation cancelled: parent operation was aborted"));
26249
+ subagent.triggerGracefulWindDown();
26250
+ if (debug) {
26251
+ console.error(`[DELEGATE] Parent abort signal received \u2014 triggered graceful wind-down on subagent ${sessionId}`);
26252
+ }
26253
+ if (tracer) {
26254
+ tracer.addEvent("delegation.parent_abort_phase1", {
26255
+ "delegation.session_id": sessionId,
26256
+ "delegation.parent_session_id": parentSessionId,
26257
+ "delegation.action": "graceful_wind_down"
26258
+ });
26259
+ }
26260
+ parentAbortHardCancelId = setTimeout(() => {
26261
+ if (debug) {
26262
+ console.error(`[DELEGATE] Graceful wind-down deadline expired \u2014 hard cancelling subagent ${sessionId}`);
26263
+ }
26264
+ if (tracer) {
26265
+ tracer.addEvent("delegation.parent_abort_phase2", {
26266
+ "delegation.session_id": sessionId,
26267
+ "delegation.parent_session_id": parentSessionId,
26268
+ "delegation.action": "hard_cancel"
26269
+ });
26270
+ }
26271
+ subagent.cancel();
26272
+ reject2(new Error("Delegation cancelled: parent operation was aborted (graceful wind-down deadline expired)"));
26273
+ }, 3e4);
26217
26274
  };
26218
26275
  parentAbortSignal.addEventListener("abort", parentAbortHandler, { once: true });
26219
26276
  }
@@ -26229,6 +26286,13 @@ async function delegate({
26229
26286
  if (parentAbortHandler && parentAbortSignal) {
26230
26287
  parentAbortSignal.removeEventListener("abort", parentAbortHandler);
26231
26288
  }
26289
+ if (parentAbortHardCancelId) {
26290
+ clearTimeout(parentAbortHardCancelId);
26291
+ parentAbortHardCancelId = null;
26292
+ }
26293
+ if (onSubagentCompleted) {
26294
+ onSubagentCompleted(sessionId);
26295
+ }
26232
26296
  }
26233
26297
  if (timeoutId !== null) {
26234
26298
  clearTimeout(timeoutId);
@@ -27064,14 +27128,64 @@ function detectStuckResponse(response) {
27064
27128
  }
27065
27129
  return false;
27066
27130
  }
27131
+ function splitQuotedString(input) {
27132
+ const tokens = [];
27133
+ let current2 = "";
27134
+ let inQuote = null;
27135
+ let i = 0;
27136
+ while (i < input.length) {
27137
+ const ch = input[i];
27138
+ if (inQuote) {
27139
+ if (ch === "\\" && i + 1 < input.length) {
27140
+ current2 += input[i + 1];
27141
+ i += 2;
27142
+ continue;
27143
+ }
27144
+ if (ch === inQuote) {
27145
+ inQuote = null;
27146
+ i++;
27147
+ continue;
27148
+ }
27149
+ current2 += ch;
27150
+ i++;
27151
+ } else {
27152
+ if (ch === '"' || ch === "'") {
27153
+ inQuote = ch;
27154
+ i++;
27155
+ continue;
27156
+ }
27157
+ if (/[\s,]/.test(ch)) {
27158
+ if (current2.length > 0) {
27159
+ tokens.push(current2);
27160
+ current2 = "";
27161
+ }
27162
+ i++;
27163
+ continue;
27164
+ }
27165
+ current2 += ch;
27166
+ i++;
27167
+ }
27168
+ }
27169
+ if (current2.length > 0) {
27170
+ tokens.push(current2);
27171
+ }
27172
+ return tokens;
27173
+ }
27067
27174
  function parseTargets(targets) {
27068
27175
  if (!targets || typeof targets !== "string") {
27069
27176
  return [];
27070
27177
  }
27071
- return targets.split(/[\s,]+/).filter((f) => f.length > 0);
27178
+ return splitQuotedString(targets);
27072
27179
  }
27073
27180
  function parseAndResolvePaths(pathStr, cwd) {
27074
27181
  if (!pathStr) return [];
27182
+ if (/["']/.test(pathStr)) {
27183
+ const paths2 = splitQuotedString(pathStr);
27184
+ return paths2.map((p) => {
27185
+ if ((0, import_path5.isAbsolute)(p)) return p;
27186
+ return cwd ? (0, import_path5.resolve)(cwd, p) : p;
27187
+ });
27188
+ }
27075
27189
  let paths = pathStr.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
27076
27190
  paths = paths.flatMap((p) => {
27077
27191
  if (!/\s/.test(p)) return [p];
@@ -27081,9 +27195,7 @@ function parseAndResolvePaths(pathStr, cwd) {
27081
27195
  return allLookLikePaths ? parts : [p];
27082
27196
  });
27083
27197
  return paths.map((p) => {
27084
- if ((0, import_path5.isAbsolute)(p)) {
27085
- return p;
27086
- }
27198
+ if ((0, import_path5.isAbsolute)(p)) return p;
27087
27199
  return cwd ? (0, import_path5.resolve)(cwd, p) : p;
27088
27200
  });
27089
27201
  }
@@ -27116,7 +27228,7 @@ var init_common = __esm({
27116
27228
  searchSchema = external_exports2.object({
27117
27229
  query: external_exports2.string().describe("Search query \u2014 natural language questions or Elasticsearch-style keywords both work. For keywords: use quotes for exact phrases, AND/OR for boolean logic, - for negation. Probe handles stemming and camelCase/snake_case splitting automatically, so do NOT try case or style variations of the same keyword."),
27118
27230
  path: external_exports2.string().optional().default(".").describe('Path to search in. For dependencies use "go:github.com/owner/repo", "js:package_name", or "rust:cargo_name" etc.'),
27119
- exact: external_exports2.boolean().optional().default(false).describe('Default (false) enables stemming and keyword splitting for exploratory search - "getUserData" matches "get", "user", "data", etc. Set true for precise symbol lookup where "getUserData" matches only "getUserData". Use true when you know the exact symbol name.'),
27231
+ exact: external_exports2.boolean().optional().default(false).describe(`Default (false) enables stemming and keyword splitting for exploratory search - "getUserData" matches "get", "user", "data", etc. Set true for precise symbol lookup OR when searching for strings with punctuation/quotes/empty values (e.g. 'description: ""' \u2014 BM25 strips punctuation so exact=true is required for literal matching). Use true when you know the exact symbol name or need literal string matching.`),
27120
27232
  maxTokens: external_exports2.number().nullable().optional().describe("Maximum tokens to return. Default is 20000. Set to null for unlimited results."),
27121
27233
  session: external_exports2.string().optional().describe("Session ID for result caching and pagination. Pass the session ID from a previous search to get additional results (next page). Results already shown in a session are automatically excluded. Omit for a fresh search."),
27122
27234
  nextPage: external_exports2.boolean().optional().default(false).describe("Set to true when requesting the next page of results. Requires passing the same session ID from the previous search output.")
@@ -27427,6 +27539,10 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27427
27539
  "- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).",
27428
27540
  "- exact=true matches the literal string only \u2014 no stemming, no splitting.",
27429
27541
  '- This is ideal for precise lookups: exact=true "ForwardMessage", exact=true "SessionLimiter", exact=true "ThrottleRetryLimit".',
27542
+ "- IMPORTANT: Use exact=true when searching for strings containing punctuation, quotes, or empty values.",
27543
+ " Default BM25 search strips punctuation and treats quoted empty strings as noise.",
27544
+ ` Example: searching for 'description: ""' with exact=false will NOT find empty description fields \u2014 it just matches "description".`,
27545
+ ` Use exact=true for literal patterns like 'description: ""', 'value: \\'\\'', or any YAML/config field with specific punctuation.`,
27430
27546
  "- Do NOT use exact=true for exploratory/conceptual queries \u2014 use the default for those.",
27431
27547
  "",
27432
27548
  "Combining searches with OR:",
@@ -27486,7 +27602,13 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27486
27602
  "WHEN TO STOP:",
27487
27603
  "- After you have explored the main concept AND related subsystems.",
27488
27604
  "- Once you have 5-15 targets covering different aspects of the query.",
27489
- '- If you get a "DUPLICATE SEARCH BLOCKED" message, move on.',
27605
+ '- If you get a "DUPLICATE SEARCH BLOCKED" message, do NOT rephrase the same query \u2014 try a FUNDAMENTALLY different approach:',
27606
+ " * Switch between exact=true and exact=false",
27607
+ " * Search for a broader term and filter results manually",
27608
+ " * Use listFiles to browse the directory structure directly",
27609
+ " * Look for related/surrounding patterns instead of the exact string",
27610
+ "- If 2-3 genuinely different search approaches fail, STOP and report what you tried and why it failed.",
27611
+ " Do NOT keep trying variations of the same failing concept.",
27490
27612
  "",
27491
27613
  "Strategy:",
27492
27614
  "1. Analyze the query \u2014 identify key concepts, then brainstorm SYNONYMS and alternative terms for each.",
@@ -27558,8 +27680,8 @@ var init_vercel = __esm({
27558
27680
  }
27559
27681
  return result;
27560
27682
  };
27561
- const previousSearches = /* @__PURE__ */ new Set();
27562
- let consecutiveDupBlocks = 0;
27683
+ const previousSearches = /* @__PURE__ */ new Map();
27684
+ const dupBlockCounts = /* @__PURE__ */ new Map();
27563
27685
  const paginationCounts = /* @__PURE__ */ new Map();
27564
27686
  const MAX_PAGES_PER_QUERY = 3;
27565
27687
  return (0, import_ai.tool)({
@@ -27610,20 +27732,25 @@ var init_vercel = __esm({
27610
27732
  return await search(searchOptions);
27611
27733
  };
27612
27734
  if (!searchDelegate) {
27613
- const searchKey = `${searchQuery}::${exact || false}`;
27735
+ const searchKey = `${searchPath}::${searchQuery}::${exact || false}`;
27614
27736
  if (!nextPage) {
27615
27737
  if (previousSearches.has(searchKey)) {
27616
- consecutiveDupBlocks++;
27738
+ const blockCount = (dupBlockCounts.get(searchKey) || 0) + 1;
27739
+ dupBlockCounts.set(searchKey, blockCount);
27617
27740
  if (debug) {
27618
- console.error(`[DEDUP] Blocked duplicate search (${consecutiveDupBlocks}x): "${searchQuery}" (path: "${searchPath}")`);
27741
+ console.error(`[DEDUP] Blocked duplicate search (${blockCount}x): "${searchQuery}" (path: "${searchPath}")`);
27742
+ }
27743
+ if (blockCount >= 3) {
27744
+ return "STOP. You have been blocked " + blockCount + " times for repeating the same search. You MUST provide your final answer NOW with whatever information you have. Do NOT call any more tools.";
27619
27745
  }
27620
- if (consecutiveDupBlocks >= 3) {
27621
- return "STOP. You have been blocked " + consecutiveDupBlocks + " times for repeating searches. You MUST output your final JSON answer NOW with whatever targets you have found. Do NOT call any more tools.";
27746
+ const prev = previousSearches.get(searchKey);
27747
+ if (prev.hadResults) {
27748
+ return `DUPLICATE SEARCH BLOCKED (${blockCount}x). You already searched for "${searchQuery}" in this path and found results. Do NOT repeat. Use extract to examine the files you already found, try a COMPLETELY different keyword, or provide your final answer.`;
27622
27749
  }
27623
- return "DUPLICATE SEARCH BLOCKED (" + consecutiveDupBlocks + "x). You already searched for this. Do NOT repeat \u2014 probe searches recursively across all paths. Either: (1) use extract on results you already found, (2) try a COMPLETELY different keyword, or (3) output your final answer NOW.";
27750
+ const exactHint = exact ? "You used exact=true. Try a broader search with exact=false, or use listFiles to browse the directory structure." : `Try exact=true if you need literal/punctuation matching (e.g. 'description: ""'), or use listFiles to explore directories, or search for a broader/related term and filter manually.`;
27751
+ return `DUPLICATE SEARCH BLOCKED (${blockCount}x). You already searched for "${searchQuery}" in this path and got NO results. This term does not appear in the codebase. Do NOT repeat or rephrase \u2014 try a FUNDAMENTALLY different approach: ${exactHint} If multiple approaches have failed, provide your final answer with what you know.`;
27624
27752
  }
27625
- previousSearches.add(searchKey);
27626
- consecutiveDupBlocks = 0;
27753
+ previousSearches.set(searchKey, { hadResults: false });
27627
27754
  paginationCounts.set(searchKey, 0);
27628
27755
  } else {
27629
27756
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
@@ -27637,6 +27764,14 @@ var init_vercel = __esm({
27637
27764
  }
27638
27765
  try {
27639
27766
  const result = maybeAnnotate(await runRawSearch());
27767
+ if (typeof result === "string" && result.includes("No results found")) {
27768
+ if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, "").trim())) {
27769
+ return result + "\n\n\u26A0\uFE0F Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names).";
27770
+ }
27771
+ } else if (typeof result === "string") {
27772
+ const entry = previousSearches.get(searchKey);
27773
+ if (entry) entry.hadResults = true;
27774
+ }
27640
27775
  if (options.fileTracker && typeof result === "string") {
27641
27776
  options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {
27642
27777
  });
@@ -27919,7 +28054,31 @@ var init_vercel = __esm({
27919
28054
  });
27920
28055
  };
27921
28056
  delegateTool = (options = {}) => {
27922
- const { debug = false, timeout = 300, cwd, allowedFolders, workspaceRoot, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, delegationManager = null } = options;
28057
+ const {
28058
+ debug = false,
28059
+ timeout = 300,
28060
+ cwd,
28061
+ allowedFolders,
28062
+ workspaceRoot,
28063
+ enableBash = false,
28064
+ bashConfig,
28065
+ architectureFileName,
28066
+ enableMcp = false,
28067
+ mcpConfig = null,
28068
+ mcpConfigPath = null,
28069
+ delegationManager = null,
28070
+ // Timeout settings inherited from parent agent
28071
+ timeoutBehavior,
28072
+ maxOperationTimeout,
28073
+ requestTimeout,
28074
+ gracefulTimeoutBonusSteps,
28075
+ negotiatedTimeoutBudget,
28076
+ negotiatedTimeoutMaxRequests,
28077
+ negotiatedTimeoutMaxPerRequest,
28078
+ parentOperationStartTime,
28079
+ onSubagentCreated,
28080
+ onSubagentCompleted
28081
+ } = options;
27923
28082
  return (0, import_ai.tool)({
27924
28083
  name: "delegate",
27925
28084
  description: delegateDescription,
@@ -27963,9 +28122,30 @@ var init_vercel = __esm({
27963
28122
  console.error(`Using workspace root: ${effectivePath} (cwd was: ${cwd || "not set"})`);
27964
28123
  }
27965
28124
  }
28125
+ let effectiveTimeout = timeout;
28126
+ if (parentOperationStartTime && maxOperationTimeout) {
28127
+ const elapsed = Date.now() - parentOperationStartTime;
28128
+ const remaining = maxOperationTimeout - elapsed;
28129
+ const budgetCap = Math.max(30, Math.floor(remaining * 0.9 / 1e3));
28130
+ if (budgetCap < effectiveTimeout) {
28131
+ effectiveTimeout = budgetCap;
28132
+ if (debug) {
28133
+ console.error(`[DELEGATE] Capping timeout from ${timeout}s to ${effectiveTimeout}s (remaining parent budget: ${Math.floor(remaining / 1e3)}s)`);
28134
+ }
28135
+ if (tracer) {
28136
+ tracer.addEvent("delegation.budget_capped", {
28137
+ "delegation.original_timeout_s": timeout,
28138
+ "delegation.effective_timeout_s": effectiveTimeout,
28139
+ "delegation.parent_elapsed_ms": elapsed,
28140
+ "delegation.parent_remaining_ms": remaining,
28141
+ "delegation.parent_session_id": parentSessionId
28142
+ });
28143
+ }
28144
+ }
28145
+ }
27966
28146
  const result = await delegate({
27967
28147
  task,
27968
- timeout,
28148
+ timeout: effectiveTimeout,
27969
28149
  debug,
27970
28150
  currentIteration: currentIteration || 0,
27971
28151
  maxIterations: maxIterations || 30,
@@ -27984,7 +28164,14 @@ var init_vercel = __esm({
27984
28164
  mcpConfigPath,
27985
28165
  delegationManager,
27986
28166
  // Per-instance delegation limits
27987
- parentAbortSignal
28167
+ parentAbortSignal,
28168
+ // Inherit timeout settings for subagent
28169
+ timeoutBehavior,
28170
+ requestTimeout,
28171
+ gracefulTimeoutBonusSteps,
28172
+ // Subagent lifecycle callbacks for graceful stop coordination
28173
+ onSubagentCreated,
28174
+ onSubagentCompleted
27988
28175
  });
27989
28176
  return result;
27990
28177
  }
@@ -48935,6 +49122,16 @@ var init_fileTracker = __esm({
48935
49122
  }
48936
49123
  });
48937
49124
 
49125
+ // src/agent/otelLogBridge.js
49126
+ var import_module, _require;
49127
+ var init_otelLogBridge = __esm({
49128
+ "src/agent/otelLogBridge.js"() {
49129
+ "use strict";
49130
+ import_module = require("module");
49131
+ _require = (0, import_module.createRequire)("file:///");
49132
+ }
49133
+ });
49134
+
48938
49135
  // src/agent/simpleTelemetry.js
48939
49136
  var import_fs10, import_path11;
48940
49137
  var init_simpleTelemetry = __esm({
@@ -48942,6 +49139,7 @@ var init_simpleTelemetry = __esm({
48942
49139
  "use strict";
48943
49140
  import_fs10 = require("fs");
48944
49141
  import_path11 = require("path");
49142
+ init_otelLogBridge();
48945
49143
  }
48946
49144
  });
48947
49145
 
@@ -88952,6 +89150,9 @@ function isMethodAllowed(methodName, allowedMethods, blockedMethods) {
88952
89150
  }
88953
89151
  function createTransport(serverConfig) {
88954
89152
  const { transport, command, args, url: url2, env } = serverConfig;
89153
+ if (serverConfig.transportInstance) {
89154
+ return serverConfig.transportInstance;
89155
+ }
88955
89156
  switch (transport) {
88956
89157
  case "stdio":
88957
89158
  return new import_stdio.StdioClientTransport({
@@ -89314,6 +89515,53 @@ var init_client = __esm({
89314
89515
  throw error40;
89315
89516
  }
89316
89517
  }
89518
+ /**
89519
+ * Call graceful_stop on all MCP servers that expose it.
89520
+ * This signals agent-type MCP servers to wrap up their work.
89521
+ * @returns {Promise<Array<{server: string, success: boolean, error?: string}>>}
89522
+ */
89523
+ async callGracefulStopAll() {
89524
+ const results = [];
89525
+ for (const [serverName, clientInfo] of this.clients) {
89526
+ const qualifiedName = `${serverName}_graceful_stop`;
89527
+ if (this.tools.has(qualifiedName)) {
89528
+ if (this.debug) {
89529
+ console.log(`[DEBUG] MCP callGracefulStopAll: calling graceful_stop on server "${serverName}"`);
89530
+ }
89531
+ try {
89532
+ const timeoutMs = 5e3;
89533
+ const timeoutPromise = new Promise(
89534
+ (_, reject2) => setTimeout(() => reject2(new Error("graceful_stop timeout")), timeoutMs)
89535
+ );
89536
+ await Promise.race([
89537
+ clientInfo.client.callTool({ name: "graceful_stop", arguments: {} }, void 0, { timeout: timeoutMs }),
89538
+ timeoutPromise
89539
+ ]);
89540
+ results.push({ server: serverName, success: true });
89541
+ if (this.debug) {
89542
+ console.log(`[DEBUG] MCP callGracefulStopAll: server "${serverName}" acknowledged graceful_stop`);
89543
+ }
89544
+ } catch (e) {
89545
+ results.push({ server: serverName, success: false, error: e.message });
89546
+ if (this.debug) {
89547
+ console.log(`[DEBUG] MCP callGracefulStopAll: server "${serverName}" graceful_stop failed: ${e.message}`);
89548
+ }
89549
+ }
89550
+ }
89551
+ }
89552
+ if (this.debug) {
89553
+ const withStop = results.length;
89554
+ const total = this.clients.size;
89555
+ console.log(`[DEBUG] MCP callGracefulStopAll: ${withStop}/${total} servers had graceful_stop tool`);
89556
+ }
89557
+ this.recordMcpEvent("graceful_stop.sweep_completed", {
89558
+ servers_total: this.clients.size,
89559
+ servers_with_graceful_stop: results.length,
89560
+ servers_acknowledged: results.filter((r) => r.success).length,
89561
+ servers_failed: results.filter((r) => !r.success).length
89562
+ });
89563
+ return results;
89564
+ }
89317
89565
  /**
89318
89566
  * Get all available tools with their schemas
89319
89567
  * @returns {Object} Map of tool name to tool definition
@@ -89341,10 +89589,29 @@ var init_client = __esm({
89341
89589
  inputSchema: tool6.inputSchema,
89342
89590
  execute: async (args) => {
89343
89591
  const result = await this.callTool(name15, args);
89344
- if (result.content && result.content[0]) {
89345
- return result.content[0].text;
89592
+ if (!result.content || !result.content[0]) {
89593
+ return JSON.stringify(result);
89594
+ }
89595
+ const hasImage = result.content.some((block) => block.type === "image");
89596
+ if (hasImage) {
89597
+ return { _mcpContent: result.content };
89598
+ }
89599
+ return result.content[0].text;
89600
+ },
89601
+ // Convert MCP content blocks (including images) to Vercel AI SDK format
89602
+ toModelOutput: ({ output }) => {
89603
+ if (output && typeof output === "object" && output._mcpContent) {
89604
+ const parts = [];
89605
+ for (const block of output._mcpContent) {
89606
+ if (block.type === "text") {
89607
+ parts.push({ type: "text", text: block.text });
89608
+ } else if (block.type === "image") {
89609
+ parts.push({ type: "image-data", data: block.data, mediaType: block.mimeType });
89610
+ }
89611
+ }
89612
+ return { type: "content", value: parts };
89346
89613
  }
89347
- return JSON.stringify(result);
89614
+ return { type: "text", value: typeof output === "string" ? output : JSON.stringify(output) };
89348
89615
  }
89349
89616
  };
89350
89617
  }
@@ -89524,6 +89791,16 @@ var init_xmlBridge = __esm({
89524
89791
  isMcpTool(toolName) {
89525
89792
  return toolName in this.mcpTools;
89526
89793
  }
89794
+ /**
89795
+ * Call graceful_stop on all MCP servers that expose it.
89796
+ * @returns {Promise<Array>}
89797
+ */
89798
+ async callGracefulStopAll() {
89799
+ if (this.mcpManager) {
89800
+ return this.mcpManager.callGracefulStopAll();
89801
+ }
89802
+ return [];
89803
+ }
89527
89804
  /**
89528
89805
  * Clean up MCP connections
89529
89806
  */
@@ -99762,6 +100039,7 @@ var init_ProbeAgent = __esm({
99762
100039
  this.debug = options.debug || process.env.DEBUG === "1";
99763
100040
  this.cancelled = false;
99764
100041
  this._abortController = new AbortController();
100042
+ this._activeSubagents = /* @__PURE__ */ new Map();
99765
100043
  this.tracer = options.tracer || null;
99766
100044
  this.outline = !!options.outline;
99767
100045
  this.searchDelegate = options.searchDelegate !== void 0 ? !!options.searchDelegate : true;
@@ -99870,6 +100148,35 @@ var init_ProbeAgent = __esm({
99870
100148
  if (this.debug) {
99871
100149
  console.log(`[DEBUG] Max operation timeout: ${this.maxOperationTimeout}ms`);
99872
100150
  }
100151
+ this.timeoutBehavior = options.timeoutBehavior ?? (() => {
100152
+ const val = process.env.TIMEOUT_BEHAVIOR;
100153
+ if (val === "hard") return "hard";
100154
+ if (val === "negotiated") return "negotiated";
100155
+ return "graceful";
100156
+ })();
100157
+ this.gracefulTimeoutBonusSteps = options.gracefulTimeoutBonusSteps ?? (() => {
100158
+ const parsed = parseInt(process.env.GRACEFUL_TIMEOUT_BONUS_STEPS, 10);
100159
+ return isNaN(parsed) || parsed < 1 || parsed > 20 ? 4 : parsed;
100160
+ })();
100161
+ this.negotiatedTimeoutBudget = options.negotiatedTimeoutBudget ?? (() => {
100162
+ const parsed = parseInt(process.env.NEGOTIATED_TIMEOUT_BUDGET, 10);
100163
+ return isNaN(parsed) || parsed < 6e4 || parsed > 72e5 ? 18e5 : parsed;
100164
+ })();
100165
+ this.negotiatedTimeoutMaxRequests = options.negotiatedTimeoutMaxRequests ?? (() => {
100166
+ const parsed = parseInt(process.env.NEGOTIATED_TIMEOUT_MAX_REQUESTS, 10);
100167
+ return isNaN(parsed) || parsed < 1 || parsed > 10 ? 3 : parsed;
100168
+ })();
100169
+ this.negotiatedTimeoutMaxPerRequest = options.negotiatedTimeoutMaxPerRequest ?? (() => {
100170
+ const parsed = parseInt(process.env.NEGOTIATED_TIMEOUT_MAX_PER_REQUEST, 10);
100171
+ return isNaN(parsed) || parsed < 6e4 || parsed > 36e5 ? 6e5 : parsed;
100172
+ })();
100173
+ this.gracefulStopDeadline = options.gracefulStopDeadline ?? (() => {
100174
+ const parsed = parseInt(process.env.GRACEFUL_STOP_DEADLINE, 10);
100175
+ return isNaN(parsed) || parsed < 5e3 || parsed > 3e5 ? 45e3 : parsed;
100176
+ })();
100177
+ if (this.debug) {
100178
+ console.log(`[DEBUG] Timeout behavior: ${this.timeoutBehavior}, bonus steps: ${this.gracefulTimeoutBonusSteps}, graceful stop deadline: ${this.gracefulStopDeadline}ms`);
100179
+ }
99873
100180
  this.retryConfig = options.retry || {};
99874
100181
  this.retryManager = null;
99875
100182
  this.fallbackConfig = options.fallback || null;
@@ -100220,6 +100527,18 @@ var init_ProbeAgent = __esm({
100220
100527
  // Per-instance delegation limits
100221
100528
  parentAbortSignal: this._abortController.signal,
100222
100529
  // Propagate cancellation to delegations
100530
+ // Timeout settings for delegate subagents to inherit
100531
+ timeoutBehavior: this.timeoutBehavior,
100532
+ maxOperationTimeout: this.maxOperationTimeout,
100533
+ requestTimeout: this.requestTimeout,
100534
+ gracefulTimeoutBonusSteps: this.gracefulTimeoutBonusSteps,
100535
+ negotiatedTimeoutBudget: this.negotiatedTimeoutBudget,
100536
+ negotiatedTimeoutMaxRequests: this.negotiatedTimeoutMaxRequests,
100537
+ negotiatedTimeoutMaxPerRequest: this.negotiatedTimeoutMaxPerRequest,
100538
+ parentOperationStartTime: this._operationStartTime,
100539
+ // For remaining budget calculation
100540
+ onSubagentCreated: (sid, subagent) => this._registerSubagent(sid, subagent),
100541
+ onSubagentCompleted: (sid) => this._unregisterSubagent(sid),
100223
100542
  outputBuffer: this._outputBuffer,
100224
100543
  concurrencyLimiter: this.concurrencyLimiter,
100225
100544
  // Global AI concurrency limiter
@@ -100803,12 +101122,16 @@ var init_ProbeAgent = __esm({
100803
101122
  }, { once: true });
100804
101123
  }
100805
101124
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
100806
- timeoutState.timeoutId = setTimeout(() => {
100807
- controller.abort();
100808
- if (this.debug) {
100809
- console.log(`[DEBUG] Operation timed out after ${this.maxOperationTimeout}ms (max operation timeout)`);
100810
- }
100811
- }, this.maxOperationTimeout);
101125
+ const gts = this._gracefulTimeoutState;
101126
+ if ((this.timeoutBehavior === "graceful" || this.timeoutBehavior === "negotiated") && gts) {
101127
+ } else {
101128
+ timeoutState.timeoutId = setTimeout(() => {
101129
+ controller.abort();
101130
+ if (this.debug) {
101131
+ console.log(`[DEBUG] Operation timed out after ${this.maxOperationTimeout}ms (max operation timeout)`);
101132
+ }
101133
+ }, this.maxOperationTimeout);
101134
+ }
100812
101135
  }
100813
101136
  try {
100814
101137
  const useClaudeCode = this.clientApiProvider === "claude-code" || process.env.USE_CLAUDE_CODE === "true";
@@ -102187,6 +102510,7 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102187
102510
  } else {
102188
102511
  options = schemaOrOptions || {};
102189
102512
  }
102513
+ this._operationStartTime = Date.now();
102190
102514
  try {
102191
102515
  const oldHistoryLength = this.history.length;
102192
102516
  if (this._outputBuffer && !options?._schemaFormatted && !options?._completionPromptProcessed) {
@@ -102267,7 +102591,10 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102267
102591
  }
102268
102592
  }
102269
102593
  let currentIteration = 0;
102270
- let finalResult = "I was unable to complete your request due to reaching the maximum number of tool iterations.";
102594
+ let finalResult = null;
102595
+ const DEFAULT_MAX_ITER_MSG = "I was unable to complete your request due to reaching the maximum number of tool iterations.";
102596
+ const _toolCallLog = [];
102597
+ let abortSummaryTaken = false;
102271
102598
  const baseMaxIterations = options._maxIterationsOverride || this.maxIterations || MAX_TOOL_ITERATIONS;
102272
102599
  const maxIterations = options._maxIterationsOverride ? baseMaxIterations : options.schema ? baseMaxIterations + 4 : baseMaxIterations;
102273
102600
  const isClaudeCode = this.clientApiProvider === "claude-code" || process.env.USE_CLAUDE_CODE === "true";
@@ -102399,6 +102726,228 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102399
102726
  }
102400
102727
  let completionPromptInjected = false;
102401
102728
  let preCompletionResult = null;
102729
+ const gracefulTimeoutState = {
102730
+ triggered: false,
102731
+ // Set to true when soft timeout fires
102732
+ bonusStepsUsed: 0,
102733
+ // Steps taken after soft timeout
102734
+ bonusStepsMax: this.gracefulTimeoutBonusSteps
102735
+ };
102736
+ this._gracefulTimeoutState = gracefulTimeoutState;
102737
+ const negotiatedTimeoutState = {
102738
+ extensionsUsed: 0,
102739
+ totalExtraTimeMs: 0,
102740
+ softTimeoutId: null,
102741
+ hardAbortTimeoutId: null,
102742
+ maxRequests: this.negotiatedTimeoutMaxRequests,
102743
+ maxPerRequestMs: this.negotiatedTimeoutMaxPerRequest,
102744
+ budgetMs: this.negotiatedTimeoutBudget,
102745
+ observerRunning: false,
102746
+ // true while observer LLM call is in flight
102747
+ extensionMessage: null,
102748
+ // message to show in prepareStep after extension granted
102749
+ startTime: Date.now()
102750
+ };
102751
+ this._negotiatedTimeoutState = negotiatedTimeoutState;
102752
+ const activeTools = /* @__PURE__ */ new Map();
102753
+ this._activeTools = activeTools;
102754
+ const onToolCall = (event) => {
102755
+ const key = event.toolCallId || `${event.name}:${JSON.stringify(event.args || {}).slice(0, 100)}`;
102756
+ if (event.status === "started") {
102757
+ activeTools.set(key, {
102758
+ name: event.name,
102759
+ args: event.args,
102760
+ startedAt: event.timestamp || (/* @__PURE__ */ new Date()).toISOString()
102761
+ });
102762
+ } else if (event.status === "completed" || event.status === "error") {
102763
+ activeTools.delete(key);
102764
+ }
102765
+ };
102766
+ this.events.on("toolCall", onToolCall);
102767
+ const runTimeoutObserver = async () => {
102768
+ if (negotiatedTimeoutState.observerRunning) return;
102769
+ negotiatedTimeoutState.observerRunning = true;
102770
+ const remainingRequests = negotiatedTimeoutState.maxRequests - negotiatedTimeoutState.extensionsUsed;
102771
+ const remainingBudgetMs = negotiatedTimeoutState.budgetMs - negotiatedTimeoutState.totalExtraTimeMs;
102772
+ const maxPerReqMin = Math.round(negotiatedTimeoutState.maxPerRequestMs / 6e4);
102773
+ const elapsedMin = Math.round((Date.now() - negotiatedTimeoutState.startTime) / 6e4);
102774
+ if (remainingRequests <= 0 || remainingBudgetMs <= 0) {
102775
+ if (this.debug) {
102776
+ console.log(`[DEBUG] Timeout observer: no extensions/budget remaining \u2014 aborting in-flight tools and triggering graceful wind-down`);
102777
+ }
102778
+ if (this.tracer) {
102779
+ this.tracer.addEvent("negotiated_timeout.observer_exhausted", {
102780
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
102781
+ max_requests: negotiatedTimeoutState.maxRequests,
102782
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
102783
+ budget_ms: negotiatedTimeoutState.budgetMs,
102784
+ elapsed_min: elapsedMin,
102785
+ active_tools: Array.from(activeTools.values()).map((t) => t.name)
102786
+ });
102787
+ }
102788
+ await this._initiateGracefulStop(gracefulTimeoutState, "budget/extensions exhausted");
102789
+ negotiatedTimeoutState.observerRunning = false;
102790
+ return;
102791
+ }
102792
+ const activeToolsList = Array.from(activeTools.values());
102793
+ const now = Date.now();
102794
+ const formatDuration = (ms) => {
102795
+ const totalSec = Math.round(ms / 1e3);
102796
+ if (totalSec < 60) return `${totalSec}s`;
102797
+ const min = Math.floor(totalSec / 60);
102798
+ const sec = totalSec % 60;
102799
+ if (min < 60) return `${min}m ${sec}s`;
102800
+ const hr = Math.floor(min / 60);
102801
+ const remainMin = min % 60;
102802
+ return `${hr}h ${remainMin}m`;
102803
+ };
102804
+ const activeToolsDesc = activeToolsList.length > 0 ? activeToolsList.map((t) => {
102805
+ const runningForMs = now - new Date(t.startedAt).getTime();
102806
+ return `- ${t.name}(${JSON.stringify(t.args || {}).slice(0, 200)}) \u2014 running for ${formatDuration(runningForMs)}`;
102807
+ }).join("\n") : "(none currently running)";
102808
+ const recentHistory = this.history.slice(-6).map((msg) => {
102809
+ const content = typeof msg.content === "string" ? msg.content.slice(0, 300) : JSON.stringify(msg.content).slice(0, 300);
102810
+ return `[${msg.role}]: ${content}`;
102811
+ }).join("\n");
102812
+ const observerPrompt = `You are a timeout observer for an AI coding agent. The agent has been working for ${elapsedMin} minute(s) and has reached its time limit.
102813
+
102814
+ ## Recent Conversation
102815
+ ${recentHistory || "(no history yet)"}
102816
+
102817
+ ## Currently Running Tools
102818
+ ${activeToolsDesc}
102819
+
102820
+ ## Budget
102821
+ - Extensions used: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}
102822
+ - Time budget remaining: ${Math.round(remainingBudgetMs / 6e4)} minutes
102823
+ - Max per extension: ${maxPerReqMin} minutes
102824
+
102825
+ Decide whether the agent should get more time. EXTEND if:
102826
+ - Tools are actively running (especially delegates or complex analysis) \u2014 they need time to finish
102827
+ - The agent is making clear progress on a complex task
102828
+ - New information is being gathered that will improve the final answer
102829
+
102830
+ DO NOT EXTEND if:
102831
+ - The agent appears stuck in a loop (repeating the same tool calls or getting the same errors)
102832
+ - The conversation shows the agent retrying failed operations without changing approach
102833
+ - The agent has enough information to answer but keeps searching for more
102834
+ - Tool calls are returning empty or error results repeatedly
102835
+ - The agent is doing redundant work (searching for things it already found)
102836
+
102837
+ A stuck agent will not recover with more time \u2014 it will just burn the budget. Better to force it to answer with what it has.
102838
+
102839
+ Respond with ONLY valid JSON (no markdown, no explanation):
102840
+ {"extend": true, "minutes": <1-${maxPerReqMin}>, "reason": "your reason here"}
102841
+ or
102842
+ {"extend": false, "reason": "your reason here"}`;
102843
+ const observerFn = async () => {
102844
+ const modelInstance = this.provider ? this.provider(this.model) : this.model;
102845
+ if (this.debug) {
102846
+ console.log(`[DEBUG] Timeout observer: making LLM call (${activeToolsList.length} active tools, ${elapsedMin} min elapsed)`);
102847
+ }
102848
+ if (this.tracer) {
102849
+ this.tracer.addEvent("negotiated_timeout.observer_invoked", {
102850
+ elapsed_min: elapsedMin,
102851
+ active_tools: activeToolsList.map((t) => t.name),
102852
+ active_tools_detail: activeToolsList.map((t) => ({
102853
+ name: t.name,
102854
+ running_for_ms: now - new Date(t.startedAt).getTime(),
102855
+ args_preview: JSON.stringify(t.args || {}).slice(0, 100)
102856
+ })),
102857
+ active_tools_count: activeToolsList.length,
102858
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
102859
+ remaining_requests: remainingRequests,
102860
+ remaining_budget_ms: remainingBudgetMs,
102861
+ history_length: this.history.length
102862
+ });
102863
+ }
102864
+ const observerResult = await (0, import_ai6.generateText)({
102865
+ model: modelInstance,
102866
+ messages: [{ role: "user", content: observerPrompt }],
102867
+ maxTokens: 500
102868
+ });
102869
+ const responseText = observerResult.text.trim();
102870
+ if (this.tracer) {
102871
+ this.tracer.addEvent("negotiated_timeout.observer_response", {
102872
+ response_text: responseText,
102873
+ usage_prompt_tokens: observerResult.usage?.promptTokens,
102874
+ usage_completion_tokens: observerResult.usage?.completionTokens
102875
+ });
102876
+ }
102877
+ const jsonStr = responseText.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
102878
+ const decision = JSON.parse(jsonStr);
102879
+ if (decision.extend && decision.minutes > 0) {
102880
+ const requestedMs = Math.min(decision.minutes, maxPerReqMin) * 6e4;
102881
+ const grantedMs = Math.min(requestedMs, remainingBudgetMs, negotiatedTimeoutState.maxPerRequestMs);
102882
+ const grantedMin = Math.round(grantedMs / 6e4 * 10) / 10;
102883
+ negotiatedTimeoutState.extensionsUsed++;
102884
+ negotiatedTimeoutState.totalExtraTimeMs += grantedMs;
102885
+ negotiatedTimeoutState.extensionMessage = `\u23F0 Time limit was reached. The timeout observer granted ${grantedMin} more minute(s) (reason: ${decision.reason || "work in progress"}). Extensions remaining: ${negotiatedTimeoutState.maxRequests - negotiatedTimeoutState.extensionsUsed}. Continue your work efficiently.`;
102886
+ negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
102887
+ runTimeoutObserver();
102888
+ }, grantedMs);
102889
+ if (this.debug) {
102890
+ console.log(`[DEBUG] Timeout observer: granted ${grantedMin} min (reason: ${decision.reason}). Extensions: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}`);
102891
+ }
102892
+ if (this.tracer) {
102893
+ this.tracer.addEvent("negotiated_timeout.observer_extended", {
102894
+ decision_reason: decision.reason,
102895
+ requested_minutes: decision.minutes,
102896
+ granted_ms: grantedMs,
102897
+ granted_min: grantedMin,
102898
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
102899
+ max_requests: negotiatedTimeoutState.maxRequests,
102900
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
102901
+ budget_remaining_ms: remainingBudgetMs - grantedMs,
102902
+ active_tools: activeToolsList.map((t) => t.name),
102903
+ active_tools_count: activeToolsList.length
102904
+ });
102905
+ }
102906
+ } else {
102907
+ if (this.debug) {
102908
+ console.log(`[DEBUG] Timeout observer: declined extension (reason: ${decision.reason}). Initiating graceful stop.`);
102909
+ }
102910
+ if (this.tracer) {
102911
+ this.tracer.addEvent("negotiated_timeout.observer_declined", {
102912
+ decision_reason: decision.reason,
102913
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
102914
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
102915
+ elapsed_min: elapsedMin,
102916
+ active_tools: activeToolsList.map((t) => t.name)
102917
+ });
102918
+ }
102919
+ await this._initiateGracefulStop(gracefulTimeoutState, `observer declined: ${decision.reason}`);
102920
+ }
102921
+ };
102922
+ try {
102923
+ if (this.tracer) {
102924
+ await this.tracer.withSpan("negotiated_timeout.observer", observerFn, {
102925
+ "timeout.elapsed_min": elapsedMin,
102926
+ "timeout.extensions_used": negotiatedTimeoutState.extensionsUsed,
102927
+ "timeout.active_tools_count": activeToolsList.length,
102928
+ "timeout.remaining_budget_ms": remainingBudgetMs
102929
+ });
102930
+ } else {
102931
+ await observerFn();
102932
+ }
102933
+ } catch (err) {
102934
+ if (this.debug) {
102935
+ console.log(`[DEBUG] Timeout observer: LLM call failed (${err.message}). Initiating graceful stop.`);
102936
+ }
102937
+ if (this.tracer) {
102938
+ this.tracer.addEvent("negotiated_timeout.observer_error", {
102939
+ error_message: err.message,
102940
+ error_name: err.name,
102941
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
102942
+ elapsed_min: elapsedMin
102943
+ });
102944
+ }
102945
+ await this._initiateGracefulStop(gracefulTimeoutState, `observer error: ${err.message}`);
102946
+ } finally {
102947
+ negotiatedTimeoutState.observerRunning = false;
102948
+ }
102949
+ };
102950
+ negotiatedTimeoutState.runObserver = runTimeoutObserver;
102402
102951
  let compactionAttempted = false;
102403
102952
  while (true) {
102404
102953
  try {
@@ -102408,6 +102957,15 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102408
102957
  messages: messagesForAI,
102409
102958
  tools: tools2,
102410
102959
  stopWhen: ({ steps }) => {
102960
+ if (gracefulTimeoutState.triggered) {
102961
+ if (gracefulTimeoutState.bonusStepsUsed >= gracefulTimeoutState.bonusStepsMax) {
102962
+ if (this.debug) {
102963
+ console.log(`[DEBUG] stopWhen: graceful timeout bonus steps exhausted (${gracefulTimeoutState.bonusStepsUsed}/${gracefulTimeoutState.bonusStepsMax}), forcing stop`);
102964
+ }
102965
+ return true;
102966
+ }
102967
+ return false;
102968
+ }
102411
102969
  if (steps.length >= maxIterations) return true;
102412
102970
  const lastStep = steps[steps.length - 1];
102413
102971
  const modelWantsToStop = lastStep?.finishReason === "stop" && (!lastStep?.toolCalls || lastStep.toolCalls.length === 0);
@@ -102451,9 +103009,45 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102451
103009
  return false;
102452
103010
  },
102453
103011
  prepareStep: ({ steps, stepNumber }) => {
103012
+ if (negotiatedTimeoutState.extensionMessage && !gracefulTimeoutState.triggered) {
103013
+ const msg = negotiatedTimeoutState.extensionMessage;
103014
+ negotiatedTimeoutState.extensionMessage = null;
103015
+ if (this.debug) {
103016
+ console.log(`[DEBUG] prepareStep: delivering timeout observer extension message`);
103017
+ }
103018
+ return { userMessage: msg };
103019
+ }
103020
+ if (gracefulTimeoutState.triggered) {
103021
+ gracefulTimeoutState.bonusStepsUsed++;
103022
+ const remaining = gracefulTimeoutState.bonusStepsMax - gracefulTimeoutState.bonusStepsUsed;
103023
+ if (gracefulTimeoutState.bonusStepsUsed === 1) {
103024
+ if (this.debug) {
103025
+ console.log(`[DEBUG] prepareStep: graceful timeout wind-down step 1/${gracefulTimeoutState.bonusStepsMax}`);
103026
+ }
103027
+ if (this.tracer) {
103028
+ this.tracer.addEvent("graceful_timeout.wind_down_started", {
103029
+ bonus_steps_max: gracefulTimeoutState.bonusStepsMax,
103030
+ current_iteration: currentIteration,
103031
+ max_iterations: maxIterations
103032
+ });
103033
+ }
103034
+ return {
103035
+ toolChoice: "none",
103036
+ userMessage: `\u26A0\uFE0F TIME LIMIT REACHED. You are running out of time. You have ${remaining} step(s) remaining. Provide your BEST answer NOW using the information you have already gathered. Do NOT call any more tools. Summarize your findings and respond completely. If something was not completed, honestly state what was not done and provide any partial results or recommendations you can offer.`
103037
+ };
103038
+ }
103039
+ if (this.debug) {
103040
+ console.log(`[DEBUG] prepareStep: graceful timeout wind-down step ${gracefulTimeoutState.bonusStepsUsed}/${gracefulTimeoutState.bonusStepsMax} (${remaining} remaining)`);
103041
+ }
103042
+ return { toolChoice: "none" };
103043
+ }
102454
103044
  if (stepNumber === maxIterations - 1) {
103045
+ const searchesTried = _toolCallLog.filter((tc) => tc.name === "search").map((tc) => `"${tc.args.query || ""}"${tc.args.exact ? " (exact)" : ""}`).filter((v, i, a) => a.indexOf(v) === i);
103046
+ const searchSummary = searchesTried.length > 0 ? `
103047
+ Searches attempted: ${searchesTried.join(", ")}` : "";
102455
103048
  return {
102456
- toolChoice: "none"
103049
+ toolChoice: "none",
103050
+ userMessage: `\u26A0\uFE0F LAST ITERATION \u2014 you are out of tool calls. Provide your BEST answer NOW with the information gathered so far. If you could not find what was requested, explain exactly what you searched for and why it did not work, so the caller can try a different approach.${searchSummary}`
102457
103051
  };
102458
103052
  }
102459
103053
  if (steps.length >= 2) {
@@ -102532,6 +103126,11 @@ Double-check your response based on the criteria above. If everything looks good
102532
103126
  const { toolResults, toolCalls, text, reasoningText, finishReason, usage } = stepResult;
102533
103127
  currentIteration++;
102534
103128
  toolContext.currentIteration = currentIteration;
103129
+ if (toolCalls?.length > 0) {
103130
+ for (const tc of toolCalls) {
103131
+ _toolCallLog.push({ name: tc.toolName, args: tc.args || {} });
103132
+ }
103133
+ }
102535
103134
  if (this.tracer) {
102536
103135
  const stepEvent = {
102537
103136
  "iteration": currentIteration,
@@ -102554,6 +103153,12 @@ Double-check your response based on the criteria above. If everything looks good
102554
103153
  }));
102555
103154
  }
102556
103155
  this.tracer.addEvent("iteration.step", stepEvent);
103156
+ if (gracefulTimeoutState.triggered) {
103157
+ this.tracer.addEvent("graceful_timeout.wind_down_step", {
103158
+ bonus_step: gracefulTimeoutState.bonusStepsUsed,
103159
+ bonus_max: gracefulTimeoutState.bonusStepsMax
103160
+ });
103161
+ }
102557
103162
  }
102558
103163
  if (usage) {
102559
103164
  this.tokenCounter.recordUsage(usage);
@@ -102599,22 +103204,59 @@ Double-check your response based on the criteria above. If everything looks good
102599
103204
  }
102600
103205
  const executeAIRequest = async () => {
102601
103206
  const result = await this.streamTextWithRetryAndFallback(streamOptions);
102602
- const steps = await result.steps;
102603
- let finalText;
102604
- if (steps && steps.length > 1) {
102605
- const lastStepText = steps[steps.length - 1].text;
102606
- finalText = lastStepText || await result.text;
102607
- } else {
102608
- finalText = await result.text;
103207
+ let gracefulTimeoutId = null;
103208
+ let hardAbortTimeoutId = null;
103209
+ if (this.timeoutBehavior === "graceful" && gracefulTimeoutState && this.maxOperationTimeout > 0) {
103210
+ gracefulTimeoutId = setTimeout(() => {
103211
+ gracefulTimeoutState.triggered = true;
103212
+ if (this.debug) {
103213
+ console.log(`[DEBUG] Soft timeout after ${this.maxOperationTimeout}ms \u2014 entering wind-down mode (${gracefulTimeoutState.bonusStepsMax} bonus steps)`);
103214
+ }
103215
+ hardAbortTimeoutId = setTimeout(() => {
103216
+ if (this._abortController) {
103217
+ this._abortController.abort();
103218
+ }
103219
+ if (this.debug) {
103220
+ console.log(`[DEBUG] Hard abort \u2014 wind-down safety net expired after 60s`);
103221
+ }
103222
+ }, 6e4);
103223
+ }, this.maxOperationTimeout);
102609
103224
  }
102610
- if (this.debug) {
102611
- console.log(`[DEBUG] streamText completed: ${steps?.length || 0} steps, finalText=${finalText?.length || 0} chars`);
103225
+ if (this.timeoutBehavior === "negotiated" && this.maxOperationTimeout > 0) {
103226
+ negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
103227
+ if (this.debug) {
103228
+ console.log(`[DEBUG] Soft timeout after ${this.maxOperationTimeout}ms \u2014 invoking timeout observer`);
103229
+ }
103230
+ runTimeoutObserver();
103231
+ }, this.maxOperationTimeout);
102612
103232
  }
102613
- const usage = await result.usage;
102614
- if (usage) {
102615
- this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
103233
+ try {
103234
+ const steps = await result.steps;
103235
+ let finalText;
103236
+ if (steps && steps.length > 1) {
103237
+ const lastStepText = steps[steps.length - 1].text;
103238
+ finalText = lastStepText || await result.text;
103239
+ } else {
103240
+ finalText = await result.text;
103241
+ }
103242
+ if (this.debug) {
103243
+ console.log(`[DEBUG] streamText completed: ${steps?.length || 0} steps, finalText=${finalText?.length || 0} chars`);
103244
+ }
103245
+ const usage = await result.usage;
103246
+ if (usage) {
103247
+ this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
103248
+ }
103249
+ return { finalText, result };
103250
+ } finally {
103251
+ if (gracefulTimeoutId) clearTimeout(gracefulTimeoutId);
103252
+ if (hardAbortTimeoutId) clearTimeout(hardAbortTimeoutId);
103253
+ if (negotiatedTimeoutState.softTimeoutId) clearTimeout(negotiatedTimeoutState.softTimeoutId);
103254
+ if (this._gracefulStopHardAbortId) {
103255
+ clearTimeout(this._gracefulStopHardAbortId);
103256
+ this._gracefulStopHardAbortId = null;
103257
+ }
103258
+ this.events.removeListener("toolCall", onToolCall);
102616
103259
  }
102617
- return { finalText, result };
102618
103260
  };
102619
103261
  let aiResult;
102620
103262
  if (this.tracer) {
@@ -102651,13 +103293,57 @@ Double-check your response based on the criteria above. If everything looks good
102651
103293
  } else if (aiResult.finalText) {
102652
103294
  finalResult = aiResult.finalText;
102653
103295
  }
103296
+ if (gracefulTimeoutState.triggered) {
103297
+ const timeoutNotice = "**Note: This response was generated under a time constraint. The research may be incomplete, and some planned searches or analysis steps were not completed.**\n\n";
103298
+ if (!finalResult || finalResult === DEFAULT_MAX_ITER_MSG || finalResult.startsWith("I was unable to complete your request after")) {
103299
+ try {
103300
+ const allText = await aiResult.result.text;
103301
+ if (allText && allText.trim()) {
103302
+ finalResult = timeoutNotice + allText;
103303
+ if (this.debug) {
103304
+ console.log(`[DEBUG] Graceful timeout: using concatenated step text (${allText.length} chars)`);
103305
+ }
103306
+ } else {
103307
+ const steps = await aiResult.result.steps;
103308
+ const toolSummaries = [];
103309
+ for (const step of steps || []) {
103310
+ if (step.toolResults?.length > 0) {
103311
+ for (const tr of step.toolResults) {
103312
+ const resultText = typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result);
103313
+ if (resultText && resultText.length > 0 && resultText.length < 5e3) {
103314
+ toolSummaries.push(resultText.substring(0, 2e3));
103315
+ }
103316
+ }
103317
+ }
103318
+ }
103319
+ if (toolSummaries.length > 0) {
103320
+ finalResult = `${timeoutNotice}The operation timed out before a complete answer could be generated. Here is the partial information gathered:
103321
+
103322
+ ${toolSummaries.join("\n\n---\n\n")}`;
103323
+ if (this.debug) {
103324
+ console.log(`[DEBUG] Graceful timeout: built fallback from ${toolSummaries.length} tool results`);
103325
+ }
103326
+ } else {
103327
+ finalResult = "The operation timed out before enough information could be gathered to provide an answer. Please try again with a simpler query or increase the timeout.";
103328
+ }
103329
+ }
103330
+ } catch (e) {
103331
+ if (this.debug) {
103332
+ console.log(`[DEBUG] Graceful timeout fallback error: ${e.message}`);
103333
+ }
103334
+ finalResult = "The operation timed out before enough information could be gathered to provide an answer. Please try again with a simpler query or increase the timeout.";
103335
+ }
103336
+ } else {
103337
+ finalResult = timeoutNotice + finalResult;
103338
+ }
103339
+ }
102654
103340
  const resultMessages = await aiResult.result.response?.messages;
102655
103341
  if (resultMessages) {
102656
103342
  for (const msg of resultMessages) {
102657
103343
  currentMessages.push(msg);
102658
103344
  }
102659
103345
  }
102660
- if (this.completionPrompt && !options._completionPromptProcessed && !completionPromptInjected && finalResult) {
103346
+ if (this.completionPrompt && !options._completionPromptProcessed && !completionPromptInjected && !abortSummaryTaken && finalResult) {
102661
103347
  completionPromptInjected = true;
102662
103348
  preCompletionResult = finalResult;
102663
103349
  if (this.debug) {
@@ -102729,6 +103415,118 @@ Double-check your response based on the criteria above. If everything looks good
102729
103415
  }
102730
103416
  break;
102731
103417
  } catch (error40) {
103418
+ if (gracefulTimeoutState.triggered && error40?.name === "AbortError") {
103419
+ if (this.debug) {
103420
+ console.log(`[DEBUG] Negotiated timeout: abort caught \u2014 making summary LLM call with conversation context`);
103421
+ }
103422
+ if (this.tracer) {
103423
+ this.tracer.addEvent("negotiated_timeout.abort_summary_started", {
103424
+ conversation_messages: currentMessages.length,
103425
+ has_schema: !!options.schema,
103426
+ has_tasks: !!(this.enableTasks && this.taskManager)
103427
+ });
103428
+ }
103429
+ try {
103430
+ let taskContext = "";
103431
+ if (this.enableTasks && this.taskManager) {
103432
+ const taskSummary = this.taskManager.getTaskSummary?.();
103433
+ if (taskSummary) {
103434
+ taskContext = `
103435
+
103436
+ ## Task Status
103437
+ ${taskSummary}
103438
+
103439
+ Acknowledge which tasks were completed and which were not.`;
103440
+ }
103441
+ }
103442
+ let schemaContext = "";
103443
+ if (options.schema) {
103444
+ try {
103445
+ const parsedSchema = typeof options.schema === "string" ? JSON.parse(options.schema) : options.schema;
103446
+ schemaContext = `
103447
+
103448
+ IMPORTANT: Your response MUST be valid JSON matching this schema:
103449
+ ${JSON.stringify(parsedSchema, null, 2)}
103450
+
103451
+ Respond with ONLY valid JSON \u2014 no markdown, no explanation, no text outside the JSON object. Include all findings and partial results within the JSON structure. If fields cannot be fully populated due to the interruption, use partial data or null values as appropriate.`;
103452
+ } catch {
103453
+ }
103454
+ }
103455
+ const summaryPrompt = `Your operation was interrupted by a timeout observer because the time limit was reached. Some of your tool calls were cancelled mid-execution.
103456
+
103457
+ Please provide a DETAILED summary of:
103458
+ 1. What you were asked to do (the original task)
103459
+ 2. What you accomplished \u2014 include ALL findings, code snippets, data, and conclusions you gathered
103460
+ 3. What was still in progress or not yet started
103461
+ 4. Any partial results or recommendations you can offer based on what you found so far${taskContext}${schemaContext}
103462
+
103463
+ Be thorough \u2014 this is the user's only response. Include all useful information you collected.`;
103464
+ const summaryMessages = [
103465
+ ...currentMessages,
103466
+ { role: "user", content: summaryPrompt }
103467
+ ];
103468
+ const modelInstance = this.provider ? this.provider(this.model) : this.model;
103469
+ const summaryFn = async () => {
103470
+ const summaryResult = await (0, import_ai6.generateText)({
103471
+ model: modelInstance,
103472
+ messages: this.prepareMessagesWithImages(summaryMessages),
103473
+ maxTokens: 4e3
103474
+ });
103475
+ if (this.tracer) {
103476
+ this.tracer.addEvent("negotiated_timeout.abort_summary_completed", {
103477
+ summary_length: summaryResult.text?.length || 0,
103478
+ usage_prompt_tokens: summaryResult.usage?.promptTokens,
103479
+ usage_completion_tokens: summaryResult.usage?.completionTokens
103480
+ });
103481
+ }
103482
+ if (summaryResult.usage) {
103483
+ this.tokenCounter.recordUsage(summaryResult.usage);
103484
+ }
103485
+ return summaryResult.text;
103486
+ };
103487
+ let summaryText;
103488
+ if (this.tracer) {
103489
+ summaryText = await this.tracer.withSpan("negotiated_timeout.abort_summary", summaryFn, {
103490
+ "summary.conversation_messages": currentMessages.length
103491
+ });
103492
+ } else {
103493
+ summaryText = await summaryFn();
103494
+ }
103495
+ if (options.schema) {
103496
+ finalResult = summaryText || "{}";
103497
+ } else {
103498
+ const timeoutNotice = "**Note: This response was generated under a time constraint. The timeout observer interrupted the operation because the time budget was exhausted.**\n\n";
103499
+ finalResult = timeoutNotice + (summaryText || "The operation was interrupted before a response could be generated.");
103500
+ }
103501
+ if (options.onStream && finalResult) {
103502
+ options.onStream(finalResult);
103503
+ }
103504
+ if (this.debug) {
103505
+ console.log(`[DEBUG] Negotiated timeout: summary produced ${summaryText?.length || 0} chars`);
103506
+ }
103507
+ } catch (summaryErr) {
103508
+ if (this.debug) {
103509
+ console.log(`[DEBUG] Negotiated timeout: summary call failed (${summaryErr.message}), falling back to partial text`);
103510
+ }
103511
+ if (this.tracer) {
103512
+ this.tracer.addEvent("negotiated_timeout.abort_summary_error", {
103513
+ error_message: summaryErr.message
103514
+ });
103515
+ }
103516
+ const partialTexts = currentMessages.filter((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim()).map((m) => m.content);
103517
+ if (options.schema) {
103518
+ finalResult = partialTexts.length > 0 ? partialTexts[partialTexts.length - 1] : "{}";
103519
+ } else {
103520
+ const timeoutNotice = "**Note: This response was generated under a time constraint. The operation was interrupted and some work was not completed.**\n\n";
103521
+ finalResult = partialTexts.length > 0 ? timeoutNotice + partialTexts[partialTexts.length - 1] : timeoutNotice + "The operation was interrupted before enough information could be gathered. Please try again with a simpler query or increase the timeout.";
103522
+ }
103523
+ if (options.onStream && finalResult) {
103524
+ options.onStream(finalResult);
103525
+ }
103526
+ }
103527
+ abortSummaryTaken = true;
103528
+ break;
103529
+ }
102732
103530
  if (!compactionAttempted && handleContextLimitError) {
102733
103531
  const compactionResult = handleContextLimitError(error40, currentMessages, {
102734
103532
  keepLastSegment: true,
@@ -102763,6 +103561,36 @@ Double-check your response based on the criteria above. If everything looks good
102763
103561
  }
102764
103562
  if (currentIteration >= maxIterations) {
102765
103563
  console.warn(`[WARN] Max tool iterations (${maxIterations}) reached for session ${this.sessionId}.`);
103564
+ if (!finalResult || finalResult === DEFAULT_MAX_ITER_MSG) {
103565
+ try {
103566
+ const searchQueries = [];
103567
+ const toolCounts = {};
103568
+ for (const tc of _toolCallLog) {
103569
+ toolCounts[tc.name] = (toolCounts[tc.name] || 0) + 1;
103570
+ if (tc.name === "search") {
103571
+ const q = tc.args.query || "";
103572
+ const exact = tc.args.exact ? " (exact)" : "";
103573
+ searchQueries.push(`"${q}"${exact}`);
103574
+ }
103575
+ }
103576
+ const toolBreakdown = Object.entries(toolCounts).map(([name15, count]) => `${name15}: ${count}x`).join(", ");
103577
+ const uniqueSearches = [...new Set(searchQueries)];
103578
+ let summary = `I was unable to complete your request after ${currentIteration} tool iterations.
103579
+
103580
+ `;
103581
+ summary += `Tool calls made: ${toolBreakdown || "none"}
103582
+ `;
103583
+ if (uniqueSearches.length > 0) {
103584
+ summary += `Search queries tried: ${uniqueSearches.join(", ")}
103585
+ `;
103586
+ }
103587
+ summary += `
103588
+ The search approach may be fundamentally wrong for this query. Consider: using exact=true for literal string matching, using bash/grep for pattern-based file searches, or trying a completely different strategy instead of repeating similar searches.`;
103589
+ finalResult = summary;
103590
+ } catch {
103591
+ finalResult = DEFAULT_MAX_ITER_MSG;
103592
+ }
103593
+ }
102766
103594
  }
102767
103595
  this.history = currentMessages.map((msg) => ({ ...msg }));
102768
103596
  if (this.history.length > MAX_HISTORY_MESSAGES) {
@@ -103297,6 +104125,134 @@ Double-check your response based on the criteria above. If everything looks good
103297
104125
  console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
103298
104126
  }
103299
104127
  }
104128
+ /**
104129
+ * Trigger graceful wind-down from outside (e.g., parent agent).
104130
+ * Unlike cancel(), this does NOT abort — it sets the graceful timeout flag
104131
+ * so the agent finishes its current step and then winds down naturally.
104132
+ */
104133
+ triggerGracefulWindDown() {
104134
+ if (this._gracefulTimeoutState && !this._gracefulTimeoutState.triggered) {
104135
+ this._gracefulTimeoutState.triggered = true;
104136
+ if (this.debug) {
104137
+ console.log(`[DEBUG] Graceful wind-down triggered externally for session ${this.sessionId}`);
104138
+ }
104139
+ if (this.tracer) {
104140
+ this.tracer.addEvent("graceful_stop.external_trigger", {
104141
+ "session.id": this.sessionId
104142
+ });
104143
+ }
104144
+ } else if (this.debug) {
104145
+ console.log(`[DEBUG] Graceful wind-down already active for session ${this.sessionId}, skipping`);
104146
+ }
104147
+ }
104148
+ /**
104149
+ * Initiate two-phase graceful stop: signal subagents and MCP servers to wind down,
104150
+ * then hard-abort after a deadline if they haven't finished.
104151
+ * @param {Object} gracefulTimeoutState - The graceful timeout state object from run()
104152
+ * @param {string} reason - Why the graceful stop was initiated
104153
+ */
104154
+ async _initiateGracefulStop(gracefulTimeoutState, reason) {
104155
+ if (gracefulTimeoutState.triggered) return;
104156
+ if (this.debug) {
104157
+ console.log(`[DEBUG] Initiating graceful stop: ${reason} (subagents: ${this._activeSubagents.size}, hasMcpBridge: ${!!this.mcpBridge}, deadline: ${this.gracefulStopDeadline}ms)`);
104158
+ }
104159
+ gracefulTimeoutState.triggered = true;
104160
+ if (this.tracer) {
104161
+ this.tracer.addEvent("graceful_stop.initiated", {
104162
+ "session.id": this.sessionId,
104163
+ "graceful_stop.reason": reason,
104164
+ "graceful_stop.active_subagents": this._activeSubagents.size,
104165
+ "graceful_stop.has_mcp_bridge": !!this.mcpBridge,
104166
+ "graceful_stop.deadline_ms": this.gracefulStopDeadline
104167
+ });
104168
+ }
104169
+ let subagentsSignalled = 0;
104170
+ let subagentErrors = 0;
104171
+ for (const [sid, subagent] of this._activeSubagents) {
104172
+ try {
104173
+ subagent.triggerGracefulWindDown();
104174
+ subagentsSignalled++;
104175
+ if (this.debug) {
104176
+ console.log(`[DEBUG] Triggered graceful wind-down on subagent ${sid}`);
104177
+ }
104178
+ } catch (e) {
104179
+ subagentErrors++;
104180
+ if (this.debug) {
104181
+ console.log(`[DEBUG] Failed to trigger wind-down on subagent ${sid}: ${e.message}`);
104182
+ }
104183
+ }
104184
+ }
104185
+ let mcpResults = [];
104186
+ if (this.mcpBridge) {
104187
+ try {
104188
+ mcpResults = await this.mcpBridge.callGracefulStopAll();
104189
+ if (this.debug && mcpResults.length > 0) {
104190
+ console.log(`[DEBUG] MCP graceful_stop results: ${JSON.stringify(mcpResults)}`);
104191
+ }
104192
+ } catch (e) {
104193
+ if (this.debug) {
104194
+ console.log(`[DEBUG] MCP graceful_stop failed: ${e.message}`);
104195
+ }
104196
+ }
104197
+ }
104198
+ if (this.tracer) {
104199
+ this.tracer.addEvent("graceful_stop.signals_sent", {
104200
+ "session.id": this.sessionId,
104201
+ "graceful_stop.subagents_signalled": subagentsSignalled,
104202
+ "graceful_stop.subagent_errors": subagentErrors,
104203
+ "graceful_stop.mcp_servers_called": mcpResults.filter((r) => r.success).length,
104204
+ "graceful_stop.mcp_servers_failed": mcpResults.filter((r) => !r.success).length,
104205
+ "graceful_stop.mcp_servers_total": mcpResults.length
104206
+ });
104207
+ }
104208
+ this._gracefulStopHardAbortId = setTimeout(() => {
104209
+ if (this.debug) {
104210
+ console.log(`[DEBUG] Graceful stop deadline (${this.gracefulStopDeadline}ms) expired \u2014 hard aborting`);
104211
+ }
104212
+ if (this.tracer) {
104213
+ this.tracer.addEvent("graceful_stop.deadline_expired", {
104214
+ "session.id": this.sessionId,
104215
+ "graceful_stop.deadline_ms": this.gracefulStopDeadline
104216
+ });
104217
+ }
104218
+ if (this._abortController) this._abortController.abort();
104219
+ }, this.gracefulStopDeadline);
104220
+ }
104221
+ /**
104222
+ * Register an active subagent for graceful stop coordination.
104223
+ * @param {string} sessionId
104224
+ * @param {ProbeAgent} subagent
104225
+ */
104226
+ _registerSubagent(sessionId, subagent) {
104227
+ this._activeSubagents.set(sessionId, subagent);
104228
+ if (this.debug) {
104229
+ console.log(`[DEBUG] Registered subagent ${sessionId} (active: ${this._activeSubagents.size})`);
104230
+ }
104231
+ if (this.tracer) {
104232
+ this.tracer.addEvent("subagent.registered", {
104233
+ "session.id": this.sessionId,
104234
+ "subagent.session_id": sessionId,
104235
+ "subagent.active_count": this._activeSubagents.size
104236
+ });
104237
+ }
104238
+ }
104239
+ /**
104240
+ * Unregister a completed subagent.
104241
+ * @param {string} sessionId
104242
+ */
104243
+ _unregisterSubagent(sessionId) {
104244
+ this._activeSubagents.delete(sessionId);
104245
+ if (this.debug) {
104246
+ console.log(`[DEBUG] Unregistered subagent ${sessionId} (active: ${this._activeSubagents.size})`);
104247
+ }
104248
+ if (this.tracer) {
104249
+ this.tracer.addEvent("subagent.unregistered", {
104250
+ "session.id": this.sessionId,
104251
+ "subagent.session_id": sessionId,
104252
+ "subagent.active_count": this._activeSubagents.size
104253
+ });
104254
+ }
104255
+ }
103300
104256
  /**
103301
104257
  * Get the abort signal for this agent.
103302
104258
  * Delegations and subagents should check this signal.