@probelabs/probe 0.6.0-rc295 → 0.6.0-rc297

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-rc297-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-rc297-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-rc297-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-rc297-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-rc297-x86_64-unknown-linux-musl.tar.gz} +0 -0
  7. package/build/agent/ProbeAgent.d.ts +40 -2
  8. package/build/agent/ProbeAgent.js +703 -11
  9. package/build/agent/mcp/client.js +115 -4
  10. package/build/agent/mcp/xmlBridge.js +13 -1
  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 +1095 -185
  18. package/cjs/agent/simpleTelemetry.cjs +112 -0
  19. package/cjs/index.cjs +1207 -185
  20. package/index.d.ts +26 -0
  21. package/package.json +2 -2
  22. package/src/agent/ProbeAgent.d.ts +40 -2
  23. package/src/agent/ProbeAgent.js +703 -11
  24. package/src/agent/mcp/client.js +115 -4
  25. package/src/agent/mcp/xmlBridge.js +13 -1
  26. package/src/agent/otelLogBridge.js +184 -0
  27. package/src/agent/simpleTelemetry.js +8 -0
  28. package/src/delegate.js +75 -6
  29. package/src/index.js +6 -2
  30. package/src/tools/common.js +84 -11
  31. package/src/tools/vercel.js +78 -18
