@probelabs/probe 0.6.0-rc206 → 0.6.0-rc207

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.
@@ -91,6 +91,45 @@ const MAX_TOOL_ITERATIONS = (() => {
91
91
  })();
92
92
  const MAX_HISTORY_MESSAGES = 100;
93
93
 
94
+ /**
95
+ * Extract tool name from wrapped_tool:toolName format
96
+ * @param {string} wrappedToolError - Error string in format 'wrapped_tool:toolName'
97
+ * @returns {string} The extracted tool name or 'unknown' if format is invalid
98
+ */
99
+ function extractWrappedToolName(wrappedToolError) {
100
+ if (!wrappedToolError || typeof wrappedToolError !== 'string') {
101
+ return 'unknown';
102
+ }
103
+ const colonIndex = wrappedToolError.indexOf(':');
104
+ return colonIndex !== -1 ? wrappedToolError.slice(colonIndex + 1) : 'unknown';
105
+ }
106
+
107
+ /**
108
+ * Check if an error indicates a wrapped tool format error
109
+ * @param {string|null} error - Error from detectUnrecognizedToolCall
110
+ * @returns {boolean} True if it's a wrapped tool error
111
+ */
112
+ function isWrappedToolError(error) {
113
+ return error && typeof error === 'string' && error.startsWith('wrapped_tool:');
114
+ }
115
+
116
+ /**
117
+ * Create error message for wrapped tool format issues
118
+ * @param {string} wrappedToolName - The tool name that was incorrectly wrapped
119
+ * @returns {string} User-friendly error message with correct format instructions
120
+ */
121
+ function createWrappedToolErrorMessage(wrappedToolName) {
122
+ return `Your response contained an incorrectly formatted tool call (${wrappedToolName} wrapped in XML tags). This cannot be used.
123
+
124
+ Please use the CORRECT format:
125
+
126
+ <${wrappedToolName}>
127
+ Your content here
128
+ </${wrappedToolName}>
129
+
130
+ Do NOT wrap in other tags like <api_call>, <tool_name>, <function>, etc.`;
131
+ }
132
+
94
133
  // Supported image file extensions (imported from shared config)
95
134
 
96
135
  // Maximum image file size (20MB) to prevent OOM attacks
@@ -2542,6 +2581,11 @@ Follow these instructions carefully:
2542
2581
  }
2543
2582
  }
2544
2583
 
2584
+ // Circuit breaker for repeated format errors
2585
+ let lastFormatErrorType = null;
2586
+ let sameFormatErrorCount = 0;
2587
+ const MAX_REPEATED_FORMAT_ERRORS = 3;
2588
+
2545
2589
  // Tool iteration loop (only for non-CLI engines like Vercel/Anthropic/OpenAI)
2546
2590
  while (currentIteration < maxIterations && !completionAttempted) {
2547
2591
  currentIteration++;
@@ -2835,7 +2879,28 @@ Follow these instructions carefully:
2835
2879
  );
2836
2880
 
2837
2881
  if (lastAssistantMessage) {
2838
- finalResult = lastAssistantMessage.content;
2882
+ const prevContent = lastAssistantMessage.content;
2883
+
2884
+ // Check for patterns indicating a failed/wrapped tool call attempt
2885
+ // Use detectUnrecognizedToolCall for consistent detection logic
2886
+ const wrappedToolError = detectUnrecognizedToolCall(prevContent, validTools);
2887
+
2888
+ if (isWrappedToolError(wrappedToolError)) {
2889
+ // Previous response was a broken tool call attempt - don't reuse it
2890
+ const wrappedToolName = extractWrappedToolName(wrappedToolError);
2891
+ if (this.debug) {
2892
+ console.log(`[DEBUG] Previous response contains wrapped tool '${wrappedToolName}' - rejecting for __PREVIOUS_RESPONSE__`);
2893
+ }
2894
+ currentMessages.push({ role: 'assistant', content: assistantResponseContent });
2895
+ currentMessages.push({
2896
+ role: 'user',
2897
+ content: createWrappedToolErrorMessage(wrappedToolName)
2898
+ });
2899
+ completionAttempted = false;
2900
+ continue; // Don't use broken response, continue the loop
2901
+ }
2902
+
2903
+ finalResult = prevContent;
2839
2904
  if (this.debug) console.log(`[DEBUG] Using previous response as completion: ${finalResult.substring(0, 100)}...`);
2840
2905
  } else {
2841
2906
  finalResult = 'Error: No previous response found to use as completion.';
@@ -3165,7 +3230,32 @@ Follow these instructions carefully:
3165
3230
  const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
3166
3231
 
3167
3232
  let reminderContent;
3168
- if (unrecognizedTool) {
3233
+ if (isWrappedToolError(unrecognizedTool)) {
3234
+ // AI wrapped a valid tool name in arbitrary XML tags - provide clear format error
3235
+ const wrappedToolName = extractWrappedToolName(unrecognizedTool);
3236
+ if (this.debug) {
3237
+ console.log(`[DEBUG] Detected wrapped tool '${wrappedToolName}' in assistant response - wrong XML format.`);
3238
+ }
3239
+ const toolError = new ParameterError(
3240
+ `Tool '${wrappedToolName}' found but in WRONG FORMAT - do not wrap tools in other XML tags.`,
3241
+ {
3242
+ suggestion: `Use the tool tag DIRECTLY without any wrapper:
3243
+
3244
+ CORRECT FORMAT:
3245
+ <${wrappedToolName}>
3246
+ <param>value</param>
3247
+ </${wrappedToolName}>
3248
+
3249
+ WRONG (what you did - do not wrap in other tags):
3250
+ <api_call><tool_name>${wrappedToolName}</tool_name>...</api_call>
3251
+ <function>${wrappedToolName}</function>
3252
+ <call name="${wrappedToolName}">...</call>
3253
+
3254
+ Remove ALL wrapper tags and use <${wrappedToolName}> directly as the outermost tag.`
3255
+ }
3256
+ );
3257
+ reminderContent = `<tool_result>\n${formatErrorForAI(toolError)}\n</tool_result>`;
3258
+ } else if (unrecognizedTool) {
3169
3259
  // AI tried to use a tool that's not available - provide clear error
3170
3260
  if (this.debug) {
3171
3261
  console.log(`[DEBUG] Detected unrecognized tool '${unrecognizedTool}' in assistant response.`);
@@ -3175,6 +3265,33 @@ Follow these instructions carefully:
3175
3265
  });
3176
3266
  reminderContent = `<tool_result>\n${formatErrorForAI(toolError)}\n</tool_result>`;
3177
3267
  } else {
3268
+ // No tool call detected at all - check if this is the last iteration
3269
+ // On the last iteration, if the AI gave a substantive response without using
3270
+ // attempt_completion, accept it as the final answer rather than losing the content
3271
+ if (currentIteration >= maxIterations) {
3272
+ // Clean up the response - remove thinking tags
3273
+ let cleanedResponse = assistantResponseContent;
3274
+ // Remove <thinking>...</thinking> blocks
3275
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
3276
+ // Also remove unclosed thinking tags
3277
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, '').trim();
3278
+
3279
+ // Only use if there's substantial content (not just a failed tool call attempt)
3280
+ const hasSubstantialContent = cleanedResponse.length > 50 &&
3281
+ !cleanedResponse.includes('<api_call>') &&
3282
+ !cleanedResponse.includes('<tool_name>') &&
3283
+ !cleanedResponse.includes('<function>');
3284
+
3285
+ if (hasSubstantialContent) {
3286
+ if (this.debug) {
3287
+ console.log(`[DEBUG] Max iterations reached - accepting AI response as final answer (${cleanedResponse.length} chars)`);
3288
+ }
3289
+ finalResult = cleanedResponse;
3290
+ completionAttempted = true;
3291
+ break;
3292
+ }
3293
+ }
3294
+
3178
3295
  // Standard reminder - no tool call detected at all
3179
3296
  reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
3180
3297
 
@@ -3206,6 +3323,31 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
3206
3323
  console.log(`[DEBUG] No tool call detected in assistant response. Prompting for tool use.`);
3207
3324
  }
