@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.
- package/bin/binaries/probe-v0.6.0-rc207-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc207-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc207-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc207-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc207-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +188 -6
- package/build/agent/index.js +239 -16
- package/build/agent/outputTruncator.js +108 -0
- package/build/tools/common.js +31 -0
- package/cjs/agent/ProbeAgent.cjs +6657 -7513
- package/cjs/index.cjs +6682 -7538
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +188 -6
- package/src/agent/outputTruncator.js +108 -0
- package/src/tools/common.js +31 -0
- package/bin/binaries/probe-v0.6.0-rc205-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc205-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc205-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc205-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc205-x86_64-unknown-linux-musl.tar.gz +0 -0
package/package.json
CHANGED
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 20000;
|
|
7
|
+
const CHARS_PER_TOKEN = 4; // Conservative approximation
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate and normalize a token limit value.
|
|
11
|
+
* Returns the default if the value is invalid (NaN, negative, zero).
|
|
12
|
+
* @param {any} value - The value to validate
|
|
13
|
+
* @returns {number} A valid positive token limit
|
|
14
|
+
*/
|
|
15
|
+
function validateTokenLimit(value) {
|
|
16
|
+
const num = Number(value);
|
|
17
|
+
if (isNaN(num) || num <= 0) {
|
|
18
|
+
return DEFAULT_MAX_OUTPUT_TOKENS;
|
|
19
|
+
}
|
|
20
|
+
return num;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the maximum output tokens limit based on priority:
|
|
25
|
+
* 1. Constructor value (if provided and valid)
|
|
26
|
+
* 2. Environment variable PROBE_MAX_OUTPUT_TOKENS (if valid)
|
|
27
|
+
* 3. Default (20000)
|
|
28
|
+
* @param {number|undefined} constructorValue - Value passed to ProbeAgent constructor
|
|
29
|
+
* @returns {number} The maximum output tokens limit (always a valid positive number)
|
|
30
|
+
*/
|
|
31
|
+
export function getMaxOutputTokens(constructorValue) {
|
|
32
|
+
if (constructorValue !== undefined && constructorValue !== null) {
|
|
33
|
+
const validated = validateTokenLimit(constructorValue);
|
|
34
|
+
// Only use constructor value if it was valid; otherwise fall through to env/default
|
|
35
|
+
if (validated !== DEFAULT_MAX_OUTPUT_TOKENS || Number(constructorValue) === DEFAULT_MAX_OUTPUT_TOKENS) {
|
|
36
|
+
return validated;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (process.env.PROBE_MAX_OUTPUT_TOKENS) {
|
|
40
|
+
return validateTokenLimit(process.env.PROBE_MAX_OUTPUT_TOKENS);
|
|
41
|
+
}
|
|
42
|
+
return DEFAULT_MAX_OUTPUT_TOKENS;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Truncate tool output if it exceeds the token limit.
|
|
47
|
+
* When truncated, saves full output to a temp file and returns a message with the file path.
|
|
48
|
+
* If file system operations fail, returns truncated content without file reference.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} content - The tool output content to potentially truncate
|
|
51
|
+
* @param {Object} tokenCounter - TokenCounter instance with countTokens method
|
|
52
|
+
* @param {string} sessionId - Session ID for naming temp files
|
|
53
|
+
* @param {number} maxTokens - Maximum tokens allowed (defaults to 20000)
|
|
54
|
+
* @returns {Promise<{truncated: boolean, content: string, tempFilePath?: string, originalTokens?: number, error?: string}>}
|
|
55
|
+
*/
|
|
56
|
+
export async function truncateIfNeeded(content, tokenCounter, sessionId, maxTokens) {
|
|
57
|
+
const limit = validateTokenLimit(maxTokens);
|
|
58
|
+
const tokenCount = tokenCounter.countTokens(content);
|
|
59
|
+
|
|
60
|
+
if (tokenCount <= limit) {
|
|
61
|
+
return { truncated: false, content };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Truncate to approximately maxTokens worth of characters
|
|
65
|
+
const maxChars = limit * CHARS_PER_TOKEN;
|
|
66
|
+
const truncatedContent = content.substring(0, maxChars);
|
|
67
|
+
|
|
68
|
+
// Try to write full output to temp file
|
|
69
|
+
let tempFilePath = null;
|
|
70
|
+
let fileError = null;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const tempDir = join(tmpdir(), 'probe-output');
|
|
74
|
+
await mkdir(tempDir, { recursive: true });
|
|
75
|
+
tempFilePath = join(tempDir, `tool-output-${sessionId || 'unknown'}-${randomUUID()}.txt`);
|
|
76
|
+
await writeFile(tempFilePath, content, 'utf8');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
fileError = err.message || 'Unknown file system error';
|
|
79
|
+
tempFilePath = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let message;
|
|
83
|
+
if (tempFilePath) {
|
|
84
|
+
message = `Output exceeded maximum size (${tokenCount} tokens, limit: ${limit}).
|
|
85
|
+
Full output saved to: ${tempFilePath}
|
|
86
|
+
|
|
87
|
+
--- Truncated Output (first ${limit} tokens approx) ---
|
|
88
|
+
${truncatedContent}
|
|
89
|
+
...
|
|
90
|
+
--- End of Truncated Output ---`;
|
|
91
|
+
} else {
|
|
92
|
+
message = `Output exceeded maximum size (${tokenCount} tokens, limit: ${limit}).
|
|
93
|
+
Warning: Could not save full output to file (${fileError}).
|
|
94
|
+
|
|
95
|
+
--- Truncated Output (first ${limit} tokens approx) ---
|
|
96
|
+
${truncatedContent}
|
|
97
|
+
...
|
|
98
|
+
--- End of Truncated Output ---`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
truncated: true,
|
|
103
|
+
content: message,
|
|
104
|
+
tempFilePath: tempFilePath || undefined,
|
|
105
|
+
originalTokens: tokenCount,
|
|
106
|
+
error: fileError || undefined
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/tools/common.js
CHANGED
|
@@ -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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|