feat: Allow cancellation of in-progress Gemini requests and pre-execution checks

- Implements cancellation for Gemini requests while they are actively being processed by the model.
- Extends cancellation support to the  logic within tools. This allows users to cancel operations during the phase where the system is determining if a tool execution requires user confirmation, which can include potentially long-running pre-flight checks or LLM-based corrections.
- Underlying LLM calls for edit corrections (within  and ) and next speaker checks can now also be cancelled.
- Previously, cancellation of the main request was not possible until text started streaming, and pre-execution checks were not cancellable.
- This change leverages the updated SDK's ability to accept an abort token and threads s throughout the request, tool execution, and pre-execution check lifecycle.

Fixes https://github.com/google-gemini/gemini-cli/issues/531
This commit is contained in:
Taylor Mullen
2025-05-27 23:40:25 -07:00
committed by N. Taylor Mullen
parent bfeaac8441
commit f2f2ecf9d8
16 changed files with 260 additions and 61 deletions

View File

@@ -157,7 +157,7 @@ export class GeminiClient {
async *sendMessageStream(
chat: GeminiChat,
request: PartListUnion,
signal?: AbortSignal,
signal: AbortSignal,
turns: number = this.MAX_TURNS,
): AsyncGenerator<ServerGeminiStreamEvent> {
if (!turns) {
@@ -169,8 +169,8 @@ export class GeminiClient {
for await (const event of resultStream) {
yield event;
}
if (!turn.pendingToolCalls.length) {
const nextSpeakerCheck = await checkNextSpeaker(chat, this);
if (!turn.pendingToolCalls.length && signal && !signal.aborted) {
const nextSpeakerCheck = await checkNextSpeaker(chat, this, signal);
if (nextSpeakerCheck?.next_speaker === 'model') {
const nextRequest = [{ text: 'Please continue.' }];
yield* this.sendMessageStream(chat, nextRequest, signal, turns - 1);
@@ -181,6 +181,7 @@ export class GeminiClient {
async generateJson(
contents: Content[],
schema: SchemaUnion,
abortSignal: AbortSignal,
model: string = 'gemini-2.0-flash',
config: GenerateContentConfig = {},
): Promise<Record<string, unknown>> {
@@ -188,6 +189,7 @@ export class GeminiClient {
const userMemory = this.config.getUserMemory();
const systemInstruction = getCoreSystemPrompt(userMemory);
const requestConfig = {
abortSignal,
...this.generateContentConfig,
...config,
};
@@ -232,6 +234,11 @@ export class GeminiClient {
);
}
} catch (error) {
if (abortSignal.aborted) {
// Regular cancellation error, fail normally
throw error;
}
// Avoid double reporting for the empty response case handled above
if (
error instanceof Error &&