import { z } from 'zod';
import { ServiceBubble } from '../../types/service-bubble-class.js';
import { CredentialType, BUBBLE_CREDENTIAL_OPTIONS, RECOMMENDED_MODELS, } from '@bubblelab/shared-schemas';
import { StateGraph, MessagesAnnotation } from '@langchain/langgraph';
import { ChatOpenAI } from '@langchain/openai';
import { ChatAnthropic } from '@langchain/anthropic';
import { HumanMessage, AIMessage, ToolMessage, AIMessageChunk, } from '@langchain/core/messages';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { AvailableModels } from '@bubblelab/shared-schemas';
import { AvailableTools, } from '../../types/available-tools.js';
import { BubbleFactory } from '../../bubble-factory.js';
import { ConversationMessageSchema } from '@bubblelab/shared-schemas';
import { extractAndStreamThinkingTokens, formatFinalResponse, generationsToMessageContent, } from '../../utils/agent-formatter.js';
import { isAIMessage, isAIMessageChunk } from '@langchain/core/messages';
import { HarmBlockThreshold, HarmCategory } from '@google/generative-ai';
import { SafeGeminiChat } from '../../utils/safe-gemini-chat.js';
import { zodSchemaToJsonString, buildJsonSchemaInstruction, } from '../../utils/zod-schema.js';
import { getCapability, } from '../../capabilities/index.js';
import { applyCapabilityPostprocessing, applyCapabilityPreprocessing, } from './capability-pipeline.js';
// Define backup model configuration schema
const BackupModelConfigSchema = z.object({
    model: AvailableModels.describe('Backup AI model to use if the primary model fails (format: provider/model-name).'),
    temperature: z
        .number()
        .min(0)
        .max(2)
        .optional()
        .describe('Temperature for backup model. If not specified, uses primary model temperature.'),
    maxTokens: z
        .number()
        .positive()
        .optional()
        .describe('Max tokens for backup model. If not specified, uses primary model maxTokens.'),
    reasoningEffort: z
        .enum(['low', 'medium', 'high'])
        .optional()
        .describe('Reasoning effort for backup model. If not specified, uses primary model reasoningEffort.'),
    maxRetries: z
        .number()
        .int()
        .min(0)
        .max(10)
        .optional()
        .describe('Max retries for backup model. If not specified, uses primary model maxRetries.'),
});
// Define model configuration
const ModelConfigSchema = z.object({
    model: AvailableModels.describe('AI model to use (format: provider/model-name).'),
    temperature: z
        .number()
        .min(0)
        .max(2)
        .default(1)
        .describe('Temperature for response randomness (0 = deterministic, 2 = very random)'),
    maxTokens: z
        .number()
        .positive()
        .optional()
        .default(64000)
        .describe('Maximum number of tokens to generate in response, keep at default of 40000 unless the response is expected to be certain length'),
    reasoningEffort: z
        .enum(['low', 'medium', 'high'])
        .optional()
        .describe('Reasoning effort for model. If not specified, uses primary model reasoningEffort.'),
    maxRetries: z
        .number()
        .int()
        .min(0)
        .max(10)
        .default(3)
        .describe('Maximum number of retries for API calls (default: 3). Useful for handling transient errors like 503 Service Unavailable.'),
    provider: z
        .array(z.string())
        .optional()
        .describe('Providers for ai agent (open router only).'),
    jsonMode: z
        .boolean()
        .default(false)
        .describe('When true, returns clean JSON response, you must provide the exact JSON schema in the system prompt'),
    backupModel: BackupModelConfigSchema.default({
        model: RECOMMENDED_MODELS.FAST,
    })
        .optional()
        .describe('Backup model configuration to use if the primary model fails.'),
});
// Define tool configuration for pre-registered tools
const ToolConfigSchema = z.object({
    name: AvailableTools.describe('Name of the tool type or tool bubble to enable for the AI agent'),
    credentials: z
        .record(z.nativeEnum(CredentialType), z.string())
        .default({})
        .optional()
        .describe('Credential types to use for the tool bubble (injected at runtime)'),
    config: z
        .record(z.string(), z.unknown())
        .optional()
        .describe('Configuration for the tool or tool bubble'),
});
// Define custom tool schema for runtime-defined tools
const CustomToolSchema = z.object({
    name: z
        .string()
        .min(1)
        .describe('Unique name for your custom tool (e.g., "calculate-tax")'),
    description: z
        .string()
        .min(1)
        .describe('Description of what the tool does - helps the AI know when to use it'),
    schema: z
        .union([
        z.record(z.string(), z.unknown()),
        z.custom((val) => val && typeof val === 'object' && '_def' in val),
    ])
        .describe('Zod schema object defining the tool parameters. Can be either a plain object (e.g., { amount: z.number() }) or a Zod object directly (e.g., z.object({ amount: z.number() })).'),
    func: z
        .function()
        .args(z.record(z.string(), z.unknown()))
        .returns(z.promise(z.unknown()))
        .describe('Async function that executes the tool logic. Receives params matching the schema and returns a result.'),
});
// Define image input schemas - supports both base64 data and URLs
const Base64ImageSchema = z.object({
    type: z.literal('base64').default('base64'),
    data: z
        .string()
        .describe('Base64 encoded image data (without data:image/... prefix)'),
    mimeType: z
        .string()
        .default('image/png')
        .describe('MIME type of the image (e.g., image/png, image/jpeg)'),
    description: z
        .string()
        .optional()
        .describe('Optional description or context for the image'),
});
const UrlImageSchema = z.object({
    type: z.literal('url'),
    url: z.string().url().describe('URL to the image (http/https)'),
    description: z
        .string()
        .optional()
        .describe('Optional description or context for the image'),
});
const ImageInputSchema = z.discriminatedUnion('type', [
    Base64ImageSchema,
    UrlImageSchema,
]);
// Schema for the expected JSON output structure - accepts either a Zod schema or a JSON schema string
const ExpectedOutputSchema = z.union([
    z.custom((val) => val?._def !== undefined),
    z.string(),
]);
// Schema for a single capability configuration on the AI agent
const CapabilityConfigSchema = z.object({
    id: z
        .string()
        .min(1)
        .describe('Capability ID (e.g., "google-doc-knowledge-base")'),
    inputs: z
        .record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]))
        .default({})
        .describe('Input parameter values for this capability'),
    credentials: z
        .record(z.nativeEnum(CredentialType), z.string())
        .default({})
        .optional()
        .describe('Capability-specific credentials (injected at runtime)'),
});
// Define the parameters schema for the AI Agent bubble
const AIAgentParamsSchema = z.object({
    message: z
        .string()
        .min(1, 'Message is required')
        .describe('The message or question to send to the AI agent'),
    images: z
        .array(ImageInputSchema)
        .default([])
        .describe('Array of base64 encoded images to include with the message (for multimodal AI models). Example: [{type: "base64", data: "base64...", mimeType: "image/png", description: "A beautiful image of a cat"}] or [{type: "url", url: "https://example.com/image.png", description: "A beautiful image of a cat"}]'),
    conversationHistory: z
        .array(ConversationMessageSchema)
        .optional()
        .describe('Previous conversation messages for multi-turn conversations. When provided, messages are sent as separate turns to enable KV cache optimization. Format: [{role: "user", content: "..."}, {role: "assistant", content: "..."}, ...]'),
    systemPrompt: z
        .string()
        .default('You are a helpful AI assistant')
        .describe('System prompt that defines the AI agents behavior and personality'),
    name: z
        .string()
        .default('AI Agent')
        .optional()
        .describe('A friendly name for the AI agent'),
    model: ModelConfigSchema.default({
        model: RECOMMENDED_MODELS.FAST,
        temperature: 1,
        maxTokens: 65536,
        maxRetries: 3,
        jsonMode: false,
    }).describe('AI model configuration including provider, temperature, and tokens, retries, and json mode. Always include this.'),
    tools: z
        .array(ToolConfigSchema)
        .default([])
        .describe('Array of pre-registered tools the AI agent can use. Can be tool types (web-search-tool, web-scrape-tool, web-crawl-tool, web-extract-tool, instagram-tool). If using image models, set the tools to []'),
    customTools: z
        .array(CustomToolSchema)
        .default([])
        .optional()
        .describe('Array of custom runtime-defined tools with their own schemas and functions. Use this to add domain-specific tools without pre-registration. Example: [{ name: "calculate-tax", description: "Calculates sales tax", schema: { amount: z.number() }, func: async (input) => {...} }]'),
    maxIterations: z
        .number()
        .positive()
        .min(4)
        .default(40)
        .describe('Maximum number of iterations for the agent workflow, 5 iterations per turn of conversation'),
    credentials: z
        .record(z.nativeEnum(CredentialType), z.string())
        .optional()
        .describe('Object mapping credential types to values (injected at runtime)'),
    streaming: z
        .boolean()
        .default(false)
        .describe('Enable real-time streaming of tokens, tool calls, and iteration progress'),
    capabilities: z
        .array(CapabilityConfigSchema)
        .default([])
        .optional()
        .describe('Capabilities that extend the agent with bundled tools, prompts, and credentials. Example: [{ id: "google-doc-knowledge-base", inputs: { docId: "your-doc-id" } }]'),
    expectedOutputSchema: ExpectedOutputSchema.optional().describe('Zod schema or JSON schema string that defines the expected structure of the AI response. When provided, automatically enables JSON mode and instructs the AI to output in the exact format. Example: z.object({ summary: z.string(), items: z.array(z.object({ name: z.string(), score: z.number() })) })'),
    // Note: beforeToolCall and afterToolCall are function hooks added via TypeScript interface
    // They cannot be part of the Zod schema but are available in the params
});
const AIAgentResultSchema = z.object({
    response: z
        .string()
        .describe('The AI agents final response to the user message. For text responses, returns plain text. If JSON mode is enabled, returns a JSON string. For image generation models (like gemini-2.5-flash-image-preview), returns base64-encoded image data with data URI format (data:image/png;base64,...)'),
    reasoning: z
        .string()
        .nullable()
        .optional()
        .describe('The reasoning/thinking tokens from the model (if available). Present for deep research models and reasoning models.'),
    toolCalls: z
        .array(z.object({
        tool: z.string().describe('Name of the tool that was called'),
        input: z.unknown().describe('Input parameters passed to the tool'),
        output: z.unknown().describe('Output returned by the tool'),
    }))
        .describe('Array of tool calls made during the conversation'),
    iterations: z
        .number()
        .describe('Number of back-and-forth iterations in the agent workflow'),
    totalCost: z
        .number()
        .optional()
        .describe('Total cost in USD for this request (includes tokens + web search for deep research models)'),
    error: z
        .string()
        .describe('Error message of the run, undefined if successful'),
    success: z
        .boolean()
        .describe('Whether the agent execution completed successfully'),
});
export class AIAgentBubble extends ServiceBubble {
    static type = 'service';
    static service = 'ai-agent';
    static authType = 'apikey';
    static bubbleName = 'ai-agent';
    static schema = AIAgentParamsSchema;
    static resultSchema = AIAgentResultSchema;
    static shortDescription = 'AI agent with LangGraph for tool-enabled conversations, multimodal support, and JSON mode';
    static longDescription = `
    An AI agent powered by LangGraph that can use any tool bubble to answer questions.
    Use cases:
    - Add tools to enhance the AI agent's capabilities (web-search-tool, web-scrape-tool)
    - Multi-step reasoning with tool assistance
    - Tool-augmented conversations with any registered tool
    - JSON mode for structured output (strips markdown formatting)
  `;
    static alias = 'agent';
    factory;
    beforeToolCallHook;
    afterToolCallHook;
    afterLLMCallHook;
    streamingCallback;
    shouldStopAfterTools = false;
    shouldContinueToAgent = false;
    constructor(params = {
        message: 'Hello, how are you?',
        systemPrompt: 'You are a helpful AI assistant',
        model: { model: RECOMMENDED_MODELS.FAST },
    }, context, instanceId) {
        super(params, context, instanceId);
        this.beforeToolCallHook = params.beforeToolCall;
        this.afterToolCallHook = params.afterToolCall;
        this.afterLLMCallHook = params.afterLLMCall;
        this.streamingCallback = params.streamingCallback;
        this.factory = new BubbleFactory();
    }
    async testCredential() {
        // Make a test API call to the model provider
        const llm = this.initializeModel(this.params.model);
        const response = await llm.invoke(['Hello, how are you?']);
        if (response.content) {
            return true;
        }
        return false;
    }
    /**
     * Build effective model config from primary and optional backup settings
     */
    buildModelConfig(primaryConfig, backupConfig) {
        if (!backupConfig) {
            return primaryConfig;
        }
        return {
            model: backupConfig.model,
            temperature: backupConfig.temperature ?? primaryConfig.temperature,
            maxTokens: backupConfig.maxTokens ?? primaryConfig.maxTokens,
            maxRetries: backupConfig.maxRetries ?? primaryConfig.maxRetries,
            provider: primaryConfig.provider,
            jsonMode: primaryConfig.jsonMode,
            backupModel: undefined, // Don't chain backup models
        };
    }
    /**
     * Core execution logic for running the agent with a given model config
     */
    async executeWithModel(modelConfig) {
        const { message, images, systemPrompt, tools, customTools, maxIterations, conversationHistory, } = this.params;
        // Initialize the language model
        const llm = this.initializeModel(modelConfig);
        // Initialize tools (both pre-registered and custom)
        const agentTools = await this.initializeTools(tools, customTools);
        // Create the agent graph
        const graph = await this.createAgentGraph(llm, agentTools, systemPrompt);
        // Execute the agent
        return this.executeAgent(graph, message, images, maxIterations, modelConfig, conversationHistory);
    }
    /**
     * Modify params before execution - centralizes all param transformations
     */
    async beforeAction() {
        // Deduplicate capabilities by id — keep the first occurrence of each
        if (this.params.capabilities && this.params.capabilities.length > 1) {
            const seen = new Set();
            this.params.capabilities = this.params.capabilities.filter((c) => {
                if (seen.has(c.id))
                    return false;
                seen.add(c.id);
                return true;
            });
        }
        // Enforce minimum maxTokens of 10000
        if (this.params.model.maxTokens === undefined ||
            this.params.model.maxTokens < 10000) {
            this.params.model.maxTokens = 10000;
        }
        // Auto-enable JSON mode when expectedOutputSchema is provided
        if (this.params.expectedOutputSchema) {
            this.params.model.jsonMode = true;
            // Enhance system prompt with JSON schema instructions
            const schemaString = zodSchemaToJsonString(this.params.expectedOutputSchema);
            this.params.systemPrompt = `${this.params.systemPrompt}\n\n${buildJsonSchemaInstruction(schemaString)}`;
        }
        // Inject current UTC time into system prompt
        const now = new Date().toLocaleString('en-US', {
            weekday: 'long',
            year: 'numeric',
            month: 'long',
            day: 'numeric',
            hour: '2-digit',
            minute: '2-digit',
            timeZone: 'UTC',
            timeZoneName: 'short',
        });
        this.params.systemPrompt = `${this.params.systemPrompt}\n\n**System time:** ${now}`;
        // Apply capability model overrides and system prompt injections
        await applyCapabilityPreprocessing(this.params, this.context, this.resolveCapabilityCredentials.bind(this));
        // Auto-inject trigger conversation history if no explicit conversationHistory was provided
        // This enables Slack thread context to automatically flow into AI agents
        if (!this.params.conversationHistory?.length &&
            this.context?.triggerConversationHistory) {
            this.params.conversationHistory = this.context
                .triggerConversationHistory;
        }
    }
    async performAction(context) {
        // Context is available but not currently used in this implementation
        void context;
        // Apply param transformations before execution
        await this.beforeAction();
        try {
            let result;
            // Check if this is a deep research model - bypass LangChain and call OpenRouter directly
            if (this.isDeepResearchModel(this.params.model.model)) {
                console.log('[AIAgent] Deep research model detected, using direct OpenRouter API');
                result = await this.executeDeepResearchViaOpenRouter();
            }
            else {
                result = await this.executeWithModel(this.params.model);
            }
            // Append capability response additions (e.g. tips, blurbs)
            if (result.success) {
                result = await applyCapabilityPostprocessing(result, this.params, this.context, this.resolveCapabilityCredentials.bind(this));
            }
            return result;
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : 'Unknown error';
            console.warn('[AIAgent] Execution error:', errorMessage);
            // Return error information but mark as recoverable
            return {
                response: `Error: ${errorMessage}`,
                success: false,
                toolCalls: [],
                error: errorMessage,
                iterations: 0,
            };
        }
    }
    getCredentialType() {
        return this.getCredentialTypeForModel(this.params.model.model);
    }
    /**
     * Get credential type for a specific model string
     */
    getCredentialTypeForModel(model) {
        const [provider] = model.split('/');
        switch (provider) {
            case 'openai':
                return CredentialType.OPENAI_CRED;
            case 'google':
                return CredentialType.GOOGLE_GEMINI_CRED;
            case 'anthropic':
                return CredentialType.ANTHROPIC_CRED;
            case 'openrouter':
                return CredentialType.OPENROUTER_CRED;
            default:
                throw new Error(`Unsupported model provider: ${provider}`);
        }
    }
    chooseCredential() {
        const { model } = this.params;
        const credentials = this.params.credentials;
        const [provider] = model.model.split('/');
        // If no credentials were injected, throw error immediately (like PostgreSQL)
        if (!credentials || typeof credentials !== 'object') {
            throw new Error(`No ${provider.toUpperCase()} credentials provided`);
        }
        // Choose credential based on the model provider
        switch (provider) {
            case 'openai':
                return credentials[CredentialType.OPENAI_CRED];
            case 'google':
                return credentials[CredentialType.GOOGLE_GEMINI_CRED];
            case 'anthropic':
                return credentials[CredentialType.ANTHROPIC_CRED];
            case 'openrouter':
                return credentials[CredentialType.OPENROUTER_CRED];
            default:
                throw new Error(`Unsupported model provider: ${provider}`);
        }
    }
    /**
     * Check if the model is a deep research model that requires direct API call
     */
    isDeepResearchModel(model) {
        return (model === 'openrouter/openai/o3-deep-research' ||
            model === 'openrouter/openai/o4-mini-deep-research');
    }
    /**
     * Execute deep research models via OpenRouter API directly
     * Bypasses LangChain since these models have compatibility issues
     */
    async executeDeepResearchViaOpenRouter() {
        const { message, systemPrompt, model, conversationHistory } = this.params;
        // Extract the model name without 'openrouter/' prefix
        const modelName = model.model.replace('openrouter/', '');
        const credentials = this.params.credentials;
        const apiKey = credentials?.[CredentialType.OPENROUTER_CRED];
        if (!apiKey) {
            throw new Error('OpenRouter API key is required for deep research models');
        }
        // Build messages array
        const messages = [];
        // Add system prompt
        if (systemPrompt) {
            messages.push({ role: 'system', content: systemPrompt });
        }
        // Add conversation history if provided
        if (conversationHistory && conversationHistory.length > 0) {
            for (const historyMsg of conversationHistory) {
                if (historyMsg.role === 'user' || historyMsg.role === 'assistant') {
                    messages.push({ role: historyMsg.role, content: historyMsg.content });
                }
            }
        }
        // Add current message
        messages.push({ role: 'user', content: message });
        // Emit start event
        await this.streamingCallback?.({
            type: 'llm_start',
            data: {
                model: model.model,
                temperature: model.temperature,
            },
        });
        try {
            const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
                method: 'POST',
                headers: {
                    Authorization: `Bearer ${apiKey}`,
                    'Content-Type': 'application/json',
                    'HTTP-Referer': 'https://bubblelab.ai',
                    'X-Title': 'BubbleLab',
                },
                body: JSON.stringify({
                    model: modelName,
                    messages,
                    temperature: model.temperature,
                    max_tokens: model.maxTokens,
                    usage: { include: true },
                }),
            });
            if (!response.ok) {
                const errorData = (await response.json());
                throw new Error(`OpenRouter API error: ${response.status} - ${errorData?.error?.message || response.statusText}`);
            }
            const data = (await response.json());
            const finalResponse = data.choices?.[0]?.message?.content || '';
            const reasoning = data.choices?.[0]?.message?.reasoning;
            const totalCost = data.usage?.cost;
            // Log total cost if available
            if (typeof totalCost === 'number' && this.context?.logger) {
                this.context.logger.logTokenUsage({
                    usage: totalCost,
                    service: CredentialType.OPENROUTER_CRED,
                    unit: 'total_cost_usd',
                    subService: model.model,
                }, `Deep Research total cost: $${totalCost.toFixed(4)}`, {
                    bubbleName: 'ai-agent',
                    variableId: this.context?.variableId,
                    operationType: 'bubble_execution',
                });
            }
            // Emit complete event
            await this.streamingCallback?.({
                type: 'llm_complete',
                data: {
                    messageId: data.id,
                    content: finalResponse,
                    totalTokens: data.usage?.total_tokens,
                },
            });
            return {
                response: finalResponse,
                reasoning,
                toolCalls: [],
                iterations: 1,
                totalCost,
                error: '',
                success: true,
            };
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : 'Unknown error';
            await this.streamingCallback?.({
                type: 'error',
                data: {
                    error: errorMessage,
                    recoverable: false,
                },
            });
            return {
                response: `Deep Research Error: ${errorMessage}`,
                toolCalls: [],
                iterations: 0,
                error: errorMessage,
                success: false,
            };
        }
    }
    initializeModel(modelConfig) {
        const { model, temperature, maxTokens, maxRetries } = modelConfig;
        const slashIndex = model.indexOf('/');
        const provider = model.substring(0, slashIndex);
        const modelName = model.substring(slashIndex + 1);
        const reasoningEffort = modelConfig.reasoningEffort;
        // Get credential based on the modelConfig's provider (not this.params.model)
        const credentials = this.params.credentials;
        if (!credentials || typeof credentials !== 'object') {
            throw new Error(`No ${provider.toUpperCase()} credentials provided`);
        }
        let apiKey;
        switch (provider) {
            case 'openai':
                apiKey = credentials[CredentialType.OPENAI_CRED];
                break;
            case 'google':
                apiKey = credentials[CredentialType.GOOGLE_GEMINI_CRED];
                break;
            case 'anthropic':
                apiKey = credentials[CredentialType.ANTHROPIC_CRED];
                break;
            case 'openrouter':
                apiKey = credentials[CredentialType.OPENROUTER_CRED];
                break;
            default:
                throw new Error(`Unsupported model provider: ${provider}`);
        }
        if (!apiKey) {
            throw new Error(`No credential found for provider: ${provider}`);
        }
        // Enable streaming if streamingCallback is provided
        const enableStreaming = !!this.streamingCallback;
        // Default to 3 retries if not specified
        const retries = maxRetries ?? 3;
        switch (provider) {
            case 'openai':
                return new ChatOpenAI({
                    model: modelName,
                    temperature,
                    maxTokens,
                    apiKey,
                    ...(reasoningEffort && {
                        reasoning: {
                            effort: reasoningEffort,
                            summary: 'auto',
                        },
                    }),
                    streaming: enableStreaming,
                    maxRetries: retries,
                });
            case 'google': {
                const thinkingConfig = reasoningEffort
                    ? {
                        includeThoughts: reasoningEffort ? true : false,
                        thinkingBudget: reasoningEffort === 'low'
                            ? 1025
                            : reasoningEffort === 'medium'
                                ? 5000
                                : 10000,
                    }
                    : undefined;
                return new SafeGeminiChat({
                    model: modelName,
                    temperature,
                    maxOutputTokens: maxTokens,
                    ...(thinkingConfig && { thinkingConfig }),
                    apiKey,
                    // 3.0 pro preview does breaks with streaming, disabled temporarily until fixed
                    streaming: false,
                    maxRetries: retries,
                    // Disable all safety filters to prevent candidateContent.parts.reduce errors
                    // when Gemini blocks content and returns candidates without content field
                    safetySettings: [
                        {
                            category: HarmCategory.HARM_CATEGORY_HARASSMENT,
                            threshold: HarmBlockThreshold.BLOCK_NONE,
                        },
                        {
                            category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
                            threshold: HarmBlockThreshold.BLOCK_NONE,
                        },
                        {
                            category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
                            threshold: HarmBlockThreshold.BLOCK_NONE,
                        },
                        {
                            category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
                            threshold: HarmBlockThreshold.BLOCK_NONE,
                        },
                    ],
                });
            }
            case 'anthropic': {
                // Configure Anthropic "thinking" only when reasoning is enabled.
                // Anthropic's API does not allow `budget_tokens` when thinking is disabled.
                const thinkingConfig = reasoningEffort != null
                    ? {
                        type: 'enabled',
                        budget_tokens: reasoningEffort === 'low'
                            ? 1025
                            : reasoningEffort === 'medium'
                                ? 5000
                                : 10000,
                    }
                    : undefined;
                return new ChatAnthropic({
                    model: modelName,
                    temperature,
                    anthropicApiKey: apiKey,
                    maxTokens,
                    streaming: true,
                    apiKey,
                    ...(thinkingConfig && { thinking: thinkingConfig }),
                    maxRetries: retries,
                });
            }
            case 'openrouter':
                console.log('openrouter', modelName);
                return new ChatOpenAI({
                    model: modelName,
                    __includeRawResponse: true,
                    temperature,
                    maxTokens,
                    apiKey,
                    streaming: enableStreaming,
                    maxRetries: retries,
                    configuration: {
                        baseURL: 'https://openrouter.ai/api/v1',
                    },
                    modelKwargs: {
                        provider: {
                            order: this.params.model.provider,
                        },
                        reasoning: {
                            effort: reasoningEffort ?? 'medium',
                            exclude: false,
                        },
                    },
                });
            default:
                throw new Error(`Unsupported model provider: ${provider}`);
        }
    }
    async initializeTools(toolConfigs, customToolConfigs = []) {
        const tools = [];
        await this.factory.registerDefaults();
        // First, initialize custom tools
        for (const customTool of customToolConfigs) {
            try {
                console.log(`🛠️ [AIAgent] Initializing custom tool: ${customTool.name}`);
                // Handle both plain object and Zod object schemas
                let schema;
                if (customTool.schema &&
                    typeof customTool.schema === 'object' &&
                    '_def' in customTool.schema) {
                    // Already a Zod schema object, use it directly
                    schema = customTool.schema;
                }
                else {
                    // Plain object, convert to Zod object
                    schema = z.object(customTool.schema);
                }
                const dynamicTool = new DynamicStructuredTool({
                    name: customTool.name,
                    description: customTool.description,
                    schema: schema,
                    func: customTool.func,
                });
                tools.push(dynamicTool);
            }
            catch (error) {
                console.error(`Error initializing custom tool '${customTool.name}':`, error);
                // Continue with other tools even if one fails
                continue;
            }
        }
        // Then, initialize pre-registered tools from factory
        for (const toolConfig of toolConfigs) {
            try {
                const ToolBubbleClass = this.factory.get(toolConfig.name);
                if (!ToolBubbleClass) {
                    if (this.context && this.context.logger) {
                        this.context.logger.warn(`Tool bubble '${toolConfig.name}' not found in factory. This tool will not be used.`);
                    }
                    console.warn(`Tool bubble '${toolConfig.name}' not found in factory. This tool will not be used.`);
                    continue;
                }
                // Check if it's a tool bubble (has toAgentTool method)
                if (!('type' in ToolBubbleClass) || ToolBubbleClass.type !== 'tool') {
                    console.warn(`Bubble '${toolConfig.name}' is not a tool bubble`);
                    continue;
                }
                // Convert to LangGraph tool and add to tools array
                if (!ToolBubbleClass.toolAgent) {
                    console.warn(`Tool bubble '${toolConfig.name}' does not have a toolAgent method`);
                    continue;
                }
                // Get tool's credential requirements and pass relevant credentials from AI agent
                const toolCredentialOptions = BUBBLE_CREDENTIAL_OPTIONS[toolConfig.name] || [];
                const toolCredentials = {};
                // Pass AI agent's credentials to tools that need them
                for (const credType of toolCredentialOptions) {
                    if (this.params.credentials && this.params.credentials[credType]) {
                        toolCredentials[credType] = this.params.credentials[credType];
                    }
                }
                // Merge with any explicitly provided tool credentials (explicit ones take precedence)
                const finalToolCredentials = {
                    ...toolCredentials,
                    ...(toolConfig.credentials || {}),
                };
                console.log(`🔍 [AIAgent] Passing credentials to ${toolConfig.name}:`, Object.keys(finalToolCredentials));
                const langGraphTool = ToolBubbleClass.toolAgent(finalToolCredentials, toolConfig.config || {}, this.context);
                const dynamicTool = new DynamicStructuredTool({
                    name: langGraphTool.name,
                    description: langGraphTool.description,
                    schema: langGraphTool.schema,
                    func: langGraphTool.func,
                });
                tools.push(dynamicTool);
            }
            catch (error) {
                console.error(`Error initializing tool '${toolConfig.name}':`, error);
                // Continue with other tools even if one fails
                continue;
            }
        }
        // 3. Capability tools
        const caps = this.params.capabilities ?? [];
        if (caps.length > 1) {
            // Multi-capability: delegation mode — register a single use-capability tool
            const capIds = caps.map((c) => c.id);
            tools.push(new DynamicStructuredTool({
                name: 'use-capability',
                description: 'Delegate a task to a specialized capability. The capability has its own tools and context to handle the task.',
                schema: z.object({
                    capabilityId: z
                        .enum(capIds)
                        .describe('Which capability to delegate to'),
                    task: z
                        .string()
                        .describe('Clear description of what to do. Include any relevant context from the conversation. Always include information about the users timezone and current time.'),
                }),
                func: async (input) => {
                    const capabilityId = input.capabilityId;
                    const task = input.task;
                    const capConfig = caps.find((c) => c.id === capabilityId);
                    const capDef = getCapability(capabilityId);
                    if (!capConfig || !capDef)
                        return { error: `Capability "${capabilityId}" not found` };
                    const subAgent = new AIAgentBubble({
                        message: task,
                        systemPrompt: '', // capability's systemPrompt fills this via beforeAction
                        name: `Capability Agent: ${capDef.metadata.name}`,
                        model: { ...this.params.model },
                        capabilities: [capConfig], // single cap = eager load in sub-agent
                        credentials: this.params.credentials,
                    }, this.context);
                    const result = await subAgent.action();
                    if (!result.success) {
                        return { success: false, error: result.error };
                    }
                    return { success: true, response: result.data?.response };
                },
            }));
            console.log(`🔧 [AIAgent] Multi-capability delegation mode: registered use-capability tool for [${capIds.join(', ')}]`);
        }
        else {
            // Single capability or none: register tools directly as before
            for (const capConfig of caps) {
                const capDef = getCapability(capConfig.id);
                if (!capDef) {
                    console.warn(`[AIAgent] Capability '${capConfig.id}' not found in registry. Skipping.`);
                    continue;
                }
                try {
                    // Shared ctx captured by all tool funcs via closure — we mutate bubbleContext per-tool
                    const ctx = {
                        credentials: this.resolveCapabilityCredentials(capDef, capConfig),
                        inputs: capConfig.inputs ?? {},
                        bubbleContext: this.context,
                    };
                    const toolFuncs = capDef.createTools(ctx);
                    const logger = this.context?.logger;
                    for (const toolMeta of capDef.metadata.tools) {
                        const func = toolFuncs[toolMeta.name];
                        if (!func)
                            continue;
                        // Resolve this capability tool's variableId and uniqueId from the dependency graph
                        const { variableId: capToolVariableId, uniqueId: capToolUniqueId } = this.resolveCapabilityToolNode(toolMeta.name);
                        // Build a per-tool child context so inner bubbles resolve under this capability tool
                        const capToolContext = this.context
                            ? {
                                ...this.context,
                                variableId: capToolVariableId,
                                currentUniqueId: capToolUniqueId,
                                __uniqueIdCounters__: {},
                            }
                            : undefined;
                        // Wrap the tool function with execution logging and per-tool context
                        const wrappedFunc = async (input) => {
                            logger?.logBubbleExecution(capToolVariableId, toolMeta.name, toolMeta.name, input);
                            try {
                                // Swap ctx.bubbleContext so inner bubbles resolve under the capability tool node
                                ctx.bubbleContext = capToolContext;
                                const result = await func(input);
                                ctx.bubbleContext = this.context; // restore
                                logger?.logBubbleExecutionComplete(capToolVariableId, toolMeta.name, toolMeta.name, result);
                                return result;
                            }
                            catch (error) {
                                ctx.bubbleContext = this.context; // restore on error
                                logger?.logBubbleExecutionComplete(capToolVariableId, toolMeta.name, toolMeta.name, { success: false, error: String(error) });
                                throw error;
                            }
                        };
                        // Convert JSON schema back to Zod for DynamicStructuredTool
                        const toolSchema = this.jsonSchemaToZod(toolMeta.parameterSchema);
                        const dynamicTool = new DynamicStructuredTool({
                            name: toolMeta.name,
                            description: toolMeta.description,
                            schema: toolSchema,
                            func: wrappedFunc,
                        });
                        tools.push(dynamicTool);
                        console.log(`🔧 [AIAgent] Registered capability tool: ${toolMeta.name} (from ${capConfig.id}, variableId: ${capToolVariableId})`);
                    }
                }
                catch (error) {
                    console.error(`Error initializing capability '${capConfig.id}':`, error);
                    continue;
                }
            }
        }
        return tools;
    }
    /**
     * Resolves credentials for a capability by pulling from the bubble-level
     * credentials (this.params.credentials) for each type the capability requires,
     * then overlaying any explicitly set capConfig.credentials on top.
     */
    resolveCapabilityCredentials(capDef, capConfig) {
        const resolved = {};
        // Pull from bubble-level credentials for each type the capability needs
        const allCredTypes = [
            ...capDef.metadata.requiredCredentials,
            ...(capDef.metadata.optionalCredentials ?? []),
        ];
        for (const credType of allCredTypes) {
            if (this.params.credentials && this.params.credentials[credType]) {
                resolved[credType] = this.params.credentials[credType];
            }
        }
        // Overlay any explicitly set capability-level credentials (take precedence)
        if (capConfig.credentials) {
            for (const [key, value] of Object.entries(capConfig.credentials)) {
                resolved[key] = value;
            }
        }
        return resolved;
    }
    /**
     * Resolves the variableId and uniqueId for a capability tool by walking the dependency graph.
     * Finds the child node matching the given tool name under the current ai-agent node.
     * Falls back to -999/empty string if the dependency graph is unavailable or the tool is not found.
     */
    resolveCapabilityToolNode(toolName) {
        const graph = this.context?.dependencyGraph;
        const currentId = this.context?.currentUniqueId;
        if (!graph)
            return { variableId: -999, uniqueId: '' };
        // Find the current ai-agent node in the dependency graph
        const findByUniqueId = (node, target) => {
            if (node.uniqueId === target)
                return node;
            for (const child of node.dependencies || []) {
                const found = findByUniqueId(child, target);
                if (found)
                    return found;
            }
            return null;
        };
        const parentNode = currentId ? findByUniqueId(graph, currentId) : graph;
        if (!parentNode?.dependencies)
            return { variableId: -999, uniqueId: '' };
        // Find the child node matching the capability tool name
        const matchingChild = parentNode.dependencies.find((c) => c.name === toolName);
        return {
            variableId: matchingChild?.variableId ?? -999,
            uniqueId: matchingChild?.uniqueId ?? '',
        };
    }
    /**
     * Converts a JSON Schema object to a Zod schema for DynamicStructuredTool.
     * Handles common JSON Schema types used by capability tool definitions.
     */
    jsonSchemaToZod(jsonSchema) {
        const properties = jsonSchema.properties;
        const required = jsonSchema.required ?? [];
        if (!properties || Object.keys(properties).length === 0) {
            return z.object({});
        }
        const shape = {};
        for (const [key, prop] of Object.entries(properties)) {
            let fieldSchema;
            switch (prop.type) {
                case 'string':
                    fieldSchema = z.string();
                    if (prop.description)
                        fieldSchema = fieldSchema.describe(prop.description);
                    break;
                case 'number':
                case 'integer':
                    fieldSchema = z.number();
                    if (prop.description)
                        fieldSchema = fieldSchema.describe(prop.description);
                    break;
                case 'boolean':
                    fieldSchema = z.boolean();
                    if (prop.description)
                        fieldSchema = fieldSchema.describe(prop.description);
                    break;
                case 'array':
                    fieldSchema = z.array(z.unknown());
                    if (prop.description)
                        fieldSchema = fieldSchema.describe(prop.description);
                    break;
                default:
                    fieldSchema = z.unknown();
                    if (prop.description)
                        fieldSchema = fieldSchema.describe(prop.description);
                    break;
            }
            if (!required.includes(key)) {
                fieldSchema = fieldSchema.optional();
            }
            shape[key] = fieldSchema;
        }
        return z.object(shape);
    }
    /**
     * Custom tool execution node that supports hooks
     */
    async executeToolsWithHooks(state, tools) {
        const { messages } = state;
        const lastMessage = messages[messages.length - 1];
        const toolCalls = lastMessage.tool_calls || [];
        const toolMessages = [];
        let currentMessages = [...messages];
        // Reset stop flag at the start of tool execution
        this.shouldStopAfterTools = false;
        // Execute each tool call
        for (const toolCall of toolCalls) {
            const tool = tools.find((t) => t.name === toolCall.name);
            if (!tool) {
                console.warn(`Tool ${toolCall.name} not found`);
                const availableToolNames = tools.map((t) => t.name).join(', ');
                const errorContent = `Error: Tool "${toolCall.name}" not found. Available tools: ${availableToolNames}`;
                const startTime = Date.now();
                // Send tool_start event (include variableId for console tracking)
                this.streamingCallback?.({
                    type: 'tool_call_start',
                    data: {
                        tool: toolCall.name,
                        input: toolCall.args,
                        callId: toolCall.id,
                        variableId: this.context?.variableId,
                    },
                });
                // Send tool_complete event with error
                this.streamingCallback?.({
                    type: 'tool_call_complete',
                    data: {
                        callId: toolCall.id,
                        input: toolCall.args,
                        tool: toolCall.name,
                        output: { error: errorContent },
                        duration: Date.now() - startTime,
                        variableId: this.context?.variableId,
                    },
                });
                const errorMessage = new ToolMessage({
                    content: errorContent,
                    tool_call_id: toolCall.id,
                });
                toolMessages.push(errorMessage);
                currentMessages = [...currentMessages, errorMessage];
                continue;
            }
            const startTime = Date.now();
            try {
                // Call beforeToolCall hook if provided
                const hookResult_before = await this.beforeToolCallHook?.({
                    toolName: toolCall.name,
                    toolInput: toolCall.args,
                    messages: currentMessages,
                });
                this.streamingCallback?.({
                    type: 'tool_call_start',
                    data: {
                        tool: toolCall.name,
                        input: toolCall.args,
                        callId: toolCall.id,
                        variableId: this.context?.variableId,
                    },
                });
                // If hook returns modified messages/toolInput, apply them
                if (hookResult_before) {
                    if (hookResult_before.messages) {
                        currentMessages = hookResult_before.messages;
                    }
                    toolCall.args = hookResult_before.toolInput;
                }
                // Execute the tool
                const toolOutput = await tool.invoke(toolCall.args);
                // Create tool message
                const toolMessage = new ToolMessage({
                    content: typeof toolOutput === 'string'
                        ? toolOutput
                        : JSON.stringify(toolOutput),
                    tool_call_id: toolCall.id,
                });
                toolMessages.push(toolMessage);
                currentMessages = [...currentMessages, toolMessage];
                // Call afterToolCall hook if provided
                const hookResult_after = await this.afterToolCallHook?.({
                    toolName: toolCall.name,
                    toolInput: toolCall.args,
                    toolOutput,
                    messages: currentMessages,
                });
                // If hook returns modified messages, update current messages
                if (hookResult_after) {
                    if (hookResult_after.messages) {
                        currentMessages = hookResult_after.messages;
                    }
                    // Check if hook wants to stop execution
                    if (hookResult_after.shouldStop === true) {
                        this.shouldStopAfterTools = true;
                    }
                }
                this.streamingCallback?.({
                    type: 'tool_call_complete',
                    data: {
                        callId: toolCall.id,
                        input: toolCall.args,
                        tool: toolCall.name,
                        output: toolOutput,
                        duration: Date.now() - startTime,
                        variableId: this.context?.variableId,
                    },
                });
            }
            catch (error) {
                console.error(`Error executing tool ${toolCall.name}:`, error);
                const errorContent = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
                const errorMessage = new ToolMessage({
                    content: errorContent,
                    tool_call_id: toolCall.id,
                });
                toolMessages.push(errorMessage);
                currentMessages = [...currentMessages, errorMessage];
                // Send tool_complete event even on failure so frontend can track it properly
                this.streamingCallback?.({
                    type: 'tool_call_complete',
                    data: {
                        callId: toolCall.id,
                        input: toolCall.args,
                        tool: toolCall.name,
                        output: { error: errorContent },
                        duration: Date.now() - startTime,
                        variableId: this.context?.variableId,
                    },
                });
            }
        }
        // Return the updated messages
        // If hooks modified messages, use those; otherwise use the original messages + tool messages
        if (currentMessages.length !== messages.length + toolMessages.length) {
            console.error('[AIAgent] Current messages length does not match expected length', currentMessages.length, messages.length, toolMessages.length);
            return { messages: currentMessages };
        }
        return { messages: toolMessages };
    }
    async createAgentGraph(llm, tools, systemPrompt) {
        // Define the agent node
        const agentNode = async ({ messages }) => {
            // systemPrompt is already enhanced by beforeAction() if expectedOutputSchema was provided
            const systemMessage = new HumanMessage(systemPrompt);
            const allMessages = [systemMessage, ...messages];
            // Helper function for exponential backoff with jitter
            const exponentialBackoff = (attemptNumber) => {
                // Base delay: 1 second, exponentially increases (1s, 2s, 4s, 8s, ...)
                const baseDelay = 1000;
                const maxDelay = 32000; // Cap at 32 seconds
                const delay = Math.min(baseDelay * Math.pow(2, attemptNumber - 1), maxDelay);
                // Add jitter (random ±25% variation) to prevent thundering herd
                const jitter = delay * 0.25 * (Math.random() - 0.5);
                const finalDelay = delay + jitter;
                return new Promise((resolve) => setTimeout(resolve, finalDelay));
            };
            // Shared onFailedAttempt callback to avoid duplication
            const onFailedAttempt = async (error) => {
                const attemptNumber = error.attemptNumber;
                const retriesLeft = error.retriesLeft;
                // Check if this is a candidateContent error
                const errorMessage = error.message || String(error);
                if (errorMessage.includes('candidateContent') ||
                    errorMessage.includes('parts.reduce') ||
                    errorMessage.includes('undefined is not an object')) {
                    this.context?.logger?.error(`[AIAgent] Gemini candidateContent error detected (attempt ${attemptNumber}). This indicates blocked/empty content from Gemini API.`);
                }
                this.context?.logger?.warn(`[AIAgent] LLM call failed (attempt ${attemptNumber}/${this.params.model.maxRetries}). Retries left: ${retriesLeft}. Error: ${error.message}`);
                // Optionally emit streaming event for retry
                if (this.streamingCallback) {
                    await this.streamingCallback({
                        type: 'error',
                        data: {
                            error: `Retry attempt ${attemptNumber}/${this.params.model.maxRetries}: ${error.message}`,
                            recoverable: retriesLeft > 0,
                        },
                    });
                }
                // Wait with exponential backoff before retrying
                if (retriesLeft > 0) {
                    await exponentialBackoff(attemptNumber);
                }
            };
            // If we have tools, bind them to the LLM, then add retry logic
            // IMPORTANT: Must bind tools FIRST, then add retry - not the other way around
            const modelWithTools = tools.length > 0
                ? llm.bindTools(tools).withRetry({
                    stopAfterAttempt: this.params.model.maxRetries,
                    onFailedAttempt,
                })
                : llm.withRetry({
                    stopAfterAttempt: this.params.model.maxRetries,
                    onFailedAttempt,
                });
            try {
                // Use streaming if streamingCallback is provided
                if (this.streamingCallback) {
                    const messageId = `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
                    // Use invoke with callbacks for streaming
                    const response = await modelWithTools.invoke(allMessages, {
                        callbacks: [
                            {
                                handleLLMStart: async () => {
                                    await this.streamingCallback?.({
                                        type: 'llm_start',
                                        data: {
                                            model: this.params.model.model,
                                            temperature: this.params.model.temperature,
                                        },
                                    });
                                },
                                handleLLMEnd: async (output) => {
                                    // Extract thinking tokens from different model providers
                                    const thinking = extractAndStreamThinkingTokens(output);
                                    if (thinking) {
                                        await this.streamingCallback?.({
                                            type: 'think',
                                            data: {
                                                content: thinking,
                                                messageId,
                                            },
                                        });
                                    }
                                    const content = formatFinalResponse(generationsToMessageContent(output.generations.flat()), this.params.model.model).response;
                                    await this.streamingCallback?.({
                                        type: 'llm_complete',
                                        data: {
                                            messageId,
                                            content: content,
                                            totalTokens: output.llmOutput?.usage_metadata?.total_tokens,
                                        },
                                    });
                                },
                            },
                        ],
                    });
                    return { messages: [response] };
                }
                else {
                    // Non-streaming fallback
                    const response = await modelWithTools.invoke(allMessages);
                    return { messages: [response] };
                }
            }
            catch (error) {
                // Catch candidateContent errors that slip through SafeGeminiChat
                const errorMessage = error instanceof Error ? error.message : String(error);
                if (errorMessage.includes('candidateContent') ||
                    errorMessage.includes('parts.reduce') ||
                    errorMessage.includes('undefined is not an object')) {
                    console.error('[AIAgent] Caught candidateContent error in agentNode:', errorMessage);
                    // Return error as AIMessage instead of crashing
                    return {
                        messages: [
                            new AIMessage({
                                content: `[Gemini Error] Unable to generate response due to content filtering. Error: ${errorMessage}`,
                                additional_kwargs: {
                                    finishReason: 'ERROR',
                                    error: errorMessage,
                                },
                            }),
                        ],
                    };
                }
                // Rethrow other errors
                throw error;
            }
        };
        // Node that runs after agent to check afterLLMCall hook before routing
        const afterLLMCheckNode = async ({ messages, }) => {
            // Reset the flag at the start
            this.shouldContinueToAgent = false;
            // Get the last AI message
            const lastMessage = messages[messages.length - 1];
            const hasToolCalls = !!(lastMessage.tool_calls && lastMessage.tool_calls.length > 0);
            // Only call hook if we're about to end (no tool calls) and hook is provided
            if (!hasToolCalls && this.afterLLMCallHook) {
                console.log('[AIAgent] No tool calls detected, calling afterLLMCall hook');
                const hookResult = await this.afterLLMCallHook({
                    messages,
                    lastAIMessage: lastMessage,
                    hasToolCalls,
                });
                // If hook wants to continue to agent, set flag and return modified messages
                if (hookResult.continueToAgent) {
                    console.log('[AIAgent] afterLLMCall hook requested retry to agent');
                    this.shouldContinueToAgent = true;
                    // Return the modified messages from the hook
                    // We need to return only the new messages to append
                    const newMessages = hookResult.messages.slice(messages.length);
                    return { messages: newMessages };
                }
            }
            // No modifications needed
            return { messages: [] };
        };
        // Define conditional edge function after LLM check
        const shouldContinueAfterLLMCheck = ({ messages, }) => {
            // First check if afterLLMCall hook requested continuing to agent
            if (this.shouldContinueToAgent) {
                return 'agent';
            }
            // Find the last AI message (could be followed by human messages from hook)
            const aiMessages = [];
            for (const msg of messages) {
                if (isAIMessage(msg)) {
                    aiMessages.push(msg);
                }
                else if ('tool_calls' in msg &&
                    msg.constructor?.name === 'AIMessageChunk') {
                    aiMessages.push(msg);
                }
            }
            const lastAIMessage = aiMessages[aiMessages.length - 1];
            // Check if the last AI message has tool calls
            if (lastAIMessage?.tool_calls && lastAIMessage.tool_calls.length > 0) {
                return 'tools';
            }
            return '__end__';
        };
        // Define conditional edge after tools to check if we should stop
        const shouldContinueAfterTools = () => {
            // Check if the afterToolCall hook requested stopping
            if (this.shouldStopAfterTools) {
                return '__end__';
            }
            // Otherwise continue back to agent
            return 'agent';
        };
        // Build the graph
        const graph = new StateGraph(MessagesAnnotation).addNode('agent', agentNode);
        if (tools.length > 0) {
            // Use custom tool node with hooks support
            const toolNode = async (state) => {
                return await this.executeToolsWithHooks(state, tools);
            };
            graph
                .addNode('tools', toolNode)
                .addNode('afterLLMCheck', afterLLMCheckNode)
                .addEdge('__start__', 'agent')
                .addEdge('agent', 'afterLLMCheck')
                .addConditionalEdges('afterLLMCheck', shouldContinueAfterLLMCheck)
                .addConditionalEdges('tools', shouldContinueAfterTools);
        }
        else {
            // Even without tools, add the afterLLMCheck node for hook support
            graph
                .addNode('afterLLMCheck', afterLLMCheckNode)
                .addEdge('__start__', 'agent')
                .addEdge('agent', 'afterLLMCheck')
                .addConditionalEdges('afterLLMCheck', shouldContinueAfterLLMCheck);
        }
        return graph.compile();
    }
    async executeAgent(graph, message, images, maxIterations, modelConfig, conversationHistory) {
        const jsonMode = modelConfig.jsonMode;
        const toolCalls = [];
        let iterations = 0;
        try {
            // Build messages array starting with conversation history (for KV cache optimization)
            const initialMessages = [];
            // Convert conversation history to LangChain messages if provided
            // This enables KV cache optimization by keeping previous turns as separate messages
            if (conversationHistory && conversationHistory.length > 0) {
                for (const historyMsg of conversationHistory) {
                    switch (historyMsg.role) {
                        case 'user':
                            initialMessages.push(new HumanMessage(historyMsg.content));
                            break;
                        case 'assistant':
                            initialMessages.push(new AIMessage(historyMsg.content));
                            break;
                        case 'tool':
                            // Tool messages require a tool_call_id
                            if (historyMsg.toolCallId) {
                                initialMessages.push(new ToolMessage({
                                    content: historyMsg.content,
                                    tool_call_id: historyMsg.toolCallId,
                                    name: historyMsg.name,
                                }));
                            }
                            break;
                    }
                }
            }
            // Create the current human message with text and optional images
            let humanMessage;
            if (images && images.length > 0) {
                console.log('[AIAgent] Creating multimodal message with', images.length, 'images');
                // Create multimodal content array
                const content = [{ type: 'text', text: message }];
                // Add images to content
                for (const image of images) {
                    let imageUrl;
                    if (image.type === 'base64') {
                        // Base64 encoded image
                        imageUrl = `data:${image.mimeType};base64,${image.data}`;
                    }
                    else {
                        // URL image - fetch and convert to base64 for Google Gemini compatibility
                        try {
                            console.log('[AIAgent] Fetching image from URL:', image.url);
                            const response = await fetch(image.url);
                            if (!response.ok) {
                                throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
                            }
                            const arrayBuffer = await response.arrayBuffer();
                            const base64Data = Buffer.from(arrayBuffer).toString('base64');
                            // Detect MIME type from response or default to PNG
                            const contentType = response.headers.get('content-type') || 'image/png';
                            imageUrl = `data:${contentType};base64,${base64Data}`;
                            console.log('[AIAgent] Successfully converted URL image to base64');
                        }
                        catch (error) {
                            console.error('[AIAgent] Error fetching image from URL:', error);
                            throw new Error(`Failed to load image from URL ${image.url}: ${error instanceof Error ? error.message : 'Unknown error'}`);
                        }
                    }
                    content.push({
                        type: 'image_url',
                        image_url: { url: imageUrl },
                    });
                    // Add image description if provided
                    if (image.description) {
                        content.push({
                            type: 'text',
                            text: `Image description: ${image.description}`,
                        });
                    }
                }
                humanMessage = new HumanMessage({ content });
            }
            else {
                // Text-only message
                humanMessage = new HumanMessage(message);
            }
            // Add the current message to the conversation
            initialMessages.push(humanMessage);
            const result = await graph.invoke({ messages: initialMessages }, { recursionLimit: maxIterations });
            console.log('[AIAgent] Graph execution completed');
            console.log('[AIAgent] Total messages:', result.messages.length);
            iterations = result.messages.length;
            // Extract tool calls from messages and track individual LLM calls
            // Store tool calls temporarily to match with their responses
            const toolCallMap = new Map();
            for (let i = 0; i < result.messages.length; i++) {
                const msg = result.messages[i];
                if (msg instanceof AIMessage ||
                    (msg instanceof AIMessageChunk && msg.tool_calls)) {
                    const typedToolCalls = msg.tool_calls;
                    // Log and track tool calls
                    for (const toolCall of typedToolCalls || []) {
                        toolCallMap.set(toolCall.id, {
                            name: toolCall.name,
                            args: toolCall.args,
                        });
                        console.log('[AIAgent] Tool call:', toolCall.name, 'with args:', toolCall.args);
                    }
                }
                else if (msg instanceof ToolMessage) {
                    // Match tool response to its call
                    const toolCall = toolCallMap.get(msg.tool_call_id);
                    if (toolCall) {
                        // Parse content if it's a JSON string
                        let output = msg.content;
                        if (typeof output === 'string') {
                            try {
                                output = JSON.parse(output);
                            }
                            catch {
                                // Keep as string if not valid JSON
                            }
                        }
                        console.log('[AIAgent] Tool output preview:', typeof output === 'string'
                            ? output.substring(0, 100) + '...'
                            : JSON.stringify(output).substring(0, 100) + '...');
                        toolCalls.push({
                            tool: toolCall.name,
                            input: toolCall.args,
                            output,
                        });
                    }
                }
            }
            // Get the final AI message response
            console.log('[AIAgent] Filtering AI messages...');
            const aiMessages = result.messages.filter((msg) => isAIMessage(msg) || isAIMessageChunk(msg));
            console.log('[AIAgent] Found', aiMessages.length, 'AI messages');
            const finalMessage = aiMessages[aiMessages.length - 1];
            if (finalMessage?.additional_kwargs?.finishReason === 'SAFETY_BLOCKED') {
                throw new Error(`[Gemini Error] Unable to generate a response. Please increase maxTokens in model configuration or try again with a different model.`);
            }
            // Check for MAX_TOKENS finish reason
            if (finalMessage?.additional_kwargs?.finishReason === 'MAX_TOKENS') {
                throw new Error('Response was truncated due to max tokens limit. Please increase maxTokens in model configuration.');
            }
            // Track token usage from ALL AI messages (not just the final one)
            // This is critical for multi-iteration workflows where the agent calls tools multiple times
            let totalInputTokens = 0;
            let totalOutputTokens = 0;
            let totalTokensSum = 0;
            for (const msg of result.messages) {
                if (msg instanceof AIMessage ||
                    (msg instanceof AIMessageChunk && msg.usage_metadata)) {
                    totalInputTokens +=
                        msg.usage_metadata?.input_tokens ||
                            0;
                    totalOutputTokens +=
                        msg.usage_metadata?.output_tokens ||
                            0;
                    totalTokensSum +=
                        msg.usage_metadata?.total_tokens ||
                            0;
                }
            }
            if (totalTokensSum > 0 && this.context && this.context.logger) {
                this.context.logger.logTokenUsage({
                    usage: totalInputTokens,
                    service: this.getCredentialTypeForModel(modelConfig.model),
                    unit: 'input_tokens',
                    subService: modelConfig.model,
                }, `LLM completion: ${totalInputTokens} input`, {
                    bubbleName: 'ai-agent',
                    variableId: this.context?.variableId,
                    operationType: 'bubble_execution',
                });
                this.context.logger.logTokenUsage({
                    usage: totalOutputTokens,
                    service: this.getCredentialTypeForModel(modelConfig.model),
                    unit: 'output_tokens',
                    subService: modelConfig.model,
                }, `LLM completion: ${totalOutputTokens} output`, {
                    bubbleName: 'ai-agent',
                    variableId: this.context?.variableId,
                    operationType: 'bubble_execution',
                });
            }
            const response = finalMessage?.content || '';
            // Use shared formatting method
            const formattedResult = formatFinalResponse(response, modelConfig.model, jsonMode);
            // If there's an error from formatting (e.g., invalid JSON), return early
            if (formattedResult.error) {
                return {
                    response: formattedResult.response,
                    toolCalls: toolCalls.length > 0 ? toolCalls : [],
                    iterations,
                    error: formattedResult.error,
                    success: false,
                };
            }
            const finalResponse = formattedResult.response;
            console.log('[AIAgent] Final response length:', typeof finalResponse === 'string'
                ? finalResponse.length
                : JSON.stringify(finalResponse).length);
            console.log('[AIAgent] Tool calls made:', toolCalls.length);
            console.log('[AIAgent] Execution completed with', iterations, 'iterations');
            return {
                response: typeof finalResponse === 'string'
                    ? finalResponse
                    : JSON.stringify(finalResponse),
                toolCalls: toolCalls.length > 0 ? toolCalls : [],
                iterations,
                error: '',
                success: true,
            };
        }
        catch (error) {
            console.warn('[AIAgent] Execution error (continuing):', error);
            console.log('[AIAgent] Tool calls before error:', toolCalls.length);
            console.log('[AIAgent] Iterations before error:', iterations);
            // Model fallback logic - only retry if this config has a backup model
            if (modelConfig.backupModel) {
                console.log(`[AIAgent] Retrying with backup model: ${modelConfig.backupModel.model}`);
                this.context?.logger?.warn(`Primary model ${modelConfig.model} failed: ${error instanceof Error ? error.message : 'Unknown error'}. Retrying with backup model... ${modelConfig.backupModel.model}`);
                this.streamingCallback?.({
                    type: 'error',
                    data: {
                        error: `Primary model ${modelConfig.model} failed: ${error instanceof Error ? error.message : 'Unknown error'}. Retrying with backup model... ${modelConfig.backupModel.model}`,
                        recoverable: true,
                    },
                });
                const backupModelConfig = this.buildModelConfig(modelConfig, modelConfig.backupModel);
                const backupResult = await this.executeWithModel(backupModelConfig);
                return backupResult;
            }
            const errorMessage = error instanceof Error ? error.message : 'Unknown error';
            // Return partial results to allow execution to continue
            // Include any tool calls that were completed before the error
            return {
                response: `Execution error: ${errorMessage}`,
                success: false, // Still false but don't completely halt execution
                iterations,
                toolCalls: toolCalls.length > 0 ? toolCalls : [], // Preserve completed tool calls
                error: errorMessage,
            };
        }
    }
}
//# sourceMappingURL=ai-agent.js.map