3208
3325
  }
3326
+
3327
+ // Circuit breaker: track repeated format errors and break early
3328
+ // For wrapped_tool errors, track them as a category (any wrapped_tool counts)
3329
+ // For other errors, track the exact error type
3330
+ if (unrecognizedTool) {
3331
+ const isWrapped = isWrappedToolError(unrecognizedTool);
3332
+ const errorCategory = isWrapped ? 'wrapped_tool' : unrecognizedTool;
3333
+
3334
+ if (errorCategory === lastFormatErrorType) {
3335
+ sameFormatErrorCount++;
3336
+ if (sameFormatErrorCount >= MAX_REPEATED_FORMAT_ERRORS) {
3337
+ const errorDesc = isWrapped ? 'wrapped tool format' : unrecognizedTool;
3338
+ console.error(`[ERROR] Format error category '${errorCategory}' repeated ${sameFormatErrorCount} times. Breaking loop early to prevent infinite iteration.`);
3339
+ finalResult = `Error: Unable to complete request. The AI model repeatedly used incorrect tool call format (${errorDesc}). Please try rephrasing your question or using a different model.`;
3340
+ break;
3341
+ }
3342
+ } else {
3343
+ lastFormatErrorType = errorCategory;
3344
+ sameFormatErrorCount = 1;
3345
+ }
3346
+ } else {
3347
+ // Reset counter if it's a different kind of "no tool call" situation
3348
+ lastFormatErrorType = null;
3349
+ sameFormatErrorCount = 0;
3350
+ }
3209
3351
  }
3210
3352
 
3211
3353
  // Keep message history manageable
@@ -9059,6 +9059,22 @@ function detectUnrecognizedToolCall(xmlString, validTools) {
9059
9059
  return toolName;
9060
9060
  }
9061
9061
  }
9062
+ const allToolNames = [.../* @__PURE__ */ new Set([...knownToolNames, ...validTools])];
9063
+ for (const toolName of allToolNames) {
9064
+ const escapedToolName = toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9065
+ const wrapperPatterns = [
9066
+ new RegExp(`<tool_name>\\s*${escapedToolName}\\s*</tool_name>`, "i"),
9067
+ new RegExp(`<function>\\s*${escapedToolName}\\s*</function>`, "i"),
9068
+ new RegExp(`<name>\\s*${escapedToolName}\\s*</name>`, "i"),
9069
+ // Also check for tool name immediately after api_call or call opening tag
9070
+ new RegExp(`<(?:api_call|call)[^>]*>[\\s\\S]*?<tool_name>\\s*${escapedToolName}`, "i")
9071
+ ];
9072
+ for (const pattern of wrapperPatterns) {
9073
+ if (pattern.test(xmlString)) {
9074
+ return `wrapped_tool:${toolName}`;
9075
+ }
9076
+ }
9077
+ }
9062
9078
  return null;
9063
9079
  }
