2025-09-30 17:00:54 -04:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '../config/config.js' ;
2026-03-10 18:12:59 -07:00
import { type AgentLoopContext } from '../config/agent-loop-context.js' ;
2025-09-30 17:00:54 -04:00
import { reportError } from '../utils/errorReporting.js' ;
import { GeminiChat , StreamEventType } from '../core/geminiChat.js' ;
2026-03-04 05:42:59 +05:30
import {
Type ,
type Content ,
type Part ,
type FunctionCall ,
type FunctionDeclaration ,
type Schema ,
2025-09-30 17:00:54 -04:00
} from '@google/genai' ;
import { ToolRegistry } from '../tools/tool-registry.js' ;
2026-03-12 10:17:36 -04:00
import { type AnyDeclarativeTool } from '../tools/tools.js' ;
import {
DiscoveredMCPTool ,
isMcpToolName ,
parseMcpToolName ,
MCP_TOOL_PREFIX ,
} from '../tools/mcp-tool.js' ;
2025-12-26 15:51:39 -05:00
import { CompressionStatus } from '../core/turn.js' ;
import { type ToolCallRequestInfo } from '../scheduler/types.js' ;
2026-03-09 12:22:46 -07:00
import { type Message } from '../confirmation-bus/types.js' ;
2025-11-05 16:15:28 -05:00
import { ChatCompressionService } from '../services/chatCompressionService.js' ;
2025-09-30 17:00:54 -04:00
import { getDirectoryContextString } from '../utils/environmentContext.js' ;
2025-10-08 15:42:33 -04:00
import { promptIdContext } from '../utils/promptIdContext.js' ;
2025-11-03 17:53:43 -05:00
import {
logAgentStart ,
logAgentFinish ,
logRecoveryAttempt ,
} from '../telemetry/loggers.js' ;
import {
AgentStartEvent ,
AgentFinishEvent ,
2026-02-20 22:28:55 +00:00
LlmRole ,
2026-02-21 12:41:27 -05:00
RecoveryAttemptEvent ,
2025-11-03 17:53:43 -05:00
} from '../telemetry/types.js' ;
2026-02-04 01:28:00 -05:00
import {
AgentTerminateMode ,
DEFAULT_QUERY_STRING ,
DEFAULT_MAX_TURNS ,
DEFAULT_MAX_TIME_MINUTES ,
2026-03-04 05:42:59 +05:30
type LocalAgentDefinition ,
type AgentInputs ,
type OutputObject ,
type SubagentActivityEvent ,
2026-02-04 01:28:00 -05:00
} from './types.js' ;
2026-02-21 12:41:27 -05:00
import { getErrorMessage } from '../utils/errors.js' ;
2025-09-30 17:00:54 -04:00
import { templateString } from './utils.js' ;
2026-01-08 12:39:40 -08:00
import { DEFAULT_GEMINI_MODEL , isAutoModel } from '../config/models.js' ;
import type { RoutingContext } from '../routing/routingStrategy.js' ;
2025-09-30 17:00:54 -04:00
import { parseThought } from '../utils/thoughtUtils.js' ;
2025-10-03 13:21:08 -04:00
import { type z } from 'zod' ;
import { zodToJsonSchema } from 'zod-to-json-schema' ;
2025-10-21 16:35:22 -04:00
import { debugLogger } from '../utils/debugLogger.js' ;
2025-11-19 20:41:16 -08:00
import { getModelConfigAlias } from './registry.js' ;
2025-12-19 14:11:32 -08:00
import { getVersion } from '../utils/version.js' ;
2026-01-26 17:12:55 -05:00
import { getToolCallContext } from '../utils/toolCallContext.js' ;
import { scheduleAgentTools } from './agent-scheduler.js' ;
2026-02-07 23:03:47 -05:00
import { DeadlineTimer } from '../utils/deadlineTimer.js' ;
2026-02-18 14:05:50 -08:00
import { formatUserHintsForModel } from '../utils/fastAckHelper.js' ;
2025-09-30 17:00:54 -04:00
/** A callback function to report on agent activity. */
export type ActivityCallback = ( activity : SubagentActivityEvent ) = > void ;
2025-10-02 14:07:58 -04:00
const TASK_COMPLETE_TOOL_NAME = 'complete_task' ;
2025-11-03 16:22:12 -05:00
const GRACE_PERIOD_MS = 60 * 1000 ; // 1 min
/** The possible outcomes of a single agent turn. */
type AgentTurnResult =
| {
status : 'continue' ;
nextMessage : Content ;
}
| {
status : 'stop' ;
terminateReason : AgentTerminateMode ;
finalResult : string | null ;
} ;
2025-10-02 14:07:58 -04:00
2026-01-16 16:51:10 +00:00
export function createUnauthorizedToolError ( toolName : string ) : string {
return ` Unauthorized tool call: ' ${ toolName } ' is not available to this agent. ` ;
}
2025-09-30 17:00:54 -04:00
/**
* Executes an agent loop based on an {@link AgentDefinition}.
*
2025-10-02 14:07:58 -04:00
* This executor runs the agent in a loop, calling tools until it calls the
* mandatory `complete_task` tool to signal completion.
2025-09-30 17:00:54 -04:00
*/
2025-12-17 12:06:38 -05:00
export class LocalAgentExecutor < TOutput extends z.ZodTypeAny > {
readonly definition : LocalAgentDefinition < TOutput > ;
2025-09-30 17:00:54 -04:00
private readonly agentId : string ;
private readonly toolRegistry : ToolRegistry ;
2026-03-10 18:12:59 -07:00
private readonly context : AgentLoopContext ;
2025-09-30 17:00:54 -04:00
private readonly onActivity? : ActivityCallback ;
2025-11-05 16:15:28 -05:00
private readonly compressionService : ChatCompressionService ;
2026-01-26 17:12:55 -05:00
private readonly parentCallId? : string ;
2025-11-05 16:15:28 -05:00
private hasFailedCompressionAttempt = false ;
2025-09-30 17:00:54 -04:00
2026-03-10 18:12:59 -07:00
private get config ( ) : Config {
return this . context . config ;
}
2025-09-30 17:00:54 -04:00
/**
* Creates and validates a new `AgentExecutor` instance.
*
* This method ensures that all tools specified in the agent's definition are
* safe for non-interactive use before creating the executor.
*
* @param definition The definition object for the agent.
2026-03-10 18:12:59 -07:00
* @param context The execution context.
2025-09-30 17:00:54 -04:00
* @param onActivity An optional callback to receive activity events.
2025-12-17 12:06:38 -05:00
* @returns A promise that resolves to a new `LocalAgentExecutor` instance.
2025-09-30 17:00:54 -04:00
*/
2025-10-03 13:21:08 -04:00
static async create < TOutput extends z.ZodTypeAny > (
2025-12-17 12:06:38 -05:00
definition : LocalAgentDefinition < TOutput > ,
2026-03-10 18:12:59 -07:00
context : AgentLoopContext ,
2025-09-30 17:00:54 -04:00
onActivity? : ActivityCallback ,
2025-12-17 12:06:38 -05:00
) : Promise < LocalAgentExecutor < TOutput > > {
2026-03-10 18:12:59 -07:00
const parentMessageBus = context . messageBus ;
2026-03-09 12:22:46 -07:00
// Create an override object to inject the subagent name into tool confirmation requests
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const subagentMessageBus = Object . create (
parentMessageBus ,
) as typeof parentMessageBus ;
subagentMessageBus . publish = async ( message : Message ) = > {
if ( message . type === 'tool-confirmation-request' ) {
return parentMessageBus . publish ( {
. . . message ,
subagent : definition.name ,
} ) ;
}
return parentMessageBus . publish ( message ) ;
} ;
2025-09-30 17:00:54 -04:00
// Create an isolated tool registry for this agent instance.
2026-01-04 17:11:43 -05:00
const agentToolRegistry = new ToolRegistry (
2026-03-10 18:12:59 -07:00
context . config ,
2026-03-09 12:22:46 -07:00
subagentMessageBus ,
2026-01-04 17:11:43 -05:00
) ;
2026-03-10 18:12:59 -07:00
const parentToolRegistry = context . toolRegistry ;
2026-01-23 02:18:31 +00:00
const allAgentNames = new Set (
2026-03-10 18:12:59 -07:00
context . config . getAgentRegistry ( ) . getAllAgentNames ( ) ,
2026-01-23 02:18:31 +00:00
) ;
2025-09-30 17:00:54 -04:00
2026-03-12 10:17:36 -04:00
const registerToolInstance = ( tool : AnyDeclarativeTool ) = > {
2026-01-24 01:30:18 +00:00
// Check if the tool is a subagent to prevent recursion.
// We do not allow agents to call other agents.
2026-03-12 10:17:36 -04:00
if ( allAgentNames . has ( tool . name ) ) {
2026-01-24 01:30:18 +00:00
debugLogger . warn (
2026-03-12 10:17:36 -04:00
` [LocalAgentExecutor] Skipping subagent tool ' ${ tool . name } ' for agent ' ${ definition . name } ' to prevent recursion. ` ,
2026-01-24 01:30:18 +00:00
) ;
return ;
}
2026-03-12 10:17:36 -04:00
agentToolRegistry . registerTool ( tool ) ;
} ;
const registerToolByName = ( toolName : string ) = > {
// Handle global wildcard
if ( toolName === '*' ) {
for ( const tool of parentToolRegistry . getAllTools ( ) ) {
registerToolInstance ( tool ) ;
}
return ;
}
// Handle MCP wildcards
if ( isMcpToolName ( toolName ) ) {
if ( toolName === ` ${ MCP_TOOL_PREFIX } * ` ) {
for ( const tool of parentToolRegistry . getAllTools ( ) ) {
if ( tool instanceof DiscoveredMCPTool ) {
registerToolInstance ( tool ) ;
}
}
return ;
}
const parsed = parseMcpToolName ( toolName ) ;
if ( parsed . serverName && parsed . toolName === '*' ) {
for ( const tool of parentToolRegistry . getToolsByServer (
parsed . serverName ,
) ) {
registerToolInstance ( tool ) ;
}
return ;
}
}
2026-01-24 01:30:18 +00:00
// If the tool is referenced by name, retrieve it from the parent
// registry and register it with the agent's isolated registry.
const tool = parentToolRegistry . getTool ( toolName ) ;
if ( tool ) {
2026-03-12 10:17:36 -04:00
registerToolInstance ( tool ) ;
2026-01-24 01:30:18 +00:00
}
} ;
2025-09-30 17:00:54 -04:00
if ( definition . toolConfig ) {
for ( const toolRef of definition . toolConfig . tools ) {
if ( typeof toolRef === 'string' ) {
2026-01-24 01:30:18 +00:00
registerToolByName ( toolRef ) ;
2025-09-30 17:00:54 -04:00
} else if (
typeof toolRef === 'object' &&
'name' in toolRef &&
'build' in toolRef
) {
agentToolRegistry . registerTool ( toolRef ) ;
}
// Note: Raw `FunctionDeclaration` objects in the config don't need to be
// registered; their schemas are passed directly to the model later.
}
2026-01-24 01:30:18 +00:00
} else {
// If no tools are explicitly configured, default to all available tools.
for ( const toolName of parentToolRegistry . getAllToolNames ( ) ) {
registerToolByName ( toolName ) ;
}
2025-09-30 17:00:54 -04:00
}
2026-01-24 01:30:18 +00:00
agentToolRegistry . sortTools ( ) ;
2025-10-08 15:42:33 -04:00
// Get the parent prompt ID from context
2026-03-10 18:12:59 -07:00
const parentPromptId = context . promptId ;
2025-10-08 15:42:33 -04:00
2026-01-26 17:12:55 -05:00
// Get the parent tool call ID from context
const toolContext = getToolCallContext ( ) ;
const parentCallId = toolContext ? . callId ;
2025-12-17 12:06:38 -05:00
return new LocalAgentExecutor (
2025-09-30 17:00:54 -04:00
definition ,
2026-03-10 18:12:59 -07:00
context ,
2025-09-30 17:00:54 -04:00
agentToolRegistry ,
2025-10-08 15:42:33 -04:00
parentPromptId ,
2026-01-26 17:12:55 -05:00
parentCallId ,
2025-09-30 17:00:54 -04:00
onActivity ,
) ;
}
/**
* Constructs a new AgentExecutor instance.
*
* @private This constructor is private. Use the static `create` method to
* instantiate the class.
*/
private constructor (
2025-12-17 12:06:38 -05:00
definition : LocalAgentDefinition < TOutput > ,
2026-03-10 18:12:59 -07:00
context : AgentLoopContext ,
2025-09-30 17:00:54 -04:00
toolRegistry : ToolRegistry ,
2025-10-08 15:42:33 -04:00
parentPromptId : string | undefined ,
2026-01-26 17:12:55 -05:00
parentCallId : string | undefined ,
2025-09-30 17:00:54 -04:00
onActivity? : ActivityCallback ,
) {
this . definition = definition ;
2026-03-10 18:12:59 -07:00
this . context = context ;
2025-09-30 17:00:54 -04:00
this . toolRegistry = toolRegistry ;
this . onActivity = onActivity ;
2025-11-05 16:15:28 -05:00
this . compressionService = new ChatCompressionService ( ) ;
2026-01-26 17:12:55 -05:00
this . parentCallId = parentCallId ;
2025-09-30 17:00:54 -04:00
const randomIdPart = Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) ;
2025-10-08 15:42:33 -04:00
// parentPromptId will be undefined if this agent is invoked directly
// (top-level), rather than as a sub-agent.
const parentPrefix = parentPromptId ? ` ${ parentPromptId } - ` : '' ;
this . agentId = ` ${ parentPrefix } ${ this . definition . name } - ${ randomIdPart } ` ;
2025-09-30 17:00:54 -04:00
}
2025-11-03 16:22:12 -05:00
/**
* Executes a single turn of the agent's logic, from calling the model
* to processing its response.
*
* @returns An {@link AgentTurnResult} object indicating whether to continue
* or stop the agent loop.
*/
private async executeTurn (
chat : GeminiChat ,
currentMessage : Content ,
turnCounter : number ,
combinedSignal : AbortSignal ,
timeoutSignal : AbortSignal , // Pass the timeout controller's signal
2026-02-07 23:03:47 -05:00
onWaitingForConfirmation ? : ( waiting : boolean ) = > void ,
2025-11-03 16:22:12 -05:00
) : Promise < AgentTurnResult > {
const promptId = ` ${ this . agentId } # ${ turnCounter } ` ;
2025-11-05 16:15:28 -05:00
await this . tryCompressChat ( chat , promptId ) ;
2025-11-03 16:22:12 -05:00
const { functionCalls } = await promptIdContext . run ( promptId , async ( ) = >
2025-12-01 10:54:28 -08:00
this . callModel ( chat , currentMessage , combinedSignal , promptId ) ,
2025-11-03 16:22:12 -05:00
) ;
if ( combinedSignal . aborted ) {
const terminateReason = timeoutSignal . aborted
? AgentTerminateMode.TIMEOUT
: AgentTerminateMode.ABORTED ;
return {
status : 'stop' ,
terminateReason ,
finalResult : null , // 'run' method will set the final timeout string
} ;
}
// If the model stops calling tools without calling complete_task, it's an error.
if ( functionCalls . length === 0 ) {
this . emitActivity ( 'ERROR' , {
error : ` Agent stopped calling tools but did not call ' ${ TASK_COMPLETE_TOOL_NAME } ' to finalize the session. ` ,
context : 'protocol_violation' ,
} ) ;
return {
status : 'stop' ,
terminateReason : AgentTerminateMode.ERROR_NO_COMPLETE_TASK_CALL ,
finalResult : null ,
} ;
}
2026-03-02 21:04:31 +00:00
const { nextMessage , submittedOutput , taskCompleted , aborted } =
2026-02-07 23:03:47 -05:00
await this . processFunctionCalls (
functionCalls ,
combinedSignal ,
promptId ,
onWaitingForConfirmation ,
) ;
2026-03-02 21:04:31 +00:00
if ( aborted ) {
return {
status : 'stop' ,
terminateReason : AgentTerminateMode.ABORTED ,
finalResult : null ,
} ;
}
2025-11-03 16:22:12 -05:00
if ( taskCompleted ) {
const finalResult = submittedOutput ? ? 'Task completed successfully.' ;
return {
status : 'stop' ,
terminateReason : AgentTerminateMode.GOAL ,
finalResult ,
} ;
}
// Task is not complete, continue to the next turn.
return {
status : 'continue' ,
nextMessage ,
} ;
}
/**
* Generates a specific warning message for the agent's final turn.
*/
private getFinalWarningMessage (
reason :
| AgentTerminateMode . TIMEOUT
| AgentTerminateMode . MAX_TURNS
| AgentTerminateMode . ERROR_NO_COMPLETE_TASK_CALL ,
) : string {
let explanation = '' ;
switch ( reason ) {
case AgentTerminateMode.TIMEOUT :
explanation = 'You have exceeded the time limit.' ;
break ;
case AgentTerminateMode.MAX_TURNS :
explanation = 'You have exceeded the maximum number of turns.' ;
break ;
case AgentTerminateMode.ERROR_NO_COMPLETE_TASK_CALL :
explanation = 'You have stopped calling tools without finishing.' ;
break ;
default :
throw new Error ( ` Unknown terminate reason: ${ reason } ` ) ;
}
return ` ${ explanation } You have one final chance to complete the task with a short grace period. You MUST call \` ${ TASK_COMPLETE_TOOL_NAME } \` immediately with your best answer and explain that your investigation was interrupted. Do not call any other tools. ` ;
}
/**
* Attempts a single, final recovery turn if the agent stops for a recoverable reason.
* Gives the agent a grace period to call `complete_task`.
*
* @returns The final result string if recovery was successful, or `null` if it failed.
*/
private async executeFinalWarningTurn (
chat : GeminiChat ,
turnCounter : number ,
reason :
| AgentTerminateMode . TIMEOUT
| AgentTerminateMode . MAX_TURNS
| AgentTerminateMode . ERROR_NO_COMPLETE_TASK_CALL ,
externalSignal : AbortSignal , // The original signal passed to run()
2026-02-07 23:03:47 -05:00
onWaitingForConfirmation ? : ( waiting : boolean ) = > void ,
2025-11-03 16:22:12 -05:00
) : Promise < string | null > {
this . emitActivity ( 'THOUGHT_CHUNK' , {
text : ` Execution limit reached ( ${ reason } ). Attempting one final recovery turn with a grace period. ` ,
} ) ;
2025-11-03 17:53:43 -05:00
const recoveryStartTime = Date . now ( ) ;
let success = false ;
2025-11-03 16:22:12 -05:00
const gracePeriodMs = GRACE_PERIOD_MS ;
const graceTimeoutController = new AbortController ( ) ;
const graceTimeoutId = setTimeout (
( ) = > graceTimeoutController . abort ( new Error ( 'Grace period timed out.' ) ) ,
gracePeriodMs ,
) ;
try {
const recoveryMessage : Content = {
role : 'user' ,
parts : [ { text : this.getFinalWarningMessage ( reason ) } ] ,
} ;
// We monitor both the external signal and our new grace period timeout
const combinedSignal = AbortSignal . any ( [
externalSignal ,
graceTimeoutController . signal ,
] ) ;
const turnResult = await this . executeTurn (
chat ,
recoveryMessage ,
turnCounter , // This will be the "last" turn number
combinedSignal ,
graceTimeoutController . signal , // Pass grace signal to identify a *grace* timeout
2026-02-07 23:03:47 -05:00
onWaitingForConfirmation ,
2025-11-03 16:22:12 -05:00
) ;
if (
turnResult . status === 'stop' &&
turnResult . terminateReason === AgentTerminateMode . GOAL
) {
// Success!
this . emitActivity ( 'THOUGHT_CHUNK' , {
text : 'Graceful recovery succeeded.' ,
} ) ;
2025-11-03 17:53:43 -05:00
success = true ;
2025-11-03 16:22:12 -05:00
return turnResult . finalResult ? ? 'Task completed during grace period.' ;
}
// Any other outcome (continue, error, non-GOAL stop) is a failure.
this . emitActivity ( 'ERROR' , {
error : ` Graceful recovery attempt failed. Reason: ${ turnResult . status } ` ,
context : 'recovery_turn' ,
} ) ;
return null ;
} catch ( error ) {
// This catch block will likely catch the 'Grace period timed out' error.
this . emitActivity ( 'ERROR' , {
error : ` Graceful recovery attempt failed: ${ String ( error ) } ` ,
context : 'recovery_turn' ,
} ) ;
return null ;
} finally {
clearTimeout ( graceTimeoutId ) ;
2025-11-03 17:53:43 -05:00
logRecoveryAttempt (
2026-03-10 18:12:59 -07:00
this . config ,
2025-11-03 17:53:43 -05:00
new RecoveryAttemptEvent (
this . agentId ,
this . definition . name ,
reason ,
Date . now ( ) - recoveryStartTime ,
success ,
turnCounter ,
) ,
) ;
2025-11-03 16:22:12 -05:00
}
}
2025-09-30 17:00:54 -04:00
/**
* Runs the agent.
*
* @param inputs The validated input parameters for this invocation.
* @param signal An `AbortSignal` for cancellation.
* @returns A promise that resolves to the agent's final output.
*/
async run ( inputs : AgentInputs , signal : AbortSignal ) : Promise < OutputObject > {
const startTime = Date . now ( ) ;
let turnCounter = 0 ;
2025-10-08 15:42:33 -04:00
let terminateReason : AgentTerminateMode = AgentTerminateMode . ERROR ;
let finalResult : string | null = null ;
2026-02-04 01:28:00 -05:00
const maxTimeMinutes =
this . definition . runConfig . maxTimeMinutes ? ? DEFAULT_MAX_TIME_MINUTES ;
const maxTurns = this . definition . runConfig . maxTurns ? ? DEFAULT_MAX_TURNS ;
2026-02-07 23:03:47 -05:00
const deadlineTimer = new DeadlineTimer (
2026-01-13 14:31:34 -08:00
maxTimeMinutes * 60 * 1000 ,
2026-02-07 23:03:47 -05:00
'Agent timed out.' ,
2025-11-03 15:33:04 -05:00
) ;
2026-02-07 23:03:47 -05:00
// Track time spent waiting for user confirmation to credit it back to the agent.
const onWaitingForConfirmation = ( waiting : boolean ) = > {
if ( waiting ) {
deadlineTimer . pause ( ) ;
} else {
deadlineTimer . resume ( ) ;
}
} ;
2025-11-03 15:33:04 -05:00
// Combine the external signal with the internal timeout signal.
2026-02-07 23:03:47 -05:00
const combinedSignal = AbortSignal . any ( [ signal , deadlineTimer . signal ] ) ;
2025-11-03 15:33:04 -05:00
2025-10-08 15:42:33 -04:00
logAgentStart (
2026-03-10 18:12:59 -07:00
this . config ,
2025-10-08 15:42:33 -04:00
new AgentStartEvent ( this . agentId , this . definition . name ) ,
) ;
2025-09-30 17:00:54 -04:00
2025-11-03 16:22:12 -05:00
let chat : GeminiChat | undefined ;
let tools : FunctionDeclaration [ ] | undefined ;
2025-09-30 17:00:54 -04:00
try {
2025-12-19 14:11:32 -08:00
// Inject standard runtime context into inputs
const augmentedInputs = {
. . . inputs ,
cliVersion : await getVersion ( ) ,
2026-03-10 18:12:59 -07:00
activeModel : this.config.getActiveModel ( ) ,
2025-12-19 14:11:32 -08:00
today : new Date ( ) . toLocaleDateString ( ) ,
} ;
2025-11-03 16:22:12 -05:00
tools = this . prepareToolsList ( ) ;
2025-12-19 14:11:32 -08:00
chat = await this . createChatObject ( augmentedInputs , tools ) ;
2025-10-01 16:21:01 -04:00
const query = this . definition . promptConfig . query
2025-12-19 14:11:32 -08:00
? templateString ( this . definition . promptConfig . query , augmentedInputs )
2026-01-21 16:56:01 -08:00
: DEFAULT_QUERY_STRING ;
2025-11-03 16:22:12 -05:00
2026-02-18 14:05:50 -08:00
const pendingHintsQueue : string [ ] = [ ] ;
const hintListener = ( hint : string ) = > {
pendingHintsQueue . push ( hint ) ;
} ;
// Capture the index of the last hint before starting to avoid re-injecting old hints.
// NOTE: Hints added AFTER this point will be broadcast to all currently running
// local agents via the listener below.
2026-03-10 18:12:59 -07:00
const startIndex = this . config . userHintService . getLatestHintIndex ( ) ;
this . config . userHintService . onUserHint ( hintListener ) ;
2026-02-18 14:05:50 -08:00
try {
const initialHints =
2026-03-10 18:12:59 -07:00
this . config . userHintService . getUserHintsAfter ( startIndex ) ;
2026-02-18 14:05:50 -08:00
const formattedInitialHints = formatUserHintsForModel ( initialHints ) ;
let currentMessage : Content = formattedInitialHints
? {
role : 'user' ,
parts : [ { text : formattedInitialHints } , { text : query } ] ,
}
: { role : 'user' , parts : [ { text : query } ] } ;
while ( true ) {
// Check for termination conditions like max turns.
const reason = this . checkTermination ( turnCounter , maxTurns ) ;
if ( reason ) {
terminateReason = reason ;
break ;
}
2025-09-30 17:00:54 -04:00
2026-02-18 14:05:50 -08:00
// Check for timeout or external abort.
if ( combinedSignal . aborted ) {
// Determine which signal caused the abort.
terminateReason = deadlineTimer . signal . aborted
? AgentTerminateMode.TIMEOUT
: AgentTerminateMode.ABORTED ;
break ;
}
const turnResult = await this . executeTurn (
chat ,
currentMessage ,
turnCounter ++ ,
combinedSignal ,
deadlineTimer . signal ,
onWaitingForConfirmation ,
) ;
2025-09-30 17:00:54 -04:00
2026-02-18 14:05:50 -08:00
if ( turnResult . status === 'stop' ) {
terminateReason = turnResult . terminateReason ;
// Only set finalResult if the turn provided one (e.g., error or goal).
if ( turnResult . finalResult ) {
finalResult = turnResult . finalResult ;
}
break ; // Exit the loop for *any* stop reason.
2025-11-03 16:22:12 -05:00
}
2025-09-30 17:00:54 -04:00
2026-02-18 14:05:50 -08:00
// If status is 'continue', update message for the next loop
currentMessage = turnResult . nextMessage ;
// Check for new user steering hints collected via subscription
if ( pendingHintsQueue . length > 0 ) {
const hintsToProcess = [ . . . pendingHintsQueue ] ;
pendingHintsQueue . length = 0 ;
const formattedHints = formatUserHintsForModel ( hintsToProcess ) ;
if ( formattedHints ) {
// Append hints to the current message (next turn)
currentMessage . parts ? ? = [ ] ;
currentMessage . parts . unshift ( { text : formattedHints } ) ;
}
}
}
} finally {
2026-03-10 18:12:59 -07:00
this . config . userHintService . offUserHint ( hintListener ) ;
2025-11-03 16:22:12 -05:00
}
2025-09-30 17:00:54 -04:00
2025-11-03 16:22:12 -05:00
// === UNIFIED RECOVERY BLOCK ===
// Only attempt recovery if it's a known recoverable reason.
// We don't recover from GOAL (already done) or ABORTED (user cancelled).
if (
terminateReason !== AgentTerminateMode . ERROR &&
terminateReason !== AgentTerminateMode . ABORTED &&
terminateReason !== AgentTerminateMode . GOAL
) {
const recoveryResult = await this . executeFinalWarningTurn (
chat ,
turnCounter , // Use current turnCounter for the recovery attempt
terminateReason ,
signal , // Pass the external signal
2026-02-07 23:03:47 -05:00
onWaitingForConfirmation ,
2025-11-03 16:22:12 -05:00
) ;
2025-10-02 14:07:58 -04:00
2025-11-03 16:22:12 -05:00
if ( recoveryResult !== null ) {
// Recovery Succeeded
2025-10-02 14:07:58 -04:00
terminateReason = AgentTerminateMode . GOAL ;
2025-11-03 16:22:12 -05:00
finalResult = recoveryResult ;
} else {
// Recovery Failed. Set the final error message based on the *original* reason.
if ( terminateReason === AgentTerminateMode . TIMEOUT ) {
2026-02-04 01:28:00 -05:00
finalResult = ` Agent timed out after ${ maxTimeMinutes } minutes. ` ;
2025-11-03 16:22:12 -05:00
this . emitActivity ( 'ERROR' , {
error : finalResult ,
context : 'timeout' ,
} ) ;
} else if ( terminateReason === AgentTerminateMode . MAX_TURNS ) {
2026-02-04 01:28:00 -05:00
finalResult = ` Agent reached max turns limit ( ${ maxTurns } ). ` ;
2025-11-03 16:22:12 -05:00
this . emitActivity ( 'ERROR' , {
error : finalResult ,
context : 'max_turns' ,
} ) ;
} else if (
terminateReason === AgentTerminateMode . ERROR_NO_COMPLETE_TASK_CALL
) {
// The finalResult was already set by executeTurn, but we re-emit just in case.
finalResult =
finalResult ||
` Agent stopped calling tools but did not call ' ${ TASK_COMPLETE_TOOL_NAME } '. ` ;
this . emitActivity ( 'ERROR' , {
error : finalResult ,
context : 'protocol_violation' ,
} ) ;
}
2025-10-02 14:07:58 -04:00
}
2025-11-03 15:33:04 -05:00
}
2025-11-03 16:22:12 -05:00
// === FINAL RETURN LOGIC ===
2025-10-02 14:07:58 -04:00
if ( terminateReason === AgentTerminateMode . GOAL ) {
2025-09-30 17:00:54 -04:00
return {
2025-10-02 14:07:58 -04:00
result : finalResult || 'Task completed.' ,
2025-09-30 17:00:54 -04:00
terminate_reason : terminateReason ,
} ;
}
return {
2025-10-02 14:07:58 -04:00
result :
finalResult || 'Agent execution was terminated before completion.' ,
2025-09-30 17:00:54 -04:00
terminate_reason : terminateReason ,
} ;
} catch ( error ) {
2025-11-03 15:33:04 -05:00
// Check if the error is an AbortError caused by our internal timeout.
if (
error instanceof Error &&
error . name === 'AbortError' &&
2026-02-07 23:03:47 -05:00
deadlineTimer . signal . aborted &&
2025-11-03 15:33:04 -05:00
! signal . aborted // Ensure the external signal was not the cause
) {
terminateReason = AgentTerminateMode . TIMEOUT ;
2025-11-03 16:22:12 -05:00
// Also use the unified recovery logic here
if ( chat && tools ) {
const recoveryResult = await this . executeFinalWarningTurn (
chat ,
turnCounter , // Use current turnCounter
AgentTerminateMode . TIMEOUT ,
signal ,
2026-02-07 23:03:47 -05:00
onWaitingForConfirmation ,
2025-11-03 16:22:12 -05:00
) ;
if ( recoveryResult !== null ) {
// Recovery Succeeded
terminateReason = AgentTerminateMode . GOAL ;
finalResult = recoveryResult ;
return {
result : finalResult ,
terminate_reason : terminateReason ,
} ;
}
}
// Recovery failed or wasn't possible
2026-02-04 01:28:00 -05:00
finalResult = ` Agent timed out after ${ maxTimeMinutes } minutes. ` ;
2025-11-03 15:33:04 -05:00
this . emitActivity ( 'ERROR' , {
error : finalResult ,
context : 'timeout' ,
} ) ;
return {
result : finalResult ,
terminate_reason : terminateReason ,
} ;
}
2025-09-30 17:00:54 -04:00
this . emitActivity ( 'ERROR' , { error : String ( error ) } ) ;
2025-11-03 15:33:04 -05:00
throw error ; // Re-throw other errors or external aborts.
2025-10-08 15:42:33 -04:00
} finally {
2026-02-07 23:03:47 -05:00
deadlineTimer . abort ( ) ;
2025-10-08 15:42:33 -04:00
logAgentFinish (
2026-03-10 18:12:59 -07:00
this . config ,
2025-10-08 15:42:33 -04:00
new AgentFinishEvent (
this . agentId ,
this . definition . name ,
Date . now ( ) - startTime ,
turnCounter ,
terminateReason ,
) ,
) ;
2025-09-30 17:00:54 -04:00
}
}
2025-11-05 16:15:28 -05:00
private async tryCompressChat (
chat : GeminiChat ,
prompt_id : string ,
) : Promise < void > {
2026-01-13 14:31:34 -08:00
const model = this . definition . modelConfig . model ? ? DEFAULT_GEMINI_MODEL ;
2025-11-05 16:15:28 -05:00
const { newHistory , info } = await this . compressionService . compress (
chat ,
prompt_id ,
false ,
model ,
2026-03-10 18:12:59 -07:00
this . config ,
2025-11-05 16:15:28 -05:00
this . hasFailedCompressionAttempt ,
) ;
if (
info . compressionStatus ===
CompressionStatus . COMPRESSION_FAILED_INFLATED_TOKEN_COUNT
) {
this . hasFailedCompressionAttempt = true ;
} else if ( info . compressionStatus === CompressionStatus . COMPRESSED ) {
if ( newHistory ) {
chat . setHistory ( newHistory ) ;
this . hasFailedCompressionAttempt = false ;
}
2026-02-19 19:06:36 +00:00
} else if ( info . compressionStatus === CompressionStatus . CONTENT_TRUNCATED ) {
if ( newHistory ) {
chat . setHistory ( newHistory ) ;
// Do NOT reset hasFailedCompressionAttempt.
// We only truncated content because summarization previously failed.
// We want to keep avoiding expensive summarization calls.
}
2025-11-05 16:15:28 -05:00
}
}
2025-09-30 17:00:54 -04:00
/**
* Calls the generative model with the current context and tools.
*
* @returns The model's response, including any tool calls or text.
*/
private async callModel (
chat : GeminiChat ,
2025-10-02 14:07:58 -04:00
message : Content ,
2025-09-30 17:00:54 -04:00
signal : AbortSignal ,
promptId : string ,
) : Promise < { functionCalls : FunctionCall [ ] ; textResponse : string } > {
2026-01-08 12:39:40 -08:00
const modelConfigAlias = getModelConfigAlias ( this . definition ) ;
// Resolve the model config early to get the concrete model string (which may be `auto`).
2026-03-10 18:12:59 -07:00
const resolvedConfig = this . config . modelConfigService . getResolvedConfig ( {
model : modelConfigAlias ,
overrideScope : this.definition.name ,
} ) ;
2026-01-08 12:39:40 -08:00
const requestedModel = resolvedConfig . model ;
let modelToUse : string ;
if ( isAutoModel ( requestedModel ) ) {
// TODO(joshualitt): This try / catch is inconsistent with the routing
// behavior for the main agent. Ideally, we would have a universal
// policy for routing failure. Given routing failure does not necessarily
// mean generation will fail, we may want to share this logic with
// other places we use model routing.
try {
const routingContext : RoutingContext = {
history : chat.getHistory ( /*curated=*/ true ) ,
request : message.parts || [ ] ,
signal ,
requestedModel ,
} ;
2026-03-10 18:12:59 -07:00
const router = this . config . getModelRouterService ( ) ;
2026-01-08 12:39:40 -08:00
const decision = await router . route ( routingContext ) ;
modelToUse = decision . model ;
} catch ( error ) {
debugLogger . warn ( ` Error during model routing: ${ error } ` ) ;
modelToUse = DEFAULT_GEMINI_MODEL ;
}
} else {
modelToUse = requestedModel ;
}
2026-02-17 12:32:30 -05:00
const role = LlmRole . SUBAGENT ;
2025-09-30 17:00:54 -04:00
const responseStream = await chat . sendMessageStream (
2025-11-19 20:41:16 -08:00
{
2026-01-08 12:39:40 -08:00
model : modelToUse ,
2025-11-19 20:41:16 -08:00
overrideScope : this.definition.name ,
} ,
message . parts || [ ] ,
2025-09-30 17:00:54 -04:00
promptId ,
2025-11-19 20:41:16 -08:00
signal ,
2026-02-17 12:32:30 -05:00
role ,
2025-09-30 17:00:54 -04:00
) ;
const functionCalls : FunctionCall [ ] = [ ] ;
let textResponse = '' ;
for await ( const resp of responseStream ) {
if ( signal . aborted ) break ;
if ( resp . type === StreamEventType . CHUNK ) {
const chunk = resp . value ;
const parts = chunk . candidates ? . [ 0 ] ? . content ? . parts ;
// Extract and emit any subject "thought" content from the model.
const { subject } = parseThought (
parts ? . find ( ( p ) = > p . thought ) ? . text || '' ,
) ;
if ( subject ) {
this . emitActivity ( 'THOUGHT_CHUNK' , { text : subject } ) ;
}
// Collect any function calls requested by the model.
if ( chunk . functionCalls ) {
functionCalls . push ( . . . chunk . functionCalls ) ;
}
// Handle text response (non-thought text)
const text =
parts
? . filter ( ( p ) = > ! p . thought && p . text )
. map ( ( p ) = > p . text )
. join ( '' ) || '' ;
if ( text ) {
textResponse += text ;
}
}
}
return { functionCalls , textResponse } ;
}
/** Initializes a `GeminiChat` instance for the agent run. */
2025-12-01 10:54:28 -08:00
private async createChatObject (
inputs : AgentInputs ,
tools : FunctionDeclaration [ ] ,
) : Promise < GeminiChat > {
2025-11-19 20:41:16 -08:00
const { promptConfig } = this . definition ;
2025-09-30 17:00:54 -04:00
if ( ! promptConfig . systemPrompt && ! promptConfig . initialMessages ) {
throw new Error (
'PromptConfig must define either `systemPrompt` or `initialMessages`.' ,
) ;
}
2025-10-01 16:21:01 -04:00
const startHistory = this . applyTemplateToInitialMessages (
promptConfig . initialMessages ? ? [ ] ,
inputs ,
) ;
2025-09-30 17:00:54 -04:00
// Build system instruction from the templated prompt string.
const systemInstruction = promptConfig . systemPrompt
? await this . buildSystemPrompt ( inputs )
: undefined ;
try {
return new GeminiChat (
2026-03-10 18:12:59 -07:00
this . config ,
2025-11-19 20:41:16 -08:00
systemInstruction ,
2025-12-01 10:54:28 -08:00
[ { functionDeclarations : tools } ] ,
2025-09-30 17:00:54 -04:00
startHistory ,
2026-02-21 12:41:27 -05:00
undefined ,
undefined ,
'subagent' ,
2025-09-30 17:00:54 -04:00
) ;
2026-02-21 12:41:27 -05:00
} catch ( e : unknown ) {
2025-09-30 17:00:54 -04:00
await reportError (
2026-02-21 12:41:27 -05:00
e ,
2025-09-30 17:00:54 -04:00
` Error initializing Gemini chat for agent ${ this . definition . name } . ` ,
startHistory ,
'startChat' ,
) ;
// Re-throw as a more specific error after reporting.
2026-02-21 12:41:27 -05:00
throw new Error ( ` Failed to create chat object: ${ getErrorMessage ( e ) } ` ) ;
2025-09-30 17:00:54 -04:00
}
}
/**
* Executes function calls requested by the model and returns the results.
*
2025-10-02 14:07:58 -04:00
* @returns A new `Content` object for history, any submitted output, and completion status.
2025-09-30 17:00:54 -04:00
*/
private async processFunctionCalls (
functionCalls : FunctionCall [ ] ,
signal : AbortSignal ,
promptId : string ,
2026-02-07 23:03:47 -05:00
onWaitingForConfirmation ? : ( waiting : boolean ) = > void ,
2025-10-02 14:07:58 -04:00
) : Promise < {
nextMessage : Content ;
submittedOutput : string | null ;
taskCompleted : boolean ;
2026-03-02 21:04:31 +00:00
aborted : boolean ;
2025-10-02 14:07:58 -04:00
} > {
2025-09-30 17:00:54 -04:00
const allowedToolNames = new Set ( this . toolRegistry . getAllToolNames ( ) ) ;
2025-10-02 14:07:58 -04:00
// Always allow the completion tool
allowedToolNames . add ( TASK_COMPLETE_TOOL_NAME ) ;
2025-09-30 17:00:54 -04:00
2025-10-02 14:07:58 -04:00
let submittedOutput : string | null = null ;
let taskCompleted = false ;
2026-03-02 21:04:31 +00:00
let aborted = false ;
2025-10-02 14:07:58 -04:00
2026-01-26 17:12:55 -05:00
// We'll separate complete_task from other tools
const toolRequests : ToolCallRequestInfo [ ] = [ ] ;
// Map to keep track of tool name by callId for activity emission
const toolNameMap = new Map < string , string > ( ) ;
// Synchronous results (like complete_task or unauthorized calls)
const syncResults = new Map < string , Part > ( ) ;
2025-10-02 14:07:58 -04:00
for ( const [ index , functionCall ] of functionCalls . entries ( ) ) {
const callId = functionCall . id ? ? ` ${ promptId } - ${ index } ` ;
2025-12-12 17:43:43 -08:00
const args = functionCall . args ? ? { } ;
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-26 17:12:55 -05:00
const toolName = functionCall . name as string ;
2025-10-02 14:07:58 -04:00
2026-03-02 21:04:31 +00:00
let displayName = toolName ;
let description : string | undefined = undefined ;
try {
const tool = this . toolRegistry . getTool ( toolName ) ;
if ( tool ) {
displayName = tool . displayName ? ? toolName ;
const invocation = tool . build ( args ) ;
description = invocation . getDescription ( ) ;
}
} catch {
// Ignore errors during formatting for activity emission
}
2025-10-02 14:07:58 -04:00
this . emitActivity ( 'TOOL_CALL_START' , {
2026-01-26 17:12:55 -05:00
name : toolName ,
2026-03-02 21:04:31 +00:00
displayName ,
description ,
2025-10-02 14:07:58 -04:00
args ,
2026-03-09 22:56:00 +05:30
callId ,
2025-10-02 14:07:58 -04:00
} ) ;
2026-01-26 17:12:55 -05:00
if ( toolName === TASK_COMPLETE_TOOL_NAME ) {
2025-10-02 14:07:58 -04:00
if ( taskCompleted ) {
const error =
'Task already marked complete in this turn. Ignoring duplicate call.' ;
2026-01-26 17:12:55 -05:00
syncResults . set ( callId , {
2025-10-02 14:07:58 -04:00
functionResponse : {
name : TASK_COMPLETE_TOOL_NAME ,
response : { error } ,
id : callId ,
} ,
} ) ;
this . emitActivity ( 'ERROR' , {
context : 'tool_call' ,
2026-01-26 17:12:55 -05:00
name : toolName ,
2025-10-02 14:07:58 -04:00
error ,
} ) ;
continue ;
}
const { outputConfig } = this . definition ;
taskCompleted = true ; // Signal completion regardless of output presence
if ( outputConfig ) {
const outputName = outputConfig . outputName ;
if ( args [ outputName ] !== undefined ) {
2025-10-03 13:21:08 -04:00
const outputValue = args [ outputName ] ;
const validationResult = outputConfig . schema . safeParse ( outputValue ) ;
if ( ! validationResult . success ) {
taskCompleted = false ; // Validation failed, revoke completion
const error = ` Output validation failed: ${ JSON . stringify ( validationResult . error . flatten ( ) ) } ` ;
2026-01-26 17:12:55 -05:00
syncResults . set ( callId , {
2025-10-03 13:21:08 -04:00
functionResponse : {
name : TASK_COMPLETE_TOOL_NAME ,
response : { error } ,
id : callId ,
} ,
} ) ;
this . emitActivity ( 'ERROR' , {
context : 'tool_call' ,
2026-01-26 17:12:55 -05:00
name : toolName ,
2025-10-03 13:21:08 -04:00
error ,
} ) ;
continue ;
}
2026-02-20 22:28:55 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
2025-10-03 13:21:08 -04:00
const validatedOutput = validationResult . data ;
if ( this . definition . processOutput ) {
submittedOutput = this . definition . processOutput ( validatedOutput ) ;
} else {
submittedOutput =
typeof outputValue === 'string'
? outputValue
: JSON.stringify ( outputValue , null , 2 ) ;
}
2026-01-26 17:12:55 -05:00
syncResults . set ( callId , {
2025-10-02 14:07:58 -04:00
functionResponse : {
name : TASK_COMPLETE_TOOL_NAME ,
response : { result : 'Output submitted and task completed.' } ,
id : callId ,
} ,
} ) ;
this . emitActivity ( 'TOOL_CALL_END' , {
2026-01-26 17:12:55 -05:00
name : toolName ,
2026-03-09 22:56:00 +05:30
id : callId ,
2025-10-02 14:07:58 -04:00
output : 'Output submitted and task completed.' ,
} ) ;
} else {
// Failed to provide required output.
taskCompleted = false ; // Revoke completion status
const error = ` Missing required argument ' ${ outputName } ' for completion. ` ;
2026-01-26 17:12:55 -05:00
syncResults . set ( callId , {
2025-10-02 14:07:58 -04:00
functionResponse : {
name : TASK_COMPLETE_TOOL_NAME ,
response : { error } ,
id : callId ,
} ,
} ) ;
this . emitActivity ( 'ERROR' , {
context : 'tool_call' ,
2026-01-26 17:12:55 -05:00
name : toolName ,
2026-03-09 22:56:00 +05:30
callId ,
2025-10-02 14:07:58 -04:00
error ,
} ) ;
}
} else {
2025-12-17 22:46:55 -05:00
// No outputConfig - use default 'result' parameter
const resultArg = args [ 'result' ] ;
if (
resultArg !== undefined &&
resultArg !== null &&
resultArg !== ''
) {
submittedOutput =
typeof resultArg === 'string'
? resultArg
: JSON.stringify ( resultArg , null , 2 ) ;
2026-01-26 17:12:55 -05:00
syncResults . set ( callId , {
2025-12-17 22:46:55 -05:00
functionResponse : {
name : TASK_COMPLETE_TOOL_NAME ,
response : { status : 'Result submitted and task completed.' } ,
id : callId ,
} ,
} ) ;
this . emitActivity ( 'TOOL_CALL_END' , {
2026-01-26 17:12:55 -05:00
name : toolName ,
2026-03-09 22:56:00 +05:30
id : callId ,
2025-12-17 22:46:55 -05:00
output : 'Result submitted and task completed.' ,
} ) ;
} else {
// No result provided - this is an error for agents expected to return results
taskCompleted = false ; // Revoke completion
const error =
'Missing required "result" argument. You must provide your findings when calling complete_task.' ;
2026-01-26 17:12:55 -05:00
syncResults . set ( callId , {
2025-12-17 22:46:55 -05:00
functionResponse : {
name : TASK_COMPLETE_TOOL_NAME ,
response : { error } ,
id : callId ,
} ,
} ) ;
this . emitActivity ( 'ERROR' , {
context : 'tool_call' ,
2026-01-26 17:12:55 -05:00
name : toolName ,
2026-03-09 22:56:00 +05:30
callId ,
2025-12-17 22:46:55 -05:00
error ,
} ) ;
}
2025-10-02 14:07:58 -04:00
}
continue ;
2025-09-30 17:00:54 -04:00
}
2025-10-02 14:07:58 -04:00
// Handle standard tools
2026-01-26 17:12:55 -05:00
if ( ! allowedToolNames . has ( toolName ) ) {
const error = createUnauthorizedToolError ( toolName ) ;
2025-12-17 12:06:38 -05:00
debugLogger . warn ( ` [LocalAgentExecutor] Blocked call: ${ error } ` ) ;
2025-10-02 14:07:58 -04:00
2026-01-26 17:12:55 -05:00
syncResults . set ( callId , {
2025-10-02 14:07:58 -04:00
functionResponse : {
2026-01-26 17:12:55 -05:00
name : toolName ,
2025-10-02 14:07:58 -04:00
id : callId ,
response : { error } ,
} ,
2025-09-30 17:00:54 -04:00
} ) ;
2025-10-02 14:07:58 -04:00
this . emitActivity ( 'ERROR' , {
context : 'tool_call_unauthorized' ,
2026-01-26 17:12:55 -05:00
name : toolName ,
2025-10-01 16:54:00 -04:00
callId ,
2025-10-02 14:07:58 -04:00
error ,
} ) ;
continue ;
}
2025-10-01 16:54:00 -04:00
2026-01-26 17:12:55 -05:00
toolRequests . push ( {
2025-10-02 14:07:58 -04:00
callId ,
2026-01-26 17:12:55 -05:00
name : toolName ,
2025-10-02 14:07:58 -04:00
args ,
2026-01-26 17:12:55 -05:00
isClientInitiated : false , // These are coming from the subagent (the "model")
2025-10-02 14:07:58 -04:00
prompt_id : promptId ,
2026-01-26 17:12:55 -05:00
} ) ;
toolNameMap . set ( callId , toolName ) ;
}
2025-12-17 22:46:55 -05:00
2026-01-26 17:12:55 -05:00
// Execute standard tool calls using the new scheduler
if ( toolRequests . length > 0 ) {
const completedCalls = await scheduleAgentTools (
2026-03-10 18:12:59 -07:00
this . config ,
2026-01-26 17:12:55 -05:00
toolRequests ,
{
schedulerId : this.agentId ,
2026-03-11 16:01:45 -07:00
subagent : this.definition.name ,
2026-01-26 17:12:55 -05:00
parentCallId : this.parentCallId ,
toolRegistry : this.toolRegistry ,
2025-10-01 16:54:00 -04:00
signal ,
2026-02-07 23:03:47 -05:00
onWaitingForConfirmation ,
2026-01-26 17:12:55 -05:00
} ,
) ;
2025-10-01 16:54:00 -04:00
2026-01-26 17:12:55 -05:00
for ( const call of completedCalls ) {
const toolName =
toolNameMap . get ( call . request . callId ) || call . request . name ;
if ( call . status === 'success' ) {
this . emitActivity ( 'TOOL_CALL_END' , {
name : toolName ,
2026-03-09 22:56:00 +05:30
id : call.request.callId ,
2026-01-26 17:12:55 -05:00
output : call.response.resultDisplay ,
} ) ;
} else if ( call . status === 'error' ) {
2025-10-01 16:54:00 -04:00
this . emitActivity ( 'ERROR' , {
context : 'tool_call' ,
2026-01-26 17:12:55 -05:00
name : toolName ,
2026-03-09 22:56:00 +05:30
callId : call.request.callId ,
2026-01-26 17:12:55 -05:00
error : call.response.error?.message || 'Unknown error' ,
2025-10-01 16:54:00 -04:00
} ) ;
2026-01-26 17:12:55 -05:00
} else if ( call . status === 'cancelled' ) {
this . emitActivity ( 'ERROR' , {
context : 'tool_call' ,
name : toolName ,
2026-03-09 22:56:00 +05:30
callId : call.request.callId ,
2026-03-02 21:04:31 +00:00
error : 'Request cancelled.' ,
2025-10-01 16:54:00 -04:00
} ) ;
2026-03-02 21:04:31 +00:00
aborted = true ;
2025-10-01 16:54:00 -04:00
}
2026-01-26 17:12:55 -05:00
// Add result to syncResults to preserve order later
syncResults . set ( call . request . callId , call . response . responseParts [ 0 ] ) ;
}
2025-10-02 14:07:58 -04:00
}
2026-01-26 17:12:55 -05:00
// Reconstruct toolResponseParts in the original order
const toolResponseParts : Part [ ] = [ ] ;
for ( const [ index , functionCall ] of functionCalls . entries ( ) ) {
const callId = functionCall . id ? ? ` ${ promptId } - ${ index } ` ;
const part = syncResults . get ( callId ) ;
if ( part ) {
toolResponseParts . push ( part ) ;
2025-10-02 14:07:58 -04:00
}
}
2025-09-30 17:00:54 -04:00
2025-10-02 14:07:58 -04:00
// If all authorized tool calls failed (and task isn't complete), provide a generic error.
if (
functionCalls . length > 0 &&
toolResponseParts . length === 0 &&
! taskCompleted
) {
2025-09-30 17:00:54 -04:00
toolResponseParts . push ( {
2025-10-02 14:07:58 -04:00
text : 'All tool calls failed or were unauthorized. Please analyze the errors and try an alternative approach.' ,
2025-09-30 17:00:54 -04:00
} ) ;
}
2025-10-02 14:07:58 -04:00
return {
nextMessage : { role : 'user' , parts : toolResponseParts } ,
submittedOutput ,
taskCompleted ,
2026-03-02 21:04:31 +00:00
aborted ,
2025-10-02 14:07:58 -04:00
} ;
2025-09-30 17:00:54 -04:00
}
/**
* Prepares the list of tool function declarations to be sent to the model.
*/
private prepareToolsList ( ) : FunctionDeclaration [ ] {
const toolsList : FunctionDeclaration [ ] = [ ] ;
2025-10-02 14:07:58 -04:00
const { toolConfig , outputConfig } = this . definition ;
2025-09-30 17:00:54 -04:00
if ( toolConfig ) {
for ( const toolRef of toolConfig . tools ) {
2026-03-12 15:09:23 -04:00
if ( typeof toolRef === 'object' && ! ( 'schema' in toolRef ) ) {
2025-09-30 17:00:54 -04:00
// Raw `FunctionDeclaration` object.
2025-12-12 17:43:43 -08:00
toolsList . push ( toolRef ) ;
2025-09-30 17:00:54 -04:00
}
}
2026-03-12 15:09:23 -04:00
// Add schemas from tools that were explicitly registered by name, wildcard, or instance.
2026-03-12 10:17:36 -04:00
toolsList . push ( . . . this . toolRegistry . getFunctionDeclarations ( ) ) ;
2025-09-30 17:00:54 -04:00
}
2025-10-02 14:07:58 -04:00
// Always inject complete_task.
// Configure its schema based on whether output is expected.
const completeTool : FunctionDeclaration = {
name : TASK_COMPLETE_TOOL_NAME ,
description : outputConfig
? 'Call this tool to submit your final answer and complete the task. This is the ONLY way to finish.'
2025-12-17 22:46:55 -05:00
: 'Call this tool to submit your final findings and complete the task. This is the ONLY way to finish.' ,
2025-10-02 14:07:58 -04:00
parameters : {
type : Type . OBJECT ,
properties : { } ,
required : [ ] ,
} ,
} ;
if ( outputConfig ) {
2025-10-03 13:21:08 -04:00
const jsonSchema = zodToJsonSchema ( outputConfig . schema ) ;
const {
$schema : _$schema ,
definitions : _definitions ,
. . . schema
} = jsonSchema ;
completeTool . parameters ! . properties ! [ outputConfig . outputName ] =
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-10-03 13:21:08 -04:00
schema as Schema ;
2025-10-02 14:07:58 -04:00
completeTool . parameters ! . required ! . push ( outputConfig . outputName ) ;
2025-12-17 22:46:55 -05:00
} else {
completeTool . parameters ! . properties ! [ 'result' ] = {
type : Type . STRING ,
description :
'Your final results or findings to return to the orchestrator. ' +
'Ensure this is comprehensive and follows any formatting requested in your instructions.' ,
} ;
completeTool . parameters ! . required ! . push ( 'result' ) ;
2025-10-02 14:07:58 -04:00
}
toolsList . push ( completeTool ) ;
2025-09-30 17:00:54 -04:00
return toolsList ;
}
/** Builds the system prompt from the agent definition and inputs. */
private async buildSystemPrompt ( inputs : AgentInputs ) : Promise < string > {
2025-10-02 14:07:58 -04:00
const { promptConfig } = this . definition ;
2025-09-30 17:00:54 -04:00
if ( ! promptConfig . systemPrompt ) {
return '' ;
}
// Inject user inputs into the prompt template.
let finalPrompt = templateString ( promptConfig . systemPrompt , inputs ) ;
// Append environment context (CWD and folder structure).
2026-03-10 18:12:59 -07:00
const dirContext = await getDirectoryContextString ( this . config ) ;
2025-09-30 17:00:54 -04:00
finalPrompt += ` \ n \ n# Environment Context \ n ${ dirContext } ` ;
// Append standard rules for non-interactive execution.
finalPrompt += `
Important Rules:
* You are running in a non-interactive mode. You CANNOT ask the user for input or clarification.
* Work systematically using available tools to complete your task.
2025-10-02 14:07:58 -04:00
* Always use absolute paths for file operations. Construct them using the provided "Environment Context". ` ;
2025-09-30 17:00:54 -04:00
2025-12-17 22:46:55 -05:00
if ( this . definition . outputConfig ) {
finalPrompt += `
* When you have completed your task, you MUST call the \` ${ TASK_COMPLETE_TOOL_NAME } \` tool with your structured output.
* Do not call any other tools in the same turn as \` ${ TASK_COMPLETE_TOOL_NAME } \` .
* This is the ONLY way to complete your mission. If you stop calling tools without calling this, you have failed. ` ;
} else {
finalPrompt += `
2025-10-02 14:07:58 -04:00
* When you have completed your task, you MUST call the \` ${ TASK_COMPLETE_TOOL_NAME } \` tool.
2025-12-17 22:46:55 -05:00
* You MUST include your final findings in the "result" parameter. This is how you return the necessary results for the task to be marked complete.
* Ensure your findings are comprehensive and follow any specific formatting requirements provided in your instructions.
2025-10-02 14:07:58 -04:00
* Do not call any other tools in the same turn as \` ${ TASK_COMPLETE_TOOL_NAME } \` .
* This is the ONLY way to complete your mission. If you stop calling tools without calling this, you have failed. ` ;
2025-12-17 22:46:55 -05:00
}
2025-09-30 17:00:54 -04:00
2025-10-02 14:07:58 -04:00
return finalPrompt ;
2025-09-30 17:00:54 -04:00
}
/**
2025-10-01 16:21:01 -04:00
* Applies template strings to initial messages.
*
* @param initialMessages The initial messages from the prompt config.
* @param inputs The validated input parameters for this invocation.
* @returns A new array of `Content` with templated strings.
*/
private applyTemplateToInitialMessages (
initialMessages : Content [ ] ,
inputs : AgentInputs ,
) : Content [ ] {
return initialMessages . map ( ( content ) = > {
const newParts = ( content . parts ? ? [ ] ) . map ( ( part ) = > {
if ( 'text' in part && part . text !== undefined ) {
return { text : templateString ( part . text , inputs ) } ;
}
return part ;
} ) ;
return { . . . content , parts : newParts } ;
} ) ;
}
2025-09-30 17:00:54 -04:00
/**
* Checks if the agent should terminate due to exceeding configured limits.
*
* @returns The reason for termination, or `null` if execution can continue.
*/
private checkTermination (
turnCounter : number ,
2026-02-04 01:28:00 -05:00
maxTurns : number ,
2025-09-30 17:00:54 -04:00
) : AgentTerminateMode | null {
2026-02-04 01:28:00 -05:00
if ( turnCounter >= maxTurns ) {
2025-09-30 17:00:54 -04:00
return AgentTerminateMode . MAX_TURNS ;
}
return null ;
}
/** Emits an activity event to the configured callback. */
private emitActivity (
type : SubagentActivityEvent [ 'type' ] ,
data : Record < string , unknown > ,
) : void {
if ( this . onActivity ) {
const event : SubagentActivityEvent = {
isSubagentActivityEvent : true ,
agentName : this.definition.name ,
type ,
data ,
} ;
this . onActivity ( event ) ;
}
}
}