mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
feat(core): add setting to disable loop detection (#18008)
This commit is contained in:
@@ -77,6 +77,7 @@ they appear in the UI.
|
|||||||
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
|
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
|
||||||
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
|
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
|
||||||
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
|
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
|
||||||
|
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
|
||||||
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
|
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
|
||||||
|
|
||||||
### Context
|
### Context
|
||||||
|
|||||||
@@ -326,6 +326,12 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
- **Default:** `0.5`
|
- **Default:** `0.5`
|
||||||
- **Requires restart:** Yes
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`model.disableLoopDetection`** (boolean):
|
||||||
|
- **Description:** Disable automatic detection and prevention of infinite
|
||||||
|
loops.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
- **`model.skipNextSpeakerCheck`** (boolean):
|
- **`model.skipNextSpeakerCheck`** (boolean):
|
||||||
- **Description:** Skip the next speaker check.
|
- **Description:** Skip the next speaker check.
|
||||||
- **Default:** `true`
|
- **Default:** `true`
|
||||||
|
|||||||
@@ -762,6 +762,7 @@ export async function loadCliConfig(
|
|||||||
noBrowser: !!process.env['NO_BROWSER'],
|
noBrowser: !!process.env['NO_BROWSER'],
|
||||||
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
summarizeToolOutput: settings.model?.summarizeToolOutput,
|
||||||
ideMode,
|
ideMode,
|
||||||
|
disableLoopDetection: settings.model?.disableLoopDetection,
|
||||||
compressionThreshold: settings.model?.compressionThreshold,
|
compressionThreshold: settings.model?.compressionThreshold,
|
||||||
folderTrust,
|
folderTrust,
|
||||||
interactive,
|
interactive,
|
||||||
|
|||||||
@@ -739,6 +739,16 @@ const SETTINGS_SCHEMA = {
|
|||||||
'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).',
|
'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).',
|
||||||
showInDialog: true,
|
showInDialog: true,
|
||||||
},
|
},
|
||||||
|
disableLoopDetection: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Disable Loop Detection',
|
||||||
|
category: 'Model',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Disable automatic detection and prevention of infinite loops.',
|
||||||
|
showInDialog: true,
|
||||||
|
},
|
||||||
skipNextSpeakerCheck: {
|
skipNextSpeakerCheck: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Skip Next Speaker Check',
|
label: 'Skip Next Speaker Check',
|
||||||
|
|||||||
@@ -393,6 +393,7 @@ export interface ConfigParameters {
|
|||||||
includeDirectories?: string[];
|
includeDirectories?: string[];
|
||||||
bugCommand?: BugCommandSettings;
|
bugCommand?: BugCommandSettings;
|
||||||
model: string;
|
model: string;
|
||||||
|
disableLoopDetection?: boolean;
|
||||||
maxSessionTurns?: number;
|
maxSessionTurns?: number;
|
||||||
experimentalZedIntegration?: boolean;
|
experimentalZedIntegration?: boolean;
|
||||||
listSessions?: boolean;
|
listSessions?: boolean;
|
||||||
@@ -531,6 +532,7 @@ export class Config {
|
|||||||
private readonly cwd: string;
|
private readonly cwd: string;
|
||||||
private readonly bugCommand: BugCommandSettings | undefined;
|
private readonly bugCommand: BugCommandSettings | undefined;
|
||||||
private model: string;
|
private model: string;
|
||||||
|
private readonly disableLoopDetection: boolean;
|
||||||
private previewFeatures: boolean | undefined;
|
private previewFeatures: boolean | undefined;
|
||||||
private hasAccessToPreviewModel: boolean = false;
|
private hasAccessToPreviewModel: boolean = false;
|
||||||
private readonly noBrowser: boolean;
|
private readonly noBrowser: boolean;
|
||||||
@@ -697,6 +699,7 @@ export class Config {
|
|||||||
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
|
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
|
||||||
this.bugCommand = params.bugCommand;
|
this.bugCommand = params.bugCommand;
|
||||||
this.model = params.model;
|
this.model = params.model;
|
||||||
|
this.disableLoopDetection = params.disableLoopDetection ?? false;
|
||||||
this._activeModel = params.model;
|
this._activeModel = params.model;
|
||||||
this.enableAgents = params.enableAgents ?? false;
|
this.enableAgents = params.enableAgents ?? false;
|
||||||
this.agents = params.agents ?? {};
|
this.agents = params.agents ?? {};
|
||||||
@@ -1118,6 +1121,10 @@ export class Config {
|
|||||||
return this.model;
|
return this.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDisableLoopDetection(): boolean {
|
||||||
|
return this.disableLoopDetection ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
setModel(newModel: string, isTemporary: boolean = true): void {
|
setModel(newModel: string, isTemporary: boolean = true): void {
|
||||||
if (this.model !== newModel || this._activeModel !== newModel) {
|
if (this.model !== newModel || this._activeModel !== newModel) {
|
||||||
this.model = newModel;
|
this.model = newModel;
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ describe('Gemini Client (client.ts)', () => {
|
|||||||
getGlobalMemory: vi.fn().mockReturnValue(''),
|
getGlobalMemory: vi.fn().mockReturnValue(''),
|
||||||
getEnvironmentMemory: vi.fn().mockReturnValue(''),
|
getEnvironmentMemory: vi.fn().mockReturnValue(''),
|
||||||
isJitContextEnabled: vi.fn().mockReturnValue(false),
|
isJitContextEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
getDisableLoopDetection: vi.fn().mockReturnValue(false),
|
||||||
|
|
||||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
getProxy: vi.fn().mockReturnValue(undefined),
|
getProxy: vi.fn().mockReturnValue(undefined),
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ describe('LoopDetectionService', () => {
|
|||||||
mockConfig = {
|
mockConfig = {
|
||||||
getTelemetryEnabled: () => true,
|
getTelemetryEnabled: () => true,
|
||||||
isInteractive: () => false,
|
isInteractive: () => false,
|
||||||
|
getDisableLoopDetection: () => false,
|
||||||
getModelAvailabilityService: vi
|
getModelAvailabilityService: vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(createAvailabilityServiceMock()),
|
.mockReturnValue(createAvailabilityServiceMock()),
|
||||||
@@ -162,6 +163,15 @@ describe('LoopDetectionService', () => {
|
|||||||
// Should now return false even though a loop was previously detected
|
// Should now return false even though a loop was previously detected
|
||||||
expect(service.addAndCheck(event)).toBe(false);
|
expect(service.addAndCheck(event)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should skip loop detection if disabled in config', () => {
|
||||||
|
vi.spyOn(mockConfig, 'getDisableLoopDetection').mockReturnValue(true);
|
||||||
|
const event = createToolCallRequestEvent('testTool', { param: 'value' });
|
||||||
|
for (let i = 0; i < TOOL_CALL_LOOP_THRESHOLD + 2; i++) {
|
||||||
|
expect(service.addAndCheck(event)).toBe(false);
|
||||||
|
}
|
||||||
|
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Content Loop Detection', () => {
|
describe('Content Loop Detection', () => {
|
||||||
@@ -742,6 +752,7 @@ describe('LoopDetectionService LLM Checks', () => {
|
|||||||
mockConfig = {
|
mockConfig = {
|
||||||
getGeminiClient: () => mockGeminiClient,
|
getGeminiClient: () => mockGeminiClient,
|
||||||
getBaseLlmClient: () => mockBaseLlmClient,
|
getBaseLlmClient: () => mockBaseLlmClient,
|
||||||
|
getDisableLoopDetection: () => false,
|
||||||
getDebugMode: () => false,
|
getDebugMode: () => false,
|
||||||
getTelemetryEnabled: () => true,
|
getTelemetryEnabled: () => true,
|
||||||
getModel: vi.fn().mockReturnValue('cognitive-loop-v1'),
|
getModel: vi.fn().mockReturnValue('cognitive-loop-v1'),
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export class LoopDetectionService {
|
|||||||
* @returns true if a loop is detected, false otherwise
|
* @returns true if a loop is detected, false otherwise
|
||||||
*/
|
*/
|
||||||
addAndCheck(event: ServerGeminiStreamEvent): boolean {
|
addAndCheck(event: ServerGeminiStreamEvent): boolean {
|
||||||
if (this.disabledForSession) {
|
if (this.disabledForSession || this.config.getDisableLoopDetection()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ export class LoopDetectionService {
|
|||||||
* @returns A promise that resolves to `true` if a loop is detected, and `false` otherwise.
|
* @returns A promise that resolves to `true` if a loop is detected, and `false` otherwise.
|
||||||
*/
|
*/
|
||||||
async turnStarted(signal: AbortSignal) {
|
async turnStarted(signal: AbortSignal) {
|
||||||
if (this.disabledForSession) {
|
if (this.disabledForSession || this.config.getDisableLoopDetection()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.turnsInCurrentPrompt++;
|
this.turnsInCurrentPrompt++;
|
||||||
|
|||||||
@@ -457,6 +457,13 @@
|
|||||||
"default": 0.5,
|
"default": 0.5,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
"disableLoopDetection": {
|
||||||
|
"title": "Disable Loop Detection",
|
||||||
|
"description": "Disable automatic detection and prevention of infinite loops.",
|
||||||
|
"markdownDescription": "Disable automatic detection and prevention of infinite loops.\n\n- Category: `Model`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"skipNextSpeakerCheck": {
|
"skipNextSpeakerCheck": {
|
||||||
"title": "Skip Next Speaker Check",
|
"title": "Skip Next Speaker Check",
|
||||||
"description": "Skip the next speaker check.",
|
"description": "Skip the next speaker check.",
|
||||||
|
|||||||
Reference in New Issue
Block a user