Skip to content

Commit a23585f

Browse files
SandyTao520sidwan02
authored andcommitted
feat(core): add setting to disable loop detection (google-gemini#18008)
1 parent 62379a1 commit a23585f

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
@@ -401,6 +401,7 @@ export interface ConfigParameters {
401401
includeDirectories?: string[];
402402
bugCommand?: BugCommandSettings;
403403
model: string;
404+
disableLoopDetection?: boolean;
404405
maxSessionTurns?: number;
405406
experimentalZedIntegration?: boolean;
406407
listSessions?: boolean;
@@ -540,6 +541,7 @@ export class Config {
540541
private readonly cwd: string;
541542
private readonly bugCommand: BugCommandSettings | undefined;
542543
private model: string;
544+
private readonly disableLoopDetection: boolean;
543545
private previewFeatures: boolean | undefined;
544546
private hasAccessToPreviewModel: boolean = false;
545547
private readonly noBrowser: boolean;
@@ -707,6 +709,7 @@ export class Config {
707709
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
708710
this.bugCommand = params.bugCommand;
709711
this.model = params.model;
712+
this.disableLoopDetection = params.disableLoopDetection ?? false;
710713
this._activeModel = params.model;
711714
this.enableAgents = params.enableAgents ?? false;
712715
this.agents = params.agents ?? {};
@@ -1137,6 +1140,10 @@ export class Config {
11371140
return this.model;
11381141
}
11391142

1143+
getDisableLoopDetection(): boolean {
1144+
return this.disableLoopDetection ?? false;
1145+
}
1146+
11401147
setModel(newModel: string, isTemporary: boolean = true): void {
11411148
if (this.model !== newModel || this._activeModel !== newModel) {
11421149
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)