@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
package/cjs/index.cjs CHANGED
@@ -27051,14 +27051,64 @@ function detectStuckResponse(response) {
27051
27051
  }
27052
27052
  return false;
27053
27053
  }
27054
+ function splitQuotedString(input) {
27055
+ const tokens = [];
27056
+ let current2 = "";
27057
+ let inQuote = null;
27058
+ let i = 0;
27059
+ while (i < input.length) {
27060
+ const ch = input[i];
27061
+ if (inQuote) {
27062
+ if (ch === "\\" && i + 1 < input.length) {
27063
+ current2 += input[i + 1];
27064
+ i += 2;
27065
+ continue;
27066
+ }
27067
+ if (ch === inQuote) {
27068
+ inQuote = null;
27069
+ i++;
27070
+ continue;
27071
+ }
27072
+ current2 += ch;
27073
+ i++;
27074
+ } else {
27075
+ if (ch === '"' || ch === "'") {
27076
+ inQuote = ch;
27077
+ i++;
27078
+ continue;
27079
+ }
27080
+ if (/[\s,]/.test(ch)) {
27081
+ if (current2.length > 0) {
27082
+ tokens.push(current2);
27083
+ current2 = "";
27084
+ }
27085
+ i++;
27086
+ continue;
27087
+ }
27088
+ current2 += ch;
27089
+ i++;
27090
+ }
27091
+ }
27092
+ if (current2.length > 0) {
27093
+ tokens.push(current2);
27094
+ }
27095
+ return tokens;
27096
+ }
27054
27097
  function parseTargets(targets) {
27055
27098
  if (!targets || typeof targets !== "string") {
27056
27099
  return [];
27057
27100
  }
27058
- return targets.split(/[\s,]+/).filter((f) => f.length > 0);
27101
+ return splitQuotedString(targets);
27059
27102
  }
