Skip to content

Commit 3747946

Browse files
committed
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
1 parent 271d01b commit 3747946

File tree

2 files changed

+151
-1
lines changed

2 files changed

+151
-1
lines changed

src/core/tools/ToolRepetitionDetector.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,25 @@ export class ToolRepetitionDetector {
108108
}
109109
}
110110

111-
// Create the object with the tool name and sorted parameters
111+
// For native protocol tools, also include nativeArgs if present
112+
// This ensures tools with array parameters (like read_file with files array)
113+
// are properly differentiated instead of all appearing identical
114+
const sortedNativeArgs: Record<string, unknown> = {}
115+
if (toolUse.nativeArgs && typeof toolUse.nativeArgs === "object") {
116+
const nativeKeys = Object.keys(toolUse.nativeArgs).sort()
117+
for (const key of nativeKeys) {
118+
if (Object.prototype.hasOwnProperty.call(toolUse.nativeArgs, key)) {
119+
sortedNativeArgs[key] = (toolUse.nativeArgs as Record<string, unknown>)[key]
120+
}
121+
}
122+
}
123+
124+
// Create the object with the tool name, sorted parameters, and sorted native args
112125
const toolObject = {
113126
name: toolUse.name,
114127
parameters: sortedParams,
128+
// Only include nativeArgs if it has content
129+
...(Object.keys(sortedNativeArgs).length > 0 ? { nativeArgs: sortedNativeArgs } : {}),
115130
}
116131

117132
// Convert to a canonical JSON string

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
})

0 commit comments

Comments
 (0)