| |
| |
| |
| |
| |
|
|
| import type { |
| Config, |
| ResumedSessionData, |
| UserFeedbackPayload, |
| AgentEvent, |
| ContentPart, |
| } from '@google/gemini-cli-core'; |
| import { isSlashCommand } from './ui/utils/commandUtils.js'; |
| import type { LoadedSettings } from './config/settings.js'; |
| import { |
| convertSessionToClientHistory, |
| FatalError, |
| FatalAuthenticationError, |
| FatalInputError, |
| FatalSandboxError, |
| FatalConfigError, |
| FatalTurnLimitedError, |
| FatalToolExecutionError, |
| FatalCancellationError, |
| promptIdContext, |
| OutputFormat, |
| JsonFormatter, |
| StreamJsonFormatter, |
| JsonStreamEventType, |
| uiTelemetryService, |
| coreEvents, |
| CoreEvent, |
| createWorkingStdio, |
| Scheduler, |
| ROOT_SCHEDULER_ID, |
| LegacyAgentSession, |
| ToolErrorType, |
| geminiPartsToContentParts, |
| displayContentToString, |
| debugLogger, |
| 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 { 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 } from './utils/errors.js'; |
| import { TextOutput } from './ui/utils/textOutput.js'; |
|
|
| interface RunNonInteractiveParams { |
| config: Config; |
| settings: LoadedSettings; |
| input: string; |
| prompt_id: string; |
| resumedSessionData?: ResumedSessionData; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function runNonInteractive({ |
| config, |
| settings, |
| input, |
| prompt_id, |
| resumedSessionData, |
| }: RunNonInteractiveParams): Promise<void> { |
| 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; |
| let abortSession = () => {}; |
| 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') { |
| |
| cleanupStdinCancellation(); |
| consolePatcher.cleanup(); |
| 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, |
| }); |
| } |
|
|
| |
| const session = new LegacyAgentSession({ |
| client: geminiClient, |
| scheduler, |
| config, |
| promptId: prompt_id, |
| }); |
|
|
| |
| abortSession = () => { |
| void session.abort(); |
| }; |
| abortController.signal.addEventListener('abort', abortSession); |
| if (abortController.signal.aborted) { |
| throw new FatalCancellationError('Operation cancelled.'); |
| } |
|
|
| |
| const { streamId } = await session.send({ |
| message: { |
| content: geminiPartsToContentParts(query), |
| displayContent: input, |
| }, |
| }); |
| if (streamId === null) { |
| throw new Error( |
| 'LegacyAgentSession.send() unexpectedly returned no stream for a message send.', |
| ); |
| } |
|
|
| const getTextContent = (parts?: ContentPart[]): string | undefined => { |
| const text = parts |
| ?.map((part) => (part.type === 'text' ? part.text : '')) |
| .join(''); |
| return text ? text : undefined; |
| }; |
|
|
| const emitFinalResult = (errorPayload?: { |
| type: string; |
| message: string; |
| }): void => { |
| if (streamFormatter) { |
| const metrics = uiTelemetryService.getMetrics(); |
| const durationMs = Date.now() - startTime; |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.RESULT, |
| timestamp: new Date().toISOString(), |
| status: errorPayload ? '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, |
| errorPayload, |
| warnings, |
| ), |
| ); |
| } else { |
| textOutput.ensureTrailingNewline(); |
| } |
| }; |
|
|
| const reconstructFatalError = (event: AgentEvent<'error'>): Error => { |
| const errorMeta = event._meta; |
| const name = |
| typeof errorMeta?.['errorName'] === 'string' |
| ? errorMeta['errorName'] |
| : undefined; |
|
|
| let errToThrow: Error; |
| switch (name) { |
| case 'FatalAuthenticationError': |
| errToThrow = new FatalAuthenticationError(event.message); |
| break; |
| case 'FatalInputError': |
| errToThrow = new FatalInputError(event.message); |
| break; |
| case 'FatalSandboxError': |
| errToThrow = new FatalSandboxError(event.message); |
| break; |
| case 'FatalConfigError': |
| errToThrow = new FatalConfigError(event.message); |
| break; |
| case 'FatalTurnLimitedError': |
| errToThrow = new FatalTurnLimitedError(event.message); |
| break; |
| case 'FatalToolExecutionError': |
| errToThrow = new FatalToolExecutionError(event.message); |
| break; |
| case 'FatalCancellationError': |
| errToThrow = new FatalCancellationError(event.message); |
| break; |
| case 'FatalError': |
| errToThrow = new FatalError( |
| event.message, |
| typeof errorMeta?.['exitCode'] === 'number' |
| ? errorMeta['exitCode'] |
| : 1, |
| ); |
| break; |
| default: |
| errToThrow = new Error(event.message); |
| if (name) { |
| Object.defineProperty(errToThrow, 'name', { |
| value: name, |
| enumerable: true, |
| }); |
| } |
| break; |
| } |
|
|
| if (errorMeta?.['exitCode'] !== undefined) { |
| Object.defineProperty(errToThrow, 'exitCode', { |
| value: errorMeta['exitCode'], |
| enumerable: true, |
| }); |
| } |
| if (errorMeta?.['code'] !== undefined) { |
| Object.defineProperty(errToThrow, 'code', { |
| value: errorMeta['code'], |
| enumerable: true, |
| }); |
| } |
| if (errorMeta?.['status'] !== undefined) { |
| Object.defineProperty(errToThrow, 'status', { |
| value: errorMeta['status'], |
| enumerable: true, |
| }); |
| } |
| return errToThrow; |
| }; |
|
|
| |
| let responseText = ''; |
| let preToolResponseText: string | undefined; |
| let streamEnded = false; |
| const warnings: string[] = []; |
| for await (const event of session.stream({ streamId })) { |
| if (streamEnded) break; |
| switch (event.type) { |
| case 'message': { |
| if (event.role === 'agent') { |
| for (const part of event.content) { |
| if (part.type === 'text') { |
| const isRaw = |
| config.getRawOutput() || config.getAcceptRawOutputRisk(); |
| const output = isRaw ? part.text : stripAnsi(part.text); |
| 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 (part.text) { |
| textOutput.write(output); |
| } |
| } |
| } |
| } |
| } |
| break; |
| } |
| case 'tool_request': { |
| if (config.getOutputFormat() === OutputFormat.JSON) { |
| |
| |
| preToolResponseText = responseText || preToolResponseText; |
| responseText = ''; |
| } |
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.TOOL_USE, |
| timestamp: new Date().toISOString(), |
| tool_name: event.name, |
| tool_id: event.requestId, |
| parameters: event.args, |
| }); |
| } |
| break; |
| } |
| case 'tool_response': { |
| textOutput.ensureTrailingNewline(); |
| if (streamFormatter) { |
| const display = event.display?.result; |
| const displayText = displayContentToString(display); |
| const errorMsg = getTextContent(event.content) ?? 'Tool error'; |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.TOOL_RESULT, |
| timestamp: new Date().toISOString(), |
| tool_id: event.requestId, |
| status: event.isError ? 'error' : 'success', |
| output: displayText, |
| error: event.isError |
| ? { |
| type: |
| typeof event.data?.['errorType'] === 'string' |
| ? event.data['errorType'] |
| : 'TOOL_EXECUTION_ERROR', |
| message: errorMsg, |
| } |
| : undefined, |
| }); |
| } |
| if (event.isError) { |
| const display = event.display?.result; |
| const displayText = displayContentToString(display); |
| const errorMsg = getTextContent(event.content) ?? 'Tool error'; |
|
|
| if (event.data?.['errorType'] === ToolErrorType.STOP_EXECUTION) { |
| if ( |
| config.getOutputFormat() === OutputFormat.JSON && |
| !responseText && |
| preToolResponseText |
| ) { |
| responseText = preToolResponseText; |
| } |
| const stopMessage = `Agent execution stopped: ${errorMsg}`; |
| if (config.getOutputFormat() === OutputFormat.TEXT) { |
| process.stderr.write(`${stopMessage}\n`); |
| } |
| } |
|
|
| if (event.data?.['errorType'] === ToolErrorType.NO_SPACE_LEFT) { |
| throw new FatalToolExecutionError( |
| 'Error executing tool ' + |
| event.name + |
| ': ' + |
| (displayText || errorMsg), |
| ); |
| } |
| handleToolError( |
| event.name, |
| new Error(errorMsg), |
| config, |
| typeof event.data?.['errorType'] === 'string' |
| ? event.data['errorType'] |
| : undefined, |
| displayText, |
| ); |
| } |
| break; |
| } |
| case 'error': { |
| if (event._meta?.['code'] === 'INVALID_STREAM') { |
| const errorTypeVal = event._meta?.['errorType']; |
| const errorType = |
| typeof errorTypeVal === 'string' ? errorTypeVal : undefined; |
|
|
| let errorMessage = event.message; |
| if (errorType === 'NO_RESPONSE_TEXT') { |
| errorMessage = TRUE_EMPTY_RESPONSE_MESSAGE; |
| } else if (errorType === 'THINKING_ONLY_RESPONSE') { |
| errorMessage = THINKING_ONLY_COMPRESS_SUGGESTION; |
| } else if (errorType === 'MAX_TOKENS_EXCEEDED') { |
| errorMessage = MAX_TOKENS_EXCEEDED_SUGGESTION; |
| } else if (errorType === 'SAFETY_BLOCKED') { |
| errorMessage = SAFETY_BLOCKED_MESSAGE; |
| } else if (errorType === 'RECITATION_BLOCKED') { |
| errorMessage = RECITATION_BLOCKED_MESSAGE; |
| } else if (errorType === 'OTHER_BLOCKED') { |
| errorMessage = OTHER_BLOCKED_MESSAGE; |
| } |
|
|
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.ERROR, |
| timestamp: new Date().toISOString(), |
| severity: 'error', |
| message: errorMessage, |
| }); |
| } else if (config.getOutputFormat() === OutputFormat.TEXT) { |
| process.stderr.write(`[ERROR] ${errorMessage}\n`); |
| } |
|
|
| |
| uiTelemetryService.recordSemanticValidationError( |
| geminiClient.getCurrentSequenceModel() ?? config.getModel(), |
| errorType || 'INVALID_STREAM', |
| ); |
|
|
| |
| emitFinalResult({ |
| type: 'INVALID_STREAM', |
| message: errorMessage, |
| }); |
| streamEnded = true; |
| break; |
| } |
|
|
| if (event.fatal) { |
| throw reconstructFatalError(event); |
| } |
|
|
| const errorCode = event._meta?.['code']; |
|
|
| if (errorCode === 'AGENT_EXECUTION_BLOCKED') { |
| const blockMessage = `Agent execution blocked: ${event.message.trim()}`; |
| 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); |
| break; |
| } |
|
|
| const severity = |
| event.status === 'RESOURCE_EXHAUSTED' ? 'error' : 'warning'; |
| if (config.getOutputFormat() === OutputFormat.TEXT) { |
| process.stderr.write(`[WARNING] ${event.message}\n`); |
| } |
| if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.ERROR, |
| timestamp: new Date().toISOString(), |
| severity, |
| message: stripAnsi(event.message), |
| }); |
| } |
| warnings.push(event.message); |
| break; |
| } |
| case 'agent_end': { |
| if (event.reason === 'aborted') { |
| throw new FatalCancellationError('Operation cancelled.'); |
| } else if (event.reason === 'max_turns') { |
| const isConfiguredTurnLimit = |
| typeof event.data?.['maxTurns'] === 'number' || |
| typeof event.data?.['turnCount'] === 'number'; |
|
|
| if (isConfiguredTurnLimit) { |
| throw new FatalTurnLimitedError( |
| 'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.', |
| ); |
| } else if (streamFormatter) { |
| streamFormatter.emitEvent({ |
| type: JsonStreamEventType.ERROR, |
| timestamp: new Date().toISOString(), |
| severity: 'error', |
| message: 'Maximum session turns exceeded', |
| }); |
| } |
| } |
|
|
| const stopMessage = |
| typeof event.data?.['message'] === 'string' |
| ? event.data['message'] |
| : ''; |
| if (stopMessage && config.getOutputFormat() === OutputFormat.TEXT) { |
| process.stderr.write(`Agent execution stopped: ${stopMessage}\n`); |
| } |
|
|
| emitFinalResult(); |
| streamEnded = true; |
| break; |
| } |
| case 'initialize': |
| case 'session_update': |
| case 'agent_start': |
| case 'tool_update': |
| case 'elicitation_request': |
| case 'elicitation_response': |
| case 'usage': |
| case 'custom': |
| |
| break; |
| default: |
| debugLogger.error('Unknown agent event type:', event); |
| event satisfies never; |
| break; |
| } |
| } |
| } catch (error) { |
| errorToHandle = error; |
| } finally { |
| |
| cleanupStdinCancellation(); |
| abortController.signal.removeEventListener('abort', abortSession); |
|
|
| scheduler?.dispose(); |
| consolePatcher.cleanup(); |
| coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback); |
| } |
|
|
| if (errorToHandle) { |
| handleError(errorToHandle, config); |
| } |
| }); |
| } |
|
|