@@ -26106,8 +26106,15 @@ async function delegate({
26106
26106
  // Optional per-instance manager, falls back to default singleton
26107
26107
  concurrencyLimiter = null,
26108
26108
  // Optional global AI concurrency limiter
26109
- parentAbortSignal = null
26109
+ parentAbortSignal = null,
26110
26110
  // Optional AbortSignal from parent to cancel this delegation
26111
+ // Timeout settings inherited from parent agent
26112
+ timeoutBehavior = void 0,
26113
+ requestTimeout = void 0,
26114
+ gracefulTimeoutBonusSteps = void 0,
26115
+ // Subagent lifecycle callbacks for graceful stop coordination
26116
+ onSubagentCreated = null,
26117
+ onSubagentCompleted = null
26111
26118
  }) {
26112
26119
  if (!task || typeof task !== "string") {
26113
26120
  throw new Error("Task parameter is required and must be a string");
@@ -26190,12 +26197,38 @@ async function delegate({
26190
26197
  // Inherit from parent
26191
26198
  mcpConfigPath,
26192
26199
  // Inherit from parent
26193
- concurrencyLimiter
26200
+ concurrencyLimiter,
26194
26201
  // Inherit global AI concurrency limiter
26202
+ // Inherit timeout behavior from parent — subagent gets its own graceful wind-down
26203
+ // so it can produce partial results instead of being hard-killed by the external timer.
26204
+ // The external delegate timeout (capped to parent's remaining budget) is the hard limit;
26205
+ // maxOperationTimeout on the subagent is set slightly shorter so its own wind-down
26206
+ // fires before the external kill.
26207
+ maxOperationTimeout: Math.max(1e4, timeout * 1e3 - 15e3),
26208
+ // 15s before external kill
26209
+ timeoutBehavior: timeoutBehavior || "graceful",
26210
+ requestTimeout,
26211
+ gracefulTimeoutBonusSteps: gracefulTimeoutBonusSteps ?? 2
26212
+ // fewer steps for subagents
26195
26213
  });
26214
+ if (onSubagentCreated) {
26215
+ onSubagentCreated(sessionId, subagent);
26216
+ }
26196
26217
  if (debug) {
26197
26218
  console.error(`[DELEGATE] Created subagent with session ${sessionId}`);
26198
26219
  console.error(`[DELEGATE] Subagent config: promptType=${promptType}, enableDelegate=false, maxIterations=${remainingIterations}`);
26220
+ console.error(`[DELEGATE] Timeout inheritance: externalTimeout=${timeout}s, maxOperationTimeout=${Math.max(1e4, timeout * 1e3 - 15e3)}ms, behavior=${timeoutBehavior || "graceful"}, bonusSteps=${gracefulTimeoutBonusSteps ?? 2}`);
26221
+ }
26222
+ if (tracer) {
26223
+ tracer.addEvent("delegation.subagent_created", {
26224
+ "delegation.session_id": sessionId,
26225
+ "delegation.parent_session_id": parentSessionId,
26226
+ "delegation.external_timeout_s": timeout,
26227
+ "delegation.internal_timeout_ms": Math.max(1e4, timeout * 1e3 - 15e3),
26228
+ "delegation.timeout_behavior": timeoutBehavior || "graceful",
26229
+ "delegation.bonus_steps": gracefulTimeoutBonusSteps ?? 2,
26230
+ "delegation.max_iterations": remainingIterations
26231
+ });
26199
26232
  }
26200
26233
  const timeoutPromise = new Promise((_, reject2) => {
26201
26234
  timeoutId = setTimeout(() => {
@@ -26204,6 +26237,7 @@ async function delegate({
26204
26237
  }, timeout * 1e3);
26205
26238
  });
26206
26239
  let parentAbortHandler;
26240
+ let parentAbortHardCancelId = null;
26207
26241
  const parentAbortPromise = new Promise((_, reject2) => {
26208
26242
  if (parentAbortSignal) {
26209
26243
  if (parentAbortSignal.aborted) {
@@ -26212,8 +26246,31 @@ async function delegate({
26212
26246
  return;
26213
26247
  }
26214
26248
  parentAbortHandler = () => {
26215
- subagent.cancel();
26216
- reject2(new Error("Delegation cancelled: parent operation was aborted"));
26249
+ subagent.triggerGracefulWindDown();
26250
+ if (debug) {
26251
+ console.error(`[DELEGATE] Parent abort signal received \u2014 triggered graceful wind-down on subagent ${sessionId}`);
26252
+ }
26253
+ if (tracer) {
26254
+ tracer.addEvent("delegation.parent_abort_phase1", {
26255
+ "delegation.session_id": sessionId,
26256
+ "delegation.parent_session_id": parentSessionId,
26257
+ "delegation.action": "graceful_wind_down"
26258
+ });
26259
+ }
26260
+ parentAbortHardCancelId = setTimeout(() => {
26261
+ if (debug) {
26262
+ console.error(`[DELEGATE] Graceful wind-down deadline expired \u2014 hard cancelling subagent ${sessionId}`);
26263
+ }
26264
+ if (tracer) {
26265
+ tracer.addEvent("delegation.parent_abort_phase2", {
26266
+ "delegation.session_id": sessionId,
26267
+ "delegation.parent_session_id": parentSessionId,
26268
+ "delegation.action": "hard_cancel"
26269
+ });
26270
+ }
26271
+ subagent.cancel();
26272
+ reject2(new Error("Delegation cancelled: parent operation was aborted (graceful wind-down deadline expired)"));
26273
+ }, 3e4);
26217
26274
  };
26218
26275
  parentAbortSignal.addEventListener("abort", parentAbortHandler, { once: true });
26219
26276
  }
@@ -26229,6 +26286,13 @@ async function delegate({
26229
26286
  if (parentAbortHandler && parentAbortSignal) {
26230
26287
  parentAbortSignal.removeEventListener("abort", parentAbortHandler);
26231
26288
  }
26289
+ if (parentAbortHardCancelId) {
26290
+ clearTimeout(parentAbortHardCancelId);
26291
+ parentAbortHardCancelId = null;
26292
+ }
26293
+ if (onSubagentCompleted) {
26294
+ onSubagentCompleted(sessionId);
26295
+ }
26232
26296
  }
26233
26297
  if (timeoutId !== null) {
26234
26298
  clearTimeout(timeoutId);
@@ -27064,14 +27128,64 @@ function detectStuckResponse(response) {
27064
27128
  }
27065
27129
  return false;
27066
27130
  }
27131
+ function splitQuotedString(input) {
27132
+ const tokens = [];
27133
+ let current2 = "";
27134
+ let inQuote = null;
27135
+ let i = 0;
27136
+ while (i < input.length) {
27137
+ const ch = input[i];
27138
+ if (inQuote) {
27139
+ if (ch === "\\" && i + 1 < input.length) {
27140
+ current2 += input[i + 1];
27141
+ i += 2;
27142
+ continue;
27143
+ }
27144
+ if (ch === inQuote) {
27145
+ inQuote = null;
27146
+ i++;
27147
+ continue;
27148
+ }
27149
+ current2 += ch;
27150
+ i++;
27151
+ } else {
27152
+ if (ch === '"' || ch === "'") {
27153
+ inQuote = ch;
27154
+ i++;
27155
+ continue;
27156
+ }
27157
+ if (/[\s,]/.test(ch)) {
27158
+ if (current2.length > 0) {
27159
+ tokens.push(current2);
27160
+ current2 = "";
27161
+ }
27162
+ i++;
27163
+ continue;
27164
+ }
27165
+ current2 += ch;
27166
+ i++;
27167
+ }
27168
+ }
27169
+ if (current2.length > 0) {
27170
+ tokens.push(current2);
27171
+ }
27172
+ return tokens;
27173
+ }
27067
27174
  function parseTargets(targets) {
27068
27175
  if (!targets || typeof targets !== "string") {
27069
27176
  return [];
27070
27177
  }
27071
- return targets.split(/[\s,]+/).filter((f) => f.length > 0);
27178
+ return splitQuotedString(targets);
27072
27179
  }
27073
27180
  function parseAndResolvePaths(pathStr, cwd) {
27074
27181
  if (!pathStr) return [];
27182
+ if (/["']/.test(pathStr)) {
27183
+ const paths2 = splitQuotedString(pathStr);
27184
+ return paths2.map((p) => {
27185
+ if ((0, import_path5.isAbsolute)(p)) return p;
27186
+ return cwd ? (0, import_path5.resolve)(cwd, p) : p;
27187
+ });
27188
+ }
27075
27189
  let paths = pathStr.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
27076
27190
  paths = paths.flatMap((p) => {
27077
27191
  if (!/\s/.test(p)) return [p];
@@ -27081,9 +27195,7 @@ function parseAndResolvePaths(pathStr, cwd) {
27081
27195
  return allLookLikePaths ? parts : [p];
27082
27196
  });
27083
27197
  return paths.map((p) => {
27084
- if ((0, import_path5.isAbsolute)(p)) {
27085
- return p;
27086
- }
27198
+ if ((0, import_path5.isAbsolute)(p)) return p;
27087
27199
  return cwd ? (0, import_path5.resolve)(cwd, p) : p;
27088
27200
  });
27089
27201
  }
@@ -27116,7 +27228,7 @@ var init_common = __esm({
27116
27228
  searchSchema = external_exports2.object({
27117
27229
  query: external_exports2.string().describe("Search query \u2014 natural language questions or Elasticsearch-style keywords both work. For keywords: use quotes for exact phrases, AND/OR for boolean logic, - for negation. Probe handles stemming and camelCase/snake_case splitting automatically, so do NOT try case or style variations of the same keyword."),
27118
27230
  path: external_exports2.string().optional().default(".").describe('Path to search in. For dependencies use "go:github.com/owner/repo", "js:package_name", or "rust:cargo_name" etc.'),
27119
- exact: external_exports2.boolean().optional().default(false).describe('Default (false) enables stemming and keyword splitting for exploratory search - "getUserData" matches "get", "user", "data", etc. Set true for precise symbol lookup where "getUserData" matches only "getUserData". Use true when you know the exact symbol name.'),
27231
+ exact: external_exports2.boolean().optional().default(false).describe(`Default (false) enables stemming and keyword splitting for exploratory search - "getUserData" matches "get", "user", "data", etc. Set true for precise symbol lookup OR when searching for strings with punctuation/quotes/empty values (e.g. 'description: ""' \u2014 BM25 strips punctuation so exact=true is required for literal matching). Use true when you know the exact symbol name or need literal string matching.`),
27120
27232
  maxTokens: external_exports2.number().nullable().optional().describe("Maximum tokens to return. Default is 20000. Set to null for unlimited results."),
27121
27233
  session: external_exports2.string().optional().describe("Session ID for result caching and pagination. Pass the session ID from a previous search to get additional results (next page). Results already shown in a session are automatically excluded. Omit for a fresh search."),
27122
27234
  nextPage: external_exports2.boolean().optional().default(false).describe("Set to true when requesting the next page of results. Requires passing the same session ID from the previous search output.")
@@ -27427,6 +27539,10 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27427
27539
  "- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).",
27428
27540
  "- exact=true matches the literal string only \u2014 no stemming, no splitting.",
27429
27541
  '- This is ideal for precise lookups: exact=true "ForwardMessage", exact=true "SessionLimiter", exact=true "ThrottleRetryLimit".',
27542
+ "- IMPORTANT: Use exact=true when searching for strings containing punctuation, quotes, or empty values.",
27543
+ " Default BM25 search strips punctuation and treats quoted empty strings as noise.",
27544
+ ` Example: searching for 'description: ""' with exact=false will NOT find empty description fields \u2014 it just matches "description".`,
27545
+ ` Use exact=true for literal patterns like 'description: ""', 'value: \\'\\'', or any YAML/config field with specific punctuation.`,
27430
27546
  "- Do NOT use exact=true for exploratory/conceptual queries \u2014 use the default for those.",
27431
27547
  "",
27432
27548
  "Combining searches with OR:",
@@ -27486,7 +27602,13 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27486
27602
  "WHEN TO STOP:",
27487
27603
  "- After you have explored the main concept AND related subsystems.",
27488
27604
  "- Once you have 5-15 targets covering different aspects of the query.",
27489
- '- If you get a "DUPLICATE SEARCH BLOCKED" message, move on.',
27605
+ '- If you get a "DUPLICATE SEARCH BLOCKED" message, do NOT rephrase the same query \u2014 try a FUNDAMENTALLY different approach:',
27606
+ " * Switch between exact=true and exact=false",
27607
+ " * Search for a broader term and filter results manually",
27608
+ " * Use listFiles to browse the directory structure directly",
27609
+ " * Look for related/surrounding patterns instead of the exact string",
27610
+ "- If 2-3 genuinely different search approaches fail, STOP and report what you tried and why it failed.",
27611
+ " Do NOT keep trying variations of the same failing concept.",
27490
27612
  "",
27491
27613
  "Strategy:",
27492
27614
  "1. Analyze the query \u2014 identify key concepts, then brainstorm SYNONYMS and alternative terms for each.",
@@ -27558,8 +27680,8 @@ var init_vercel = __esm({
27558
27680
  }
27559
27681
  return result;
27560
27682
  };
27561
- const previousSearches = /* @__PURE__ */ new Set();
27562
- let consecutiveDupBlocks = 0;
27683
+ const previousSearches = /* @__PURE__ */ new Map();
27684
+ const dupBlockCounts = /* @__PURE__ */ new Map();
27563
27685
  const paginationCounts = /* @__PURE__ */ new Map();
27564
27686
  const MAX_PAGES_PER_QUERY = 3;
27565
27687
  return (0, import_ai.tool)({
@@ -27610,20 +27732,25 @@ var init_vercel = __esm({
27610
27732
  return await search(searchOptions);
27611
27733
  };
27612
27734
  if (!searchDelegate) {
27613
- const searchKey = `${searchQuery}::${exact || false}`;
27735
+ const searchKey = `${searchPath}::${searchQuery}::${exact || false}`;
27614
27736
  if (!nextPage) {
27615
27737
  if (previousSearches.has(searchKey)) {
27616
- consecutiveDupBlocks++;
27738
+ const blockCount = (dupBlockCounts.get(searchKey) || 0) + 1;
27739
+ dupBlockCounts.set(searchKey, blockCount);
27617
27740
  if (debug) {
27618
- console.error(`[DEDUP] Blocked duplicate search (${consecutiveDupBlocks}x): "${searchQuery}" (path: "${searchPath}")`);
27741
+ console.error(`[DEDUP] Blocked duplicate search (${blockCount}x): "${searchQuery}" (path: "${searchPath}")`);
27742
+ }
27743
+ if (blockCount >= 3) {
27744
+ return "STOP. You have been blocked " + blockCount + " times for repeating the same search. You MUST provide your final answer NOW with whatever information you have. Do NOT call any more tools.";
27619
27745
  }
27620
- if (consecutiveDupBlocks >= 3) {
27621
- return "STOP. You have been blocked " + consecutiveDupBlocks + " times for repeating searches. You MUST output your final JSON answer NOW with whatever targets you have found. Do NOT call any more tools.";
27746
+ const prev = previousSearches.get(searchKey);
27747
+ if (prev.hadResults) {
27748
+ return `DUPLICATE SEARCH BLOCKED (${blockCount}x). You already searched for "${searchQuery}" in this path and found results. Do NOT repeat. Use extract to examine the files you already found, try a COMPLETELY different keyword, or provide your final answer.`;
27622
27749
  }
27623
- return "DUPLICATE SEARCH BLOCKED (" + consecutiveDupBlocks + "x). You already searched for this. Do NOT repeat \u2014 probe searches recursively across all paths. Either: (1) use extract on results you already found, (2) try a COMPLETELY different keyword, or (3) output your final answer NOW.";
27750
+ const exactHint = exact ? "You used exact=true. Try a broader search with exact=false, or use listFiles to browse the directory structure." : `Try exact=true if you need literal/punctuation matching (e.g. 'description: ""'), or use listFiles to explore directories, or search for a broader/related term and filter manually.`;
27751
+ return `DUPLICATE SEARCH BLOCKED (${blockCount}x). You already searched for "${searchQuery}" in this path and got NO results. This term does not appear in the codebase. Do NOT repeat or rephrase \u2014 try a FUNDAMENTALLY different approach: ${exactHint} If multiple approaches have failed, provide your final answer with what you know.`;
27624
27752
  }
27625
- previousSearches.add(searchKey);
27626
- consecutiveDupBlocks = 0;
27753
+ previousSearches.set(searchKey, { hadResults: false });
27627
27754
  paginationCounts.set(searchKey, 0);
27628
27755
  } else {
27629
27756
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
@@ -27637,6 +27764,14 @@ var init_vercel = __esm({
27637
27764
  }
27638
27765
  try {
27639
27766
  const result = maybeAnnotate(await runRawSearch());
27767
+ if (typeof result === "string" && result.includes("No results found")) {
27768
+ if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, "").trim())) {
27769
+ return result + "\n\n\u26A0\uFE0F Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names).";
27770
+ }
27771
+ } else if (typeof result === "string") {
27772
+ const entry = previousSearches.get(searchKey);
27773
+ if (entry) entry.hadResults = true;
27774
+ }
27640
27775
  if (options.fileTracker && typeof result === "string") {
27641
27776
  options.fileTracker.trackFilesFromOutput(result, effectiveSearchCwd).catch(() => {
27642
27777
  });
@@ -27919,7 +28054,31 @@ var init_vercel = __esm({
27919
28054
  });
27920
28055
  };
27921
28056
  delegateTool = (options = {}) => {
27922
- const { debug = false, timeout = 300, cwd, allowedFolders, workspaceRoot, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, delegationManager = null } = options;
28057
+ const {
28058
+ debug = false,
28059
+ timeout = 300,
28060
+ cwd,
28061
+ allowedFolders,
28062
+ workspaceRoot,
28063
+ enableBash = false,
28064
+ bashConfig,
28065
+ architectureFileName,
28066
+ enableMcp = false,
28067
+ mcpConfig = null,
28068
+ mcpConfigPath = null,
28069
+ delegationManager = null,
28070
+ // Timeout settings inherited from parent agent
28071
+ timeoutBehavior,
28072
+ maxOperationTimeout,
28073
+ requestTimeout,
28074
+ gracefulTimeoutBonusSteps,
28075
+ negotiatedTimeoutBudget,
28076
+ negotiatedTimeoutMaxRequests,
28077
+ negotiatedTimeoutMaxPerRequest,
28078
+ parentOperationStartTime,
28079
+ onSubagentCreated,
28080
+ onSubagentCompleted
28081
+ } = options;
27923
28082
  return (0, import_ai.tool)({
27924
28083
  name: "delegate",
27925
28084
  description: delegateDescription,
@@ -27963,9 +28122,30 @@ var init_vercel = __esm({
27963
28122
  console.error(`Using workspace root: ${effectivePath} (cwd was: ${cwd || "not set"})`);
27964
28123
  }
27965
28124
  }
28125
+ let effectiveTimeout = timeout;
28126
+ if (parentOperationStartTime && maxOperationTimeout) {
28127
+ const elapsed = Date.now() - parentOperationStartTime;
28128
+ const remaining = maxOperationTimeout - elapsed;
28129
+ const budgetCap = Math.max(30, Math.floor(remaining * 0.9 / 1e3));
28130
+ if (budgetCap < effectiveTimeout) {
28131
+ effectiveTimeout = budgetCap;
28132
+ if (debug) {
28133
+ console.error(`[DELEGATE] Capping timeout from ${timeout}s to ${effectiveTimeout}s (remaining parent budget: ${Math.floor(remaining / 1e3)}s)`);
28134
+ }
28135
+ if (tracer) {
28136
+ tracer.addEvent("delegation.budget_capped", {
28137
+ "delegation.original_timeout_s": timeout,
28138
+ "delegation.effective_timeout_s": effectiveTimeout,
28139
+ "delegation.parent_elapsed_ms": elapsed,
28140
+ "delegation.parent_remaining_ms": remaining,
28141
+ "delegation.parent_session_id": parentSessionId
28142
+ });
28143
+ }
28144
+ }
28145
+ }
27966
28146
  const result = await delegate({
27967
28147
  task,
27968
- timeout,
28148
+ timeout: effectiveTimeout,
27969
28149
  debug,
27970
28150
  currentIteration: currentIteration || 0,
27971
28151
  maxIterations: maxIterations || 30,
@@ -27984,7 +28164,14 @@ var init_vercel = __esm({
27984
28164
  mcpConfigPath,
27985
28165
  delegationManager,
27986
28166
  // Per-instance delegation limits
27987
- parentAbortSignal
28167
+ parentAbortSignal,
28168
+ // Inherit timeout settings for subagent
28169
+ timeoutBehavior,
28170
+ requestTimeout,
28171
+ gracefulTimeoutBonusSteps,
28172
+ // Subagent lifecycle callbacks for graceful stop coordination
28173
+ onSubagentCreated,
28174
+ onSubagentCompleted
27988
28175
  });
27989
28176
  return result;
27990
28177
  }
@@ -48935,6 +49122,16 @@ var init_fileTracker = __esm({
48935
49122
  }
48936
49123
  });
48937
49124
 
49125
+ // src/agent/otelLogBridge.js
49126
+ var import_module, _require;
49127
+ var init_otelLogBridge = __esm({
49128
+ "src/agent/otelLogBridge.js"() {
49129
+ "use strict";
49130
+ import_module = require("module");
49131
+ _require = (0, import_module.createRequire)("file:///");
49132
+ }
49133
+ });
49134
+
48938
49135
  // src/agent/simpleTelemetry.js
48939
49136
  var import_fs10, import_path11;
48940
49137
  var init_simpleTelemetry = __esm({
@@ -48942,6 +49139,7 @@ var init_simpleTelemetry = __esm({
48942
49139
  "use strict";
48943
49140
  import_fs10 = require("fs");
48944
49141
  import_path11 = require("path");
49142
+ init_otelLogBridge();
48945
49143
  }
48946
49144
  });
48947
49145
 
@@ -63420,7 +63618,22 @@ function mapFlowchartParserError(err, text) {
63420
63618
  const lineContent = allLines[Math.max(0, line - 1)] || "";
63421
63619
  const beforeQuote = lineContent.slice(0, Math.max(0, column - 1));
63422
63620
  const hasLinkBefore = beforeQuote.match(/--\s*$|==\s*$|-\.\s*$|-\.-\s*$|\[\s*$/);
63621
+ const caret0 = Math.max(0, column - 1);
63622
+ const firstBar = lineContent.lastIndexOf("|", caret0);
63623
+ const secondBar = firstBar >= 0 ? lineContent.indexOf("|", caret0 + 1) : -1;
63624
+ const inPipeLabel = firstBar >= 0 && secondBar > firstBar && firstBar < caret0 && secondBar > caret0;
63423
63625
  if (inLinkRule || hasLinkBefore) {
63626
+ if (tokType === "QuotedString" && inPipeLabel) {
63627
+ return {
63628
+ line,
63629
+ column,
63630
+ severity: "error",
63631
+ code: "FL-EDGE-LABEL-QUOTE-IN-PIPES",
63632
+ message: "Quotes are not supported inside pipe-delimited edge labels.",
63633
+ hint: "Use &quot; inside |...|, e.g., --|e.g. &quot;navigate to example.com&quot;|-->",
63634
+ length: len
63635
+ };
63636
+ }
63424
63637
  if (tokType === "DiamondOpen" || tokType === "DiamondClose") {
63425
63638
  return {
63426
63639
  line,
@@ -64022,17 +64235,6 @@ ${br.example}`,
64022
64235
  if (inRule("arrow") && err.name === "NoViableAltException") {
64023
64236
  return { line, column, severity: "error", code: "SE-ARROW-INVALID", message: `Invalid sequence arrow near '${found}'.`, hint: "Use ->, -->, ->>, -->>, -x, --x, -), --), <<->>, or <<-->>", length: len };
64024
64237
  }
64025
- if ((err.name === "NoViableAltException" || err.name === "MismatchedTokenException") && tokType === "Minus") {
64026
- return {
64027
- line,
64028
- column,
64029
- severity: "error",
64030
- code: "SE-BULLET-LINE-UNSUPPORTED",
64031
- message: "Bullet list lines starting with '-' are not supported in sequence diagrams.",
64032
- hint: "Wrap free\u2011form text in a note block instead, for example:\nNote over A : Item 1\nNote over A\n - Item 1\n - Item 2\nend note",
64033
- length: len
64034
- };
64035
- }
64036
64238
  if (inRule("noteStmt")) {
64037
64239
  if (err.name === "MismatchedTokenException" && exp("Colon")) {
64038
64240
  return { line, column, severity: "error", code: "SE-NOTE-MALFORMED", message: "Malformed note: missing colon before the note text.", hint: "Example: Note right of Alice: Hello", length: len };
@@ -64041,6 +64243,22 @@ ${br.example}`,
64041
64243
  return { line, column, severity: "error", code: "SE-NOTE-MALFORMED", message: "Malformed note statement. Use left|right of X or over X[,Y]: text", hint: "Examples: Note over A,B: hi", length: len };
64042
64244
  }
64043
64245
  }
64246
+ if ((err.name === "NoViableAltException" || err.name === "MismatchedTokenException") && tokType === "Minus") {
64247
+ const nonWs = ltxt.search(/\S/);
64248
+ const minusAtLineStart = nonWs >= 0 && ltxt[nonWs] === "-" && column === nonWs + 1;
64249
+ if (!minusAtLineStart) {
64250
+ } else {
64251
+ return {
64252
+ line,
64253
+ column,
64254
+ severity: "error",
64255
+ code: "SE-BULLET-LINE-UNSUPPORTED",
64256
+ message: "Bullet list lines starting with '-' are not supported in sequence diagrams.",
64257
+ hint: "Wrap free\u2011form text in a note block instead, for example:\nNote over A : Item 1\nNote over A\n - Item 1\n - Item 2\nend note",
64258
+ length: len
64259
+ };
64260
+ }
64261
+ }
64044
64262
  if (tokType === "ElseKeyword" && isInRule(err, "criticalBlock")) {
64045
64263
  return {
64046
64264
  line,
@@ -64840,7 +65058,7 @@ var Identifier2, NumberLiteral3, SequenceKeyword, ParticipantKeyword, ActorKeywo
64840
65058
  var init_lexer4 = __esm({
64841
65059
  "node_modules/@probelabs/maid/out/diagrams/sequence/lexer.js"() {
64842
65060
  init_api6();
64843
- Identifier2 = createToken({ name: "Identifier", pattern: /[A-Za-z_][A-Za-z0-9_]*/ });
65061
+ Identifier2 = createToken({ name: "Identifier", pattern: /[A-Za-z_][A-Za-z0-9_.]*(?:-(?![>x)\s])[A-Za-z0-9_.]+)*/ });
64844
65062
  NumberLiteral3 = createToken({ name: "NumberLiteral", pattern: /[0-9]+/ });
64845
65063
  SequenceKeyword = createToken({ name: "SequenceKeyword", pattern: /sequenceDiagram/, longer_alt: Identifier2 });
64846
65064
  ParticipantKeyword = createToken({ name: "ParticipantKeyword", pattern: /participant/i, longer_alt: Identifier2 });
@@ -66660,6 +66878,135 @@ var init_validate5 = __esm({
66660
66878
  }
66661
66879
  });
66662
66880
 
66881
+ // node_modules/@probelabs/maid/out/core/frontmatter.js
66882
+ function parseFrontmatter(input) {
66883
+ const text = input.startsWith("\uFEFF") ? input.slice(1) : input;
66884
+ const lines = text.split(/\r?\n/);
66885
+ if (lines.length < 3 || lines[0].trim() !== "---")
66886
+ return null;
66887
+ let i = 1;
66888
+ const block = [];
66889
+ while (i < lines.length && lines[i].trim() !== "---") {
66890
+ block.push(lines[i]);
66891
+ i++;
66892
+ }
66893
+ if (i >= lines.length)
66894
+ return null;
66895
+ const body = lines.slice(i + 1).join("\n");
66896
+ const bodyStartLine = i + 2;
66897
+ const raw = block.join("\n");
66898
+ const config2 = {};
66899
+ const themeVars = {};
66900
+ let themeUnderConfig = false;
66901
+ let ctx = "root";
66902
+ for (const line of block) {
66903
+ if (!line.trim())
66904
+ continue;
66905
+ const indent = line.match(/^\s*/)?.[0].length ?? 0;
66906
+ const mKey = line.match(/^\s*([A-Za-z0-9_\-]+):\s*(.*)$/);
66907
+ if (!mKey)
66908
+ continue;
66909
+ const key = mKey[1];
66910
+ let value = mKey[2] || "";
66911
+ if (indent === 0) {
66912
+ if (key === "config") {
66913
+ ctx = "config";
66914
+ continue;
66915
+ }
66916
+ if (key === "themeVariables") {
66917
+ ctx = "theme";
66918
+ continue;
66919
+ }
66920
+ ctx = "root";
66921
+ continue;
66922
+ }
66923
+ if (ctx === "config") {
66924
+ if (indent <= 2 && key !== "pie" && key !== "themeVariables")
66925
+ continue;
66926
+ if (key === "pie") {
66927
+ ctx = "config.pie";
66928
+ ensure(config2, "pie", {});
66929
+ continue;
66930
+ }
66931
+ if (key === "themeVariables") {
66932
+ ctx = "theme";
66933
+ themeUnderConfig = true;
66934
+ continue;
66935
+ }
66936
+ continue;
66937
+ }
66938
+ if (ctx === "config.pie") {
66939
+ if (indent < 4) {
66940
+ if (key === "pie") {
66941
+ ctx = "config.pie";
66942
+ ensure(config2, "pie", {});
66943
+ continue;
66944
+ }
66945
+ if (key === "themeVariables") {
66946
+ ctx = "theme";
66947
+ themeUnderConfig = true;
66948
+ continue;
66949
+ }
66950
+ ctx = "config";
66951
+ continue;
66952
+ }
66953
+ setKV(config2.pie, key, value);
66954
+ continue;
66955
+ }
66956
+ if (ctx === "theme") {
66957
+ if (indent < 2) {
66958
+ ctx = "root";
66959
+ continue;
66960
+ }
66961
+ setKV(themeVars, key, value);
66962
+ continue;
66963
+ }
66964
+ }
66965
+ if (themeUnderConfig && Object.keys(themeVars).length) {
66966
+ ensure(config2, "themeVariables", {});
66967
+ Object.assign(config2.themeVariables, themeVars);
66968
+ }
66969
+ return {
66970
+ raw,
66971
+ body,
66972
+ bodyStartLine,
66973
+ config: Object.keys(config2).length ? config2 : void 0,
66974
+ themeVariables: Object.keys(themeVars).length ? themeVars : void 0
66975
+ };
66976
+ }
66977
+ function ensure(obj, key, def) {
66978
+ if (obj[key] == null)
66979
+ obj[key] = def;
66980
+ }
66981
+ function unquote(val) {
66982
+ const v = val.trim();
66983
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
66984
+ return v.slice(1, -1);
66985
+ }
66986
+ return v;
66987
+ }
66988
+ function setKV(target, key, rawValue) {
66989
+ const v = unquote(rawValue);
66990
+ if (v === "") {
66991
+ target[key] = "";
66992
+ return;
66993
+ }
66994
+ const num = Number(v);
66995
+ if (!Number.isNaN(num) && /^-?[0-9]+(\.[0-9]+)?$/.test(v)) {
66996
+ target[key] = num;
66997
+ return;
66998
+ }
66999
+ if (/^(true|false)$/i.test(v)) {
67000
+ target[key] = /^true$/i.test(v);
67001
+ return;
67002
+ }
67003
+ target[key] = v;
67004
+ }
67005
+ var init_frontmatter = __esm({
67006
+ "node_modules/@probelabs/maid/out/core/frontmatter.js"() {
67007
+ }
67008
+ });
67009
+
66663
67010
  // node_modules/@probelabs/maid/out/core/router.js
66664
67011
  function firstNonCommentLine(text) {
66665
67012
  const lines = text.split(/\r?\n/);
@@ -66674,7 +67021,8 @@ function firstNonCommentLine(text) {
66674
67021
  return void 0;
66675
67022
  }
66676
67023
  function detectDiagramType(text) {
66677
- const header = firstNonCommentLine(text);
67024
+ const { content } = stripFrontmatter(text);
67025
+ const header = firstNonCommentLine(content);
66678
67026
  if (!header)
66679
67027
  return "unknown";
66680
67028
  if (/^(flowchart|graph)\b/i.test(header))
@@ -66726,20 +67074,26 @@ function isOtherMermaidDiagram(headerLine) {
66726
67074
  return OTHER.has(t);
66727
67075
  }
66728
67076
  function validate(text, options = {}) {
66729
- const type = detectDiagramType(text);
67077
+ const { content, lineOffset } = stripFrontmatter(text);
67078
+ const type = detectDiagramType(content);
67079
+ const withOffset = (errors) => {
67080
+ if (lineOffset === 0)
67081
+ return errors;
67082
+ return errors.map((e) => ({ ...e, line: Math.max(1, (e.line || 1) + lineOffset) }));
67083
+ };
66730
67084
  switch (type) {
66731
67085
  case "flowchart":
66732
- return { type, errors: validateFlowchart(text, options) };
67086
+ return { type, errors: withOffset(validateFlowchart(content, options)) };
66733
67087
  case "pie":
66734
- return { type, errors: validatePie(text, options) };
67088
+ return { type, errors: withOffset(validatePie(content, options)) };
66735
67089
  case "sequence":
66736
- return { type, errors: validateSequence(text, options) };
67090
+ return { type, errors: withOffset(validateSequence(content, options)) };
66737
67091
  case "class":
66738
- return { type, errors: validateClass(text, options) };
67092
+ return { type, errors: withOffset(validateClass(content, options)) };
66739
67093
  case "state":
66740
- return { type, errors: validateState(text, options) };
67094
+ return { type, errors: withOffset(validateState(content, options)) };
66741
67095
  default:
66742
- const header = firstNonCommentLine(text);
67096
+ const header = firstNonCommentLine(content);
66743
67097
  if (isOtherMermaidDiagram(header)) {
66744
67098
  return { type, errors: [] };
66745
67099
  }
@@ -66747,7 +67101,7 @@ function validate(text, options = {}) {
66747
67101
  type,
66748
67102
  errors: [
66749
67103
  {
66750
- line: 1,
67104
+ line: lineOffset + 1,
66751
67105
  column: 1,
66752
67106
  message: 'Diagram must start with "graph", "flowchart", "pie", "sequenceDiagram", "classDiagram" or "stateDiagram[-v2]"',
66753
67107
  severity: "error",
@@ -66758,6 +67112,12 @@ function validate(text, options = {}) {
66758
67112
  };
66759
67113
  }
66760
67114
  }
67115
+ function stripFrontmatter(text) {
67116
+ const fm = parseFrontmatter(text);
67117
+ if (!fm)
67118
+ return { content: text, lineOffset: 0 };
67119
+ return { content: fm.body, lineOffset: fm.bodyStartLine - 1 };
67120
+ }
66761
67121
  var init_router = __esm({
66762
67122
  "node_modules/@probelabs/maid/out/core/router.js"() {
66763
67123
  init_validate();
@@ -66765,6 +67125,7 @@ var init_router = __esm({
66765
67125
  init_validate3();
66766
67126
  init_validate4();
66767
67127
  init_validate5();
67128
+ init_frontmatter();
66768
67129
  }
66769
67130
  });
66770
67131
 
@@ -66976,16 +67337,18 @@ function computeFixes(text, errors, level = "safe") {
66976
67337
  }
66977
67338
  continue;
66978
67339
  }
66979
- if (is("FL-EDGE-LABEL-BRACKET", e) || is("FL-EDGE-LABEL-CURLY-IN-PIPES", e)) {
67340
+ if (is("FL-EDGE-LABEL-BRACKET", e) || is("FL-EDGE-LABEL-CURLY-IN-PIPES", e) || is("FL-EDGE-LABEL-QUOTE-IN-PIPES", e)) {
66980
67341
  const lineText = lineTextAt(text, e.line);
66981
- const firstBar = lineText.indexOf("|");
66982
- const secondBar = firstBar >= 0 ? lineText.indexOf("|", firstBar + 1) : -1;
67342
+ const col = Math.max(0, e.column - 1);
67343
+ const firstBar = lineText.lastIndexOf("|", col);
67344
+ const secondBar = firstBar >= 0 ? lineText.indexOf("|", col + 1) : -1;
66983
67345
  if (firstBar >= 0 && secondBar > firstBar) {
66984
67346
  const before = lineText.slice(0, firstBar + 1);
66985
67347
  const label = lineText.slice(firstBar + 1, secondBar);
66986
67348
  const after = lineText.slice(secondBar);
66987
67349
  let fixedLabel = label.replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
66988
67350
  fixedLabel = fixedLabel.replace(/\{/g, "&#123;").replace(/\}/g, "&#125;");
67351
+ fixedLabel = fixedLabel.replace(/\\"/g, "&quot;").replace(/"/g, "&quot;");
66989
67352
  const fixedLine = before + fixedLabel + after;
66990
67353
  const finalLine = fixedLine.replace(/\[([^\]]*)\]/g, (m, seg) => "[" + String(seg).replace(/`/g, "") + "]");
66991
67354
  edits.push({ start: { line: e.line, column: 1 }, end: { line: e.line, column: lineText.length + 1 }, newText: finalLine });
@@ -78375,7 +78738,7 @@ ${overlay}</g>`;
78375
78738
  });
78376
78739
 
78377
78740
  // node_modules/@probelabs/maid/out/renderer/pie-builder.js
78378
- function unquote(s) {
78741
+ function unquote2(s) {
78379
78742
  if (!s)
78380
78743
  return s;
78381
78744
  const first2 = s.charAt(0);
@@ -78425,7 +78788,7 @@ function buildPieModel(text) {
78425
78788
  const collect = (k) => {
78426
78789
  const arr = tnode.children?.[k] ?? [];
78427
78790
  for (const tok of arr)
78428
- parts.push(unquote(tok.image));
78791
+ parts.push(unquote2(tok.image));
78429
78792
  };
78430
78793
  collect("QuotedString");
78431
78794
  collect("Text");
@@ -78438,7 +78801,7 @@ function buildPieModel(text) {
78438
78801
  const labelTok = snode.children?.sliceLabel?.[0]?.children?.QuotedString?.[0];
78439
78802
  const numTok = snode.children?.NumberLiteral?.[0];
78440
78803
  if (labelTok && numTok) {
78441
- const label = unquote(labelTok.image).trim();
78804
+ const label = unquote2(labelTok.image).trim();
78442
78805
  const value = Number(numTok.image);
78443
78806
  if (!Number.isNaN(value)) {
78444
78807
  model.slices.push({ label, value });
@@ -79594,128 +79957,6 @@ var init_state_renderer = __esm({
79594
79957
  }
79595
79958
  });
79596
79959
 
79597
- // node_modules/@probelabs/maid/out/core/frontmatter.js
79598
- function parseFrontmatter(input) {
79599
- const text = input.startsWith("\uFEFF") ? input.slice(1) : input;
79600
- const lines = text.split(/\r?\n/);
79601
- if (lines.length < 3 || lines[0].trim() !== "---")
79602
- return null;
79603
- let i = 1;
79604
- const block = [];
79605
- while (i < lines.length && lines[i].trim() !== "---") {
79606
- block.push(lines[i]);
79607
- i++;
79608
- }
79609
- if (i >= lines.length)
79610
- return null;
79611
- const body = lines.slice(i + 1).join("\n");
79612
- const raw = block.join("\n");
79613
- const config2 = {};
79614
- const themeVars = {};
79615
- let themeUnderConfig = false;
79616
- let ctx = "root";
79617
- for (const line of block) {
79618
- if (!line.trim())
79619
- continue;
79620
- const indent = line.match(/^\s*/)?.[0].length ?? 0;
79621
- const mKey = line.match(/^\s*([A-Za-z0-9_\-]+):\s*(.*)$/);
79622
- if (!mKey)
79623
- continue;
79624
- const key = mKey[1];
79625
- let value = mKey[2] || "";
79626
- if (indent === 0) {
79627
- if (key === "config") {
79628
- ctx = "config";
79629
- continue;
79630
- }
79631
- if (key === "themeVariables") {
79632
- ctx = "theme";
79633
- continue;
79634
- }
79635
- ctx = "root";
79636
- continue;
79637
- }
79638
- if (ctx === "config") {
79639
- if (indent <= 2 && key !== "pie" && key !== "themeVariables")
79640
- continue;
79641
- if (key === "pie") {
79642
- ctx = "config.pie";
79643
- ensure(config2, "pie", {});
79644
- continue;
79645
- }
79646
- if (key === "themeVariables") {
79647
- ctx = "theme";
79648
- themeUnderConfig = true;
79649
- continue;
79650
- }
79651
- continue;
79652
- }
79653
- if (ctx === "config.pie") {
79654
- if (indent < 4) {
79655
- if (key === "pie") {
79656
- ctx = "config.pie";
79657
- ensure(config2, "pie", {});
79658
- continue;
79659
- }
79660
- if (key === "themeVariables") {
79661
- ctx = "theme";
79662
- themeUnderConfig = true;
79663
- continue;
79664
- }
79665
- ctx = "config";
79666
- continue;
79667
- }
79668
- setKV(config2.pie, key, value);
79669
- continue;
79670
- }
79671
- if (ctx === "theme") {
79672
- if (indent < 2) {
79673
- ctx = "root";
79674
- continue;
79675
- }
79676
- setKV(themeVars, key, value);
79677
- continue;
79678
- }
79679
- }
79680
- if (themeUnderConfig && Object.keys(themeVars).length) {
79681
- ensure(config2, "themeVariables", {});
79682
- Object.assign(config2.themeVariables, themeVars);
79683
- }
79684
- return { raw, body, config: Object.keys(config2).length ? config2 : void 0, themeVariables: Object.keys(themeVars).length ? themeVars : void 0 };
79685
- }
79686
- function ensure(obj, key, def) {
79687
- if (obj[key] == null)
79688
- obj[key] = def;
79689
- }
79690
- function unquote2(val) {
79691
- const v = val.trim();
79692
- if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
79693
- return v.slice(1, -1);
79694
- }
79695
- return v;
79696
- }
79697
- function setKV(target, key, rawValue) {
79698
- const v = unquote2(rawValue);
79699
- if (v === "") {
79700
- target[key] = "";
79701
- return;
79702
- }
79703
- const num = Number(v);
79704
- if (!Number.isNaN(num) && /^-?[0-9]+(\.[0-9]+)?$/.test(v)) {
79705
- target[key] = num;
79706
- return;
79707
- }
79708
- if (/^(true|false)$/i.test(v)) {
79709
- target[key] = /^true$/i.test(v);
79710
- return;
79711
- }
79712
- target[key] = v;
79713
- }
79714
- var init_frontmatter = __esm({
79715
- "node_modules/@probelabs/maid/out/core/frontmatter.js"() {
79716
- }
79717
- });
79718
-
79719
79960
  // node_modules/@probelabs/maid/out/renderer/class-builder.js
79720
79961
  function textFromTokens3(tokens) {
79721
79962
  if (!tokens || tokens.length === 0)
@@ -88952,6 +89193,9 @@ function isMethodAllowed(methodName, allowedMethods, blockedMethods) {
88952
89193
  }
88953
89194
  function createTransport(serverConfig) {
88954
89195
  const { transport, command, args, url: url2, env } = serverConfig;
89196
+ if (serverConfig.transportInstance) {
89197
+ return serverConfig.transportInstance;
89198
+ }
88955
89199
  switch (transport) {
88956
89200
  case "stdio":
88957
89201
  return new import_stdio.StdioClientTransport({
@@ -89035,6 +89279,7 @@ var init_client = __esm({
89035
89279
  this.debug = options.debug || process.env.DEBUG_MCP === "1";
89036
89280
  this.config = null;
89037
89281
  this.tracer = options.tracer || null;
89282
+ this.agentEvents = options.agentEvents || null;
89038
89283
  }
89039
89284
  /**
89040
89285
  * Record an MCP telemetry event if tracer is available
@@ -89262,11 +89507,21 @@ var init_client = __esm({
89262
89507
  throw new Error(`Server ${tool6.serverName} not connected`);
89263
89508
  }
89264
89509
  const startTime = Date.now();
89510
+ const toolCallId = `mcp-${toolName}-${startTime}`;
89265
89511
  this.recordMcpEvent("tool.call_started", {
89266
89512
  toolName,
89267
89513
  serverName: tool6.serverName,
89268
89514
  originalToolName: tool6.originalName
89269
89515
  });
89516
+ if (this.agentEvents) {
89517
+ this.agentEvents.emit("toolCall", {
89518
+ toolCallId,
89519
+ name: toolName,
89520
+ args,
89521
+ status: "started",
89522
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
89523
+ });
89524
+ }
89270
89525
  try {
89271
89526
  if (this.debug) {
89272
89527
  console.error(`[MCP DEBUG] Calling ${toolName} with args:`, JSON.stringify(args, null, 2));
@@ -89296,6 +89551,14 @@ var init_client = __esm({
89296
89551
  originalToolName: tool6.originalName,
89297
89552
  durationMs
89298
89553
  });
89554
+ if (this.agentEvents) {
89555
+ this.agentEvents.emit("toolCall", {
89556
+ toolCallId,
89557
+ name: toolName,
89558
+ status: "completed",
89559
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
89560
+ });
89561
+ }
89299
89562
  return result;
89300
89563
  } catch (error40) {
89301
89564
  const durationMs = Date.now() - startTime;
@@ -89311,9 +89574,64 @@ var init_client = __esm({
89311
89574
  durationMs,
89312
89575
  isTimeout: error40.message.includes("timeout")
89313
89576
  });
89577
+ if (this.agentEvents) {
89578
+ this.agentEvents.emit("toolCall", {
89579
+ toolCallId,
89580
+ name: toolName,
89581
+ status: "error",
89582
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
89583
+ });
89584
+ }
89314
89585
  throw error40;
89315
89586
  }
89316
89587
  }
89588
+ /**
89589
+ * Call graceful_stop on all MCP servers that expose it.
89590
+ * This signals agent-type MCP servers to wrap up their work.
89591
+ * @returns {Promise<Array<{server: string, success: boolean, error?: string}>>}
89592
+ */
89593
+ async callGracefulStopAll() {
89594
+ const results = [];
89595
+ for (const [serverName, clientInfo] of this.clients) {
89596
+ const qualifiedName = `${serverName}_graceful_stop`;
89597
+ if (this.tools.has(qualifiedName)) {
89598
+ if (this.debug) {
89599
+ console.log(`[DEBUG] MCP callGracefulStopAll: calling graceful_stop on server "${serverName}"`);
89600
+ }
89601
+ try {
89602
+ const timeoutMs = 5e3;
89603
+ const timeoutPromise = new Promise(
89604
+ (_, reject2) => setTimeout(() => reject2(new Error("graceful_stop timeout")), timeoutMs)
89605
+ );
89606
+ await Promise.race([
89607
+ clientInfo.client.callTool({ name: "graceful_stop", arguments: {} }, void 0, { timeout: timeoutMs }),
89608
+ timeoutPromise
89609
+ ]);
89610
+ results.push({ server: serverName, success: true });
89611
+ if (this.debug) {
89612
+ console.log(`[DEBUG] MCP callGracefulStopAll: server "${serverName}" acknowledged graceful_stop`);
89613
+ }
89614
+ } catch (e) {
89615
+ results.push({ server: serverName, success: false, error: e.message });
89616
+ if (this.debug) {
89617
+ console.log(`[DEBUG] MCP callGracefulStopAll: server "${serverName}" graceful_stop failed: ${e.message}`);
89618
+ }
89619
+ }
89620
+ }
89621
+ }
89622
+ if (this.debug) {
89623
+ const withStop = results.length;
89624
+ const total = this.clients.size;
89625
+ console.log(`[DEBUG] MCP callGracefulStopAll: ${withStop}/${total} servers had graceful_stop tool`);
89626
+ }
89627
+ this.recordMcpEvent("graceful_stop.sweep_completed", {
89628
+ servers_total: this.clients.size,
89629
+ servers_with_graceful_stop: results.length,
89630
+ servers_acknowledged: results.filter((r) => r.success).length,
89631
+ servers_failed: results.filter((r) => !r.success).length
89632
+ });
89633
+ return results;
89634
+ }
89317
89635
  /**
89318
89636
  * Get all available tools with their schemas
89319
89637
  * @returns {Object} Map of tool name to tool definition
@@ -89341,10 +89659,29 @@ var init_client = __esm({
89341
89659
  inputSchema: tool6.inputSchema,
89342
89660
  execute: async (args) => {
89343
89661
  const result = await this.callTool(name15, args);
89344
- if (result.content && result.content[0]) {
89345
- return result.content[0].text;
89662
+ if (!result.content || !result.content[0]) {
89663
+ return JSON.stringify(result);
89664
+ }
89665
+ const hasImage = result.content.some((block) => block.type === "image");
89666
+ if (hasImage) {
89667
+ return { _mcpContent: result.content };
89668
+ }
89669
+ return result.content[0].text;
89670
+ },
89671
+ // Convert MCP content blocks (including images) to Vercel AI SDK format
89672
+ toModelOutput: ({ output }) => {
89673
+ if (output && typeof output === "object" && output._mcpContent) {
89674
+ const parts = [];
89675
+ for (const block of output._mcpContent) {
89676
+ if (block.type === "text") {
89677
+ parts.push({ type: "text", text: block.text });
89678
+ } else if (block.type === "image") {
89679
+ parts.push({ type: "image-data", data: block.data, mediaType: block.mimeType });
89680
+ }
89681
+ }
89682
+ return { type: "content", value: parts };
89346
89683
  }
89347
- return JSON.stringify(result);
89684
+ return { type: "text", value: typeof output === "string" ? output : JSON.stringify(output) };
89348
89685
  }
89349
89686
  };
89350
89687
  }
@@ -89430,6 +89767,7 @@ var init_xmlBridge = __esm({
89430
89767
  constructor(options = {}) {
89431
89768
  this.debug = options.debug || false;
89432
89769
  this.tracer = options.tracer || null;
89770
+ this.agentEvents = options.agentEvents || null;
89433
89771
  this.mcpTools = {};
89434
89772
  this.mcpManager = null;
89435
89773
  this.toolDescriptions = {};
@@ -89472,7 +89810,7 @@ var init_xmlBridge = __esm({
89472
89810
  if (this.debug) {
89473
89811
  console.error("[MCP DEBUG] Initializing MCP client manager...");
89474
89812
  }
89475
- this.mcpManager = new MCPClientManager({ debug: this.debug, tracer: this.tracer });
89813
+ this.mcpManager = new MCPClientManager({ debug: this.debug, tracer: this.tracer, agentEvents: this.agentEvents });
89476
89814
  const result = await this.mcpManager.initialize(mcpConfigs);
89477
89815
  const vercelTools = this.mcpManager.getVercelTools();
89478
89816
  this.mcpTools = vercelTools;
@@ -89524,6 +89862,16 @@ var init_xmlBridge = __esm({
89524
89862
  isMcpTool(toolName) {
89525
89863
  return toolName in this.mcpTools;
89526
89864
  }
89865
+ /**
89866
+ * Call graceful_stop on all MCP servers that expose it.
89867
+ * @returns {Promise<Array>}
89868
+ */
89869
+ async callGracefulStopAll() {
89870
+ if (this.mcpManager) {
89871
+ return this.mcpManager.callGracefulStopAll();
89872
+ }
89873
+ return [];
89874
+ }
89527
89875
  /**
89528
89876
  * Clean up MCP connections
89529
89877
  */
@@ -96909,7 +97257,7 @@ function deriveDescription(rawDescription, body) {
96909
97257
  }
96910
97258
  return truncateDescription(description);
96911
97259
  }
96912
- function stripFrontmatter(content) {
97260
+ function stripFrontmatter2(content) {
96913
97261
  const { body } = extractFrontmatter(content);
96914
97262
  return body.trim();
96915
97263
  }
@@ -97050,7 +97398,7 @@ var init_registry = __esm({
97050
97398
  const skill = this.skillsByName.get(name15);
97051
97399
  if (!skill) return null;
97052
97400
  const content = await (0, import_promises3.readFile)(skill.skillFilePath, "utf8");
97053
- return stripFrontmatter(content);
97401
+ return stripFrontmatter2(content);
97054
97402
  }
97055
97403
  async _resolveRealPath(target) {
97056
97404
  try {
@@ -99762,6 +100110,7 @@ var init_ProbeAgent = __esm({
99762
100110
  this.debug = options.debug || process.env.DEBUG === "1";
99763
100111
  this.cancelled = false;
99764
100112
  this._abortController = new AbortController();
100113
+ this._activeSubagents = /* @__PURE__ */ new Map();
99765
100114
  this.tracer = options.tracer || null;
99766
100115
  this.outline = !!options.outline;
99767
100116
  this.searchDelegate = options.searchDelegate !== void 0 ? !!options.searchDelegate : true;
@@ -99873,14 +100222,31 @@ var init_ProbeAgent = __esm({
99873
100222
  this.timeoutBehavior = options.timeoutBehavior ?? (() => {
99874
100223
  const val = process.env.TIMEOUT_BEHAVIOR;
99875
100224
  if (val === "hard") return "hard";
100225
+ if (val === "negotiated") return "negotiated";
99876
100226
  return "graceful";
99877
100227
  })();
99878
100228
  this.gracefulTimeoutBonusSteps = options.gracefulTimeoutBonusSteps ?? (() => {
99879
100229
  const parsed = parseInt(process.env.GRACEFUL_TIMEOUT_BONUS_STEPS, 10);
99880
100230
  return isNaN(parsed) || parsed < 1 || parsed > 20 ? 4 : parsed;
99881
100231
  })();
100232
+ this.negotiatedTimeoutBudget = options.negotiatedTimeoutBudget ?? (() => {
100233
+ const parsed = parseInt(process.env.NEGOTIATED_TIMEOUT_BUDGET, 10);
100234
+ return isNaN(parsed) || parsed < 6e4 || parsed > 72e5 ? 18e5 : parsed;
100235
+ })();
100236
+ this.negotiatedTimeoutMaxRequests = options.negotiatedTimeoutMaxRequests ?? (() => {
100237
+ const parsed = parseInt(process.env.NEGOTIATED_TIMEOUT_MAX_REQUESTS, 10);
100238
+ return isNaN(parsed) || parsed < 1 || parsed > 10 ? 3 : parsed;
100239
+ })();
100240
+ this.negotiatedTimeoutMaxPerRequest = options.negotiatedTimeoutMaxPerRequest ?? (() => {
100241
+ const parsed = parseInt(process.env.NEGOTIATED_TIMEOUT_MAX_PER_REQUEST, 10);
100242
+ return isNaN(parsed) || parsed < 6e4 || parsed > 36e5 ? 6e5 : parsed;
100243
+ })();
100244
+ this.gracefulStopDeadline = options.gracefulStopDeadline ?? (() => {
100245
+ const parsed = parseInt(process.env.GRACEFUL_STOP_DEADLINE, 10);
100246
+ return isNaN(parsed) || parsed < 5e3 || parsed > 3e5 ? 45e3 : parsed;
100247
+ })();
99882
100248
  if (this.debug) {
99883
- console.log(`[DEBUG] Timeout behavior: ${this.timeoutBehavior}, bonus steps: ${this.gracefulTimeoutBonusSteps}`);
100249
+ console.log(`[DEBUG] Timeout behavior: ${this.timeoutBehavior}, bonus steps: ${this.gracefulTimeoutBonusSteps}, graceful stop deadline: ${this.gracefulStopDeadline}ms`);
99884
100250
  }
99885
100251
  this.retryConfig = options.retry || {};
99886
100252
  this.retryManager = null;
@@ -100232,6 +100598,18 @@ var init_ProbeAgent = __esm({
100232
100598
  // Per-instance delegation limits
100233
100599
  parentAbortSignal: this._abortController.signal,
100234
100600
  // Propagate cancellation to delegations
100601
+ // Timeout settings for delegate subagents to inherit
100602
+ timeoutBehavior: this.timeoutBehavior,
100603
+ maxOperationTimeout: this.maxOperationTimeout,
100604
+ requestTimeout: this.requestTimeout,
100605
+ gracefulTimeoutBonusSteps: this.gracefulTimeoutBonusSteps,
100606
+ negotiatedTimeoutBudget: this.negotiatedTimeoutBudget,
100607
+ negotiatedTimeoutMaxRequests: this.negotiatedTimeoutMaxRequests,
100608
+ negotiatedTimeoutMaxPerRequest: this.negotiatedTimeoutMaxPerRequest,
100609
+ parentOperationStartTime: this._operationStartTime,
100610
+ // For remaining budget calculation
100611
+ onSubagentCreated: (sid, subagent) => this._registerSubagent(sid, subagent),
100612
+ onSubagentCompleted: (sid) => this._unregisterSubagent(sid),
100235
100613
  outputBuffer: this._outputBuffer,
100236
100614
  concurrencyLimiter: this.concurrencyLimiter,
100237
100615
  // Global AI concurrency limiter
@@ -100816,7 +101194,7 @@ var init_ProbeAgent = __esm({
100816
101194
  }
100817
101195
  if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
100818
101196
  const gts = this._gracefulTimeoutState;
100819
- if (this.timeoutBehavior === "graceful" && gts) {
101197
+ if ((this.timeoutBehavior === "graceful" || this.timeoutBehavior === "negotiated") && gts) {
100820
101198
  } else {
100821
101199
  timeoutState.timeoutId = setTimeout(() => {
100822
101200
  controller.abort();
@@ -101720,7 +102098,7 @@ var init_ProbeAgent = __esm({
101720
102098
  }
101721
102099
  mcpConfig = null;
101722
102100
  }
101723
- this.mcpBridge = new MCPXmlBridge({ debug: this.debug });
102101
+ this.mcpBridge = new MCPXmlBridge({ debug: this.debug, agentEvents: this.events });
101724
102102
  await this.mcpBridge.initialize(mcpConfig);
101725
102103
  const mcpToolNames = this.mcpBridge.getToolNames();
101726
102104
  const mcpToolCount = mcpToolNames.length;
@@ -102203,6 +102581,7 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102203
102581
  } else {
102204
102582
  options = schemaOrOptions || {};
102205
102583
  }
102584
+ this._operationStartTime = Date.now();
102206
102585
  try {
102207
102586
  const oldHistoryLength = this.history.length;
102208
102587
  if (this._outputBuffer && !options?._schemaFormatted && !options?._completionPromptProcessed) {
@@ -102283,7 +102662,10 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102283
102662
  }
102284
102663
  }
102285
102664
  let currentIteration = 0;
102286
- let finalResult = "I was unable to complete your request due to reaching the maximum number of tool iterations.";
102665
+ let finalResult = null;
102666
+ const DEFAULT_MAX_ITER_MSG = "I was unable to complete your request due to reaching the maximum number of tool iterations.";
102667
+ const _toolCallLog = [];
102668
+ let abortSummaryTaken = false;
102287
102669
  const baseMaxIterations = options._maxIterationsOverride || this.maxIterations || MAX_TOOL_ITERATIONS;
102288
102670
  const maxIterations = options._maxIterationsOverride ? baseMaxIterations : options.schema ? baseMaxIterations + 4 : baseMaxIterations;
102289
102671
  const isClaudeCode = this.clientApiProvider === "claude-code" || process.env.USE_CLAUDE_CODE === "true";
@@ -102423,6 +102805,233 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102423
102805
  bonusStepsMax: this.gracefulTimeoutBonusSteps
102424
102806
  };
102425
102807
  this._gracefulTimeoutState = gracefulTimeoutState;
102808
+ const negotiatedTimeoutState = {
102809
+ extensionsUsed: 0,
102810
+ totalExtraTimeMs: 0,
102811
+ softTimeoutId: null,
102812
+ hardAbortTimeoutId: null,
102813
+ maxRequests: this.negotiatedTimeoutMaxRequests,
102814
+ maxPerRequestMs: this.negotiatedTimeoutMaxPerRequest,
102815
+ budgetMs: this.negotiatedTimeoutBudget,
102816
+ observerRunning: false,
102817
+ // true while observer LLM call is in flight
102818
+ extensionMessage: null,
102819
+ // message to show in prepareStep after extension granted
102820
+ startTime: Date.now()
102821
+ };
102822
+ this._negotiatedTimeoutState = negotiatedTimeoutState;
102823
+ const activeTools = /* @__PURE__ */ new Map();
102824
+ this._activeTools = activeTools;
102825
+ const onToolCall = (event) => {
102826
+ const key = event.toolCallId || `${event.name}:${JSON.stringify(event.args || {}).slice(0, 100)}`;
102827
+ if (event.status === "started") {
102828
+ activeTools.set(key, {
102829
+ name: event.name,
102830
+ args: event.args,
102831
+ startedAt: event.timestamp || (/* @__PURE__ */ new Date()).toISOString()
102832
+ });
102833
+ } else if (event.status === "completed" || event.status === "error") {
102834
+ activeTools.delete(key);
102835
+ }
102836
+ };
102837
+ this.events.on("toolCall", onToolCall);
102838
+ const runTimeoutObserver = async () => {
102839
+ if (negotiatedTimeoutState.observerRunning) return;
102840
+ negotiatedTimeoutState.observerRunning = true;
102841
+ const remainingRequests = negotiatedTimeoutState.maxRequests - negotiatedTimeoutState.extensionsUsed;
102842
+ const remainingBudgetMs = negotiatedTimeoutState.budgetMs - negotiatedTimeoutState.totalExtraTimeMs;
102843
+ const maxPerReqMin = Math.round(negotiatedTimeoutState.maxPerRequestMs / 6e4);
102844
+ const elapsedMin = Math.round((Date.now() - negotiatedTimeoutState.startTime) / 6e4);
102845
+ if (remainingRequests <= 0 || remainingBudgetMs <= 0) {
102846
+ if (this.debug) {
102847
+ console.log(`[DEBUG] Timeout observer: no extensions/budget remaining \u2014 aborting in-flight tools and triggering graceful wind-down`);
102848
+ }
102849
+ if (this.tracer) {
102850
+ this.tracer.addEvent("negotiated_timeout.observer_exhausted", {
102851
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
102852
+ max_requests: negotiatedTimeoutState.maxRequests,
102853
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
102854
+ budget_ms: negotiatedTimeoutState.budgetMs,
102855
+ elapsed_min: elapsedMin,
102856
+ active_tools: Array.from(activeTools.values()).map((t) => t.name)
102857
+ });
102858
+ }
102859
+ await this._initiateGracefulStop(gracefulTimeoutState, "budget/extensions exhausted");
102860
+ negotiatedTimeoutState.observerRunning = false;
102861
+ return;
102862
+ }
102863
+ const activeToolsList = Array.from(activeTools.values());
102864
+ const now = Date.now();
102865
+ const formatDuration = (ms) => {
102866
+ const totalSec = Math.round(ms / 1e3);
102867
+ if (totalSec < 60) return `${totalSec}s`;
102868
+ const min = Math.floor(totalSec / 60);
102869
+ const sec = totalSec % 60;
102870
+ if (min < 60) return `${min}m ${sec}s`;
102871
+ const hr = Math.floor(min / 60);
102872
+ const remainMin = min % 60;
102873
+ return `${hr}h ${remainMin}m`;
102874
+ };
102875
+ const activeToolsDesc = activeToolsList.length > 0 ? activeToolsList.map((t) => {
102876
+ const runningForMs = now - new Date(t.startedAt).getTime();
102877
+ return `- ${t.name}(${JSON.stringify(t.args || {}).slice(0, 200)}) \u2014 running for ${formatDuration(runningForMs)}`;
102878
+ }).join("\n") : "(none currently running)";
102879
+ const recentHistory = this.history.slice(-6).map((msg) => {
102880
+ const content = typeof msg.content === "string" ? msg.content.slice(0, 300) : JSON.stringify(msg.content).slice(0, 300);
102881
+ return `[${msg.role}]: ${content}`;
102882
+ }).join("\n");
102883
+ 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.
102884
+
102885
+ ## Recent Conversation
102886
+ ${recentHistory || "(no history yet)"}
102887
+
102888
+ ## Currently Running Tools
102889
+ ${activeToolsDesc}
102890
+
102891
+ ## Budget
102892
+ - Extensions used: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}
102893
+ - Time budget remaining: ${Math.round(remainingBudgetMs / 6e4)} minutes
102894
+ - Max per extension: ${maxPerReqMin} minutes
102895
+
102896
+ Decide whether the agent should get more time. EXTEND if:
102897
+ - Tools are actively running (especially delegates or complex analysis) \u2014 they need time to finish
102898
+ - The agent is making clear progress on a complex task
102899
+ - New information is being gathered that will improve the final answer
102900
+
102901
+ DO NOT EXTEND if:
102902
+ - The agent appears stuck in a loop (repeating the same tool calls or getting the same errors)
102903
+ - The conversation shows the agent retrying failed operations without changing approach
102904
+ - The agent has enough information to answer but keeps searching for more
102905
+ - Tool calls are returning empty or error results repeatedly
102906
+ - The agent is doing redundant work (searching for things it already found)
102907
+
102908
+ 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.
102909
+
102910
+ Respond with ONLY valid JSON (no markdown, no explanation):
102911
+ {"extend": true, "minutes": <1-${maxPerReqMin}>, "reason": "your reason here"}
102912
+ or
102913
+ {"extend": false, "reason": "your reason here"}`;
102914
+ const observerFn = async () => {
102915
+ const modelInstance = this.provider ? this.provider(this.model) : this.model;
102916
+ if (this.debug) {
102917
+ console.log(`[DEBUG] Timeout observer: making LLM call (${activeToolsList.length} active tools, ${elapsedMin} min elapsed)`);
102918
+ }
102919
+ if (this.tracer) {
102920
+ this.tracer.addEvent("negotiated_timeout.observer_invoked", {
102921
+ elapsed_min: elapsedMin,
102922
+ active_tools: activeToolsList.map((t) => t.name),
102923
+ active_tools_detail: activeToolsList.map((t) => ({
102924
+ name: t.name,
102925
+ running_for_ms: now - new Date(t.startedAt).getTime(),
102926
+ args_preview: JSON.stringify(t.args || {}).slice(0, 100)
102927
+ })),
102928
+ active_tools_count: activeToolsList.length,
102929
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
102930
+ remaining_requests: remainingRequests,
102931
+ remaining_budget_ms: remainingBudgetMs,
102932
+ history_length: this.history.length
102933
+ });
102934
+ }
102935
+ const observerResult = await (0, import_ai6.generateText)({
102936
+ model: modelInstance,
102937
+ messages: [{ role: "user", content: observerPrompt }],
102938
+ maxTokens: 500
102939
+ });
102940
+ const responseText = observerResult.text.trim();
102941
+ if (this.tracer) {
102942
+ this.tracer.addEvent("negotiated_timeout.observer_response", {
102943
+ response_text: responseText,
102944
+ usage_prompt_tokens: observerResult.usage?.promptTokens,
102945
+ usage_completion_tokens: observerResult.usage?.completionTokens
102946
+ });
102947
+ }
102948
+ const jsonStr = responseText.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
102949
+ const decision = JSON.parse(jsonStr);
102950
+ if (decision.extend && decision.minutes > 0) {
102951
+ const requestedMs = Math.min(decision.minutes, maxPerReqMin) * 6e4;
102952
+ const grantedMs = Math.min(requestedMs, remainingBudgetMs, negotiatedTimeoutState.maxPerRequestMs);
102953
+ const grantedMin = Math.round(grantedMs / 6e4 * 10) / 10;
102954
+ negotiatedTimeoutState.extensionsUsed++;
102955
+ negotiatedTimeoutState.totalExtraTimeMs += grantedMs;
102956
+ 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.`;
102957
+ negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
102958
+ runTimeoutObserver();
102959
+ }, grantedMs);
102960
+ if (this.debug) {
102961
+ console.log(`[DEBUG] Timeout observer: granted ${grantedMin} min (reason: ${decision.reason}). Extensions: ${negotiatedTimeoutState.extensionsUsed}/${negotiatedTimeoutState.maxRequests}`);
102962
+ }
102963
+ if (this.tracer) {
102964
+ this.tracer.addEvent("negotiated_timeout.observer_extended", {
102965
+ decision_reason: decision.reason,
102966
+ requested_minutes: decision.minutes,
102967
+ granted_ms: grantedMs,
102968
+ granted_min: grantedMin,
102969
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
102970
+ max_requests: negotiatedTimeoutState.maxRequests,
102971
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
102972
+ budget_remaining_ms: remainingBudgetMs - grantedMs,
102973
+ active_tools: activeToolsList.map((t) => t.name),
102974
+ active_tools_count: activeToolsList.length
102975
+ });
102976
+ }
102977
+ this.events.emit("timeout.extended", {
102978
+ grantedMs,
102979
+ reason: decision.reason || "work in progress",
102980
+ extensionsUsed: negotiatedTimeoutState.extensionsUsed,
102981
+ extensionsRemaining: negotiatedTimeoutState.maxRequests - negotiatedTimeoutState.extensionsUsed,
102982
+ totalExtraTimeMs: negotiatedTimeoutState.totalExtraTimeMs,
102983
+ budgetRemainingMs: remainingBudgetMs - grantedMs
102984
+ });
102985
+ } else {
102986
+ if (this.debug) {
102987
+ console.log(`[DEBUG] Timeout observer: declined extension (reason: ${decision.reason}). Initiating graceful stop.`);
102988
+ }
102989
+ if (this.tracer) {
102990
+ this.tracer.addEvent("negotiated_timeout.observer_declined", {
102991
+ decision_reason: decision.reason,
102992
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
102993
+ total_extra_time_ms: negotiatedTimeoutState.totalExtraTimeMs,
102994
+ elapsed_min: elapsedMin,
102995
+ active_tools: activeToolsList.map((t) => t.name)
102996
+ });
102997
+ }
102998
+ this.events.emit("timeout.windingDown", {
102999
+ reason: decision.reason || "observer declined",
103000
+ extensionsUsed: negotiatedTimeoutState.extensionsUsed,
103001
+ totalExtraTimeMs: negotiatedTimeoutState.totalExtraTimeMs
103002
+ });
103003
+ await this._initiateGracefulStop(gracefulTimeoutState, `observer declined: ${decision.reason}`);
103004
+ }
103005
+ };
103006
+ try {
103007
+ if (this.tracer) {
103008
+ await this.tracer.withSpan("negotiated_timeout.observer", observerFn, {
103009
+ "timeout.elapsed_min": elapsedMin,
103010
+ "timeout.extensions_used": negotiatedTimeoutState.extensionsUsed,
103011
+ "timeout.active_tools_count": activeToolsList.length,
103012
+ "timeout.remaining_budget_ms": remainingBudgetMs
103013
+ });
103014
+ } else {
103015
+ await observerFn();
103016
+ }
103017
+ } catch (err) {
103018
+ if (this.debug) {
103019
+ console.log(`[DEBUG] Timeout observer: LLM call failed (${err.message}). Initiating graceful stop.`);
103020
+ }
103021
+ if (this.tracer) {
103022
+ this.tracer.addEvent("negotiated_timeout.observer_error", {
103023
+ error_message: err.message,
103024
+ error_name: err.name,
103025
+ extensions_used: negotiatedTimeoutState.extensionsUsed,
103026
+ elapsed_min: elapsedMin
103027
+ });
103028
+ }
103029
+ await this._initiateGracefulStop(gracefulTimeoutState, `observer error: ${err.message}`);
103030
+ } finally {
103031
+ negotiatedTimeoutState.observerRunning = false;
103032
+ }
103033
+ };
103034
+ negotiatedTimeoutState.runObserver = runTimeoutObserver;
102426
103035
  let compactionAttempted = false;
102427
103036
  while (true) {
102428
103037
  try {
@@ -102484,6 +103093,14 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102484
103093
  return false;
102485
103094
  },
102486
103095
  prepareStep: ({ steps, stepNumber }) => {
103096
+ if (negotiatedTimeoutState.extensionMessage && !gracefulTimeoutState.triggered) {
103097
+ const msg = negotiatedTimeoutState.extensionMessage;
103098
+ negotiatedTimeoutState.extensionMessage = null;
103099
+ if (this.debug) {
103100
+ console.log(`[DEBUG] prepareStep: delivering timeout observer extension message`);
103101
+ }
103102
+ return { userMessage: msg };
103103
+ }
102487
103104
  if (gracefulTimeoutState.triggered) {
102488
103105
  gracefulTimeoutState.bonusStepsUsed++;
102489
103106
  const remaining = gracefulTimeoutState.bonusStepsMax - gracefulTimeoutState.bonusStepsUsed;
@@ -102509,8 +103126,12 @@ You are working with a workspace. Available paths: ${workspaceDesc}
102509
103126
  return { toolChoice: "none" };
102510
103127
  }
102511
103128
  if (stepNumber === maxIterations - 1) {
103129
+ 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);
103130
+ const searchSummary = searchesTried.length > 0 ? `
103131
+ Searches attempted: ${searchesTried.join(", ")}` : "";
102512
103132
  return {
102513
- toolChoice: "none"
103133
+ toolChoice: "none",
103134
+ 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
103135
  };
102515
103136
  }
102516
103137
  if (steps.length >= 2) {
@@ -102589,6 +103210,11 @@ Double-check your response based on the criteria above. If everything looks good
102589
103210
  const { toolResults, toolCalls, text, reasoningText, finishReason, usage } = stepResult;
102590
103211
  currentIteration++;
102591
103212
  toolContext.currentIteration = currentIteration;
103213
+ if (toolCalls?.length > 0) {
103214
+ for (const tc of toolCalls) {
103215
+ _toolCallLog.push({ name: tc.toolName, args: tc.args || {} });
103216
+ }
103217
+ }
102592
103218
  if (this.tracer) {
102593
103219
  const stepEvent = {
102594
103220
  "iteration": currentIteration,
@@ -102680,6 +103306,14 @@ Double-check your response based on the criteria above. If everything looks good
102680
103306
  }, 6e4);
102681
103307
  }, this.maxOperationTimeout);
102682
103308
  }
103309
+ if (this.timeoutBehavior === "negotiated" && this.maxOperationTimeout > 0) {
103310
+ negotiatedTimeoutState.softTimeoutId = setTimeout(() => {
103311
+ if (this.debug) {
103312
+ console.log(`[DEBUG] Soft timeout after ${this.maxOperationTimeout}ms \u2014 invoking timeout observer`);
103313
+ }
103314
+ runTimeoutObserver();
103315
+ }, this.maxOperationTimeout);
103316
+ }
102683
103317
  try {
102684
103318
  const steps = await result.steps;
102685
103319
  let finalText;
@@ -102700,6 +103334,12 @@ Double-check your response based on the criteria above. If everything looks good
102700
103334
  } finally {
102701
103335
  if (gracefulTimeoutId) clearTimeout(gracefulTimeoutId);
102702
103336
  if (hardAbortTimeoutId) clearTimeout(hardAbortTimeoutId);
103337
+ if (negotiatedTimeoutState.softTimeoutId) clearTimeout(negotiatedTimeoutState.softTimeoutId);
103338
+ if (this._gracefulStopHardAbortId) {
103339
+ clearTimeout(this._gracefulStopHardAbortId);
103340
+ this._gracefulStopHardAbortId = null;
103341
+ }
103342
+ this.events.removeListener("toolCall", onToolCall);
102703
103343
  }
102704
103344
  };
102705
103345
  let aiResult;
@@ -102739,7 +103379,7 @@ Double-check your response based on the criteria above. If everything looks good
102739
103379
  }
102740
103380
  if (gracefulTimeoutState.triggered) {
102741
103381
  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.") {
103382
+ if (!finalResult || finalResult === DEFAULT_MAX_ITER_MSG || finalResult.startsWith("I was unable to complete your request after")) {
102743
103383
  try {
102744
103384
  const allText = await aiResult.result.text;
102745
103385
  if (allText && allText.trim()) {
@@ -102787,7 +103427,7 @@ ${toolSummaries.join("\n\n---\n\n")}`;
102787
103427
  currentMessages.push(msg);
102788
103428
  }
102789
103429
  }
102790
- if (this.completionPrompt && !options._completionPromptProcessed && !completionPromptInjected && finalResult) {
103430
+ if (this.completionPrompt && !options._completionPromptProcessed && !completionPromptInjected && !abortSummaryTaken && finalResult) {
102791
103431
  completionPromptInjected = true;
102792
103432
  preCompletionResult = finalResult;
102793
103433
  if (this.debug) {
@@ -102859,6 +103499,118 @@ Double-check your response based on the criteria above. If everything looks good
102859
103499
  }
102860
103500
  break;
102861
103501
  } catch (error40) {
103502
+ if (gracefulTimeoutState.triggered && error40?.name === "AbortError") {
103503
+ if (this.debug) {
103504
+ console.log(`[DEBUG] Negotiated timeout: abort caught \u2014 making summary LLM call with conversation context`);
103505
+ }
103506
+ if (this.tracer) {
103507
+ this.tracer.addEvent("negotiated_timeout.abort_summary_started", {
103508
+ conversation_messages: currentMessages.length,
103509
+ has_schema: !!options.schema,
103510
+ has_tasks: !!(this.enableTasks && this.taskManager)
103511
+ });
103512
+ }
103513
+ try {
103514
+ let taskContext = "";
103515
+ if (this.enableTasks && this.taskManager) {
103516
+ const taskSummary = this.taskManager.getTaskSummary?.();
103517
+ if (taskSummary) {
103518
+ taskContext = `
103519
+
103520
+ ## Task Status
103521
+ ${taskSummary}
103522
+
103523
+ Acknowledge which tasks were completed and which were not.`;
103524
+ }
103525
+ }
103526
+ let schemaContext = "";
103527
+ if (options.schema) {
103528
+ try {
103529
+ const parsedSchema = typeof options.schema === "string" ? JSON.parse(options.schema) : options.schema;
103530
+ schemaContext = `
103531
+
103532
+ IMPORTANT: Your response MUST be valid JSON matching this schema:
103533
+ ${JSON.stringify(parsedSchema, null, 2)}
103534
+
103535
+ 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.`;
103536
+ } catch {
103537
+ }
103538
+ }
103539
+ 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.
103540
+
103541
+ Please provide a DETAILED summary of:
103542
+ 1. What you were asked to do (the original task)
103543
+ 2. What you accomplished \u2014 include ALL findings, code snippets, data, and conclusions you gathered
103544
+ 3. What was still in progress or not yet started
103545
+ 4. Any partial results or recommendations you can offer based on what you found so far${taskContext}${schemaContext}
103546
+
103547
+ Be thorough \u2014 this is the user's only response. Include all useful information you collected.`;
103548
+ const summaryMessages = [
103549
+ ...currentMessages,
103550
+ { role: "user", content: summaryPrompt }
103551
+ ];
103552
+ const modelInstance = this.provider ? this.provider(this.model) : this.model;
103553
+ const summaryFn = async () => {
103554
+ const summaryResult = await (0, import_ai6.generateText)({
103555
+ model: modelInstance,
103556
+ messages: this.prepareMessagesWithImages(summaryMessages),
103557
+ maxTokens: 4e3
103558
+ });
103559
+ if (this.tracer) {
103560
+ this.tracer.addEvent("negotiated_timeout.abort_summary_completed", {
103561
+ summary_length: summaryResult.text?.length || 0,
103562
+ usage_prompt_tokens: summaryResult.usage?.promptTokens,
103563
+ usage_completion_tokens: summaryResult.usage?.completionTokens
103564
+ });
103565
+ }
103566
+ if (summaryResult.usage) {
103567
+ this.tokenCounter.recordUsage(summaryResult.usage);
103568
+ }
103569
+ return summaryResult.text;
103570
+ };
103571
+ let summaryText;
103572
+ if (this.tracer) {
103573
+ summaryText = await this.tracer.withSpan("negotiated_timeout.abort_summary", summaryFn, {
103574
+ "summary.conversation_messages": currentMessages.length
103575
+ });
103576
+ } else {
103577
+ summaryText = await summaryFn();
103578
+ }
103579
+ if (options.schema) {
103580
+ finalResult = summaryText || "{}";
103581
+ } else {
103582
+ 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";
103583
+ finalResult = timeoutNotice + (summaryText || "The operation was interrupted before a response could be generated.");
103584
+ }
103585
+ if (options.onStream && finalResult) {
103586
+ options.onStream(finalResult);
103587
+ }
103588
+ if (this.debug) {
103589
+ console.log(`[DEBUG] Negotiated timeout: summary produced ${summaryText?.length || 0} chars`);
103590
+ }
103591
+ } catch (summaryErr) {
103592
+ if (this.debug) {
103593
+ console.log(`[DEBUG] Negotiated timeout: summary call failed (${summaryErr.message}), falling back to partial text`);
103594
+ }
103595
+ if (this.tracer) {
103596
+ this.tracer.addEvent("negotiated_timeout.abort_summary_error", {
103597
+ error_message: summaryErr.message
103598
+ });
103599
+ }
103600
+ const partialTexts = currentMessages.filter((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim()).map((m) => m.content);
103601
+ if (options.schema) {
103602
+ finalResult = partialTexts.length > 0 ? partialTexts[partialTexts.length - 1] : "{}";
103603
+ } else {
103604
+ const timeoutNotice = "**Note: This response was generated under a time constraint. The operation was interrupted and some work was not completed.**\n\n";
103605
+ 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.";
103606
+ }
103607
+ if (options.onStream && finalResult) {
103608
+ options.onStream(finalResult);
103609
+ }
103610
+ }
103611
+ abortSummaryTaken = true;
103612
+ break;
103613
+ }
102862
103614
  if (!compactionAttempted && handleContextLimitError) {
102863
103615
  const compactionResult = handleContextLimitError(error40, currentMessages, {
102864
103616
  keepLastSegment: true,
@@ -102893,6 +103645,36 @@ Double-check your response based on the criteria above. If everything looks good
102893
103645
  }
102894
103646
  if (currentIteration >= maxIterations) {
102895
103647
  console.warn(`[WARN] Max tool iterations (${maxIterations}) reached for session ${this.sessionId}.`);
103648
+ if (!finalResult || finalResult === DEFAULT_MAX_ITER_MSG) {
103649
+ try {
103650
+ const searchQueries = [];
103651
+ const toolCounts = {};
103652
+ for (const tc of _toolCallLog) {
103653
+ toolCounts[tc.name] = (toolCounts[tc.name] || 0) + 1;
103654
+ if (tc.name === "search") {
103655
+ const q = tc.args.query || "";
103656
+ const exact = tc.args.exact ? " (exact)" : "";
103657
+ searchQueries.push(`"${q}"${exact}`);
103658
+ }
103659
+ }
103660
+ const toolBreakdown = Object.entries(toolCounts).map(([name15, count]) => `${name15}: ${count}x`).join(", ");
103661
+ const uniqueSearches = [...new Set(searchQueries)];
103662
+ let summary = `I was unable to complete your request after ${currentIteration} tool iterations.
103663
+
103664
+ `;
103665
+ summary += `Tool calls made: ${toolBreakdown || "none"}
103666
+ `;
103667
+ if (uniqueSearches.length > 0) {
103668
+ summary += `Search queries tried: ${uniqueSearches.join(", ")}
103669
+ `;
103670
+ }
103671
+ summary += `
103672
+ 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.`;
103673
+ finalResult = summary;
103674
+ } catch {
103675
+ finalResult = DEFAULT_MAX_ITER_MSG;
103676
+ }
103677
+ }
102896
103678
  }
102897
103679
  this.history = currentMessages.map((msg) => ({ ...msg }));
102898
103680
  if (this.history.length > MAX_HISTORY_MESSAGES) {
@@ -103427,6 +104209,134 @@ Double-check your response based on the criteria above. If everything looks good
103427
104209
  console.log(`[DEBUG] Agent cancelled for session ${this.sessionId}`);
103428
104210
  }
103429
104211
  }
104212
+ /**
104213
+ * Trigger graceful wind-down from outside (e.g., parent agent).
104214
+ * Unlike cancel(), this does NOT abort — it sets the graceful timeout flag
104215
+ * so the agent finishes its current step and then winds down naturally.
104216
+ */
104217
+ triggerGracefulWindDown() {
104218
+ if (this._gracefulTimeoutState && !this._gracefulTimeoutState.triggered) {
104219
+ this._gracefulTimeoutState.triggered = true;
104220
+ if (this.debug) {
104221
+ console.log(`[DEBUG] Graceful wind-down triggered externally for session ${this.sessionId}`);
104222
+ }
104223
+ if (this.tracer) {
104224
+ this.tracer.addEvent("graceful_stop.external_trigger", {
104225
+ "session.id": this.sessionId
104226
+ });
104227
+ }
104228
+ } else if (this.debug) {
104229
+ console.log(`[DEBUG] Graceful wind-down already active for session ${this.sessionId}, skipping`);
104230
+ }
104231
+ }
104232
+ /**
104233
+ * Initiate two-phase graceful stop: signal subagents and MCP servers to wind down,
104234
+ * then hard-abort after a deadline if they haven't finished.
104235
+ * @param {Object} gracefulTimeoutState - The graceful timeout state object from run()
104236
+ * @param {string} reason - Why the graceful stop was initiated
104237
+ */
104238
+ async _initiateGracefulStop(gracefulTimeoutState, reason) {
104239
+ if (gracefulTimeoutState.triggered) return;
104240
+ if (this.debug) {
104241
+ console.log(`[DEBUG] Initiating graceful stop: ${reason} (subagents: ${this._activeSubagents.size}, hasMcpBridge: ${!!this.mcpBridge}, deadline: ${this.gracefulStopDeadline}ms)`);
104242
+ }
104243
+ gracefulTimeoutState.triggered = true;
104244
+ if (this.tracer) {
104245
+ this.tracer.addEvent("graceful_stop.initiated", {
104246
+ "session.id": this.sessionId,
104247
+ "graceful_stop.reason": reason,
104248
+ "graceful_stop.active_subagents": this._activeSubagents.size,
104249
+ "graceful_stop.has_mcp_bridge": !!this.mcpBridge,
104250
+ "graceful_stop.deadline_ms": this.gracefulStopDeadline
104251
+ });
104252
+ }
104253
+ let subagentsSignalled = 0;
104254
+ let subagentErrors = 0;
104255
+ for (const [sid, subagent] of this._activeSubagents) {
104256
+ try {
104257
+ subagent.triggerGracefulWindDown();
104258
+ subagentsSignalled++;
104259
+ if (this.debug) {
104260
+ console.log(`[DEBUG] Triggered graceful wind-down on subagent ${sid}`);
104261
+ }
104262
+ } catch (e) {
104263
+ subagentErrors++;
104264
+ if (this.debug) {
104265
+ console.log(`[DEBUG] Failed to trigger wind-down on subagent ${sid}: ${e.message}`);
104266
+ }
104267
+ }
104268
+ }
104269
+ let mcpResults = [];
104270
+ if (this.mcpBridge) {
104271
+ try {
104272
+ mcpResults = await this.mcpBridge.callGracefulStopAll();
104273
+ if (this.debug && mcpResults.length > 0) {
104274
+ console.log(`[DEBUG] MCP graceful_stop results: ${JSON.stringify(mcpResults)}`);
104275
+ }
104276
+ } catch (e) {
104277
+ if (this.debug) {
104278
+ console.log(`[DEBUG] MCP graceful_stop failed: ${e.message}`);
104279
+ }
104280
+ }
104281
+ }
104282
+ if (this.tracer) {
104283
+ this.tracer.addEvent("graceful_stop.signals_sent", {
104284
+ "session.id": this.sessionId,
104285
+ "graceful_stop.subagents_signalled": subagentsSignalled,
104286
+ "graceful_stop.subagent_errors": subagentErrors,
104287
+ "graceful_stop.mcp_servers_called": mcpResults.filter((r) => r.success).length,
104288
+ "graceful_stop.mcp_servers_failed": mcpResults.filter((r) => !r.success).length,
104289
+ "graceful_stop.mcp_servers_total": mcpResults.length
104290
+ });
104291
+ }
104292
+ this._gracefulStopHardAbortId = setTimeout(() => {
104293
+ if (this.debug) {
104294
+ console.log(`[DEBUG] Graceful stop deadline (${this.gracefulStopDeadline}ms) expired \u2014 hard aborting`);
104295
+ }
104296
+ if (this.tracer) {
104297
+ this.tracer.addEvent("graceful_stop.deadline_expired", {
104298
+ "session.id": this.sessionId,
104299
+ "graceful_stop.deadline_ms": this.gracefulStopDeadline
104300
+ });
104301
+ }
104302
+ if (this._abortController) this._abortController.abort();
104303
+ }, this.gracefulStopDeadline);
104304
+ }
104305
+ /**
104306
+ * Register an active subagent for graceful stop coordination.
104307
+ * @param {string} sessionId
104308
+ * @param {ProbeAgent} subagent
104309
+ */
104310
+ _registerSubagent(sessionId, subagent) {
104311
+ this._activeSubagents.set(sessionId, subagent);
104312
+ if (this.debug) {
104313
+ console.log(`[DEBUG] Registered subagent ${sessionId} (active: ${this._activeSubagents.size})`);
104314
+ }
104315
+ if (this.tracer) {
104316
+ this.tracer.addEvent("subagent.registered", {
104317
+ "session.id": this.sessionId,
104318
+ "subagent.session_id": sessionId,
104319
+ "subagent.active_count": this._activeSubagents.size
104320
+ });
104321
+ }
104322
+ }
104323
+ /**
104324
+ * Unregister a completed subagent.
104325
+ * @param {string} sessionId
104326
+ */
104327
+ _unregisterSubagent(sessionId) {
104328
+ this._activeSubagents.delete(sessionId);
104329
+ if (this.debug) {
104330
+ console.log(`[DEBUG] Unregistered subagent ${sessionId} (active: ${this._activeSubagents.size})`);
104331
+ }
104332
+ if (this.tracer) {
104333
+ this.tracer.addEvent("subagent.unregistered", {
104334
+ "session.id": this.sessionId,
104335
+ "subagent.session_id": sessionId,
104336
+ "subagent.active_count": this._activeSubagents.size
104337
+ });
104338
+ }
104339
+ }
103430
104340
  /**
103431
104341
  * Get the abort signal for this agent.
103432
104342
  * Delegations and subagents should check this signal.