From 469092a72cbe368b69df25c0caeefbc911b6d6fd Mon Sep 17 00:00:00 2001 From: cynthialong0-0 <82900738+cynthialong0-0@users.noreply.github.com> Date: Tue, 5 May 2026 17:33:31 -0700 Subject: [PATCH] fix(cli): provide JSON output for AgentExecutionStopped in non-interactive mode (#26504) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/cli/src/nonInteractiveCli.test.ts | 71 +++++++++++++++++++ packages/cli/src/nonInteractiveCli.ts | 14 ++++ .../src/nonInteractiveCliAgentSession.test.ts | 70 ++++++++++++++++++ 3 files changed, 155 insertions(+) diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts index 4cfb6423bb..14d7ae22fb 100644 --- a/packages/cli/src/nonInteractiveCli.test.ts +++ b/packages/cli/src/nonInteractiveCli.test.ts @@ -2045,6 +2045,77 @@ describe('runNonInteractive', () => { expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(1); }); + it('should write JSON output when AgentExecutionStopped event occurs', async () => { + vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON); + vi.spyOn(uiTelemetryService, 'getMetrics').mockReturnValue( + MOCK_SESSION_METRICS, + ); + + const events: ServerGeminiStreamEvent[] = [ + { type: GeminiEventType.Content, value: 'Partial content' }, + { + type: GeminiEventType.AgentExecutionStopped, + value: { reason: 'Stopped by hook' }, + }, + ]; + + mockGeminiClient.sendMessageStream.mockReturnValue( + createStreamFromEvents(events), + ); + + await runNonInteractive({ + config: mockConfig, + settings: mockSettings, + input: 'test stop', + prompt_id: 'prompt-id-stop-json', + }); + + expect(processStdoutSpy).toHaveBeenCalledWith( + JSON.stringify( + { + session_id: 'test-session-id', + response: 'Partial content', + stats: MOCK_SESSION_METRICS, + warnings: ['Agent execution stopped: Stopped by hook'], + }, + null, + 2, + ), + ); + }); + + it('should emit result event when AgentExecutionStopped event occurs in streaming JSON mode', async () => { + vi.mocked(mockConfig.getOutputFormat).mockReturnValue( + OutputFormat.STREAM_JSON, + ); + vi.spyOn(uiTelemetryService, 'getMetrics').mockReturnValue( + MOCK_SESSION_METRICS, + ); + + const events: ServerGeminiStreamEvent[] = [ + { type: GeminiEventType.Content, value: 'Partial content' }, + { + type: GeminiEventType.AgentExecutionStopped, + value: { reason: 'Stopped by hook' }, + }, + ]; + + mockGeminiClient.sendMessageStream.mockReturnValue( + createStreamFromEvents(events), + ); + + await runNonInteractive({ + config: mockConfig, + settings: mockSettings, + input: 'test stop', + prompt_id: 'prompt-id-stop-stream', + }); + + const output = getWrittenOutput(); + expect(output).toContain('"type":"result"'); + expect(output).toContain('"status":"success"'); + }); + it('should handle AgentExecutionBlocked event', async () => { const allEvents: ServerGeminiStreamEvent[] = [ { diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 47de5d9846..29184d45ff 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -400,6 +400,20 @@ export async function runNonInteractive( durationMs, ), }); + } else if (config.getOutputFormat() === OutputFormat.JSON) { + const formatter = new JsonFormatter(); + const stats = uiTelemetryService.getMetrics(); + textOutput.write( + formatter.format( + config.getSessionId(), + responseText, + stats, + undefined, + [...warnings, stopMessage], + ), + ); + } else { + textOutput.ensureTrailingNewline(); // Ensure a final newline } return; } else if (event.type === GeminiEventType.AgentExecutionBlocked) { diff --git a/packages/cli/src/nonInteractiveCliAgentSession.test.ts b/packages/cli/src/nonInteractiveCliAgentSession.test.ts index 1ae71b282f..77920f1879 100644 --- a/packages/cli/src/nonInteractiveCliAgentSession.test.ts +++ b/packages/cli/src/nonInteractiveCliAgentSession.test.ts @@ -2208,6 +2208,76 @@ describe('runNonInteractive', () => { expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(1); }); + it('should write JSON output when AgentExecutionStopped event occurs', async () => { + vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON); + vi.spyOn(uiTelemetryService, 'getMetrics').mockReturnValue( + MOCK_SESSION_METRICS, + ); + + const events: ServerGeminiStreamEvent[] = [ + { type: GeminiEventType.Content, value: 'Partial content' }, + { + type: GeminiEventType.AgentExecutionStopped, + value: { reason: 'Stopped by hook' }, + }, + ]; + + mockGeminiClient.sendMessageStream.mockReturnValue( + createStreamFromEvents(events), + ); + + await runNonInteractive({ + config: mockConfig, + settings: mockSettings, + input: 'test stop', + prompt_id: 'prompt-id-stop-json', + }); + + expect(processStdoutSpy).toHaveBeenCalledWith( + JSON.stringify( + { + session_id: 'test-session-id', + response: 'Partial content', + stats: MOCK_SESSION_METRICS, + }, + null, + 2, + ), + ); + }); + + it('should emit result event when AgentExecutionStopped event occurs in streaming JSON mode', async () => { + vi.mocked(mockConfig.getOutputFormat).mockReturnValue( + OutputFormat.STREAM_JSON, + ); + vi.spyOn(uiTelemetryService, 'getMetrics').mockReturnValue( + MOCK_SESSION_METRICS, + ); + + const events: ServerGeminiStreamEvent[] = [ + { type: GeminiEventType.Content, value: 'Partial content' }, + { + type: GeminiEventType.AgentExecutionStopped, + value: { reason: 'Stopped by hook' }, + }, + ]; + + mockGeminiClient.sendMessageStream.mockReturnValue( + createStreamFromEvents(events), + ); + + await runNonInteractive({ + config: mockConfig, + settings: mockSettings, + input: 'test stop', + prompt_id: 'prompt-id-stop-stream', + }); + + const output = getWrittenOutput(); + expect(output).toContain('"type":"result"'); + expect(output).toContain('"status":"success"'); + }); + it('should handle AgentExecutionBlocked event', async () => { const allEvents: ServerGeminiStreamEvent[] = [ {