mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-19 06:20:44 -07:00
fix(core): mitigate infinite ReAct loops and prompt injection loops (#28429)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -175,6 +175,93 @@ describe('LoopDetectionService', () => {
|
||||
}
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should detect an alternating tool call loop (cycle of length 2)', () => {
|
||||
const eventA = createToolCallRequestEvent('read_file', {
|
||||
file_path: 'loop_a.txt',
|
||||
});
|
||||
const eventB = createToolCallRequestEvent('read_file', {
|
||||
file_path: 'loop_b.txt',
|
||||
});
|
||||
|
||||
let loopCount = 0;
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const currentEvent = i % 2 === 0 ? eventA : eventB;
|
||||
const result = service.addAndCheck(currentEvent);
|
||||
if (result.count > 0) {
|
||||
loopCount = result.count;
|
||||
}
|
||||
}
|
||||
|
||||
expect(loopCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should detect a cyclic tool call loop of length 3', () => {
|
||||
const eventA = createToolCallRequestEvent('read_file', {
|
||||
file_path: 'loop_a.txt',
|
||||
});
|
||||
const eventB = createToolCallRequestEvent('read_file', {
|
||||
file_path: 'loop_b.txt',
|
||||
});
|
||||
const eventC = createToolCallRequestEvent('read_file', {
|
||||
file_path: 'loop_c.txt',
|
||||
});
|
||||
|
||||
let loopCount = 0;
|
||||
const sequence = [eventA, eventB, eventC];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const result = service.addAndCheck(sequence[i % 3]);
|
||||
if (result.count > 0) {
|
||||
loopCount = result.count;
|
||||
}
|
||||
}
|
||||
|
||||
expect(loopCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should detect a cyclic tool call loop of length 5', () => {
|
||||
const e1 = createToolCallRequestEvent('t', { id: 1 });
|
||||
const e2 = createToolCallRequestEvent('t', { id: 2 });
|
||||
const e3 = createToolCallRequestEvent('t', { id: 3 });
|
||||
const e4 = createToolCallRequestEvent('t', { id: 4 });
|
||||
const e5 = createToolCallRequestEvent('t', { id: 5 });
|
||||
|
||||
let loopCount = 0;
|
||||
const sequence = [e1, e2, e3, e4, e5];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const result = service.addAndCheck(sequence[i % 5]);
|
||||
if (result.count > 0) {
|
||||
loopCount = result.count;
|
||||
}
|
||||
}
|
||||
|
||||
expect(loopCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not detect loops for non-looping alternating sequences that vary', () => {
|
||||
const eventA = createToolCallRequestEvent('read_file', {
|
||||
file_path: 'loop_a.txt',
|
||||
});
|
||||
const eventB = createToolCallRequestEvent('read_file', {
|
||||
file_path: 'loop_b.txt',
|
||||
});
|
||||
const eventC = createToolCallRequestEvent('read_file', {
|
||||
file_path: 'loop_c.txt',
|
||||
});
|
||||
|
||||
// Run some alternating calls, then break the pattern with eventC
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(service.addAndCheck(i % 2 === 0 ? eventA : eventB).count).toBe(
|
||||
0,
|
||||
);
|
||||
}
|
||||
expect(service.addAndCheck(eventC).count).toBe(0);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(service.addAndCheck(i % 2 === 0 ? eventA : eventB).count).toBe(
|
||||
0,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Content Loop Detection', () => {
|
||||
|
||||
@@ -136,8 +136,7 @@ export class LoopDetectionService {
|
||||
private userPrompt = '';
|
||||
|
||||
// Tool call tracking
|
||||
private lastToolCallKey: string | null = null;
|
||||
private toolCallRepetitionCount: number = 0;
|
||||
private toolCallHistory: string[] = [];
|
||||
|
||||
// Content streaming tracking
|
||||
private streamContentHistory = '';
|
||||
@@ -313,15 +312,39 @@ export class LoopDetectionService {
|
||||
|
||||
private checkToolCallLoop(toolCall: { name: string; args: object }): boolean {
|
||||
const key = this.getToolCallKey(toolCall);
|
||||
if (this.lastToolCallKey === key) {
|
||||
this.toolCallRepetitionCount++;
|
||||
} else {
|
||||
this.lastToolCallKey = key;
|
||||
this.toolCallRepetitionCount = 1;
|
||||
this.toolCallHistory.push(key);
|
||||
|
||||
const maxRequiredLength = 5 * TOOL_CALL_LOOP_THRESHOLD;
|
||||
if (this.toolCallHistory.length > maxRequiredLength) {
|
||||
this.toolCallHistory = this.toolCallHistory.slice(-maxRequiredLength);
|
||||
}
|
||||
if (this.toolCallRepetitionCount >= TOOL_CALL_LOOP_THRESHOLD) {
|
||||
return true;
|
||||
|
||||
const n = this.toolCallHistory.length;
|
||||
const R = TOOL_CALL_LOOP_THRESHOLD; // 5
|
||||
|
||||
// Check for repeating patterns of cycle length k from 1 to 5
|
||||
for (let k = 1; k <= 5; k++) {
|
||||
const requiredLength = k * R;
|
||||
if (n >= requiredLength) {
|
||||
const cycle = this.toolCallHistory.slice(-k);
|
||||
let isPatternMatch = true;
|
||||
|
||||
for (let i = 0; i < requiredLength; i++) {
|
||||
const indexFromEnd = requiredLength - i;
|
||||
const actualKey = this.toolCallHistory[n - indexFromEnd];
|
||||
const expectedKey = cycle[i % k];
|
||||
if (actualKey !== expectedKey) {
|
||||
isPatternMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPatternMatch) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -739,8 +762,7 @@ export class LoopDetectionService {
|
||||
}
|
||||
|
||||
private resetToolCallCount(): void {
|
||||
this.lastToolCallKey = null;
|
||||
this.toolCallRepetitionCount = 0;
|
||||
this.toolCallHistory = [];
|
||||
}
|
||||
|
||||
private resetContentTracking(resetHistory = true): void {
|
||||
|
||||
Reference in New Issue
Block a user