2025-06-01 16:11:37 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-10-14 09:51:00 -06:00
import type {
Config ,
ToolCallRequestInfo ,
2025-11-10 18:31:00 -07:00
ResumedSessionData ,
2025-10-23 14:14:14 -04:00
UserFeedbackPayload ,
2025-10-14 09:51:00 -06:00
} from '@google/gemini-cli-core' ;
2025-09-19 13:49:35 +00:00
import { isSlashCommand } from './ui/utils/commandUtils.js' ;
import type { LoadedSettings } from './config/settings.js' ;
2025-06-01 16:11:37 -07:00
import {
2025-07-31 05:36:12 -07:00
GeminiEventType ,
2025-08-25 21:44:45 -07:00
FatalInputError ,
2025-09-09 01:14:15 -04:00
promptIdContext ,
2025-09-11 05:19:47 +09:00
OutputFormat ,
JsonFormatter ,
2025-10-15 13:55:37 -07:00
StreamJsonFormatter ,
JsonStreamEventType ,
2025-09-11 05:19:47 +09:00
uiTelemetryService ,
2025-10-20 18:16:47 -04:00
debugLogger ,
2025-10-23 14:14:14 -04:00
coreEvents ,
CoreEvent ,
2025-12-02 15:08:25 -08:00
createWorkingStdio ,
2025-12-17 15:12:59 -08:00
recordToolCallInteractions ,
2025-12-31 07:22:53 +08:00
ToolErrorType ,
2026-01-26 22:11:29 -05:00
Scheduler ,
ROOT_SCHEDULER_ID ,
2025-06-25 05:41:11 -07:00
} from '@google/gemini-cli-core' ;
2025-09-19 13:49:35 +00:00
2025-08-26 00:04:53 +02:00
import type { Content , Part } from '@google/genai' ;
2025-11-03 13:49:01 -08:00
import readline from 'node:readline' ;
2026-01-20 23:58:37 -05:00
import stripAnsi from 'strip-ansi' ;
2025-06-01 16:11:37 -07:00
2025-11-10 18:31:00 -07:00
import { convertSessionToHistoryFormats } from './ui/hooks/useSessionBrowser.js' ;
2025-09-19 13:49:35 +00:00
import { handleSlashCommand } from './nonInteractiveCliCommands.js' ;
2025-08-05 16:11:21 -07:00
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js' ;
2025-08-21 14:47:40 -04:00
import { handleAtCommand } from './ui/hooks/atCommandProcessor.js' ;
2025-09-11 05:19:47 +09:00
import {
handleError ,
handleToolError ,
handleCancellationError ,
handleMaxTurnsExceededError ,
} from './utils/errors.js' ;
2025-10-27 07:57:54 -07:00
import { TextOutput } from './ui/utils/textOutput.js' ;
2025-06-23 17:30:13 -04:00
2025-10-29 17:54:40 -04:00
interface RunNonInteractiveParams {
config : Config ;
settings : LoadedSettings ;
input : string ;
prompt_id : string ;
2025-11-10 18:31:00 -07:00
resumedSessionData? : ResumedSessionData ;
2025-10-29 17:54:40 -04:00
}
export async function runNonInteractive ( {
config ,
settings ,
input ,
prompt_id ,
2025-11-10 18:31:00 -07:00
resumedSessionData ,
2025-10-29 17:54:40 -04:00
} : RunNonInteractiveParams ) : Promise < void > {
2025-09-09 01:14:15 -04:00
return promptIdContext . run ( prompt_id , async ( ) = > {
const consolePatcher = new ConsolePatcher ( {
stderr : true ,
debugMode : config.getDebugMode ( ) ,
2025-11-20 10:44:02 -08:00
onNewMessage : ( msg ) = > {
coreEvents . emitConsoleLog ( msg . type , msg . content ) ;
} ,
2025-08-21 14:47:40 -04:00
} ) ;
2026-01-28 09:02:41 -08:00
if ( config . storage && process . env [ 'GEMINI_CLI_ACTIVITY_LOG_FILE' ] ) {
const { registerActivityLogger } = await import (
'./utils/activityLogger.js'
) ;
registerActivityLogger ( config ) ;
}
2025-12-02 15:08:25 -08:00
const { stdout : workingStdout } = createWorkingStdio ( ) ;
const textOutput = new TextOutput ( workingStdout ) ;
2025-08-21 14:47:40 -04:00
2025-10-23 14:14:14 -04:00
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 ` ) ;
}
} ;
2025-10-15 13:55:37 -07:00
const startTime = Date . now ( ) ;
const streamFormatter =
config . getOutputFormat ( ) === OutputFormat . STREAM_JSON
? new StreamJsonFormatter ( )
: null ;
2025-11-03 13:49:01 -08:00
const abortController = new AbortController ( ) ;
// Track cancellation state
let isAborting = false ;
let cancelMessageTimer : NodeJS.Timeout | null = null ;
// Setup stdin listener for Ctrl+C detection
let stdinWasRaw = false ;
let rl : readline.Interface | null = null ;
const setupStdinCancellation = ( ) = > {
// Only setup if stdin is a TTY (user can interact)
if ( ! process . stdin . isTTY ) {
return ;
}
// Save original raw mode state
stdinWasRaw = process . stdin . isRaw || false ;
// Enable raw mode to capture individual keypresses
process . stdin . setRawMode ( true ) ;
process . stdin . resume ( ) ;
// Setup readline to emit keypress events
rl = readline . createInterface ( {
input : process.stdin ,
escapeCodeTimeout : 0 ,
} ) ;
readline . emitKeypressEvents ( process . stdin , rl ) ;
// Listen for Ctrl+C
const keypressHandler = (
str : string ,
key : { name? : string ; ctrl? : boolean } ,
) = > {
// Detect Ctrl+C: either ctrl+c key combo or raw character code 3
if ( ( key && key . ctrl && key . name === 'c' ) || str === '\u0003' ) {
// Only handle once
if ( isAborting ) {
return ;
}
isAborting = true ;
// Only show message if cancellation takes longer than 200ms
// This reduces verbosity for fast cancellations
cancelMessageTimer = setTimeout ( ( ) = > {
process . stderr . write ( '\nCancelling...\n' ) ;
} , 200 ) ;
abortController . abort ( ) ;
// Note: Don't exit here - let the abort flow through the system
// and trigger handleCancellationError() which will exit with proper code
}
} ;
process . stdin . on ( 'keypress' , keypressHandler ) ;
} ;
const cleanupStdinCancellation = ( ) = > {
// Clear any pending cancel message timer
if ( cancelMessageTimer ) {
clearTimeout ( cancelMessageTimer ) ;
cancelMessageTimer = null ;
}
// Cleanup readline and stdin listeners
if ( rl ) {
rl . close ( ) ;
rl = null ;
}
// Remove keypress listener
process . stdin . removeAllListeners ( 'keypress' ) ;
// Restore stdin to original state
if ( process . stdin . isTTY ) {
process . stdin . setRawMode ( stdinWasRaw ) ;
process . stdin . pause ( ) ;
}
} ;
2025-10-23 18:52:16 -07:00
let errorToHandle : unknown | undefined ;
2025-09-09 01:14:15 -04:00
try {
consolePatcher . patch ( ) ;
2025-11-03 13:49:01 -08:00
2026-01-20 23:58:37 -05:00
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' ,
) ;
}
2025-11-03 13:49:01 -08:00
// Setup stdin cancellation listener
setupStdinCancellation ( ) ;
2025-10-23 14:14:14 -04:00
coreEvents . on ( CoreEvent . UserFeedback , handleUserFeedback ) ;
2025-11-20 10:44:02 -08:00
coreEvents . drainBacklogs ( ) ;
2025-10-23 14:14:14 -04:00
2025-09-09 01:14:15 -04:00
// Handle EPIPE errors when the output is piped to a command that closes early.
process . stdout . on ( 'error' , ( err : NodeJS.ErrnoException ) = > {
if ( err . code === 'EPIPE' ) {
// Exit gracefully if the pipe is closed.
process . exit ( 0 ) ;
}
} ) ;
const geminiClient = config . getGeminiClient ( ) ;
2026-01-26 22:11:29 -05:00
const scheduler = new Scheduler ( {
config ,
messageBus : config.getMessageBus ( ) ,
getPreferredEditor : ( ) = > undefined ,
schedulerId : ROOT_SCHEDULER_ID ,
} ) ;
2025-09-09 01:14:15 -04:00
2025-11-10 18:31:00 -07:00
// Initialize chat. Resume if resume data is passed.
if ( resumedSessionData ) {
await geminiClient . resumeChat (
convertSessionToHistoryFormats (
resumedSessionData . conversation . messages ,
) . clientHistory ,
resumedSessionData ,
) ;
}
2025-10-15 13:55:37 -07:00
// Emit init event for streaming JSON
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
type : JsonStreamEventType . INIT ,
timestamp : new Date ( ) . toISOString ( ) ,
session_id : config.getSessionId ( ) ,
model : config.getModel ( ) ,
} ) ;
}
2025-09-19 13:49:35 +00:00
let query : Part [ ] | undefined ;
2025-09-09 01:14:15 -04:00
2025-09-19 13:49:35 +00:00
if ( isSlashCommand ( input ) ) {
const slashCommandResult = await handleSlashCommand (
input ,
abortController ,
config ,
settings ,
2025-07-11 07:55:03 -07:00
) ;
2025-09-19 13:49:35 +00:00
// If a slash command is found and returns a prompt, use it.
2025-12-11 01:11:54 +08:00
// Otherwise, slashCommandResult falls through to the default prompt
2025-09-19 13:49:35 +00:00
// handling.
if ( slashCommandResult ) {
query = slashCommandResult as Part [ ] ;
}
}
if ( ! query ) {
2025-12-13 06:31:12 +05:30
const { processedQuery , error } = await handleAtCommand ( {
2025-09-19 13:49:35 +00:00
query : input ,
config ,
addItem : ( _item , _timestamp ) = > 0 ,
onDebugMessage : ( ) = > { } ,
messageId : Date.now ( ) ,
signal : abortController.signal ,
} ) ;
2025-12-13 06:31:12 +05:30
if ( error || ! processedQuery ) {
2025-09-19 13:49:35 +00:00
// An error occurred during @include processing (e.g., file not found).
// The error message is already logged by handleAtCommand.
throw new FatalInputError (
2025-12-13 06:31:12 +05:30
error || 'Exiting due to an error processing the @ command.' ,
2025-09-19 13:49:35 +00:00
) ;
}
query = processedQuery as Part [ ] ;
2025-07-11 07:55:03 -07:00
}
2025-06-01 16:11:37 -07:00
2025-10-15 13:55:37 -07:00
// Emit user message event for streaming JSON
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
type : JsonStreamEventType . MESSAGE ,
timestamp : new Date ( ) . toISOString ( ) ,
role : 'user' ,
content : input ,
} ) ;
}
2025-09-19 13:49:35 +00:00
let currentMessages : Content [ ] = [ { role : 'user' , parts : query } ] ;
2025-09-09 01:14:15 -04:00
let turnCount = 0 ;
while ( true ) {
turnCount ++ ;
if (
config . getMaxSessionTurns ( ) >= 0 &&
turnCount > config . getMaxSessionTurns ( )
) {
2025-09-11 05:19:47 +09:00
handleMaxTurnsExceededError ( config ) ;
2025-06-01 16:11:37 -07:00
}
2025-09-09 01:14:15 -04:00
const toolCallRequests : ToolCallRequestInfo [ ] = [ ] ;
2025-07-31 05:36:12 -07:00
2025-09-09 01:14:15 -04:00
const responseStream = geminiClient . sendMessageStream (
currentMessages [ 0 ] ? . parts || [ ] ,
abortController . signal ,
prompt_id ,
2026-01-30 10:09:27 -08:00
undefined ,
false ,
turnCount === 1 ? input : undefined ,
2025-09-09 01:14:15 -04:00
) ;
2025-06-01 16:11:37 -07:00
2025-09-11 05:19:47 +09:00
let responseText = '' ;
2025-09-09 01:14:15 -04:00
for await ( const event of responseStream ) {
if ( abortController . signal . aborted ) {
2025-09-11 05:19:47 +09:00
handleCancellationError ( config ) ;
2025-09-09 01:14:15 -04:00
}
2025-06-01 16:11:37 -07:00
2025-09-09 01:14:15 -04:00
if ( event . type === GeminiEventType . Content ) {
2026-01-20 23:58:37 -05:00
const isRaw =
config . getRawOutput ( ) || config . getAcceptRawOutputRisk ( ) ;
const output = isRaw ? event.value : stripAnsi ( event . value ) ;
2025-10-15 13:55:37 -07:00
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
type : JsonStreamEventType . MESSAGE ,
timestamp : new Date ( ) . toISOString ( ) ,
role : 'assistant' ,
2026-01-20 23:58:37 -05:00
content : output ,
2025-10-15 13:55:37 -07:00
delta : true ,
} ) ;
} else if ( config . getOutputFormat ( ) === OutputFormat . JSON ) {
2026-01-20 23:58:37 -05:00
responseText += output ;
2025-09-11 05:19:47 +09:00
} else {
2025-10-27 07:57:54 -07:00
if ( event . value ) {
2026-01-20 23:58:37 -05:00
textOutput . write ( output ) ;
2025-10-27 07:57:54 -07:00
}
2025-09-11 05:19:47 +09:00
}
2025-09-09 01:14:15 -04:00
} else if ( event . type === GeminiEventType . ToolCallRequest ) {
2025-10-15 13:55:37 -07:00
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 ,
} ) ;
}
2025-09-09 01:14:15 -04:00
toolCallRequests . push ( event . value ) ;
2025-10-15 13:55:37 -07:00
} else if ( event . type === GeminiEventType . LoopDetected ) {
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
type : JsonStreamEventType . ERROR ,
timestamp : new Date ( ) . toISOString ( ) ,
severity : 'warning' ,
message : 'Loop detected, stopping execution' ,
} ) ;
}
} else if ( event . type === GeminiEventType . MaxSessionTurns ) {
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
type : JsonStreamEventType . ERROR ,
timestamp : new Date ( ) . toISOString ( ) ,
severity : 'error' ,
message : 'Maximum session turns exceeded' ,
} ) ;
}
2025-10-23 18:52:16 -07:00
} else if ( event . type === GeminiEventType . Error ) {
throw event . value . error ;
2026-01-04 18:58:34 -08:00
} else if ( event . type === GeminiEventType . AgentExecutionStopped ) {
2026-01-09 15:47:14 -05:00
const stopMessage = ` Agent execution stopped: ${ event . value . systemMessage ? . trim ( ) || event . value . reason } ` ;
2026-01-04 18:58:34 -08:00
if ( config . getOutputFormat ( ) === OutputFormat . TEXT ) {
process . stderr . write ( ` ${ stopMessage } \ n ` ) ;
}
// Emit final result event for streaming JSON if needed
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 ,
) ,
} ) ;
}
return ;
} else if ( event . type === GeminiEventType . AgentExecutionBlocked ) {
2026-01-09 15:47:14 -05:00
const blockMessage = ` Agent execution blocked: ${ event . value . systemMessage ? . trim ( ) || event . value . reason } ` ;
2026-01-04 18:58:34 -08:00
if ( config . getOutputFormat ( ) === OutputFormat . TEXT ) {
process . stderr . write ( ` [WARNING] ${ blockMessage } \ n ` ) ;
}
2025-06-04 23:25:57 -07:00
}
2025-09-09 01:14:15 -04:00
}
2025-06-04 23:25:57 -07:00
2025-09-09 01:14:15 -04:00
if ( toolCallRequests . length > 0 ) {
2025-10-27 07:57:54 -07:00
textOutput . ensureTrailingNewline ( ) ;
2026-01-26 22:11:29 -05:00
const completedToolCalls = await scheduler . schedule (
toolCallRequests ,
abortController . signal ,
) ;
2025-09-09 01:14:15 -04:00
const toolResponseParts : Part [ ] = [ ] ;
2025-10-14 09:51:00 -06:00
2026-01-26 22:11:29 -05:00
for ( const completedToolCall of completedToolCalls ) {
const toolResponse = completedToolCall . response ;
const requestInfo = completedToolCall . request ;
2025-09-09 01:14:15 -04:00
2025-10-15 13:55:37 -07:00
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
type : JsonStreamEventType . TOOL_RESULT ,
timestamp : new Date ( ) . toISOString ( ) ,
tool_id : requestInfo.callId ,
2026-01-26 22:11:29 -05:00
status :
completedToolCall . status === 'error' ? 'error' : 'success' ,
2025-10-15 13:55:37 -07:00
output :
typeof toolResponse . resultDisplay === 'string'
? toolResponse . resultDisplay
: undefined ,
error : toolResponse.error
? {
type : toolResponse . errorType || 'TOOL_EXECUTION_ERROR' ,
message : toolResponse.error.message ,
}
: undefined ,
} ) ;
}
2025-09-09 01:14:15 -04:00
if ( toolResponse . error ) {
2025-09-11 05:19:47 +09:00
handleToolError (
requestInfo . name ,
toolResponse . error ,
config ,
toolResponse . errorType || 'TOOL_EXECUTION_ERROR' ,
typeof toolResponse . resultDisplay === 'string'
? toolResponse . resultDisplay
: undefined ,
2025-09-09 01:14:15 -04:00
) ;
}
if ( toolResponse . responseParts ) {
toolResponseParts . push ( . . . toolResponse . responseParts ) ;
}
2025-06-01 16:11:37 -07:00
}
2025-10-14 09:51:00 -06:00
// Record tool calls with full metadata before sending responses to Gemini
try {
const currentModel =
geminiClient . getCurrentSequenceModel ( ) ? ? config . getModel ( ) ;
geminiClient
. getChat ( )
. recordCompletedToolCalls ( currentModel , completedToolCalls ) ;
2025-12-17 15:12:59 -08:00
await recordToolCallInteractions ( config , completedToolCalls ) ;
2025-10-14 09:51:00 -06:00
} catch ( error ) {
2025-10-20 18:16:47 -04:00
debugLogger . error (
2025-10-14 09:51:00 -06:00
` Error recording completed tool call information: ${ error } ` ,
) ;
}
2025-12-31 07:22:53 +08:00
// Check if any tool requested to stop execution immediately
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 ` ) ;
}
// Emit final result event for streaming JSON
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 ) ,
) ;
} else {
textOutput . ensureTrailingNewline ( ) ; // Ensure a final newline
}
return ;
}
2025-09-09 01:14:15 -04:00
currentMessages = [ { role : 'user' , parts : toolResponseParts } ] ;
} else {
2025-10-15 13:55:37 -07:00
// Emit final result event for streaming JSON
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 ) {
2025-09-11 05:19:47 +09:00
const formatter = new JsonFormatter ( ) ;
const stats = uiTelemetryService . getMetrics ( ) ;
2025-12-04 22:36:20 +05:30
textOutput . write (
formatter . format ( config . getSessionId ( ) , responseText , stats ) ,
) ;
2025-09-11 05:19:47 +09:00
} else {
2025-10-27 07:57:54 -07:00
textOutput . ensureTrailingNewline ( ) ; // Ensure a final newline
2025-09-11 05:19:47 +09:00
}
2025-09-09 01:14:15 -04:00
return ;
2025-06-01 16:11:37 -07:00
}
2025-09-09 01:14:15 -04:00
}
} catch ( error ) {
2025-10-23 18:52:16 -07:00
errorToHandle = error ;
2025-09-09 01:14:15 -04:00
} finally {
2025-11-03 13:49:01 -08:00
// Cleanup stdin cancellation before other cleanup
cleanupStdinCancellation ( ) ;
2025-09-09 01:14:15 -04:00
consolePatcher . cleanup ( ) ;
2025-10-23 14:14:14 -04:00
coreEvents . off ( CoreEvent . UserFeedback , handleUserFeedback ) ;
2025-06-01 16:11:37 -07:00
}
2025-10-23 18:52:16 -07:00
if ( errorToHandle ) {
handleError ( errorToHandle , config ) ;
}
2025-09-09 01:14:15 -04:00
} ) ;
2025-06-01 16:11:37 -07:00
}