diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 059b72437f..4dee75b93b 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -728,6 +728,23 @@ describe('Gemini Client (client.ts)', () => { ); }); + it('yields UserCancelled when processTurn throws AbortError', async () => { + const abortError = new Error('Aborted'); + abortError.name = 'AbortError'; + vi.spyOn(client['loopDetector'], 'turnStarted').mockRejectedValueOnce( + abortError, + ); + + const stream = client.sendMessageStream( + [{ text: 'Hi' }], + new AbortController().signal, + 'prompt-id-abort-error', + ); + const events = await fromAsync(stream); + + expect(events).toEqual([{ type: GeminiEventType.UserCancelled }]); + }); + it.each([ { compressionStatus: diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 14e2f42bc3..2ab75cf365 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -34,7 +34,7 @@ import { type RetryAvailabilityContext, } from '../utils/retry.js'; import type { ValidationRequiredError } from '../utils/googleQuotaErrors.js'; -import { getErrorMessage } from '../utils/errors.js'; +import { getErrorMessage, isAbortError } from '../utils/errors.js'; import { tokenLimit } from './tokenLimits.js'; import type { ChatRecordingService, @@ -957,6 +957,12 @@ export class GeminiClient { ); } } + } catch (error) { + if (signal?.aborted || isAbortError(error)) { + yield { type: GeminiEventType.UserCancelled }; + return turn; + } + throw error; } finally { const hookState = this.hookStateMap.get(prompt_id); if (hookState) {