9064
9080
  function parseTargets(targets) {
@@ -68676,6 +68692,27 @@ import { EventEmitter as EventEmitter5 } from "events";
68676
68692
  import { existsSync as existsSync6 } from "fs";
68677
68693
  import { readFile as readFile3, stat, readdir as readdir3 } from "fs/promises";
68678
68694
  import { resolve as resolve6, isAbsolute as isAbsolute5, dirname as dirname5, basename, normalize as normalize2, sep as sep5 } from "path";
68695
+ function extractWrappedToolName(wrappedToolError) {
68696
+ if (!wrappedToolError || typeof wrappedToolError !== "string") {
68697
+ return "unknown";
68698
+ }
68699
+ const colonIndex = wrappedToolError.indexOf(":");
68700
+ return colonIndex !== -1 ? wrappedToolError.slice(colonIndex + 1) : "unknown";
68701
+ }
68702
+ function isWrappedToolError(error) {
68703
+ return error && typeof error === "string" && error.startsWith("wrapped_tool:");
68704
+ }
68705
+ function createWrappedToolErrorMessage(wrappedToolName) {
68706
+ return `Your response contained an incorrectly formatted tool call (${wrappedToolName} wrapped in XML tags). This cannot be used.
68707
+
68708
+ Please use the CORRECT format:
68709
+
68710
+ <${wrappedToolName}>
68711
+ Your content here
68712
+ </${wrappedToolName}>
68713
+
68714
+ Do NOT wrap in other tags like <api_call>, <tool_name>, <function>, etc.`;
68715
+ }
68679
68716
  var MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, MAX_IMAGE_FILE_SIZE, ProbeAgent;
68680
68717
  var init_ProbeAgent = __esm({
68681
68718
  "src/agent/ProbeAgent.js"() {
@@ -70675,6 +70712,9 @@ You are working with a repository located at: ${searchDirectory}
70675
70712
  console.log(`[DEBUG] Schema provided, using extended iteration limit: ${maxIterations} (base: ${baseMaxIterations})`);
70676
70713
  }
70677
70714
  }
70715
+ let lastFormatErrorType = null;
70716
+ let sameFormatErrorCount = 0;
70717
+ const MAX_REPEATED_FORMAT_ERRORS = 3;
70678
70718
  while (currentIteration < maxIterations && !completionAttempted) {
70679
70719
  currentIteration++;
70680
70720
  if (this.cancelled) throw new Error("Request was cancelled by the user");
@@ -70873,7 +70913,22 @@ You are working with a repository located at: ${searchDirectory}
70873
70913
  (msg) => msg.role === "assistant" && msg.content && !(this.mcpBridge ? parseHybridXmlToolCall(msg.content, validTools, this.mcpBridge) : parseXmlToolCallWithThinking(msg.content, validTools))
70874
70914
  );
70875
70915
  if (lastAssistantMessage) {
70876
- finalResult = lastAssistantMessage.content;
70916
+ const prevContent = lastAssistantMessage.content;
70917
+ const wrappedToolError = detectUnrecognizedToolCall(prevContent, validTools);
70918
+ if (isWrappedToolError(wrappedToolError)) {
70919
+ const wrappedToolName = extractWrappedToolName(wrappedToolError);
70920
+ if (this.debug) {
70921
+ console.log(`[DEBUG] Previous response contains wrapped tool '${wrappedToolName}' - rejecting for __PREVIOUS_RESPONSE__`);
70922
+ }
70923
+ currentMessages.push({ role: "assistant", content: assistantResponseContent });
70924
+ currentMessages.push({
70925
+ role: "user",
70926
+ content: createWrappedToolErrorMessage(wrappedToolName)
70927
+ });
70928
+ completionAttempted = false;
70929
+ continue;
70930
+ }
70931
+ finalResult = prevContent;
70877
70932
  if (this.debug) console.log(`[DEBUG] Using previous response as completion: ${finalResult.substring(0, 100)}...`);
70878
70933
  } else {
70879
70934
  finalResult = "Error: No previous response found to use as completion.";
@@ -71144,7 +71199,33 @@ ${errorXml}
71144
71199
  currentMessages.push({ role: "assistant", content: assistantResponseContent });
71145
71200
  const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
71146
71201
  let reminderContent;
71147
- if (unrecognizedTool) {
71202
+ if (isWrappedToolError(unrecognizedTool)) {
71203
+ const wrappedToolName = extractWrappedToolName(unrecognizedTool);
71204
+ if (this.debug) {
71205
+ console.log(`[DEBUG] Detected wrapped tool '${wrappedToolName}' in assistant response - wrong XML format.`);
71206
+ }
71207
+ const toolError = new ParameterError(
71208
+ `Tool '${wrappedToolName}' found but in WRONG FORMAT - do not wrap tools in other XML tags.`,
71209
+ {
71210
+ suggestion: `Use the tool tag DIRECTLY without any wrapper:
71211
+
71212
+ CORRECT FORMAT:
71213
+ <${wrappedToolName}>
71214
+ <param>value</param>
71215
+ </${wrappedToolName}>
71216
+
71217
+ WRONG (what you did - do not wrap in other tags):
71218
+ <api_call><tool_name>${wrappedToolName}</tool_name>...</api_call>
71219
+ <function>${wrappedToolName}</function>
71220
+ <call name="${wrappedToolName}">...</call>
71221
+
71222
+ Remove ALL wrapper tags and use <${wrappedToolName}> directly as the outermost tag.`
71223
+ }
71224
+ );
71225
+ reminderContent = `<tool_result>
71226
+ ${formatErrorForAI(toolError)}
71227
+ </tool_result>`;
71228
+ } else if (unrecognizedTool) {
71148
71229
  if (this.debug) {
71149
71230
  console.log(`[DEBUG] Detected unrecognized tool '${unrecognizedTool}' in assistant response.`);
71150
71231
  }
@@ -71155,6 +71236,20 @@ ${errorXml}
71155
71236
  ${formatErrorForAI(toolError)}
71156
71237
  </tool_result>`;
71157
71238
  } else {
71239
+ if (currentIteration >= maxIterations) {
71240
+ let cleanedResponse = assistantResponseContent;
71241
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "").trim();
71242
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, "").trim();
71243
+ const hasSubstantialContent = cleanedResponse.length > 50 && !cleanedResponse.includes("<api_call>") && !cleanedResponse.includes("<tool_name>") && !cleanedResponse.includes("<function>");
71244
+ if (hasSubstantialContent) {
71245
+ if (this.debug) {
71246
+ console.log(`[DEBUG] Max iterations reached - accepting AI response as final answer (${cleanedResponse.length} chars)`);
71247
+ }
71248
+ finalResult = cleanedResponse;
71249
+ completionAttempted = true;
71250
+ break;
71251
+ }
71252
+ }
71158
71253
  reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
71159
71254
 
71160
71255
  Remember: Use proper XML format with BOTH opening and closing tags:
@@ -71184,6 +71279,25 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
71184
71279
  console.log(`[DEBUG] No tool call detected in assistant response. Prompting for tool use.`);
71185
71280
  }
71186
71281
  }
71282
+ if (unrecognizedTool) {
71283
+ const isWrapped = isWrappedToolError(unrecognizedTool);
71284
+ const errorCategory = isWrapped ? "wrapped_tool" : unrecognizedTool;
71285
+ if (errorCategory === lastFormatErrorType) {
71286
+ sameFormatErrorCount++;
71287
+ if (sameFormatErrorCount >= MAX_REPEATED_FORMAT_ERRORS) {
71288
+ const errorDesc = isWrapped ? "wrapped tool format" : unrecognizedTool;
71289
+ console.error(`[ERROR] Format error category '${errorCategory}' repeated ${sameFormatErrorCount} times. Breaking loop early to prevent infinite iteration.`);
71290
+ finalResult = `Error: Unable to complete request. The AI model repeatedly used incorrect tool call format (${errorDesc}). Please try rephrasing your question or using a different model.`;
71291
+ break;
71292
+ }
71293
+ } else {
71294
+ lastFormatErrorType = errorCategory;
71295
+ sameFormatErrorCount = 1;
71296
+ }
71297
+ } else {
71298
+ lastFormatErrorType = null;
71299
+ sameFormatErrorCount = 0;
71300
+ }
71187
71301
  }
71188
71302
  if (currentMessages.length > MAX_HISTORY_MESSAGES) {
71189
71303
  const messagesBefore = currentMessages.length;
@@ -617,6 +617,37 @@ export function detectUnrecognizedToolCall(xmlString, validTools) {
617
617
  }
618
618
  }
619
619
 
620
+ // Check if any valid tool name appears inside specific wrapper patterns
621
+ // This catches cases where AI wraps tools in arbitrary tags like:
622
+ // <api_call><tool_name>attempt_completion</tool_name>...</api_call>
623
+ // <function>search</function>
624
+ // <call name="extract">...</call>
625
+ // Only match specific wrapper patterns to avoid false positives with normal text
626
+ const allToolNames = [...new Set([...knownToolNames, ...validTools])];
627
+ for (const toolName of allToolNames) {
628
+ // Escape regex metacharacters in tool name to prevent regex errors
629
+ const escapedToolName = toolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
630
+
631
+ // Match specific wrapper patterns that indicate a tool call attempt:
632
+ // 1. <tool_name>toolName</tool_name> - common Claude API-style wrapper
633
+ // 2. <function>toolName</function> - function call style
634
+ // 3. <name>toolName</name> - generic name wrapper
635
+ // 4. <call><name>toolName - partial wrapper patterns
636
+ const wrapperPatterns = [
637
+ new RegExp(`<tool_name>\\s*${escapedToolName}\\s*</tool_name>`, 'i'),
638
+ new RegExp(`<function>\\s*${escapedToolName}\\s*</function>`, 'i'),
639
+ new RegExp(`<name>\\s*${escapedToolName}\\s*</name>`, 'i'),
640
+ // Also check for tool name immediately after api_call or call opening tag
641
+ new RegExp(`<(?:api_call|call)[^>]*>[\\s\\S]*?<tool_name>\\s*${escapedToolName}`, 'i')
642
+ ];
643
+
644
+ for (const pattern of wrapperPatterns) {
645
+ if (pattern.test(xmlString)) {
646
+ return `wrapped_tool:${toolName}`;
647
+ }
648
+ }
649
+ }
650
+
620
651
  return null;
621
652
  }
622
653
 
@@ -36006,6 +36006,22 @@ function detectUnrecognizedToolCall(xmlString, validTools) {
36006
36006
  return toolName;
36007
36007
  }
36008
36008
  }
36009
+ const allToolNames = [.../* @__PURE__ */ new Set([...knownToolNames, ...validTools])];
36010
+ for (const toolName of allToolNames) {
36011
+ const escapedToolName = toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36012
+ const wrapperPatterns = [
36013
+ new RegExp(`<tool_name>\\s*${escapedToolName}\\s*</tool_name>`, "i"),
36014
+ new RegExp(`<function>\\s*${escapedToolName}\\s*</function>`, "i"),
36015
+ new RegExp(`<name>\\s*${escapedToolName}\\s*</name>`, "i"),
36016
+ // Also check for tool name immediately after api_call or call opening tag
36017
+ new RegExp(`<(?:api_call|call)[^>]*>[\\s\\S]*?<tool_name>\\s*${escapedToolName}`, "i")
36018
+ ];
36019
+ for (const pattern of wrapperPatterns) {
36020
+ if (pattern.test(xmlString)) {
36021
+ return `wrapped_tool:${toolName}`;
36022
+ }
36023
+ }
36024
+ }
36009
36025
  return null;
36010
36026
  }
36011
36027
  function parseTargets(targets) {
@@ -95379,6 +95395,27 @@ __export(ProbeAgent_exports, {
95379
95395
  ProbeAgent: () => ProbeAgent
95380
95396
  });
95381
95397
  module.exports = __toCommonJS(ProbeAgent_exports);
95398
+ function extractWrappedToolName(wrappedToolError) {
95399
+ if (!wrappedToolError || typeof wrappedToolError !== "string") {
95400
+ return "unknown";
95401
+ }
95402
+ const colonIndex = wrappedToolError.indexOf(":");
95403
+ return colonIndex !== -1 ? wrappedToolError.slice(colonIndex + 1) : "unknown";
95404
+ }
95405
+ function isWrappedToolError(error2) {
95406
+ return error2 && typeof error2 === "string" && error2.startsWith("wrapped_tool:");
95407
+ }
95408
+ function createWrappedToolErrorMessage(wrappedToolName) {
95409
+ return `Your response contained an incorrectly formatted tool call (${wrappedToolName} wrapped in XML tags). This cannot be used.
95410
+
95411
+ Please use the CORRECT format:
95412
+
95413
+ <${wrappedToolName}>
95414
+ Your content here
95415
+ </${wrappedToolName}>
95416
+
95417
+ Do NOT wrap in other tags like <api_call>, <tool_name>, <function>, etc.`;
95418
+ }
95382
95419
  var import_dotenv2, import_anthropic2, import_openai2, import_google2, import_ai5, import_crypto9, import_events4, import_fs14, import_promises6, import_path17, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, MAX_IMAGE_FILE_SIZE, ProbeAgent;
95383
95420
  var init_ProbeAgent = __esm({
95384
95421
  "src/agent/ProbeAgent.js"() {
@@ -97388,6 +97425,9 @@ You are working with a repository located at: ${searchDirectory}
97388
97425
  console.log(`[DEBUG] Schema provided, using extended iteration limit: ${maxIterations} (base: ${baseMaxIterations})`);
97389
97426
  }
97390
97427
  }
97428
+ let lastFormatErrorType = null;
97429
+ let sameFormatErrorCount = 0;
97430
+ const MAX_REPEATED_FORMAT_ERRORS = 3;
97391
97431
  while (currentIteration < maxIterations && !completionAttempted) {
97392
97432
  currentIteration++;
97393
97433
  if (this.cancelled) throw new Error("Request was cancelled by the user");
@@ -97586,7 +97626,22 @@ You are working with a repository located at: ${searchDirectory}
97586
97626
  (msg) => msg.role === "assistant" && msg.content && !(this.mcpBridge ? parseHybridXmlToolCall(msg.content, validTools, this.mcpBridge) : parseXmlToolCallWithThinking(msg.content, validTools))
97587
97627
  );
97588
97628
  if (lastAssistantMessage) {
97589
- finalResult = lastAssistantMessage.content;
97629
+ const prevContent = lastAssistantMessage.content;
97630
+ const wrappedToolError = detectUnrecognizedToolCall(prevContent, validTools);
97631
+ if (isWrappedToolError(wrappedToolError)) {
97632
+ const wrappedToolName = extractWrappedToolName(wrappedToolError);
97633
+ if (this.debug) {
97634
+ console.log(`[DEBUG] Previous response contains wrapped tool '${wrappedToolName}' - rejecting for __PREVIOUS_RESPONSE__`);
97635
+ }
97636
+ currentMessages.push({ role: "assistant", content: assistantResponseContent });
97637
+ currentMessages.push({
97638
+ role: "user",
97639
+ content: createWrappedToolErrorMessage(wrappedToolName)
97640
+ });
97641
+ completionAttempted = false;
97642
+ continue;
97643
+ }
97644
+ finalResult = prevContent;
97590
97645
  if (this.debug) console.log(`[DEBUG] Using previous response as completion: ${finalResult.substring(0, 100)}...`);
97591
97646
  } else {
97592
97647
  finalResult = "Error: No previous response found to use as completion.";
@@ -97857,7 +97912,33 @@ ${errorXml}
97857
97912
  currentMessages.push({ role: "assistant", content: assistantResponseContent });
97858
97913
  const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
97859
97914
  let reminderContent;
97860
- if (unrecognizedTool) {
97915
+ if (isWrappedToolError(unrecognizedTool)) {
97916
+ const wrappedToolName = extractWrappedToolName(unrecognizedTool);
97917
+ if (this.debug) {
97918
+ console.log(`[DEBUG] Detected wrapped tool '${wrappedToolName}' in assistant response - wrong XML format.`);
97919
+ }
97920
+ const toolError = new ParameterError(
97921
+ `Tool '${wrappedToolName}' found but in WRONG FORMAT - do not wrap tools in other XML tags.`,
97922
+ {
97923
+ suggestion: `Use the tool tag DIRECTLY without any wrapper:
97924
+
97925
+ CORRECT FORMAT:
97926
+ <${wrappedToolName}>
97927
+ <param>value</param>
97928
+ </${wrappedToolName}>
97929
+
97930
+ WRONG (what you did - do not wrap in other tags):
97931
+ <api_call><tool_name>${wrappedToolName}</tool_name>...</api_call>
97932
+ <function>${wrappedToolName}</function>
97933
+ <call name="${wrappedToolName}">...</call>
97934
+
97935
+ Remove ALL wrapper tags and use <${wrappedToolName}> directly as the outermost tag.`
97936
+ }
97937
+ );
97938
+ reminderContent = `<tool_result>
97939
+ ${formatErrorForAI(toolError)}
97940
+ </tool_result>`;
97941
+ } else if (unrecognizedTool) {
97861
97942
  if (this.debug) {
97862
97943
  console.log(`[DEBUG] Detected unrecognized tool '${unrecognizedTool}' in assistant response.`);
97863
97944
  }
@@ -97868,6 +97949,20 @@ ${errorXml}
97868
97949
  ${formatErrorForAI(toolError)}
97869
97950
  </tool_result>`;
97870
97951
  } else {
97952
+ if (currentIteration >= maxIterations) {
97953
+ let cleanedResponse = assistantResponseContent;
97954
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "").trim();
97955
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, "").trim();
97956
+ const hasSubstantialContent = cleanedResponse.length > 50 && !cleanedResponse.includes("<api_call>") && !cleanedResponse.includes("<tool_name>") && !cleanedResponse.includes("<function>");
97957
+ if (hasSubstantialContent) {
97958
+ if (this.debug) {
97959
+ console.log(`[DEBUG] Max iterations reached - accepting AI response as final answer (${cleanedResponse.length} chars)`);
97960
+ }
97961
+ finalResult = cleanedResponse;
97962
+ completionAttempted = true;
97963
+ break;
97964
+ }
97965
+ }
97871
97966
  reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
97872
97967
 
97873
97968
  Remember: Use proper XML format with BOTH opening and closing tags:
@@ -97897,6 +97992,25 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
97897
97992
  console.log(`[DEBUG] No tool call detected in assistant response. Prompting for tool use.`);
97898
97993
  }
97899
97994
  }
97995
+ if (unrecognizedTool) {
97996
+ const isWrapped = isWrappedToolError(unrecognizedTool);
97997
+ const errorCategory = isWrapped ? "wrapped_tool" : unrecognizedTool;
97998
+ if (errorCategory === lastFormatErrorType) {
97999
+ sameFormatErrorCount++;
98000
+ if (sameFormatErrorCount >= MAX_REPEATED_FORMAT_ERRORS) {
98001
+ const errorDesc = isWrapped ? "wrapped tool format" : unrecognizedTool;
98002
+ console.error(`[ERROR] Format error category '${errorCategory}' repeated ${sameFormatErrorCount} times. Breaking loop early to prevent infinite iteration.`);
98003
+ finalResult = `Error: Unable to complete request. The AI model repeatedly used incorrect tool call format (${errorDesc}). Please try rephrasing your question or using a different model.`;
98004
+ break;
98005
+ }
98006
+ } else {
98007
+ lastFormatErrorType = errorCategory;
98008
+ sameFormatErrorCount = 1;
98009
+ }
98010
+ } else {
98011
+ lastFormatErrorType = null;
98012
+ sameFormatErrorCount = 0;
98013
+ }
97900
98014
  }
