@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/build/agent/index.js
CHANGED
|
@@ -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) {
|
|
@@ -9718,10 +9734,10 @@ var init_vercel = __esm({
|
|
|
9718
9734
|
let extractOptions = { cwd: effectiveCwd };
|
|
9719
9735
|
if (input_content) {
|
|
9720
9736
|
const { writeFileSync: writeFileSync2, unlinkSync } = await import("fs");
|
|
9721
|
-
const { join:
|
|
9722
|
-
const { tmpdir } = await import("os");
|
|
9723
|
-
const { randomUUID:
|
|
9724
|
-
tempFilePath =
|
|
9737
|
+
const { join: join5 } = await import("path");
|
|
9738
|
+
const { tmpdir: tmpdir2 } = await import("os");
|
|
9739
|
+
const { randomUUID: randomUUID9 } = await import("crypto");
|
|
9740
|
+
tempFilePath = join5(tmpdir2(), `probe-extract-${randomUUID9()}.txt`);
|
|
9725
9741
|
writeFileSync2(tempFilePath, input_content);
|
|
9726
9742
|
if (debug) {
|
|
9727
9743
|
console.error(`Created temporary file for input content: ${tempFilePath}`);
|
|
@@ -67117,10 +67133,88 @@ var init_contextCompactor = __esm({
|
|
|
67117
67133
|
}
|
|
67118
67134
|
});
|
|
67119
67135
|
|
|
67136
|
+
// src/agent/outputTruncator.js
|
|
67137
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
67138
|
+
import { tmpdir } from "os";
|
|
67139
|
+
import { join as join4 } from "path";
|
|
67140
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
67141
|
+
function validateTokenLimit(value) {
|
|
67142
|
+
const num = Number(value);
|
|
67143
|
+
if (isNaN(num) || num <= 0) {
|
|
67144
|
+
return DEFAULT_MAX_OUTPUT_TOKENS;
|
|
67145
|
+
}
|
|
67146
|
+
return num;
|
|
67147
|
+
}
|
|
67148
|
+
function getMaxOutputTokens(constructorValue) {
|
|
67149
|
+
if (constructorValue !== void 0 && constructorValue !== null) {
|
|
67150
|
+
const validated = validateTokenLimit(constructorValue);
|
|
67151
|
+
if (validated !== DEFAULT_MAX_OUTPUT_TOKENS || Number(constructorValue) === DEFAULT_MAX_OUTPUT_TOKENS) {
|
|
67152
|
+
return validated;
|
|
67153
|
+
}
|
|
67154
|
+
}
|
|
67155
|
+
if (process.env.PROBE_MAX_OUTPUT_TOKENS) {
|
|
67156
|
+
return validateTokenLimit(process.env.PROBE_MAX_OUTPUT_TOKENS);
|
|
67157
|
+
}
|
|
67158
|
+
return DEFAULT_MAX_OUTPUT_TOKENS;
|
|
67159
|
+
}
|
|
67160
|
+
async function truncateIfNeeded(content, tokenCounter, sessionId, maxTokens) {
|
|
67161
|
+
const limit = validateTokenLimit(maxTokens);
|
|
67162
|
+
const tokenCount = tokenCounter.countTokens(content);
|
|
67163
|
+
if (tokenCount <= limit) {
|
|
67164
|
+
return { truncated: false, content };
|
|
67165
|
+
}
|
|
67166
|
+
const maxChars = limit * CHARS_PER_TOKEN;
|
|
67167
|
+
const truncatedContent = content.substring(0, maxChars);
|
|
67168
|
+
let tempFilePath = null;
|
|
67169
|
+
let fileError = null;
|
|
67170
|
+
try {
|
|
67171
|
+
const tempDir = join4(tmpdir(), "probe-output");
|
|
67172
|
+
await mkdir(tempDir, { recursive: true });
|
|
67173
|
+
tempFilePath = join4(tempDir, `tool-output-${sessionId || "unknown"}-${randomUUID4()}.txt`);
|
|
67174
|
+
await writeFile(tempFilePath, content, "utf8");
|
|
67175
|
+
} catch (err) {
|
|
67176
|
+
fileError = err.message || "Unknown file system error";
|
|
67177
|
+
tempFilePath = null;
|
|
67178
|
+
}
|
|
67179
|
+
let message;
|
|
67180
|
+
if (tempFilePath) {
|
|
67181
|
+
message = `Output exceeded maximum size (${tokenCount} tokens, limit: ${limit}).
|
|
67182
|
+
Full output saved to: ${tempFilePath}
|
|
67183
|
+
|
|
67184
|
+
--- Truncated Output (first ${limit} tokens approx) ---
|
|
67185
|
+
${truncatedContent}
|
|
67186
|
+
...
|
|
67187
|
+
--- End of Truncated Output ---`;
|
|
67188
|
+
} else {
|
|
67189
|
+
message = `Output exceeded maximum size (${tokenCount} tokens, limit: ${limit}).
|
|
67190
|
+
Warning: Could not save full output to file (${fileError}).
|
|
67191
|
+
|
|
67192
|
+
--- Truncated Output (first ${limit} tokens approx) ---
|
|
67193
|
+
${truncatedContent}
|
|
67194
|
+
...
|
|
67195
|
+
--- End of Truncated Output ---`;
|
|
67196
|
+
}
|
|
67197
|
+
return {
|
|
67198
|
+
truncated: true,
|
|
67199
|
+
content: message,
|
|
67200
|
+
tempFilePath: tempFilePath || void 0,
|
|
67201
|
+
originalTokens: tokenCount,
|
|
67202
|
+
error: fileError || void 0
|
|
67203
|
+
};
|
|
67204
|
+
}
|
|
67205
|
+
var DEFAULT_MAX_OUTPUT_TOKENS, CHARS_PER_TOKEN;
|
|
67206
|
+
var init_outputTruncator = __esm({
|
|
67207
|
+
"src/agent/outputTruncator.js"() {
|
|
67208
|
+
"use strict";
|
|
67209
|
+
DEFAULT_MAX_OUTPUT_TOKENS = 2e4;
|
|
67210
|
+
CHARS_PER_TOKEN = 4;
|
|
67211
|
+
}
|
|
67212
|
+
});
|
|
67213
|
+
|
|
67120
67214
|
// src/agent/mcp/built-in-server.js
|
|
67121
67215
|
import { createServer } from "http";
|
|
67122
67216
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
67123
|
-
import { randomUUID as
|
|
67217
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
67124
67218
|
import { Server as MCPServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
67125
67219
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
67126
67220
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -67372,7 +67466,7 @@ var init_built_in_server = __esm({
|
|
|
67372
67466
|
}
|
|
67373
67467
|
const eventStore = new InMemoryEventStore();
|
|
67374
67468
|
transport = new StreamableHTTPServerTransport({
|
|
67375
|
-
sessionIdGenerator: () =>
|
|
67469
|
+
sessionIdGenerator: () => randomUUID5(),
|
|
67376
67470
|
eventStore,
|
|
67377
67471
|
// Enable resumability
|
|
67378
67472
|
onsessioninitialized: (newSessionId) => {
|
|
@@ -68593,11 +68687,32 @@ import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
|
|
|
68593
68687
|
import { createGoogleGenerativeAI as createGoogleGenerativeAI2 } from "@ai-sdk/google";
|
|
68594
68688
|
import { createAmazonBedrock as createAmazonBedrock2 } from "@ai-sdk/amazon-bedrock";
|
|
68595
68689
|
import { streamText as streamText2 } from "ai";
|
|
68596
|
-
import { randomUUID as
|
|
68690
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
68597
68691
|
import { EventEmitter as EventEmitter5 } from "events";
|
|
68598
68692
|
import { existsSync as existsSync6 } from "fs";
|
|
68599
68693
|
import { readFile as readFile3, stat, readdir as readdir3 } from "fs/promises";
|
|
68600
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
|
+
}
|
|
68601
68716
|
var MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, MAX_IMAGE_FILE_SIZE, ProbeAgent;
|
|
68602
68717
|
var init_ProbeAgent = __esm({
|
|
68603
68718
|
"src/agent/ProbeAgent.js"() {
|
|
@@ -68622,6 +68737,7 @@ var init_ProbeAgent = __esm({
|
|
|
68622
68737
|
init_FallbackManager();
|
|
68623
68738
|
init_contextCompactor();
|
|
68624
68739
|
init_error_types();
|
|
68740
|
+
init_outputTruncator();
|
|
68625
68741
|
init_tasks();
|
|
68626
68742
|
dotenv2.config();
|
|
68627
68743
|
MAX_TOOL_ITERATIONS = (() => {
|
|
@@ -68681,9 +68797,10 @@ var init_ProbeAgent = __esm({
|
|
|
68681
68797
|
* @param {boolean} [options.fallback.stopOnSuccess=true] - Stop on first success
|
|
68682
68798
|
* @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
|
|
68683
68799
|
* @param {string} [options.completionPrompt] - Custom prompt to run after attempt_completion for validation/review (runs before mermaid/JSON validation)
|
|
68800
|
+
* @param {number} [options.maxOutputTokens] - Maximum tokens for tool output before truncation (default: 20000, can also be set via PROBE_MAX_OUTPUT_TOKENS env var)
|
|
68684
68801
|
*/
|
|
68685
68802
|
constructor(options = {}) {
|
|
68686
|
-
this.sessionId = options.sessionId ||
|
|
68803
|
+
this.sessionId = options.sessionId || randomUUID6();
|
|
68687
68804
|
this.customPrompt = options.systemPrompt || options.customPrompt || null;
|
|
68688
68805
|
this.promptType = options.promptType || "code-explorer";
|
|
68689
68806
|
this.allowEdit = !!options.allowEdit;
|
|
@@ -68742,6 +68859,7 @@ var init_ProbeAgent = __esm({
|
|
|
68742
68859
|
this.clientApiKey = null;
|
|
68743
68860
|
this.clientApiUrl = null;
|
|
68744
68861
|
this.tokenCounter = new TokenCounter();
|
|
68862
|
+
this.maxOutputTokens = getMaxOutputTokens(options.maxOutputTokens);
|
|
68745
68863
|
if (this.debug) {
|
|
68746
68864
|
console.log(`[DEBUG] Generated session ID for agent: ${this.sessionId}`);
|
|
68747
68865
|
console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
|
|
@@ -70594,6 +70712,9 @@ You are working with a repository located at: ${searchDirectory}
|
|
|
70594
70712
|
console.log(`[DEBUG] Schema provided, using extended iteration limit: ${maxIterations} (base: ${baseMaxIterations})`);
|
|
70595
70713
|
}
|
|
70596
70714
|
}
|
|
70715
|
+
let lastFormatErrorType = null;
|
|
70716
|
+
let sameFormatErrorCount = 0;
|
|
70717
|
+
const MAX_REPEATED_FORMAT_ERRORS = 3;
|
|
70597
70718
|
while (currentIteration < maxIterations && !completionAttempted) {
|
|
70598
70719
|
currentIteration++;
|
|
70599
70720
|
if (this.cancelled) throw new Error("Request was cancelled by the user");
|
|
@@ -70792,7 +70913,22 @@ You are working with a repository located at: ${searchDirectory}
|
|
|
70792
70913
|
(msg) => msg.role === "assistant" && msg.content && !(this.mcpBridge ? parseHybridXmlToolCall(msg.content, validTools, this.mcpBridge) : parseXmlToolCallWithThinking(msg.content, validTools))
|
|
70793
70914
|
);
|
|
70794
70915
|
if (lastAssistantMessage) {
|
|
70795
|
-
|
|
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;
|
|
70796
70932
|
if (this.debug) console.log(`[DEBUG] Using previous response as completion: ${finalResult.substring(0, 100)}...`);
|
|
70797
70933
|
} else {
|
|
70798
70934
|
finalResult = "Error: No previous response found to use as completion.";
|
|
@@ -70833,7 +70969,21 @@ You are working with a repository located at: ${searchDirectory}
|
|
|
70833
70969
|
`);
|
|
70834
70970
|
}
|
|
70835
70971
|
const executionResult = await this.mcpBridge.mcpTools[toolName].execute(params);
|
|
70836
|
-
|
|
70972
|
+
let toolResultContent = typeof executionResult === "string" ? executionResult : JSON.stringify(executionResult, null, 2);
|
|
70973
|
+
try {
|
|
70974
|
+
const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
|
|
70975
|
+
if (truncateResult.truncated) {
|
|
70976
|
+
toolResultContent = truncateResult.content;
|
|
70977
|
+
if (this.debug) {
|
|
70978
|
+
console.log(`[DEBUG] Tool output truncated: ${truncateResult.originalTokens} tokens -> saved to ${truncateResult.tempFilePath || "N/A"}`);
|
|
70979
|
+
if (truncateResult.error) {
|
|
70980
|
+
console.log(`[DEBUG] Truncation file error: ${truncateResult.error}`);
|
|
70981
|
+
}
|
|
70982
|
+
}
|
|
70983
|
+
}
|
|
70984
|
+
} catch (truncateError) {
|
|
70985
|
+
console.error(`[WARN] Tool output truncation failed: ${truncateError.message}`);
|
|
70986
|
+
}
|
|
70837
70987
|
if (this.debug) {
|
|
70838
70988
|
const preview = toolResultContent.length > 500 ? toolResultContent.substring(0, 500) + "..." : toolResultContent;
|
|
70839
70989
|
console.error(`[DEBUG] ========================================`);
|
|
@@ -70982,7 +71132,21 @@ ${errorXml}
|
|
|
70982
71132
|
throw toolError;
|
|
70983
71133
|
}
|
|
70984
71134
|
currentMessages.push({ role: "assistant", content: assistantResponseContent });
|
|
70985
|
-
|
|
71135
|
+
let toolResultContent = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult, null, 2);
|
|
71136
|
+
try {
|
|
71137
|
+
const truncateResult = await truncateIfNeeded(toolResultContent, this.tokenCounter, this.sessionId, this.maxOutputTokens);
|
|
71138
|
+
if (truncateResult.truncated) {
|
|
71139
|
+
toolResultContent = truncateResult.content;
|
|
71140
|
+
if (this.debug) {
|
|
71141
|
+
console.log(`[DEBUG] Tool output truncated: ${truncateResult.originalTokens} tokens -> saved to ${truncateResult.tempFilePath || "N/A"}`);
|
|
71142
|
+
if (truncateResult.error) {
|
|
71143
|
+
console.log(`[DEBUG] Truncation file error: ${truncateResult.error}`);
|
|
71144
|
+
}
|
|
71145
|
+
}
|
|
71146
|
+
}
|
|
71147
|
+
} catch (truncateError) {
|
|
71148
|
+
console.error(`[WARN] Tool output truncation failed: ${truncateError.message}`);
|
|
71149
|
+
}
|
|
70986
71150
|
const toolResultMessage = `<tool_result>
|
|
70987
71151
|
${toolResultContent}
|
|
70988
71152
|
</tool_result>`;
|
|
@@ -71035,7 +71199,33 @@ ${errorXml}
|
|
|
71035
71199
|
currentMessages.push({ role: "assistant", content: assistantResponseContent });
|
|
71036
71200
|
const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
|
|
71037
71201
|
let reminderContent;
|
|
71038
|
-
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) {
|
|
71039
71229
|
if (this.debug) {
|
|
71040
71230
|
console.log(`[DEBUG] Detected unrecognized tool '${unrecognizedTool}' in assistant response.`);
|
|
71041
71231
|
}
|
|
@@ -71046,6 +71236,20 @@ ${errorXml}
|
|
|
71046
71236
|
${formatErrorForAI(toolError)}
|
|
71047
71237
|
</tool_result>`;
|
|
71048
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
|
+
}
|
|
71049
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.
|
|
71050
71254
|
|
|
71051
71255
|
Remember: Use proper XML format with BOTH opening and closing tags:
|
|
@@ -71075,6 +71279,25 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
|
|
|
71075
71279
|
console.log(`[DEBUG] No tool call detected in assistant response. Prompting for tool use.`);
|
|
71076
71280
|
}
|
|
71077
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
|
+
}
|
|
71078
71301
|
}
|
|
71079
71302
|
if (currentMessages.length > MAX_HISTORY_MESSAGES) {
|
|
71080
71303
|
const messagesBefore = currentMessages.length;
|
|
@@ -71591,7 +71814,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
71591
71814
|
*/
|
|
71592
71815
|
clone(options = {}) {
|
|
71593
71816
|
const {
|
|
71594
|
-
sessionId =
|
|
71817
|
+
sessionId = randomUUID6(),
|
|
71595
71818
|
stripInternalMessages = true,
|
|
71596
71819
|
keepSystemMessage = true,
|
|
71597
71820
|
deepCopy = true,
|
|
@@ -71818,7 +72041,7 @@ import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
|
|
|
71818
72041
|
import { resolve as resolve7 } from "path";
|
|
71819
72042
|
|
|
71820
72043
|
// src/agent/acp/server.js
|
|
71821
|
-
import { randomUUID as
|
|
72044
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
71822
72045
|
|
|
71823
72046
|
// src/agent/acp/connection.js
|
|
71824
72047
|
import { EventEmitter as EventEmitter6 } from "events";
|
|
@@ -72310,7 +72533,7 @@ var ACPServer = class {
|
|
|
72310
72533
|
* Handle new session request
|
|
72311
72534
|
*/
|
|
72312
72535
|
async handleNewSession(params) {
|
|
72313
|
-
const sessionId = params?.sessionId ||
|
|
72536
|
+
const sessionId = params?.sessionId || randomUUID7();
|
|
72314
72537
|
const mode = params?.mode || SessionMode.NORMAL;
|
|
72315
72538
|
const session = new ACPSession(sessionId, mode);
|
|
72316
72539
|
this.sessions.set(sessionId, session);
|
|
@@ -72482,7 +72705,7 @@ var ACPServer = class {
|
|
|
72482
72705
|
};
|
|
72483
72706
|
|
|
72484
72707
|
// src/agent/acp/tools.js
|
|
72485
|
-
import { randomUUID as
|
|
72708
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
72486
72709
|
|
|
72487
72710
|
// src/agent/index.js
|
|
72488
72711
|
dotenv3.config();
|
|
@@ -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/build/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
|
|