/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { reportError } from '../utils/errorReporting.js'; import { Config } from '../config/config.js'; import { ToolCallRequestInfo } from '../core/turn.js'; import { executeToolCall } from '../core/nonInteractiveToolExecutor.js'; import { createContentGenerator } from '../core/contentGenerator.js'; import { getEnvironmentContext } from '../utils/environmentContext.js'; import { Content, Part, FunctionCall, GenerateContentConfig, FunctionDeclaration, GenerateContentResponseUsageMetadata, } from '@google/genai'; import { GeminiChat } from '../core/geminiChat.js'; import { SubAgentEventEmitter } from './subagent-events.js'; import { formatCompact, formatDetailed } from './subagent-result-format.js'; import { SubagentStatistics } from './subagent-statistics.js'; import { SubagentHooks } from './subagent-hooks.js'; import { logSubagentExecution } from '../telemetry/loggers.js'; import { SubagentExecutionEvent } from '../telemetry/types.js'; /** * @fileoverview Defines the configuration interfaces for a subagent. * * These interfaces specify the structure for defining the subagent's prompt, * the model parameters, and the execution settings. */ interface ExecutionStats { startTimeMs: number; totalDurationMs: number; rounds: number; totalToolCalls: number; successfulToolCalls: number; failedToolCalls: number; inputTokens?: number; outputTokens?: number; totalTokens?: number; estimatedCost?: number; } /** * Describes the possible termination modes for a subagent. * This enum provides a clear indication of why a subagent's execution might have ended. */ export enum SubagentTerminateMode { /** * Indicates that the subagent's execution terminated due to an unrecoverable error. */ ERROR = 'ERROR', /** * Indicates that the subagent's execution terminated because it exceeded the maximum allowed working time. */ TIMEOUT = 'TIMEOUT', /** * Indicates that the subagent's execution successfully completed all its defined goals. */ GOAL = 'GOAL', /** * Indicates that the subagent's execution terminated because it exceeded the maximum number of turns. */ MAX_TURNS = 'MAX_TURNS', } /** * Represents the output structure of a subagent's execution. * This interface defines the data that a subagent will return upon completion, * including the final result and the reason for its termination. */ export interface OutputObject { /** * The final result text returned by the subagent upon completion. * This contains the direct output from the model's final response. */ result: string; /** * The reason for the subagent's termination, indicating whether it completed * successfully, timed out, or encountered an error. */ terminate_reason: SubagentTerminateMode; } /** * Configures the initial prompt for the subagent. */ export interface PromptConfig { /** * A single system prompt string that defines the subagent's persona and instructions. * Note: You should use either `systemPrompt` or `initialMessages`, but not both. */ systemPrompt?: string; /** * An array of user/model content pairs to seed the chat history for few-shot prompting. * Note: You should use either `systemPrompt` or `initialMessages`, but not both. */ initialMessages?: Content[]; } /** * Configures the tools available to the subagent during its execution. */ export interface ToolConfig { /** * A list of tool names (from the tool registry) or full function declarations * that the subagent is permitted to use. */ tools: Array; } /** * Configures the generative model parameters for the subagent. * This interface specifies the model to be used and its associated generation settings, * such as temperature and top-p values, which influence the creativity and diversity of the model's output. */ export interface ModelConfig { /** * The name or identifier of the model to be used (e.g., 'gemini-2.5-pro'). * * TODO: In the future, this needs to support 'auto' or some other string to support routing use cases. */ model?: string; /** * The temperature for the model's sampling process. */ temp?: number; /** * The top-p value for nucleus sampling. */ top_p?: number; } /** * Configures the execution environment and constraints for the subagent. * This interface defines parameters that control the subagent's runtime behavior, * such as maximum execution time, to prevent infinite loops or excessive resource consumption. * * TODO: Consider adding max_tokens as a form of budgeting. */ export interface RunConfig { /** The maximum execution time for the subagent in minutes. */ max_time_minutes?: number; /** * The maximum number of conversational turns (a user message + model response) * before the execution is terminated. Helps prevent infinite loops. */ max_turns?: number; } /** * Manages the runtime context state for the subagent. * This class provides a mechanism to store and retrieve key-value pairs * that represent the dynamic state and variables accessible to the subagent * during its execution. */ export class ContextState { private state: Record = {}; /** * Retrieves a value from the context state. * * @param key - The key of the value to retrieve. * @returns The value associated with the key, or undefined if the key is not found. */ get(key: string): unknown { return this.state[key]; } /** * Sets a value in the context state. * * @param key - The key to set the value under. * @param value - The value to set. */ set(key: string, value: unknown): void { this.state[key] = value; } /** * Retrieves all keys in the context state. * * @returns An array of all keys in the context state. */ get_keys(): string[] { return Object.keys(this.state); } } /** * Replaces `${...}` placeholders in a template string with values from a context. * * This function identifies all placeholders in the format `${key}`, validates that * each key exists in the provided `ContextState`, and then performs the substitution. * * @param template The template string containing placeholders. * @param context The `ContextState` object providing placeholder values. * @returns The populated string with all placeholders replaced. * @throws {Error} if any placeholder key is not found in the context. */ function templateString(template: string, context: ContextState): string { const placeholderRegex = /\$\{(\w+)\}/g; // First, find all unique keys required by the template. const requiredKeys = new Set( Array.from(template.matchAll(placeholderRegex), (match) => match[1]), ); // Check if all required keys exist in the context. const contextKeys = new Set(context.get_keys()); const missingKeys = Array.from(requiredKeys).filter( (key) => !contextKeys.has(key), ); if (missingKeys.length > 0) { throw new Error( `Missing context values for the following keys: ${missingKeys.join( ', ', )}`, ); } // Perform the replacement using a replacer function. return template.replace(placeholderRegex, (_match, key) => String(context.get(key)), ); } /** * Represents the scope and execution environment for a subagent. * This class orchestrates the subagent's lifecycle, managing its chat interactions, * runtime context, and the collection of its outputs. */ export class SubAgentScope { output: OutputObject = { terminate_reason: SubagentTerminateMode.ERROR, result: '', }; executionStats: ExecutionStats = { startTimeMs: 0, totalDurationMs: 0, rounds: 0, totalToolCalls: 0, successfulToolCalls: 0, failedToolCalls: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, estimatedCost: 0, }; private toolUsage = new Map< string, { count: number; success: number; failure: number; lastError?: string; totalDurationMs?: number; averageDurationMs?: number; } >(); private eventEmitter?: SubAgentEventEmitter; private finalText: string = ''; private readonly stats = new SubagentStatistics(); private hooks?: SubagentHooks; private readonly subagentId: string; /** * Constructs a new SubAgentScope instance. * @param name - The name for the subagent, used for logging and identification. * @param runtimeContext - The shared runtime configuration and services. * @param promptConfig - Configuration for the subagent's prompt and behavior. * @param modelConfig - Configuration for the generative model parameters. * @param runConfig - Configuration for the subagent's execution environment. * @param toolConfig - Optional configuration for tools available to the subagent. */ private constructor( readonly name: string, readonly runtimeContext: Config, private readonly promptConfig: PromptConfig, private readonly modelConfig: ModelConfig, private readonly runConfig: RunConfig, private readonly toolConfig?: ToolConfig, eventEmitter?: SubAgentEventEmitter, hooks?: SubagentHooks, ) { const randomPart = Math.random().toString(36).slice(2, 8); this.subagentId = `${this.name}-${randomPart}`; this.eventEmitter = eventEmitter; this.hooks = hooks; } /** * Creates and validates a new SubAgentScope instance. * This factory method ensures that all tools provided in the prompt configuration * are valid for non-interactive use before creating the subagent instance. * @param {string} name - The name of the subagent. * @param {Config} runtimeContext - The shared runtime configuration and services. * @param {PromptConfig} promptConfig - Configuration for the subagent's prompt and behavior. * @param {ModelConfig} modelConfig - Configuration for the generative model parameters. * @param {RunConfig} runConfig - Configuration for the subagent's execution environment. * @param {ToolConfig} [toolConfig] - Optional configuration for tools. * @returns {Promise} A promise that resolves to a valid SubAgentScope instance. * @throws {Error} If any tool requires user confirmation. */ static async create( name: string, runtimeContext: Config, promptConfig: PromptConfig, modelConfig: ModelConfig, runConfig: RunConfig, toolConfig?: ToolConfig, eventEmitter?: SubAgentEventEmitter, hooks?: SubagentHooks, ): Promise { // Validate tools for non-interactive use if (toolConfig?.tools) { const toolRegistry = runtimeContext.getToolRegistry(); for (const toolItem of toolConfig.tools) { if (typeof toolItem !== 'string') { continue; // Skip inline function declarations } const tool = toolRegistry.getTool(toolItem); if (!tool) { continue; // Skip unknown tools } // Check if tool has required parameters const hasRequiredParams = tool.schema?.parameters?.required && Array.isArray(tool.schema.parameters.required) && tool.schema.parameters.required.length > 0; if (hasRequiredParams) { // Can't check interactivity without parameters, log warning and continue console.warn( `Cannot check tool "${toolItem}" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.`, ); continue; } // Try to build the tool to check if it requires confirmation try { const toolInstance = tool.build({}); const confirmationDetails = await toolInstance.shouldConfirmExecute( new AbortController().signal, ); if (confirmationDetails) { throw new Error( `Tool "${toolItem}" requires user confirmation and cannot be used in a non-interactive subagent.`, ); } } catch (error) { // If we can't build the tool, assume it's safe if ( error instanceof Error && error.message.includes('requires user confirmation') ) { throw error; // Re-throw confirmation errors } // For other build errors, log warning and continue console.warn( `Cannot check tool "${toolItem}" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.`, ); } } } return new SubAgentScope( name, runtimeContext, promptConfig, modelConfig, runConfig, toolConfig, eventEmitter, hooks, ); } /** * Runs the subagent in a non-interactive mode. * This method orchestrates the subagent's execution loop, including prompt templating, * tool execution, and termination conditions. * @param {ContextState} context - The current context state containing variables for prompt templating. * @returns {Promise} A promise that resolves when the subagent has completed its execution. */ async runNonInteractive( context: ContextState, externalSignal?: AbortSignal, ): Promise { const chat = await this.createChatObject(context); if (!chat) { this.output.terminate_reason = SubagentTerminateMode.ERROR; return; } const abortController = new AbortController(); const onAbort = () => abortController.abort(); if (externalSignal) { if (externalSignal.aborted) abortController.abort(); externalSignal.addEventListener('abort', onAbort, { once: true }); } const toolRegistry = this.runtimeContext.getToolRegistry(); // Prepare the list of tools available to the subagent. // If no explicit toolConfig or it contains "*" or is empty, inherit all tools. const toolsList: FunctionDeclaration[] = []; if (this.toolConfig) { const asStrings = this.toolConfig.tools.filter( (t): t is string => typeof t === 'string', ); const hasWildcard = asStrings.includes('*'); const onlyInlineDecls = this.toolConfig.tools.filter( (t): t is FunctionDeclaration => typeof t !== 'string', ); if (hasWildcard || asStrings.length === 0) { toolsList.push(...toolRegistry.getFunctionDeclarations()); } else { toolsList.push( ...toolRegistry.getFunctionDeclarationsFiltered(asStrings), ); } toolsList.push(...onlyInlineDecls); } else { // Inherit all available tools by default when not specified. toolsList.push(...toolRegistry.getFunctionDeclarations()); } const initialTaskText = String( (context.get('task_prompt') as string) ?? 'Get Started!', ); let currentMessages: Content[] = [ { role: 'user', parts: [{ text: initialTaskText }] }, ]; const startTime = Date.now(); this.executionStats.startTimeMs = startTime; this.stats.start(startTime); let turnCounter = 0; try { // Emit start event this.eventEmitter?.emit('start', { subagentId: this.subagentId, name: this.name, model: this.modelConfig.model, tools: (this.toolConfig?.tools || ['*']).map((t) => typeof t === 'string' ? t : t.name, ), timestamp: Date.now(), }); // Log telemetry for subagent start const startEvent = new SubagentExecutionEvent(this.name, 'started'); logSubagentExecution(this.runtimeContext, startEvent); while (true) { // Check termination conditions. if ( this.runConfig.max_turns && turnCounter >= this.runConfig.max_turns ) { this.output.terminate_reason = SubagentTerminateMode.MAX_TURNS; break; } let durationMin = (Date.now() - startTime) / (1000 * 60); if ( this.runConfig.max_time_minutes && durationMin >= this.runConfig.max_time_minutes ) { this.output.terminate_reason = SubagentTerminateMode.TIMEOUT; break; } const promptId = `${this.runtimeContext.getSessionId()}#${this.subagentId}#${turnCounter++}`; const messageParams = { message: currentMessages[0]?.parts || [], config: { abortSignal: abortController.signal, tools: [{ functionDeclarations: toolsList }], }, }; const responseStream = await chat.sendMessageStream( messageParams, promptId, ); this.eventEmitter?.emit('round_start', { subagentId: this.subagentId, round: turnCounter, promptId, timestamp: Date.now(), }); const functionCalls: FunctionCall[] = []; let roundText = ''; let lastUsage: GenerateContentResponseUsageMetadata | undefined = undefined; for await (const resp of responseStream) { if (abortController.signal.aborted) return; if (resp.functionCalls) functionCalls.push(...resp.functionCalls); const content = resp.candidates?.[0]?.content; const parts = content?.parts || []; for (const p of parts) { const txt = (p as Part & { text?: string }).text; if (txt) roundText += txt; if (txt) this.eventEmitter?.emit('model_text', { subagentId: this.subagentId, round: turnCounter, text: txt, timestamp: Date.now(), }); } if (resp.usageMetadata) lastUsage = resp.usageMetadata; } this.executionStats.rounds = turnCounter; this.stats.setRounds(turnCounter); durationMin = (Date.now() - startTime) / (1000 * 60); if ( this.runConfig.max_time_minutes && durationMin >= this.runConfig.max_time_minutes ) { this.output.terminate_reason = SubagentTerminateMode.TIMEOUT; break; } // Update token usage if available if (lastUsage) { const inTok = Number(lastUsage.promptTokenCount || 0); const outTok = Number(lastUsage.candidatesTokenCount || 0); if (isFinite(inTok) || isFinite(outTok)) { this.stats.recordTokens( isFinite(inTok) ? inTok : 0, isFinite(outTok) ? outTok : 0, ); // mirror legacy fields for compatibility this.executionStats.inputTokens = (this.executionStats.inputTokens || 0) + (isFinite(inTok) ? inTok : 0); this.executionStats.outputTokens = (this.executionStats.outputTokens || 0) + (isFinite(outTok) ? outTok : 0); this.executionStats.totalTokens = (this.executionStats.inputTokens || 0) + (this.executionStats.outputTokens || 0); this.executionStats.estimatedCost = (this.executionStats.inputTokens || 0) * 3e-5 + (this.executionStats.outputTokens || 0) * 6e-5; } } if (functionCalls.length > 0) { currentMessages = await this.processFunctionCalls( functionCalls, abortController, promptId, ); } else { // No tool calls — treat this as the model's final answer. if (roundText && roundText.trim().length > 0) { this.finalText = roundText.trim(); this.output.result = this.finalText; this.output.terminate_reason = SubagentTerminateMode.GOAL; break; } // Otherwise, nudge the model to finalize a result. currentMessages = [ { role: 'user', parts: [ { text: 'Please provide the final result now and stop calling tools.', }, ], }, ]; } this.eventEmitter?.emit('round_end', { subagentId: this.subagentId, round: turnCounter, promptId, timestamp: Date.now(), }); } } catch (error) { console.error('Error during subagent execution:', error); this.output.terminate_reason = SubagentTerminateMode.ERROR; this.eventEmitter?.emit('error', { subagentId: this.subagentId, error: error instanceof Error ? error.message : String(error), timestamp: Date.now(), }); // Log telemetry for subagent error const errorEvent = new SubagentExecutionEvent(this.name, 'failed', { terminate_reason: SubagentTerminateMode.ERROR, result: error instanceof Error ? error.message : String(error), }); logSubagentExecution(this.runtimeContext, errorEvent); throw error; } finally { if (externalSignal) externalSignal.removeEventListener('abort', onAbort); this.executionStats.totalDurationMs = Date.now() - startTime; const summary = this.stats.getSummary(Date.now()); this.eventEmitter?.emit('finish', { subagentId: this.subagentId, terminate_reason: this.output.terminate_reason, timestamp: Date.now(), rounds: summary.rounds, totalDurationMs: summary.totalDurationMs, totalToolCalls: summary.totalToolCalls, successfulToolCalls: summary.successfulToolCalls, failedToolCalls: summary.failedToolCalls, inputTokens: summary.inputTokens, outputTokens: summary.outputTokens, totalTokens: summary.totalTokens, }); // Log telemetry for subagent completion const completionEvent = new SubagentExecutionEvent( this.name, this.output.terminate_reason === SubagentTerminateMode.GOAL ? 'completed' : 'failed', { terminate_reason: this.output.terminate_reason, result: this.finalText, execution_summary: this.formatCompactResult( 'Subagent execution completed', ), }, ); logSubagentExecution(this.runtimeContext, completionEvent); await this.hooks?.onStop?.({ subagentId: this.subagentId, name: this.name, terminateReason: this.output.terminate_reason, summary: summary as unknown as Record, timestamp: Date.now(), }); } } /** * Processes a list of function calls, executing each one and collecting their responses. * This method iterates through the provided function calls, executes them using the * `executeToolCall` function (or handles `self.emitvalue` internally), and aggregates * their results. It also manages error reporting for failed tool executions. * @param {FunctionCall[]} functionCalls - An array of `FunctionCall` objects to process. * @param {ToolRegistry} toolRegistry - The tool registry to look up and execute tools. * @param {AbortController} abortController - An `AbortController` to signal cancellation of tool executions. * @returns {Promise} A promise that resolves to an array of `Content` parts representing the tool responses, * which are then used to update the chat history. */ private async processFunctionCalls( functionCalls: FunctionCall[], abortController: AbortController, promptId: string, ): Promise { const toolResponseParts: Part[] = []; for (const functionCall of functionCalls) { const toolName = String(functionCall.name || 'unknown'); const callId = functionCall.id ?? `${functionCall.name}-${Date.now()}`; const requestInfo: ToolCallRequestInfo = { callId, name: functionCall.name as string, args: (functionCall.args ?? {}) as Record, isClientInitiated: true, prompt_id: promptId, }; // Execute tools with timing and hooks const start = Date.now(); await this.hooks?.preToolUse?.({ subagentId: this.subagentId, name: this.name, toolName, args: requestInfo.args, timestamp: Date.now(), }); const toolResponse = await executeToolCall( this.runtimeContext, requestInfo, abortController.signal, ); const duration = Date.now() - start; // Update tool call stats this.executionStats.totalToolCalls += 1; if (toolResponse.error) { this.executionStats.failedToolCalls += 1; } else { this.executionStats.successfulToolCalls += 1; } // Update per-tool usage const tu = this.toolUsage.get(toolName) || { count: 0, success: 0, failure: 0, totalDurationMs: 0, averageDurationMs: 0, }; tu.count += 1; if (toolResponse?.error) { tu.failure += 1; const disp = typeof toolResponse.resultDisplay === 'string' ? toolResponse.resultDisplay : toolResponse.resultDisplay ? JSON.stringify(toolResponse.resultDisplay) : undefined; tu.lastError = disp || toolResponse.error?.message || 'Unknown error'; } else { tu.success += 1; } if (typeof tu.totalDurationMs === 'number') { tu.totalDurationMs += duration; tu.averageDurationMs = tu.count > 0 ? tu.totalDurationMs / tu.count : tu.totalDurationMs; } else { tu.totalDurationMs = duration; tu.averageDurationMs = duration; } this.toolUsage.set(toolName, tu); // Emit tool call/result events this.eventEmitter?.emit('tool_call', { subagentId: this.subagentId, round: this.executionStats.rounds, callId, name: toolName, args: requestInfo.args, timestamp: Date.now(), }); this.eventEmitter?.emit('tool_result', { subagentId: this.subagentId, round: this.executionStats.rounds, callId, name: toolName, success: !toolResponse?.error, error: toolResponse?.error ? typeof toolResponse.resultDisplay === 'string' ? toolResponse.resultDisplay : toolResponse.resultDisplay ? JSON.stringify(toolResponse.resultDisplay) : toolResponse.error.message : undefined, durationMs: duration, timestamp: Date.now(), }); // Update statistics service this.stats.recordToolCall( toolName, !toolResponse?.error, duration, this.toolUsage.get(toolName)?.lastError, ); // post-tool hook await this.hooks?.postToolUse?.({ subagentId: this.subagentId, name: this.name, toolName, args: requestInfo.args, success: !toolResponse?.error, durationMs: duration, errorMessage: toolResponse?.error ? typeof toolResponse.resultDisplay === 'string' ? toolResponse.resultDisplay : toolResponse.resultDisplay ? JSON.stringify(toolResponse.resultDisplay) : toolResponse.error.message : undefined, timestamp: Date.now(), }); if (toolResponse.error) { console.error( `Error executing tool ${functionCall.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`, ); } if (toolResponse.responseParts) { const parts = Array.isArray(toolResponse.responseParts) ? toolResponse.responseParts : [toolResponse.responseParts]; for (const part of parts) { if (typeof part === 'string') { toolResponseParts.push({ text: part }); } else if (part) { toolResponseParts.push(part); } } } } // If all tool calls failed, inform the model so it can re-evaluate. if (functionCalls.length > 0 && toolResponseParts.length === 0) { toolResponseParts.push({ text: 'All tool calls failed. Please analyze the errors and try an alternative approach.', }); } return [{ role: 'user', parts: toolResponseParts }]; } getEventEmitter() { return this.eventEmitter; } getStatistics() { const total = this.executionStats.totalToolCalls; const successRate = total > 0 ? (this.executionStats.successfulToolCalls / total) * 100 : 0; return { ...this.executionStats, successRate, toolUsage: Array.from(this.toolUsage.entries()).map(([name, v]) => ({ name, ...v, })), }; } formatCompactResult(taskDesc: string, _useColors = false) { const stats = this.getStatistics(); return formatCompact( { rounds: stats.rounds, totalDurationMs: stats.totalDurationMs, totalToolCalls: stats.totalToolCalls, successfulToolCalls: stats.successfulToolCalls, failedToolCalls: stats.failedToolCalls, successRate: stats.successRate, inputTokens: this.executionStats.inputTokens, outputTokens: this.executionStats.outputTokens, totalTokens: this.executionStats.totalTokens, }, taskDesc, ); } getFinalText(): string { return this.finalText; } formatDetailedResult(taskDesc: string) { const stats = this.getStatistics(); return formatDetailed( { rounds: stats.rounds, totalDurationMs: stats.totalDurationMs, totalToolCalls: stats.totalToolCalls, successfulToolCalls: stats.successfulToolCalls, failedToolCalls: stats.failedToolCalls, successRate: stats.successRate, inputTokens: this.executionStats.inputTokens, outputTokens: this.executionStats.outputTokens, totalTokens: this.executionStats.totalTokens, toolUsage: stats.toolUsage, }, taskDesc, ); } private async createChatObject(context: ContextState) { if (!this.promptConfig.systemPrompt && !this.promptConfig.initialMessages) { throw new Error( 'PromptConfig must have either `systemPrompt` or `initialMessages` defined.', ); } if (this.promptConfig.systemPrompt && this.promptConfig.initialMessages) { throw new Error( 'PromptConfig cannot have both `systemPrompt` and `initialMessages` defined.', ); } const envParts = await getEnvironmentContext(this.runtimeContext); const envHistory: Content[] = [ { role: 'user', parts: envParts }, { role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] }, ]; const start_history = [ ...envHistory, ...(this.promptConfig.initialMessages ?? []), ]; const systemInstruction = this.promptConfig.systemPrompt ? this.buildChatSystemPrompt(context) : undefined; try { const generationConfig: GenerateContentConfig & { systemInstruction?: string | Content; } = { temperature: this.modelConfig.temp, topP: this.modelConfig.top_p, }; if (systemInstruction) { generationConfig.systemInstruction = systemInstruction; } const contentGenerator = await createContentGenerator( this.runtimeContext.getContentGeneratorConfig(), this.runtimeContext, this.runtimeContext.getSessionId(), ); if (this.modelConfig.model) { this.runtimeContext.setModel(this.modelConfig.model); } return new GeminiChat( this.runtimeContext, contentGenerator, generationConfig, start_history, ); } catch (error) { await reportError( error, 'Error initializing Gemini chat session.', start_history, 'startChat', ); // The calling function will handle the undefined return. return undefined; } } private buildChatSystemPrompt(context: ContextState): string { if (!this.promptConfig.systemPrompt) { // This should ideally be caught in createChatObject, but serves as a safeguard. return ''; } let finalPrompt = templateString(this.promptConfig.systemPrompt, context); // Add general non-interactive instructions. finalPrompt += ` Important Rules: - You operate in non-interactive mode: do not ask the user questions; proceed with available context. - Use tools only when necessary to obtain facts or make changes. - When the task is complete, return the final result as a normal model response (not a tool call) and stop.`; return finalPrompt; } }