@probelabs/probe 0.6.0-rc205 → 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.
@@ -70,6 +70,7 @@ import { RetryManager, createRetryManagerFromEnv } from './RetryManager.js';
70
70
  import { FallbackManager, createFallbackManagerFromEnv, buildFallbackProvidersFromEnv } from './FallbackManager.js';
71
71
  import { handleContextLimitError } from './contextCompactor.js';
72
72
  import { formatErrorForAI, ParameterError } from '../utils/error-types.js';
73
+ import { truncateIfNeeded, getMaxOutputTokens } from './outputTruncator.js';
73
74
  import {
74
75
  TaskManager,
75
76
  createTaskTool,
@@ -90,6 +91,45 @@ const MAX_TOOL_ITERATIONS = (() => {
90
91
  })();
91
92
  const MAX_HISTORY_MESSAGES = 100;
92
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
+
93
133
  // Supported image file extensions (imported from shared config)
94
134
 
95
135
  // Maximum image file size (20MB) to prevent OOM attacks
@@ -145,6 +185,7 @@ export class ProbeAgent {
145
185
  * @param {boolean} [options.fallback.stopOnSuccess=true] - Stop on first success
146
186
  * @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
147
187
  * @param {string} [options.completionPrompt] - Custom prompt to run after attempt_completion for validation/review (runs before mermaid/JSON validation)
188
+ * @param {number} [options.maxOutputTokens] - Maximum tokens for tool output before truncation (default: 20000, can also be set via PROBE_MAX_OUTPUT_TOKENS env var)
148
189
  */
149
190
  constructor(options = {}) {
150
191
  // Basic configuration
@@ -237,6 +278,9 @@ export class ProbeAgent {
237
278
  // Initialize token counter
238
279
  this.tokenCounter = new TokenCounter();
239
280
 
281
+ // Maximum output tokens for tool results (truncate if exceeded)
282
+ this.maxOutputTokens = getMaxOutputTokens(options.maxOutputTokens);
283
+
240
284
  if (this.debug) {
241
285
  console.log(`[DEBUG] Generated session ID for agent: ${this.sessionId}`);
242
286
  console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
@@ -2537,6 +2581,11 @@ Follow these instructions carefully:
2537
2581
  }
2538
2582
  }
2539
2583
 
2584
+ // Circuit breaker for repeated format errors
2585
+ let lastFormatErrorType = null;
2586
+ let sameFormatErrorCount = 0;
2587
+ const MAX_REPEATED_FORMAT_ERRORS = 3;
2588
+
2540
2589
  // Tool iteration loop (only for non-CLI engines like Vercel/Anthropic/OpenAI)
2541
2590
  while (currentIteration < maxIterations && !completionAttempted) {
2542
2591
  currentIteration++;
@@ -2830,7 +2879,28 @@ Follow these instructions carefully:
2830
2879
  );
2831
2880
 
2832
2881
  if (lastAssistantMessage) {
2833
- 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;
2834
2904
  if (this.debug) console.log(`[DEBUG] Using previous response as completion: ${finalResult.substring(0, 100)}...`);
2835
2905
  } else {
2836
2906
  finalResult = 'Error: No previous response found to use as completion.';
@@ -2882,7 +2952,24 @@ Follow these instructions carefully:
2882
2952
  // Execute MCP tool through the bridge
2883
2953
  const executionResult = await this.mcpBridge.mcpTools[toolName].execute(params);
2884
2954
 
2885
- const toolResultContent = typeof executionResult === 'string' ? executionResult : JSON.stringify(executionResult, null, 2);
2955
+ let toolResultContent = typeof executionResult === 'string' ? executionResult : JSON.stringify(executionResult, null, 2);
2956
+
2957
+ // Truncate if output exceeds token limit
2958
+ try {
2959
+ const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
2960
+ if (truncateResult.truncated) {
2961
+ toolResultContent = truncateResult.content;
2962
+ if (this.debug) {
2963
+ console.log(`[DEBUG] Tool output truncated: ${truncateResult.originalTokens} tokens -> saved to ${truncateResult.tempFilePath || 'N/A'}`);
2964
+ if (truncateResult.error) {
2965
+ console.log(`[DEBUG] Truncation file error: ${truncateResult.error}`);
2966
+ }
2967
+ }
2968
+ }
2969
+ } catch (truncateError) {
2970
+ // If truncation fails entirely, log and continue with original content
2971
+ console.error(`[WARN] Tool output truncation failed: ${truncateError.message}`);
2972
+ }
2886
2973
 
2887
2974
  // Log MCP tool result in debug mode
2888
2975
  if (this.debug) {
@@ -3059,10 +3146,28 @@ Follow these instructions carefully:
3059
3146
 
3060
3147
  // Add assistant response and tool result to conversation
3061
3148
  currentMessages.push({ role: 'assistant', content: assistantResponseContent });
3062
-
3063
- const toolResultContent = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2);
3149
+
3150
+ let toolResultContent = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult, null, 2);
3151
+
3152
+ // Truncate if output exceeds token limit
3153
+ try {
3154
+ const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
3155
+ if (truncateResult.truncated) {
3156
+ toolResultContent = truncateResult.content;
3157
+ if (this.debug) {
3158
+ console.log(`[DEBUG] Tool output truncated: ${truncateResult.originalTokens} tokens -> saved to ${truncateResult.tempFilePath || 'N/A'}`);
3159
+ if (truncateResult.error) {
3160
+ console.log(`[DEBUG] Truncation file error: ${truncateResult.error}`);
3161
+ }
3162
+ }
3163
+ }
3164
+ } catch (truncateError) {
3165
+ // If truncation fails entirely, log and continue with original content
3166
+ console.error(`[WARN] Tool output truncation failed: ${truncateError.message}`);
3167
+ }
3168
+
3064
3169
  const toolResultMessage = `<tool_result>\n${toolResultContent}\n</tool_result>`;
3065
-
3170
+
3066
3171
  currentMessages.push({
3067
3172
  role: 'user',
3068
3173
  content: toolResultMessage
@@ -3125,7 +3230,32 @@ Follow these instructions carefully:
3125
3230
  const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
3126
3231
 
3127
3232
  let reminderContent;
3128
- 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) {
3129
3259
  // AI tried to use a tool that's not available - provide clear error
3130
3260
  if (this.debug) {
3131
3261
  console.log(`[DEBUG] Detected unrecognized tool '${unrecognizedTool}' in assistant response.`);
@@ -3135,6 +3265,33 @@ Follow these instructions carefully:
3135
3265
  });
3136
3266
  reminderContent = `<tool_result>\n${formatErrorForAI(toolError)}\n</tool_result>`;
3137
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
+
3138
3295
  // Standard reminder - no tool call detected at all
3139
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.
3140
3297
 
@@ -3166,6 +3323,31 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
3166
3323
  console.log(`[DEBUG] No tool call detected in assistant response. Prompting for tool use.`);
3167
3324
  }
3168
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
+ }
3169
3351
  }
3170
3352
 
3171
3353
  // Keep message history manageable