@probelabs/probe 0.6.0-rc295 → 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-rc295-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-rc295-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-rc295-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-rc295-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-rc295-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 +8 -2
  8. package/build/agent/ProbeAgent.js +683 -10
  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 +858 -32
  18. package/cjs/agent/simpleTelemetry.cjs +112 -0
  19. package/cjs/index.cjs +970 -32
  20. package/index.d.ts +26 -0
  21. package/package.json +1 -1
  22. package/src/agent/ProbeAgent.d.ts +8 -2
  23. package/src/agent/ProbeAgent.js +683 -10
  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}")`);
27619
27742
  }
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.";
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.";
27622
27745
  }
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.";
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.`;
27749
+ }
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;
@@ -99873,14 +100151,31 @@ var init_ProbeAgent = __esm({
99873
100151
  this.timeoutBehavior = options.timeoutBehavior ?? (() => {
99874
100152
  const val = process.env.TIMEOUT_BEHAVIOR;
99875
100153
  if (val === "hard") return "hard";
100154
+ if (val === "negotiated") return "negotiated";
99876
100155
  return "graceful";
99877
100156
  })();
99878
100157
  this.gracefulTimeoutBonusSteps = options.gracefulTimeoutBonusSteps ?? (() => {
99879
100158
  const parsed = parseInt(process.env.GRACEFUL_TIMEOUT_BONUS_STEPS, 10);
99880
100159
  return isNaN(parsed) || parsed < 1 || parsed > 20 ? 4 : parsed;
99881
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
+ })();
99882
100177
  if (this.debug) {
99883
- console.log(`[DEBUG] Timeout behavior: ${this.timeoutBehavior}, bonus steps: ${this.gracefulTimeoutBonusSteps}`);
100178
+ console.log(`[DEBUG] Timeout behavior: ${this.timeoutBehavior}, bonus steps: ${this.gracefulTimeoutBonusSteps}, graceful stop deadline: ${this.gracefulStopDeadline}ms`);
99884
100179
  }
99885
100180
  this.retryConfig = options.retry || {};
99886
100181
  this.retryManager = null;
@@ -100232,6 +100527,18 @@ var init_ProbeAgent = __esm({
100232
100527
  // Per-instance delegation limits
100233
100528
  parentAbortSignal: this._abortController.signal,
100234
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),
100235
100542
  outputBuffer: this._outputBuffer,
100236
100543
  concurrencyLimiter: this.concurrencyLimiter,
100237
100544
  // Global AI concurrency limiter
@@ -100816,7 +101123,7 @@ var init_ProbeAgent = __esm({
100816
101123
  }
100817
101124
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
100818
101125
  const gts = this._gracefulTimeoutState;
100819
- if (this.timeoutBehavior === "graceful" && gts) {
101126
+ if ((this.timeoutBehavior === "graceful" || this.timeoutBehavior === "negotiated") && gts) {
100820
101127
  } else {
100821
101128
  timeoutState.timeoutId = setTimeout(() => {
100822
101129
  controller.abort();
@@ -102203,6 +102510,7 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102203
102510
  } else {
102204
102511
  options = schemaOrOptions || {};
102205
102512
  }
102513
+ this._operationStartTime = Date.now();
102206
102514
  try {
102207
102515
  const oldHistoryLength = this.history.length;
102208
102516
  if (this._outputBuffer && !options?._schemaFormatted && !options?._completionPromptProcessed) {
@@ -102283,7 +102591,10 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102283
102591
  }
102284
102592
  }
102285
102593
  let currentIteration = 0;
102286
- 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;
102287
102598
  const baseMaxIterations = options._maxIterationsOverride || this.maxIterations || MAX_TOOL_ITERATIONS;
102288
102599
  const maxIterations = options._maxIterationsOverride ? baseMaxIterations : options.schema ? baseMaxIterations + 4 : baseMaxIterations;
102289
102600
  const isClaudeCode = this.clientApiProvider === "claude-code" || process.env.USE_CLAUDE_CODE === "true";
@@ -102423,6 +102734,220 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102423
102734
  bonusStepsMax: this.gracefulTimeoutBonusSteps
102424
102735
  };
102425
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;
102426
102951
  let compactionAttempted = false;
102427
102952
  while (true) {
102428
102953
  try {
@@ -102484,6 +103009,14 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102484
103009
  return false;
102485
103010
  },
102486
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
+ }
102487
103020
  if (gracefulTimeoutState.triggered) {
102488
103021
  gracefulTimeoutState.bonusStepsUsed++;
102489
103022
  const remaining = gracefulTimeoutState.bonusStepsMax - gracefulTimeoutState.bonusStepsUsed;
@@ -102509,8 +103042,12 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102509
103042
  return { toolChoice: "none" };
102510
103043
  }
102511
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(", ")}` : "";
102512
103048
  return {
102513
- 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}`
102514
103051
  };
102515
103052
  }
102516
103053
  if (steps.length >= 2) {
@@ -102589,6 +103126,11 @@ Double-check your response based on the criteria above. If everything looks good
102589
103126
  const { toolResults, toolCalls, text, reasoningText, finishReason, usage } = stepResult;
102590
103127
  currentIteration++;
102591
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
+ }
102592
103134
  if (this.tracer) {
102593
103135
  const stepEvent = {
102594
103136
  "iteration": currentIteration,
@@ -102680,6 +103222,14 @@ Double-check your response based on the criteria above. If everything looks good
102680
103222
  }, 6e4);
102681
103223
  }, this.maxOperationTimeout);
102682
103224
  }
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);
103232
+ }
102683
103233
  try {
102684
103234
  const steps = await result.steps;
102685
103235
  let finalText;
@@ -102700,6 +103250,12 @@ Double-check your response based on the criteria above. If everything looks good
102700
103250
  } finally {
102701
103251
  if (gracefulTimeoutId) clearTimeout(gracefulTimeoutId);
102702
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);
102703
103259
  }
102704
103260
  };
102705
103261
  let aiResult;
@@ -102739,7 +103295,7 @@ Double-check your response based on the criteria above. If everything looks good
102739
103295
  }
102740
103296
  if (gracefulTimeoutState.triggered) {
102741
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";
102742
- if (!finalResult || finalResult === "I was unable to complete your request due to reaching the maximum number of tool iterations.") {
103298
+ if (!finalResult || finalResult === DEFAULT_MAX_ITER_MSG || finalResult.startsWith("I was unable to complete your request after")) {
102743
103299
  try {
102744
103300
  const allText = await aiResult.result.text;
102745
103301
  if (allText && allText.trim()) {
@@ -102787,7 +103343,7 @@ ${toolSummaries.join("\n\n---\n\n")}`;
102787
103343
  currentMessages.push(msg);
