diff --git a/packages/cli/src/acp/acpClient.test.ts b/packages/cli/src/acp/acpClient.test.ts index 9e4b89ea20..14295954dd 100644 --- a/packages/cli/src/acp/acpClient.test.ts +++ b/packages/cli/src/acp/acpClient.test.ts @@ -28,6 +28,7 @@ import { LlmRole, type GitService, processSingleFileContent, + InvalidStreamError, } from '@google/gemini-cli-core'; import { SettingScope, @@ -785,6 +786,32 @@ describe('Session', () => { expect(result).toMatchObject({ stopReason: 'end_turn' }); }); + it('should handle prompt with empty response (InvalidStreamError)', async () => { + mockChat.sendMessageStream.mockRejectedValue( + new InvalidStreamError('Empty response', 'NO_RESPONSE_TEXT'), + ); + + const result = await session.prompt({ + sessionId: 'session-1', + prompt: [{ type: 'text', text: 'Hi' }], + }); + + expect(mockChat.sendMessageStream).toHaveBeenCalled(); + expect(result).toMatchObject({ stopReason: 'end_turn' }); + }); + + it('should handle prompt with empty response (NO_RESPONSE_TEXT anomaly)', async () => { + mockChat.sendMessageStream.mockRejectedValue({ type: 'NO_RESPONSE_TEXT' }); + + const result = await session.prompt({ + sessionId: 'session-1', + prompt: [{ type: 'text', text: 'Hi' }], + }); + + expect(mockChat.sendMessageStream).toHaveBeenCalled(); + expect(result).toMatchObject({ stopReason: 'end_turn' }); + }); + it('should handle /memory command', async () => { const handleCommandSpy = vi .spyOn( diff --git a/packages/cli/src/acp/acpClient.ts b/packages/cli/src/acp/acpClient.ts index 59c6cb2b3f..6b76ffdc7a 100644 --- a/packages/cli/src/acp/acpClient.ts +++ b/packages/cli/src/acp/acpClient.ts @@ -48,6 +48,7 @@ import { PREVIEW_GEMINI_MODEL_AUTO, getDisplayString, processSingleFileContent, + InvalidStreamError, type AgentLoopContext, updatePolicy, } from '@google/gemini-cli-core'; @@ -851,6 +852,37 @@ export class Session { return { stopReason: CoreToolCallStatus.Cancelled }; } + if ( + error instanceof InvalidStreamError || + (error && + typeof error === 'object' && + 'type' in error && + error.type === 'NO_RESPONSE_TEXT') + ) { + // The stream ended with an empty response or malformed tool call. + // Treat this as a graceful end to the model's turn rather than a crash. + return { + stopReason: 'end_turn', + _meta: { + quota: { + token_count: { + input_tokens: totalInputTokens, + output_tokens: totalOutputTokens, + }, + model_usage: Array.from(modelUsageMap.entries()).map( + ([modelName, counts]) => ({ + model: modelName, + token_count: { + input_tokens: counts.input, + output_tokens: counts.output, + }, + }), + ), + }, + }, + }; + } + throw new acp.RequestError( getErrorStatus(error) || 500, getAcpErrorMessage(error),