Skip to content

Commit e860f51

Browse files
authored
feat(core): add setting to disable loop detection (google-gemini#18008)
1 parent b0be1f1 commit e860f51

9 files changed

Lines changed: 46 additions & 2 deletions

File tree

docs/cli/settings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ they appear in the UI.
7777
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
7878
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
7979
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
80+
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
8081
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
8182

8283
### Context

docs/get-started/configuration.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,12 @@ their corresponding top-level category object in your `settings.json` file.
326326
- **Default:** `0.5`
327327
- **Requires restart:** Yes
328328

329+
- **`model.disableLoopDetection`** (boolean):
330+
- **Description:** Disable automatic detection and prevention of infinite
331+
loops.
332+
- **Default:** `false`
333+
- **Requires restart:** Yes
334+
329335
- **`model.skipNextSpeakerCheck`** (boolean):
330336
- **Description:** Skip the next speaker check.
331337
- **Default:** `true`

packages/cli/src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,7 @@ export async function loadCliConfig(
762762
noBrowser: !!process.env['NO_BROWSER'],
763763
summarizeToolOutput: settings.model?.summarizeToolOutput,
764764
ideMode,
765+
disableLoopDetection: settings.model?.disableLoopDetection,
765766
compressionThreshold: settings.model?.compressionThreshold,
766767
folderTrust,
767768
interactive,

packages/cli/src/config/settingsSchema.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,16 @@ const SETTINGS_SCHEMA = {
739739
'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).',
740740
showInDialog: true,
741741
},
742+
disableLoopDetection: {
743+
type: 'boolean',
744+
label: 'Disable Loop Detection',
745+
category: 'Model',
746+
requiresRestart: true,
747+
default: false,
748+
description:
749+
'Disable automatic detection and prevention of infinite loops.',
750+
showInDialog: true,
751+
},
742752
skipNextSpeakerCheck: {
743753
type: 'boolean',
744754
label: 'Skip Next Speaker Check',

packages/core/src/config/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ export interface ConfigParameters {
393393
includeDirectories?: string[];
394394
bugCommand?: BugCommandSettings;
395395
model: string;
396+
disableLoopDetection?: boolean;
396397
maxSessionTurns?: number;
397398
experimentalZedIntegration?: boolean;
398399
listSessions?: boolean;
@@ -531,6 +532,7 @@ export class Config {
531532
private readonly cwd: string;
532533
private readonly bugCommand: BugCommandSettings | undefined;
533534
private model: string;
535+
private readonly disableLoopDetection: boolean;
534536
private previewFeatures: boolean | undefined;
535537
private hasAccessToPreviewModel: boolean = false;
536538
private readonly noBrowser: boolean;
@@ -697,6 +699,7 @@ export class Config {
697699
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
698700
this.bugCommand = params.bugCommand;
699701
this.model = params.model;
702+
this.disableLoopDetection = params.disableLoopDetection ?? false;
700703
this._activeModel = params.model;
701704
this.enableAgents = params.enableAgents ?? false;
702705
this.agents = params.agents ?? {};
@@ -1118,6 +1121,10 @@ export class Config {
11181121
return this.model;
11191122
}
11201123

1124+
getDisableLoopDetection(): boolean {
1125+
return this.disableLoopDetection ?? false;
1126+
}
1127+
11211128
setModel(newModel: string, isTemporary: boolean = true): void {
11221129
if (this.model !== newModel || this._activeModel !== newModel) {
11231130
this.model = newModel;

packages/core/src/core/client.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ describe('Gemini Client (client.ts)', () => {
213213
getGlobalMemory: vi.fn().mockReturnValue(''),
214214
getEnvironmentMemory: vi.fn().mockReturnValue(''),
215215
isJitContextEnabled: vi.fn().mockReturnValue(false),
216+
getDisableLoopDetection: vi.fn().mockReturnValue(false),
216217

217218
getSessionId: vi.fn().mockReturnValue('test-session-id'),
218219
getProxy: vi.fn().mockReturnValue(undefined),

packages/core/src/services/loopDetectionService.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe('LoopDetectionService', () => {
3838
mockConfig = {
3939
getTelemetryEnabled: () => true,
4040
isInteractive: () => false,
41+
getDisableLoopDetection: () => false,
4142
getModelAvailabilityService: vi
4243
.fn()
4344
.mockReturnValue(createAvailabilityServiceMock()),
@@ -162,6 +163,15 @@ describe('LoopDetectionService', () => {
162163
// Should now return false even though a loop was previously detected
163164
expect(service.addAndCheck(event)).toBe(false);
164165
});
166+
167+
it('should skip loop detection if disabled in config', () => {
168+
vi.spyOn(mockConfig, 'getDisableLoopDetection').mockReturnValue(true);
169+
const event = createToolCallRequestEvent('testTool', { param: 'value' });
170+
for (let i = 0; i < TOOL_CALL_LOOP_THRESHOLD + 2; i++) {
171+
expect(service.addAndCheck(event)).toBe(false);
172+
}
173+
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
174+
});
165175
});
166176

167177
describe('Content Loop Detection', () => {
@@ -742,6 +752,7 @@ describe('LoopDetectionService LLM Checks', () => {
742752
mockConfig = {
743753
getGeminiClient: () => mockGeminiClient,
744754
getBaseLlmClient: () => mockBaseLlmClient,
755+
getDisableLoopDetection: () => false,
745756
getDebugMode: () => false,
746757
getTelemetryEnabled: () => true,
747758
getModel: vi.fn().mockReturnValue('cognitive-loop-v1'),

packages/core/src/services/loopDetectionService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export class LoopDetectionService {
147147
* @returns true if a loop is detected, false otherwise
148148
*/
149149
addAndCheck(event: ServerGeminiStreamEvent): boolean {
150-
if (this.disabledForSession) {
150+
if (this.disabledForSession || this.config.getDisableLoopDetection()) {
151151
return false;
152152
}
153153

@@ -182,7 +182,7 @@ export class LoopDetectionService {
182182
* @returns A promise that resolves to `true` if a loop is detected, and `false` otherwise.
183183
*/
184184
async turnStarted(signal: AbortSignal) {
185-
if (this.disabledForSession) {
185+
if (this.disabledForSession || this.config.getDisableLoopDetection()) {
186186
return false;
187187
}
188188
this.turnsInCurrentPrompt++;

schemas/settings.schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,13 @@
457457
"default": 0.5,
458458
"type": "number"
459459
},
460+
"disableLoopDetection": {
461+
"title": "Disable Loop Detection",
462+
"description": "Disable automatic detection and prevention of infinite loops.",
463+
"markdownDescription": "Disable automatic detection and prevention of infinite loops.\n\n- Category: `Model`\n- Requires restart: `yes`\n- Default: `false`",
464+
"default": false,
465+
"type": "boolean"
466+
},
460467
"skipNextSpeakerCheck": {
461468
"title": "Skip Next Speaker Check",
462469
"description": "Skip the next speaker check.",

0 commit comments

Comments
 (0)