27060
27103
  function parseAndResolvePaths(pathStr, cwd) {
27061
27104
  if (!pathStr) return [];
27105
+ if (/["']/.test(pathStr)) {
27106
+ const paths2 = splitQuotedString(pathStr);
27107
+ return paths2.map((p) => {
27108
+ if ((0, import_path5.isAbsolute)(p)) return p;
27109
+ return cwd ? (0, import_path5.resolve)(cwd, p) : p;
27110
+ });
27111
+ }
27062
27112
  let paths = pathStr.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
27063
27113
  paths = paths.flatMap((p) => {
27064
27114
  if (!/\s/.test(p)) return [p];
@@ -27068,9 +27118,7 @@ function parseAndResolvePaths(pathStr, cwd) {
27068
27118
  return allLookLikePaths ? parts : [p];
27069
27119
  });
27070
27120
  return paths.map((p) => {
27071
- if ((0, import_path5.isAbsolute)(p)) {
27072
- return p;
27073
- }
27121
+ if ((0, import_path5.isAbsolute)(p)) return p;
27074
27122
  return cwd ? (0, import_path5.resolve)(cwd, p) : p;
27075
27123
  });
27076
27124
  }
@@ -27103,7 +27151,7 @@ var init_common = __esm({
27103
27151
  searchSchema = external_exports2.object({
27104
27152
  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."),
27105
27153
  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.'),
27106
- 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.'),
27154
+ 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.`),
27107
27155
  maxTokens: external_exports2.number().nullable().optional().describe("Maximum tokens to return. Default is 20000. Set to null for unlimited results."),
27108
27156
  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."),
27109
27157
  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.")
@@ -73542,6 +73590,9 @@ function isMethodAllowed(methodName, allowedMethods, blockedMethods) {
73542
73590
  }
73543
73591
  function createTransport(serverConfig) {
73544
73592
  const { transport, command, args, url: url2, env } = serverConfig;
73593
+ if (serverConfig.transportInstance) {
73594
+ return serverConfig.transportInstance;
73595
+ }
73545
73596
  switch (transport) {
73546
73597
  case "stdio":
73547
73598
  return new import_stdio.StdioClientTransport({
@@ -73904,6 +73955,53 @@ var init_client = __esm({
73904
73955
  throw error40;
73905
73956
  }
73906
73957
  }
73958
+ /**
73959
+ * Call graceful_stop on all MCP servers that expose it.
73960
+ * This signals agent-type MCP servers to wrap up their work.
73961
+ * @returns {Promise<Array<{server: string, success: boolean, error?: string}>>}
73962
+ */
73963
+ async callGracefulStopAll() {
73964
+ const results = [];
73965
+ for (const [serverName, clientInfo] of this.clients) {
73966
+ const qualifiedName = `${serverName}_graceful_stop`;
73967
+ if (this.tools.has(qualifiedName)) {
73968
+ if (this.debug) {
73969
+ console.log(`[DEBUG] MCP callGracefulStopAll: calling graceful_stop on server "${serverName}"`);
73970
+ }
73971
+ try {
73972
+ const timeoutMs = 5e3;
73973
+ const timeoutPromise = new Promise(
73974
+ (_, reject2) => setTimeout(() => reject2(new Error("graceful_stop timeout")), timeoutMs)
73975
+ );
73976
+ await Promise.race([
73977
+ clientInfo.client.callTool({ name: "graceful_stop", arguments: {} }, void 0, { timeout: timeoutMs }),
73978
+ timeoutPromise
73979
+ ]);
73980
+ results.push({ server: serverName, success: true });
73981
+ if (this.debug) {
73982
+ console.log(`[DEBUG] MCP callGracefulStopAll: server "${serverName}" acknowledged graceful_stop`);
73983
+ }
73984
+ } catch (e) {
73985
+ results.push({ server: serverName, success: false, error: e.message });
73986
+ if (this.debug) {
73987
+ console.log(`[DEBUG] MCP callGracefulStopAll: server "${serverName}" graceful_stop failed: ${e.message}`);
73988
+ }
73989
+ }
73990
+ }
73991
+ }
73992
+ if (this.debug) {
73993
+ const withStop = results.length;
73994
+ const total = this.clients.size;
73995
+ console.log(`[DEBUG] MCP callGracefulStopAll: ${withStop}/${total} servers had graceful_stop tool`);
73996
+ }
73997
+ this.recordMcpEvent("graceful_stop.sweep_completed", {
73998
+ servers_total: this.clients.size,
73999
+ servers_with_graceful_stop: results.length,
74000
+ servers_acknowledged: results.filter((r) => r.success).length,
74001
+ servers_failed: results.filter((r) => !r.success).length
74002
+ });
74003
+ return results;
74004
+ }
73907
74005
  /**
73908
74006
  * Get all available tools with their schemas
73909
74007
  * @returns {Object} Map of tool name to tool definition
@@ -73931,10 +74029,29 @@ var init_client = __esm({
73931
74029
  inputSchema: tool6.inputSchema,
73932
74030
  execute: async (args) => {
73933
74031
  const result = await this.callTool(name15, args);
73934
- if (result.content && result.content[0]) {
73935
- return result.content[0].text;
74032
+ if (!result.content || !result.content[0]) {
74033
+ return JSON.stringify(result);
74034
+ }
74035
+ const hasImage = result.content.some((block) => block.type === "image");
74036
+ if (hasImage) {
74037
+ return { _mcpContent: result.content };
73936
74038
  }
73937
- return JSON.stringify(result);
74039
+ return result.content[0].text;
74040
+ },
74041
+ // Convert MCP content blocks (including images) to Vercel AI SDK format
74042
+ toModelOutput: ({ output }) => {
74043
+ if (output && typeof output === "object" && output._mcpContent) {
74044
+ const parts = [];
74045
+ for (const block of output._mcpContent) {
74046
+ if (block.type === "text") {
74047
+ parts.push({ type: "text", text: block.text });
74048
+ } else if (block.type === "image") {
74049
+ parts.push({ type: "image-data", data: block.data, mediaType: block.mimeType });
74050
+ }
74051
+ }
74052
+ return { type: "content", value: parts };
74053
+ }
74054
+ return { type: "text", value: typeof output === "string" ? output : JSON.stringify(output) };
73938
74055
  }
73939
74056
  };
73940
74057
  }
@@ -74114,6 +74231,16 @@ var init_xmlBridge = __esm({
74114
74231
  isMcpTool(toolName) {
74115
74232
  return toolName in this.mcpTools;
74116
74233
  }
74234
+ /**
74235
+ * Call graceful_stop on all MCP servers that expose it.
74236
+ * @returns {Promise<Array>}
74237
+ */
74238
+ async callGracefulStopAll() {
74239
+ if (this.mcpManager) {
74240
+ return this.mcpManager.callGracefulStopAll();
74241
+ }
74242
+ return [];
74243
+ }
74117
74244
  /**
74118
74245
  * Clean up MCP connections
74119
74246
  */
@@ -96673,6 +96800,7 @@ var init_ProbeAgent = __esm({
96673
96800
  this.debug = options.debug || process.env.DEBUG === "1";
96674
96801
  this.cancelled = false;
96675
96802
  this._abortController = new AbortController();
96803
+ this._activeSubagents = /* @__PURE__ */ new Map();
96676
96804
  this.tracer = options.tracer || null;
96677
96805
  this.outline = !!options.outline;
96678
96806
  this.searchDelegate = options.searchDelegate !== void 0 ? !!options.searchDelegate : true;
@@ -96781,6 +96909,35 @@ var init_ProbeAgent = __esm({
96781
96909
  if (this.debug) {
96782
96910
  console.log(`[DEBUG] Max operation timeout: ${this.maxOperationTimeout}ms`);
96783
96911
  }
96912
+ this.timeoutBehavior = options.timeoutBehavior ?? (() => {
96913
+ const val = process.env.TIMEOUT_BEHAVIOR;
96914
+ if (val === "hard") return "hard";
96915
+ if (val === "negotiated") return "negotiated";
96916
+ return "graceful";
96917
+ })();
96918
+ this.gracefulTimeoutBonusSteps = options.gracefulTimeoutBonusSteps ?? (() => {
96919
+ const parsed = parseInt(process.env.GRACEFUL_TIMEOUT_BONUS_STEPS, 10);
96920
+ return isNaN(parsed) || parsed < 1 || parsed > 20 ? 4 : parsed;
96921
+ })();
96922
+ this.negotiatedTimeoutBudget = options.negotiatedTimeoutBudget ?? (() => {
96923
+ const parsed = parseInt(process.env.NEGOTIATED_TIMEOUT_BUDGET, 10);
96924
+ return isNaN(parsed) || parsed < 6e4 || parsed > 72e5 ? 18e5 : parsed;
96925
+ })();
96926
+ this.negotiatedTimeoutMaxRequests = options.negotiatedTimeoutMaxRequests ?? (() => {
96927
+ const parsed = parseInt(process.env.NEGOTIATED_TIMEOUT_MAX_REQUESTS, 10);
96928
+ return isNaN(parsed) || parsed < 1 || parsed > 10 ? 3 : parsed;
96929
+ })();
96930
+ this.negotiatedTimeoutMaxPerRequest = options.negotiatedTimeoutMaxPerRequest ?? (() => {
96931
+ const parsed = parseInt(process.env.NEGOTIATED_TIMEOUT_MAX_PER_REQUEST, 10);
96932
+ return isNaN(parsed) || parsed < 6e4 || parsed > 36e5 ? 6e5 : parsed;
96933
+ })();
96934
+ this.gracefulStopDeadline = options.gracefulStopDeadline ?? (() => {
96935
+ const parsed = parseInt(process.env.GRACEFUL_STOP_DEADLINE, 10);
96936
+ return isNaN(parsed) || parsed < 5e3 || parsed > 3e5 ? 45e3 : parsed;
96937
+ })();
96938
+ if (this.debug) {
96939
+ console.log(`[DEBUG] Timeout behavior: ${this.timeoutBehavior}, bonus steps: ${this.gracefulTimeoutBonusSteps}, graceful stop deadline: ${this.gracefulStopDeadline}ms`);
96940
+ }
96784
96941
  this.retryConfig = options.retry || {};
96785
96942
  this.retryManager = null;
96786
96943
  this.fallbackConfig = options.fallback || null;
@@ -97131,6 +97288,18 @@ var init_ProbeAgent = __esm({
97131
97288
  // Per-instance delegation limits
97132
97289
  parentAbortSignal: this._abortController.signal,
97133
97290
  // Propagate cancellation to delegations
97291
+ // Timeout settings for delegate subagents to inherit
97292
+ timeoutBehavior: this.timeoutBehavior,
97293
+ maxOperationTimeout: this.maxOperationTimeout,
97294
+ requestTimeout: this.requestTimeout,
97295
+ gracefulTimeoutBonusSteps: this.gracefulTimeoutBonusSteps,
97296
+ negotiatedTimeoutBudget: this.negotiatedTimeoutBudget,
97297
+ negotiatedTimeoutMaxRequests: this.negotiatedTimeoutMaxRequests,
97298
+ negotiatedTimeoutMaxPerRequest: this.negotiatedTimeoutMaxPerRequest,
97299
+ parentOperationStartTime: this._operationStartTime,
97300
+ // For remaining budget calculation
97301
+ onSubagentCreated: (sid, subagent) => this._registerSubagent(sid, subagent),
97302
+ onSubagentCompleted: (sid) => this._unregisterSubagent(sid),
97134
97303
  outputBuffer: this._outputBuffer,
97135
97304
  concurrencyLimiter: this.concurrencyLimiter,
97136
97305
  // Global AI concurrency limiter
@@ -97714,12 +97883,16 @@ var init_ProbeAgent = __esm({
97714
97883
  }, { once: true });
97715
97884
  }
97716
97885
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
97717
- timeoutState.timeoutId = setTimeout(() => {
97718
- controller.abort();
97719
- if (this.debug) {
97720
- console.log(`[DEBUG] Operation timed out after ${this.maxOperationTimeout}ms (max operation timeout)`);
97721
- }
97722
- }, this.maxOperationTimeout);
97886
+ const gts = this._gracefulTimeoutState;
97887
+ if ((this.timeoutBehavior === "graceful" || this.timeoutBehavior === "negotiated") && gts) {
97888
+ } else {
97889
+ timeoutState.timeoutId = setTimeout(() => {
97890
+ controller.abort();
97891
+ if (this.debug) {
97892
+ console.log(`[DEBUG] Operation timed out after ${this.maxOperationTimeout}ms (max operation timeout)`);
97893
+ }
97894
+ }, this.maxOperationTimeout);
97895
+ }
97723
97896
  }
97724
97897
  try {
97725
97898
  const useClaudeCode = this.clientApiProvider === "claude-code" || process.env.USE_CLAUDE_CODE === "true";
@@ -99098,6 +99271,7 @@ You are working with a workspace. Available paths: ${workspaceDesc}
99098
99271
  } else {
99099
99272
  options = schemaOrOptions || {};
99100
99273
  }
99274
+ this._operationStartTime = Date.now();
99101
99275
  try {
99102
99276
  const oldHistoryLength = this.history.length;
99103
99277
  if (this._outputBuffer && !options?._schemaFormatted && !options?._completionPromptProcessed) {
@@ -99178,7 +99352,10 @@ You are working with a workspace. Available paths: ${workspaceDesc}
99178
99352
  }
99179
99353
  }
99180
99354
  let currentIteration = 0;
99181
- let finalResult = "I was unable to complete your request due to reaching the maximum number of tool iterations.";
99355
+ let finalResult = null;
99356
+ const DEFAULT_MAX_ITER_MSG = "I was unable to complete your request due to reaching the maximum number of tool iterations.";
99357
+ const _toolCallLog = [];
99358
+ let abortSummaryTaken = false;
99182
99359
  const baseMaxIterations = options._maxIterationsOverride || this.maxIterations || MAX_TOOL_ITERATIONS;
99183
99360
  const maxIterations = options._maxIterationsOverride ? baseMaxIterations : options.schema ? baseMaxIterations + 4 : baseMaxIterations;
99184
99361
  const isClaudeCode = this.clientApiProvider === "claude-code" || process.env.USE_CLAUDE_CODE === "true";
@@ -99310,6 +99487,228 @@ You are working with a workspace. Available paths: ${workspaceDesc}
99310
99487
  }
99311
99488
  let completionPromptInjected = false;
99312
99489
  let preCompletionResult = null;
99490
+ const gracefulTimeoutState = {
99491
+ triggered: false,
99492
+ // Set to true when soft timeout fires
99493
+ bonusStepsUsed: 0,
99494
+ // Steps taken after soft timeout
99495
+ bonusStepsMax: this.gracefulTimeoutBonusSteps
99496
+ };
99497
+ this._gracefulTimeoutState = gracefulTimeoutState;
99498
+ const negotiatedTimeoutState = {
99499
+ extensionsUsed: 0,
99500
+ totalExtraTimeMs: 0,
99501
+ softTimeoutId: null,
99502
+ hardAbortTimeoutId: null,
99503
+ maxRequests: this.negotiatedTimeoutMaxRequests,
99504
+ maxPerRequestMs: this.negotiatedTimeoutMaxPerRequest,
99505
+ budgetMs: this.negotiatedTimeoutBudget,
99506
+ observerRunning: false,
99507
+ // true while observer LLM call is in flight
99508
+ extensionMessage: null,
99509
+ // message to show in prepareStep after extension granted
99510
+ startTime: Date.now()
99511
+ };
99512
+ this._negotiatedTimeoutState = negotiatedTimeoutState;
99513
+ const activeTools = /* @__PURE__ */ new Map();
99514
+ this._activeTools = activeTools;
99515
+ const onToolCall = (event) => {
99516
+ const key = event.toolCallId || `${event.name}:${JSON.stringify(event.args || {}).slice(0, 100)}`;
99517
+ if (event.status === "started") {
99518
+ activeTools.set(key, {
99519
+ name: event.name,
99520
+ args: event.args,
99521
+ startedAt: event.timestamp || (/* @__PURE__ */ new Date()).toISOString()
99522
+ });
99523
+ } else if (event.status === "completed" || event.status === "error") {
99524
+ activeTools.delete(key);
99525
+ }
99526
+ };
99527
+ this.events.on("toolCall", onToolCall);
99528
+ const runTimeoutObserver = async () => {
99529
+ if (negotiatedTimeoutState.observerRunning) return;
99530
+ negotiatedTimeoutState.observerRunning = true;
99531
+ const remainingRequests = negotiatedTimeoutState.maxRequests - negotiatedTimeoutState.extensionsUsed;
99532
+ const remainingBudgetMs = negotiatedTimeoutState.budgetMs - negotiatedTimeoutState.totalExtraTimeMs;
99533
+ const maxPerReqMin = Math.round(negotiatedTimeoutState.maxPerRequestMs / 6e4);
99534
+ const elapsedMin = Math.round((Date.now() - negotiatedTimeoutState.startTime) / 6e4);
99535
+ if (remainingRequests <= 0 || remainingBudgetMs <= 0) {
99536
+ if (this.debug) {
99537
+ console.log(`[DEBUG] Timeout observer: no extensions/budget remaining \u2014 aborting in-flight tools and triggering graceful wind-down`);
99538
+ }
99539
+ if (this.tracer) {
99540
+ this.tracer.addEvent("negotiated_timeout.observer_exhausted", {
99541
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
99542
+ max_requests: negotiatedTimeoutState.maxRequests,
99543
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
99544
+ budget_ms: negotiatedTimeoutState.budgetMs,
99545
+ elapsed_min: elapsedMin,
99546
+ active_tools: Array.from(activeTools.values()).map((t) => t.name)
99547
+ });
99548
+ }
99549
+ await this._initiateGracefulStop(gracefulTimeoutState, "budget/extensions exhausted");
99550
+ negotiatedTimeoutState.observerRunning = false;
99551
+ return;
99552
+ }
99553
+ const activeToolsList = Array.from(activeTools.values());
99554
+ const now = Date.now();
99555
+ const formatDuration = (ms) => {
99556
+ const totalSec = Math.round(ms / 1e3);
99557
+ if (totalSec < 60) return `${totalSec}s`;
99558
+ const min = Math.floor(totalSec / 60);
99559
+ const sec = totalSec % 60;
99560
+ if (min < 60) return `${min}m ${sec}s`;
99561
+ const hr = Math.floor(min / 60);
99562
+ const remainMin = min % 60;
99563
+ return `${hr}h ${remainMin}m`;
99564
+ };
99565
+ const activeToolsDesc = activeToolsList.length > 0 ? activeToolsList.map((t) => {
99566
+ const runningForMs = now - new Date(t.startedAt).getTime();
99567
+ return `- ${t.name}(${JSON.stringify(t.args || {}).slice(0, 200)}) \u2014 running for ${formatDuration(runningForMs)}`;
99568
+ }).join("\n") : "(none currently running)";
99569
+ const recentHistory = this.history.slice(-6).map((msg) => {
99570
+ const content = typeof msg.content === "string" ? msg.content.slice(0, 300) : JSON.stringify(msg.content).slice(0, 300);
99571
+ return `[${msg.role}]: ${content}`;
99572
+ }).join("\n");
99573
+ 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.
99574
+
99575
+ ## Recent Conversation
99576
+ ${recentHistory || "(no history yet)"}
99577
+
99578
+ ## Currently Running Tools
99579
+ ${activeToolsDesc}
99580
+
99581
+ ## Budget
99582
+ - Extensions used: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}
99583
+ - Time budget remaining: ${Math.round(remainingBudgetMs / 6e4)} minutes
99584
+ - Max per extension: ${maxPerReqMin} minutes
99585
+
99586
+ Decide whether the agent should get more time. EXTEND if:
99587
+ - Tools are actively running (especially delegates or complex analysis) \u2014 they need time to finish
99588
+ - The agent is making clear progress on a complex task
99589
+ - New information is being gathered that will improve the final answer
99590
+
99591
+ DO NOT EXTEND if:
99592
+ - The agent appears stuck in a loop (repeating the same tool calls or getting the same errors)
99593
+ - The conversation shows the agent retrying failed operations without changing approach
99594
+ - The agent has enough information to answer but keeps searching for more
99595
+ - Tool calls are returning empty or error results repeatedly
99596
+ - The agent is doing redundant work (searching for things it already found)
99597
+
99598
+ 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.
99599
+
99600
+ Respond with ONLY valid JSON (no markdown, no explanation):
99601
+ {"extend": true, "minutes": <1-${maxPerReqMin}>, "reason": "your reason here"}
99602
+ or
99603
+ {"extend": false, "reason": "your reason here"}`;
99604
+ const observerFn = async () => {
99605
+ const modelInstance = this.provider ? this.provider(this.model) : this.model;
99606
+ if (this.debug) {
99607
+ console.log(`[DEBUG] Timeout observer: making LLM call (${activeToolsList.length} active tools, ${elapsedMin} min elapsed)`);
99608
+ }
99609
+ if (this.tracer) {
99610
+ this.tracer.addEvent("negotiated_timeout.observer_invoked", {
99611
+ elapsed_min: elapsedMin,
99612
+ active_tools: activeToolsList.map((t) => t.name),
99613
+ active_tools_detail: activeToolsList.map((t) => ({
99614
+ name: t.name,
99615
+ running_for_ms: now - new Date(t.startedAt).getTime(),
99616
+ args_preview: JSON.stringify(t.args || {}).slice(0, 100)
99617
+ })),
99618
+ active_tools_count: activeToolsList.length,
99619
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
99620
+ remaining_requests: remainingRequests,
99621
+ remaining_budget_ms: remainingBudgetMs,
99622
+ history_length: this.history.length
99623
+ });
99624
+ }
99625
+ const observerResult = await (0, import_ai4.generateText)({
99626
+ model: modelInstance,
99627
+ messages: [{ role: "user", content: observerPrompt }],
99628
+ maxTokens: 500
99629
+ });
99630
+ const responseText = observerResult.text.trim();
99631
+ if (this.tracer) {
99632
+ this.tracer.addEvent("negotiated_timeout.observer_response", {
99633
+ response_text: responseText,
99634
+ usage_prompt_tokens: observerResult.usage?.promptTokens,
99635
+ usage_completion_tokens: observerResult.usage?.completionTokens
99636
+ });
99637
+ }
99638
+ const jsonStr = responseText.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
99639
+ const decision = JSON.parse(jsonStr);
99640
+ if (decision.extend && decision.minutes > 0) {
99641
+ const requestedMs = Math.min(decision.minutes, maxPerReqMin) * 6e4;
99642
+ const grantedMs = Math.min(requestedMs, remainingBudgetMs, negotiatedTimeoutState.maxPerRequestMs);
99643
+ const grantedMin = Math.round(grantedMs / 6e4 * 10) / 10;
99644
+ negotiatedTimeoutState.extensionsUsed++;
99645
+ negotiatedTimeoutState.totalExtraTimeMs += grantedMs;
99646
+ 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.`;
99647
+ negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
99648
+ runTimeoutObserver();
99649
+ }, grantedMs);
99650
+ if (this.debug) {
99651
+ console.log(`[DEBUG] Timeout observer: granted ${grantedMin} min (reason: ${decision.reason}). Extensions: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}`);
99652
+ }
99653
+ if (this.tracer) {
99654
+ this.tracer.addEvent("negotiated_timeout.observer_extended", {
99655
+ decision_reason: decision.reason,
99656
+ requested_minutes: decision.minutes,
99657
+ granted_ms: grantedMs,
99658
+ granted_min: grantedMin,
99659
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
99660
+ max_requests: negotiatedTimeoutState.maxRequests,
99661
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
99662
+ budget_remaining_ms: remainingBudgetMs - grantedMs,
99663
+ active_tools: activeToolsList.map((t) => t.name),
99664
+ active_tools_count: activeToolsList.length
99665
+ });
99666
+ }
99667
+ } else {
99668
+ if (this.debug) {
99669
+ console.log(`[DEBUG] Timeout observer: declined extension (reason: ${decision.reason}). Initiating graceful stop.`);
99670
+ }
99671
+ if (this.tracer) {
99672
+ this.tracer.addEvent("negotiated_timeout.observer_declined", {
99673
+ decision_reason: decision.reason,
99674
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
99675
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
99676
+ elapsed_min: elapsedMin,
99677
+ active_tools: activeToolsList.map((t) => t.name)
99678
+ });
99679
+ }
99680
+ await this._initiateGracefulStop(gracefulTimeoutState, `observer declined: ${decision.reason}`);
99681
+ }
99682
+ };
99683
+ try {
99684
+ if (this.tracer) {
99685
+ await this.tracer.withSpan("negotiated_timeout.observer", observerFn, {
99686
+ "timeout.elapsed_min": elapsedMin,
99687
+ "timeout.extensions_used": negotiatedTimeoutState.extensionsUsed,
99688
+ "timeout.active_tools_count": activeToolsList.length,
99689
+ "timeout.remaining_budget_ms": remainingBudgetMs
99690
+ });
99691
+ } else {
99692
+ await observerFn();
99693
+ }
99694
+ } catch (err) {
99695
+ if (this.debug) {
99696
+ console.log(`[DEBUG] Timeout observer: LLM call failed (${err.message}). Initiating graceful stop.`);
99697
+ }
99698
+ if (this.tracer) {
99699
+ this.tracer.addEvent("negotiated_timeout.observer_error", {
99700
+ error_message: err.message,
99701
+ error_name: err.name,
99702
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
99703
+ elapsed_min: elapsedMin
99704
+ });
99705
+ }
99706
+ await this._initiateGracefulStop(gracefulTimeoutState, `observer error: ${err.message}`);
99707
+ } finally {
99708
+ negotiatedTimeoutState.observerRunning = false;
99709
+ }
99710
+ };
99711
+ negotiatedTimeoutState.runObserver = runTimeoutObserver;
99313
99712
  let compactionAttempted = false;
99314
99713
  while (true) {
99315
99714
  try {
@@ -99319,6 +99718,15 @@ You are working with a workspace. Available paths: ${workspaceDesc}
99319
99718
  messages: messagesForAI,
99320
99719
  tools: tools2,
99321
99720
  stopWhen: ({ steps }) => {
99721
+ if (gracefulTimeoutState.triggered) {
99722
+ if (gracefulTimeoutState.bonusStepsUsed >= gracefulTimeoutState.bonusStepsMax) {
99723
+ if (this.debug) {
99724
+ console.log(`[DEBUG] stopWhen: graceful timeout bonus steps exhausted (${gracefulTimeoutState.bonusStepsUsed}/${gracefulTimeoutState.bonusStepsMax}), forcing stop`);
99725
+ }
99726
+ return true;
99727
+ }
99728
+ return false;
99729
+ }
99322
99730
  if (steps.length >= maxIterations) return true;
99323
99731
  const lastStep = steps[steps.length - 1];
99324
99732
  const modelWantsToStop = lastStep?.finishReason === "stop" && (!lastStep?.toolCalls || lastStep.toolCalls.length === 0);
@@ -99362,9 +99770,45 @@ You are working with a workspace. Available paths: ${workspaceDesc}
99362
99770
  return false;
99363
99771
  },
99364
99772
  prepareStep: ({ steps, stepNumber }) => {
99773
+ if (negotiatedTimeoutState.extensionMessage && !gracefulTimeoutState.triggered) {
99774
+ const msg = negotiatedTimeoutState.extensionMessage;
99775
+ negotiatedTimeoutState.extensionMessage = null;
99776
+ if (this.debug) {
99777
+ console.log(`[DEBUG] prepareStep: delivering timeout observer extension message`);
99778
+ }
99779
+ return { userMessage: msg };
99780
+ }
99781
+ if (gracefulTimeoutState.triggered) {
99782
+ gracefulTimeoutState.bonusStepsUsed++;
99783
+ const remaining = gracefulTimeoutState.bonusStepsMax - gracefulTimeoutState.bonusStepsUsed;
99784
+ if (gracefulTimeoutState.bonusStepsUsed === 1) {
99785
+ if (this.debug) {
99786
+ console.log(`[DEBUG] prepareStep: graceful timeout wind-down step 1/${gracefulTimeoutState.bonusStepsMax}`);
99787
+ }
99788
+ if (this.tracer) {
99789
+ this.tracer.addEvent("graceful_timeout.wind_down_started", {
99790
+ bonus_steps_max: gracefulTimeoutState.bonusStepsMax,
99791
+ current_iteration: currentIteration,
99792
+ max_iterations: maxIterations
99793
+ });
99794
+ }
99795
+ return {
99796
+ toolChoice: "none",
99797
+ 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.`
99798
+ };
99799
+ }
99800
+ if (this.debug) {
99801
+ console.log(`[DEBUG] prepareStep: graceful timeout wind-down step ${gracefulTimeoutState.bonusStepsUsed}/${gracefulTimeoutState.bonusStepsMax} (${remaining} remaining)`);
99802
+ }
99803
+ return { toolChoice: "none" };
99804
+ }
99365
99805
  if (stepNumber === maxIterations - 1) {
99806
+ 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);
99807
+ const searchSummary = searchesTried.length > 0 ? `
99808
+ Searches attempted: ${searchesTried.join(", ")}` : "";
99366
99809
  return {
99367
- toolChoice: "none"
99810
+ toolChoice: "none",
99811
+ 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}`
99368
99812
  };
99369
99813
  }
99370
99814
  if (steps.length >= 2) {
@@ -99443,6 +99887,11 @@ Double-check your response based on the criteria above. If everything looks good
99443
99887
  const { toolResults, toolCalls, text, reasoningText, finishReason, usage } = stepResult;
99444
99888
  currentIteration++;
99445
99889
  toolContext.currentIteration = currentIteration;
99890
+ if (toolCalls?.length > 0) {
99891
+ for (const tc of toolCalls) {
99892
+ _toolCallLog.push({ name: tc.toolName, args: tc.args || {} });
99893
+ }
99894
+ }
99446
99895
  if (this.tracer) {
99447
99896
  const stepEvent = {
99448
99897
  "iteration": currentIteration,
@@ -99465,6 +99914,12 @@ Double-check your response based on the criteria above. If everything looks good
99465
99914
  }));
99466
99915
  }
99467
99916
  this.tracer.addEvent("iteration.step", stepEvent);
99917
+ if (gracefulTimeoutState.triggered) {
99918
+ this.tracer.addEvent("graceful_timeout.wind_down_step", {
99919
+ bonus_step: gracefulTimeoutState.bonusStepsUsed,
99920
+ bonus_max: gracefulTimeoutState.bonusStepsMax
99921
+ });
99922
+ }
99468
99923
  }
99469
99924
  if (usage) {
99470
99925
  this.tokenCounter.recordUsage(usage);
@@ -99510,22 +99965,59 @@ Double-check your response based on the criteria above. If everything looks good
99510
99965
  }
99511
99966
  const executeAIRequest = async () => {
99512
99967
  const result = await this.streamTextWithRetryAndFallback(streamOptions);
99513
- const steps = await result.steps;
99514
- let finalText;
99515
- if (steps && steps.length > 1) {
99516
- const lastStepText = steps[steps.length - 1].text;
99517
- finalText = lastStepText || await result.text;
99518
- } else {
99519
- finalText = await result.text;
99968
+ let gracefulTimeoutId = null;
99969
+ let hardAbortTimeoutId = null;
99970
+ if (this.timeoutBehavior === "graceful" && gracefulTimeoutState && this.maxOperationTimeout > 0) {
99971
+ gracefulTimeoutId = setTimeout(() => {
99972
+ gracefulTimeoutState.triggered = true;
99973
+ if (this.debug) {
99974
+ console.log(`[DEBUG] Soft timeout after ${this.maxOperationTimeout}ms \u2014 entering wind-down mode (${gracefulTimeoutState.bonusStepsMax} bonus steps)`);
99975
+ }
99976
+ hardAbortTimeoutId = setTimeout(() => {
99977
+ if (this._abortController) {
99978
+ this._abortController.abort();
99979
+ }
99980
+ if (this.debug) {
99981
+ console.log(`[DEBUG] Hard abort \u2014 wind-down safety net expired after 60s`);
99982
+ }
99983
+ }, 6e4);
99984
+ }, this.maxOperationTimeout);
99520
99985
  }
99521
- if (this.debug) {
99522
- console.log(`[DEBUG] streamText completed: ${steps?.length || 0} steps, finalText=${finalText?.length || 0} chars`);
99986
+ if (this.timeoutBehavior === "negotiated" && this.maxOperationTimeout > 0) {
99987
+ negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
99988
+ if (this.debug) {
99989
+ console.log(`[DEBUG] Soft timeout after ${this.maxOperationTimeout}ms \u2014 invoking timeout observer`);
99990
+ }
99991
+ runTimeoutObserver();
99992
+ }, this.maxOperationTimeout);
99523
99993
  }
99524
- const usage = await result.usage;
99525
- if (usage) {
99526
- this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
99994
+ try {
99995
+ const steps = await result.steps;
99996
+ let finalText;
99997
+ if (steps && steps.length > 1) {
99998
+ const lastStepText = steps[steps.length - 1].text;
99999
+ finalText = lastStepText || await result.text;
100000
+ } else {
100001
+ finalText = await result.text;
100002
+ }
100003
+ if (this.debug) {
100004
+ console.log(`[DEBUG] streamText completed: ${steps?.length || 0} steps, finalText=${finalText?.length || 0} chars`);
100005
+ }
100006
+ const usage = await result.usage;
100007
+ if (usage) {
100008
+ this.tokenCounter.recordUsage(usage, result.experimental_providerMetadata);
100009
+ }
100010
+ return { finalText, result };
100011
+ } finally {
100012
+ if (gracefulTimeoutId) clearTimeout(gracefulTimeoutId);
100013
+ if (hardAbortTimeoutId) clearTimeout(hardAbortTimeoutId);
100014
+ if (negotiatedTimeoutState.softTimeoutId) clearTimeout(negotiatedTimeoutState.softTimeoutId);
100015
+ if (this._gracefulStopHardAbortId) {
100016
+ clearTimeout(this._gracefulStopHardAbortId);
100017
+ this._gracefulStopHardAbortId = null;
100018
+ }
100019
+ this.events.removeListener("toolCall", onToolCall);
99527
100020
  }
99528
- return { finalText, result };
99529
100021
  };
99530
100022
  let aiResult;
99531
100023
  if (this.tracer) {
@@ -99562,13 +100054,57 @@ Double-check your response based on the criteria above. If everything looks good
99562
100054
  } else if (aiResult.finalText) {
99563
100055
  finalResult = aiResult.finalText;
99564
100056
  }
100057
+ if (gracefulTimeoutState.triggered) {
100058
+ 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";
100059
+ if (!finalResult || finalResult === DEFAULT_MAX_ITER_MSG || finalResult.startsWith("I was unable to complete your request after")) {
100060
+ try {
100061
+ const allText = await aiResult.result.text;
100062
+ if (allText && allText.trim()) {
100063
+ finalResult = timeoutNotice + allText;
100064
+ if (this.debug) {
100065
+ console.log(`[DEBUG] Graceful timeout: using concatenated step text (${allText.length} chars)`);
100066
+ }
100067
+ } else {
100068
+ const steps = await aiResult.result.steps;
100069
+ const toolSummaries = [];
100070
+ for (const step of steps || []) {
100071
+ if (step.toolResults?.length > 0) {
100072
+ for (const tr of step.toolResults) {
100073
+ const resultText = typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result);
100074
+ if (resultText && resultText.length > 0 && resultText.length < 5e3) {
100075
+ toolSummaries.push(resultText.substring(0, 2e3));
100076
+ }
100077
+ }
100078
+ }
100079
+ }
100080
+ if (toolSummaries.length > 0) {
100081
+ finalResult = `${timeoutNotice}The operation timed out before a complete answer could be generated. Here is the partial information gathered:
100082
+
100083
+ ${toolSummaries.join("\n\n---\n\n")}`;
100084
+ if (this.debug) {
100085
+ console.log(`[DEBUG] Graceful timeout: built fallback from ${toolSummaries.length} tool results`);
100086
+ }
100087
+ } else {
100088
+ 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.";
100089
+ }
100090
+ }
100091
+ } catch (e) {
100092
+ if (this.debug) {
100093
+ console.log(`[DEBUG] Graceful timeout fallback error: ${e.message}`);
100094
+ }
100095
+ 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.";
100096
+ }
100097
+ } else {
100098
+ finalResult = timeoutNotice + finalResult;
100099
+ }
100100
+ }
99565
100101
  const resultMessages = await aiResult.result.response?.messages;
99566
100102
  if (resultMessages) {
99567
100103
  for (const msg of resultMessages) {
99568
100104
  currentMessages.push(msg);
99569
100105
  }
99570
100106
  }
99571
- if (this.completionPrompt && !options._completionPromptProcessed && !completionPromptInjected && finalResult) {
100107
+ if (this.completionPrompt && !options._completionPromptProcessed && !completionPromptInjected && !abortSummaryTaken && finalResult) {
99572
100108
  completionPromptInjected = true;
99573
100109
  preCompletionResult = finalResult;
99574
100110
  if (this.debug) {
@@ -99640,6 +100176,118 @@ Double-check your response based on the criteria above. If everything looks good
99640
100176
  }
99641
100177
  break;
99642
100178
  } catch (error40) {
100179
+ if (gracefulTimeoutState.triggered && error40?.name === "AbortError") {
100180
+ if (this.debug) {
100181
+ console.log(`[DEBUG] Negotiated timeout: abort caught \u2014 making summary LLM call with conversation context`);
100182
+ }
100183
+ if (this.tracer) {
100184
+ this.tracer.addEvent("negotiated_timeout.abort_summary_started", {
100185
+ conversation_messages: currentMessages.length,
100186
+ has_schema: !!options.schema,
100187
+ has_tasks: !!(this.enableTasks && this.taskManager)
100188
+ });
100189
+ }
100190
+ try {
100191
+ let taskContext = "";
100192
+ if (this.enableTasks && this.taskManager) {
100193
+ const taskSummary = this.taskManager.getTaskSummary?.();
100194
+ if (taskSummary) {
100195
+ taskContext = `
100196
+
100197
+ ## Task Status
100198
+ ${taskSummary}
100199
+
100200
+ Acknowledge which tasks were completed and which were not.`;
100201
+ }
100202
+ }
100203
+ let schemaContext = "";
100204
+ if (options.schema) {
100205
+ try {
100206
+ const parsedSchema = typeof options.schema === "string" ? JSON.parse(options.schema) : options.schema;
100207
+ schemaContext = `
100208
+
100209
+ IMPORTANT: Your response MUST be valid JSON matching this schema:
100210
+ ${JSON.stringify(parsedSchema, null, 2)}
100211
+
100212
+ 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.`;
100213
+ } catch {
100214
+ }
100215
+ }
100216
+ 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.
100217
+
100218
+ Please provide a DETAILED summary of:
100219
+ 1. What you were asked to do (the original task)
100220
+ 2. What you accomplished \u2014 include ALL findings, code snippets, data, and conclusions you gathered
100221
+ 3. What was still in progress or not yet started
100222
+ 4. Any partial results or recommendations you can offer based on what you found so far${taskContext}${schemaContext}
100223
+
100224
+ Be thorough \u2014 this is the user's only response. Include all useful information you collected.`;
100225
+ const summaryMessages = [
100226
+ ...currentMessages,
100227
+ { role: "user", content: summaryPrompt }
100228
+ ];
100229
+ const modelInstance = this.provider ? this.provider(this.model) : this.model;
100230
+ const summaryFn = async () => {
100231
+ const summaryResult = await (0, import_ai4.generateText)({
100232
+ model: modelInstance,
100233
+ messages: this.prepareMessagesWithImages(summaryMessages),
100234
+ maxTokens: 4e3
100235
+ });
100236
+ if (this.tracer) {
100237
+ this.tracer.addEvent("negotiated_timeout.abort_summary_completed", {
100238
+ summary_length: summaryResult.text?.length || 0,
100239
+ usage_prompt_tokens: summaryResult.usage?.promptTokens,
100240
+ usage_completion_tokens: summaryResult.usage?.completionTokens
100241
+ });
100242
+ }
100243
+ if (summaryResult.usage) {
100244
+ this.tokenCounter.recordUsage(summaryResult.usage);
100245
+ }
100246
+ return summaryResult.text;
100247
+ };
100248
+ let summaryText;
100249
+ if (this.tracer) {
100250
+ summaryText = await this.tracer.withSpan("negotiated_timeout.abort_summary", summaryFn, {
100251
+ "summary.conversation_messages": currentMessages.length
100252
+ });
100253
+ } else {
100254
+ summaryText = await summaryFn();
100255
+ }
100256
+ if (options.schema) {
100257
+ finalResult = summaryText || "{}";
100258
+ } else {
100259
+ 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";
100260
+ finalResult = timeoutNotice + (summaryText || "The operation was interrupted before a response could be generated.");
100261
+ }
100262
+ if (options.onStream && finalResult) {
100263
+ options.onStream(finalResult);
100264
+ }
100265
+ if (this.debug) {
100266
+ console.log(`[DEBUG] Negotiated timeout: summary produced ${summaryText?.length || 0} chars`);
100267
+ }
100268
+ } catch (summaryErr) {
100269
+ if (this.debug) {
100270
+ console.log(`[DEBUG] Negotiated timeout: summary call failed (${summaryErr.message}), falling back to partial text`);
100271
+ }
100272
+ if (this.tracer) {
100273
+ this.tracer.addEvent("negotiated_timeout.abort_summary_error", {
100274
+ error_message: summaryErr.message
100275
+ });
100276
+ }
100277
+ const partialTexts = currentMessages.filter((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim()).map((m) => m.content);
100278
+ if (options.schema) {
100279
+ finalResult = partialTexts.length > 0 ? partialTexts[partialTexts.length - 1] : "{}";
100280
+ } else {
100281
+ const timeoutNotice = "**Note: This response was generated under a time constraint. The operation was interrupted and some work was not completed.**\n\n";
100282
+ 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.";
100283
+ }
100284
+ if (options.onStream && finalResult) {
100285
+ options.onStream(finalResult);
100286
+ }
100287
+ }
100288
+ abortSummaryTaken = true;
100289
+ break;
100290
+ }
99643
100291
  if (!compactionAttempted && handleContextLimitError) {
99644
100292
  const compactionResult = handleContextLimitError(error40, currentMessages, {
99645
100293
  keepLastSegment: true,
@@ -99674,6 +100322,36 @@ Double-check your response based on the criteria above. If everything looks good
99674
100322
  }
99675
100323
  if (currentIteration >= maxIterations) {
99676
100324
  console.warn(`[WARN] Max tool iterations (${maxIterations}) reached for session ${this.sessionId}.`);
100325
+ if (!finalResult || finalResult === DEFAULT_MAX_ITER_MSG) {
100326
+ try {
100327
+ const searchQueries = [];
100328
+ const toolCounts = {};
100329
+ for (const tc of _toolCallLog) {
100330
+ toolCounts[tc.name] = (toolCounts[tc.name] || 0) + 1;
100331
+ if (tc.name === "search") {
100332
+ const q = tc.args.query || "";
100333
+ const exact = tc.args.exact ? " (exact)" : "";
100334
+ searchQueries.push(`"${q}"${exact}`);
100335
+ }
100336
+ }
100337
+ const toolBreakdown = Object.entries(toolCounts).map(([name15, count]) => `${name15}: ${count}x`).join(", ");
100338
+ const uniqueSearches = [...new Set(searchQueries)];
100339
+ let summary = `I was unable to complete your request after ${currentIteration} tool iterations.
100340
+
100341
+ `;
100342
+ summary += `Tool calls made: ${toolBreakdown || "none"}
100343
+ `;
100344
+ if (uniqueSearches.length > 0) {
100345
+ summary += `Search queries tried: ${uniqueSearches.join(", ")}
100346
+ `;
100347
+ }
100348
+ summary += `
100349
+ 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.`;
100350
+ finalResult = summary;
100351
+ } catch {
100352
+ finalResult = DEFAULT_MAX_ITER_MSG;
100353
+ }
100354
+ }
99677
100355
  }
99678
100356
  this.history = currentMessages.map((msg) => ({ ...msg }));
99679
100357
  if (this.history.length > MAX_HISTORY_MESSAGES) {
@@ -100208,6 +100886,134 @@ Double-check your response based on the criteria above. If everything looks good
100208
100886
  console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
100209
100887
  }
100210
100888
  }
100889
+ /**
100890
+ * Trigger graceful wind-down from outside (e.g., parent agent).
100891
+ * Unlike cancel(), this does NOT abort — it sets the graceful timeout flag
100892
+ * so the agent finishes its current step and then winds down naturally.
100893
+ */
100894
+ triggerGracefulWindDown() {
100895
+ if (this._gracefulTimeoutState && !this._gracefulTimeoutState.triggered) {
100896
+ this._gracefulTimeoutState.triggered = true;
100897
+ if (this.debug) {
100898
+ console.log(`[DEBUG] Graceful wind-down triggered externally for session ${this.sessionId}`);
100899
+ }
100900
+ if (this.tracer) {
100901
+ this.tracer.addEvent("graceful_stop.external_trigger", {
100902
+ "session.id": this.sessionId
100903
+ });
100904
+ }
100905
+ } else if (this.debug) {
100906
+ console.log(`[DEBUG] Graceful wind-down already active for session ${this.sessionId}, skipping`);
100907
+ }
100908
+ }
100909
+ /**
100910
+ * Initiate two-phase graceful stop: signal subagents and MCP servers to wind down,
100911
+ * then hard-abort after a deadline if they haven't finished.
100912
+ * @param {Object} gracefulTimeoutState - The graceful timeout state object from run()
100913
+ * @param {string} reason - Why the graceful stop was initiated
100914
+ */
100915
+ async _initiateGracefulStop(gracefulTimeoutState, reason) {
100916
+ if (gracefulTimeoutState.triggered) return;
100917
+ if (this.debug) {
100918
+ console.log(`[DEBUG] Initiating graceful stop: ${reason} (subagents: ${this._activeSubagents.size}, hasMcpBridge: ${!!this.mcpBridge}, deadline: ${this.gracefulStopDeadline}ms)`);
100919
+ }
100920
+ gracefulTimeoutState.triggered = true;
100921
+ if (this.tracer) {
100922
+ this.tracer.addEvent("graceful_stop.initiated", {
100923
+ "session.id": this.sessionId,
100924
+ "graceful_stop.reason": reason,
100925
+ "graceful_stop.active_subagents": this._activeSubagents.size,
100926
+ "graceful_stop.has_mcp_bridge": !!this.mcpBridge,
100927
+ "graceful_stop.deadline_ms": this.gracefulStopDeadline
100928
+ });
100929
+ }
100930
+ let subagentsSignalled = 0;
100931
+ let subagentErrors = 0;
100932
+ for (const [sid, subagent] of this._activeSubagents) {
100933
+ try {
100934
+ subagent.triggerGracefulWindDown();
100935
+ subagentsSignalled++;
100936
+ if (this.debug) {
100937
+ console.log(`[DEBUG] Triggered graceful wind-down on subagent ${sid}`);
100938
+ }
100939
+ } catch (e) {
100940
+ subagentErrors++;
100941
+ if (this.debug) {
100942
+ console.log(`[DEBUG] Failed to trigger wind-down on subagent ${sid}: ${e.message}`);
100943
+ }
100944
+ }
100945
+ }
100946
+ let mcpResults = [];
100947
+ if (this.mcpBridge) {
100948
+ try {
100949
+ mcpResults = await this.mcpBridge.callGracefulStopAll();
100950
+ if (this.debug && mcpResults.length > 0) {
100951
+ console.log(`[DEBUG] MCP graceful_stop results: ${JSON.stringify(mcpResults)}`);
100952
+ }
100953
+ } catch (e) {
100954
+ if (this.debug) {
100955
+ console.log(`[DEBUG] MCP graceful_stop failed: ${e.message}`);
100956
+ }
100957
+ }
100958
+ }
100959
+ if (this.tracer) {
100960
+ this.tracer.addEvent("graceful_stop.signals_sent", {
100961
+ "session.id": this.sessionId,
100962
+ "graceful_stop.subagents_signalled": subagentsSignalled,
100963
+ "graceful_stop.subagent_errors": subagentErrors,
100964
+ "graceful_stop.mcp_servers_called": mcpResults.filter((r) => r.success).length,
100965
+ "graceful_stop.mcp_servers_failed": mcpResults.filter((r) => !r.success).length,
100966
+ "graceful_stop.mcp_servers_total": mcpResults.length
100967
+ });
100968
+ }
100969
+ this._gracefulStopHardAbortId = setTimeout(() => {
100970
+ if (this.debug) {
100971
+ console.log(`[DEBUG] Graceful stop deadline (${this.gracefulStopDeadline}ms) expired \u2014 hard aborting`);
100972
+ }
100973
+ if (this.tracer) {
100974
+ this.tracer.addEvent("graceful_stop.deadline_expired", {
100975
+ "session.id": this.sessionId,
100976
+ "graceful_stop.deadline_ms": this.gracefulStopDeadline
100977
+ });
100978
+ }
100979
+ if (this._abortController) this._abortController.abort();
100980
+ }, this.gracefulStopDeadline);
100981
+ }
100982
+ /**
100983
+ * Register an active subagent for graceful stop coordination.
100984
+ * @param {string} sessionId
100985
+ * @param {ProbeAgent} subagent
100986
+ */
100987
+ _registerSubagent(sessionId, subagent) {
100988
+ this._activeSubagents.set(sessionId, subagent);
100989
+ if (this.debug) {
100990
+ console.log(`[DEBUG] Registered subagent ${sessionId} (active: ${this._activeSubagents.size})`);
100991
+ }
100992
+ if (this.tracer) {
100993
+ this.tracer.addEvent("subagent.registered", {
100994
+ "session.id": this.sessionId,
100995
+ "subagent.session_id": sessionId,
100996
+ "subagent.active_count": this._activeSubagents.size
100997
+ });
100998
+ }
100999
+ }
101000
+ /**
101001
+ * Unregister a completed subagent.
101002
+ * @param {string} sessionId
101003
+ */
101004
+ _unregisterSubagent(sessionId) {
101005
+ this._activeSubagents.delete(sessionId);
101006
+ if (this.debug) {
101007
+ console.log(`[DEBUG] Unregistered subagent ${sessionId} (active: ${this._activeSubagents.size})`);
101008
+ }
101009
+ if (this.tracer) {
101010
+ this.tracer.addEvent("subagent.unregistered", {
101011
+ "session.id": this.sessionId,
101012
+ "subagent.session_id": sessionId,
101013
+ "subagent.active_count": this._activeSubagents.size
101014
+ });
101015
+ }
101016
+ }
100211
101017
  /**
100212
101018
  * Get the abort signal for this agent.
100213
101019
  * Delegations and subagents should check this signal.
@@ -100250,8 +101056,15 @@ async function delegate({
100250
101056
  // Optional per-instance manager, falls back to default singleton
100251
101057
  concurrencyLimiter = null,
100252
101058
  // Optional global AI concurrency limiter
100253
- parentAbortSignal = null
101059
+ parentAbortSignal = null,
100254
101060
  // Optional AbortSignal from parent to cancel this delegation
101061
+ // Timeout settings inherited from parent agent
101062
+ timeoutBehavior = void 0,
101063
+ requestTimeout = void 0,
101064
+ gracefulTimeoutBonusSteps = void 0,
101065
+ // Subagent lifecycle callbacks for graceful stop coordination
101066
+ onSubagentCreated = null,
101067
+ onSubagentCompleted = null
100255
101068
  }) {
100256
101069
  if (!task || typeof task !== "string") {
100257
101070
  throw new Error("Task parameter is required and must be a string");
@@ -100334,12 +101147,38 @@ async function delegate({
100334
101147
  // Inherit from parent
100335
101148
  mcpConfigPath,
100336
101149
  // Inherit from parent
100337
- concurrencyLimiter
101150
+ concurrencyLimiter,
100338
101151
  // Inherit global AI concurrency limiter
101152
+ // Inherit timeout behavior from parent — subagent gets its own graceful wind-down
101153
+ // so it can produce partial results instead of being hard-killed by the external timer.
101154
+ // The external delegate timeout (capped to parent's remaining budget) is the hard limit;
101155
+ // maxOperationTimeout on the subagent is set slightly shorter so its own wind-down
101156
+ // fires before the external kill.
101157
+ maxOperationTimeout: Math.max(1e4, timeout * 1e3 - 15e3),
101158
+ // 15s before external kill
101159
+ timeoutBehavior: timeoutBehavior || "graceful",
101160
+ requestTimeout,
101161
+ gracefulTimeoutBonusSteps: gracefulTimeoutBonusSteps ?? 2
101162
+ // fewer steps for subagents
100339
101163
  });
101164
+ if (onSubagentCreated) {
101165
+ onSubagentCreated(sessionId, subagent);
101166
+ }
100340
101167
  if (debug) {
100341
101168
  console.error(`[DELEGATE] Created subagent with session ${sessionId}`);
100342
101169
  console.error(`[DELEGATE] Subagent config: promptType=${promptType}, enableDelegate=false, maxIterations=${remainingIterations}`);
101170
+ console.error(`[DELEGATE] Timeout inheritance: externalTimeout=${timeout}s, maxOperationTimeout=${Math.max(1e4, timeout * 1e3 - 15e3)}ms, behavior=${timeoutBehavior || "graceful"}, bonusSteps=${gracefulTimeoutBonusSteps ?? 2}`);
101171
+ }
101172
+ if (tracer) {
101173
+ tracer.addEvent("delegation.subagent_created", {
101174
+ "delegation.session_id": sessionId,
101175
+ "delegation.parent_session_id": parentSessionId,
101176
+ "delegation.external_timeout_s": timeout,
101177
+ "delegation.internal_timeout_ms": Math.max(1e4, timeout * 1e3 - 15e3),
101178
+ "delegation.timeout_behavior": timeoutBehavior || "graceful",
101179
+ "delegation.bonus_steps": gracefulTimeoutBonusSteps ?? 2,
101180
+ "delegation.max_iterations": remainingIterations
101181
+ });
100343
101182
  }
100344
101183
  const timeoutPromise = new Promise((_, reject2) => {
100345
101184
  timeoutId = setTimeout(() => {
@@ -100348,6 +101187,7 @@ async function delegate({
100348
101187
  }, timeout * 1e3);
100349
101188
  });
100350
101189
  let parentAbortHandler;
101190
+ let parentAbortHardCancelId = null;
100351
101191
  const parentAbortPromise = new Promise((_, reject2) => {
100352
101192
  if (parentAbortSignal) {
100353
101193
  if (parentAbortSignal.aborted) {
@@ -100356,8 +101196,31 @@ async function delegate({
100356
101196
  return;
100357
101197
  }
100358
101198
  parentAbortHandler = () => {
100359
- subagent.cancel();
100360
- reject2(new Error("Delegation cancelled: parent operation was aborted"));
101199
+ subagent.triggerGracefulWindDown();
101200
+ if (debug) {
101201
+ console.error(`[DELEGATE] Parent abort signal received \u2014 triggered graceful wind-down on subagent ${sessionId}`);
101202
+ }
101203
+ if (tracer) {
101204
+ tracer.addEvent("delegation.parent_abort_phase1", {
101205
+ "delegation.session_id": sessionId,
101206
+ "delegation.parent_session_id": parentSessionId,
101207
+ "delegation.action": "graceful_wind_down"
101208
+ });
101209
+ }
101210
+ parentAbortHardCancelId = setTimeout(() => {
101211
+ if (debug) {
101212
+ console.error(`[DELEGATE] Graceful wind-down deadline expired \u2014 hard cancelling subagent ${sessionId}`);
101213
+ }
101214
+ if (tracer) {
101215
+ tracer.addEvent("delegation.parent_abort_phase2", {
101216
+ "delegation.session_id": sessionId,
101217
+ "delegation.parent_session_id": parentSessionId,
101218
+ "delegation.action": "hard_cancel"
101219
+ });
101220
+ }
101221
+ subagent.cancel();
101222
+ reject2(new Error("Delegation cancelled: parent operation was aborted (graceful wind-down deadline expired)"));
101223
+ }, 3e4);
100361
101224
  };
100362
101225
  parentAbortSignal.addEventListener("abort", parentAbortHandler, { once: true });
100363
101226
  }
@@ -100373,6 +101236,13 @@ async function delegate({
100373
101236
  if (parentAbortHandler && parentAbortSignal) {
100374
101237
  parentAbortSignal.removeEventListener("abort", parentAbortHandler);
100375
101238
  }
101239
+ if (parentAbortHardCancelId) {
101240
+ clearTimeout(parentAbortHardCancelId);
101241
+ parentAbortHardCancelId = null;
101242
+ }
101243
+ if (onSubagentCompleted) {
101244
+ onSubagentCompleted(sessionId);
101245
+ }
100376
101246
  }
100377
101247
  if (timeoutId !== null) {
100378
101248
  clearTimeout(timeoutId);
@@ -101397,6 +102267,10 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
101397
102267
  "- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).",
101398
102268
  "- exact=true matches the literal string only \u2014 no stemming, no splitting.",
101399
102269
  '- This is ideal for precise lookups: exact=true "ForwardMessage", exact=true "SessionLimiter", exact=true "ThrottleRetryLimit".',
102270
+ "- IMPORTANT: Use exact=true when searching for strings containing punctuation, quotes, or empty values.",
102271
+ " Default BM25 search strips punctuation and treats quoted empty strings as noise.",
102272
+ ` Example: searching for 'description: ""' with exact=false will NOT find empty description fields \u2014 it just matches "description".`,
102273
+ ` Use exact=true for literal patterns like 'description: ""', 'value: \\'\\'', or any YAML/config field with specific punctuation.`,
101400
102274
  "- Do NOT use exact=true for exploratory/conceptual queries \u2014 use the default for those.",
101401
102275
  "",
101402
102276
  "Combining searches with OR:",
@@ -101456,7 +102330,13 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
101456
102330
  "WHEN TO STOP:",
101457
102331
  "- After you have explored the main concept AND related subsystems.",
101458
102332
  "- Once you have 5-15 targets covering different aspects of the query.",
101459
- '- If you get a "DUPLICATE SEARCH BLOCKED" message, move on.',
102333
+ '- If you get a "DUPLICATE SEARCH BLOCKED" message, do NOT rephrase the same query \u2014 try a FUNDAMENTALLY different approach:',
102334
+ " * Switch between exact=true and exact=false",
102335
+ " * Search for a broader term and filter results manually",
102336
+ " * Use listFiles to browse the directory structure directly",
102337
+ " * Look for related/surrounding patterns instead of the exact string",
102338
+ "- If 2-3 genuinely different search approaches fail, STOP and report what you tried and why it failed.",
102339
+ " Do NOT keep trying variations of the same failing concept.",
101460
102340
  "",
101461
102341
  "Strategy:",
101462
102342
  "1. Analyze the query \u2014 identify key concepts, then brainstorm SYNONYMS and alternative terms for each.",
@@ -101528,8 +102408,8 @@ var init_vercel = __esm({
101528
102408
  }
101529
102409
  return result;
101530
102410
  };
101531
- const previousSearches = /* @__PURE__ */ new Set();
101532
- let consecutiveDupBlocks = 0;
102411
+ const previousSearches = /* @__PURE__ */ new Map();
102412
+ const dupBlockCounts = /* @__PURE__ */ new Map();
101533
102413
  const paginationCounts = /* @__PURE__ */ new Map();
101534
102414
  const MAX_PAGES_PER_QUERY = 3;
101535
102415
  return (0, import_ai5.tool)({
@@ -101580,20 +102460,25 @@ var init_vercel = __esm({
101580
102460
  return await search(searchOptions);
101581
102461
  };
101582
102462
  if (!searchDelegate) {
101583
- const searchKey = `${searchQuery}::${exact || false}`;
102463
+ const searchKey = `${searchPath}::${searchQuery}::${exact || false}`;
101584
102464
  if (!nextPage) {
101585
102465
  if (previousSearches.has(searchKey)) {
101586
- consecutiveDupBlocks++;
102466
+ const blockCount = (dupBlockCounts.get(searchKey) || 0) + 1;
102467
+ dupBlockCounts.set(searchKey, blockCount);
101587
102468
  if (debug) {
101588
- console.error(`[DEDUP] Blocked duplicate search (${consecutiveDupBlocks}x): "${searchQuery}" (path: "${searchPath}")`);
102469
+ console.error(`[DEDUP] Blocked duplicate search (${blockCount}x): "${searchQuery}" (path: "${searchPath}")`);
102470
+ }
102471
+ if (blockCount >= 3) {
102472
+ 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.";
101589
102473
  }
101590
- if (consecutiveDupBlocks >= 3) {
101591
- 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.";
102474
+ const prev = previousSearches.get(searchKey);
102475
+ if (prev.hadResults) {
102476
+ 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.`;
101592
102477
  }
101593
- 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.";
102478
+ 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.`;
102479
+ 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.`;
101594
102480
  }
101595
- previousSearches.add(searchKey);
101596
- consecutiveDupBlocks = 0;
102481
+ previousSearches.set(searchKey, { hadResults: false });
101597
102482
  paginationCounts.set(searchKey, 0);
101598
102483
  } else {
101599
102484
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
@@ -101607,6 +102492,14 @@ var init_vercel = __esm({
101607
102492
  }
101608
102493
  try {
101609
102494
  const result = maybeAnnotate(await runRawSearch());
102495
+ if (typeof result === "string" && result.includes("No results found")) {
102496
+ if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, "").trim())) {
102497
+ 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).";
102498
+ }
102499
+ } else if (typeof result === "string") {
102500
+ const entry = previousSearches.get(searchKey);
102501
+ if (entry) entry.hadResults = true;
102502
+ }
101610
102503
  if (options.fileTracker && typeof result === "string") {
101611
102504
  options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {
101612
102505
  });
@@ -101889,7 +102782,31 @@ var init_vercel = __esm({
101889
102782
  });
101890
102783
  };
101891
102784
  delegateTool = (options = {}) => {
101892
- const { debug = false, timeout = 300, cwd, allowedFolders, workspaceRoot, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, delegationManager = null } = options;
102785
+ const {
102786
+ debug = false,
102787
+ timeout = 300,
102788
+ cwd,
102789
+ allowedFolders,
102790
+ workspaceRoot,
102791
+ enableBash = false,
102792
+ bashConfig,
102793
+ architectureFileName,
102794
+ enableMcp = false,
102795
+ mcpConfig = null,
102796
+ mcpConfigPath = null,
102797
+ delegationManager = null,
102798
+ // Timeout settings inherited from parent agent
102799
+ timeoutBehavior,
102800
+ maxOperationTimeout,
102801
+ requestTimeout,
102802
+ gracefulTimeoutBonusSteps,
102803
+ negotiatedTimeoutBudget,
102804
+ negotiatedTimeoutMaxRequests,
102805
+ negotiatedTimeoutMaxPerRequest,
102806
+ parentOperationStartTime,
102807
+ onSubagentCreated,
102808
+ onSubagentCompleted
102809
+ } = options;
101893
102810
  return (0, import_ai5.tool)({
101894
102811
  name: "delegate",
101895
102812
  description: delegateDescription,
@@ -101933,9 +102850,30 @@ var init_vercel = __esm({
101933
102850
  console.error(`Using workspace root: ${effectivePath} (cwd was: ${cwd || "not set"})`);
101934
102851
  }
101935
102852
  }
102853
+ let effectiveTimeout = timeout;
102854
+ if (parentOperationStartTime && maxOperationTimeout) {
102855
+ const elapsed = Date.now() - parentOperationStartTime;
102856
+ const remaining = maxOperationTimeout - elapsed;
102857
+ const budgetCap = Math.max(30, Math.floor(remaining * 0.9 / 1e3));
102858
+ if (budgetCap < effectiveTimeout) {
102859
+ effectiveTimeout = budgetCap;
102860
+ if (debug) {
102861
+ console.error(`[DELEGATE] Capping timeout from ${timeout}s to ${effectiveTimeout}s (remaining parent budget: ${Math.floor(remaining / 1e3)}s)`);
102862
+ }
102863
+ if (tracer) {
102864
+ tracer.addEvent("delegation.budget_capped", {
102865
+ "delegation.original_timeout_s": timeout,
102866
+ "delegation.effective_timeout_s": effectiveTimeout,
102867
+ "delegation.parent_elapsed_ms": elapsed,
102868
+ "delegation.parent_remaining_ms": remaining,
102869
+ "delegation.parent_session_id": parentSessionId
102870
+ });
102871
+ }
102872
+ }
102873
+ }
101936
102874
  const result = await delegate({
101937
102875
  task,
101938
- timeout,
102876
+ timeout: effectiveTimeout,
101939
102877
  debug,
101940
102878
  currentIteration: currentIteration || 0,
101941
102879
  maxIterations: maxIterations || 30,
@@ -101954,7 +102892,14 @@ var init_vercel = __esm({
101954
102892
  mcpConfigPath,
101955
102893
  delegationManager,
101956
102894
  // Per-instance delegation limits
101957
- parentAbortSignal
102895
+ parentAbortSignal,
102896
+ // Inherit timeout settings for subagent
102897
+ timeoutBehavior,
102898
+ requestTimeout,
102899
+ gracefulTimeoutBonusSteps,
102900
+ // Subagent lifecycle callbacks for graceful stop coordination
102901
+ onSubagentCreated,
102902
+ onSubagentCompleted
101958
102903
  });
101959
102904
  return result;
101960
102905
  }
@@ -103433,6 +104378,121 @@ var init_file_lister = __esm({
103433
104378
  }
103434
104379
  });
103435
104380
 
104381
+ // src/agent/otelLogBridge.js
104382
+ function getOtelApi() {
104383
+ if (otelApiAttempted) return otelApi;
104384
+ otelApiAttempted = true;
104385
+ try {
104386
+ otelApi = (function(name15) {
104387
+ return _require(name15);
104388
+ })("@opentelemetry/api");
104389
+ } catch {
104390
+ }
104391
+ return otelApi;
104392
+ }
104393
+ function getOtelLogger() {
104394
+ if (otelLoggerAttempted) return otelLogger;
104395
+ otelLoggerAttempted = true;
104396
+ try {
104397
+ const { logs } = (function(name15) {
104398
+ return _require(name15);
104399
+ })("@opentelemetry/api-logs");
104400
+ otelLogger = logs.getLogger("probe-agent");
104401
+ } catch {
104402
+ }
104403
+ return otelLogger;
104404
+ }
104405
+ function getTraceSuffix() {
104406
+ try {
104407
+ const api2 = getOtelApi();
104408
+ if (!api2) return "";
104409
+ const span = api2.trace.getSpan(api2.context.active());
104410
+ const ctx = span?.spanContext?.();
104411
+ if (!ctx?.traceId) return "";
104412
+ return ` [trace_id=${ctx.traceId} span_id=${ctx.spanId}]`;
104413
+ } catch {
104414
+ return "";
104415
+ }
104416
+ }
104417
+ function emitOtelLog(msg, level) {
104418
+ try {
104419
+ const logger = getOtelLogger();
104420
+ if (!logger) return;
104421
+ const api2 = getOtelApi();
104422
+ let traceId, spanId;
104423
+ if (api2) {
104424
+ const span = api2.trace.getSpan(api2.context.active());
104425
+ const ctx = span?.spanContext?.();
104426
+ if (ctx?.traceId) {
104427
+ traceId = ctx.traceId;
104428
+ spanId = ctx.spanId;
104429
+ }
104430
+ }
104431
+ logger.emit({
104432
+ severityNumber: OTEL_SEVERITY[level] || 9,
104433
+ severityText: level.toUpperCase(),
104434
+ body: msg,
104435
+ attributes: {
104436
+ "probe.logger": true,
104437
+ ...traceId ? { trace_id: traceId, span_id: spanId } : {}
104438
+ }
104439
+ });
104440
+ } catch {
104441
+ }
104442
+ }
104443
+ function patchConsole() {
104444
+ if (patched) return;
104445
+ const methods = ["log", "info", "warn", "error"];
104446
+ const c = globalThis.console;
104447
+ for (const m of methods) {
104448
+ const orig = c[m].bind(c);
104449
+ originals[m] = orig;
104450
+ c[m] = (...args) => {
104451
+ const msgParts = args.map(
104452
+ (a) => typeof a === "string" ? a : a instanceof Error ? a.message : JSON.stringify(a)
104453
+ );
104454
+ const msg = msgParts.join(" ");
104455
+ emitOtelLog(msg, m === "log" ? "log" : m);
104456
+ const suffix = getTraceSuffix();
104457
+ if (suffix) {
104458
+ if (typeof args[0] === "string") {
104459
+ args[0] = args[0] + suffix;
104460
+ } else {
104461
+ args.push(suffix);
104462
+ }
104463
+ }
104464
+ return orig(...args);
104465
+ };
104466
+ }
104467
+ patched = true;
104468
+ }
104469
+ var import_module, _require, OTEL_SEVERITY, patched, originals, otelApi, otelApiAttempted, otelLogger, otelLoggerAttempted;
104470
+ var init_otelLogBridge = __esm({
104471
+ "src/agent/otelLogBridge.js"() {
104472
+ "use strict";
104473
+ import_module = require("module");
104474
+ _require = (0, import_module.createRequire)("file:///");
104475
+ OTEL_SEVERITY = {
104476
+ log: 9,
104477
+ // INFO
104478
+ info: 9,
104479
+ // INFO
104480
+ warn: 13,
104481
+ // WARN
104482
+ error: 17,
104483
+ // ERROR
104484
+ debug: 5
104485
+ // DEBUG
104486
+ };
104487
+ patched = false;
104488
+ originals = {};
104489
+ otelApi = null;
104490
+ otelApiAttempted = false;
104491
+ otelLogger = null;
104492
+ otelLoggerAttempted = false;
104493
+ }
104494
+ });
104495
+
103436
104496
  // src/agent/simpleTelemetry.js
103437
104497
  function initializeSimpleTelemetryFromOptions(options) {
103438
104498
  const telemetry = new SimpleTelemetry({
@@ -103441,6 +104501,7 @@ function initializeSimpleTelemetryFromOptions(options) {
103441
104501
  enableConsole: options.traceConsole,
103442
104502
  filePath: options.traceFile || "./traces.jsonl"
103443
104503
  });
104504
+ patchConsole();
103444
104505
  return telemetry;
103445
104506
  }
103446
104507
  var import_fs15, import_path18, SimpleTelemetry, SimpleAppTracer;
@@ -103449,6 +104510,7 @@ var init_simpleTelemetry = __esm({
103449
104510
  "use strict";
103450
104511
  import_fs15 = require("fs");
103451
104512
  import_path18 = require("path");
104513
+ init_otelLogBridge();
103452
104514
  SimpleTelemetry = class {
103453
104515
  constructor(options = {}) {
103454
104516
  this.serviceName = options.serviceName || "probe-agent";
@@ -103899,6 +104961,9 @@ var init_hooks = __esm({
103899
104961
  var index_exports = {};
103900
104962
  __export(index_exports, {
103901
104963
  DEFAULT_SYSTEM_MESSAGE: () => DEFAULT_SYSTEM_MESSAGE,
104964
+ ENGINE_ACTIVITY_TIMEOUT_DEFAULT: () => ENGINE_ACTIVITY_TIMEOUT_DEFAULT,
104965
+ ENGINE_ACTIVITY_TIMEOUT_MAX: () => ENGINE_ACTIVITY_TIMEOUT_MAX,
104966
+ ENGINE_ACTIVITY_TIMEOUT_MIN: () => ENGINE_ACTIVITY_TIMEOUT_MIN,
103902
104967
  FileTracker: () => FileTracker,
103903
104968
  HOOK_TYPES: () => HOOK_TYPES,
103904
104969
  HookManager: () => HookManager,
@@ -103985,6 +105050,9 @@ init_index();
103985
105050
  // Annotate the CommonJS export names for ESM import in node:
103986
105051
  0 && (module.exports = {
103987
105052
  DEFAULT_SYSTEM_MESSAGE,
105053
+ ENGINE_ACTIVITY_TIMEOUT_DEFAULT,
105054
+ ENGINE_ACTIVITY_TIMEOUT_MAX,
105055
+ ENGINE_ACTIVITY_TIMEOUT_MIN,
103988
105056
  FileTracker,
103989
105057
  HOOK_TYPES,
103990
105058
  HookManager,