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 ,
2025-11-10 18:31:00 -07:00
ResumedSessionData ,
2025-10-23 14:14:14 -04:00
UserFeedbackPayload ,
2026-03-20 11:19:34 -04:00
AgentEvent ,
ContentPart ,
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 {
2026-02-22 20:18:07 -05:00
convertSessionToClientHistory ,
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-23 14:14:14 -04:00
coreEvents ,
CoreEvent ,
2025-12-02 15:08:25 -08:00
createWorkingStdio ,
2026-01-26 22:11:29 -05:00
Scheduler ,
ROOT_SCHEDULER_ID ,
2026-03-20 11:19:34 -04:00
LegacyAgentSession ,
ToolErrorType ,
geminiPartsToContentParts ,
2025-06-25 05:41:11 -07:00
} from '@google/gemini-cli-core' ;
2025-09-19 13:49:35 +00:00
2026-03-20 11:19:34 -04:00
import type { 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-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
2026-02-06 16:20:22 -08:00
if ( process . env [ 'GEMINI_CLI_ACTIVITY_LOG_TARGET' ] ) {
2026-02-10 08:54:23 -08:00
const { setupInitialActivityLogger } = await import (
2026-02-09 14:03:10 -08:00
'./utils/devtoolsService.js'
2026-01-28 09:02:41 -08:00
) ;
2026-02-10 08:54:23 -08:00
await setupInitialActivityLogger ( config ) ;
2026-01-28 09:02:41 -08:00
}
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 ( ) ;
}
} ;
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 ( {
2026-03-10 18:12:59 -07:00
context : config ,
2026-01-26 22:11:29 -05:00
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 (
2026-02-22 20:18:07 -05:00
convertSessionToClientHistory (
2025-11-10 18:31:00 -07:00
resumedSessionData . conversation . messages ,
2026-02-22 20:18:07 -05:00
) ,
2025-11-10 18:31:00 -07:00
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 ( slashCommandResult ) {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-09-19 13:49:35 +00:00
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 ,
2026-03-13 03:35:12 +05:30
escapePastedAtSymbols : false ,
2025-09-19 13:49:35 +00:00
} ) ;
2025-12-13 06:31:12 +05:30
if ( error || ! processedQuery ) {
2025-09-19 13:49:35 +00:00
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
) ;
}
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
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 ,
} ) ;
}
2026-03-20 11:19:34 -04:00
// Create LegacyAgentSession — owns the agentic loop
const session = new LegacyAgentSession ( {
client : geminiClient ,
scheduler ,
config ,
promptId : prompt_id ,
} ) ;
// Wire Ctrl+C to session abort
abortController . signal . addEventListener ( 'abort' , ( ) = > {
void session . abort ( ) ;
} ) ;
// Start the agentic loop (runs in background)
2026-03-20 11:19:34 -04:00
const { streamId } = await session . send ( {
2026-03-20 11:19:34 -04:00
message : geminiPartsToContentParts ( query ) ,
} ) ;
2025-09-09 01:14:15 -04:00
2026-03-20 11:19:34 -04:00
const getFirstText = ( parts? : ContentPart [ ] ) : string | undefined = > {
const part = parts ? . [ 0 ] ;
return part ? . type === 'text' ? part.text : undefined ;
} ;
const emitFinalSuccessResult = ( ) : void = > {
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 ( ) ;
2025-06-01 16:11:37 -07:00
}
2026-03-20 11:19:34 -04:00
} ;
2025-06-01 16:11:37 -07:00
2026-03-20 11:19:34 -04:00
const reconstructFatalError = ( event : AgentEvent < 'error' > ) : Error = > {
const errToThrow = new Error ( event . message ) ;
const errorMeta = event . _meta ;
if ( errorMeta ? . [ 'exitCode' ] !== undefined ) {
Object . defineProperty ( errToThrow , 'exitCode' , {
value : errorMeta [ 'exitCode' ] ,
enumerable : true ,
} ) ;
}
if ( errorMeta ? . [ 'errorName' ] !== undefined ) {
Object . defineProperty ( errToThrow , 'name' , {
value : errorMeta [ 'errorName' ] ,
enumerable : true ,
} ) ;
}
if ( errorMeta ? . [ 'code' ] !== undefined ) {
Object . defineProperty ( errToThrow , 'code' , {
value : errorMeta [ 'code' ] ,
enumerable : true ,
} ) ;
}
return errToThrow ;
} ;
2025-06-01 16:11:37 -07:00
2026-03-20 11:19:34 -04:00
// Consume AgentEvents for output formatting
let responseText = '' ;
let streamEnded = false ;
2026-03-20 11:19:34 -04:00
for await ( const event of session . stream ( { streamId } ) ) {
2026-03-20 11:19:34 -04:00
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 ) ;
}
}
}
2025-10-27 07:57:54 -07:00
}
2025-09-11 05:19:47 +09:00
}
2026-03-20 11:19:34 -04:00
break ;
}
case 'tool_request' : {
2025-10-15 13:55:37 -07:00
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
type : JsonStreamEventType . TOOL_USE ,
timestamp : new Date ( ) . toISOString ( ) ,
2026-03-20 11:19:34 -04:00
tool_name : event.name ,
tool_id : event.requestId ,
parameters : event.args ,
2025-10-15 13:55:37 -07:00
} ) ;
}
2026-03-20 11:19:34 -04:00
break ;
2025-06-04 23:25:57 -07:00
}
2026-03-20 11:19:34 -04:00
case 'tool_response' : {
textOutput . ensureTrailingNewline ( ) ;
2025-10-15 13:55:37 -07:00
if ( streamFormatter ) {
2026-03-20 11:19:34 -04:00
const displayText = getFirstText ( event . displayContent ) ;
const errorMsg = getFirstText ( event . content ) ? ? 'Tool error' ;
2025-10-15 13:55:37 -07:00
streamFormatter . emitEvent ( {
type : JsonStreamEventType . TOOL_RESULT ,
timestamp : new Date ( ) . toISOString ( ) ,
2026-03-20 11:19:34 -04:00
tool_id : event.requestId ,
status : event.isError ? 'error' : 'success' ,
output : displayText ,
error : event.isError
2025-10-15 13:55:37 -07:00
? {
2026-03-20 11:19:34 -04:00
type :
typeof event . data ? . [ 'errorType' ] === 'string'
? event . data [ 'errorType' ]
: 'TOOL_EXECUTION_ERROR' ,
message : errorMsg ,
2025-10-15 13:55:37 -07:00
}
: undefined ,
} ) ;
}
2026-03-20 11:19:34 -04:00
if ( event . isError ) {
const displayText = getFirstText ( event . displayContent ) ;
const errorMsg = getFirstText ( event . content ) ? ? 'Tool error' ;
if ( event . data ? . [ 'errorType' ] === ToolErrorType . STOP_EXECUTION ) {
const stopMessage = ` Agent execution stopped: ${ errorMsg } ` ;
if ( config . getOutputFormat ( ) === OutputFormat . TEXT ) {
process . stderr . write ( ` ${ stopMessage } \ n ` ) ;
}
}
2025-10-15 13:55:37 -07:00
2025-09-11 05:19:47 +09:00
handleToolError (
2026-03-20 11:19:34 -04:00
event . name ,
new Error ( errorMsg ) ,
2025-09-11 05:19:47 +09:00
config ,
2026-03-20 11:19:34 -04:00
typeof event . data ? . [ 'errorType' ] === 'string'
? event . data [ 'errorType' ]
2025-09-11 05:19:47 +09:00
: undefined ,
2026-03-20 11:19:34 -04:00
displayText ,
2025-09-09 01:14:15 -04:00
) ;
}
2026-03-20 11:19:34 -04:00
break ;
2025-06-01 16:11:37 -07:00
}
2026-03-20 11:19:34 -04:00
case 'error' : {
if ( event . fatal ) {
throw reconstructFatalError ( event ) ;
}
2025-10-14 09:51:00 -06:00
2026-03-20 11:19:34 -04:00
const errorCode = event . _meta ? . [ 'code' ] ;
2025-10-14 09:51:00 -06:00
2026-03-20 11:19:34 -04:00
if ( errorCode === 'AGENT_EXECUTION_BLOCKED' ) {
if ( config . getOutputFormat ( ) === OutputFormat . TEXT ) {
process . stderr . write ( ` [WARNING] ${ event . message } \ n ` ) ;
}
break ;
}
2025-12-31 07:22:53 +08:00
2026-03-20 11:19:34 -04:00
const severity =
event . status === 'RESOURCE_EXHAUSTED' ? 'error' : 'warning' ;
2025-12-31 07:22:53 +08:00
if ( config . getOutputFormat ( ) === OutputFormat . TEXT ) {
2026-03-20 11:19:34 -04:00
process . stderr . write ( ` [WARNING] ${ event . message } \ n ` ) ;
2025-12-31 07:22:53 +08:00
}
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
2026-03-20 11:19:34 -04:00
type : JsonStreamEventType . ERROR ,
2025-12-31 07:22:53 +08:00
timestamp : new Date ( ) . toISOString ( ) ,
2026-03-20 11:19:34 -04:00
severity ,
message : event.message ,
2025-12-31 07:22:53 +08:00
} ) ;
}
2026-03-20 11:19:34 -04:00
break ;
2025-12-31 07:22:53 +08:00
}
2026-03-20 11:19:34 -04:00
case 'agent_end' : {
2026-03-20 11:19:34 -04:00
if ( event . reason === 'aborted' ) {
handleCancellationError ( config ) ;
} else if ( event . reason === 'max_turns' ) {
2026-03-20 11:19:34 -04:00
const isSessionLimit =
typeof event . data ? . [ 'maxTurns' ] === 'number' &&
typeof event . data ? . [ 'turnCount' ] === 'number' ;
if ( isSessionLimit ) {
handleMaxTurnsExceededError ( config ) ;
}
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
type : JsonStreamEventType . ERROR ,
timestamp : new Date ( ) . toISOString ( ) ,
severity : 'error' ,
message : 'Maximum session turns exceeded' ,
} ) ;
}
2026-03-20 11:19:34 -04:00
}
2025-12-31 07:22:53 +08:00
2026-03-20 11:19:34 -04:00
const stopMessage =
typeof event . data ? . [ 'message' ] === 'string'
? event . data [ 'message' ]
: '' ;
if ( stopMessage && config . getOutputFormat ( ) === OutputFormat . TEXT ) {
process . stderr . write ( ` Agent execution stopped: ${ stopMessage } \ n ` ) ;
}
emitFinalSuccessResult ( ) ;
streamEnded = true ;
break ;
}
case 'custom' : {
if ( event . kind === 'loop_detected' ) {
if ( streamFormatter ) {
streamFormatter . emitEvent ( {
type : JsonStreamEventType . ERROR ,
timestamp : new Date ( ) . toISOString ( ) ,
severity : 'warning' ,
message : 'Loop detected, stopping execution' ,
} ) ;
}
}
break ;
2025-09-11 05:19:47 +09:00
}
2026-03-20 11:19:34 -04:00
default :
break ;
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
}