/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { reportError } from '../utils/errorReporting.js';
import { ToolRegistry } from '../tools/tool-registry.js';
import { executeToolCall } from './nonInteractiveToolExecutor.js';
import { getEnvironmentContext } from '../utils/environmentContext.js';
import { Type } from '@google/genai';
import { GeminiChat, StreamEventType } from './geminiChat.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.
 */
/**
 * Describes the possible termination modes for a subagent.
 * This enum provides a clear indication of why a subagent's execution might have ended.
 */
export var SubagentTerminateMode;
(function (SubagentTerminateMode) {
    /**
     * Indicates that the subagent's execution terminated due to an unrecoverable error.
     */
    SubagentTerminateMode["ERROR"] = "ERROR";
    /**
     * Indicates that the subagent's execution terminated because it exceeded the maximum allowed working time.
     */
    SubagentTerminateMode["TIMEOUT"] = "TIMEOUT";
    /**
     * Indicates that the subagent's execution successfully completed all its defined goals.
     */
    SubagentTerminateMode["GOAL"] = "GOAL";
    /**
     * Indicates that the subagent's execution terminated because it exceeded the maximum number of turns.
     */
    SubagentTerminateMode["MAX_TURNS"] = "MAX_TURNS";
})(SubagentTerminateMode || (SubagentTerminateMode = {}));
/**
 * 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 {
    state = {};
    /**
     * 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) {
        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, value) {
        this.state[key] = value;
    }
    /**
     * Retrieves all keys in the context state.
     *
     * @returns An array of all keys in the context state.
     */
    get_keys() {
        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, context) {
    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 {
    name;
    runtimeContext;
    promptConfig;
    modelConfig;
    runConfig;
    output = {
        terminate_reason: SubagentTerminateMode.ERROR,
        emitted_vars: {},
    };
    subagentId;
    toolConfig;
    outputConfig;
    onMessage;
    toolRegistry;
    /**
     * 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 options - Optional configurations for the subagent.
     */
    constructor(name, runtimeContext, promptConfig, modelConfig, runConfig, toolRegistry, options = {}) {
        this.name = name;
        this.runtimeContext = runtimeContext;
        this.promptConfig = promptConfig;
        this.modelConfig = modelConfig;
        this.runConfig = runConfig;
        const randomPart = Math.random().toString(36).slice(2, 8);
        this.subagentId = `${this.name}-${randomPart}`;
        this.toolConfig = options.toolConfig;
        this.outputConfig = options.outputConfig;
        this.onMessage = options.onMessage;
        this.toolRegistry = toolRegistry;
    }
    /**
     * 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 name - The name of the subagent.
     * @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 options - Optional configurations for the subagent.
     * @returns A promise that resolves to a valid SubAgentScope instance.
     * @throws {Error} If any tool requires user confirmation.
     */
    static async create(name, runtimeContext, promptConfig, modelConfig, runConfig, options = {}) {
        const subagentToolRegistry = new ToolRegistry(runtimeContext);
        if (options.toolConfig) {
            for (const tool of options.toolConfig.tools) {
                if (typeof tool === 'string') {
                    const toolFromRegistry = (await runtimeContext.getToolRegistry()).getTool(tool);
                    if (toolFromRegistry) {
                        subagentToolRegistry.registerTool(toolFromRegistry);
                    }
                }
                else if (typeof tool === 'object' &&
                    'name' in tool &&
                    'build' in tool) {
                    subagentToolRegistry.registerTool(tool);
                }
                else {
                    // This is a FunctionDeclaration, which we can't add to the registry.
                    // We'll rely on the validation below to catch any issues.
                }
            }
            for (const tool of subagentToolRegistry.getAllTools()) {
                const schema = tool.schema.parametersJsonSchema;
                const requiredParams = schema?.required ?? [];
                if (requiredParams.length > 0) {
                    // This check is imperfect. A tool might require parameters but still
                    // be interactive (e.g., `delete_file(path)`). However, we cannot
                    // build a generic invocation without knowing what dummy parameters
                    // to provide. Crashing here because `build({})` fails is worse
                    // than allowing a potential hang later if an interactive tool is
                    // used. This is a best-effort check.
                    console.warn(`Cannot check tool "${tool.name}" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.`);
                    continue;
                }
                const invocation = tool.build({});
                const confirmationDetails = await invocation.shouldConfirmExecute(new AbortController().signal);
                if (confirmationDetails) {
                    throw new Error(`Tool "${tool.name}" requires user confirmation and cannot be used in a non-interactive subagent.`);
                }
            }
        }
        return new SubAgentScope(name, runtimeContext, promptConfig, modelConfig, runConfig, subagentToolRegistry, options);
    }
    /**
     * 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<void>} A promise that resolves when the subagent has completed its execution.
     */
    async runNonInteractive(context) {
        const startTime = Date.now();
        let turnCounter = 0;
        try {
            const chat = await this.createChatObject(context);
            if (!chat) {
                this.output.terminate_reason = SubagentTerminateMode.ERROR;
                return;
            }
            const abortController = new AbortController();
            // Prepare the list of tools available to the subagent.
            const toolsList = [];
            if (this.toolConfig) {
                const toolsToLoad = [];
                for (const tool of this.toolConfig.tools) {
                    if (typeof tool === 'string') {
                        toolsToLoad.push(tool);
                    }
                    else if (typeof tool === 'object' && 'schema' in tool) {
                        // This is a tool instance with a schema property
                        toolsList.push(tool.schema);
                    }
                    else {
                        // This is a raw FunctionDeclaration
                        toolsList.push(tool);
                    }
                }
                toolsList.push(...this.toolRegistry.getFunctionDeclarationsFiltered(toolsToLoad));
            }
            // Add local scope functions if outputs are expected.
            if (this.outputConfig && this.outputConfig.outputs) {
                toolsList.push(...this.getScopeLocalFuncDefs());
            }
            let currentMessages = [
                { role: 'user', parts: [{ text: 'Get Started!' }] },
            ];
            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 (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(this.modelConfig.model, messageParams, promptId);
                const functionCalls = [];
                let textResponse = '';
                for await (const resp of responseStream) {
                    if (abortController.signal.aborted)
                        return;
                    if (resp.type === StreamEventType.CHUNK && resp.value.functionCalls) {
                        functionCalls.push(...resp.value.functionCalls);
                    }
                    if (resp.type === StreamEventType.CHUNK && resp.value.text) {
                        textResponse += resp.value.text;
                    }
                }
                if (this.onMessage && textResponse) {
                    this.onMessage(textResponse);
                }
                durationMin = (Date.now() - startTime) / (1000 * 60);
                if (durationMin >= this.runConfig.max_time_minutes) {
                    this.output.terminate_reason = SubagentTerminateMode.TIMEOUT;
                    break;
                }
                if (functionCalls.length > 0) {
                    currentMessages = await this.processFunctionCalls(functionCalls, abortController, promptId);
                }
                // Check for goal completion after processing function calls,
                // as `self.emitvalue` might have completed the requirements.
                if (this.outputConfig &&
                    Object.keys(this.outputConfig.outputs).length > 0) {
                    const remainingVars = Object.keys(this.outputConfig.outputs).filter((key) => !(key in this.output.emitted_vars));
                    if (remainingVars.length === 0) {
                        this.output.terminate_reason = SubagentTerminateMode.GOAL;
                        break;
                    }
                }
                if (functionCalls.length === 0) {
                    // Model stopped calling tools. Check if goal is met.
                    if (!this.outputConfig ||
                        Object.keys(this.outputConfig.outputs).length === 0) {
                        this.output.terminate_reason = SubagentTerminateMode.GOAL;
                        break;
                    }
                    const remainingVars = Object.keys(this.outputConfig.outputs).filter((key) => !(key in this.output.emitted_vars));
                    if (remainingVars.length === 0) {
                        this.output.terminate_reason = SubagentTerminateMode.GOAL;
                        break;
                    }
                    const nudgeMessage = `You have stopped calling tools but have not emitted the following required variables: ${remainingVars.join(', ')}. Please use the 'self.emitvalue' tool to emit them now, or continue working if necessary.`;
                    console.debug(nudgeMessage);
                    currentMessages = [
                        {
                            role: 'user',
                            parts: [{ text: nudgeMessage }],
                        },
                    ];
                }
            }
        }
        catch (error) {
            console.error('Error during subagent execution:', error);
            this.output.terminate_reason = SubagentTerminateMode.ERROR;
            throw error;
        }
    }
    /**
     * 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<Content[]>} A promise that resolves to an array of `Content` parts representing the tool responses,
     *          which are then used to update the chat history.
     */
    async processFunctionCalls(functionCalls, abortController, promptId) {
        const toolResponseParts = [];
        for (const functionCall of functionCalls) {
            if (this.onMessage) {
                const args = JSON.stringify(functionCall.args ?? {});
                // Truncate arguments
                const MAX_ARGS_LENGTH = 250;
                const truncatedArgs = args.length > MAX_ARGS_LENGTH
                    ? `${args.substring(0, MAX_ARGS_LENGTH)}...`
                    : args;
                this.onMessage(`

**Executing tool: ${functionCall.name} with args ${truncatedArgs}**

`);
            }
            const callId = functionCall.id ?? `${functionCall.name}-${Date.now()}`;
            const requestInfo = {
                callId,
                name: functionCall.name,
                args: (functionCall.args ?? {}),
                isClientInitiated: true,
                prompt_id: promptId,
            };
            let toolResponse;
            // Handle scope-local tools first.
            if (functionCall.name === 'self.emitvalue') {
                const valName = String(requestInfo.args['emit_variable_name']);
                const valVal = String(requestInfo.args['emit_variable_value']);
                this.output.emitted_vars[valName] = valVal;
                toolResponse = {
                    callId,
                    responseParts: [{ text: `Emitted variable ${valName} successfully` }],
                    resultDisplay: `Emitted variable ${valName} successfully`,
                    error: undefined,
                };
            }
            else {
                toolResponse = await executeToolCall(this.runtimeContext, requestInfo, abortController.signal);
            }
            if (toolResponse.error) {
                console.error(`Error executing tool ${functionCall.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`);
            }
            if (toolResponse.responseParts) {
                toolResponseParts.push(...toolResponse.responseParts);
            }
        }
        // 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 }];
    }
    async createChatObject(context) {
        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 = [
            { 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 = {
                temperature: this.modelConfig.temp,
                topP: this.modelConfig.top_p,
            };
            if (systemInstruction) {
                generationConfig.systemInstruction = systemInstruction;
            }
            this.runtimeContext.setModel(this.modelConfig.model);
            return new GeminiChat(this.runtimeContext, 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;
        }
    }
    /**
     * Returns an array of FunctionDeclaration objects for tools that are local to the subagent's scope.
     * Currently, this includes the `self.emitvalue` tool for emitting variables.
     * @returns An array of `FunctionDeclaration` objects.
     */
    getScopeLocalFuncDefs() {
        const emitValueTool = {
            name: 'self.emitvalue',
            description: `* This tool emits A SINGLE return value from this execution, such that it can be collected and presented to the calling function.
        * You can only emit ONE VALUE each time you call this tool. You are expected to call this tool MULTIPLE TIMES if you have MULTIPLE OUTPUTS.`,
            parameters: {
                type: Type.OBJECT,
                properties: {
                    emit_variable_name: {
                        description: 'This is the name of the variable to be returned.',
                        type: Type.STRING,
                    },
                    emit_variable_value: {
                        description: 'This is the _value_ to be returned for this variable.',
                        type: Type.STRING,
                    },
                },
                required: ['emit_variable_name', 'emit_variable_value'],
            },
        };
        return [emitValueTool];
    }
    /**
     * Builds the system prompt for the chat based on the provided configurations.
     * It templates the base system prompt and appends instructions for emitting
     * variables if an `OutputConfig` is provided.
     * @param {ContextState} context - The context for templating.
     * @returns {string} The complete system prompt.
     */
    buildChatSystemPrompt(context) {
        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 instructions for emitting variables if needed.
        if (this.outputConfig && this.outputConfig.outputs) {
            let outputInstructions = '\n\nAfter you have achieved all other goals, you MUST emit the required output variables. For each expected output, make one final call to the `self.emitvalue` tool.';
            for (const [key, value] of Object.entries(this.outputConfig.outputs)) {
                outputInstructions += `\n* Use 'self.emitvalue' to emit the '${key}' key, with a value described as: '${value}'`;
            }
            finalPrompt += outputInstructions;
        }
        // Add general non-interactive instructions.
        finalPrompt += `

Important Rules:
 * You are running in a non-interactive mode. You CANNOT ask the user for input or clarification. You must proceed with the information you have.
 * Once you believe all goals have been met and all required outputs have been emitted, stop calling tools.`;
        return finalPrompt;
    }
}
//# sourceMappingURL=subagent.js.map