97901
98015
  if (currentMessages.length > MAX_HISTORY_MESSAGES) {
97902
98016
  const messagesBefore = currentMessages.length;
package/cjs/index.cjs CHANGED
@@ -35787,6 +35787,22 @@ function detectUnrecognizedToolCall(xmlString, validTools) {
35787
35787
  return toolName;
35788
35788
  }
35789
35789
  }
35790
+ const allToolNames = [.../* @__PURE__ */ new Set([...knownToolNames, ...validTools])];
35791
+ for (const toolName of allToolNames) {
35792
+ const escapedToolName = toolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35793
+ const wrapperPatterns = [
35794
+ new RegExp(`<tool_name>\\s*${escapedToolName}\\s*</tool_name>`, "i"),
35795
+ new RegExp(`<function>\\s*${escapedToolName}\\s*</function>`, "i"),
35796
+ new RegExp(`<name>\\s*${escapedToolName}\\s*</name>`, "i"),
35797
+ // Also check for tool name immediately after api_call or call opening tag
35798
+ new RegExp(`<(?:api_call|call)[^>]*>[\\s\\S]*?<tool_name>\\s*${escapedToolName}`, "i")
35799
+ ];
35800
+ for (const pattern of wrapperPatterns) {
35801
+ if (pattern.test(xmlString)) {
35802
+ return `wrapped_tool:${toolName}`;
35803
+ }
35804
+ }
35805
+ }
35790
35806
  return null;
