Introduce loop detection service that breaks simple loop (#3919)

Co-authored-by: Scott Densmore <scottdensmore@mac.com>
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
Sandy Tao
2025-07-14 20:25:16 -07:00
committed by GitHub
parent 7ffe8038ef
commit 734da8b9d2
5 changed files with 456 additions and 1 deletions

View File

@@ -40,6 +40,7 @@ import {
} from './contentGenerator.js';
import { ProxyAgent, setGlobalDispatcher } from 'undici';
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
import { LoopDetectionService } from '../services/loopDetectionService.js';
function isThinkingSupported(model: string) {
if (model.startsWith('gemini-2.5')) return true;
@@ -100,6 +101,9 @@ export class GeminiClient {
*/
private readonly COMPRESSION_PRESERVE_THRESHOLD = 0.3;
private readonly loopDetector = new LoopDetectionService();
private lastPromptId?: string;
constructor(private config: Config) {
if (config.getProxy()) {
setGlobalDispatcher(new ProxyAgent(config.getProxy() as string));
@@ -272,6 +276,10 @@ export class GeminiClient {
turns: number = this.MAX_TURNS,
originalModel?: string,
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
if (this.lastPromptId !== prompt_id) {
this.loopDetector.reset();
this.lastPromptId = prompt_id;
}
this.sessionTurnCount++;
if (
this.config.getMaxSessionTurns() > 0 &&
@@ -297,6 +305,10 @@ export class GeminiClient {
const turn = new Turn(this.getChat(), prompt_id);
const resultStream = turn.run(request, signal);
for await (const event of resultStream) {
if (this.loopDetector.addAndCheck(event)) {
yield { type: GeminiEventType.LoopDetected };
return turn;
}
yield event;
}
if (!turn.pendingToolCalls.length && signal && !signal.aborted) {