| |
| |
| |
| |
| |
|
|
| import type { |
| Config, |
| ToolCallRequestInfo, |
| ResumedSessionData, |
| UserFeedbackPayload, |
| } from '@google/gemini-cli-core'; |
| import { isSlashCommand } from './ui/utils/commandUtils.js'; |
| import type { LoadedSettings } from './config/settings.js'; |
| import { |
| convertSessionToClientHistory, |
| GeminiEventType, |
| FatalInputError, |
| promptIdContext, |
| OutputFormat, |
| JsonFormatter, |
| StreamJsonFormatter, |
| JsonStreamEventType, |
| uiTelemetryService, |
| debugLogger, |
| coreEvents, |
| CoreEvent, |
| createWorkingStdio, |
| recordToolCallInteractions, |
| ToolErrorType, |
| Scheduler, |
| ROOT_SCHEDULER_ID, |
| THINKING_ONLY_COMPRESS_SUGGESTION, |
| MAX_TOKENS_EXCEEDED_SUGGESTION, |
| SAFETY_BLOCKED_MESSAGE, |
| RECITATION_BLOCKED_MESSAGE, |
| OTHER_BLOCKED_MESSAGE, |
| TRUE_EMPTY_RESPONSE_MESSAGE, |
| } from '@google/gemini-cli-core'; |
|
|
| import type { Content, Part } from '@google/genai'; |
| import readline from 'node:readline'; |
| import stripAnsi from 'strip-ansi'; |
|
|
| import { handleSlashCommand } from './nonInteractiveCliCommands.js'; |
| import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; |
| import { handleAtCommand } from './ui/hooks/atCommandProcessor.js'; |
| import { |
| handleError, |
| handleToolError, |
| handleCancellationError, |
| handleMaxTurnsExceededError, |
| } from './utils/errors.js'; |
| import { TextOutput } from './ui/utils/textOutput.js'; |
| import { runNonInteractive as runNonInteractiveAgentSession } from './nonInteractiveCliAgentSession.js'; |
|
|
| interface RunNonInteractiveParams { |
| config: Config; |
| settings: LoadedSettings; |
| input: string; |
| prompt_id: string; |
| resumedSessionData?: ResumedSessionData; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function runNonInteractive( |
| params: RunNonInteractiveParams, |
| ): Promise<void> { |
| const useAgentSession = params.config.getAgentSessionNoninteractiveEnabled(); |
| if (useAgentSession) { |
| debugLogger.debug( |
| '[ADK] Running non-interactive mode with ADK agent session', |
| ); |
| return runNonInteractiveAgentSession(params); |
| } |
|
|
| const { config, settings, input, prompt_id, resumedSessionData } = params; |
|
|
| return promptIdContext.run(prompt_id, async () => { |
| const consolePatcher = new ConsolePatcher({ |
| stderr: true, |
| interactive: false, |
| debugMode: config.getDebugMode(), |
| onNewMessage: (msg) => { |
| coreEvents.emitConsoleLog(msg.type, msg.content); |
| }, |
| }); |
|
|
| if (process.env['GEMINI_CLI_ACTIVITY_LOG_TARGET']) { |
| const { setupInitialActivityLogger } = await import( |
| './utils/devtoolsService.js' |
| ); |
| setupInitialActivityLogger(config); |
| } |
|
|
| const { stdout: workingStdout } = createWorkingStdio(); |
| const textOutput = new TextOutput(workingStdout); |
|
|
| const handleUserFeedback = (payload: UserFeedbackPayload) => { |
| const prefix = payload.severity.toUpperCase(); |
| process.stderr.write(`[${prefix}] ${payload.message}\n`); |
| if (payload.error && config.getDebugMode()) { |
| const errorToLog = |
| payload.error instanceof Error |
| ? payload.error.stack || payload.error.message |
| : String(payload.error); |
| process.stderr.write(`${errorToLog}\n`); |
| } |
| }; |
|
|
| const startTime = Date.now(); |
| const streamFormatter = |
| config.getOutputFormat() === OutputFormat.STREAM_JSON |
| ? new StreamJsonFormatter() |
| : null; |
|
|
| const abortController = new AbortController(); |
|
|
| |
| let isAborting = false; |
| let cancelMessageTimer: NodeJS.Timeout | null = null; |
|
|
| |
| let stdinWasRaw = false; |
| let rl: readline.Interface | null = null; |
|
|
| const setupStdinCancellation = () => { |
| |
| if (!process.stdin.isTTY) { |
| return; |
| } |
|
|
| |
| stdinWasRaw = process.stdin.isRaw || false; |
|
|
| |
| process.stdin.setRawMode(true); |
| process.stdin.resume(); |
|
|
| |
| rl = readline.createInterface({ |
| input: process.stdin, |
| escapeCodeTimeout: 0, |
| }); |
| readline.emitKeypressEvents(process.stdin, rl); |
|
|
| |
| const keypressHandler = ( |
| str: string, |
| key: { name?: string; ctrl?: boolean }, |
| ) => { |
| |
| if ((key && key.ctrl && key.name === 'c') || str === '\u0003') { |
| |
| if (isAborting) { |
| return; |
| } |
|
|
| isAborting = true; |
|
|
| |
| |
| cancelMessageTimer = setTimeout(() => { |
| process.stderr.write('\nCancelling...\n'); |
| }, 200); |
|
|
| abortController.abort(); |
| |
| |
| } |
| }; |
|
|
| process.stdin.on('keypress', keypressHandler); |
| }; |
|
|
| const cleanupStdinCancellation = () => { |
| |
| if (cancelMessageTimer) { |
| clearTimeout(cancelMessageTimer); |
| cancelMessageTimer = null; |
| } |
|
|
| |
| if (rl) { |
| rl.close(); |
| rl = null; |
| } |
|
|
| |
| process.stdin.removeAllListeners('keypress'); |
|
|
| |
| if (process.stdin.isTTY) { |
| process.stdin.setRawMode(stdinWasRaw); |
| process.stdin.pause(); |
| } |
| }; |
|
|
| let errorToHandle: unknown | undefined; |
| let scheduler: Scheduler | undefined; |
| try { |
| consolePatcher.patch(); |
|
|
| if ( |
| config.getRawOutput() && |
| !config.getAcceptRawOutputRisk() && |
| config.getOutputFormat() === OutputFormat.TEXT |
| ) { |
| process.stderr.write( |
| '[WARNING] --raw-output is enabled. Model output is not sanitized and may contain harmful ANSI sequences (e.g. for phishing or command injection). Use --accept-raw-output-risk to suppress this warning.\n', |
| ); |
| } |
|
|
| |
| setupStdinCancellation(); |
|
|
| coreEvents.on(CoreEvent.UserFeedback, handleUserFeedback); |
| coreEvents.drainBacklogs(); |
|
|
| |
| process.stdout.on('error', (err: NodeJS.ErrnoException) => { |
| if (err.code === 'EPIPE') { |
| |
| process.exit(0); |
| } |
| }); |
|
|
| const geminiClient = config.getGeminiClient(); |
| scheduler = new Scheduler({ |
| context: config, |
| messageBus: config.getMessageBus(), |
| getPreferredEditor: () => undefined, |
| schedulerId: ROOT_SCHEDULER_ID, |
| }); |
|
|
| |
| if (resumedSessionData) { |
| await geminiClient.resumeChat( |
| convertSessionToClientHistory( |
| resumedSessionData.conversation.messages, |
| ), |
| resumedSessionData, |
| ); |
| } |
|
|
| |
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.INIT, |
| timestamp: new Date().toISOString(), |
| session_id: config.getSessionId(), |
| model: config.getModel(), |
| }); |
| } |
|
|
| let query: Part[] | undefined; |
|
|
| if (isSlashCommand(input)) { |
| const slashCommandResult = await handleSlashCommand( |
| input, |
| abortController, |
| config, |
| settings, |
| ); |
| |
| |
| |
| if (slashCommandResult) { |
| |
| query = slashCommandResult as Part[]; |
| } |
| } |
|
|
| if (!query) { |
| const { processedQuery, error } = await handleAtCommand({ |
| query: input, |
| config, |
| addItem: (_item, _timestamp) => 0, |
| onDebugMessage: () => {}, |
| messageId: Date.now(), |
| signal: abortController.signal, |
| escapePastedAtSymbols: false, |
| }); |
| if (error || !processedQuery) { |
| |
| |
| throw new FatalInputError( |
| error || 'Exiting due to an error processing the @ command.', |
| ); |
| } |
| |
| query = processedQuery as Part[]; |
| } |
|
|
| |
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.MESSAGE, |
| timestamp: new Date().toISOString(), |
| role: 'user', |
| content: input, |
| }); |
| } |
|
|
| let currentMessages: Content[] = [{ role: 'user', parts: query }]; |
|
|
| let turnCount = 0; |
| let invalidStreamError: string | undefined; |
| const warnings: string[] = []; |
| while (true) { |
| turnCount++; |
| if ( |
| config.getMaxSessionTurns() >= 0 && |
| turnCount > config.getMaxSessionTurns() |
| ) { |
| handleMaxTurnsExceededError(config); |
| } |
| const toolCallRequests: ToolCallRequestInfo[] = []; |
|
|
| const responseStream = geminiClient.sendMessageStream( |
| currentMessages[0]?.parts || [], |
| abortController.signal, |
| prompt_id, |
| undefined, |
| turnCount === 1 ? input : undefined, |
| ); |
|
|
| let responseText = ''; |
| for await (const event of responseStream) { |
| if (abortController.signal.aborted) { |
| handleCancellationError(config); |
| } |
|
|
| if (event.type === GeminiEventType.Content) { |
| const isRaw = |
| config.getRawOutput() || config.getAcceptRawOutputRisk(); |
| const output = isRaw ? event.value : stripAnsi(event.value); |
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.MESSAGE, |
| timestamp: new Date().toISOString(), |
| role: 'assistant', |
| content: output, |
| delta: true, |
| }); |
| } else if (config.getOutputFormat() === OutputFormat.JSON) { |
| responseText += output; |
| } else { |
| if (event.value) { |
| textOutput.write(output); |
| } |
| } |
| } else if (event.type === GeminiEventType.ToolCallRequest) { |
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.TOOL_USE, |
| timestamp: new Date().toISOString(), |
| tool_name: event.value.name, |
| tool_id: event.value.callId, |
| parameters: event.value.args, |
| }); |
| } |
| toolCallRequests.push(event.value); |
| } else if (event.type === GeminiEventType.LoopDetected) { |
| const message = 'Loop detected, stopping execution'; |
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.ERROR, |
| timestamp: new Date().toISOString(), |
| severity: 'warning', |
| message, |
| }); |
| } |
| warnings.push(message); |
| } else if (event.type === GeminiEventType.MaxSessionTurns) { |
| const message = 'Maximum session turns exceeded'; |
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.ERROR, |
| timestamp: new Date().toISOString(), |
| severity: 'error', |
| message, |
| }); |
| } |
| warnings.push(message); |
| } else if (event.type === GeminiEventType.Error) { |
| throw event.value.error; |
| } else if (event.type === GeminiEventType.AgentExecutionStopped) { |
| const stopMessage = `Agent execution stopped: ${event.value.systemMessage?.trim() || event.value.reason}`; |
| if (config.getOutputFormat() === OutputFormat.TEXT) { |
| process.stderr.write(`${stopMessage}\n`); |
| } |
| |
| if (streamFormatter) { |
| const metrics = uiTelemetryService.getMetrics(); |
| const durationMs = Date.now() - startTime; |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.RESULT, |
| timestamp: new Date().toISOString(), |
| status: 'success', |
| stats: streamFormatter.convertToStreamStats( |
| metrics, |
| 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(); |
| } |
| return; |
| } else if (event.type === GeminiEventType.AgentExecutionBlocked) { |
| const blockMessage = `Agent execution blocked: ${event.value.systemMessage?.trim() || event.value.reason}`; |
| if (config.getOutputFormat() === OutputFormat.TEXT) { |
| process.stderr.write(`[WARNING] ${blockMessage}\n`); |
| } else if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.ERROR, |
| timestamp: new Date().toISOString(), |
| severity: 'warning', |
| message: stripAnsi(blockMessage), |
| }); |
| } |
| warnings.push(blockMessage); |
| } else if (event.type === GeminiEventType.InvalidStream) { |
| const eventValue = event.value; |
| if (eventValue?.type === 'NO_RESPONSE_TEXT') { |
| invalidStreamError = TRUE_EMPTY_RESPONSE_MESSAGE; |
| } else if (eventValue?.type === 'THINKING_ONLY_RESPONSE') { |
| invalidStreamError = THINKING_ONLY_COMPRESS_SUGGESTION; |
| } else if (eventValue?.type === 'MAX_TOKENS_EXCEEDED') { |
| invalidStreamError = MAX_TOKENS_EXCEEDED_SUGGESTION; |
| } else if (eventValue?.type === 'SAFETY_BLOCKED') { |
| invalidStreamError = SAFETY_BLOCKED_MESSAGE; |
| } else if (eventValue?.type === 'RECITATION_BLOCKED') { |
| invalidStreamError = RECITATION_BLOCKED_MESSAGE; |
| } else if (eventValue?.type === 'OTHER_BLOCKED') { |
| invalidStreamError = OTHER_BLOCKED_MESSAGE; |
| } else { |
| invalidStreamError = |
| eventValue?.message?.trim() || |
| 'Invalid stream: The model returned an empty response or malformed tool call.'; |
| } |
|
|
| |
| uiTelemetryService.recordSemanticValidationError( |
| geminiClient.getCurrentSequenceModel() ?? config.getModel(), |
| eventValue?.type || 'INVALID_STREAM', |
| ); |
|
|
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.ERROR, |
| timestamp: new Date().toISOString(), |
| severity: 'error', |
| message: invalidStreamError, |
| }); |
| } else if (config.getOutputFormat() === OutputFormat.TEXT) { |
| process.stderr.write(`[ERROR] ${invalidStreamError}\n`); |
| } |
| toolCallRequests.length = 0; |
| break; |
| } |
| } |
|
|
| if (toolCallRequests.length > 0) { |
| textOutput.ensureTrailingNewline(); |
| const completedToolCalls = await scheduler.schedule( |
| toolCallRequests, |
| abortController.signal, |
| ); |
| const toolResponseParts: Part[] = []; |
|
|
| for (const completedToolCall of completedToolCalls) { |
| const toolResponse = completedToolCall.response; |
| const requestInfo = completedToolCall.request; |
|
|
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.TOOL_RESULT, |
| timestamp: new Date().toISOString(), |
| tool_id: requestInfo.callId, |
| status: |
| completedToolCall.status === 'error' ? 'error' : 'success', |
| output: |
| typeof toolResponse.resultDisplay === 'string' |
| ? toolResponse.resultDisplay |
| : undefined, |
| error: toolResponse.error |
| ? { |
| type: toolResponse.errorType || 'TOOL_EXECUTION_ERROR', |
| message: toolResponse.error.message, |
| } |
| : undefined, |
| }); |
| } |
|
|
| if (toolResponse.error) { |
| handleToolError( |
| requestInfo.name, |
| toolResponse.error, |
| config, |
| toolResponse.errorType || 'TOOL_EXECUTION_ERROR', |
| typeof toolResponse.resultDisplay === 'string' |
| ? toolResponse.resultDisplay |
| : undefined, |
| ); |
| } |
|
|
| if (toolResponse.responseParts) { |
| toolResponseParts.push(...toolResponse.responseParts); |
| } |
| } |
|
|
| |
| try { |
| const currentModel = |
| geminiClient.getCurrentSequenceModel() ?? config.getModel(); |
| geminiClient |
| .getChat() |
| .recordCompletedToolCalls(currentModel, completedToolCalls); |
|
|
| await recordToolCallInteractions(config, completedToolCalls); |
| } catch (error) { |
| debugLogger.error( |
| `Error recording completed tool call information: ${error}`, |
| ); |
| } |
|
|
| |
| const stopExecutionTool = completedToolCalls.find( |
| (tc) => tc.response.errorType === ToolErrorType.STOP_EXECUTION, |
| ); |
|
|
| if (stopExecutionTool && stopExecutionTool.response.error) { |
| const stopMessage = `Agent execution stopped: ${stopExecutionTool.response.error.message}`; |
|
|
| if (config.getOutputFormat() === OutputFormat.TEXT) { |
| process.stderr.write(`${stopMessage}\n`); |
| } |
|
|
| |
| if (streamFormatter) { |
| const metrics = uiTelemetryService.getMetrics(); |
| const durationMs = Date.now() - startTime; |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.RESULT, |
| timestamp: new Date().toISOString(), |
| status: 'success', |
| stats: streamFormatter.convertToStreamStats( |
| metrics, |
| 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, |
| ), |
| ); |
| } else { |
| textOutput.ensureTrailingNewline(); |
| } |
| return; |
| } |
|
|
| currentMessages = [{ role: 'user', parts: toolResponseParts }]; |
| } else { |
| |
| if (streamFormatter) { |
| const metrics = uiTelemetryService.getMetrics(); |
| const durationMs = Date.now() - startTime; |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.RESULT, |
| timestamp: new Date().toISOString(), |
| status: invalidStreamError ? 'error' : 'success', |
| stats: streamFormatter.convertToStreamStats(metrics, durationMs), |
| }); |
| } else if (config.getOutputFormat() === OutputFormat.JSON) { |
| const formatter = new JsonFormatter(); |
| const stats = uiTelemetryService.getMetrics(); |
| textOutput.write( |
| formatter.format( |
| config.getSessionId(), |
| responseText, |
| stats, |
| invalidStreamError |
| ? { type: 'INVALID_STREAM', message: invalidStreamError } |
| : undefined, |
| warnings, |
| ), |
| ); |
| } else { |
| textOutput.ensureTrailingNewline(); |
| } |
| return; |
| } |
| } |
| } catch (error) { |
| errorToHandle = error; |
| } finally { |
| |
| cleanupStdinCancellation(); |
|
|
| scheduler?.dispose(); |
| consolePatcher.cleanup(); |
| coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback); |
| } |
|
|
| if (errorToHandle) { |
| handleError(errorToHandle, config); |
| } |
| }); |
| } |
|
|