102788
103344
  }
102789
103345
  }
102790
- if (this.completionPrompt && !options._completionPromptProcessed && !completionPromptInjected && finalResult) {
103346
+ if (this.completionPrompt && !options._completionPromptProcessed && !completionPromptInjected && !abortSummaryTaken && finalResult) {
102791
103347
  completionPromptInjected = true;
102792
103348
  preCompletionResult = finalResult;
102793
103349
  if (this.debug) {
@@ -102859,6 +103415,118 @@ Double-check your response based on the criteria above. If everything looks good
102859
103415
  }
102860
103416
  break;
102861
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
+ }
102862
103530
  if (!compactionAttempted && handleContextLimitError) {
102863
103531
  const compactionResult = handleContextLimitError(error40, currentMessages, {
102864
103532
  keepLastSegment: true,
@@ -102893,6 +103561,36 @@ Double-check your response based on the criteria above. If everything looks good
102893
103561
  }
102894
103562
  if (currentIteration >= maxIterations) {
102895
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
+ }
102896
103594
  }
102897
103595
  this.history = currentMessages.map((msg) => ({ ...msg }));
102898
103596
  if (this.history.length > MAX_HISTORY_MESSAGES) {
@@ -103427,6 +104125,134 @@ Double-check your response based on the criteria above. If everything looks good
103427
104125
  console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
103428
104126
  }
103429
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
+ }
103430
104256
  /**
103431
104257
  * Get the abort signal for this agent.
103432
104258
  * Delegations and subagents should check this signal.