@probelabs/probe 0.6.0-rc209 → 0.6.0-rc210
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-rc210-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc210-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc210-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc210-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc210-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +85 -4
- package/build/agent/bashCommandUtils.js +98 -12
- package/build/agent/bashPermissions.js +207 -1
- package/build/agent/index.js +275 -15
- package/build/delegate.js +11 -2
- package/build/tools/vercel.js +5 -2
- package/cjs/agent/ProbeAgent.cjs +275 -15
- package/cjs/index.cjs +275 -15
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +85 -4
- package/src/agent/bashCommandUtils.js +98 -12
- package/src/agent/bashPermissions.js +207 -1
- package/src/delegate.js +11 -2
- package/src/tools/vercel.js +5 -2
- package/bin/binaries/probe-v0.6.0-rc209-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc209-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc209-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc209-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc209-x86_64-unknown-linux-musl.tar.gz +0 -0
package/cjs/index.cjs
CHANGED
|
@@ -95328,6 +95328,9 @@ You are working with a repository located at: ${searchDirectory}
|
|
|
95328
95328
|
let lastFormatErrorType = null;
|
|
95329
95329
|
let sameFormatErrorCount = 0;
|
|
95330
95330
|
const MAX_REPEATED_FORMAT_ERRORS = 3;
|
|
95331
|
+
let lastNoToolResponse = null;
|
|
95332
|
+
let sameResponseCount = 0;
|
|
95333
|
+
const MAX_REPEATED_IDENTICAL_RESPONSES = 3;
|
|
95331
95334
|
while (currentIteration < maxIterations && !completionAttempted) {
|
|
95332
95335
|
currentIteration++;
|
|
95333
95336
|
if (this.cancelled) throw new Error("Request was cancelled by the user");
|
|
@@ -95809,6 +95812,26 @@ ${errorXml}
|
|
|
95809
95812
|
}
|
|
95810
95813
|
break;
|
|
95811
95814
|
}
|
|
95815
|
+
if (lastNoToolResponse !== null && assistantResponseContent === lastNoToolResponse) {
|
|
95816
|
+
sameResponseCount++;
|
|
95817
|
+
if (sameResponseCount >= MAX_REPEATED_IDENTICAL_RESPONSES) {
|
|
95818
|
+
let cleanedResponse = assistantResponseContent;
|
|
95819
|
+
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "").trim();
|
|
95820
|
+
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, "").trim();
|
|
95821
|
+
const hasSubstantialContent = cleanedResponse.length > 50 && !cleanedResponse.includes("<api_call>") && !cleanedResponse.includes("<tool_name>") && !cleanedResponse.includes("<function>");
|
|
95822
|
+
if (hasSubstantialContent) {
|
|
95823
|
+
if (this.debug) {
|
|
95824
|
+
console.log(`[DEBUG] Same response repeated ${sameResponseCount} times - accepting as final answer (${cleanedResponse.length} chars)`);
|
|
95825
|
+
}
|
|
95826
|
+
finalResult = cleanedResponse;
|
|
95827
|
+
completionAttempted = true;
|
|
95828
|
+
break;
|
|
95829
|
+
}
|
|
95830
|
+
}
|
|
95831
|
+
} else {
|
|
95832
|
+
lastNoToolResponse = assistantResponseContent;
|
|
95833
|
+
sameResponseCount = 1;
|
|
95834
|
+
}
|
|
95812
95835
|
currentMessages.push({ role: "assistant", content: assistantResponseContent });
|
|
95813
95836
|
const unrecognizedTool = detectUnrecognizedToolCall(assistantResponseContent, validTools);
|
|
95814
95837
|
let reminderContent;
|
|
@@ -95881,10 +95904,35 @@ Or if your previous response already contains a complete, direct answer (not a t
|
|
|
95881
95904
|
|
|
95882
95905
|
Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant message as the final answer. Only use this if that message was already a valid, complete response to the user's question.`;
|
|
95883
95906
|
}
|
|
95884
|
-
currentMessages.
|
|
95885
|
-
|
|
95886
|
-
|
|
95887
|
-
|
|
95907
|
+
const prevUserMsgIndex = currentMessages.length - 2;
|
|
95908
|
+
const prevUserMsg = currentMessages[prevUserMsgIndex];
|
|
95909
|
+
const isExistingReminder = prevUserMsg && prevUserMsg.role === "user" && (prevUserMsg.content.includes("Please use one of the available tools") || prevUserMsg.content.includes("<tool_result>"));
|
|
95910
|
+
if (isExistingReminder && sameResponseCount > 1) {
|
|
95911
|
+
const prevAssistantIndex = prevUserMsgIndex - 1;
|
|
95912
|
+
const hasSystemMessage2 = currentMessages.length > 0 && currentMessages[0].role === "system";
|
|
95913
|
+
const minValidIndex = hasSystemMessage2 ? 1 : 0;
|
|
95914
|
+
const canSafelyRemove = prevAssistantIndex >= minValidIndex && currentMessages[prevAssistantIndex] && currentMessages[prevAssistantIndex].role === "assistant" && currentMessages.length - 2 >= (hasSystemMessage2 ? 2 : 1);
|
|
95915
|
+
if (canSafelyRemove) {
|
|
95916
|
+
currentMessages.splice(prevAssistantIndex, 2);
|
|
95917
|
+
if (this.debug) {
|
|
95918
|
+
console.log(`[DEBUG] Removed duplicate assistant+reminder pair (iteration ${currentIteration}, same response #${sameResponseCount})`);
|
|
95919
|
+
}
|
|
95920
|
+
} else if (this.debug) {
|
|
95921
|
+
console.log(`[DEBUG] Skipped deduplication: pattern validation failed (prevAssistantIndex=${prevAssistantIndex}, arrayLength=${currentMessages.length})`);
|
|
95922
|
+
}
|
|
95923
|
+
const iterationHint = `
|
|
95924
|
+
|
|
95925
|
+
(Attempt #${sameResponseCount}: Your previous ${sameResponseCount} responses were identical. If you have a complete answer, use <attempt_complete></attempt_complete> to finalize it.)`;
|
|
95926
|
+
currentMessages.push({
|
|
95927
|
+
role: "user",
|
|
95928
|
+
content: reminderContent + iterationHint
|
|
95929
|
+
});
|
|
95930
|
+
} else {
|
|
95931
|
+
currentMessages.push({
|
|
95932
|
+
role: "user",
|
|
95933
|
+
content: reminderContent
|
|
95934
|
+
});
|
|
95935
|
+
}
|
|
95888
95936
|
if (this.debug) {
|
|
95889
95937
|
if (unrecognizedTool) {
|
|
95890
95938
|
console.log(`[DEBUG] Unrecognized tool '${unrecognizedTool}' used. Providing error feedback.`);
|
|
@@ -96656,7 +96704,10 @@ async function delegate({
|
|
|
96656
96704
|
disableTools = false,
|
|
96657
96705
|
searchDelegate = void 0,
|
|
96658
96706
|
schema = null,
|
|
96659
|
-
enableTasks = false
|
|
96707
|
+
enableTasks = false,
|
|
96708
|
+
enableMcp = false,
|
|
96709
|
+
mcpConfig = null,
|
|
96710
|
+
mcpConfigPath = null
|
|
96660
96711
|
}) {
|
|
96661
96712
|
if (!task || typeof task !== "string") {
|
|
96662
96713
|
throw new Error("Task parameter is required and must be a string");
|
|
@@ -96712,8 +96763,14 @@ async function delegate({
|
|
|
96712
96763
|
allowedTools,
|
|
96713
96764
|
disableTools,
|
|
96714
96765
|
searchDelegate,
|
|
96715
|
-
enableTasks
|
|
96766
|
+
enableTasks,
|
|
96716
96767
|
// Inherit from parent (subagent gets isolated TaskManager)
|
|
96768
|
+
enableMcp,
|
|
96769
|
+
// Inherit from parent (subagent creates own MCPXmlBridge)
|
|
96770
|
+
mcpConfig,
|
|
96771
|
+
// Inherit from parent
|
|
96772
|
+
mcpConfigPath
|
|
96773
|
+
// Inherit from parent
|
|
96717
96774
|
});
|
|
96718
96775
|
if (debug) {
|
|
96719
96776
|
console.error(`[DELEGATE] Created subagent with session ${sessionId}`);
|
|
@@ -97271,7 +97328,7 @@ var init_vercel = __esm({
|
|
|
97271
97328
|
});
|
|
97272
97329
|
};
|
|
97273
97330
|
delegateTool = (options = {}) => {
|
|
97274
|
-
const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig, architectureFileName } = options;
|
|
97331
|
+
const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null } = options;
|
|
97275
97332
|
return (0, import_ai4.tool)({
|
|
97276
97333
|
name: "delegate",
|
|
97277
97334
|
description: delegateDescription,
|
|
@@ -97330,7 +97387,10 @@ var init_vercel = __esm({
|
|
|
97330
97387
|
enableBash,
|
|
97331
97388
|
bashConfig,
|
|
97332
97389
|
architectureFileName,
|
|
97333
|
-
searchDelegate
|
|
97390
|
+
searchDelegate,
|
|
97391
|
+
enableMcp,
|
|
97392
|
+
mcpConfig,
|
|
97393
|
+
mcpConfigPath
|
|
97334
97394
|
});
|
|
97335
97395
|
return result;
|
|
97336
97396
|
}
|
|
@@ -97857,6 +97917,38 @@ function parseSimpleCommand(command) {
|
|
|
97857
97917
|
isComplex: false
|
|
97858
97918
|
};
|
|
97859
97919
|
}
|
|
97920
|
+
const stripQuotedContent = (str) => {
|
|
97921
|
+
let result = "";
|
|
97922
|
+
let inQuotes2 = false;
|
|
97923
|
+
let quoteChar2 = "";
|
|
97924
|
+
for (let i4 = 0; i4 < str.length; i4++) {
|
|
97925
|
+
const char = str[i4];
|
|
97926
|
+
const nextChar = str[i4 + 1];
|
|
97927
|
+
if (!inQuotes2 && char === "\\" && nextChar !== void 0) {
|
|
97928
|
+
i4++;
|
|
97929
|
+
continue;
|
|
97930
|
+
}
|
|
97931
|
+
if (inQuotes2 && quoteChar2 === '"' && char === "\\" && nextChar !== void 0) {
|
|
97932
|
+
i4++;
|
|
97933
|
+
continue;
|
|
97934
|
+
}
|
|
97935
|
+
if (!inQuotes2 && (char === '"' || char === "'")) {
|
|
97936
|
+
inQuotes2 = true;
|
|
97937
|
+
quoteChar2 = char;
|
|
97938
|
+
continue;
|
|
97939
|
+
}
|
|
97940
|
+
if (inQuotes2 && char === quoteChar2) {
|
|
97941
|
+
inQuotes2 = false;
|
|
97942
|
+
quoteChar2 = "";
|
|
97943
|
+
continue;
|
|
97944
|
+
}
|
|
97945
|
+
if (!inQuotes2) {
|
|
97946
|
+
result += char;
|
|
97947
|
+
}
|
|
97948
|
+
}
|
|
97949
|
+
return result;
|
|
97950
|
+
};
|
|
97951
|
+
const strippedForOperators = stripQuotedContent(trimmed);
|
|
97860
97952
|
const complexPatterns = [
|
|
97861
97953
|
/\|/,
|
|
97862
97954
|
// Pipes
|
|
@@ -97882,7 +97974,7 @@ function parseSimpleCommand(command) {
|
|
|
97882
97974
|
// Brace expansion like {a,b} or {1..10} (but not find {} placeholders)
|
|
97883
97975
|
];
|
|
97884
97976
|
for (const pattern of complexPatterns) {
|
|
97885
|
-
if (pattern.test(
|
|
97977
|
+
if (pattern.test(strippedForOperators)) {
|
|
97886
97978
|
return {
|
|
97887
97979
|
success: false,
|
|
97888
97980
|
error: "Complex shell commands with pipes, operators, or redirections are not supported for security reasons",
|
|
@@ -97897,17 +97989,17 @@ function parseSimpleCommand(command) {
|
|
|
97897
97989
|
let current = "";
|
|
97898
97990
|
let inQuotes = false;
|
|
97899
97991
|
let quoteChar = "";
|
|
97900
|
-
let escaped = false;
|
|
97901
97992
|
for (let i4 = 0; i4 < trimmed.length; i4++) {
|
|
97902
97993
|
const char = trimmed[i4];
|
|
97903
97994
|
const nextChar = i4 + 1 < trimmed.length ? trimmed[i4 + 1] : "";
|
|
97904
|
-
if (
|
|
97905
|
-
current +=
|
|
97906
|
-
|
|
97995
|
+
if (!inQuotes && char === "\\" && nextChar) {
|
|
97996
|
+
current += nextChar;
|
|
97997
|
+
i4++;
|
|
97907
97998
|
continue;
|
|
97908
97999
|
}
|
|
97909
|
-
if (char === "\\" &&
|
|
97910
|
-
|
|
98000
|
+
if (inQuotes && quoteChar === '"' && char === "\\" && nextChar) {
|
|
98001
|
+
current += nextChar;
|
|
98002
|
+
i4++;
|
|
97911
98003
|
continue;
|
|
97912
98004
|
}
|
|
97913
98005
|
if (!inQuotes && (char === '"' || char === "'")) {
|
|
@@ -98240,8 +98332,97 @@ var init_bashPermissions = __esm({
|
|
|
98240
98332
|
});
|
|
98241
98333
|
return result;
|
|
98242
98334
|
}
|
|
98335
|
+
/**
|
|
98336
|
+
* Split a complex command into component commands by operators
|
|
98337
|
+
*
|
|
98338
|
+
* ## Escape Handling (Security-Critical)
|
|
98339
|
+
*
|
|
98340
|
+
* This function intentionally PRESERVES escape sequences (both backslash AND
|
|
98341
|
+
* escaped character) in the output. This is step 1 of a 2-step parsing process:
|
|
98342
|
+
*
|
|
98343
|
+
* 1. _splitComplexCommand: Splits by operators, PRESERVES escapes → `echo "test\" && b"`
|
|
98344
|
+
* 2. parseCommand: Interprets escapes in each component → args: ['test" && b']
|
|
98345
|
+
*
|
|
98346
|
+
* This differs from stripQuotedContent() in bashCommandUtils.js which REMOVES
|
|
98347
|
+
* escapes entirely (for operator detection only).
|
|
98348
|
+
*
|
|
98349
|
+
* The security rationale: if we stripped escapes here, `\"` would become `"`,
|
|
98350
|
+
* potentially causing incorrect quote boundary detection and allowing operator
|
|
98351
|
+
* injection. By preserving escapes, parseCommand() can correctly interpret them.
|
|
98352
|
+
*
|
|
98353
|
+
* See bashCommandUtils.js module header for the full escape handling architecture.
|
|
98354
|
+
*
|
|
98355
|
+
* @private
|
|
98356
|
+
* @param {string} command - Complex command to split
|
|
98357
|
+
* @returns {string[]} Array of component commands (with escapes preserved)
|
|
98358
|
+
*/
|
|
98359
|
+
_splitComplexCommand(command) {
|
|
98360
|
+
const components = [];
|
|
98361
|
+
let current = "";
|
|
98362
|
+
let inQuotes = false;
|
|
98363
|
+
let quoteChar = "";
|
|
98364
|
+
let i4 = 0;
|
|
98365
|
+
while (i4 < command.length) {
|
|
98366
|
+
const char = command[i4];
|
|
98367
|
+
const nextChar = command[i4 + 1] || "";
|
|
98368
|
+
if (!inQuotes && char === "\\") {
|
|
98369
|
+
current += char;
|
|
98370
|
+
if (nextChar) {
|
|
98371
|
+
current += nextChar;
|
|
98372
|
+
i4 += 2;
|
|
98373
|
+
} else {
|
|
98374
|
+
i4++;
|
|
98375
|
+
}
|
|
98376
|
+
continue;
|
|
98377
|
+
}
|
|
98378
|
+
if (inQuotes && quoteChar === '"' && char === "\\" && nextChar) {
|
|
98379
|
+
current += char + nextChar;
|
|
98380
|
+
i4 += 2;
|
|
98381
|
+
continue;
|
|
98382
|
+
}
|
|
98383
|
+
if (!inQuotes && (char === '"' || char === "'")) {
|
|
98384
|
+
inQuotes = true;
|
|
98385
|
+
quoteChar = char;
|
|
98386
|
+
current += char;
|
|
98387
|
+
i4++;
|
|
98388
|
+
continue;
|
|
98389
|
+
}
|
|
98390
|
+
if (inQuotes && char === quoteChar) {
|
|
98391
|
+
inQuotes = false;
|
|
98392
|
+
quoteChar = "";
|
|
98393
|
+
current += char;
|
|
98394
|
+
i4++;
|
|
98395
|
+
continue;
|
|
98396
|
+
}
|
|
98397
|
+
if (!inQuotes) {
|
|
98398
|
+
if (char === "&" && nextChar === "&" || char === "|" && nextChar === "|") {
|
|
98399
|
+
if (current.trim()) {
|
|
98400
|
+
components.push(current.trim());
|
|
98401
|
+
}
|
|
98402
|
+
current = "";
|
|
98403
|
+
i4 += 2;
|
|
98404
|
+
continue;
|
|
98405
|
+
}
|
|
98406
|
+
if (char === "|") {
|
|
98407
|
+
if (current.trim()) {
|
|
98408
|
+
components.push(current.trim());
|
|
98409
|
+
}
|
|
98410
|
+
current = "";
|
|
98411
|
+
i4++;
|
|
98412
|
+
continue;
|
|
98413
|
+
}
|
|
98414
|
+
}
|
|
98415
|
+
current += char;
|
|
98416
|
+
i4++;
|
|
98417
|
+
}
|
|
98418
|
+
if (current.trim()) {
|
|
98419
|
+
components.push(current.trim());
|
|
98420
|
+
}
|
|
98421
|
+
return components;
|
|
98422
|
+
}
|
|
98243
98423
|
/**
|
|
98244
98424
|
* Check a complex command against complex patterns in allow/deny lists
|
|
98425
|
+
* Also supports auto-allowing commands where all components are individually allowed
|
|
98245
98426
|
* @private
|
|
98246
98427
|
* @param {string} command - Complex command to check
|
|
98247
98428
|
* @returns {Object} Permission result
|
|
@@ -98296,6 +98477,85 @@ var init_bashPermissions = __esm({
|
|
|
98296
98477
|
return result;
|
|
98297
98478
|
}
|
|
98298
98479
|
}
|
|
98480
|
+
const components = this._splitComplexCommand(command);
|
|
98481
|
+
if (this.debug) {
|
|
98482
|
+
console.log(`[BashPermissions] Checking ${components.length} command components: ${JSON.stringify(components)}`);
|
|
98483
|
+
}
|
|
98484
|
+
if (components.length > 1) {
|
|
98485
|
+
const componentResults = [];
|
|
98486
|
+
let allAllowed = true;
|
|
98487
|
+
let deniedComponent = null;
|
|
98488
|
+
let deniedReason = null;
|
|
98489
|
+
for (const component of components) {
|
|
98490
|
+
const parsed = parseCommand(component);
|
|
98491
|
+
if (parsed.error || parsed.isComplex) {
|
|
98492
|
+
if (this.debug) {
|
|
98493
|
+
console.log(`[BashPermissions] Component "${component}" is complex or has error: ${parsed.error}`);
|
|
98494
|
+
}
|
|
98495
|
+
allAllowed = false;
|
|
98496
|
+
deniedComponent = component;
|
|
98497
|
+
deniedReason = parsed.error || "Component contains nested complex constructs";
|
|
98498
|
+
break;
|
|
98499
|
+
}
|
|
98500
|
+
if (matchesAnyPattern(parsed, this.denyPatterns)) {
|
|
98501
|
+
if (this.debug) {
|
|
98502
|
+
console.log(`[BashPermissions] Component "${component}" matches deny pattern`);
|
|
98503
|
+
}
|
|
98504
|
+
allAllowed = false;
|
|
98505
|
+
deniedComponent = component;
|
|
98506
|
+
deniedReason = "Component matches deny pattern";
|
|
98507
|
+
break;
|
|
98508
|
+
}
|
|
98509
|
+
if (!matchesAnyPattern(parsed, this.allowPatterns)) {
|
|
98510
|
+
if (this.debug) {
|
|
98511
|
+
console.log(`[BashPermissions] Component "${component}" not in allow list`);
|
|
98512
|
+
}
|
|
98513
|
+
allAllowed = false;
|
|
98514
|
+
deniedComponent = component;
|
|
98515
|
+
deniedReason = "Component not in allow list";
|
|
98516
|
+
break;
|
|
98517
|
+
}
|
|
98518
|
+
componentResults.push({ component, parsed, allowed: true });
|
|
98519
|
+
}
|
|
98520
|
+
if (allAllowed) {
|
|
98521
|
+
if (this.debug) {
|
|
98522
|
+
console.log(`[BashPermissions] ALLOWED - all ${components.length} components passed individual checks`);
|
|
98523
|
+
}
|
|
98524
|
+
const result = {
|
|
98525
|
+
allowed: true,
|
|
98526
|
+
command,
|
|
98527
|
+
isComplex: true,
|
|
98528
|
+
allowedByComponents: true,
|
|
98529
|
+
components: componentResults
|
|
98530
|
+
};
|
|
98531
|
+
this.recordBashEvent("permission.allowed", {
|
|
98532
|
+
command,
|
|
98533
|
+
isComplex: true,
|
|
98534
|
+
allowedByComponents: true,
|
|
98535
|
+
componentCount: components.length
|
|
98536
|
+
});
|
|
98537
|
+
return result;
|
|
98538
|
+
} else {
|
|
98539
|
+
if (this.debug) {
|
|
98540
|
+
console.log(`[BashPermissions] DENIED - component "${deniedComponent}" failed: ${deniedReason}`);
|
|
98541
|
+
}
|
|
98542
|
+
const result = {
|
|
98543
|
+
allowed: false,
|
|
98544
|
+
reason: `Component "${deniedComponent}" not allowed: ${deniedReason}`,
|
|
98545
|
+
command,
|
|
98546
|
+
isComplex: true,
|
|
98547
|
+
failedComponent: deniedComponent
|
|
98548
|
+
};
|
|
98549
|
+
this.recordBashEvent("permission.denied", {
|
|
98550
|
+
command,
|
|
98551
|
+
reason: "component_not_allowed",
|
|
98552
|
+
failedComponent: deniedComponent,
|
|
98553
|
+
componentReason: deniedReason,
|
|
98554
|
+
isComplex: true
|
|
98555
|
+
});
|
|
98556
|
+
return result;
|
|
98557
|
+
}
|
|
98558
|
+
}
|
|
98299
98559
|
if (this.debug) {
|
|
98300
98560
|
console.log(`[BashPermissions] DENIED - no matching complex pattern found`);
|
|
98301
98561
|
}
|
package/package.json
CHANGED
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -2587,6 +2587,11 @@ Follow these instructions carefully:
|
|
|
2587
2587
|
let sameFormatErrorCount = 0;
|
|
2588
2588
|
const MAX_REPEATED_FORMAT_ERRORS = 3;
|
|
2589
2589
|
|
|
2590
|
+
// Circuit breaker for repeated identical responses without tool calls
|
|
2591
|
+
let lastNoToolResponse = null;
|
|
2592
|
+
let sameResponseCount = 0;
|
|
2593
|
+
const MAX_REPEATED_IDENTICAL_RESPONSES = 3;
|
|
2594
|
+
|
|
2590
2595
|
// Tool iteration loop (only for non-CLI engines like Vercel/Anthropic/OpenAI)
|
|
2591
2596
|
while (currentIteration < maxIterations && !completionAttempted) {
|
|
2592
2597
|
currentIteration++;
|
|
@@ -3224,6 +3229,36 @@ Follow these instructions carefully:
|
|
|
3224
3229
|
break;
|
|
3225
3230
|
}
|
|
3226
3231
|
|
|
3232
|
+
// Check for repeated identical responses - if AI gives same response 3 times,
|
|
3233
|
+
// accept it as the final answer instead of continuing the loop
|
|
3234
|
+
if (lastNoToolResponse !== null && assistantResponseContent === lastNoToolResponse) {
|
|
3235
|
+
sameResponseCount++;
|
|
3236
|
+
if (sameResponseCount >= MAX_REPEATED_IDENTICAL_RESPONSES) {
|
|
3237
|
+
// Clean up the response - remove thinking tags
|
|
3238
|
+
let cleanedResponse = assistantResponseContent;
|
|
3239
|
+
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
|
|
3240
|
+
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, '').trim();
|
|
3241
|
+
|
|
3242
|
+
const hasSubstantialContent = cleanedResponse.length > 50 &&
|
|
3243
|
+
!cleanedResponse.includes('<api_call>') &&
|
|
3244
|
+
!cleanedResponse.includes('<tool_name>') &&
|
|
3245
|
+
!cleanedResponse.includes('<function>');
|
|
3246
|
+
|
|
3247
|
+
if (hasSubstantialContent) {
|
|
3248
|
+
if (this.debug) {
|
|
3249
|
+
console.log(`[DEBUG] Same response repeated ${sameResponseCount} times - accepting as final answer (${cleanedResponse.length} chars)`);
|
|
3250
|
+
}
|
|
3251
|
+
finalResult = cleanedResponse;
|
|
3252
|
+
completionAttempted = true;
|
|
3253
|
+
break;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
} else {
|
|
3257
|
+
// Different response, reset counter
|
|
3258
|
+
lastNoToolResponse = assistantResponseContent;
|
|
3259
|
+
sameResponseCount = 1;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3227
3262
|
// Add assistant response and ask for tool usage
|
|
3228
3263
|
currentMessages.push({ role: 'assistant', content: assistantResponseContent });
|
|
3229
3264
|
|
|
@@ -3313,10 +3348,56 @@ Or if your previous response already contains a complete, direct answer (not a t
|
|
|
3313
3348
|
Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant message as the final answer. Only use this if that message was already a valid, complete response to the user's question.`;
|
|
3314
3349
|
}
|
|
3315
3350
|
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3351
|
+
// Check if we should replace the previous reminder instead of appending
|
|
3352
|
+
// After pushing assistant message, the previous user message (if a reminder) is at length - 2
|
|
3353
|
+
// Message pattern: [..., prev_assistant, prev_user_reminder, current_assistant]
|
|
3354
|
+
const prevUserMsgIndex = currentMessages.length - 2;
|
|
3355
|
+
const prevUserMsg = currentMessages[prevUserMsgIndex];
|
|
3356
|
+
const isExistingReminder = prevUserMsg && prevUserMsg.role === 'user' &&
|
|
3357
|
+
(prevUserMsg.content.includes('Please use one of the available tools') ||
|
|
3358
|
+
prevUserMsg.content.includes('<tool_result>'));
|
|
3359
|
+
|
|
3360
|
+
if (isExistingReminder && sameResponseCount > 1) {
|
|
3361
|
+
// Replace the previous reminder with updated content and remove duplicated assistant message
|
|
3362
|
+
// This prevents context bloat from repeated identical exchanges
|
|
3363
|
+
// Pattern: [..., prev_assistant, prev_user_reminder, current_assistant] -> [..., current_assistant, new_reminder]
|
|
3364
|
+
const prevAssistantIndex = prevUserMsgIndex - 1;
|
|
3365
|
+
|
|
3366
|
+
// Validate the expected pattern before splicing:
|
|
3367
|
+
// 1. prevAssistantIndex must be valid (>= 0)
|
|
3368
|
+
// 2. If there's a system message at index 0, don't remove it (prevAssistantIndex > 0)
|
|
3369
|
+
// 3. Must be an assistant message at prevAssistantIndex
|
|
3370
|
+
// 4. After removal, array should have at least 2 messages (current assistant + new reminder)
|
|
3371
|
+
const hasSystemMessage = currentMessages.length > 0 && currentMessages[0].role === 'system';
|
|
3372
|
+
const minValidIndex = hasSystemMessage ? 1 : 0;
|
|
3373
|
+
const canSafelyRemove = prevAssistantIndex >= minValidIndex &&
|
|
3374
|
+
currentMessages[prevAssistantIndex] &&
|
|
3375
|
+
currentMessages[prevAssistantIndex].role === 'assistant' &&
|
|
3376
|
+
(currentMessages.length - 2) >= (hasSystemMessage ? 2 : 1); // After removal: at least system+assistant or just assistant
|
|
3377
|
+
|
|
3378
|
+
if (canSafelyRemove) {
|
|
3379
|
+
// Remove the duplicate assistant and old reminder (2 messages starting at prevAssistantIndex)
|
|
3380
|
+
currentMessages.splice(prevAssistantIndex, 2);
|
|
3381
|
+
if (this.debug) {
|
|
3382
|
+
console.log(`[DEBUG] Removed duplicate assistant+reminder pair (iteration ${currentIteration}, same response #${sameResponseCount})`);
|
|
3383
|
+
}
|
|
3384
|
+
} else if (this.debug) {
|
|
3385
|
+
console.log(`[DEBUG] Skipped deduplication: pattern validation failed (prevAssistantIndex=${prevAssistantIndex}, arrayLength=${currentMessages.length})`);
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
// Add iteration context to help the AI understand this is a repeated attempt
|
|
3389
|
+
const iterationHint = `\n\n(Attempt #${sameResponseCount}: Your previous ${sameResponseCount} responses were identical. If you have a complete answer, use <attempt_complete></attempt_complete> to finalize it.)`;
|
|
3390
|
+
currentMessages.push({
|
|
3391
|
+
role: 'user',
|
|
3392
|
+
content: reminderContent + iterationHint
|
|
3393
|
+
});
|
|
3394
|
+
} else {
|
|
3395
|
+
currentMessages.push({
|
|
3396
|
+
role: 'user',
|
|
3397
|
+
content: reminderContent
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3320
3401
|
if (this.debug) {
|
|
3321
3402
|
if (unrecognizedTool) {
|
|
3322
3403
|
console.log(`[DEBUG] Unrecognized tool '${unrecognizedTool}' used. Providing error feedback.`);
|
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified command parsing utilities for bash tool
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This module provides a single source of truth for parsing shell commands.
|
|
5
5
|
* It supports only simple commands (no pipes, operators, or substitutions)
|
|
6
6
|
* to align with the executor's capabilities.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* ## Escape Handling Architecture
|
|
9
|
+
*
|
|
10
|
+
* There are THREE different escape handling behaviors in the bash permission system,
|
|
11
|
+
* each serving a distinct purpose:
|
|
12
|
+
*
|
|
13
|
+
* 1. **stripQuotedContent()** (in parseSimpleCommand): SKIPS both backslash AND next char
|
|
14
|
+
* - Purpose: Detect operators (|, &&, ||) that exist OUTSIDE quoted strings
|
|
15
|
+
* - Output is never used for execution, only for operator detection
|
|
16
|
+
* - Example: `echo "a && b"` → strips quoted content → no `&&` detected outside quotes
|
|
17
|
+
*
|
|
18
|
+
* 2. **parseSimpleCommand()** main loop: STRIPS backslash, KEEPS escaped char
|
|
19
|
+
* - Purpose: Extract actual argument values that would be passed to the command
|
|
20
|
+
* - Matches bash behavior where `\"` inside double quotes becomes `"`
|
|
21
|
+
* - Example: `echo "he said \"hi\""` → args: ['he said "hi"']
|
|
22
|
+
*
|
|
23
|
+
* 3. **_splitComplexCommand()** (in bashPermissions.js): PRESERVES both backslash AND next char
|
|
24
|
+
* - Purpose: Split complex commands by operators while preserving escape sequences
|
|
25
|
+
* - Output is passed to parseCommand() which will then interpret the escapes
|
|
26
|
+
* - Example: `echo "test\" && b" && cmd` → components passed to parseCommand for final parsing
|
|
27
|
+
*
|
|
28
|
+
* This design ensures:
|
|
29
|
+
* - Commands with operators inside quotes (e.g., `echo "a && b"`) are NOT incorrectly
|
|
30
|
+
* flagged as complex commands
|
|
31
|
+
* - Escaped quotes (e.g., `\"`) don't prematurely end quoted sections
|
|
32
|
+
* - Each component gets proper escape interpretation in the final parsing step
|
|
33
|
+
*
|
|
8
34
|
* @module bashCommandUtils
|
|
9
35
|
*/
|
|
10
36
|
|
|
@@ -38,7 +64,64 @@ export function parseSimpleCommand(command) {
|
|
|
38
64
|
};
|
|
39
65
|
}
|
|
40
66
|
|
|
67
|
+
// Strip quoted content before checking for complex operators
|
|
68
|
+
// This prevents detecting operators inside quotes (e.g., echo "a && b")
|
|
69
|
+
//
|
|
70
|
+
// IMPORTANT: This function is ONLY used to detect operators, NOT for argument parsing.
|
|
71
|
+
// It REMOVES both backslash AND escaped character from the output, unlike parseSimpleCommand
|
|
72
|
+
// which interprets escapes. This is intentional - we only care about finding operators
|
|
73
|
+
// that exist outside of quoted strings. See module header for architecture details.
|
|
74
|
+
const stripQuotedContent = (str) => {
|
|
75
|
+
let result = '';
|
|
76
|
+
let inQuotes = false;
|
|
77
|
+
let quoteChar = '';
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < str.length; i++) {
|
|
80
|
+
const char = str[i];
|
|
81
|
+
const nextChar = str[i + 1];
|
|
82
|
+
|
|
83
|
+
// Handle escape sequences outside quotes - skip both chars (not operators)
|
|
84
|
+
if (!inQuotes && char === '\\' && nextChar !== undefined) {
|
|
85
|
+
// Skip the backslash and next char - they can't be operators
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle escape sequences inside double quotes
|
|
91
|
+
// In bash, only ", $, `, \, and newline are escapable in double quotes
|
|
92
|
+
if (inQuotes && quoteChar === '"' && char === '\\' && nextChar !== undefined) {
|
|
93
|
+
// Skip both the backslash and the escaped character (stays inside quotes)
|
|
94
|
+
i++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Start of quoted section
|
|
99
|
+
if (!inQuotes && (char === '"' || char === "'")) {
|
|
100
|
+
inQuotes = true;
|
|
101
|
+
quoteChar = char;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// End of quoted section
|
|
106
|
+
if (inQuotes && char === quoteChar) {
|
|
107
|
+
inQuotes = false;
|
|
108
|
+
quoteChar = '';
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Only add characters that are outside quotes
|
|
113
|
+
if (!inQuotes) {
|
|
114
|
+
result += char;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
|
|
41
121
|
// Check for complex shell constructs that we don't support
|
|
122
|
+
// Use stripped version (without quoted content) for operator detection
|
|
123
|
+
const strippedForOperators = stripQuotedContent(trimmed);
|
|
124
|
+
|
|
42
125
|
const complexPatterns = [
|
|
43
126
|
/\|/, // Pipes
|
|
44
127
|
/&&/, // Logical AND
|
|
@@ -54,7 +137,7 @@ export function parseSimpleCommand(command) {
|
|
|
54
137
|
];
|
|
55
138
|
|
|
56
139
|
for (const pattern of complexPatterns) {
|
|
57
|
-
if (pattern.test(
|
|
140
|
+
if (pattern.test(strippedForOperators)) {
|
|
58
141
|
return {
|
|
59
142
|
success: false,
|
|
60
143
|
error: 'Complex shell commands with pipes, operators, or redirections are not supported for security reasons',
|
|
@@ -71,22 +154,25 @@ export function parseSimpleCommand(command) {
|
|
|
71
154
|
let current = '';
|
|
72
155
|
let inQuotes = false;
|
|
73
156
|
let quoteChar = '';
|
|
74
|
-
let escaped = false;
|
|
75
157
|
|
|
76
158
|
for (let i = 0; i < trimmed.length; i++) {
|
|
77
159
|
const char = trimmed[i];
|
|
78
160
|
const nextChar = i + 1 < trimmed.length ? trimmed[i + 1] : '';
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
161
|
+
|
|
162
|
+
// Handle escapes outside quotes
|
|
163
|
+
if (!inQuotes && char === '\\' && nextChar) {
|
|
164
|
+
// Add the escaped character (skip the backslash)
|
|
165
|
+
current += nextChar;
|
|
166
|
+
i++; // Skip next character
|
|
84
167
|
continue;
|
|
85
168
|
}
|
|
86
169
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
170
|
+
// Handle escapes inside double quotes (single quotes don't process escapes)
|
|
171
|
+
if (inQuotes && quoteChar === '"' && char === '\\' && nextChar) {
|
|
172
|
+
// In double quotes, backslash escapes certain characters
|
|
173
|
+
// Add the escaped character to current
|
|
174
|
+
current += nextChar;
|
|
175
|
+
i++; // Skip next character
|
|
90
176
|
continue;
|
|
91
177
|
}
|
|
92
178
|
|