35791
35807
  }
35792
35808
  function parseTargets(targets) {
@@ -93061,6 +93077,27 @@ var ProbeAgent_exports = {};
93061
93077
  __export(ProbeAgent_exports, {
93062
93078
  ProbeAgent: () => ProbeAgent
93063
93079
  });
93080
+ function extractWrappedToolName(wrappedToolError) {
93081
+ if (!wrappedToolError || typeof wrappedToolError !== "string") {
93082
+ return "unknown";
93083
+ }
93084
+ const colonIndex = wrappedToolError.indexOf(":");
93085
+ return colonIndex !== -1 ? wrappedToolError.slice(colonIndex + 1) : "unknown";
93086
+ }
93087
+ function isWrappedToolError(error2) {
93088
+ return error2 && typeof error2 === "string" && error2.startsWith("wrapped_tool:");
93089
+ }
93090
+ function createWrappedToolErrorMessage(wrappedToolName) {
93091
+ return `Your response contained an incorrectly formatted tool call (${wrappedToolName} wrapped in XML tags). This cannot be used.
93092
+
93093
+ Please use the CORRECT format:
93094
+
93095
+ <${wrappedToolName}>
93096
+ Your content here
93097
+ </${wrappedToolName}>
93098
+
93099
+ Do NOT wrap in other tags like <api_call>, <tool_name>, <function>, etc.`;
93100
+ }
93064
93101
  var import_dotenv, import_anthropic2, import_openai2, import_google2, import_ai3, import_crypto8, import_events4, import_fs11, import_promises6, import_path13, MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, MAX_IMAGE_FILE_SIZE, ProbeAgent;
93065
93102
  var init_ProbeAgent = __esm({
93066
93103
  "src/agent/ProbeAgent.js"() {
@@ -95071,6 +95108,9 @@ You are working with a repository located at: ${searchDirectory}
95071
95108
  console.log(`[DEBUG] Schema provided, using extended iteration limit: ${maxIterations} (base: ${baseMaxIterations})`);
95072
95109
  }
95073
95110
  }
95111
+ let lastFormatErrorType = null;
95112
+ let sameFormatErrorCount = 0;
95113
+ const MAX_REPEATED_FORMAT_ERRORS = 3;
95074
95114
  while (currentIteration < maxIterations && !completionAttempted) {
95075
95115
  currentIteration++;
95076
95116
  if (this.cancelled) throw new Error("Request was cancelled by the user");
@@ -95269,7 +95309,22 @@ You are working with a repository located at: ${searchDirectory}
95269
95309
  (msg) => msg.role === "assistant" && msg.content && !(this.mcpBridge ? parseHybridXmlToolCall(msg.content, validTools, this.mcpBridge) : parseXmlToolCallWithThinking(msg.content, validTools))
95270
95310
  );
95271
95311
  if (lastAssistantMessage) {
95272
- finalResult = lastAssistantMessage.content;
95312
+ const prevContent = lastAssistantMessage.content;
95313
+ const wrappedToolError = detectUnrecognizedToolCall(prevContent, validTools);
95314
+ if (isWrappedToolError(wrappedToolError)) {
95315
+ const wrappedToolName = extractWrappedToolName(wrappedToolError);
95316
+ if (this.debug) {
95317
+ console.log(`[DEBUG] Previous response contains wrapped tool '${wrappedToolName}' - rejecting for __PREVIOUS_RESPONSE__`);
95318
+ }
95319
+ currentMessages.push({ role: "assistant", content: assistantResponseContent });
95320
+ currentMessages.push({
95321
+ role: "user",
95322
+ content: createWrappedToolErrorMessage(wrappedToolName)
95323
+ });
95324
+ completionAttempted = false;
95325
+ continue;
95326
+ }
95327
+ finalResult = prevContent;
95273
95328
  if (this.debug) console.log(`[DEBUG] Using previous response as completion: ${finalResult.substring(0, 100)}...`);
95274
95329
  } else {
95275
95330
  finalResult = "Error: No previous response found to use as completion.";
@@ -95540,7 +95595,33 @@ ${errorXml}
95540
95595
  currentMessages.push({ role: "assistant", content: assistantResponseContent });
95541
95596
  const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
95542
95597
  let reminderContent;
95543
- if (unrecognizedTool) {
95598
+ if (isWrappedToolError(unrecognizedTool)) {
95599
+ const wrappedToolName = extractWrappedToolName(unrecognizedTool);
95600
+ if (this.debug) {
95601
+ console.log(`[DEBUG] Detected wrapped tool '${wrappedToolName}' in assistant response - wrong XML format.`);
95602
+ }
95603
+ const toolError = new ParameterError(
95604
+ `Tool '${wrappedToolName}' found but in WRONG FORMAT - do not wrap tools in other XML tags.`,
95605
+ {
95606
+ suggestion: `Use the tool tag DIRECTLY without any wrapper:
95607
+
95608
+ CORRECT FORMAT:
95609
+ <${wrappedToolName}>
95610
+ <param>value</param>
95611
+ </${wrappedToolName}>
95612
+
95613
+ WRONG (what you did - do not wrap in other tags):
95614
+ <api_call><tool_name>${wrappedToolName}</tool_name>...</api_call>
95615
+ <function>${wrappedToolName}</function>
95616
+ <call name="${wrappedToolName}">...</call>
95617
+
95618
+ Remove ALL wrapper tags and use <${wrappedToolName}> directly as the outermost tag.`
95619
+ }
95620
+ );
95621
+ reminderContent = `<tool_result>
95622
+ ${formatErrorForAI(toolError)}
95623
+ </tool_result>`;
95624
+ } else if (unrecognizedTool) {
95544
95625
  if (this.debug) {
95545
95626
  console.log(`[DEBUG] Detected unrecognized tool '${unrecognizedTool}' in assistant response.`);
95546
95627
  }
@@ -95551,6 +95632,20 @@ ${errorXml}
95551
95632
  ${formatErrorForAI(toolError)}
95552
95633
  </tool_result>`;
95553
95634
  } else {
95635
+ if (currentIteration >= maxIterations) {
95636
+ let cleanedResponse = assistantResponseContent;
95637
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "").trim();
95638
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, "").trim();
95639
+ const hasSubstantialContent = cleanedResponse.length > 50 && !cleanedResponse.includes("<api_call>") && !cleanedResponse.includes("<tool_name>") && !cleanedResponse.includes("<function>");
95640
+ if (hasSubstantialContent) {
95641
+ if (this.debug) {
95642
+ console.log(`[DEBUG] Max iterations reached - accepting AI response as final answer (${cleanedResponse.length} chars)`);
95643
+ }
95644
+ finalResult = cleanedResponse;
95645
+ completionAttempted = true;
95646
+ break;
95647
+ }
95648
+ }
95554
95649
  reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
95555
95650
 
95556
95651
  Remember: Use proper XML format with BOTH opening and closing tags:
@@ -95580,6 +95675,25 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
95580
95675
  console.log(`[DEBUG] No tool call detected in assistant response. Prompting for tool use.`);
95581
95676
  }
95582
95677
  }
95678
+ if (unrecognizedTool) {
95679
+ const isWrapped = isWrappedToolError(unrecognizedTool);
95680
+ const errorCategory = isWrapped ? "wrapped_tool" : unrecognizedTool;
95681
+ if (errorCategory === lastFormatErrorType) {
95682
+ sameFormatErrorCount++;
95683
+ if (sameFormatErrorCount >= MAX_REPEATED_FORMAT_ERRORS) {
95684
+ const errorDesc = isWrapped ? "wrapped tool format" : unrecognizedTool;
95685
+ console.error(`[ERROR] Format error category '${errorCategory}' repeated ${sameFormatErrorCount} times. Breaking loop early to prevent infinite iteration.`);
95686
+ finalResult = `Error: Unable to complete request. The AI model repeatedly used incorrect tool call format (${errorDesc}). Please try rephrasing your question or using a different model.`;
95687
+ break;
95688
+ }
95689
+ } else {
95690
+ lastFormatErrorType = errorCategory;
95691
+ sameFormatErrorCount = 1;
95692
+ }
95693
+ } else {
95694
+ lastFormatErrorType = null;
95695
+ sameFormatErrorCount = 0;
95696
+ }
95583
95697
  }
95584
95698
  if (currentMessages.length > MAX_HISTORY_MESSAGES) {
95585
95699
  const messagesBefore = currentMessages.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc206",
3
+ "version": "0.6.0-rc207",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -91,6 +91,45 @@ const MAX_TOOL_ITERATIONS = (() => {
91
91
  })();
92
92
  const MAX_HISTORY_MESSAGES = 100;
93
93
 
94
+ /**
95
+ * Extract tool name from wrapped_tool:toolName format
96
+ * @param {string} wrappedToolError - Error string in format 'wrapped_tool:toolName'
97
+ * @returns {string} The extracted tool name or 'unknown' if format is invalid
98
+ */
99
+ function extractWrappedToolName(wrappedToolError) {
100
+ if (!wrappedToolError || typeof wrappedToolError !== 'string') {
101
+ return 'unknown';
102
+ }
103
+ const colonIndex = wrappedToolError.indexOf(':');
104
+ return colonIndex !== -1 ? wrappedToolError.slice(colonIndex + 1) : 'unknown';
105
+ }
106
+
107
+ /**
108
+ * Check if an error indicates a wrapped tool format error
109
+ * @param {string|null} error - Error from detectUnrecognizedToolCall
110
+ * @returns {boolean} True if it's a wrapped tool error
111
+ */
112
+ function isWrappedToolError(error) {
113
+ return error && typeof error === 'string' && error.startsWith('wrapped_tool:');
114
+ }
115
+
116
+ /**
117
+ * Create error message for wrapped tool format issues
118
+ * @param {string} wrappedToolName - The tool name that was incorrectly wrapped
119
+ * @returns {string} User-friendly error message with correct format instructions
120
+ */
121
+ function createWrappedToolErrorMessage(wrappedToolName) {
122
+ return `Your response contained an incorrectly formatted tool call (${wrappedToolName} wrapped in XML tags). This cannot be used.
123
+
124
+ Please use the CORRECT format:
125
+
126
+ <${wrappedToolName}>
127
+ Your content here
128
+ </${wrappedToolName}>
129
+
130
+ Do NOT wrap in other tags like <api_call>, <tool_name>, <function>, etc.`;
131
+ }
132
+
94
133
  // Supported image file extensions (imported from shared config)
95
134
 
96
135
  // Maximum image file size (20MB) to prevent OOM attacks
@@ -2542,6 +2581,11 @@ Follow these instructions carefully:
2542
2581
  }
2543
2582
  }
2544
2583
 
2584
+ // Circuit breaker for repeated format errors
2585
+ let lastFormatErrorType = null;
2586
+ let sameFormatErrorCount = 0;
2587
+ const MAX_REPEATED_FORMAT_ERRORS = 3;
2588
+
2545
2589
  // Tool iteration loop (only for non-CLI engines like Vercel/Anthropic/OpenAI)
2546
2590
  while (currentIteration < maxIterations && !completionAttempted) {
2547
2591
  currentIteration++;
@@ -2835,7 +2879,28 @@ Follow these instructions carefully:
2835
2879
  );
2836
2880
 
2837
2881
  if (lastAssistantMessage) {
2838
- finalResult = lastAssistantMessage.content;
2882
+ const prevContent = lastAssistantMessage.content;
2883
+
2884
+ // Check for patterns indicating a failed/wrapped tool call attempt
2885
+ // Use detectUnrecognizedToolCall for consistent detection logic
2886
+ const wrappedToolError = detectUnrecognizedToolCall(prevContent, validTools);
2887
+
2888
+ if (isWrappedToolError(wrappedToolError)) {
2889
+ // Previous response was a broken tool call attempt - don't reuse it
2890
+ const wrappedToolName = extractWrappedToolName(wrappedToolError);
2891
+ if (this.debug) {
2892
+ console.log(`[DEBUG] Previous response contains wrapped tool '${wrappedToolName}' - rejecting for __PREVIOUS_RESPONSE__`);
2893
+ }
2894
+ currentMessages.push({ role: 'assistant', content: assistantResponseContent });
2895
+ currentMessages.push({
2896
+ role: 'user',
2897
+ content: createWrappedToolErrorMessage(wrappedToolName)
2898
+ });
2899
+ completionAttempted = false;
2900
+ continue; // Don't use broken response, continue the loop
2901
+ }
2902
+
2903
+ finalResult = prevContent;
2839
2904
  if (this.debug) console.log(`[DEBUG] Using previous response as completion: ${finalResult.substring(0, 100)}...`);
2840
2905
  } else {
2841
2906
  finalResult = 'Error: No previous response found to use as completion.';
@@ -3165,7 +3230,32 @@ Follow these instructions carefully:
3165
3230
  const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
3166
3231
 
3167
3232
  let reminderContent;
3168
- if (unrecognizedTool) {
3233
+ if (isWrappedToolError(unrecognizedTool)) {
3234
+ // AI wrapped a valid tool name in arbitrary XML tags - provide clear format error
3235
+ const wrappedToolName = extractWrappedToolName(unrecognizedTool);
3236
+ if (this.debug) {
3237
+ console.log(`[DEBUG] Detected wrapped tool '${wrappedToolName}' in assistant response - wrong XML format.`);
3238
+ }
3239
+ const toolError = new ParameterError(
3240
+ `Tool '${wrappedToolName}' found but in WRONG FORMAT - do not wrap tools in other XML tags.`,
3241
+ {
3242
+ suggestion: `Use the tool tag DIRECTLY without any wrapper:
3243
+
3244
+ CORRECT FORMAT:
3245
+ <${wrappedToolName}>
3246
+ <param>value</param>
3247
+ </${wrappedToolName}>
3248
+
3249
+ WRONG (what you did - do not wrap in other tags):
3250
+ <api_call><tool_name>${wrappedToolName}</tool_name>...</api_call>
3251
+ <function>${wrappedToolName}</function>
3252
+ <call name="${wrappedToolName}">...</call>
3253
+
3254
+ Remove ALL wrapper tags and use <${wrappedToolName}> directly as the outermost tag.`
3255
+ }
3256
+ );
3257
+ reminderContent = `<tool_result>\n${formatErrorForAI(toolError)}\n</tool_result>`;
3258
+ } else if (unrecognizedTool) {
3169
3259
  // AI tried to use a tool that's not available - provide clear error
3170
3260
  if (this.debug) {
3171
3261
  console.log(`[DEBUG] Detected unrecognized tool '${unrecognizedTool}' in assistant response.`);
@@ -3175,6 +3265,33 @@ Follow these instructions carefully:
3175
3265
  });
3176
3266
  reminderContent = `<tool_result>\n${formatErrorForAI(toolError)}\n</tool_result>`;
3177
3267
  } else {
3268
+ // No tool call detected at all - check if this is the last iteration
3269
+ // On the last iteration, if the AI gave a substantive response without using
3270
+ // attempt_completion, accept it as the final answer rather than losing the content
3271
+ if (currentIteration >= maxIterations) {
3272
+ // Clean up the response - remove thinking tags
3273
+ let cleanedResponse = assistantResponseContent;
3274
+ // Remove <thinking>...</thinking> blocks
3275
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
3276
+ // Also remove unclosed thinking tags
3277
+ cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, '').trim();
3278
+
3279
+ // Only use if there's substantial content (not just a failed tool call attempt)
3280
+ const hasSubstantialContent = cleanedResponse.length > 50 &&
3281
+ !cleanedResponse.includes('<api_call>') &&
3282
+ !cleanedResponse.includes('<tool_name>') &&
3283
+ !cleanedResponse.includes('<function>');
3284
+
3285
+ if (hasSubstantialContent) {
3286
+ if (this.debug) {
3287
+ console.log(`[DEBUG] Max iterations reached - accepting AI response as final answer (${cleanedResponse.length} chars)`);
3288
+ }
3289
+ finalResult = cleanedResponse;
3290
+ completionAttempted = true;
3291
+ break;
3292
+ }
3293
+ }
3294
+
3178
3295
  // Standard reminder - no tool call detected at all
3179
3296
  reminderContent = `Please use one of the available tools to help answer the question, or use attempt_completion if you have enough information to provide a final answer.
3180
3297
 
@@ -3206,6 +3323,31 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
3206
3323
  console.log(`[DEBUG] No tool call detected in assistant response. Prompting for tool use.`);
3207
3324
  }
3208
3325
  }
3326
+
3327
+ // Circuit breaker: track repeated format errors and break early
3328
+ // For wrapped_tool errors, track them as a category (any wrapped_tool counts)
3329
+ // For other errors, track the exact error type
3330
+ if (unrecognizedTool) {
3331
+ const isWrapped = isWrappedToolError(unrecognizedTool);
3332
+ const errorCategory = isWrapped ? 'wrapped_tool' : unrecognizedTool;
3333
+
3334
+ if (errorCategory === lastFormatErrorType) {
3335
+ sameFormatErrorCount++;
3336
+ if (sameFormatErrorCount >= MAX_REPEATED_FORMAT_ERRORS) {
3337
+ const errorDesc = isWrapped ? 'wrapped tool format' : unrecognizedTool;
3338
+ console.error(`[ERROR] Format error category '${errorCategory}' repeated ${sameFormatErrorCount} times. Breaking loop early to prevent infinite iteration.`);
3339
+ finalResult = `Error: Unable to complete request. The AI model repeatedly used incorrect tool call format (${errorDesc}). Please try rephrasing your question or using a different model.`;
3340
+ break;
3341
+ }
3342
+ } else {
3343
+ lastFormatErrorType = errorCategory;
3344
+ sameFormatErrorCount = 1;
3345
+ }
3346
+ } else {
3347
+ // Reset counter if it's a different kind of "no tool call" situation
3348
+ lastFormatErrorType = null;
3349
+ sameFormatErrorCount = 0;
3350
+ }
3209
3351
  }
3210
3352
 
3211
3353
  // Keep message history manageable
@@ -617,6 +617,37 @@ export function detectUnrecognizedToolCall(xmlString, validTools) {
617
617
  }
618
618
  }
619
619
 
620
+ // Check if any valid tool name appears inside specific wrapper patterns
621
+ // This catches cases where AI wraps tools in arbitrary tags like:
622
+ // <api_call><tool_name>attempt_completion</tool_name>...</api_call>
623
+ // <function>search</function>
624
+ // <call name="extract">...</call>
625
+ // Only match specific wrapper patterns to avoid false positives with normal text
626
+ const allToolNames = [...new Set([...knownToolNames, ...validTools])];
627
+ for (const toolName of allToolNames) {
628
+ // Escape regex metacharacters in tool name to prevent regex errors
629
+ const escapedToolName = toolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
630
+
631
+ // Match specific wrapper patterns that indicate a tool call attempt:
632
+ // 1. <tool_name>toolName</tool_name> - common Claude API-style wrapper
633
+ // 2. <function>toolName</function> - function call style
634
+ // 3. <name>toolName</name> - generic name wrapper
635
+ // 4. <call><name>toolName - partial wrapper patterns
636
+ const wrapperPatterns = [
637
+ new RegExp(`<tool_name>\\s*${escapedToolName}\\s*</tool_name>`, 'i'),
638
+ new RegExp(`<function>\\s*${escapedToolName}\\s*</function>`, 'i'),
639
+ new RegExp(`<name>\\s*${escapedToolName}\\s*</name>`, 'i'),
640
+ // Also check for tool name immediately after api_call or call opening tag
641
+ new RegExp(`<(?:api_call|call)[^>]*>[\\s\\S]*?<tool_name>\\s*${escapedToolName}`, 'i')
642
+ ];
643
+
644
+ for (const pattern of wrapperPatterns) {
645
+ if (pattern.test(xmlString)) {
646
+ return `wrapped_tool:${toolName}`;
647
+ }
648
+ }
649
+ }
650
+
620
651
  return null;
621
652
  }
622
653