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:
amelidev
2026-07-17 15:03:46 -06:00
committed by GitHub
parent 69e0c2659e
commit acae7124bd
2 changed files with 120 additions and 11 deletions
@@ -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', () => {