Add support for specifying maxSessionTurns via the settings configuration (#3507)

This commit is contained in:
anj-s
2025-07-11 07:55:03 -07:00
committed by GitHub
parent 0151a9e1a3
commit c9e1e6d3bd
10 changed files with 231 additions and 15 deletions

View File

@@ -312,6 +312,7 @@ export async function loadCliConfig(
bugCommand: settings.bugCommand,
model: argv.model!,
extensionContextFilePaths,
maxSessionTurns: settings.maxSessionTurns ?? -1,
listExtensions: argv.listExtensions || false,
activeExtensions: activeExtensions.map((e) => ({
name: e.config.name,

View File

@@ -80,6 +80,9 @@ export interface Settings {
hideWindowTitle?: boolean;
hideTips?: boolean;
// Setting for setting maximum number of user/model/tool turns in a session.
maxSessionTurns?: number;
// Add other settings here.
}

View File

@@ -53,6 +53,7 @@ describe('runNonInteractive', () => {
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
getContentGeneratorConfig: vi.fn().mockReturnValue({}),
getMaxSessionTurns: vi.fn().mockReturnValue(10),
initialize: vi.fn(),
} as unknown as Config;
@@ -294,4 +295,50 @@ describe('runNonInteractive', () => {
'Unfortunately the tool does not exist.',
);
});
it('should exit when max session turns are exceeded', async () => {
const functionCall: FunctionCall = {
id: 'fcLoop',
name: 'loopTool',
args: {},
};
const toolResponsePart: Part = {
functionResponse: {
name: 'loopTool',
id: 'fcLoop',
response: { result: 'still looping' },
},
};
// Config with a max turn of 1
vi.mocked(mockConfig.getMaxSessionTurns).mockReturnValue(1);
const { executeToolCall: mockCoreExecuteToolCall } = await import(
'@google/gemini-cli-core'
);
vi.mocked(mockCoreExecuteToolCall).mockResolvedValue({
callId: 'fcLoop',
responseParts: [toolResponsePart],
resultDisplay: 'Still looping',
error: undefined,
});
const stream = (async function* () {
yield { functionCalls: [functionCall] } as GenerateContentResponse;
})();
mockChat.sendMessageStream.mockResolvedValue(stream);
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
await runNonInteractive(mockConfig, 'Trigger loop');
expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
`
Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.`,
);
expect(mockProcessExit).not.toHaveBeenCalled();
});
});

View File

@@ -63,9 +63,19 @@ export async function runNonInteractive(
const chat = await geminiClient.getChat();
const abortController = new AbortController();
let currentMessages: Content[] = [{ role: 'user', parts: [{ text: input }] }];
let turnCount = 0;
try {
while (true) {
turnCount++;
if (
config.getMaxSessionTurns() > 0 &&
turnCount > config.getMaxSessionTurns()
) {
console.error(
'\n Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
);
return;
}
const functionCalls: FunctionCall[] = [];
const responseStream = await chat.sendMessageStream(

View File

@@ -431,6 +431,20 @@ export const useGeminiStream = (
[addItem, config],
);
const handleMaxSessionTurnsEvent = useCallback(
() =>
addItem(
{
type: 'info',
text:
`The session has reached the maximum number of turns: ${config.getMaxSessionTurns()}. ` +
`Please update this limit in your setting.json file.`,
},
Date.now(),
),
[addItem, config],
);
const processGeminiStreamEvents = useCallback(
async (
stream: AsyncIterable<GeminiEvent>,
@@ -467,6 +481,9 @@ export const useGeminiStream = (
case ServerGeminiEventType.ToolCallResponse:
// do nothing
break;
case ServerGeminiEventType.MaxSessionTurns:
handleMaxSessionTurnsEvent();
break;
default: {
// enforces exhaustive switch-case
const unreachable: never = event;
@@ -485,6 +502,7 @@ export const useGeminiStream = (
handleErrorEvent,
scheduleToolCalls,
handleChatCompressionEvent,
handleMaxSessionTurnsEvent,
],
);