Skip to content

Commit 89068c4

Browse files
daniel-lxsmrubens
andauthored
fix: Include nativeArgs in tool repetition detection (#9377)
* fix: Include nativeArgs in tool repetition detection Fixes false positive 'stuck in a loop' error for native protocol tools like read_file that store parameters in nativeArgs instead of params. Previously, the ToolRepetitionDetector only compared the params object, which was empty for native protocol tools. This caused all read_file calls to appear identical, triggering false loop detection even when reading different files. Changes: - Updated serializeToolUse() to include nativeArgs in comparison - Added comprehensive tests for native protocol scenarios - Maintains backward compatibility with XML protocol tools Closes: Issue reported in Discord about read_file loop detection * Try to use safe-stable-stringify in the tool repetition detector --------- Co-authored-by: Matt Rubens <[email protected]>
1 parent 7c07945 commit 89068c4

File tree

4 files changed

+154
-18
lines changed

4 files changed

+154
-18
lines changed

pnpm-lock.yaml

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/tools/ToolRepetitionDetector.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import stringify from "safe-stable-stringify"
12
import { ToolUse } from "../../shared/tools"
23
import { t } from "../../i18n"
34

@@ -95,26 +96,16 @@ export class ToolRepetitionDetector {
9596
* @returns JSON string representation of the tool use with sorted parameter keys
9697
*/
9798
private serializeToolUse(toolUse: ToolUse): string {
98-
// Create a new parameters object with alphabetically sorted keys
99-
const sortedParams: Record<string, unknown> = {}
100-
101-
// Get parameter keys and sort them alphabetically
102-
const sortedKeys = Object.keys(toolUse.params).sort()
103-
104-
// Populate the sorted parameters object in a type-safe way
105-
for (const key of sortedKeys) {
106-
if (Object.prototype.hasOwnProperty.call(toolUse.params, key)) {
107-
sortedParams[key] = toolUse.params[key as keyof typeof toolUse.params]
108-
}
99+
const toolObject: Record<string, any> = {
100+
name: toolUse.name,
101+
params: toolUse.params,
109102
}
110103

111-
// Create the object with the tool name and sorted parameters
112-
const toolObject = {
113-
name: toolUse.name,
114-
parameters: sortedParams,
104+
// Only include nativeArgs if it has content
105+
if (toolUse.nativeArgs && Object.keys(toolUse.nativeArgs).length > 0) {
106+
toolObject.nativeArgs = toolUse.nativeArgs
115107
}
116108

117-
// Convert to a canonical JSON string
118-
return JSON.stringify(toolObject)
109+
return stringify(toolObject)
119110
}
120111
}

src/core/tools/__tests__/ToolRepetitionDetector.spec.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,4 +562,139 @@ describe("ToolRepetitionDetector", () => {
562562
expect(result.askUser).toBeDefined()
563563
})
564564
})
565+
566+
// ===== Native Protocol (nativeArgs) tests =====
567+
describe("native protocol with nativeArgs", () => {
568+
it("should differentiate read_file calls with different files in nativeArgs", () => {
569+
const detector = new ToolRepetitionDetector(2)
570+
571+
// Create read_file tool use with nativeArgs (like native protocol does)
572+
const readFile1: ToolUse = {
573+
type: "tool_use",
574+
name: "read_file" as ToolName,
575+
params: {}, // Empty for native protocol
576+
partial: false,
577+
nativeArgs: {
578+
files: [{ path: "file1.ts" }],
579+
},
580+
}
581+
582+
const readFile2: ToolUse = {
583+
type: "tool_use",
584+
name: "read_file" as ToolName,
585+
params: {}, // Empty for native protocol
586+
partial: false,
587+
nativeArgs: {
588+
files: [{ path: "file2.ts" }],
589+
},
590+
}
591+
592+
// First call with file1
593+
expect(detector.check(readFile1).allowExecution).toBe(true)
594+
595+
// Second call with file2 - should be treated as different
596+
expect(detector.check(readFile2).allowExecution).toBe(true)
597+
598+
// Third call with file1 again - should reset counter
599+
expect(detector.check(readFile1).allowExecution).toBe(true)
600+
})
601+
602+
it("should detect repetition when same files are read multiple times with nativeArgs", () => {
603+
const detector = new ToolRepetitionDetector(2)
604+
605+
// Create identical read_file tool uses
606+
const readFile: ToolUse = {
607+
type: "tool_use",
608+
name: "read_file" as ToolName,
609+
params: {}, // Empty for native protocol
610+
partial: false,
611+
nativeArgs: {
612+
files: [{ path: "same-file.ts" }],
613+
},
614+
}
615+
616+
// First call allowed
617+
expect(detector.check(readFile).allowExecution).toBe(true)
618+
619+
// Second call allowed
620+
expect(detector.check(readFile).allowExecution).toBe(true)
621+
622+
// Third identical call should be blocked (limit is 2)
623+
const result = detector.check(readFile)
624+
expect(result.allowExecution).toBe(false)
625+
expect(result.askUser).toBeDefined()
626+
})
627+
628+
it("should differentiate read_file calls with multiple files in different orders", () => {
629+
const detector = new ToolRepetitionDetector(2)
630+
631+
const readFile1: ToolUse = {
632+
type: "tool_use",
633+
name: "read_file" as ToolName,
634+
params: {},
635+
partial: false,
636+
nativeArgs: {
637+
files: [{ path: "a.ts" }, { path: "b.ts" }],
638+
},
639+
}
640+
641+
const readFile2: ToolUse = {
642+
type: "tool_use",
643+
name: "read_file" as ToolName,
644+
params: {},
645+
partial: false,
646+
nativeArgs: {
647+
files: [{ path: "b.ts" }, { path: "a.ts" }],
648+
},
649+
}
650+
651+
// Different order should be treated as different calls
652+
expect(detector.check(readFile1).allowExecution).toBe(true)
653+
expect(detector.check(readFile2).allowExecution).toBe(true)
654+
})
655+
656+
it("should handle tools with both params and nativeArgs", () => {
657+
const detector = new ToolRepetitionDetector(2)
658+
659+
const tool1: ToolUse = {
660+
type: "tool_use",
661+
name: "execute_command" as ToolName,
662+
params: { command: "ls" },
663+
partial: false,
664+
nativeArgs: {
665+
command: "ls",
666+
cwd: "/home/user",
667+
},
668+
}
669+
670+
const tool2: ToolUse = {
671+
type: "tool_use",
672+
name: "execute_command" as ToolName,
673+
params: { command: "ls" },
674+
partial: false,
675+
nativeArgs: {
676+
command: "ls",
677+
cwd: "/home/admin",
678+
},
679+
}
680+
681+
// Different cwd in nativeArgs should make these different
682+
expect(detector.check(tool1).allowExecution).toBe(true)
683+
expect(detector.check(tool2).allowExecution).toBe(true)
684+
})
685+
686+
it("should handle tools with only params (no nativeArgs)", () => {
687+
const detector = new ToolRepetitionDetector(2)
688+
689+
const legacyTool = createToolUse("read_file", "read_file", { path: "test.txt" })
690+
691+
// Should work the same as before
692+
expect(detector.check(legacyTool).allowExecution).toBe(true)
693+
expect(detector.check(legacyTool).allowExecution).toBe(true)
694+
695+
const result = detector.check(legacyTool)
696+
expect(result.allowExecution).toBe(false)
697+
expect(result.askUser).toBeDefined()
698+
})
699+
})
565700
})

src/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@
509509
"puppeteer-chromium-resolver": "^24.0.0",
510510
"puppeteer-core": "^23.4.0",
511511
"reconnecting-eventsource": "^1.6.4",
512+
"safe-stable-stringify": "^2.5.0",
512513
"sanitize-filename": "^1.6.3",
513514
"say": "^0.16.0",
514515
"serialize-error": "^12.0.0",

0 commit comments

Comments
 (0)