From 2a8a461738073b38854ba4852078ab3771bb9584 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Tue, 16 Sep 2025 04:52:37 -0500 Subject: [PATCH 01/12] Skipping secret key verification in automated mode --- .../nodejs/examples/with-http-wrapper.js | 16 +++++------ .../panels/ai_chat/common/EvaluationConfig.ts | 28 +++++++++++++------ .../ai_chat/evaluation/EvaluationAgent.ts | 13 +++++++-- .../evaluation/remote/EvaluationAgent.ts | 9 ++++++ 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/eval-server/nodejs/examples/with-http-wrapper.js b/eval-server/nodejs/examples/with-http-wrapper.js index ae017ac7599..2ec9d0f16c7 100644 --- a/eval-server/nodejs/examples/with-http-wrapper.js +++ b/eval-server/nodejs/examples/with-http-wrapper.js @@ -11,30 +11,30 @@ import { HTTPWrapper } from '../src/lib/HTTPWrapper.js'; console.log('๐Ÿ”ง Creating EvalServer...'); const evalServer = new EvalServer({ - authKey: 'hello', + // No authKey - authentication disabled for automated mode host: '127.0.0.1', - port: 8080 + port: 8082 }); console.log('๐Ÿ”ง Creating HTTP wrapper...'); const httpWrapper = new HTTPWrapper(evalServer, { - port: 8081, + port: 8080, host: '127.0.0.1' }); console.log('๐Ÿ”ง Starting EvalServer...'); await evalServer.start(); -console.log('โœ… EvalServer started on ws://127.0.0.1:8080'); +console.log('โœ… EvalServer started on ws://127.0.0.1:8082'); console.log('๐Ÿ”ง Starting HTTP wrapper...'); await httpWrapper.start(); -console.log('โœ… HTTP API started on http://127.0.0.1:8081'); +console.log('โœ… HTTP API started on http://127.0.0.1:8080'); console.log('โณ Waiting for DevTools client to connect...'); -console.log(' WebSocket URL: ws://127.0.0.1:8080'); -console.log(' HTTP API URL: http://127.0.0.1:8081'); -console.log(' Auth Key: hello'); +console.log(' WebSocket URL: ws://127.0.0.1:8082'); +console.log(' HTTP API URL: http://127.0.0.1:8080'); +console.log(' Auth: Disabled (automated mode)'); // Add periodic status check setInterval(() => { diff --git a/front_end/panels/ai_chat/common/EvaluationConfig.ts b/front_end/panels/ai_chat/common/EvaluationConfig.ts index be5169fe6cb..cda09b94b0e 100644 --- a/front_end/panels/ai_chat/common/EvaluationConfig.ts +++ b/front_end/panels/ai_chat/common/EvaluationConfig.ts @@ -30,7 +30,7 @@ class EvaluationConfigStore { private static instance: EvaluationConfigStore; private config: EvaluationConfiguration = { enabled: false, - endpoint: 'ws://localhost:8080', + endpoint: 'ws://localhost:8082', secretKey: '', clientId: '' }; @@ -50,16 +50,23 @@ class EvaluationConfigStore { private loadFromLocalStorage(): void { try { - // In automated mode, set default to enabled if not already set - if (BUILD_CONFIG.AUTOMATED_MODE && - localStorage.getItem('ai_chat_evaluation_enabled') === null) { - localStorage.setItem('ai_chat_evaluation_enabled', 'true'); - logger.info('Automated mode: defaulted evaluation to enabled'); + // In automated mode, set defaults if not already set + if (BUILD_CONFIG.AUTOMATED_MODE) { + if (localStorage.getItem('ai_chat_evaluation_enabled') === null) { + localStorage.setItem('ai_chat_evaluation_enabled', 'true'); + logger.info('Automated mode: defaulted evaluation to enabled'); + } + if (localStorage.getItem('ai_chat_evaluation_endpoint') === null) { + localStorage.setItem('ai_chat_evaluation_endpoint', 'ws://localhost:8082'); + logger.info('Automated mode: defaulted endpoint to ws://localhost:8082'); + } + // No secret key needed in automated mode } const enabled = localStorage.getItem('ai_chat_evaluation_enabled') === 'true'; - const endpoint = localStorage.getItem('ai_chat_evaluation_endpoint') || 'ws://localhost:8080'; - const secretKey = localStorage.getItem('ai_chat_evaluation_secret_key') || ''; + const endpoint = localStorage.getItem('ai_chat_evaluation_endpoint') || 'ws://localhost:8082'; + // Don't use secretKey in automated mode + const secretKey = BUILD_CONFIG.AUTOMATED_MODE ? undefined : (localStorage.getItem('ai_chat_evaluation_secret_key') || ''); const clientId = localStorage.getItem('ai_chat_evaluation_client_id') || ''; this.config = { @@ -98,7 +105,10 @@ class EvaluationConfigStore { try { localStorage.setItem('ai_chat_evaluation_enabled', String(this.config.enabled)); localStorage.setItem('ai_chat_evaluation_endpoint', this.config.endpoint); - localStorage.setItem('ai_chat_evaluation_secret_key', this.config.secretKey || ''); + // Don't save secret key in automated mode + if (!BUILD_CONFIG.AUTOMATED_MODE) { + localStorage.setItem('ai_chat_evaluation_secret_key', this.config.secretKey || ''); + } localStorage.setItem('ai_chat_evaluation_client_id', this.config.clientId || ''); } catch (error) { logger.warn('Failed to save evaluation config to localStorage:', error); diff --git a/front_end/panels/ai_chat/evaluation/EvaluationAgent.ts b/front_end/panels/ai_chat/evaluation/EvaluationAgent.ts index 7564b17927c..b152fa2f4b5 100644 --- a/front_end/panels/ai_chat/evaluation/EvaluationAgent.ts +++ b/front_end/panels/ai_chat/evaluation/EvaluationAgent.ts @@ -4,6 +4,7 @@ import { WebSocketRPCClient } from '../common/WebSocketRPCClient.js'; import { getEvaluationConfig, getEvaluationClientId } from '../common/EvaluationConfig.js'; +import { BUILD_CONFIG } from '../core/BuildConfig.js'; import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; import { AgentService } from '../core/AgentService.js'; import { createLogger } from '../core/Logger.js'; @@ -252,6 +253,14 @@ export class EvaluationAgent { } private async handleAuthRequest(message: RegistrationAckMessage): Promise { + // In automated mode, skip authentication entirely + if (BUILD_CONFIG.AUTOMATED_MODE) { + logger.info('Automated mode: Skipping authentication verification'); + const authMessage = createAuthVerifyMessage(message.clientId, true); + this.client?.send(authMessage); + return; + } + if (!message.serverSecretKey) { logger.error('Server did not provide secret key for verification'); this.disconnect(); @@ -265,10 +274,10 @@ export class EvaluationAgent { // Verify if the server's secret key matches the client's configured key const verified = clientSecretKey === message.serverSecretKey; - logger.info('Verifying secret key', { + logger.info('Verifying secret key', { hasClientKey: !!clientSecretKey, hasServerKey: !!message.serverSecretKey, - verified + verified }); // Send verification response diff --git a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts index 56ff739b9ff..d13e487b910 100644 --- a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts +++ b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import { BUILD_CONFIG } from '../../core/BuildConfig.js'; import { WebSocketRPCClient } from '../../common/WebSocketRPCClient.js'; import { getEvaluationConfig, getEvaluationClientId } from '../../common/EvaluationConfig.js'; import { ToolRegistry } from '../../agent_framework/ConfigurableAgentTool.js'; @@ -264,6 +265,14 @@ export class EvaluationAgent { } private async handleAuthRequest(message: RegistrationAckMessage): Promise { + // In automated mode, skip authentication entirely + if (BUILD_CONFIG.AUTOMATED_MODE) { + logger.info('Automated mode: Skipping authentication verification'); + const authMessage = createAuthVerifyMessage(message.clientId, true); + this.client?.send(authMessage); + return; + } + if (!message.serverSecretKey) { logger.error('Server did not provide secret key for verification'); this.disconnect(); From e9a4af4515f28bcb957528aa8261e4b1d3f66add Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Tue, 16 Sep 2025 13:01:18 -0500 Subject: [PATCH 02/12] Initial refactoring of LLM config --- eval-server/nodejs/.env.example | 45 +++ eval-server/nodejs/CLAUDE.md | 73 +++- eval-server/nodejs/examples/library-usage.js | 194 ++++++++- eval-server/nodejs/examples/multiple-evals.js | 60 ++- eval-server/nodejs/src/api-server.js | 162 ++++++-- eval-server/nodejs/src/config.js | 43 +- eval-server/nodejs/src/lib/EvalServer.js | 160 +++++++- front_end/panels/ai_chat/core/AgentNodes.ts | 19 +- front_end/panels/ai_chat/core/AgentService.ts | 156 +++++--- .../ai_chat/core/LLMConfigurationManager.ts | 368 ++++++++++++++++++ .../evaluation/remote/EvaluationAgent.ts | 137 ++++++- .../evaluation/remote/EvaluationProtocol.ts | 81 ++++ front_end/panels/ai_chat/ui/AIChatPanel.ts | 56 ++- 13 files changed, 1418 insertions(+), 136 deletions(-) create mode 100644 eval-server/nodejs/.env.example create mode 100644 front_end/panels/ai_chat/core/LLMConfigurationManager.ts diff --git a/eval-server/nodejs/.env.example b/eval-server/nodejs/.env.example new file mode 100644 index 00000000000..a19f3a1cdf5 --- /dev/null +++ b/eval-server/nodejs/.env.example @@ -0,0 +1,45 @@ +# Evaluation Server Configuration +# Copy this file to .env and configure your settings + +# Server Configuration +PORT=8080 +HOST=127.0.0.1 + +# LLM Provider API Keys +# Configure one or more providers for evaluation + +# OpenAI Configuration +OPENAI_API_KEY=sk-your-openai-api-key-here + +# LiteLLM Configuration (if using a LiteLLM server) +LITELLM_ENDPOINT=http://localhost:4000 +LITELLM_API_KEY=your-litellm-api-key-here + +# Groq Configuration +GROQ_API_KEY=gsk_your-groq-api-key-here + +# OpenRouter Configuration +OPENROUTER_API_KEY=sk-or-v1-your-openrouter-api-key-here + +# Default LLM Configuration for Evaluations +# These will be used as fallbacks when not specified in evaluation requests +DEFAULT_PROVIDER=openai +DEFAULT_MAIN_MODEL=gpt-4 +DEFAULT_MINI_MODEL=gpt-4-mini +DEFAULT_NANO_MODEL=gpt-3.5-turbo + +# Logging Configuration +LOG_LEVEL=info +LOG_DIR=./logs + +# Client Configuration +CLIENTS_DIR=./clients +EVALS_DIR=./evals + +# RPC Configuration +RPC_TIMEOUT=30000 + +# Security +# Set this to enable authentication for client connections +# Leave empty to disable authentication +AUTH_SECRET_KEY= \ No newline at end of file diff --git a/eval-server/nodejs/CLAUDE.md b/eval-server/nodejs/CLAUDE.md index 5db83421a3f..9fb61eaf670 100644 --- a/eval-server/nodejs/CLAUDE.md +++ b/eval-server/nodejs/CLAUDE.md @@ -22,6 +22,16 @@ bo-eval-server is a WebSocket-based evaluation server for LLM agents that implem - `OPENAI_API_KEY` - OpenAI API key for LLM judge functionality - `PORT` - WebSocket server port (default: 8080) +### LLM Provider Configuration (Optional) +- `GROQ_API_KEY` - Groq API key for Groq provider support +- `OPENROUTER_API_KEY` - OpenRouter API key for OpenRouter provider support +- `LITELLM_ENDPOINT` - LiteLLM server endpoint URL +- `LITELLM_API_KEY` - LiteLLM API key for LiteLLM provider support +- `DEFAULT_PROVIDER` - Default LLM provider (openai, groq, openrouter, litellm) +- `DEFAULT_MAIN_MODEL` - Default main model name +- `DEFAULT_MINI_MODEL` - Default mini model name +- `DEFAULT_NANO_MODEL` - Default nano model name + ## Architecture ### Core Components @@ -33,10 +43,11 @@ bo-eval-server is a WebSocket-based evaluation server for LLM agents that implem - Handles bidirectional RPC communication **RPC Client** (`src/rpc-client.js`) -- Implements JSON-RPC 2.0 protocol for server-to-client calls +- Implements JSON-RPC 2.0 protocol for bidirectional communication - Manages request/response correlation with unique IDs - Handles timeouts and error conditions - Calls `Evaluate(request: String) -> String` method on connected agents +- Supports `configure_llm` method for dynamic LLM provider configuration **LLM Evaluator** (`src/evaluator.js`) - Integrates with OpenAI API for LLM-as-a-judge functionality @@ -78,7 +89,10 @@ logs/ # Log files (created automatically) ### Key Features - **Bidirectional RPC**: Server can call methods on connected clients -- **LLM-as-a-Judge**: Automated evaluation of agent responses using GPT-4 +- **Multi-Provider LLM Support**: Support for OpenAI, Groq, OpenRouter, and LiteLLM providers +- **Dynamic LLM Configuration**: Runtime configuration via `configure_llm` JSON-RPC method +- **Per-Client Configuration**: Each connected client can have different LLM settings +- **LLM-as-a-Judge**: Automated evaluation of agent responses using configurable LLM providers - **Concurrent Evaluations**: Support for multiple agents and parallel evaluations - **Structured Logging**: All interactions logged as JSON for analysis - **Interactive CLI**: Built-in CLI for testing and server management @@ -93,6 +107,61 @@ Agents must implement: - `Evaluate(task: string) -> string` method - "ready" message to signal availability for evaluations +### LLM Configuration Protocol + +The server supports dynamic LLM configuration via the `configure_llm` JSON-RPC method: + +```json +{ + "jsonrpc": "2.0", + "method": "configure_llm", + "params": { + "provider": "openai|groq|openrouter|litellm", + "apiKey": "your-api-key", + "endpoint": "endpoint-url-for-litellm", + "models": { + "main": "main-model-name", + "mini": "mini-model-name", + "nano": "nano-model-name" + }, + "partial": false + }, + "id": "config-request-id" +} +``` + +### Evaluation Model Configuration + +Evaluations support nested model configuration for flexible per-tier settings: + +```json +{ + "jsonrpc": "2.0", + "method": "evaluate", + "params": { + "tool": "chat", + "input": {"message": "Hello"}, + "model": { + "main_model": { + "provider": "openai", + "model": "gpt-4", + "api_key": "sk-main-key" + }, + "mini_model": { + "provider": "openai", + "model": "gpt-4-mini", + "api_key": "sk-mini-key" + }, + "nano_model": { + "provider": "groq", + "model": "llama-3.1-8b-instant", + "api_key": "gsk-nano-key" + } + } + } +} +``` + ### Configuration All configuration is managed through environment variables and `src/config.js`. Key settings: diff --git a/eval-server/nodejs/examples/library-usage.js b/eval-server/nodejs/examples/library-usage.js index 45da6081540..cfb3ffdf23a 100644 --- a/eval-server/nodejs/examples/library-usage.js +++ b/eval-server/nodejs/examples/library-usage.js @@ -7,6 +7,7 @@ // Simple example demonstrating the programmatic API usage import { EvalServer } from '../src/lib/EvalServer.js'; +import { CONFIG } from '../src/config.js'; console.log('๐Ÿ”ง Creating server...'); const server = new EvalServer({ @@ -31,20 +32,57 @@ server.onConnect(async client => { console.log(' - Client tabId:', client.tabId); console.log(' - Client info:', client.getInfo()); + // Check available LLM providers + console.log('\n๐Ÿ”‘ Available LLM Providers:'); + const availableProviders = []; + if (CONFIG.providers.openai.apiKey) { + availableProviders.push('openai'); + console.log(' โœ… OpenAI configured'); + } + if (CONFIG.providers.groq.apiKey) { + availableProviders.push('groq'); + console.log(' โœ… Groq configured'); + } + if (CONFIG.providers.openrouter.apiKey) { + availableProviders.push('openrouter'); + console.log(' โœ… OpenRouter configured'); + } + if (CONFIG.providers.litellm.apiKey && CONFIG.providers.litellm.endpoint) { + availableProviders.push('litellm'); + console.log(' โœ… LiteLLM configured'); + } + + if (availableProviders.length === 0) { + console.log(' โŒ No providers configured. Add API keys to .env file.'); + console.log(' โ„น๏ธ Example: OPENAI_API_KEY=sk-your-key-here'); + } + try { - console.log('๐Ÿ”„ Starting evaluation...'); + // Demonstrate basic evaluation first + console.log('\n๐Ÿ”„ Starting basic evaluation...'); let response = await client.evaluate({ - id: "test_eval", - name: "Capital of France", - description: "Simple test evaluation", + id: "basic_eval", + name: "Capital of France", + description: "Basic test evaluation", tool: "chat", input: { message: "What is the capital of France?" } }); - - console.log('โœ… Evaluation completed!'); + + console.log('โœ… Basic evaluation completed!'); console.log('๐Ÿ“Š Response:', JSON.stringify(response, null, 2)); + + // Demonstrate explicit model selection if OpenAI is available + if (CONFIG.providers.openai.apiKey) { + await demonstrateModelSelection(client); + } + + // Demonstrate LLM configuration if providers are available + if (availableProviders.length > 0) { + await demonstrateLLMConfiguration(client, availableProviders); + } + } catch (error) { console.log('โŒ Evaluation failed:', error.message); } @@ -54,6 +92,150 @@ server.onDisconnect(clientInfo => { console.log('๐Ÿ‘‹ CLIENT DISCONNECTED:', clientInfo); }); +// Function to demonstrate explicit model selection within OpenAI +async function demonstrateModelSelection(client) { + console.log('\n๐Ÿค– Demonstrating Model Selection (OpenAI)...'); + + const modelTests = [ + { + model: 'gpt-4', + task: 'Complex reasoning', + message: 'Solve this step by step: If a train travels 60 mph for 2.5 hours, how far does it go?' + }, + { + model: 'gpt-4-mini', + task: 'Simple question', + message: 'What is 2 + 2?' + }, + { + model: 'gpt-3.5-turbo', + task: 'Creative writing', + message: 'Write a one-sentence story about a cat.' + } + ]; + + for (const test of modelTests) { + console.log(`\n๐Ÿ”ง Testing ${test.model} for ${test.task}...`); + + try { + const response = await client.evaluate({ + id: `model_test_${test.model.replace(/[^a-z0-9]/g, '_')}`, + name: `${test.model} ${test.task}`, + tool: "chat", + input: { + message: test.message + }, + model: { + main_model: { + provider: "openai", + model: test.model, + api_key: CONFIG.providers.openai.apiKey + } + } + }); + + console.log(` โœ… ${test.model} completed successfully`); + console.log(` ๐Ÿ“Š Response: ${JSON.stringify(response.output).substring(0, 100)}...`); + + // Wait between tests + await new Promise(resolve => setTimeout(resolve, 1500)); + + } catch (error) { + console.log(` โŒ ${test.model} failed: ${error.message}`); + } + } + + console.log('\nโœจ Model selection demonstration completed!'); +} + +// Function to demonstrate LLM configuration +async function demonstrateLLMConfiguration(client, availableProviders) { + console.log('\n๐Ÿงช Demonstrating LLM Configuration...'); + + for (const provider of availableProviders.slice(0, 2)) { // Test up to 2 providers + console.log(`\n๐Ÿ”ง Configuring ${provider.toUpperCase()} provider...`); + + try { + // Configure different models based on provider + let models; + switch (provider) { + case 'openai': + models = { + main: 'gpt-4', + mini: 'gpt-4-mini', + nano: 'gpt-3.5-turbo' + }; + break; + case 'groq': + models = { + main: 'llama-3.1-8b-instant', + mini: 'llama-3.1-8b-instant', + nano: 'llama-3.1-8b-instant' + }; + break; + case 'openrouter': + models = { + main: 'anthropic/claude-3-sonnet', + mini: 'anthropic/claude-3-haiku', + nano: 'anthropic/claude-3-haiku' + }; + break; + case 'litellm': + models = { + main: 'claude-3-sonnet-20240229', + mini: 'claude-3-haiku-20240307', + nano: 'claude-3-haiku-20240307' + }; + break; + } + + console.log(` ๐Ÿ“ฆ Models: main=${models.main}, mini=${models.mini}, nano=${models.nano}`); + + // Run evaluation with specific provider configuration + const response = await client.evaluate({ + id: `${provider}_config_eval`, + name: `${provider.toUpperCase()} Configuration Test`, + description: `Test evaluation using ${provider} provider`, + tool: "chat", + input: { + message: `Hello! This is a test using the ${provider} provider. Please respond with a brief confirmation.` + }, + model: { + main_model: { + provider: provider, + model: models.main, + api_key: CONFIG.providers[provider].apiKey, + endpoint: CONFIG.providers[provider].endpoint + }, + mini_model: { + provider: provider, + model: models.mini, + api_key: CONFIG.providers[provider].apiKey, + endpoint: CONFIG.providers[provider].endpoint + }, + nano_model: { + provider: provider, + model: models.nano, + api_key: CONFIG.providers[provider].apiKey, + endpoint: CONFIG.providers[provider].endpoint + } + } + }); + + console.log(` โœ… ${provider.toUpperCase()} evaluation completed successfully`); + console.log(` ๐Ÿ“Š Response preview: ${JSON.stringify(response.output).substring(0, 100)}...`); + + // Wait between provider tests + await new Promise(resolve => setTimeout(resolve, 2000)); + + } catch (error) { + console.log(` โŒ ${provider.toUpperCase()} configuration test failed:`, error.message); + } + } + + console.log('\nโœจ LLM configuration demonstration completed!'); +} + console.log('๐Ÿ”ง Starting server...'); await server.start(); console.log('โœ… Server started successfully on ws://127.0.0.1:8080'); diff --git a/eval-server/nodejs/examples/multiple-evals.js b/eval-server/nodejs/examples/multiple-evals.js index cd5ee980a1f..b65522f2528 100755 --- a/eval-server/nodejs/examples/multiple-evals.js +++ b/eval-server/nodejs/examples/multiple-evals.js @@ -9,11 +9,12 @@ import { EvalServer } from '../src/lib/EvalServer.js'; import { EvaluationStack } from '../src/lib/EvaluationStack.js'; +import { CONFIG } from '../src/config.js'; console.log('๐Ÿ”ง Creating evaluation stack...'); const evalStack = new EvaluationStack(); -// Create multiple diverse evaluations for the stack +// Create multiple diverse evaluations for the stack with different LLM configurations const evaluations = [ { id: "math_eval", @@ -22,25 +23,49 @@ const evaluations = [ tool: "chat", input: { message: "What is 15 * 7 + 23? Please show your calculation steps." - } + }, + // Use OpenAI if available, otherwise default + model: CONFIG.providers.openai.apiKey ? { + main_model: { + provider: 'openai', + model: 'gpt-4', + api_key: CONFIG.providers.openai.apiKey + } + } : {} }, { - id: "geography_eval", + id: "geography_eval", name: "Capital of France", description: "Geography knowledge test", tool: "chat", input: { message: "What is the capital of France?" - } + }, + // Use Groq if available, otherwise default + model: CONFIG.providers.groq.apiKey ? { + main_model: { + provider: 'groq', + model: 'llama-3.1-8b-instant', + api_key: CONFIG.providers.groq.apiKey + } + } : {} }, { id: "creative_eval", name: "Creative Writing", description: "Short creative writing task", - tool: "chat", + tool: "chat", input: { message: "Write a two-sentence story about a robot discovering friendship." - } + }, + // Use OpenRouter if available, otherwise default + model: CONFIG.providers.openrouter.apiKey ? { + main_model: { + provider: 'openrouter', + model: 'anthropic/claude-3-sonnet', + api_key: CONFIG.providers.openrouter.apiKey + } + } : {} }, { id: "tech_eval", @@ -49,7 +74,16 @@ const evaluations = [ tool: "chat", input: { message: "Explain what HTTP stands for and what it's used for in simple terms." - } + }, + // Use LiteLLM if available, otherwise default + model: (CONFIG.providers.litellm.apiKey && CONFIG.providers.litellm.endpoint) ? { + main_model: { + provider: 'litellm', + model: 'claude-3-haiku-20240307', + api_key: CONFIG.providers.litellm.apiKey, + endpoint: CONFIG.providers.litellm.endpoint + } + } : {} } ]; @@ -57,7 +91,8 @@ const evaluations = [ console.log('๐Ÿ“š Adding evaluations to stack...'); evaluations.forEach((evaluation, index) => { evalStack.push(evaluation); - console.log(` ${index + 1}. ${evaluation.name} (${evaluation.id})`); + const providerInfo = evaluation.model?.main_model?.provider ? ` [${evaluation.model.main_model.provider}]` : ' [default]'; + console.log(` ${index + 1}. ${evaluation.name} (${evaluation.id})${providerInfo}`); }); console.log(`โœ… Stack initialized with ${evalStack.size()} evaluations`); @@ -94,13 +129,18 @@ server.onConnect(async client => { // Pop the next evaluation from the stack const evaluation = evalStack.pop(); - console.log(`๐Ÿ“‹ Assigning evaluation: "${evaluation.name}" (${evaluation.id})`); + const providerInfo = evaluation.model?.main_model?.provider ? ` using ${evaluation.model.main_model.provider}` : ' using default provider'; + console.log(`๐Ÿ“‹ Assigning evaluation: "${evaluation.name}" (${evaluation.id})${providerInfo}`); console.log(`๐Ÿ“Š Remaining evaluations in stack: ${evalStack.size()}`); try { console.log('๐Ÿ”„ Starting evaluation...'); + if (evaluation.model?.main_model?.provider) { + console.log(`๐Ÿ”ง Using LLM provider: ${evaluation.model.main_model.provider} with model: ${evaluation.model.main_model.model}`); + } + let response = await client.evaluate(evaluation); - + console.log('โœ… Evaluation completed!'); console.log(`๐Ÿ“Š Response for "${evaluation.name}":`, JSON.stringify(response, null, 2)); } catch (error) { diff --git a/eval-server/nodejs/src/api-server.js b/eval-server/nodejs/src/api-server.js index 7b0b6355cdd..bfccf1a9145 100644 --- a/eval-server/nodejs/src/api-server.js +++ b/eval-server/nodejs/src/api-server.js @@ -255,7 +255,7 @@ class APIServer { } /** - * Handle OpenAI Responses API compatible requests + * Handle OpenAI Responses API compatible requests with nested model format */ async handleResponsesRequest(requestBody) { try { @@ -264,12 +264,12 @@ class APIServer { throw new Error('Missing or invalid "input" field. Expected a string.'); } - // Merge request parameters with config defaults - const modelConfig = this.mergeModelConfig(requestBody); - + // Handle nested model configuration directly + const nestedModelConfig = this.processNestedModelConfig(requestBody); + logger.info('Processing responses request:', { input: requestBody.input, - modelConfig + modelConfig: nestedModelConfig }); // Find a connected and ready client @@ -279,7 +279,7 @@ class APIServer { } // Create a dynamic evaluation for this request - const evaluation = this.createDynamicEvaluation(requestBody.input, modelConfig); + const evaluation = this.createDynamicEvaluationNested(requestBody.input, nestedModelConfig); // Execute the evaluation on the DevTools client logger.info('Executing evaluation on DevTools client', { @@ -288,10 +288,10 @@ class APIServer { }); const result = await this.evaluationServer.executeEvaluation(readyClient, evaluation); - + // Debug: log the result structure logger.debug('executeEvaluation result:', result); - + // Extract the response text from the result const responseText = this.extractResponseText(result); @@ -305,16 +305,86 @@ class APIServer { } /** - * Merge request model parameters with config.yaml defaults + * Process nested model configuration from request body + */ + processNestedModelConfig(requestBody) { + const defaults = this.configDefaults?.model || {}; + + // If nested format is provided, use it directly with fallbacks + if (requestBody.model) { + return { + main_model: requestBody.model.main_model || this.createDefaultModelConfig('main', defaults), + mini_model: requestBody.model.mini_model || this.createDefaultModelConfig('mini', defaults), + nano_model: requestBody.model.nano_model || this.createDefaultModelConfig('nano', defaults) + }; + } + + // Legacy flat format support - convert to nested + if (requestBody.main_model || requestBody.provider || requestBody.api_key) { + const legacyConfig = this.mergeModelConfig(requestBody); + return this.convertFlatToNested(legacyConfig); + } + + // No model config provided, use defaults + return { + main_model: this.createDefaultModelConfig('main', defaults), + mini_model: this.createDefaultModelConfig('mini', defaults), + nano_model: this.createDefaultModelConfig('nano', defaults) + }; + } + + /** + * Create default model configuration for a tier + */ + createDefaultModelConfig(tier, defaults) { + const defaultModels = { + main: defaults.main_model || 'gpt-4', + mini: defaults.mini_model || 'gpt-4-mini', + nano: defaults.nano_model || 'gpt-3.5-turbo' + }; + + return { + provider: defaults.provider || 'openai', + model: defaultModels[tier], + api_key: process.env.OPENAI_API_KEY + }; + } + + /** + * Convert flat model config to nested format (legacy support) + */ + convertFlatToNested(flatConfig) { + return { + main_model: { + provider: flatConfig.provider, + model: flatConfig.main_model, + api_key: flatConfig.api_key + }, + mini_model: { + provider: flatConfig.provider, + model: flatConfig.mini_model, + api_key: flatConfig.api_key + }, + nano_model: { + provider: flatConfig.provider, + model: flatConfig.nano_model, + api_key: flatConfig.api_key + } + }; + } + + /** + * Merge request model parameters with config.yaml defaults (legacy) */ mergeModelConfig(requestBody) { const defaults = this.configDefaults?.model || {}; - + return { - main_model: requestBody.main_model || defaults.main_model || 'gpt-4.1', - mini_model: requestBody.mini_model || defaults.mini_model || 'gpt-4.1-mini', - nano_model: requestBody.nano_model || defaults.nano_model || 'gpt-4.1-nano', - provider: requestBody.provider || defaults.provider || 'openai' + main_model: requestBody.main_model || defaults.main_model || 'gpt-4', + mini_model: requestBody.mini_model || defaults.mini_model || 'gpt-4-mini', + nano_model: requestBody.nano_model || defaults.nano_model || 'gpt-3.5-turbo', + provider: requestBody.provider || defaults.provider || 'openai', + api_key: requestBody.api_key || process.env.OPENAI_API_KEY }; } @@ -331,30 +401,76 @@ class APIServer { } /** - * Create a dynamic evaluation object for the API request + * Create a dynamic evaluation object with nested model configuration + */ + createDynamicEvaluationNested(input, nestedModelConfig) { + const evaluationId = `api-eval-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + + return { + id: evaluationId, + name: 'API Request', + description: 'Dynamic evaluation created from API request', + enabled: true, + tool: 'chat', + timeout: 1500000, // 25 minutes + input: { + message: input + }, + model: nestedModelConfig, + validation: { + type: 'none' // No validation needed for API responses + }, + metadata: { + tags: ['api', 'dynamic'], + priority: 'high', + source: 'api' + } + }; + } + + /** + * Create a dynamic evaluation object for the API request (legacy) */ createDynamicEvaluation(input, modelConfig) { const evaluationId = `api-eval-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; - + + // Convert flat modelConfig to nested format + const nestedModelConfig = { + main_model: { + provider: modelConfig.provider || 'openai', + model: modelConfig.main_model || 'gpt-4', + api_key: modelConfig.api_key + }, + mini_model: { + provider: modelConfig.provider || 'openai', + model: modelConfig.mini_model || modelConfig.main_model || 'gpt-4-mini', + api_key: modelConfig.api_key + }, + nano_model: { + provider: modelConfig.provider || 'openai', + model: modelConfig.nano_model || modelConfig.mini_model || modelConfig.main_model || 'gpt-3.5-turbo', + api_key: modelConfig.api_key + } + }; + return { id: evaluationId, - name: 'OpenAI API Request', - description: 'Dynamic evaluation created from OpenAI Responses API request', + name: 'API Request', + description: 'Dynamic evaluation created from API request', enabled: true, tool: 'chat', timeout: 1500000, // 25 minutes input: { - message: input, - reasoning: 'API request processing' + message: input }, - model: modelConfig, + model: nestedModelConfig, validation: { type: 'none' // No validation needed for API responses }, metadata: { - tags: ['api', 'dynamic', 'openai-responses'], + tags: ['api', 'dynamic'], priority: 'high', - source: 'openai-api' + source: 'api' } }; } diff --git a/eval-server/nodejs/src/config.js b/eval-server/nodejs/src/config.js index 632d0de167a..4bde4e52d27 100644 --- a/eval-server/nodejs/src/config.js +++ b/eval-server/nodejs/src/config.js @@ -7,21 +7,58 @@ export const CONFIG = { port: parseInt(process.env.PORT) || 8080, host: process.env.HOST || 'localhost' }, - + llm: { apiKey: process.env.OPENAI_API_KEY, model: process.env.JUDGE_MODEL || 'gpt-4', temperature: parseFloat(process.env.JUDGE_TEMPERATURE) || 0.1 }, - + + // LLM Provider Configuration for configure_llm API + providers: { + openai: { + apiKey: process.env.OPENAI_API_KEY + }, + litellm: { + endpoint: process.env.LITELLM_ENDPOINT, + apiKey: process.env.LITELLM_API_KEY + }, + groq: { + apiKey: process.env.GROQ_API_KEY + }, + openrouter: { + apiKey: process.env.OPENROUTER_API_KEY + } + }, + + // Default model configuration + defaults: { + provider: process.env.DEFAULT_PROVIDER || 'openai', + mainModel: process.env.DEFAULT_MAIN_MODEL || 'gpt-4', + miniModel: process.env.DEFAULT_MINI_MODEL || 'gpt-4-mini', + nanoModel: process.env.DEFAULT_NANO_MODEL || 'gpt-3.5-turbo' + }, + logging: { level: process.env.LOG_LEVEL || 'info', dir: process.env.LOG_DIR || './logs' }, - + rpc: { timeout: parseInt(process.env.RPC_TIMEOUT) || 1500000, // 25 minutes default maxConcurrentEvaluations: parseInt(process.env.MAX_CONCURRENT_EVALUATIONS) || 10 + }, + + security: { + authSecretKey: process.env.AUTH_SECRET_KEY + }, + + clients: { + dir: process.env.CLIENTS_DIR || './clients' + }, + + evals: { + dir: process.env.EVALS_DIR || './evals' } }; diff --git a/eval-server/nodejs/src/lib/EvalServer.js b/eval-server/nodejs/src/lib/EvalServer.js index 5471720bc57..07a2d6f03d0 100644 --- a/eval-server/nodejs/src/lib/EvalServer.js +++ b/eval-server/nodejs/src/lib/EvalServer.js @@ -265,6 +265,12 @@ export class EvalServer extends EventEmitter { return; } + // Handle RPC requests from client to server + if (data.jsonrpc === '2.0' && data.method && data.id) { + await this.handleRpcRequest(connection, data); + return; + } + // Handle other message types switch (data.type) { case 'register': @@ -313,6 +319,129 @@ export class EvalServer extends EventEmitter { } } + /** + * Handle RPC requests from client to server + */ + async handleRpcRequest(connection, request) { + try { + const { method, params, id } = request; + + logger.info('Received RPC request', { + connectionId: connection.id, + clientId: connection.clientId, + method, + requestId: id + }); + + let result = null; + + switch (method) { + case 'configure_llm': + result = await this.handleConfigureLLM(connection, params); + break; + default: + throw new Error(`Unknown method: ${method}`); + } + + // Send success response + this.sendMessage(connection.ws, { + jsonrpc: '2.0', + result, + id + }); + + } catch (error) { + logger.error('RPC request failed', { + connectionId: connection.id, + clientId: connection.clientId, + method: request.method, + requestId: request.id, + error: error.message + }); + + // Send error response + this.sendMessage(connection.ws, { + jsonrpc: '2.0', + error: { + code: -32603, // Internal error + message: error.message + }, + id: request.id + }); + } + } + + /** + * Handle configure_llm RPC method + */ + async handleConfigureLLM(connection, params) { + if (!connection.registered) { + throw new Error('Client must be registered before configuring LLM'); + } + + const { provider, apiKey, endpoint, models, partial = false } = params; + + // Validate provider + const supportedProviders = ['openai', 'litellm', 'groq', 'openrouter']; + if (!supportedProviders.includes(provider)) { + throw new Error(`Unsupported provider: ${provider}. Supported providers: ${supportedProviders.join(', ')}`); + } + + // Validate models + if (!models || !models.main) { + throw new Error('Main model is required'); + } + + // Store configuration for this client connection + if (!connection.llmConfig) { + connection.llmConfig = {}; + } + + // Apply configuration (full or partial update) + if (partial && connection.llmConfig) { + // Partial update - merge with existing config + connection.llmConfig = { + ...connection.llmConfig, + provider: provider || connection.llmConfig.provider, + apiKey: apiKey || connection.llmConfig.apiKey, + endpoint: endpoint || connection.llmConfig.endpoint, + models: { + ...connection.llmConfig.models, + ...models + } + }; + } else { + // Full update - replace entire config + connection.llmConfig = { + provider, + apiKey: apiKey || CONFIG.providers[provider]?.apiKey, + endpoint: endpoint || CONFIG.providers[provider]?.endpoint, + models: { + main: models.main, + mini: models.mini || models.main, + nano: models.nano || models.mini || models.main + } + }; + } + + logger.info('LLM configuration updated', { + clientId: connection.clientId, + provider: connection.llmConfig.provider, + models: connection.llmConfig.models, + hasApiKey: !!connection.llmConfig.apiKey, + hasEndpoint: !!connection.llmConfig.endpoint + }); + + return { + status: 'success', + message: 'LLM configuration updated successfully', + appliedConfig: { + provider: connection.llmConfig.provider, + models: connection.llmConfig.models + } + }; + } + /** * Handle client registration */ @@ -562,6 +691,35 @@ export class EvalServer extends EventEmitter { 'running' ); + // Prepare model configuration - use client config if available, otherwise evaluation config, otherwise defaults + let modelConfig = evaluation.model || {}; + + if (connection.llmConfig) { + // New nested format: separate config objects for each model tier + modelConfig = { + main_model: { + provider: connection.llmConfig.provider, + model: connection.llmConfig.models.main, + api_key: connection.llmConfig.apiKey, + endpoint: connection.llmConfig.endpoint + }, + mini_model: { + provider: connection.llmConfig.provider, + model: connection.llmConfig.models.mini, + api_key: connection.llmConfig.apiKey, + endpoint: connection.llmConfig.endpoint + }, + nano_model: { + provider: connection.llmConfig.provider, + model: connection.llmConfig.models.nano, + api_key: connection.llmConfig.apiKey, + endpoint: connection.llmConfig.endpoint + }, + // Include any evaluation-specific overrides + ...modelConfig + }; + } + // Prepare RPC request const rpcRequest = { jsonrpc: '2.0', @@ -572,7 +730,7 @@ export class EvalServer extends EventEmitter { url: evaluation.target?.url || evaluation.url, tool: evaluation.tool, input: evaluation.input, - model: evaluation.model, + model: modelConfig, timeout: evaluation.timeout || 30000, metadata: { tags: evaluation.metadata?.tags || [], diff --git a/front_end/panels/ai_chat/core/AgentNodes.ts b/front_end/panels/ai_chat/core/AgentNodes.ts index 34f806fc55d..a0f3efb1d54 100644 --- a/front_end/panels/ai_chat/core/AgentNodes.ts +++ b/front_end/panels/ai_chat/core/AgentNodes.ts @@ -15,6 +15,7 @@ import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; import { createLogger } from './Logger.js'; import type { AgentState } from './State.js'; import type { Runnable } from './Types.js'; +import { LLMConfigurationManager } from './LLMConfigurationManager.js'; import { AgentErrorHandler } from './AgentErrorHandler.js'; import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js'; import * as ToolNameMap from './ToolNameMap.js'; @@ -590,13 +591,17 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, const result = await withTracingContext(executionContext, async () => { console.log(`[TOOL EXECUTION PATH 1] Inside withTracingContext for tool: ${toolName}`); - const apiKeyFromState = (state.context as any)?.apiKey; - return await selectedTool.execute(toolArgs as any, { - apiKey: apiKeyFromState, - provider: this.provider, - model: this.modelName, - miniModel: this.miniModel, - nanoModel: this.nanoModel + + // Get configuration from manager (supports overrides) + const configManager = LLMConfigurationManager.getInstance(); + const config = configManager.getConfiguration(); + + return await selectedTool.execute(toolArgs as any, { + apiKey: config.apiKey, + provider: config.provider, + model: config.mainModel, + miniModel: config.miniModel, + nanoModel: config.nanoModel }); }); console.log(`[TOOL EXECUTION PATH 1] ToolExecutorNode completed tool: ${toolName}`); diff --git a/front_end/panels/ai_chat/core/AgentService.ts b/front_end/panels/ai_chat/core/AgentService.ts index cc742e7d013..ca176b381ed 100644 --- a/front_end/panels/ai_chat/core/AgentService.ts +++ b/front_end/panels/ai_chat/core/AgentService.ts @@ -13,6 +13,7 @@ import { createLogger } from './Logger.js'; import {type AgentState, createInitialState, createUserMessage} from './State.js'; import type {CompiledGraph} from './Types.js'; import { LLMClient } from '../LLM/LLMClient.js'; +import { LLMConfigurationManager } from './LLMConfigurationManager.js'; import { createTracingProvider, getCurrentTracingContext } from '../tracing/TracingConfig.js'; import type { TracingProvider, TracingContext } from '../tracing/TracingProvider.js'; import { AgentRunnerEventBus } from '../agent_framework/AgentRunnerEventBus.js'; @@ -55,10 +56,14 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ #tracingProvider!: TracingProvider; #sessionId: string; #activeAgentSessions = new Map(); + #configManager: LLMConfigurationManager; constructor() { super(); - + + // Initialize configuration manager + this.#configManager = LLMConfigurationManager.getInstance(); + // Initialize tracing this.#sessionId = this.generateSessionId(); this.#initializeTracing(); @@ -77,6 +82,9 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ // Subscribe to AgentRunner events AgentRunnerEventBus.getInstance().addEventListener('agent-progress', this.#handleAgentProgress.bind(this)); + + // Subscribe to configuration changes + this.#configManager.addChangeListener(this.#handleConfigurationChange.bind(this)); } /** @@ -101,59 +109,62 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ */ async #initializeLLMClient(): Promise { const llm = LLMClient.getInstance(); - - // Get configuration from localStorage - const provider = localStorage.getItem('ai_chat_provider') || 'openai'; - const openaiKey = localStorage.getItem('ai_chat_api_key') || ''; - const litellmKey = localStorage.getItem('ai_chat_litellm_api_key') || ''; - const litellmEndpoint = localStorage.getItem('ai_chat_litellm_endpoint') || ''; - const groqKey = localStorage.getItem('ai_chat_groq_api_key') || ''; - const openrouterKey = localStorage.getItem('ai_chat_openrouter_api_key') || ''; - + + // Get configuration from manager (with override support) + const config = this.#configManager.getConfiguration(); + const provider = config.provider; + const apiKey = config.apiKey; + const endpoint = config.endpoint; + const providers = []; - - // Only add the selected provider - if (provider === 'openai' && openaiKey) { - providers.push({ - provider: 'openai' as const, - apiKey: openaiKey - }); - } - - if (provider === 'litellm' && litellmEndpoint) { - providers.push({ - provider: 'litellm' as const, - apiKey: litellmKey, // Can be empty for some LiteLLM endpoints - providerURL: litellmEndpoint - }); - } - - if (provider === 'groq' && groqKey) { - providers.push({ - provider: 'groq' as const, - apiKey: groqKey - }); + + // Validate and add the selected provider + const validation = this.#configManager.validateConfiguration(); + if (!validation.isValid) { + throw new Error(`Configuration validation failed: ${validation.errors.join(', ')}`); } - - if (provider === 'openrouter' && openrouterKey) { - providers.push({ - provider: 'openrouter' as const, - apiKey: openrouterKey - }); + + // Only add the selected provider if it has valid configuration + switch (provider) { + case 'openai': + if (apiKey) { + providers.push({ + provider: 'openai' as const, + apiKey + }); + } + break; + case 'litellm': + if (endpoint) { + providers.push({ + provider: 'litellm' as const, + apiKey, // Can be empty for some LiteLLM endpoints + providerURL: endpoint + }); + } + break; + case 'groq': + if (apiKey) { + providers.push({ + provider: 'groq' as const, + apiKey + }); + } + break; + case 'openrouter': + if (apiKey) { + providers.push({ + provider: 'openrouter' as const, + apiKey + }); + } + break; } - + if (providers.length === 0) { - let errorMessage = 'OpenAI API key is required for this configuration'; - if (provider === 'litellm') { - errorMessage = 'LiteLLM endpoint is required for this configuration'; - } else if (provider === 'groq') { - errorMessage = 'Groq API key is required for this configuration'; - } else if (provider === 'openrouter') { - errorMessage = 'OpenRouter API key is required for this configuration'; - } - throw new Error(errorMessage); + throw new Error(`No valid configuration found for provider ${provider}`); } - + await llm.initialize({ providers }); logger.info('LLM client initialized successfully', { selectedProvider: provider, @@ -177,7 +188,7 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ // If API key is required but not provided, throw error if (requiresApiKey && !apiKey) { - const provider = localStorage.getItem('ai_chat_provider') || 'openai'; + const provider = this.#configManager.getProvider(); let providerName = 'OpenAI'; if (provider === 'litellm') { providerName = 'LiteLLM'; @@ -189,13 +200,13 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ throw new Error(`${providerName} API key is required for this configuration`); } - // Determine selected provider for primary graph execution - const selectedProvider = (localStorage.getItem('ai_chat_provider') || 'openai') as LLMProvider; + // Get provider from configuration manager + const config = this.#configManager.getConfiguration(); // Mini and nano models are injected by caller (validated upstream) // Will throw error if model/provider configuration is invalid - this.#graph = createAgentGraph(apiKey, modelName, selectedProvider, miniModel, nanoModel); + this.#graph = createAgentGraph(apiKey, modelName, config.provider, miniModel, nanoModel); // Stash apiKey in state context for downstream tools that need it if (!this.#state.context) { (this.#state as any).context = {}; } @@ -275,6 +286,43 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ await this.#initializeTracing(); } + /** + * Handle configuration changes from LLMConfigurationManager + */ + #handleConfigurationChange(): void { + logger.info('LLM configuration changed, reinitializing if needed'); + + // If we're initialized, we need to reinitialize with new configuration + if (this.#isInitialized) { + // Mark as uninitialized to force reinit on next use + this.#isInitialized = false; + this.#graph = undefined; + + logger.info('Marked agent service for reinitialization due to config change'); + } + } + + /** + * Public method to refresh credentials and agent service + * Can be called from settings dialog or other components + */ + async refreshCredentials(): Promise { + logger.info('Refreshing credentials and reinitializing agent service'); + + this.#isInitialized = false; + this.#graph = undefined; + + // Force reinitialization on next use + try { + const config = this.#configManager.getConfiguration(); + await this.initialize(config.apiKey, config.mainModel, config.miniModel, config.nanoModel); + logger.info('Agent service reinitialized successfully'); + } catch (error) { + logger.error('Failed to reinitialize agent service:', error); + throw error; + } + } + /** * Sends a message to the AI agent */ @@ -589,7 +637,7 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ #doesCurrentConfigRequireApiKey(): boolean { try { // Check the selected provider - const selectedProvider = localStorage.getItem('ai_chat_provider') || 'openai'; + const selectedProvider = this.#configManager.getProvider(); // OpenAI provider always requires an API key if (selectedProvider === 'openai') { diff --git a/front_end/panels/ai_chat/core/LLMConfigurationManager.ts b/front_end/panels/ai_chat/core/LLMConfigurationManager.ts new file mode 100644 index 00000000000..2937c29b521 --- /dev/null +++ b/front_end/panels/ai_chat/core/LLMConfigurationManager.ts @@ -0,0 +1,368 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { createLogger } from './Logger.js'; +import type { LLMProvider } from '../LLM/LLMTypes.js'; + +const logger = createLogger('LLMConfigurationManager'); + +/** + * Configuration interface for LLM settings + */ +export interface LLMConfig { + provider: LLMProvider; + apiKey?: string; + endpoint?: string; // For LiteLLM + mainModel: string; + miniModel?: string; + nanoModel?: string; +} + +/** + * Local storage keys for LLM configuration + */ +const STORAGE_KEYS = { + PROVIDER: 'ai_chat_provider', + MODEL_SELECTION: 'ai_chat_model_selection', + MINI_MODEL: 'ai_chat_mini_model', + NANO_MODEL: 'ai_chat_nano_model', + OPENAI_API_KEY: 'ai_chat_api_key', + LITELLM_ENDPOINT: 'ai_chat_litellm_endpoint', + LITELLM_API_KEY: 'ai_chat_litellm_api_key', + GROQ_API_KEY: 'ai_chat_groq_api_key', + OPENROUTER_API_KEY: 'ai_chat_openrouter_api_key', +} as const; + +/** + * Centralized LLM configuration manager with override capabilities. + * Supports both manual mode (localStorage-based) and automated mode (override-based). + */ +export class LLMConfigurationManager { + private static instance: LLMConfigurationManager; + private overrideConfig?: Partial; // Override for automated mode + private changeListeners: Array<() => void> = []; + + private constructor() { + // Listen for localStorage changes from other tabs (manual mode) + window.addEventListener('storage', this.handleStorageChange.bind(this)); + } + + /** + * Get the singleton instance + */ + static getInstance(): LLMConfigurationManager { + if (!LLMConfigurationManager.instance) { + LLMConfigurationManager.instance = new LLMConfigurationManager(); + } + return LLMConfigurationManager.instance; + } + + /** + * Get the current provider with override fallback + */ + getProvider(): LLMProvider { + if (this.overrideConfig?.provider) { + return this.overrideConfig.provider; + } + const stored = localStorage.getItem(STORAGE_KEYS.PROVIDER); + return (stored as LLMProvider) || 'openai'; + } + + /** + * Get the main model with override fallback + */ + getMainModel(): string { + if (this.overrideConfig?.mainModel) { + return this.overrideConfig.mainModel; + } + return localStorage.getItem(STORAGE_KEYS.MODEL_SELECTION) || ''; + } + + /** + * Get the mini model with override fallback + */ + getMiniModel(): string { + if (this.overrideConfig?.miniModel) { + return this.overrideConfig.miniModel; + } + return localStorage.getItem(STORAGE_KEYS.MINI_MODEL) || ''; + } + + /** + * Get the nano model with override fallback + */ + getNanoModel(): string { + if (this.overrideConfig?.nanoModel) { + return this.overrideConfig.nanoModel; + } + return localStorage.getItem(STORAGE_KEYS.NANO_MODEL) || ''; + } + + /** + * Get the API key for the current provider with override fallback + */ + getApiKey(): string { + if (this.overrideConfig?.apiKey) { + return this.overrideConfig.apiKey; + } + + const provider = this.getProvider(); + switch (provider) { + case 'openai': + return localStorage.getItem(STORAGE_KEYS.OPENAI_API_KEY) || ''; + case 'litellm': + return localStorage.getItem(STORAGE_KEYS.LITELLM_API_KEY) || ''; + case 'groq': + return localStorage.getItem(STORAGE_KEYS.GROQ_API_KEY) || ''; + case 'openrouter': + return localStorage.getItem(STORAGE_KEYS.OPENROUTER_API_KEY) || ''; + default: + return ''; + } + } + + /** + * Get the endpoint (primarily for LiteLLM) with override fallback + */ + getEndpoint(): string | undefined { + if (this.overrideConfig?.endpoint) { + return this.overrideConfig.endpoint; + } + + const provider = this.getProvider(); + if (provider === 'litellm') { + return localStorage.getItem(STORAGE_KEYS.LITELLM_ENDPOINT) || undefined; + } + return undefined; + } + + /** + * Get the complete current configuration + */ + getConfiguration(): LLMConfig { + return { + provider: this.getProvider(), + apiKey: this.getApiKey(), + endpoint: this.getEndpoint(), + mainModel: this.getMainModel(), + miniModel: this.getMiniModel(), + nanoModel: this.getNanoModel(), + }; + } + + /** + * Set override configuration (for automated mode per-request overrides) + */ + setOverride(config: Partial): void { + logger.info('Setting configuration override', { + provider: config.provider, + mainModel: config.mainModel, + hasApiKey: !!config.apiKey, + hasEndpoint: !!config.endpoint + }); + + this.overrideConfig = { ...config }; + this.notifyListeners(); + } + + /** + * Clear override configuration + */ + clearOverride(): void { + if (this.overrideConfig) { + logger.info('Clearing configuration override'); + this.overrideConfig = undefined; + this.notifyListeners(); + } + } + + /** + * Check if override is currently active + */ + hasOverride(): boolean { + return !!this.overrideConfig; + } + + /** + * Save configuration to localStorage (for manual mode and persistent automated mode) + */ + saveConfiguration(config: LLMConfig): void { + logger.info('Saving configuration to localStorage', { + provider: config.provider, + mainModel: config.mainModel, + hasApiKey: !!config.apiKey, + hasEndpoint: !!config.endpoint + }); + + // Save provider + localStorage.setItem(STORAGE_KEYS.PROVIDER, config.provider); + + // Save models + localStorage.setItem(STORAGE_KEYS.MODEL_SELECTION, config.mainModel); + if (config.miniModel) { + localStorage.setItem(STORAGE_KEYS.MINI_MODEL, config.miniModel); + } else { + localStorage.removeItem(STORAGE_KEYS.MINI_MODEL); + } + if (config.nanoModel) { + localStorage.setItem(STORAGE_KEYS.NANO_MODEL, config.nanoModel); + } else { + localStorage.removeItem(STORAGE_KEYS.NANO_MODEL); + } + + // Save provider-specific settings + this.saveProviderSpecificSettings(config); + + // Notify listeners of configuration change + this.notifyListeners(); + } + + /** + * Load configuration from localStorage + */ + loadConfiguration(): LLMConfig { + return { + provider: this.getProvider(), + apiKey: this.getApiKey(), + endpoint: this.getEndpoint(), + mainModel: this.getMainModel(), + miniModel: this.getMiniModel(), + nanoModel: this.getNanoModel(), + }; + } + + /** + * Add a listener for configuration changes + */ + addChangeListener(listener: () => void): void { + this.changeListeners.push(listener); + } + + /** + * Remove a configuration change listener + */ + removeChangeListener(listener: () => void): void { + const index = this.changeListeners.indexOf(listener); + if (index !== -1) { + this.changeListeners.splice(index, 1); + } + } + + /** + * Validate the current configuration + */ + validateConfiguration(): { isValid: boolean; errors: string[] } { + const config = this.getConfiguration(); + const errors: string[] = []; + + // Check provider + if (!config.provider) { + errors.push('Provider is required'); + } + + // Check main model + if (!config.mainModel) { + errors.push('Main model is required'); + } + + // Provider-specific validation + switch (config.provider) { + case 'openai': + case 'groq': + case 'openrouter': + if (!config.apiKey) { + errors.push(`API key is required for ${config.provider}`); + } + break; + case 'litellm': + if (!config.endpoint) { + errors.push('Endpoint is required for LiteLLM'); + } + break; + } + + return { + isValid: errors.length === 0, + errors + }; + } + + /** + * Save provider-specific settings to localStorage + */ + private saveProviderSpecificSettings(config: LLMConfig): void { + // Clear all provider-specific keys first + localStorage.removeItem(STORAGE_KEYS.OPENAI_API_KEY); + localStorage.removeItem(STORAGE_KEYS.LITELLM_API_KEY); + localStorage.removeItem(STORAGE_KEYS.LITELLM_ENDPOINT); + localStorage.removeItem(STORAGE_KEYS.GROQ_API_KEY); + localStorage.removeItem(STORAGE_KEYS.OPENROUTER_API_KEY); + + // Save current provider's settings + switch (config.provider) { + case 'openai': + if (config.apiKey) { + localStorage.setItem(STORAGE_KEYS.OPENAI_API_KEY, config.apiKey); + } + break; + case 'litellm': + if (config.endpoint) { + localStorage.setItem(STORAGE_KEYS.LITELLM_ENDPOINT, config.endpoint); + } + if (config.apiKey) { + localStorage.setItem(STORAGE_KEYS.LITELLM_API_KEY, config.apiKey); + } + break; + case 'groq': + if (config.apiKey) { + localStorage.setItem(STORAGE_KEYS.GROQ_API_KEY, config.apiKey); + } + break; + case 'openrouter': + if (config.apiKey) { + localStorage.setItem(STORAGE_KEYS.OPENROUTER_API_KEY, config.apiKey); + } + break; + } + } + + /** + * Handle localStorage changes from other tabs + */ + private handleStorageChange(event: StorageEvent): void { + if (event.key && Object.values(STORAGE_KEYS).includes(event.key as any)) { + logger.debug('Configuration changed in another tab', { + key: event.key, + newValue: event.newValue + }); + this.notifyListeners(); + } + } + + /** + * Notify all listeners of configuration changes + */ + private notifyListeners(): void { + this.changeListeners.forEach(listener => { + try { + listener(); + } catch (error) { + logger.error('Error in configuration change listener:', error); + } + }); + } + + /** + * Get debug information about current configuration state + */ + getDebugInfo(): Record { + return { + hasOverride: this.hasOverride(), + overrideConfig: this.overrideConfig, + currentConfig: this.getConfiguration(), + validation: this.validateConfiguration(), + listenerCount: this.changeListeners.length, + }; + } +} \ No newline at end of file diff --git a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts index d13e487b910..c75013d5da3 100644 --- a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts +++ b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts @@ -4,6 +4,7 @@ import { BUILD_CONFIG } from '../../core/BuildConfig.js'; import { WebSocketRPCClient } from '../../common/WebSocketRPCClient.js'; +import { LLMConfigurationManager } from '../../core/LLMConfigurationManager.js'; import { getEvaluationConfig, getEvaluationClientId } from '../../common/EvaluationConfig.js'; import { ToolRegistry } from '../../agent_framework/ConfigurableAgentTool.js'; import { AgentService } from '../../core/AgentService.js'; @@ -21,17 +22,21 @@ import { EvaluationRequest, EvaluationSuccessResponse, EvaluationErrorResponse, + LLMConfigurationRequest, + LLMConfigurationResponse, ErrorCodes, isWelcomeMessage, isRegistrationAckMessage, isEvaluationRequest, + isLLMConfigurationRequest, isPongMessage, createRegisterMessage, createReadyMessage, createAuthVerifyMessage, createStatusMessage, createSuccessResponse, - createErrorResponse + createErrorResponse, + createLLMConfigurationResponse } from './EvaluationProtocol.js'; const logger = createLogger('EvaluationAgent'); @@ -183,6 +188,9 @@ export class EvaluationAgent { else if (isEvaluationRequest(message)) { await this.handleEvaluationRequest(message); } + else if (isLLMConfigurationRequest(message)) { + await this.handleLLMConfigurationRequest(message); + } else if (isPongMessage(message)) { logger.debug('Received pong'); } @@ -716,22 +724,46 @@ export class EvaluationAgent { let chatObservationId: string | undefined; try { + // Get configuration manager for override support + const configManager = LLMConfigurationManager.getInstance(); + + // Set override configuration if provided in input + if (input.provider || input.main_model || input.api_key) { + logger.info('Setting configuration override for chat evaluation', { + provider: input.provider, + mainModel: input.main_model, + hasApiKey: !!input.api_key + }); + + configManager.setOverride({ + provider: input.provider, + mainModel: input.main_model, + miniModel: input.mini_model, + nanoModel: input.nano_model, + apiKey: input.api_key, + endpoint: input.endpoint + }); + } + // Get or create AgentService instance const agentService = AgentService.getInstance(); - - // Use explicit models from constructor - const modelName = this.judgeModel; - const miniModel = this.miniModel; - const nanoModel = this.nanoModel; - + + // Use override configuration if set, otherwise use constructor models + const config = configManager.getConfiguration(); + const modelName = config.mainModel || this.judgeModel; + const miniModel = config.miniModel || this.miniModel; + const nanoModel = config.nanoModel || this.nanoModel; + const apiKey = config.apiKey || agentService.getApiKey(); + logger.info('Initializing AgentService for chat evaluation', { modelName, - hasApiKey: !!agentService.getApiKey(), - isInitialized: agentService.isInitialized() + hasApiKey: !!apiKey, + isInitialized: agentService.isInitialized(), + hasOverride: configManager.hasOverride() }); - - // Always reinitialize with the current model and explicit mini/nano - await agentService.initialize(agentService.getApiKey(), modelName, miniModel, nanoModel); + + // Always reinitialize with the current configuration + await agentService.initialize(apiKey, modelName, miniModel, nanoModel); // Create a child observation for the chat execution if (tracingContext) { @@ -799,7 +831,7 @@ export class EvaluationAgent { }); resolve(result); - + } catch (error) { clearTimeout(timer); @@ -814,10 +846,87 @@ export class EvaluationAgent { logger.warn('Failed to update chat execution observation with error:', updateError); } } - + logger.error('Chat evaluation failed:', error); reject(error); + } finally { + // Clear any configuration override + const configManager = LLMConfigurationManager.getInstance(); + configManager.clearOverride(); } }); } + + /** + * Handle LLM configuration requests for persistent configuration + */ + private async handleLLMConfigurationRequest(request: LLMConfigurationRequest): Promise { + const { params, id } = request; + + logger.info('Received LLM configuration request', { + provider: params.provider, + hasApiKey: !!params.apiKey, + models: params.models, + partial: params.partial + }); + + try { + // Get configuration manager + const configManager = LLMConfigurationManager.getInstance(); + + // Save configuration to localStorage (persistent mode) + configManager.saveConfiguration({ + provider: params.provider, + apiKey: params.apiKey, + endpoint: params.endpoint, + mainModel: params.models.main, + miniModel: params.models.mini, + nanoModel: params.models.nano + }); + + // Reinitialize AgentService with new configuration + const agentService = AgentService.getInstance(); + await agentService.refreshCredentials(); + + // Prepare response with applied configuration + const appliedConfig = { + provider: params.provider, + models: { + main: params.models.main, + mini: params.models.mini || '', + nano: params.models.nano || '' + } + }; + + // Send success response + const response = createLLMConfigurationResponse(id, appliedConfig); + + if (this.client) { + this.client.send(response); + } + + logger.info('LLM configuration applied successfully', { + provider: params.provider, + mainModel: params.models.main + }); + + } catch (error) { + logger.error('Failed to apply LLM configuration:', error); + + // Send error response + const errorResponse = createErrorResponse( + id, + ErrorCodes.INTERNAL_ERROR, + 'Failed to apply LLM configuration', + { + error: error instanceof Error ? error.message : String(error), + timestamp: new Date().toISOString() + } + ); + + if (this.client) { + this.client.send(errorResponse); + } + } + } } diff --git a/front_end/panels/ai_chat/evaluation/remote/EvaluationProtocol.ts b/front_end/panels/ai_chat/evaluation/remote/EvaluationProtocol.ts index c9d4c8e9acb..f0efc557b9c 100644 --- a/front_end/panels/ai_chat/evaluation/remote/EvaluationProtocol.ts +++ b/front_end/panels/ai_chat/evaluation/remote/EvaluationProtocol.ts @@ -90,6 +90,8 @@ export interface EvaluationParams { mini_model?: string; nano_model?: string; provider?: string; + api_key?: string; // New: per-request API key + endpoint?: string; // New: per-request endpoint (LiteLLM) }; timeout: number; metadata: { @@ -250,4 +252,83 @@ export function createErrorResponse( }, id }; +} + +// LLM Configuration JSON-RPC Messages + +export interface LLMConfigurationRequest { + jsonrpc: '2.0'; + method: 'configure_llm'; + params: LLMConfigurationParams; + id: string; +} + +export interface LLMConfigurationParams { + provider: 'openai' | 'litellm' | 'groq' | 'openrouter'; + apiKey?: string; + endpoint?: string; // For LiteLLM + models: { + main: string; + mini?: string; + nano?: string; + }; + // Optional: only update specific fields + partial?: boolean; +} + +export interface LLMConfigurationResponse { + jsonrpc: '2.0'; + result: { + status: 'success'; + message: string; + appliedConfig: { + provider: string; + models: { + main: string; + mini: string; + nano: string; + }; + }; + }; + id: string; +} + +// Type guard for LLM configuration +export function isLLMConfigurationRequest(msg: any): msg is LLMConfigurationRequest { + return msg?.jsonrpc === '2.0' && msg?.method === 'configure_llm'; +} + +// Helper function for LLM configuration +export function createLLMConfigurationRequest( + id: string, + params: LLMConfigurationParams +): LLMConfigurationRequest { + return { + jsonrpc: '2.0', + method: 'configure_llm', + params, + id + }; +} + +export function createLLMConfigurationResponse( + id: string, + appliedConfig: { + provider: string; + models: { + main: string; + mini: string; + nano: string; + }; + } +): LLMConfigurationResponse { + return { + jsonrpc: '2.0', + result: { + status: 'success', + message: 'LLM configuration updated successfully', + appliedConfig + }, + id + }; } \ No newline at end of file diff --git a/front_end/panels/ai_chat/ui/AIChatPanel.ts b/front_end/panels/ai_chat/ui/AIChatPanel.ts index 8f5d0b91982..c7efebad69b 100644 --- a/front_end/panels/ai_chat/ui/AIChatPanel.ts +++ b/front_end/panels/ai_chat/ui/AIChatPanel.ts @@ -12,6 +12,7 @@ import * as Lit from '../../../ui/lit/lit.js'; import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js'; import {AgentService, Events as AgentEvents} from '../core/AgentService.js'; import { LLMClient } from '../LLM/LLMClient.js'; +import { LLMConfigurationManager } from '../core/LLMConfigurationManager.js'; import { LLMProviderRegistry } from '../LLM/LLMProviderRegistry.js'; import { OpenAIProvider } from '../LLM/OpenAIProvider.js'; import { LiteLLMProvider } from '../LLM/LiteLLMProvider.js'; @@ -318,27 +319,28 @@ export class AIChatPanel extends UI.Panel.Panel { } static getMiniModel(): string { - const instance = AIChatPanel.instance(); - - // Validate the model selection before returning - instance.#validateAndFixModelSelections(); - - return instance.#miniModel || instance.#selectedModel; + const configManager = LLMConfigurationManager.getInstance(); + const miniModel = configManager.getMiniModel(); + + // Fallback to main model if mini model not set + return miniModel || configManager.getMainModel(); } static getNanoModel(): string { - const instance = AIChatPanel.instance(); - - // Validate the model selection before returning - instance.#validateAndFixModelSelections(); - - return instance.#nanoModel || instance.#miniModel || instance.#selectedModel; + const configManager = LLMConfigurationManager.getInstance(); + const nanoModel = configManager.getNanoModel(); + const miniModel = configManager.getMiniModel(); + const mainModel = configManager.getMainModel(); + + // Fallback hierarchy: nano -> mini -> main + return nanoModel || miniModel || mainModel; } static getNanoModelWithProvider(): { model: string, provider: 'openai' | 'litellm' | 'groq' | 'openrouter' } { + const configManager = LLMConfigurationManager.getInstance(); const modelName = AIChatPanel.getNanoModel(); - const provider = AIChatPanel.getProviderForModel(modelName); - + const provider = configManager.getProvider(); + return { model: modelName, provider: provider @@ -346,9 +348,10 @@ export class AIChatPanel extends UI.Panel.Panel { } static getMiniModelWithProvider(): { model: string, provider: 'openai' | 'litellm' | 'groq' | 'openrouter' } { + const configManager = LLMConfigurationManager.getInstance(); const modelName = AIChatPanel.getMiniModel(); - const provider = AIChatPanel.getProviderForModel(modelName); - + const provider = configManager.getProvider(); + return { model: modelName, provider: provider @@ -721,6 +724,7 @@ export class AIChatPanel extends UI.Panel.Panel { #apiKey: string | null = null; // Regular API key #evaluationAgent: EvaluationAgent | null = null; // Evaluation agent for this tab #mcpUnsubscribe: (() => void) | null = null; + #configManager: LLMConfigurationManager; // Store bound event listeners to properly add/remove without duplications #boundOnMessagesChanged?: (e: Common.EventTarget.EventTargetEvent) => void; @@ -733,6 +737,9 @@ export class AIChatPanel extends UI.Panel.Panel { constructor() { super(AIChatPanel.panelName); + // Initialize configuration manager + this.#configManager = LLMConfigurationManager.getInstance(); + // Initialize storage monitoring for debugging StorageMonitor.getInstance(); @@ -1098,6 +1105,23 @@ export class AIChatPanel extends UI.Panel.Panel { return this.#selectedModel; } + /** + * Set LLM configuration programmatically (for manual mode and persistent automated mode) + */ + setLLMConfiguration(config: import('../core/LLMConfigurationManager.js').LLMConfig): void { + logger.info('Setting LLM configuration programmatically', { + provider: config.provider, + mainModel: config.mainModel, + hasApiKey: !!config.apiKey + }); + + // Save configuration to localStorage + this.#configManager.saveConfiguration(config); + + // Refresh the agent service with new configuration + this.refreshCredentials(); + } + /** * Public method to refresh credential validation and agent service * Can be called from settings dialog or other components From 700d1a9ea25559bc939cff131e8d13eea4d9b7cd Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Tue, 16 Sep 2025 13:08:23 -0500 Subject: [PATCH 03/12] Added plan --- MODEL-CONFIGS.md | 450 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 MODEL-CONFIGS.md diff --git a/MODEL-CONFIGS.md b/MODEL-CONFIGS.md new file mode 100644 index 00000000000..02eb8ebdaa7 --- /dev/null +++ b/MODEL-CONFIGS.md @@ -0,0 +1,450 @@ +# LLM Model Configuration Architecture + +## Overview + +This document outlines the comprehensive architecture for programmatic LLM model provider and configuration management in the browser-operator-core AI chat system. The system supports both manual user configuration and automated evaluation configuration with per-request overrides and persistent configuration updates. + +## Current System Analysis + +### Provider Support +- **4 LLM Providers**: LiteLLM, OpenAI, Groq, OpenRouter +- **3-Tier Model Selection**: + - **Main Model**: Primary model for agent execution + - **Mini Model**: Smaller/faster model for lightweight operations + - **Nano Model**: Smallest/fastest model for simple tasks + +### Current Configuration Flow +1. **Provider Selection** (localStorage: `ai_chat_provider`) +2. **Model Selection** (localStorage: `ai_chat_model_selection`, `ai_chat_mini_model`, `ai_chat_nano_model`) +3. **API Configuration** (localStorage: provider-specific API keys and endpoints) +4. **Context Propagation**: Configuration flows through AgentService โ†’ Graph โ†’ ConfigurableAgentTool โ†’ AgentRunner +5. **Model Usage**: Models selected via `AIChatPanel.getMiniModel()`, `AIChatPanel.getNanoModel()` static methods + +### Current localStorage Keys +- `ai_chat_provider`: Selected provider ('openai', 'litellm', 'groq', 'openrouter') +- `ai_chat_model_selection`: Main model name +- `ai_chat_mini_model`: Mini model name +- `ai_chat_nano_model`: Nano model name +- `ai_chat_api_key`: OpenAI API key +- `ai_chat_litellm_endpoint`: LiteLLM server endpoint +- `ai_chat_litellm_api_key`: LiteLLM API key +- `ai_chat_groq_api_key`: Groq API key +- `ai_chat_openrouter_api_key`: OpenRouter API key + +## Proposed Architecture: LLMConfigurationManager + +### Core Design Principles +- **Single Source of Truth**: Centralized configuration management +- **Override System**: Automated mode can override without affecting localStorage +- **Persistent Configuration**: New eval API to set localStorage values programmatically +- **Backwards Compatible**: Existing localStorage-based code continues to work +- **Tab Behavior**: + - **Manual Mode**: Shared configuration across tabs via localStorage + - **Automated Mode**: Per-tab override capability for request multiplexing + +### Two Configuration Modes + +#### 1. Manual Mode +- User configures through Settings UI +- Configuration persisted to localStorage +- Changes shared across all tabs +- Traditional user experience maintained + +#### 2. Automated Mode +Two sub-modes available: + +**Per-Request Override (Temporary)** +- Configuration override for individual evaluation requests +- No localStorage changes +- Each tab can have different overrides simultaneously +- Clean up after request completion + +**Persistent Configuration (New)** +- Programmatic equivalent of Settings UI +- Updates localStorage values +- Changes apply to all tabs +- Useful for evaluation setup/teardown + +## Implementation + +### 1. LLMConfigurationManager Singleton + +```typescript +interface LLMConfig { + provider: 'openai' | 'litellm' | 'groq' | 'openrouter'; + apiKey?: string; + endpoint?: string; // For LiteLLM + mainModel: string; + miniModel?: string; + nanoModel?: string; +} + +class LLMConfigurationManager { + private static instance: LLMConfigurationManager; + private overrideConfig?: Partial; // Override for automated mode + + static getInstance(): LLMConfigurationManager; + + // Configuration retrieval with override fallback + getProvider(): string; + getMainModel(): string; + getMiniModel(): string; + getNanoModel(): string; + getApiKey(): string; + getEndpoint(): string | undefined; + + // Override management (per-request automated mode) + setOverride(config: Partial): void; + clearOverride(): void; + + // Persistence (manual mode & persistent automated mode) + saveConfiguration(config: LLMConfig): void; + loadConfiguration(): LLMConfig; +} +``` + +#### Configuration Precedence +1. **Override configuration** (if set) - for per-request automated mode +2. **localStorage values** (fallback) - for manual mode and persistent automated mode + +### 2. Protocol Extension for Persistent Configuration + +#### Add to EvaluationProtocol.ts + +```typescript +// New JSON-RPC method for persistent LLM configuration +export interface LLMConfigurationRequest { + jsonrpc: '2.0'; + method: 'configure_llm'; + params: LLMConfigurationParams; + id: string; +} + +export interface LLMConfigurationParams { + provider: 'openai' | 'litellm' | 'groq' | 'openrouter'; + apiKey?: string; + endpoint?: string; // For LiteLLM + models: { + main: string; + mini?: string; + nano?: string; + }; + // Optional: only update specific fields + partial?: boolean; +} + +export interface LLMConfigurationResponse { + jsonrpc: '2.0'; + result: { + status: 'success'; + message: string; + appliedConfig: { + provider: string; + models: { + main: string; + mini: string; + nano: string; + }; + }; + }; + id: string; +} + +// Type guard +export function isLLMConfigurationRequest(msg: any): msg is LLMConfigurationRequest; + +// Helper function +export function createLLMConfigurationRequest( + id: string, + params: LLMConfigurationParams +): LLMConfigurationRequest; +``` + +#### Update EvaluationParams for Per-Request Override + +```typescript +export interface EvaluationParams { + evaluationId: string; + name: string; + url: string; + tool: string; + input: any; + model?: { + main_model?: string; + mini_model?: string; + nano_model?: string; + provider?: string; + api_key?: string; // New: per-request API key + endpoint?: string; // New: per-request endpoint (LiteLLM) + }; + timeout: number; + metadata: { + tags: string[]; + retries: number; + priority?: 'low' | 'normal' | 'high'; + }; +} +``` + +### 3. Integration Points + +#### Manual Mode (Settings UI) +- Settings dialog calls `LLMConfigurationManager.getInstance().saveConfiguration()` +- All components read through manager methods +- Changes propagate across tabs via storage events +- User expectations: changes in one tab affect all tabs + +#### Automated Mode - Per-Request Override +```typescript +// In EvaluationAgent.executeChatEvaluation() +const configManager = LLMConfigurationManager.getInstance(); + +// Set override if provided in request +if (input.provider || input.main_model || input.api_key) { + configManager.setOverride({ + provider: input.provider, + mainModel: input.main_model, + miniModel: input.mini_model, + nanoModel: input.nano_model, + apiKey: input.api_key, + endpoint: input.endpoint + }); +} + +try { + // Execute with override + const result = await agentService.sendMessage(input.message); + return result; +} finally { + // Clean up override + configManager.clearOverride(); +} +``` + +#### Automated Mode - Persistent Configuration +```typescript +// In EvaluationAgent.handleLLMConfigurationRequest() +private async handleLLMConfigurationRequest(request: LLMConfigurationRequest): Promise { + const configManager = LLMConfigurationManager.getInstance(); + + // Save to localStorage (same as Settings UI) + configManager.saveConfiguration({ + provider: params.provider, + apiKey: params.apiKey, + endpoint: params.endpoint, + mainModel: params.models.main, + miniModel: params.models.mini, + nanoModel: params.models.nano + }); + + // Reinitialize AgentService with new configuration + const agentService = AgentService.getInstance(); + await agentService.refreshCredentials(); + + // Send success response +} +``` + +### 4. Component Updates Required + +1. **Replace localStorage access** in: + - `AgentService.ts` - Use manager for provider/key retrieval + - `AIChatPanel.ts` - Replace static model methods with manager calls + - `AgentNodes.ts` - Use manager for graph configuration + - `ConfigurableAgentTool.ts` - Use manager for CallCtx building + +2. **Add configuration refresh mechanisms**: + - AgentService reinitialize LLMClient when config changes + - Graph recreation with new model configuration + - Storage event listeners for cross-tab synchronization + +3. **Update EvaluationAgent**: + - Add `configure_llm` method handler + - Update per-request override logic + - Add configuration validation + +## Usage Examples + +### Per-Request Override (Temporary) +```typescript +// eval-server sends evaluation with custom config +{ + "jsonrpc": "2.0", + "method": "evaluate", + "params": { + "evaluationId": "eval-123", + "tool": "chat", + "input": { "message": "Hello" }, + "model": { + "provider": "openai", + "main_model": "gpt-4", + "api_key": "sk-temp-key" + } + }, + "id": "req-456" +} +``` + +### Persistent Configuration +```typescript +// eval-server sets persistent configuration +{ + "jsonrpc": "2.0", + "method": "configure_llm", + "params": { + "provider": "openai", + "apiKey": "sk-persistent-key", + "models": { + "main": "gpt-4", + "mini": "gpt-4-mini", + "nano": "gpt-3.5-turbo" + } + }, + "id": "config-789" +} +``` + +## Benefits + +### Manual Mode +- โœ… Maintains current UX - shared configuration across tabs +- โœ… Settings persist in localStorage +- โœ… No breaking changes for existing users + +### Automated Mode - Per-Request Override +- โœ… Per-request configuration without side effects +- โœ… Multiple evaluations with different configs in different tabs +- โœ… Clean separation from manual configuration +- โœ… Tab isolation for request multiplexing + +### Automated Mode - Persistent Configuration +- โœ… Programmatic equivalent of Settings UI +- โœ… Evaluation setup/teardown capabilities +- โœ… Cross-tab configuration updates +- โœ… Integration with existing localStorage system + +### Technical +- โœ… Single source of truth for all configuration +- โœ… Backwards compatible with existing code +- โœ… Simple mental model: override if present, localStorage otherwise +- โœ… No complex mode management needed +- โœ… Flexible evaluation server capabilities + +## Implementation Status + +### โœ… Phase 1: Core Infrastructure (COMPLETED) +1. โœ… **Create LLMConfigurationManager** singleton class - `/front_end/panels/ai_chat/core/LLMConfigurationManager.ts` +2. โœ… **Update AgentService** to use manager instead of localStorage +3. โœ… **Update AIChatPanel** model selection methods +4. โœ… **Add configuration refresh mechanisms** + +### โœ… Phase 2: Per-Request Override Support (COMPLETED) +5. โœ… **Update EvaluationAgent** per-request override logic +6. โœ… **Update Graph/AgentNodes** configuration passing via LLMConfigurationManager +7. โœ… **Per-request override functionality** implemented + +### โœ… Phase 3: Persistent Configuration API (COMPLETED) +8. โœ… **Extend EvaluationProtocol** with `configure_llm` method +9. โœ… **Implement `configure_llm` handler** in EvaluationAgent +10. ๐Ÿ”„ **Add eval-server support** for persistent configuration (server-side implementation needed) +11. โœ… **Add configuration validation** and error handling + +### ๐Ÿ”„ Phase 4: Testing & Documentation (NEXT) +12. โœ… **Maintain backwards compatibility** during transition +13. ๐Ÿ”„ **Add comprehensive tests** for both modes +14. โœ… **Update documentation** and usage examples +15. ๐Ÿ”„ **Performance testing** with multiple tabs and configurations + +## Key Implemented Features + +### LLMConfigurationManager (`/front_end/panels/ai_chat/core/LLMConfigurationManager.ts`) +- โœ… Singleton pattern with override support +- โœ… Configuration validation +- โœ… Change listeners for real-time updates +- โœ… localStorage persistence for manual mode +- โœ… Override system for automated mode + +### Per-Request Override Support +- โœ… EvaluationAgent supports model overrides via request parameters +- โœ… Automatic cleanup after request completion +- โœ… Configuration fallback hierarchy (override โ†’ localStorage) + +### Persistent Configuration API +- โœ… `configure_llm` JSON-RPC method in EvaluationProtocol +- โœ… Handler implementation in EvaluationAgent +- โœ… Real-time AgentService reinitialization +- โœ… Cross-tab configuration synchronization + +### Integration Points Updated +- โœ… AgentService: Uses LLMConfigurationManager instead of direct localStorage +- โœ… AIChatPanel: Static model methods updated to use manager +- โœ… AgentNodes: Tool execution context uses manager configuration +- โœ… EvaluationAgent: Supports both override and persistent modes + +## Usage Examples + +### Manual Mode (Settings UI) +```typescript +// Settings dialog usage +const panel = AIChatPanel.instance(); +panel.setLLMConfiguration({ + provider: 'openai', + apiKey: 'sk-...', + mainModel: 'gpt-4', + miniModel: 'gpt-4-mini', + nanoModel: 'gpt-3.5-turbo' +}); +``` + +### Automated Mode - Per-Request Override +```typescript +// Evaluation request with model override +{ + "jsonrpc": "2.0", + "method": "evaluate", + "params": { + "tool": "chat", + "input": { "message": "Hello" }, + "model": { + "provider": "openai", + "main_model": "gpt-4", + "api_key": "sk-temp-key" + } + } +} +``` + +### Automated Mode - Persistent Configuration +```typescript +// Set persistent configuration +{ + "jsonrpc": "2.0", + "method": "configure_llm", + "params": { + "provider": "openai", + "apiKey": "sk-persistent-key", + "models": { + "main": "gpt-4", + "mini": "gpt-4-mini", + "nano": "gpt-3.5-turbo" + } + } +} +``` + +## Migration Strategy + +### Backwards Compatibility +- Existing localStorage-based code continues to work during transition +- Gradual migration of components to use LLMConfigurationManager +- Fallback mechanisms for missing configuration values +- No breaking changes to existing evaluation requests + +### Testing Strategy +- Unit tests for LLMConfigurationManager override logic +- Integration tests for Settings UI compatibility +- End-to-end tests for evaluation server scenarios +- Cross-tab synchronization testing +- Performance testing with multiple concurrent evaluations + +This architecture provides a comprehensive solution for both manual user configuration and programmatic evaluation server control, while maintaining backwards compatibility and enabling powerful new automation capabilities. \ No newline at end of file From 7ec830271626487db66de11826b3c74dea34b884 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Tue, 16 Sep 2025 13:16:37 -0500 Subject: [PATCH 04/12] Fix the build issues --- front_end/panels/ai_chat/BUILD.gn | 2 ++ front_end/panels/ai_chat/core/AgentService.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/front_end/panels/ai_chat/BUILD.gn b/front_end/panels/ai_chat/BUILD.gn index 2645f33ef0c..b6182d922f7 100644 --- a/front_end/panels/ai_chat/BUILD.gn +++ b/front_end/panels/ai_chat/BUILD.gn @@ -56,6 +56,7 @@ devtools_module("ai_chat") { "core/PageInfoManager.ts", "core/AgentNodes.ts", "core/GraphHelpers.ts", + "core/LLMConfigurationManager.ts", "core/ToolNameMap.ts", "core/ToolSurfaceProvider.ts", "core/StateGraph.ts", @@ -192,6 +193,7 @@ _ai_chat_sources = [ "core/PageInfoManager.ts", "core/AgentNodes.ts", "core/GraphHelpers.ts", + "core/LLMConfigurationManager.ts", "core/ToolNameMap.ts", "core/ToolSurfaceProvider.ts", "core/StateGraph.ts", diff --git a/front_end/panels/ai_chat/core/AgentService.ts b/front_end/panels/ai_chat/core/AgentService.ts index ca176b381ed..a684d0df2c2 100644 --- a/front_end/panels/ai_chat/core/AgentService.ts +++ b/front_end/panels/ai_chat/core/AgentService.ts @@ -138,7 +138,7 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ if (endpoint) { providers.push({ provider: 'litellm' as const, - apiKey, // Can be empty for some LiteLLM endpoints + apiKey: apiKey || '', // Can be empty for some LiteLLM endpoints providerURL: endpoint }); } @@ -315,7 +315,7 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ // Force reinitialization on next use try { const config = this.#configManager.getConfiguration(); - await this.initialize(config.apiKey, config.mainModel, config.miniModel, config.nanoModel); + await this.initialize(config.apiKey || null, config.mainModel, config.miniModel || '', config.nanoModel || ''); logger.info('Agent service reinitialized successfully'); } catch (error) { logger.error('Failed to reinitialize agent service:', error); From ce6b75f9146dd155dd94472baa3ea4f88ef4fd6d Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Wed, 17 Sep 2025 03:27:10 -0500 Subject: [PATCH 05/12] Extra configs for automated mode --- .../evaluation/remote/EvaluationAgent.ts | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts index c75013d5da3..5b9cc166998 100644 --- a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts +++ b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts @@ -8,6 +8,7 @@ import { LLMConfigurationManager } from '../../core/LLMConfigurationManager.js'; import { getEvaluationConfig, getEvaluationClientId } from '../../common/EvaluationConfig.js'; import { ToolRegistry } from '../../agent_framework/ConfigurableAgentTool.js'; import { AgentService } from '../../core/AgentService.js'; +import { AIChatPanel } from '../../ui/AIChatPanel.js'; import { createLogger } from '../../core/Logger.js'; import { createTracingProvider, withTracingContext, isTracingEnabled, getTracingConfig } from '../../tracing/TracingConfig.js'; import type { TracingProvider, TracingContext } from '../../tracing/TracingProvider.js'; @@ -328,6 +329,7 @@ export class EvaluationAgent { private async handleEvaluationRequest(request: EvaluationRequest): Promise { const { params, id } = request; const startTime = Date.now(); + let hasSetEarlyOverride = false; logger.info('Received evaluation request', { evaluationId: params.evaluationId, @@ -338,6 +340,38 @@ export class EvaluationAgent { modelConfig: params.model }); + // CRITICAL FIX: Set configuration override early for any model configuration provided + // This allows API keys from request body to be available for UI-level validation + if (params.model && (params.model.main_model || params.model.provider || params.model.api_key)) { + const configManager = LLMConfigurationManager.getInstance(); + + // Extract configuration from nested model structure + const mainModel = params.model.main_model as any; + const provider = mainModel?.provider || params.model.provider || 'openai'; + const apiKey = mainModel?.api_key || params.model.api_key; + const modelName = mainModel?.model || mainModel || params.model.main_model; + const miniModel = (params.model.mini_model as any)?.model || params.model.mini_model; + const nanoModel = (params.model.nano_model as any)?.model || params.model.nano_model; + + if (apiKey) { + logger.info('Setting early configuration override for evaluation', { + evaluationId: params.evaluationId, + provider, + hasApiKey: !!apiKey, + modelName + }); + + configManager.setOverride({ + provider, + apiKey, + mainModel: modelName, + miniModel: miniModel || modelName, + nanoModel: nanoModel || miniModel || modelName + }); + hasSetEarlyOverride = true; + } + } + // Track active evaluation this.activeEvaluations.set(params.evaluationId, { startTime, @@ -464,10 +498,12 @@ export class EvaluationAgent { const mergedInput = { ...params.input, ...(params.model && { - main_model: params.model.main_model, - mini_model: params.model.mini_model, - nano_model: params.model.nano_model, - provider: params.model.provider + // Extract nested model configuration properly + main_model: (params.model.main_model as any)?.model || params.model.main_model, + mini_model: (params.model.mini_model as any)?.model || params.model.mini_model, + nano_model: (params.model.nano_model as any)?.model || params.model.nano_model, + provider: (params.model.main_model as any)?.provider || params.model.provider, + api_key: (params.model.main_model as any)?.api_key || (params.model as any).api_key }) }; @@ -574,6 +610,17 @@ export class EvaluationAgent { } finally { this.activeEvaluations.delete(params.evaluationId); + + // Clean up early override if we set one + if (hasSetEarlyOverride) { + try { + const configManager = LLMConfigurationManager.getInstance(); + configManager.clearOverride(); + logger.info('Cleared early configuration override', { evaluationId: params.evaluationId }); + } catch (clearError) { + logger.warn('Failed to clear early configuration override:', clearError); + } + } } } @@ -762,8 +809,21 @@ export class EvaluationAgent { hasOverride: configManager.hasOverride() }); - // Always reinitialize with the current configuration - await agentService.initialize(apiKey, modelName, miniModel, nanoModel); + // CRITICAL FIX: Set configuration override for this evaluation to use API key from request + // This is the key change that allows API keys to come from request body instead of localStorage + configManager.setOverride({ + provider: 'openai', // TODO: Extract from model config + apiKey: apiKey, + mainModel: modelName, + miniModel: miniModel, + nanoModel: nanoModel + }); + + // Send the message using AgentService directly but with configuration override + // The configuration override ensures it uses the API key from the request + const finalMessage: ChatMessage = tracingContext + ? await withTracingContext(tracingContext, () => agentService.sendMessage(input.message)) + : await agentService.sendMessage(input.message); // Create a child observation for the chat execution if (tracingContext) { @@ -784,11 +844,6 @@ export class EvaluationAgent { } } - // Send the message with the evaluation tracing context - const finalMessage: ChatMessage = tracingContext - ? await withTracingContext(tracingContext, () => agentService.sendMessage(input.message)) - : await agentService.sendMessage(input.message); - clearTimeout(timer); // Extract the response text from the final message @@ -850,8 +905,7 @@ export class EvaluationAgent { logger.error('Chat evaluation failed:', error); reject(error); } finally { - // Clear any configuration override - const configManager = LLMConfigurationManager.getInstance(); + // Clear configuration override after evaluation configManager.clearOverride(); } }); From 7724f655ff9fdad4a1c83ef3364bae719f40ee70 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Wed, 17 Sep 2025 03:31:31 -0500 Subject: [PATCH 06/12] Fix syntax --- .../panels/ai_chat/evaluation/remote/EvaluationAgent.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts index 5b9cc166998..06847464c8e 100644 --- a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts +++ b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts @@ -769,10 +769,10 @@ export class EvaluationAgent { }, timeout); let chatObservationId: string | undefined; + // Get configuration manager for override support (defined outside try-catch for finally block) + const configManager = LLMConfigurationManager.getInstance(); try { - // Get configuration manager for override support - const configManager = LLMConfigurationManager.getInstance(); // Set override configuration if provided in input if (input.provider || input.main_model || input.api_key) { @@ -813,7 +813,7 @@ export class EvaluationAgent { // This is the key change that allows API keys to come from request body instead of localStorage configManager.setOverride({ provider: 'openai', // TODO: Extract from model config - apiKey: apiKey, + apiKey: apiKey || undefined, mainModel: modelName, miniModel: miniModel, nanoModel: nanoModel From 4cda0cdfdcfdcbde4cc20a7490a7e06bb0ce5eb0 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Wed, 17 Sep 2025 18:22:30 -0500 Subject: [PATCH 07/12] Bypass key verification for chat panel in automated mode --- eval-server/nodejs/src/lib/EvalServer.js | 7 +++ .../panels/ai_chat/LLM/OpenAIProvider.ts | 14 ++++- front_end/panels/ai_chat/core/AgentService.ts | 18 ++++-- .../evaluation/remote/EvaluationAgent.ts | 63 ++++++++++++++++++- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/eval-server/nodejs/src/lib/EvalServer.js b/eval-server/nodejs/src/lib/EvalServer.js index 07a2d6f03d0..3b63d7ab478 100644 --- a/eval-server/nodejs/src/lib/EvalServer.js +++ b/eval-server/nodejs/src/lib/EvalServer.js @@ -196,6 +196,13 @@ export class EvalServer extends EventEmitter { return this.evaluationLoader.getAllEvaluations(); } + /** + * Get the client manager instance + */ + getClientManager() { + return this.clientManager; + } + /** * Handle new WebSocket connections */ diff --git a/front_end/panels/ai_chat/LLM/OpenAIProvider.ts b/front_end/panels/ai_chat/LLM/OpenAIProvider.ts index b8c30ad3239..29cee3fefda 100644 --- a/front_end/panels/ai_chat/LLM/OpenAIProvider.ts +++ b/front_end/panels/ai_chat/LLM/OpenAIProvider.ts @@ -1,12 +1,14 @@ // Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// Cache break: 2025-09-17T18:15:00Z - Add AUTOMATED_MODE bypass for validateCredentials import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo, MessageContent } from './LLMTypes.js'; import { LLMBaseProvider } from './LLMProvider.js'; import { LLMRetryManager } from './LLMErrorHandler.js'; import { LLMResponseParser } from './LLMResponseParser.js'; import { createLogger } from '../core/Logger.js'; +import { BUILD_CONFIG } from '../core/BuildConfig.js'; const logger = createLogger('OpenAIProvider'); @@ -522,9 +524,17 @@ export class OpenAIProvider extends LLMBaseProvider { * Validate that required credentials are available for OpenAI */ validateCredentials(): {isValid: boolean, message: string, missingItems?: string[]} { + // In AUTOMATED_MODE, skip credential validation since API keys are provided dynamically + if (BUILD_CONFIG.AUTOMATED_MODE) { + return { + isValid: true, + message: 'OpenAI credentials validation skipped in AUTOMATED_MODE (API keys provided dynamically).' + }; + } + const storageKeys = this.getCredentialStorageKeys(); const apiKey = localStorage.getItem(storageKeys.apiKey!); - + if (!apiKey) { return { isValid: false, @@ -532,7 +542,7 @@ export class OpenAIProvider extends LLMBaseProvider { missingItems: ['API Key'] }; } - + return { isValid: true, message: 'OpenAI credentials are configured correctly.' diff --git a/front_end/panels/ai_chat/core/AgentService.ts b/front_end/panels/ai_chat/core/AgentService.ts index a684d0df2c2..c7548f35dd6 100644 --- a/front_end/panels/ai_chat/core/AgentService.ts +++ b/front_end/panels/ai_chat/core/AgentService.ts @@ -1,6 +1,7 @@ // Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// Cache break: 2025-09-17T22:47:00Z - Add AUTOMATED_MODE bypass for createAgentGraph API key validation import * as Common from '../../../core/common/common.js'; import * as i18n from '../../../core/i18n/i18n.js'; @@ -20,7 +21,9 @@ import { AgentRunnerEventBus } from '../agent_framework/AgentRunnerEventBus.js'; import { AgentRunner } from '../agent_framework/AgentRunner.js'; import type { AgentSession, AgentMessage } from '../agent_framework/AgentSessionTypes.js'; import type { LLMProvider } from '../LLM/LLMTypes.js'; +import { BUILD_CONFIG } from './BuildConfig.js'; +// Cache break: 2025-09-17T17:54:00Z - Force rebuild with AUTOMATED_MODE bypass const logger = createLogger('AgentService'); /** @@ -186,8 +189,8 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ // Check if the configuration requires an API key const requiresApiKey = this.#doesCurrentConfigRequireApiKey(); - // If API key is required but not provided, throw error - if (requiresApiKey && !apiKey) { + // If API key is required but not provided, throw error (unless in AUTOMATED_MODE) + if (requiresApiKey && !apiKey && !BUILD_CONFIG.AUTOMATED_MODE) { const provider = this.#configManager.getProvider(); let providerName = 'OpenAI'; if (provider === 'litellm') { @@ -329,8 +332,8 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ async sendMessage(text: string, imageInput?: ImageInputData, selectedAgentType?: string | null): Promise { // Check if the current configuration requires an API key const requiresApiKey = this.#doesCurrentConfigRequireApiKey(); - - if (requiresApiKey && !this.#apiKey) { + + if (requiresApiKey && !this.#apiKey && !BUILD_CONFIG.AUTOMATED_MODE) { throw new Error('API key not set. Please set the API key in settings.'); } @@ -338,6 +341,13 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ throw new Error('Empty message. Please enter some text.'); } + // In AUTOMATED_MODE, ensure the graph is initialized even without API key + if (BUILD_CONFIG.AUTOMATED_MODE && !this.#graph) { + const config = this.#configManager.getConfiguration(); + // Initialize with empty API key in AUTOMATED_MODE - will be overridden by request + await this.initialize('', config.mainModel, config.miniModel || '', config.nanoModel || ''); + } + // Create a user message const userMessage = createUserMessage(text, imageInput); diff --git a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts index 06847464c8e..1711cb8fbbc 100644 --- a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts +++ b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts @@ -1,6 +1,7 @@ // Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// Cache break: 2025-09-17T17:38:00Z - Fixed API key validation import { BUILD_CONFIG } from '../../core/BuildConfig.js'; import { WebSocketRPCClient } from '../../common/WebSocketRPCClient.js'; @@ -342,17 +343,37 @@ export class EvaluationAgent { // CRITICAL FIX: Set configuration override early for any model configuration provided // This allows API keys from request body to be available for UI-level validation + logger.info('DEBUG: Checking for model configuration override', { + evaluationId: params.evaluationId, + hasModel: !!params.model, + model: params.model + }); + if (params.model && (params.model.main_model || params.model.provider || params.model.api_key)) { const configManager = LLMConfigurationManager.getInstance(); - // Extract configuration from nested model structure + // Extract configuration from nested model structure - handle both flat and nested formats const mainModel = params.model.main_model as any; + + // For nested format: main_model: { provider: "openai", model: "gpt-4", api_key: "key" } + // For flat format: { provider: "openai", main_model: "gpt-4", api_key: "key" } const provider = mainModel?.provider || params.model.provider || 'openai'; const apiKey = mainModel?.api_key || params.model.api_key; const modelName = mainModel?.model || mainModel || params.model.main_model; const miniModel = (params.model.mini_model as any)?.model || params.model.mini_model; const nanoModel = (params.model.nano_model as any)?.model || params.model.nano_model; + logger.info('DEBUG: Extracted model configuration', { + evaluationId: params.evaluationId, + mainModel, + provider, + hasApiKey: !!apiKey, + apiKeyLength: apiKey?.length, + modelName, + miniModel, + nanoModel + }); + if (apiKey) { logger.info('Setting early configuration override for evaluation', { evaluationId: params.evaluationId, @@ -369,7 +390,25 @@ export class EvaluationAgent { nanoModel: nanoModel || miniModel || modelName }); hasSetEarlyOverride = true; + + logger.info('DEBUG: Early override set successfully', { + evaluationId: params.evaluationId + }); + } else { + logger.warn('DEBUG: No API key found in model configuration', { + evaluationId: params.evaluationId, + mainModel, + provider + }); } + } else { + logger.warn('DEBUG: No model configuration found for override', { + evaluationId: params.evaluationId, + hasModel: !!params.model, + hasMainModel: !!(params.model?.main_model), + hasProvider: !!(params.model?.provider), + hasApiKey: !!(params.model?.api_key) + }); } // Track active evaluation @@ -495,6 +534,10 @@ export class EvaluationAgent { this.sendStatus(params.evaluationId, 'running', 0.5, 'Processing chat request...'); // Merge model configuration - prefer params.model over params.input model fields + // Also check for early override configuration from LLMConfigurationManager + const configManager = LLMConfigurationManager.getInstance(); + const overrideConfig = configManager.getConfiguration(); + const mergedInput = { ...params.input, ...(params.model && { @@ -504,8 +547,26 @@ export class EvaluationAgent { nano_model: (params.model.nano_model as any)?.model || params.model.nano_model, provider: (params.model.main_model as any)?.provider || params.model.provider, api_key: (params.model.main_model as any)?.api_key || (params.model as any).api_key + }), + // Apply early override configuration if available + ...(overrideConfig && { + provider: overrideConfig.provider || ((params.model?.main_model as any)?.provider || params.model?.provider), + api_key: overrideConfig.apiKey || ((params.model?.main_model as any)?.api_key || (params.model as any)?.api_key), + main_model: overrideConfig.mainModel || ((params.model?.main_model as any)?.model || params.model?.main_model), + mini_model: overrideConfig.miniModel || ((params.model?.mini_model as any)?.model || params.model?.mini_model), + nano_model: overrideConfig.nanoModel || ((params.model?.nano_model as any)?.model || params.model?.nano_model) }) }; + + logger.info('DEBUG: Created merged input for chat evaluation', { + evaluationId: params.evaluationId, + hasOverrideConfig: !!overrideConfig, + overrideConfig, + mergedInput: { + ...mergedInput, + api_key: mergedInput.api_key ? `${mergedInput.api_key.substring(0, 10)}...` : undefined + } + }); toolResult = await this.executeChatEvaluation( mergedInput, From 78b020bbed0a91eeada5131004749346add40762 Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Wed, 17 Sep 2025 18:24:36 -0500 Subject: [PATCH 08/12] Updated package-lock.json --- eval-server/nodejs/package-lock.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/eval-server/nodejs/package-lock.json b/eval-server/nodejs/package-lock.json index 494fa5e41b8..99f3ff787ca 100644 --- a/eval-server/nodejs/package-lock.json +++ b/eval-server/nodejs/package-lock.json @@ -16,6 +16,9 @@ "winston": "^3.11.0", "ws": "^8.16.0" }, + "bin": { + "eval-server": "src/cli/index.js" + }, "devDependencies": { "@types/ws": "^8.5.10" }, From 1ab0393eec71527589c5bd353101bcdcef473fec Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Thu, 18 Sep 2025 20:25:18 -0500 Subject: [PATCH 09/12] Extra verifications skipped --- .../panels/ai_chat/LLM/OpenAIProvider.ts | 1 - front_end/panels/ai_chat/core/AgentService.ts | 3 +- .../ai_chat/core/LLMConfigurationManager.ts | 34 +++++++++++-------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/front_end/panels/ai_chat/LLM/OpenAIProvider.ts b/front_end/panels/ai_chat/LLM/OpenAIProvider.ts index 29cee3fefda..dbfc03b3b30 100644 --- a/front_end/panels/ai_chat/LLM/OpenAIProvider.ts +++ b/front_end/panels/ai_chat/LLM/OpenAIProvider.ts @@ -1,7 +1,6 @@ // Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Cache break: 2025-09-17T18:15:00Z - Add AUTOMATED_MODE bypass for validateCredentials import type { LLMMessage, LLMResponse, LLMCallOptions, LLMProvider, ModelInfo, MessageContent } from './LLMTypes.js'; import { LLMBaseProvider } from './LLMProvider.js'; diff --git a/front_end/panels/ai_chat/core/AgentService.ts b/front_end/panels/ai_chat/core/AgentService.ts index c7548f35dd6..4c9b2f842e9 100644 --- a/front_end/panels/ai_chat/core/AgentService.ts +++ b/front_end/panels/ai_chat/core/AgentService.ts @@ -122,7 +122,8 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ const providers = []; // Validate and add the selected provider - const validation = this.#configManager.validateConfiguration(); + // Skip credential checks in AUTOMATED_MODE where API keys come from request body + const validation = this.#configManager.validateConfiguration(BUILD_CONFIG.AUTOMATED_MODE); if (!validation.isValid) { throw new Error(`Configuration validation failed: ${validation.errors.join(', ')}`); } diff --git a/front_end/panels/ai_chat/core/LLMConfigurationManager.ts b/front_end/panels/ai_chat/core/LLMConfigurationManager.ts index 2937c29b521..143241e0d31 100644 --- a/front_end/panels/ai_chat/core/LLMConfigurationManager.ts +++ b/front_end/panels/ai_chat/core/LLMConfigurationManager.ts @@ -1,6 +1,7 @@ // Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// Cache break: 2025-09-18T18:30:00Z - Add skipCredentialChecks parameter for AUTOMATED_MODE import { createLogger } from './Logger.js'; import type { LLMProvider } from '../LLM/LLMTypes.js'; @@ -251,8 +252,9 @@ export class LLMConfigurationManager { /** * Validate the current configuration + * @param skipCredentialChecks When true, bypasses API key/endpoint validation for AUTOMATED_MODE */ - validateConfiguration(): { isValid: boolean; errors: string[] } { + validateConfiguration(skipCredentialChecks = false): { isValid: boolean; errors: string[] } { const config = this.getConfiguration(); const errors: string[] = []; @@ -266,20 +268,22 @@ export class LLMConfigurationManager { errors.push('Main model is required'); } - // Provider-specific validation - switch (config.provider) { - case 'openai': - case 'groq': - case 'openrouter': - if (!config.apiKey) { - errors.push(`API key is required for ${config.provider}`); - } - break; - case 'litellm': - if (!config.endpoint) { - errors.push('Endpoint is required for LiteLLM'); - } - break; + // Provider-specific validation - skip credential checks in AUTOMATED_MODE + if (!skipCredentialChecks) { + switch (config.provider) { + case 'openai': + case 'groq': + case 'openrouter': + if (!config.apiKey) { + errors.push(`API key is required for ${config.provider}`); + } + break; + case 'litellm': + if (!config.endpoint) { + errors.push('Endpoint is required for LiteLLM'); + } + break; + } } return { From 855fb9a0cc6b2e28cb0ee5ef49f7da14fec5282f Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Thu, 18 Sep 2025 20:55:23 -0500 Subject: [PATCH 10/12] =?UTF-8?q?1.=20=E2=9C=93=20Added=20skipCredentialCh?= =?UTF-8?q?ecks=20parameter=20to=20validateConfiguration()=202.=20?= =?UTF-8?q?=E2=9C=93=20Fixed=20multi-provider=20credential=20preservation?= =?UTF-8?q?=20in=20saveProviderSpecificSettings()=203.=20=E2=9C=93=20Secur?= =?UTF-8?q?ed=20logging=20in=20both=20handleStorageChange()=20and=20getDeb?= =?UTF-8?q?ugInfo()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai_chat/core/LLMConfigurationManager.ts | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/front_end/panels/ai_chat/core/LLMConfigurationManager.ts b/front_end/panels/ai_chat/core/LLMConfigurationManager.ts index 143241e0d31..cdc0f88b9bb 100644 --- a/front_end/panels/ai_chat/core/LLMConfigurationManager.ts +++ b/front_end/panels/ai_chat/core/LLMConfigurationManager.ts @@ -1,7 +1,7 @@ // Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Cache break: 2025-09-18T18:30:00Z - Add skipCredentialChecks parameter for AUTOMATED_MODE +// Cache break: 2025-09-18T19:00:00Z - Add skipCredentialChecks + preserve credentials + secure logging import { createLogger } from './Logger.js'; import type { LLMProvider } from '../LLM/LLMTypes.js'; @@ -294,38 +294,42 @@ export class LLMConfigurationManager { /** * Save provider-specific settings to localStorage + * Only modifies settings for the active provider, preserving other providers' credentials */ private saveProviderSpecificSettings(config: LLMConfig): void { - // Clear all provider-specific keys first - localStorage.removeItem(STORAGE_KEYS.OPENAI_API_KEY); - localStorage.removeItem(STORAGE_KEYS.LITELLM_API_KEY); - localStorage.removeItem(STORAGE_KEYS.LITELLM_ENDPOINT); - localStorage.removeItem(STORAGE_KEYS.GROQ_API_KEY); - localStorage.removeItem(STORAGE_KEYS.OPENROUTER_API_KEY); - - // Save current provider's settings + // Save current provider's settings only (do not clear others) switch (config.provider) { case 'openai': if (config.apiKey) { localStorage.setItem(STORAGE_KEYS.OPENAI_API_KEY, config.apiKey); + } else { + localStorage.removeItem(STORAGE_KEYS.OPENAI_API_KEY); } break; case 'litellm': if (config.endpoint) { localStorage.setItem(STORAGE_KEYS.LITELLM_ENDPOINT, config.endpoint); + } else { + localStorage.removeItem(STORAGE_KEYS.LITELLM_ENDPOINT); } if (config.apiKey) { localStorage.setItem(STORAGE_KEYS.LITELLM_API_KEY, config.apiKey); + } else { + localStorage.removeItem(STORAGE_KEYS.LITELLM_API_KEY); } break; case 'groq': if (config.apiKey) { localStorage.setItem(STORAGE_KEYS.GROQ_API_KEY, config.apiKey); + } else { + localStorage.removeItem(STORAGE_KEYS.GROQ_API_KEY); } break; case 'openrouter': if (config.apiKey) { localStorage.setItem(STORAGE_KEYS.OPENROUTER_API_KEY, config.apiKey); + } else { + localStorage.removeItem(STORAGE_KEYS.OPENROUTER_API_KEY); } break; } @@ -336,9 +340,18 @@ export class LLMConfigurationManager { */ private handleStorageChange(event: StorageEvent): void { if (event.key && Object.values(STORAGE_KEYS).includes(event.key as any)) { + const sensitiveKeys = new Set([ + STORAGE_KEYS.OPENAI_API_KEY, + STORAGE_KEYS.LITELLM_API_KEY, + STORAGE_KEYS.GROQ_API_KEY, + STORAGE_KEYS.OPENROUTER_API_KEY, + ]); + const redacted = + sensitiveKeys.has(event.key as any) ? '(redacted)' : + (event.newValue ? `${event.newValue.slice(0, 8)}โ€ฆ` : null); logger.debug('Configuration changed in another tab', { key: event.key, - newValue: event.newValue + newValue: redacted }); this.notifyListeners(); } @@ -361,10 +374,15 @@ export class LLMConfigurationManager { * Get debug information about current configuration state */ getDebugInfo(): Record { + const redact = (cfg?: Partial) => cfg ? { + ...cfg, + apiKey: cfg.apiKey ? '(redacted)' : undefined + } : undefined; + return { hasOverride: this.hasOverride(), - overrideConfig: this.overrideConfig, - currentConfig: this.getConfiguration(), + overrideConfig: redact(this.overrideConfig), + currentConfig: redact(this.getConfiguration()), validation: this.validateConfiguration(), listenerCount: this.changeListeners.length, }; From d5888411e2af2e0e3e083aba05e737039bbc2cda Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Fri, 19 Sep 2025 10:57:31 -0500 Subject: [PATCH 11/12] Refactoring and improvements; specifically removed the double override, implemented single consolidated override --- .../evaluation/remote/EvaluationAgent.ts | 131 ++++++++++-------- 1 file changed, 77 insertions(+), 54 deletions(-) diff --git a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts index 1711cb8fbbc..283e50e8148 100644 --- a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts +++ b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts @@ -536,7 +536,8 @@ export class EvaluationAgent { // Merge model configuration - prefer params.model over params.input model fields // Also check for early override configuration from LLMConfigurationManager const configManager = LLMConfigurationManager.getInstance(); - const overrideConfig = configManager.getConfiguration(); + const hasOverride = configManager.hasOverride?.() || false; + const overrideConfig = hasOverride ? configManager.getConfiguration() : null; const mergedInput = { ...params.input, @@ -554,7 +555,8 @@ export class EvaluationAgent { api_key: overrideConfig.apiKey || ((params.model?.main_model as any)?.api_key || (params.model as any)?.api_key), main_model: overrideConfig.mainModel || ((params.model?.main_model as any)?.model || params.model?.main_model), mini_model: overrideConfig.miniModel || ((params.model?.mini_model as any)?.model || params.model?.mini_model), - nano_model: overrideConfig.nanoModel || ((params.model?.nano_model as any)?.model || params.model?.nano_model) + nano_model: overrideConfig.nanoModel || ((params.model?.nano_model as any)?.model || params.model?.nano_model), + endpoint: overrideConfig.endpoint || ((params.model as any)?.endpoint || (params.model?.main_model as any)?.endpoint) }) }; @@ -835,49 +837,37 @@ export class EvaluationAgent { try { - // Set override configuration if provided in input - if (input.provider || input.main_model || input.api_key) { - logger.info('Setting configuration override for chat evaluation', { - provider: input.provider, - mainModel: input.main_model, - hasApiKey: !!input.api_key - }); - - configManager.setOverride({ - provider: input.provider, - mainModel: input.main_model, - miniModel: input.mini_model, - nanoModel: input.nano_model, - apiKey: input.api_key, - endpoint: input.endpoint - }); - } - // Get or create AgentService instance const agentService = AgentService.getInstance(); - // Use override configuration if set, otherwise use constructor models + // Get current configuration as baseline const config = configManager.getConfiguration(); - const modelName = config.mainModel || this.judgeModel; - const miniModel = config.miniModel || this.miniModel; - const nanoModel = config.nanoModel || this.nanoModel; - const apiKey = config.apiKey || agentService.getApiKey(); - logger.info('Initializing AgentService for chat evaluation', { - modelName, + // Consolidate configuration with proper fallbacks + // Priority: input values -> existing config -> defaults + const provider = input.provider || config.provider || 'openai'; + const mainModel = input.main_model || config.mainModel || this.judgeModel; + const miniModel = input.mini_model ?? config.miniModel ?? this.miniModel; + const nanoModel = input.nano_model ?? config.nanoModel ?? this.nanoModel; + const apiKey = input.api_key ?? config.apiKey ?? agentService.getApiKey(); + const endpoint = input.endpoint ?? config.endpoint; + + logger.info('Setting consolidated configuration override for chat evaluation', { + provider: provider, + mainModel: mainModel, hasApiKey: !!apiKey, - isInitialized: agentService.isInitialized(), - hasOverride: configManager.hasOverride() + hasEndpoint: !!endpoint, + isInitialized: agentService.isInitialized() }); - // CRITICAL FIX: Set configuration override for this evaluation to use API key from request - // This is the key change that allows API keys to come from request body instead of localStorage + // Set single consolidated override with all fallback values configManager.setOverride({ - provider: 'openai', // TODO: Extract from model config - apiKey: apiKey || undefined, - mainModel: modelName, + provider: provider, + mainModel: mainModel, miniModel: miniModel, - nanoModel: nanoModel + nanoModel: nanoModel, + apiKey: apiKey || undefined, + endpoint: endpoint || undefined }); // Send the message using AgentService directly but with configuration override @@ -895,7 +885,7 @@ export class EvaluationAgent { name: 'Chat Execution', type: 'span', startTime: new Date(), - input: { message: input.message, model: modelName }, + input: { message: input.message, model: mainModel }, metadata: { evaluationType: 'chat' } @@ -931,18 +921,18 @@ export class EvaluationAgent { const result = { response: responseText, messages: agentService.getMessages(), - modelUsed: modelName, + modelUsed: mainModel, timestamp: new Date().toISOString(), evaluationMetadata: { evaluationType: 'chat', - actualModelUsed: modelName + actualModelUsed: mainModel } }; logger.info('Chat evaluation completed successfully', { responseLength: responseText.length, messageCount: result.messages.length, - modelUsed: modelName, + modelUsed: mainModel, evaluationId: tracingContext?.traceId }); @@ -989,15 +979,48 @@ export class EvaluationAgent { // Get configuration manager const configManager = LLMConfigurationManager.getInstance(); - // Save configuration to localStorage (persistent mode) - configManager.saveConfiguration({ - provider: params.provider, - apiKey: params.apiKey, - endpoint: params.endpoint, - mainModel: params.models.main, - miniModel: params.models.mini, - nanoModel: params.models.nano - }); + // Load existing configuration for partial updates + const currentConfig = configManager.loadConfiguration(); + + // Merge configurations based on partial flag + const mergedConfig = { + provider: params.partial ? (params.provider ?? currentConfig.provider) : params.provider, + apiKey: params.partial ? (params.apiKey ?? currentConfig.apiKey) : params.apiKey, + endpoint: params.partial ? (params.endpoint ?? currentConfig.endpoint) : params.endpoint, + mainModel: params.partial ? (params.models?.main ?? currentConfig.mainModel) : params.models.main, + miniModel: params.partial ? (params.models?.mini ?? currentConfig.miniModel) : params.models?.mini, + nanoModel: params.partial ? (params.models?.nano ?? currentConfig.nanoModel) : params.models?.nano + }; + + // Validate the merged configuration + const validation = configManager.validateConfiguration(); + + // Check validation after setting the merged config temporarily + configManager.saveConfiguration(mergedConfig); + const postSaveValidation = configManager.validateConfiguration(); + + if (!postSaveValidation.isValid) { + // Restore the original config if validation fails + configManager.saveConfiguration(currentConfig); + + // Send error response with validation errors + const errorResponse = createErrorResponse( + id, + ErrorCodes.INVALID_PARAMS, + 'Invalid configuration', + { errors: postSaveValidation.errors } + ); + + if (this.client) { + this.client.send(errorResponse); + } + + logger.error('Configuration validation failed', { + errors: postSaveValidation.errors + }); + + return; + } // Reinitialize AgentService with new configuration const agentService = AgentService.getInstance(); @@ -1005,11 +1028,11 @@ export class EvaluationAgent { // Prepare response with applied configuration const appliedConfig = { - provider: params.provider, + provider: mergedConfig.provider, models: { - main: params.models.main, - mini: params.models.mini || '', - nano: params.models.nano || '' + main: mergedConfig.mainModel, + mini: mergedConfig.miniModel || '', + nano: mergedConfig.nanoModel || '' } }; @@ -1021,8 +1044,8 @@ export class EvaluationAgent { } logger.info('LLM configuration applied successfully', { - provider: params.provider, - mainModel: params.models.main + provider: mergedConfig.provider, + mainModel: mergedConfig.mainModel }); } catch (error) { From 38b725c2d2746881f42aab2618782ce46f3506ad Mon Sep 17 00:00:00 2001 From: Oleh Luchkiv Date: Tue, 23 Sep 2025 08:25:41 -0500 Subject: [PATCH 12/12] Merged version --- config/gni/devtools_grd_files.gni | 53 +- config/gni/devtools_image_files.gni | 13 + .../schema-extractor/amazon-product-001.yaml | 2 +- .../evals/schema-extractor/bbc-news-001.yaml | 2 +- .../schema-extractor/bing-search-001.yaml | 2 +- .../schema-extractor/github-repo-001.yaml | 2 +- .../schema-extractor/google-flights-001.yaml | 2 +- .../schema-extractor/google-search-001.yaml | 2 +- .../evals/schema-extractor/homedepot-001.yaml | 2 +- .../evals/schema-extractor/macys-001.yaml | 2 +- .../wikipedia-search-001.yaml | 2 +- .../nodejs/evals/web-task-agent/jobs-001.yaml | 2 +- .../evals/web-task-agent/realestate-001.yaml | 2 +- .../web-task-agent-jobs-001.yaml | 2 +- .../web-task-agent-realestate-001.yaml | 2 +- eval-server/nodejs/schemas/client.schema.json | 2 +- .../nodejs/templates/default-client.yaml | 2 +- front_end/Images/src/asana-mcp.svg | 5 + front_end/Images/src/atlassian-mcp.svg | 4 + front_end/Images/src/github-mcp.svg | 4 + front_end/Images/src/google-drive-mcp.svg | 5 + front_end/Images/src/google-sheets-mcp.svg | 9 + front_end/Images/src/huggingface-mcp.svg | 11 + front_end/Images/src/intercom-mcp.svg | 1 + front_end/Images/src/invideo-mcp.svg | 4 + front_end/Images/src/linear-mcp.svg | 1 + front_end/Images/src/notion-mcp.svg | 1 + front_end/Images/src/sentry-mcp.svg | 1 + front_end/Images/src/slack-mcp.svg | 7 + front_end/Images/src/socket-mcp.svg | 5 + front_end/panels/ai_chat/BUILD.gn | 52 +- .../ai_chat/agent_framework/AgentRunner.ts | 40 +- .../agent_framework/AgentSessionTypes.ts | 4 +- .../agent_framework/ConfigurableAgentTool.ts | 33 +- .../implementation/ConfiguredAgents.ts | 1376 +- .../implementation/agents/ActionAgent.ts | 139 + .../agents/ActionVerificationAgent.ts | 110 + .../implementation/agents/AgentVersion.ts | 1 + .../implementation/agents/ClickActionAgent.ts | 84 + .../agents/ContentWriterAgent.ts | 72 + .../agents/DirectURLNavigatorAgent.ts | 72 + .../agents/EcommerceProductInfoAgent.ts | 144 + .../agents/FormFillActionAgent.ts | 86 + .../implementation/agents/HoverActionAgent.ts | 86 + .../agents/KeyboardInputActionAgent.ts | 87 + .../implementation/agents/ResearchAgent.ts | 207 + .../agents/ScrollActionAgent.ts | 88 + .../implementation/agents/SearchAgent.ts | 287 + .../implementation/agents/WebTaskAgent.ts | 241 + .../ai_chat/core/AgentDescriptorRegistry.ts | 138 + front_end/panels/ai_chat/core/AgentNodes.ts | 318 +- front_end/panels/ai_chat/core/AgentService.ts | 135 +- .../ai_chat/core/BaseOrchestratorAgent.ts | 107 +- .../panels/ai_chat/core/ConfigurableGraph.ts | 12 +- front_end/panels/ai_chat/core/Logger.ts | 2 +- .../panels/ai_chat/core/PageInfoManager.ts | 2 +- front_end/panels/ai_chat/core/State.ts | 6 + front_end/panels/ai_chat/core/StateGraph.ts | 10 +- front_end/panels/ai_chat/core/ToolNameMap.ts | 15 +- .../ai_chat/core/ToolSurfaceProvider.ts | 244 +- front_end/panels/ai_chat/core/Types.ts | 4 +- front_end/panels/ai_chat/core/Version.ts | 4 +- .../core/{ => __tests__}/AgentNodes.test.ts | 53 +- .../core/__tests__/ToolExecutorNode.test.ts | 278 + .../core/__tests__/ToolNameMap.test.ts | 142 + .../{ => __tests__}/ToolNameMapping.test.ts | 14 +- .../ToolSurfaceProvider.test.ts | 12 +- .../docs/MCP_OAuth_Implementation_Plan.md | 333 + .../evaluation/remote/EvaluationAgent.ts | 99 +- .../evaluation/runner/EvaluationRunner.ts | 30 +- .../test-cases/schema-extractor-tests.ts | 20 +- .../test-cases/web-task-agent-tests.ts | 4 +- front_end/panels/ai_chat/mcp/MCPConfig.ts | 442 +- front_end/panels/ai_chat/mcp/MCPRegistry.ts | 618 +- .../panels/ai_chat/mcp/MCPToolAdapter.ts | 2 +- .../mcp/{ => __tests__}/MCPClientSDK.test.ts | 14 +- .../ai_chat/mcp/__tests__/MCPConfig.test.ts | 173 + .../mcp/__tests__/MCPIntegration.test.ts | 321 + .../mcp/__tests__/MCPToolRegistration.test.ts | 195 + front_end/panels/ai_chat/tools/FetcherTool.ts | 42 +- .../ai_chat/tools/SchemaBasedExtractorTool.ts | 2 +- .../tools/StreamlinedSchemaExtractorTool.ts | 25 +- front_end/panels/ai_chat/tools/Tools.ts | 843 +- .../ai_chat/tracing/LangfuseProvider.ts | 42 +- front_end/panels/ai_chat/ui/AIChatPanel.ts | 57 +- front_end/panels/ai_chat/ui/ChatView.ts | 211 +- front_end/panels/ai_chat/ui/SettingsDialog.ts | 3283 +- .../ui/{ => __tests__}/AIChatPanel.test.ts | 25 +- front_end/panels/ai_chat/ui/chatView.css | 44 + front_end/panels/ai_chat/ui/input/InputBar.ts | 15 + .../ai_chat/ui/mcp/MCPConnectionsDialog.ts | 712 + .../ui/mcp/MCPConnectorsCatalogDialog.ts | 1024 + .../ui/mcp/mcpConnectorsCatalogDialog.css | 411 + .../third_party/mcp-sdk/.tonic_example.js | 20 + front_end/third_party/mcp-sdk/BUILD.gn | 62 +- front_end/third_party/mcp-sdk/LICENSE | 7 +- .../third_party/mcp-sdk/MCPOAuthProvider.ts | 261 + front_end/third_party/mcp-sdk/README.chromium | 19 +- front_end/third_party/mcp-sdk/README.md | 1497 + .../mcp-sdk/ajv/.runkit_example.js | 23 - .../third_party/mcp-sdk/ajv/.tonic_example.js | 20 + front_end/third_party/mcp-sdk/ajv/LICENSE | 2 +- front_end/third_party/mcp-sdk/ajv/README.md | 1498 +- .../third_party/mcp-sdk/ajv/dist/2019.d.ts | 19 - .../third_party/mcp-sdk/ajv/dist/2019.js | 61 - .../third_party/mcp-sdk/ajv/dist/2019.js.map | 1 - .../third_party/mcp-sdk/ajv/dist/2020.d.ts | 19 - .../third_party/mcp-sdk/ajv/dist/2020.js | 55 - .../third_party/mcp-sdk/ajv/dist/2020.js.map | 1 - .../third_party/mcp-sdk/ajv/dist/ajv-esm.js | 9 + .../mcp-sdk/ajv/dist/ajv.bundle.js | 7189 ++ .../third_party/mcp-sdk/ajv/dist/ajv.d.ts | 19 +- front_end/third_party/mcp-sdk/ajv/dist/ajv.js | 51 +- .../third_party/mcp-sdk/ajv/dist/ajv.js.map | 1 - .../third_party/mcp-sdk/ajv/dist/ajv.min.js | 3 + .../mcp-sdk/ajv/dist/ajv.min.js.map | 1 + .../mcp-sdk/ajv/dist/cjs/client/auth.d.ts | 139 - .../mcp-sdk/ajv/dist/cjs/client/auth.d.ts.map | 1 - .../mcp-sdk/ajv/dist/cjs/client/auth.js | 340 - .../mcp-sdk/ajv/dist/cjs/client/auth.js.map | 1 - .../mcp-sdk/ajv/dist/cjs/client/index.d.ts | 942 - .../mcp-sdk/ajv/dist/cjs/client/index.js.map | 1 - .../mcp-sdk/ajv/dist/cjs/client/sse.d.ts.map | 1 - .../mcp-sdk/ajv/dist/cjs/client/sse.js.map | 1 - .../mcp-sdk/ajv/dist/cjs/client/stdio.js.map | 1 - .../dist/cjs/client/streamableHttp.d.ts.map | 1 - .../ajv/dist/cjs/client/streamableHttp.js.map | 1 - .../client/simpleStreamableHttp.js.map | 1 - .../server/demoInMemoryOAuthProvider.d.ts.map | 1 - .../server/demoInMemoryOAuthProvider.js.map | 1 - .../server/jsonResponseStreamableHttp.js.map | 1 - .../examples/server/simpleSseServer.js.map | 1 - .../simpleStatelessStreamableHttp.js.map | 1 - .../examples/server/simpleStreamableHttp.js | 338 - .../server/simpleStreamableHttp.js.map | 1 - ...seAndStreamableHttpCompatibleServer.js.map | 1 - .../ajv/dist/cjs/server/auth/errors.d.ts.map | 1 - .../ajv/dist/cjs/server/auth/errors.js.map | 1 - .../cjs/server/auth/handlers/authorize.js.map | 1 - .../cjs/server/auth/handlers/register.js.map | 1 - .../cjs/server/auth/handlers/revoke.js.map | 1 - .../cjs/server/auth/handlers/token.js.map | 1 - .../server/auth/middleware/bearerAuth.js.map | 1 - .../dist/cjs/server/auth/provider.d.ts.map | 1 - .../auth/providers/proxyProvider.d.ts.map | 1 - .../auth/providers/proxyProvider.js.map | 1 - .../ajv/dist/cjs/server/completable.d.ts.map | 1 - .../ajv/dist/cjs/server/completable.js.map | 1 - .../ajv/dist/cjs/server/index.d.ts.map | 1 - .../mcp-sdk/ajv/dist/cjs/server/index.js.map | 1 - .../mcp-sdk/ajv/dist/cjs/server/mcp.d.ts.map | 1 - .../mcp-sdk/ajv/dist/cjs/server/mcp.js.map | 1 - .../mcp-sdk/ajv/dist/cjs/server/sse.d.ts.map | 1 - .../mcp-sdk/ajv/dist/cjs/server/sse.js.map | 1 - .../dist/cjs/server/streamableHttp.d.ts.map | 1 - .../ajv/dist/cjs/server/streamableHttp.js.map | 1 - .../mcp-sdk/ajv/dist/cjs/shared/auth.d.ts | 321 - .../mcp-sdk/ajv/dist/cjs/shared/auth.d.ts.map | 1 - .../mcp-sdk/ajv/dist/cjs/shared/auth.js.map | 1 - .../ajv/dist/cjs/shared/protocol.d.ts.map | 1 - .../ajv/dist/cjs/shared/protocol.js.map | 1 - .../ajv/dist/cjs/shared/transport.d.ts.map | 1 - .../mcp-sdk/ajv/dist/cjs/types.d.ts | 31751 --------- .../mcp-sdk/ajv/dist/cjs/types.d.ts.map | 1 - .../mcp-sdk/ajv/dist/cjs/types.js.map | 1 - .../ajv/dist/compile/codegen/code.d.ts | 40 - .../mcp-sdk/ajv/dist/compile/codegen/code.js | 156 - .../ajv/dist/compile/codegen/code.js.map | 1 - .../ajv/dist/compile/codegen/index.d.ts | 79 - .../mcp-sdk/ajv/dist/compile/codegen/index.js | 697 - .../ajv/dist/compile/codegen/index.js.map | 1 - .../ajv/dist/compile/codegen/scope.d.ts | 79 - .../mcp-sdk/ajv/dist/compile/codegen/scope.js | 143 - .../ajv/dist/compile/codegen/scope.js.map | 1 - .../mcp-sdk/ajv/dist/compile/errors.d.ts | 13 - .../mcp-sdk/ajv/dist/compile/errors.js | 123 - .../mcp-sdk/ajv/dist/compile/errors.js.map | 1 - .../mcp-sdk/ajv/dist/compile/index.d.ts | 80 - .../mcp-sdk/ajv/dist/compile/index.js | 242 - .../mcp-sdk/ajv/dist/compile/index.js.map | 1 - .../mcp-sdk/ajv/dist/compile/jtd/parse.d.ts | 4 - .../mcp-sdk/ajv/dist/compile/jtd/parse.js | 350 - .../mcp-sdk/ajv/dist/compile/jtd/parse.js.map | 1 - .../ajv/dist/compile/jtd/serialize.d.ts | 4 - .../mcp-sdk/ajv/dist/compile/jtd/serialize.js | 229 - .../ajv/dist/compile/jtd/serialize.js.map | 1 - .../mcp-sdk/ajv/dist/compile/jtd/types.d.ts | 6 - .../mcp-sdk/ajv/dist/compile/jtd/types.js | 14 - .../mcp-sdk/ajv/dist/compile/jtd/types.js.map | 1 - .../mcp-sdk/ajv/dist/compile/names.d.ts | 20 - .../mcp-sdk/ajv/dist/compile/names.js | 28 - .../mcp-sdk/ajv/dist/compile/names.js.map | 1 - .../mcp-sdk/ajv/dist/compile/ref_error.d.ts | 6 - .../mcp-sdk/ajv/dist/compile/ref_error.js | 12 - .../mcp-sdk/ajv/dist/compile/ref_error.js.map | 1 - .../mcp-sdk/ajv/dist/compile/resolve.d.ts | 12 - .../mcp-sdk/ajv/dist/compile/resolve.js | 155 - .../mcp-sdk/ajv/dist/compile/resolve.js.map | 1 - .../mcp-sdk/ajv/dist/compile/rules.d.ts | 28 - .../mcp-sdk/ajv/dist/compile/rules.js | 26 - .../mcp-sdk/ajv/dist/compile/rules.js.map | 1 - .../mcp-sdk/ajv/dist/compile/util.d.ts | 40 - .../mcp-sdk/ajv/dist/compile/util.js | 178 - .../mcp-sdk/ajv/dist/compile/util.js.map | 1 - .../dist/compile/validate/applicability.d.ts | 6 - .../dist/compile/validate/applicability.js | 19 - .../compile/validate/applicability.js.map | 1 - .../ajv/dist/compile/validate/boolSchema.d.ts | 4 - .../ajv/dist/compile/validate/boolSchema.js | 50 - .../dist/compile/validate/boolSchema.js.map | 1 - .../ajv/dist/compile/validate/dataType.d.ts | 17 - .../ajv/dist/compile/validate/dataType.js | 203 - .../ajv/dist/compile/validate/dataType.js.map | 1 - .../ajv/dist/compile/validate/defaults.d.ts | 2 - .../ajv/dist/compile/validate/defaults.js | 35 - .../ajv/dist/compile/validate/defaults.js.map | 1 - .../ajv/dist/compile/validate/index.d.ts | 42 - .../ajv/dist/compile/validate/index.js | 520 - .../ajv/dist/compile/validate/index.js.map | 1 - .../ajv/dist/compile/validate/keyword.d.ts | 8 - .../ajv/dist/compile/validate/keyword.js | 124 - .../ajv/dist/compile/validate/keyword.js.map | 1 - .../ajv/dist/compile/validate/subschema.d.ts | 47 - .../ajv/dist/compile/validate/subschema.js | 81 - .../dist/compile/validate/subschema.js.map | 1 - .../third_party/mcp-sdk/ajv/dist/core.d.ts | 173 - .../third_party/mcp-sdk/ajv/dist/core.js | 618 - .../third_party/mcp-sdk/ajv/dist/core.js.map | 1 - .../mcp-sdk/ajv/dist/esm/client/auth.d.ts | 139 - .../mcp-sdk/ajv/dist/esm/client/auth.d.ts.map | 1 - .../mcp-sdk/ajv/dist/esm/client/auth.js | 325 - .../mcp-sdk/ajv/dist/esm/client/auth.js.map | 1 - .../mcp-sdk/ajv/dist/esm/client/index.d.ts | 942 - .../mcp-sdk/ajv/dist/esm/client/index.js.map | 1 - .../mcp-sdk/ajv/dist/esm/client/sse.d.ts.map | 1 - .../mcp-sdk/ajv/dist/esm/client/sse.js.map | 1 - .../mcp-sdk/ajv/dist/esm/client/stdio.js.map | 1 - .../dist/esm/client/streamableHttp.d.ts.map | 1 - .../ajv/dist/esm/client/streamableHttp.js.map | 1 - .../client/simpleStreamableHttp.js.map | 1 - .../server/demoInMemoryOAuthProvider.d.ts.map | 1 - .../server/demoInMemoryOAuthProvider.js.map | 1 - .../server/jsonResponseStreamableHttp.js.map | 1 - .../examples/server/simpleSseServer.js.map | 1 - .../simpleStatelessStreamableHttp.js.map | 1 - .../examples/server/simpleStreamableHttp.js | 333 - .../server/simpleStreamableHttp.js.map | 1 - ...seAndStreamableHttpCompatibleServer.js.map | 1 - .../ajv/dist/esm/server/auth/errors.d.ts.map | 1 - .../ajv/dist/esm/server/auth/errors.js.map | 1 - .../esm/server/auth/handlers/authorize.js.map | 1 - .../esm/server/auth/handlers/register.js.map | 1 - .../esm/server/auth/handlers/revoke.js.map | 1 - .../esm/server/auth/handlers/token.js.map | 1 - .../server/auth/middleware/bearerAuth.js.map | 1 - .../dist/esm/server/auth/provider.d.ts.map | 1 - .../auth/providers/proxyProvider.d.ts.map | 1 - .../auth/providers/proxyProvider.js.map | 1 - .../ajv/dist/esm/server/completable.d.ts.map | 1 - .../ajv/dist/esm/server/completable.js.map | 1 - .../ajv/dist/esm/server/index.d.ts.map | 1 - .../mcp-sdk/ajv/dist/esm/server/index.js.map | 1 - .../mcp-sdk/ajv/dist/esm/server/mcp.d.ts.map | 1 - .../mcp-sdk/ajv/dist/esm/server/mcp.js.map | 1 - .../mcp-sdk/ajv/dist/esm/server/sse.d.ts.map | 1 - .../mcp-sdk/ajv/dist/esm/server/sse.js.map | 1 - .../dist/esm/server/streamableHttp.d.ts.map | 1 - .../ajv/dist/esm/server/streamableHttp.js.map | 1 - .../mcp-sdk/ajv/dist/esm/shared/auth.d.ts | 321 - .../mcp-sdk/ajv/dist/esm/shared/auth.d.ts.map | 1 - .../mcp-sdk/ajv/dist/esm/shared/auth.js.map | 1 - .../ajv/dist/esm/shared/protocol.d.ts.map | 1 - .../ajv/dist/esm/shared/protocol.js.map | 1 - .../ajv/dist/esm/shared/transport.d.ts.map | 1 - .../mcp-sdk/ajv/dist/esm/types.d.ts | 31751 --------- .../mcp-sdk/ajv/dist/esm/types.d.ts.map | 1 - .../mcp-sdk/ajv/dist/esm/types.js.map | 1 - .../third_party/mcp-sdk/ajv/dist/jtd.d.ts | 47 - front_end/third_party/mcp-sdk/ajv/dist/jtd.js | 72 - .../third_party/mcp-sdk/ajv/dist/jtd.js.map | 1 - .../mcp-sdk/ajv/dist/refs/data.json | 13 - .../dist/refs/json-schema-2019-09/index.d.ts | 2 - .../dist/refs/json-schema-2019-09/index.js | 28 - .../refs/json-schema-2019-09/index.js.map | 1 - .../json-schema-2019-09/meta/applicator.json | 53 - .../json-schema-2019-09/meta/content.json | 17 - .../refs/json-schema-2019-09/meta/core.json | 57 - .../refs/json-schema-2019-09/meta/format.json | 14 - .../json-schema-2019-09/meta/meta-data.json | 37 - .../json-schema-2019-09/meta/validation.json | 90 - .../dist/refs/json-schema-2019-09/schema.json | 39 - .../dist/refs/json-schema-2020-12/index.d.ts | 2 - .../dist/refs/json-schema-2020-12/index.js | 30 - .../refs/json-schema-2020-12/index.js.map | 1 - .../json-schema-2020-12/meta/applicator.json | 48 - .../json-schema-2020-12/meta/content.json | 17 - .../refs/json-schema-2020-12/meta/core.json | 51 - .../meta/format-annotation.json | 14 - .../json-schema-2020-12/meta/meta-data.json | 37 - .../json-schema-2020-12/meta/unevaluated.json | 15 - .../json-schema-2020-12/meta/validation.json | 90 - .../dist/refs/json-schema-2020-12/schema.json | 55 - .../ajv/dist/refs/json-schema-draft-06.json | 137 - .../ajv/dist/refs/json-schema-draft-07.json | 151 - .../mcp-sdk/ajv/dist/refs/jtd-schema.d.ts | 3 - .../mcp-sdk/ajv/dist/refs/jtd-schema.js | 118 - .../mcp-sdk/ajv/dist/refs/jtd-schema.js.map | 1 - .../mcp-sdk/ajv/dist/runtime/equal.d.ts | 6 - .../mcp-sdk/ajv/dist/runtime/equal.js | 7 - .../mcp-sdk/ajv/dist/runtime/equal.js.map | 1 - .../mcp-sdk/ajv/dist/runtime/parseJson.d.ts | 18 - .../mcp-sdk/ajv/dist/runtime/parseJson.js | 185 - .../mcp-sdk/ajv/dist/runtime/parseJson.js.map | 1 - .../mcp-sdk/ajv/dist/runtime/quote.d.ts | 5 - .../mcp-sdk/ajv/dist/runtime/quote.js | 30 - .../mcp-sdk/ajv/dist/runtime/quote.js.map | 1 - .../mcp-sdk/ajv/dist/runtime/re2.d.ts | 6 - .../mcp-sdk/ajv/dist/runtime/re2.js | 6 - .../mcp-sdk/ajv/dist/runtime/re2.js.map | 1 - .../mcp-sdk/ajv/dist/runtime/timestamp.d.ts | 5 - .../mcp-sdk/ajv/dist/runtime/timestamp.js | 42 - .../mcp-sdk/ajv/dist/runtime/timestamp.js.map | 1 - .../mcp-sdk/ajv/dist/runtime/ucs2length.d.ts | 5 - .../mcp-sdk/ajv/dist/runtime/ucs2length.js | 24 - .../ajv/dist/runtime/ucs2length.js.map | 1 - .../mcp-sdk/ajv/dist/runtime/uri.d.ts | 6 - .../mcp-sdk/ajv/dist/runtime/uri.js | 6 - .../mcp-sdk/ajv/dist/runtime/uri.js.map | 1 - .../ajv/dist/runtime/validation_error.d.ts | 7 - .../ajv/dist/runtime/validation_error.js | 11 - .../ajv/dist/runtime/validation_error.js.map | 1 - .../mcp-sdk/ajv/dist/standalone/index.d.ts | 6 - .../mcp-sdk/ajv/dist/standalone/index.js | 90 - .../mcp-sdk/ajv/dist/standalone/index.js.map | 1 - .../mcp-sdk/ajv/dist/standalone/instance.d.ts | 12 - .../mcp-sdk/ajv/dist/standalone/instance.js | 35 - .../ajv/dist/standalone/instance.js.map | 1 - .../mcp-sdk/ajv/dist/types/index.d.ts | 183 - .../mcp-sdk/ajv/dist/types/index.js | 3 - .../mcp-sdk/ajv/dist/types/index.js.map | 1 - .../mcp-sdk/ajv/dist/types/json-schema.d.ts | 125 - .../mcp-sdk/ajv/dist/types/json-schema.js | 3 - .../mcp-sdk/ajv/dist/types/json-schema.js.map | 1 - .../mcp-sdk/ajv/dist/types/jtd-schema.d.ts | 174 - .../mcp-sdk/ajv/dist/types/jtd-schema.js | 3 - .../mcp-sdk/ajv/dist/types/jtd-schema.js.map | 1 - .../applicator/additionalItems.d.ts | 8 - .../applicator/additionalItems.js | 49 - .../applicator/additionalItems.js.map | 1 - .../applicator/additionalProperties.d.ts | 6 - .../applicator/additionalProperties.js | 106 - .../applicator/additionalProperties.js.map | 1 - .../dist/vocabularies/applicator/allOf.d.ts | 3 - .../ajv/dist/vocabularies/applicator/allOf.js | 23 - .../dist/vocabularies/applicator/allOf.js.map | 1 - .../dist/vocabularies/applicator/anyOf.d.ts | 4 - .../ajv/dist/vocabularies/applicator/anyOf.js | 12 - .../dist/vocabularies/applicator/anyOf.js.map | 1 - .../vocabularies/applicator/contains.d.ts | 7 - .../dist/vocabularies/applicator/contains.js | 95 - .../vocabularies/applicator/contains.js.map | 1 - .../vocabularies/applicator/dependencies.d.ts | 21 - .../vocabularies/applicator/dependencies.js | 85 - .../applicator/dependencies.js.map | 1 - .../applicator/dependentSchemas.d.ts | 3 - .../applicator/dependentSchemas.js | 11 - .../applicator/dependentSchemas.js.map | 1 - .../ajv/dist/vocabularies/applicator/if.d.ts | 6 - .../ajv/dist/vocabularies/applicator/if.js | 66 - .../dist/vocabularies/applicator/if.js.map | 1 - .../dist/vocabularies/applicator/index.d.ts | 13 - .../ajv/dist/vocabularies/applicator/index.js | 44 - .../dist/vocabularies/applicator/index.js.map | 1 - .../dist/vocabularies/applicator/items.d.ts | 5 - .../ajv/dist/vocabularies/applicator/items.js | 52 - .../dist/vocabularies/applicator/items.js.map | 1 - .../vocabularies/applicator/items2020.d.ts | 6 - .../dist/vocabularies/applicator/items2020.js | 30 - .../vocabularies/applicator/items2020.js.map | 1 - .../ajv/dist/vocabularies/applicator/not.d.ts | 4 - .../ajv/dist/vocabularies/applicator/not.js | 26 - .../dist/vocabularies/applicator/not.js.map | 1 - .../dist/vocabularies/applicator/oneOf.d.ts | 6 - .../ajv/dist/vocabularies/applicator/oneOf.js | 60 - .../dist/vocabularies/applicator/oneOf.js.map | 1 - .../applicator/patternProperties.d.ts | 3 - .../applicator/patternProperties.js | 75 - .../applicator/patternProperties.js.map | 1 - .../vocabularies/applicator/prefixItems.d.ts | 3 - .../vocabularies/applicator/prefixItems.js | 12 - .../applicator/prefixItems.js.map | 1 - .../vocabularies/applicator/properties.d.ts | 3 - .../vocabularies/applicator/properties.js | 54 - .../vocabularies/applicator/properties.js.map | 1 - .../applicator/propertyNames.d.ts | 6 - .../vocabularies/applicator/propertyNames.js | 38 - .../applicator/propertyNames.js.map | 1 - .../vocabularies/applicator/thenElse.d.ts | 3 - .../dist/vocabularies/applicator/thenElse.js | 13 - .../vocabularies/applicator/thenElse.js.map | 1 - .../mcp-sdk/ajv/dist/vocabularies/code.d.ts | 17 - .../mcp-sdk/ajv/dist/vocabularies/code.js | 131 - .../mcp-sdk/ajv/dist/vocabularies/code.js.map | 1 - .../ajv/dist/vocabularies/core/id.d.ts | 3 - .../mcp-sdk/ajv/dist/vocabularies/core/id.js | 10 - .../ajv/dist/vocabularies/core/id.js.map | 1 - .../ajv/dist/vocabularies/core/index.d.ts | 3 - .../ajv/dist/vocabularies/core/index.js | 16 - .../ajv/dist/vocabularies/core/index.js.map | 1 - .../ajv/dist/vocabularies/core/ref.d.ts | 8 - .../mcp-sdk/ajv/dist/vocabularies/core/ref.js | 122 - .../ajv/dist/vocabularies/core/ref.js.map | 1 - .../vocabularies/discriminator/index.d.ts | 5 - .../dist/vocabularies/discriminator/index.js | 104 - .../vocabularies/discriminator/index.js.map | 1 - .../vocabularies/discriminator/types.d.ts | 10 - .../dist/vocabularies/discriminator/types.js | 9 - .../vocabularies/discriminator/types.js.map | 1 - .../ajv/dist/vocabularies/draft2020.d.ts | 3 - .../ajv/dist/vocabularies/draft2020.js | 23 - .../ajv/dist/vocabularies/draft2020.js.map | 1 - .../mcp-sdk/ajv/dist/vocabularies/draft7.d.ts | 3 - .../mcp-sdk/ajv/dist/vocabularies/draft7.js | 17 - .../ajv/dist/vocabularies/draft7.js.map | 1 - .../vocabularies/dynamic/dynamicAnchor.d.ts | 5 - .../vocabularies/dynamic/dynamicAnchor.js | 30 - .../vocabularies/dynamic/dynamicAnchor.js.map | 1 - .../dist/vocabularies/dynamic/dynamicRef.d.ts | 5 - .../dist/vocabularies/dynamic/dynamicRef.js | 51 - .../vocabularies/dynamic/dynamicRef.js.map | 1 - .../ajv/dist/vocabularies/dynamic/index.d.ts | 3 - .../ajv/dist/vocabularies/dynamic/index.js | 9 - .../dist/vocabularies/dynamic/index.js.map | 1 - .../vocabularies/dynamic/recursiveAnchor.d.ts | 3 - .../vocabularies/dynamic/recursiveAnchor.js | 16 - .../dynamic/recursiveAnchor.js.map | 1 - .../vocabularies/dynamic/recursiveRef.d.ts | 3 - .../dist/vocabularies/dynamic/recursiveRef.js | 10 - .../vocabularies/dynamic/recursiveRef.js.map | 1 - .../mcp-sdk/ajv/dist/vocabularies/errors.d.ts | 9 - .../mcp-sdk/ajv/dist/vocabularies/errors.js | 3 - .../ajv/dist/vocabularies/errors.js.map | 1 - .../ajv/dist/vocabularies/format/format.d.ts | 8 - .../ajv/dist/vocabularies/format/format.js | 92 - .../dist/vocabularies/format/format.js.map | 1 - .../ajv/dist/vocabularies/format/index.d.ts | 3 - .../ajv/dist/vocabularies/format/index.js | 6 - .../ajv/dist/vocabularies/format/index.js.map | 1 - .../dist/vocabularies/jtd/discriminator.d.ts | 6 - .../dist/vocabularies/jtd/discriminator.js | 71 - .../vocabularies/jtd/discriminator.js.map | 1 - .../ajv/dist/vocabularies/jtd/elements.d.ts | 5 - .../ajv/dist/vocabularies/jtd/elements.js | 24 - .../ajv/dist/vocabularies/jtd/elements.js.map | 1 - .../ajv/dist/vocabularies/jtd/enum.d.ts | 6 - .../mcp-sdk/ajv/dist/vocabularies/jtd/enum.js | 43 - .../ajv/dist/vocabularies/jtd/enum.js.map | 1 - .../ajv/dist/vocabularies/jtd/error.d.ts | 9 - .../ajv/dist/vocabularies/jtd/error.js | 20 - .../ajv/dist/vocabularies/jtd/error.js.map | 1 - .../ajv/dist/vocabularies/jtd/index.d.ts | 10 - .../ajv/dist/vocabularies/jtd/index.js | 29 - .../ajv/dist/vocabularies/jtd/index.js.map | 1 - .../ajv/dist/vocabularies/jtd/metadata.d.ts | 5 - .../ajv/dist/vocabularies/jtd/metadata.js | 25 - .../ajv/dist/vocabularies/jtd/metadata.js.map | 1 - .../ajv/dist/vocabularies/jtd/nullable.d.ts | 4 - .../ajv/dist/vocabularies/jtd/nullable.js | 22 - .../ajv/dist/vocabularies/jtd/nullable.js.map | 1 - .../vocabularies/jtd/optionalProperties.d.ts | 3 - .../vocabularies/jtd/optionalProperties.js | 15 - .../jtd/optionalProperties.js.map | 1 - .../ajv/dist/vocabularies/jtd/properties.d.ts | 22 - .../ajv/dist/vocabularies/jtd/properties.js | 149 - .../dist/vocabularies/jtd/properties.js.map | 1 - .../ajv/dist/vocabularies/jtd/ref.d.ts | 4 - .../mcp-sdk/ajv/dist/vocabularies/jtd/ref.js | 67 - .../ajv/dist/vocabularies/jtd/ref.js.map | 1 - .../ajv/dist/vocabularies/jtd/type.d.ts | 10 - .../mcp-sdk/ajv/dist/vocabularies/jtd/type.js | 69 - .../ajv/dist/vocabularies/jtd/type.js.map | 1 - .../ajv/dist/vocabularies/jtd/union.d.ts | 3 - .../ajv/dist/vocabularies/jtd/union.js | 12 - .../ajv/dist/vocabularies/jtd/union.js.map | 1 - .../ajv/dist/vocabularies/jtd/values.d.ts | 5 - .../ajv/dist/vocabularies/jtd/values.js | 51 - .../ajv/dist/vocabularies/jtd/values.js.map | 1 - .../ajv/dist/vocabularies/metadata.d.ts | 3 - .../mcp-sdk/ajv/dist/vocabularies/metadata.js | 18 - .../ajv/dist/vocabularies/metadata.js.map | 1 - .../mcp-sdk/ajv/dist/vocabularies/next.d.ts | 3 - .../mcp-sdk/ajv/dist/vocabularies/next.js | 8 - .../mcp-sdk/ajv/dist/vocabularies/next.js.map | 1 - .../dist/vocabularies/unevaluated/index.d.ts | 3 - .../dist/vocabularies/unevaluated/index.js | 7 - .../vocabularies/unevaluated/index.js.map | 1 - .../unevaluated/unevaluatedItems.d.ts | 6 - .../unevaluated/unevaluatedItems.js | 40 - .../unevaluated/unevaluatedItems.js.map | 1 - .../unevaluated/unevaluatedProperties.d.ts | 6 - .../unevaluated/unevaluatedProperties.js | 65 - .../unevaluated/unevaluatedProperties.js.map | 1 - .../dist/vocabularies/validation/const.d.ts | 6 - .../ajv/dist/vocabularies/validation/const.js | 25 - .../dist/vocabularies/validation/const.js.map | 1 - .../validation/dependentRequired.d.ts | 5 - .../validation/dependentRequired.js | 12 - .../validation/dependentRequired.js.map | 1 - .../dist/vocabularies/validation/enum.d.ts | 8 - .../ajv/dist/vocabularies/validation/enum.js | 48 - .../dist/vocabularies/validation/enum.js.map | 1 - .../dist/vocabularies/validation/index.d.ts | 16 - .../ajv/dist/vocabularies/validation/index.js | 33 - .../dist/vocabularies/validation/index.js.map | 1 - .../validation/limitContains.d.ts | 3 - .../vocabularies/validation/limitContains.js | 15 - .../validation/limitContains.js.map | 1 - .../vocabularies/validation/limitItems.d.ts | 3 - .../vocabularies/validation/limitItems.js | 24 - .../vocabularies/validation/limitItems.js.map | 1 - .../vocabularies/validation/limitLength.d.ts | 3 - .../vocabularies/validation/limitLength.js | 27 - .../validation/limitLength.js.map | 1 - .../vocabularies/validation/limitNumber.d.ts | 11 - .../vocabularies/validation/limitNumber.js | 27 - .../validation/limitNumber.js.map | 1 - .../validation/limitProperties.d.ts | 3 - .../validation/limitProperties.js | 24 - .../validation/limitProperties.js.map | 1 - .../vocabularies/validation/multipleOf.d.ts | 8 - .../vocabularies/validation/multipleOf.js | 26 - .../vocabularies/validation/multipleOf.js.map | 1 - .../dist/vocabularies/validation/pattern.d.ts | 8 - .../dist/vocabularies/validation/pattern.js | 24 - .../vocabularies/validation/pattern.js.map | 1 - .../vocabularies/validation/required.d.ts | 8 - .../dist/vocabularies/validation/required.js | 79 - .../vocabularies/validation/required.js.map | 1 - .../vocabularies/validation/uniqueItems.d.ts | 9 - .../vocabularies/validation/uniqueItems.js | 64 - .../validation/uniqueItems.js.map | 1 - front_end/third_party/mcp-sdk/ajv/lib/2019.ts | 81 - front_end/third_party/mcp-sdk/ajv/lib/2020.ts | 75 - .../third_party/mcp-sdk/ajv/lib/ajv.d.ts | 397 + front_end/third_party/mcp-sdk/ajv/lib/ajv.js | 506 + front_end/third_party/mcp-sdk/ajv/lib/ajv.ts | 70 - .../third_party/mcp-sdk/ajv/lib/cache.js | 26 + .../mcp-sdk/ajv/lib/compile/async.js | 90 + .../mcp-sdk/ajv/lib/compile/codegen/code.ts | 169 - .../mcp-sdk/ajv/lib/compile/codegen/index.ts | 852 - .../mcp-sdk/ajv/lib/compile/codegen/scope.ts | 215 - .../mcp-sdk/ajv/lib/compile/equal.js | 5 + .../mcp-sdk/ajv/lib/compile/error_classes.js | 34 + .../mcp-sdk/ajv/lib/compile/errors.ts | 184 - .../mcp-sdk/ajv/lib/compile/formats.js | 142 + .../mcp-sdk/ajv/lib/compile/index.js | 387 + .../mcp-sdk/ajv/lib/compile/index.ts | 324 - .../mcp-sdk/ajv/lib/compile/jtd/parse.ts | 411 - .../mcp-sdk/ajv/lib/compile/jtd/serialize.ts | 266 - .../mcp-sdk/ajv/lib/compile/jtd/types.ts | 16 - .../mcp-sdk/ajv/lib/compile/names.ts | 27 - .../mcp-sdk/ajv/lib/compile/ref_error.ts | 13 - .../mcp-sdk/ajv/lib/compile/resolve.js | 270 + .../mcp-sdk/ajv/lib/compile/resolve.ts | 149 - .../mcp-sdk/ajv/lib/compile/rules.js | 66 + .../mcp-sdk/ajv/lib/compile/rules.ts | 50 - .../mcp-sdk/ajv/lib/compile/schema_obj.js | 9 + .../mcp-sdk/ajv/lib/compile/ucs2length.js | 20 + .../mcp-sdk/ajv/lib/compile/util.js | 239 + .../mcp-sdk/ajv/lib/compile/util.ts | 213 - .../ajv/lib/compile/validate/applicability.ts | 22 - .../ajv/lib/compile/validate/boolSchema.ts | 47 - .../ajv/lib/compile/validate/dataType.ts | 230 - .../ajv/lib/compile/validate/defaults.ts | 32 - .../mcp-sdk/ajv/lib/compile/validate/index.ts | 582 - .../ajv/lib/compile/validate/keyword.ts | 171 - .../ajv/lib/compile/validate/subschema.ts | 135 - front_end/third_party/mcp-sdk/ajv/lib/core.ts | 891 - front_end/third_party/mcp-sdk/ajv/lib/data.js | 49 + .../mcp-sdk/ajv/lib/definition_schema.js | 37 + .../mcp-sdk/ajv/lib/dot/_limit.jst | 113 + .../mcp-sdk/ajv/lib/dot/_limitItems.jst | 12 + .../mcp-sdk/ajv/lib/dot/_limitLength.jst | 12 + .../mcp-sdk/ajv/lib/dot/_limitProperties.jst | 12 + .../third_party/mcp-sdk/ajv/lib/dot/allOf.jst | 32 + .../third_party/mcp-sdk/ajv/lib/dot/anyOf.jst | 46 + .../mcp-sdk/ajv/lib/dot/coerce.def | 51 + .../mcp-sdk/ajv/lib/dot/comment.jst | 9 + .../third_party/mcp-sdk/ajv/lib/dot/const.jst | 11 + .../mcp-sdk/ajv/lib/dot/contains.jst | 55 + .../mcp-sdk/ajv/lib/dot/custom.jst | 191 + .../mcp-sdk/ajv/lib/dot/defaults.def | 47 + .../mcp-sdk/ajv/lib/dot/definitions.def | 203 + .../mcp-sdk/ajv/lib/dot/dependencies.jst | 79 + .../third_party/mcp-sdk/ajv/lib/dot/enum.jst | 30 + .../mcp-sdk/ajv/lib/dot/errors.def | 194 + .../mcp-sdk/ajv/lib/dot/format.jst | 106 + .../third_party/mcp-sdk/ajv/lib/dot/if.jst | 73 + .../third_party/mcp-sdk/ajv/lib/dot/items.jst | 98 + .../mcp-sdk/ajv/lib/dot/missing.def | 39 + .../mcp-sdk/ajv/lib/dot/multipleOf.jst | 22 + .../third_party/mcp-sdk/ajv/lib/dot/not.jst | 43 + .../third_party/mcp-sdk/ajv/lib/dot/oneOf.jst | 54 + .../mcp-sdk/ajv/lib/dot/pattern.jst | 14 + .../mcp-sdk/ajv/lib/dot/properties.jst | 245 + .../mcp-sdk/ajv/lib/dot/propertyNames.jst | 52 + .../third_party/mcp-sdk/ajv/lib/dot/ref.jst | 85 + .../mcp-sdk/ajv/lib/dot/required.jst | 108 + .../mcp-sdk/ajv/lib/dot/uniqueItems.jst | 62 + .../mcp-sdk/ajv/lib/dot/validate.jst | 276 + .../mcp-sdk/ajv/lib/dotjs/README.md | 3 + .../mcp-sdk/ajv/lib/dotjs/_limit.js | 163 + .../mcp-sdk/ajv/lib/dotjs/_limitItems.js | 80 + .../mcp-sdk/ajv/lib/dotjs/_limitLength.js | 85 + .../mcp-sdk/ajv/lib/dotjs/_limitProperties.js | 80 + .../mcp-sdk/ajv/lib/dotjs/allOf.js | 42 + .../mcp-sdk/ajv/lib/dotjs/anyOf.js | 73 + .../mcp-sdk/ajv/lib/dotjs/comment.js | 14 + .../mcp-sdk/ajv/lib/dotjs/const.js | 56 + .../mcp-sdk/ajv/lib/dotjs/contains.js | 81 + .../mcp-sdk/ajv/lib/dotjs/custom.js | 228 + .../mcp-sdk/ajv/lib/dotjs/dependencies.js | 168 + .../third_party/mcp-sdk/ajv/lib/dotjs/enum.js | 66 + .../mcp-sdk/ajv/lib/dotjs/format.js | 150 + .../third_party/mcp-sdk/ajv/lib/dotjs/if.js | 103 + .../mcp-sdk/ajv/lib/dotjs/index.js | 33 + .../mcp-sdk/ajv/lib/dotjs/items.js | 140 + .../mcp-sdk/ajv/lib/dotjs/multipleOf.js | 80 + .../third_party/mcp-sdk/ajv/lib/dotjs/not.js | 84 + .../mcp-sdk/ajv/lib/dotjs/oneOf.js | 73 + .../mcp-sdk/ajv/lib/dotjs/pattern.js | 75 + .../mcp-sdk/ajv/lib/dotjs/properties.js | 335 + .../mcp-sdk/ajv/lib/dotjs/propertyNames.js | 81 + .../third_party/mcp-sdk/ajv/lib/dotjs/ref.js | 124 + .../mcp-sdk/ajv/lib/dotjs/required.js | 270 + .../mcp-sdk/ajv/lib/dotjs/uniqueItems.js | 86 + .../mcp-sdk/ajv/lib/dotjs/validate.js | 482 + front_end/third_party/mcp-sdk/ajv/lib/jtd.ts | 132 - .../third_party/mcp-sdk/ajv/lib/keyword.js | 146 + .../mcp-sdk/ajv/lib/refs/data.json | 26 +- .../ajv/lib/refs/json-schema-2019-09/index.ts | 28 - .../json-schema-2019-09/meta/applicator.json | 53 - .../json-schema-2019-09/meta/content.json | 17 - .../refs/json-schema-2019-09/meta/core.json | 57 - .../refs/json-schema-2019-09/meta/format.json | 14 - .../json-schema-2019-09/meta/meta-data.json | 37 - .../json-schema-2019-09/meta/validation.json | 90 - .../lib/refs/json-schema-2019-09/schema.json | 39 - .../ajv/lib/refs/json-schema-2020-12/index.ts | 30 - .../json-schema-2020-12/meta/applicator.json | 48 - .../json-schema-2020-12/meta/content.json | 17 - .../refs/json-schema-2020-12/meta/core.json | 51 - .../meta/format-annotation.json | 14 - .../json-schema-2020-12/meta/meta-data.json | 37 - .../json-schema-2020-12/meta/unevaluated.json | 15 - .../json-schema-2020-12/meta/validation.json | 90 - .../lib/refs/json-schema-2020-12/schema.json | 55 - .../ajv/lib/refs/json-schema-draft-04.json | 149 + .../ajv/lib/refs/json-schema-draft-06.json | 281 +- .../ajv/lib/refs/json-schema-draft-07.json | 309 +- .../ajv/lib/refs/json-schema-secure.json | 12 +- .../mcp-sdk/ajv/lib/refs/jtd-schema.ts | 130 - .../mcp-sdk/ajv/lib/runtime/equal.ts | 7 - .../mcp-sdk/ajv/lib/runtime/parseJson.ts | 177 - .../mcp-sdk/ajv/lib/runtime/quote.ts | 31 - .../mcp-sdk/ajv/lib/runtime/re2.ts | 6 - .../mcp-sdk/ajv/lib/runtime/timestamp.ts | 46 - .../mcp-sdk/ajv/lib/runtime/ucs2length.ts | 20 - .../mcp-sdk/ajv/lib/runtime/uri.ts | 6 - .../ajv/lib/runtime/validation_error.ts | 13 - .../mcp-sdk/ajv/lib/standalone/index.ts | 100 - .../mcp-sdk/ajv/lib/standalone/instance.ts | 36 - .../mcp-sdk/ajv/lib/types/index.ts | 244 - .../mcp-sdk/ajv/lib/types/json-schema.ts | 187 - .../mcp-sdk/ajv/lib/types/jtd-schema.ts | 273 - .../applicator/additionalItems.ts | 56 - .../applicator/additionalProperties.ts | 118 - .../ajv/lib/vocabularies/applicator/allOf.ts | 22 - .../ajv/lib/vocabularies/applicator/anyOf.ts | 14 - .../lib/vocabularies/applicator/contains.ts | 109 - .../vocabularies/applicator/dependencies.ts | 112 - .../applicator/dependentSchemas.ts | 11 - .../ajv/lib/vocabularies/applicator/if.ts | 80 - .../ajv/lib/vocabularies/applicator/index.ts | 53 - .../ajv/lib/vocabularies/applicator/items.ts | 59 - .../lib/vocabularies/applicator/items2020.ts | 36 - .../ajv/lib/vocabularies/applicator/not.ts | 38 - .../ajv/lib/vocabularies/applicator/oneOf.ts | 82 - .../applicator/patternProperties.ts | 91 - .../vocabularies/applicator/prefixItems.ts | 12 - .../lib/vocabularies/applicator/properties.ts | 57 - .../vocabularies/applicator/propertyNames.ts | 50 - .../lib/vocabularies/applicator/thenElse.ts | 13 - .../mcp-sdk/ajv/lib/vocabularies/code.ts | 168 - .../mcp-sdk/ajv/lib/vocabularies/core/id.ts | 10 - .../ajv/lib/vocabularies/core/index.ts | 16 - .../mcp-sdk/ajv/lib/vocabularies/core/ref.ts | 129 - .../lib/vocabularies/discriminator/index.ts | 113 - .../lib/vocabularies/discriminator/types.ts | 12 - .../mcp-sdk/ajv/lib/vocabularies/draft2020.ts | 23 - .../mcp-sdk/ajv/lib/vocabularies/draft7.ts | 17 - .../lib/vocabularies/dynamic/dynamicAnchor.ts | 31 - .../lib/vocabularies/dynamic/dynamicRef.ts | 51 - .../ajv/lib/vocabularies/dynamic/index.ts | 9 - .../vocabularies/dynamic/recursiveAnchor.ts | 14 - .../lib/vocabularies/dynamic/recursiveRef.ts | 10 - .../mcp-sdk/ajv/lib/vocabularies/errors.ts | 18 - .../ajv/lib/vocabularies/format/format.ts | 120 - .../ajv/lib/vocabularies/format/index.ts | 6 - .../ajv/lib/vocabularies/jtd/discriminator.ts | 89 - .../ajv/lib/vocabularies/jtd/elements.ts | 32 - .../mcp-sdk/ajv/lib/vocabularies/jtd/enum.ts | 45 - .../mcp-sdk/ajv/lib/vocabularies/jtd/error.ts | 23 - .../mcp-sdk/ajv/lib/vocabularies/jtd/index.ts | 37 - .../ajv/lib/vocabularies/jtd/metadata.ts | 24 - .../ajv/lib/vocabularies/jtd/nullable.ts | 21 - .../vocabularies/jtd/optionalProperties.ts | 15 - .../ajv/lib/vocabularies/jtd/properties.ts | 184 - .../mcp-sdk/ajv/lib/vocabularies/jtd/ref.ts | 76 - .../mcp-sdk/ajv/lib/vocabularies/jtd/type.ts | 75 - .../mcp-sdk/ajv/lib/vocabularies/jtd/union.ts | 12 - .../ajv/lib/vocabularies/jtd/values.ts | 58 - .../mcp-sdk/ajv/lib/vocabularies/metadata.ts | 17 - .../mcp-sdk/ajv/lib/vocabularies/next.ts | 8 - .../ajv/lib/vocabularies/unevaluated/index.ts | 7 - .../unevaluated/unevaluatedItems.ts | 47 - .../unevaluated/unevaluatedProperties.ts | 85 - .../ajv/lib/vocabularies/validation/const.ts | 28 - .../validation/dependentRequired.ts | 23 - .../ajv/lib/vocabularies/validation/enum.ts | 54 - .../ajv/lib/vocabularies/validation/index.ts | 49 - .../vocabularies/validation/limitContains.ts | 16 - .../lib/vocabularies/validation/limitItems.ts | 26 - .../vocabularies/validation/limitLength.ts | 30 - .../vocabularies/validation/limitNumber.ts | 42 - .../validation/limitProperties.ts | 26 - .../lib/vocabularies/validation/multipleOf.ts | 34 - .../lib/vocabularies/validation/pattern.ts | 28 - .../lib/vocabularies/validation/required.ts | 98 - .../vocabularies/validation/uniqueItems.ts | 79 - .../third_party/mcp-sdk/ajv/package.json | 136 +- .../mcp-sdk/ajv/scripts/.eslintrc.yml | 3 + .../third_party/mcp-sdk/ajv/scripts/bundle.js | 61 + .../mcp-sdk/ajv/scripts/compile-dots.js | 73 + .../third_party/mcp-sdk/ajv/scripts/info | 10 + .../mcp-sdk/ajv/scripts/prepare-tests | 12 + .../mcp-sdk/ajv/scripts/publish-built-version | 32 + .../mcp-sdk/ajv/scripts/travis-gh-pages | 23 + .../third_party/mcp-sdk/dist/ajv.bundle.js | 7189 ++ front_end/third_party/mcp-sdk/dist/ajv.min.js | 3 + .../third_party/mcp-sdk/dist/ajv.min.js.map | 1 + .../mcp-sdk/{ajv => }/dist/cjs/cli.d.ts | 0 .../mcp-sdk/{ajv => }/dist/cjs/cli.d.ts.map | 0 .../mcp-sdk/{ajv => }/dist/cjs/cli.js | 6 +- .../mcp-sdk/{ajv => }/dist/cjs/cli.js.map | 2 +- .../mcp-sdk/dist/cjs/client/auth.d.ts | 248 + .../mcp-sdk/dist/cjs/client/auth.d.ts.map | 1 + .../mcp-sdk/dist/cjs/client/auth.js | 718 + .../mcp-sdk/dist/cjs/client/auth.js.map | 1 + .../mcp-sdk/dist/cjs/client/index.d.ts | 1497 + .../{ajv => }/dist/cjs/client/index.d.ts.map | 2 +- .../{ajv => }/dist/cjs/client/index.js | 13 +- .../mcp-sdk/dist/cjs/client/index.js.map | 1 + .../mcp-sdk/dist/cjs/client/middleware.d.ts | 169 + .../dist/cjs/client/middleware.d.ts.map | 1 + .../mcp-sdk/dist/cjs/client/middleware.js | 256 + .../mcp-sdk/dist/cjs/client/middleware.js.map | 1 + .../{ajv => }/dist/cjs/client/sse.d.ts | 9 +- .../mcp-sdk/dist/cjs/client/sse.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/cjs/client/sse.js | 47 +- .../mcp-sdk/dist/cjs/client/sse.js.map | 1 + .../dist/esm => dist/cjs}/client/stdio.d.ts | 6 + .../{ajv => }/dist/cjs/client/stdio.d.ts.map | 2 +- .../{ajv => }/dist/cjs/client/stdio.js | 26 +- .../mcp-sdk/dist/cjs/client/stdio.js.map | 1 + .../dist/cjs/client/streamableHttp.d.ts | 33 +- .../dist/cjs/client/streamableHttp.d.ts.map | 1 + .../dist/cjs/client/streamableHttp.js | 85 +- .../dist/cjs/client/streamableHttp.js.map | 1 + .../{ajv => }/dist/cjs/client/websocket.d.ts | 0 .../dist/cjs/client/websocket.d.ts.map | 0 .../{ajv => }/dist/cjs/client/websocket.js | 0 .../dist/cjs/client/websocket.js.map | 0 .../client/multipleClientsParallel.d.ts | 0 .../client/multipleClientsParallel.d.ts.map | 0 .../client/multipleClientsParallel.js | 0 .../client/multipleClientsParallel.js.map | 0 .../client/parallelToolCallsClient.d.ts | 0 .../client/parallelToolCallsClient.d.ts.map | 0 .../client/parallelToolCallsClient.js | 0 .../client/parallelToolCallsClient.js.map | 0 .../examples/client/simpleOAuthClient.d.ts | 0 .../client/simpleOAuthClient.d.ts.map | 0 .../cjs/examples/client/simpleOAuthClient.js | 4 +- .../examples/client/simpleOAuthClient.js.map | 2 +- .../examples/client/simpleStreamableHttp.d.ts | 0 .../client/simpleStreamableHttp.d.ts.map | 0 .../examples/client/simpleStreamableHttp.js | 266 +- .../client/simpleStreamableHttp.js.map | 1 + .../streamableHttpWithSseFallbackClient.d.ts | 0 ...reamableHttpWithSseFallbackClient.d.ts.map | 0 .../streamableHttpWithSseFallbackClient.js | 0 ...streamableHttpWithSseFallbackClient.js.map | 0 .../server/demoInMemoryOAuthProvider.d.ts | 16 +- .../server/demoInMemoryOAuthProvider.d.ts.map | 1 + .../server/demoInMemoryOAuthProvider.js | 34 +- .../server/demoInMemoryOAuthProvider.js.map | 1 + .../server/jsonResponseStreamableHttp.d.ts | 0 .../jsonResponseStreamableHttp.d.ts.map | 0 .../server/jsonResponseStreamableHttp.js | 38 +- .../server/jsonResponseStreamableHttp.js.map | 1 + .../server/mcpServerOutputSchema.d.ts | 0 .../server/mcpServerOutputSchema.d.ts.map | 0 .../examples/server/mcpServerOutputSchema.js | 0 .../server/mcpServerOutputSchema.js.map | 0 .../cjs/examples/server/simpleSseServer.d.ts | 0 .../examples/server/simpleSseServer.d.ts.map | 0 .../cjs/examples/server/simpleSseServer.js | 30 +- .../examples/server/simpleSseServer.js.map | 1 + .../server/simpleStatelessStreamableHttp.d.ts | 0 .../simpleStatelessStreamableHttp.d.ts.map | 0 .../server/simpleStatelessStreamableHttp.js | 25 +- .../simpleStatelessStreamableHttp.js.map | 1 + .../examples/server/simpleStreamableHttp.d.ts | 0 .../server/simpleStreamableHttp.d.ts.map | 0 .../examples/server/simpleStreamableHttp.js | 587 + .../server/simpleStreamableHttp.js.map | 1 + .../sseAndStreamableHttpCompatibleServer.d.ts | 0 ...AndStreamableHttpCompatibleServer.d.ts.map | 0 .../sseAndStreamableHttpCompatibleServer.js | 25 +- ...seAndStreamableHttpCompatibleServer.js.map | 1 + .../standaloneSseWithGetStreamableHttp.d.ts | 0 ...tandaloneSseWithGetStreamableHttp.d.ts.map | 0 .../standaloneSseWithGetStreamableHttp.js | 6 +- .../standaloneSseWithGetStreamableHttp.js.map | 2 +- .../examples/server/toolWithSampleServer.d.ts | 2 + .../server/toolWithSampleServer.d.ts.map | 1 + .../examples/server/toolWithSampleServer.js | 49 + .../server/toolWithSampleServer.js.map | 1 + .../examples/shared/inMemoryEventStore.d.ts | 0 .../shared/inMemoryEventStore.d.ts.map | 0 .../cjs/examples/shared/inMemoryEventStore.js | 0 .../examples/shared/inMemoryEventStore.js.map | 0 .../mcp-sdk/{ajv => }/dist/cjs/inMemory.d.ts | 0 .../{ajv => }/dist/cjs/inMemory.d.ts.map | 0 .../mcp-sdk/{ajv => }/dist/cjs/inMemory.js | 0 .../{ajv => }/dist/cjs/inMemory.js.map | 0 .../mcp-sdk/{ajv => }/dist/cjs/package.json | 0 .../esm => dist/cjs}/server/auth/clients.d.ts | 2 +- .../dist/cjs/server/auth/clients.d.ts.map | 2 +- .../{ajv => }/dist/cjs/server/auth/clients.js | 0 .../dist/cjs/server/auth/clients.js.map | 0 .../esm => dist/cjs}/server/auth/errors.d.ts | 51 +- .../dist/cjs/server/auth/errors.d.ts.map | 1 + .../{ajv => }/dist/cjs/server/auth/errors.js | 106 +- .../dist/cjs/server/auth/errors.js.map | 1 + .../cjs/server/auth/handlers/authorize.d.ts | 0 .../server/auth/handlers/authorize.d.ts.map | 2 +- .../cjs/server/auth/handlers/authorize.js | 6 +- .../cjs/server/auth/handlers/authorize.js.map | 1 + .../cjs/server/auth/handlers/metadata.d.ts | 0 .../server/auth/handlers/metadata.d.ts.map | 0 .../dist/cjs/server/auth/handlers/metadata.js | 0 .../cjs/server/auth/handlers/metadata.js.map | 0 .../cjs/server/auth/handlers/register.d.ts | 8 +- .../server/auth/handlers/register.d.ts.map | 2 +- .../dist/cjs/server/auth/handlers/register.js | 10 +- .../cjs/server/auth/handlers/register.js.map | 1 + .../dist/cjs/server/auth/handlers/revoke.d.ts | 2 +- .../cjs/server/auth/handlers/revoke.d.ts.map | 2 +- .../dist/cjs/server/auth/handlers/revoke.js | 10 +- .../cjs/server/auth/handlers/revoke.js.map | 1 + .../dist/cjs/server/auth/handlers/token.d.ts | 0 .../cjs/server/auth/handlers/token.d.ts.map | 2 +- .../dist/cjs/server/auth/handlers/token.js | 12 +- .../cjs/server/auth/handlers/token.js.map | 1 + .../auth/middleware/allowedMethods.d.ts | 0 .../auth/middleware/allowedMethods.d.ts.map | 0 .../server/auth/middleware/allowedMethods.js | 0 .../auth/middleware/allowedMethods.js.map | 0 .../server/auth/middleware/bearerAuth.d.ts | 0 .../auth/middleware/bearerAuth.d.ts.map | 2 +- .../cjs/server/auth/middleware/bearerAuth.js | 8 +- .../server/auth/middleware/bearerAuth.js.map | 1 + .../server/auth/middleware/clientAuth.d.ts | 0 .../auth/middleware/clientAuth.d.ts.map | 2 +- .../cjs/server/auth/middleware/clientAuth.js | 1 - .../server/auth/middleware/clientAuth.js.map | 2 +- .../cjs}/server/auth/provider.d.ts | 5 +- .../dist/cjs/server/auth/provider.d.ts.map | 1 + .../dist/cjs/server/auth/provider.js | 0 .../dist/cjs/server/auth/provider.js.map | 0 .../server/auth/providers/proxyProvider.d.ts | 10 +- .../auth/providers/proxyProvider.d.ts.map | 1 + .../server/auth/providers/proxyProvider.js | 25 +- .../auth/providers/proxyProvider.js.map | 1 + .../dist/cjs/server/auth/router.d.ts | 0 .../dist/cjs/server/auth/router.d.ts.map | 0 .../{ajv => }/dist/cjs/server/auth/router.js | 2 +- .../dist/cjs/server/auth/router.js.map | 2 +- .../{ajv => }/dist/cjs/server/auth/types.d.ts | 5 + .../dist/cjs/server/auth/types.d.ts.map | 2 +- .../{ajv => }/dist/cjs/server/auth/types.js | 0 .../dist/cjs/server/auth/types.js.map | 0 .../dist/cjs/server/completable.d.ts | 4 +- .../dist/cjs/server/completable.d.ts.map | 1 + .../{ajv => }/dist/cjs/server/completable.js | 0 .../dist/cjs/server/completable.js.map | 1 + .../{ajv => }/dist/cjs/server/index.d.ts | 39 +- .../mcp-sdk/dist/cjs/server/index.d.ts.map | 1 + .../{ajv => }/dist/cjs/server/index.js | 79 +- .../mcp-sdk/dist/cjs/server/index.js.map | 1 + .../dist/esm => dist/cjs}/server/mcp.d.ts | 43 +- .../mcp-sdk/dist/cjs/server/mcp.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/cjs/server/mcp.js | 244 +- .../mcp-sdk/dist/cjs/server/mcp.js.map | 1 + .../dist/esm => dist/cjs}/server/sse.d.ts | 38 +- .../mcp-sdk/dist/cjs/server/sse.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/cjs/server/sse.js | 50 +- .../mcp-sdk/dist/cjs/server/sse.js.map | 1 + .../{ajv => }/dist/cjs/server/stdio.d.ts | 0 .../{ajv => }/dist/cjs/server/stdio.d.ts.map | 0 .../{ajv => }/dist/cjs/server/stdio.js | 0 .../{ajv => }/dist/cjs/server/stdio.js.map | 0 .../cjs}/server/streamableHttp.d.ts | 46 +- .../dist/cjs/server/streamableHttp.d.ts.map | 1 + .../dist/cjs/server/streamableHttp.js | 96 +- .../dist/cjs/server/streamableHttp.js.map | 1 + .../mcp-sdk/dist/cjs/shared/auth-utils.d.ts | 23 + .../dist/cjs/shared/auth-utils.d.ts.map | 1 + .../mcp-sdk/dist/cjs/shared/auth-utils.js | 48 + .../mcp-sdk/dist/cjs/shared/auth-utils.js.map | 1 + .../mcp-sdk/dist/cjs/shared/auth.d.ts | 621 + .../mcp-sdk/dist/cjs/shared/auth.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/cjs/shared/auth.js | 100 +- .../mcp-sdk/dist/cjs/shared/auth.js.map | 1 + .../dist/cjs/shared/metadataUtils.d.ts | 12 + .../dist/cjs/shared/metadataUtils.d.ts.map | 1 + .../mcp-sdk/dist/cjs/shared/metadataUtils.js | 29 + .../dist/cjs/shared/metadataUtils.js.map | 1 + .../{ajv => }/dist/cjs/shared/protocol.d.ts | 16 +- .../mcp-sdk/dist/cjs/shared/protocol.d.ts.map | 1 + .../{ajv => }/dist/cjs/shared/protocol.js | 64 +- .../mcp-sdk/dist/cjs/shared/protocol.js.map | 1 + .../{ajv => }/dist/cjs/shared/stdio.d.ts | 0 .../{ajv => }/dist/cjs/shared/stdio.d.ts.map | 0 .../{ajv => }/dist/cjs/shared/stdio.js | 0 .../{ajv => }/dist/cjs/shared/stdio.js.map | 0 .../{ajv => }/dist/cjs/shared/transport.d.ts | 15 +- .../dist/cjs/shared/transport.d.ts.map | 1 + .../{ajv => }/dist/cjs/shared/transport.js | 0 .../dist/cjs/shared/transport.js.map | 0 .../dist/cjs/shared/uriTemplate.d.ts | 0 .../dist/cjs/shared/uriTemplate.d.ts.map | 0 .../{ajv => }/dist/cjs/shared/uriTemplate.js | 0 .../dist/cjs/shared/uriTemplate.js.map | 0 .../third_party/mcp-sdk/dist/cjs/types.d.ts | 54852 ++++++++++++++++ .../mcp-sdk/dist/cjs/types.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/cjs/types.js | 345 +- .../third_party/mcp-sdk/dist/cjs/types.js.map | 1 + .../mcp-sdk/{ajv => }/dist/esm/cli.d.ts | 0 .../mcp-sdk/{ajv => }/dist/esm/cli.d.ts.map | 0 .../mcp-sdk/{ajv => }/dist/esm/cli.js | 6 +- .../mcp-sdk/{ajv => }/dist/esm/cli.js.map | 2 +- .../mcp-sdk/dist/esm/client/auth.d.ts | 248 + .../mcp-sdk/dist/esm/client/auth.d.ts.map | 1 + .../mcp-sdk/dist/esm/client/auth.js | 784 + .../mcp-sdk/dist/esm/client/auth.js.map | 1 + .../mcp-sdk/dist/esm/client/index.d.ts | 1497 + .../{ajv => }/dist/esm/client/index.d.ts.map | 2 +- .../{ajv => }/dist/esm/client/index.js | 15 +- .../mcp-sdk/dist/esm/client/index.js.map | 1 + .../mcp-sdk/dist/esm/client/middleware.d.ts | 169 + .../dist/esm/client/middleware.d.ts.map | 1 + .../mcp-sdk/dist/esm/client/middleware.js | 249 + .../mcp-sdk/dist/esm/client/middleware.js.map | 1 + .../{ajv => }/dist/esm/client/sse.d.ts | 9 +- .../mcp-sdk/dist/esm/client/sse.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/esm/client/sse.js | 48 +- .../mcp-sdk/dist/esm/client/sse.js.map | 1 + .../dist/cjs => dist/esm}/client/stdio.d.ts | 6 + .../{ajv => }/dist/esm/client/stdio.d.ts.map | 2 +- .../{ajv => }/dist/esm/client/stdio.js | 26 +- .../mcp-sdk/dist/esm/client/stdio.js.map | 1 + .../dist/esm/client/streamableHttp.d.ts | 33 +- .../dist/esm/client/streamableHttp.d.ts.map | 1 + .../dist/esm/client/streamableHttp.js | 87 +- .../dist/esm/client/streamableHttp.js.map | 1 + .../{ajv => }/dist/esm/client/websocket.d.ts | 0 .../dist/esm/client/websocket.d.ts.map | 0 .../{ajv => }/dist/esm/client/websocket.js | 0 .../dist/esm/client/websocket.js.map | 0 .../client/multipleClientsParallel.d.ts | 0 .../client/multipleClientsParallel.d.ts.map | 0 .../client/multipleClientsParallel.js | 0 .../client/multipleClientsParallel.js.map | 0 .../client/parallelToolCallsClient.d.ts | 0 .../client/parallelToolCallsClient.d.ts.map | 0 .../client/parallelToolCallsClient.js | 0 .../client/parallelToolCallsClient.js.map | 0 .../examples/client/simpleOAuthClient.d.ts | 0 .../client/simpleOAuthClient.d.ts.map | 0 .../esm/examples/client/simpleOAuthClient.js | 4 +- .../examples/client/simpleOAuthClient.js.map | 2 +- .../examples/client/simpleStreamableHttp.d.ts | 0 .../client/simpleStreamableHttp.d.ts.map | 0 .../examples/client/simpleStreamableHttp.js | 265 +- .../client/simpleStreamableHttp.js.map | 1 + .../streamableHttpWithSseFallbackClient.d.ts | 0 ...reamableHttpWithSseFallbackClient.d.ts.map | 0 .../streamableHttpWithSseFallbackClient.js | 0 ...streamableHttpWithSseFallbackClient.js.map | 0 .../server/demoInMemoryOAuthProvider.d.ts | 16 +- .../server/demoInMemoryOAuthProvider.d.ts.map | 1 + .../server/demoInMemoryOAuthProvider.js | 34 +- .../server/demoInMemoryOAuthProvider.js.map | 1 + .../server/jsonResponseStreamableHttp.d.ts | 0 .../jsonResponseStreamableHttp.d.ts.map | 0 .../server/jsonResponseStreamableHttp.js | 40 +- .../server/jsonResponseStreamableHttp.js.map | 1 + .../server/mcpServerOutputSchema.d.ts | 0 .../server/mcpServerOutputSchema.d.ts.map | 0 .../examples/server/mcpServerOutputSchema.js | 2 +- .../server/mcpServerOutputSchema.js.map | 0 .../esm/examples/server/simpleSseServer.d.ts | 0 .../examples/server/simpleSseServer.d.ts.map | 0 .../esm/examples/server/simpleSseServer.js | 32 +- .../examples/server/simpleSseServer.js.map | 1 + .../server/simpleStatelessStreamableHttp.d.ts | 0 .../simpleStatelessStreamableHttp.d.ts.map | 0 .../server/simpleStatelessStreamableHttp.js | 27 +- .../simpleStatelessStreamableHttp.js.map | 1 + .../examples/server/simpleStreamableHttp.d.ts | 0 .../server/simpleStreamableHttp.d.ts.map | 0 .../examples/server/simpleStreamableHttp.js | 582 + .../server/simpleStreamableHttp.js.map | 1 + .../sseAndStreamableHttpCompatibleServer.d.ts | 0 ...AndStreamableHttpCompatibleServer.d.ts.map | 0 .../sseAndStreamableHttpCompatibleServer.js | 27 +- ...seAndStreamableHttpCompatibleServer.js.map | 1 + .../standaloneSseWithGetStreamableHttp.d.ts | 0 ...tandaloneSseWithGetStreamableHttp.d.ts.map | 0 .../standaloneSseWithGetStreamableHttp.js | 6 +- .../standaloneSseWithGetStreamableHttp.js.map | 2 +- .../examples/server/toolWithSampleServer.d.ts | 2 + .../server/toolWithSampleServer.d.ts.map | 1 + .../examples/server/toolWithSampleServer.js | 47 + .../server/toolWithSampleServer.js.map | 1 + .../examples/shared/inMemoryEventStore.d.ts | 0 .../shared/inMemoryEventStore.d.ts.map | 0 .../esm/examples/shared/inMemoryEventStore.js | 0 .../examples/shared/inMemoryEventStore.js.map | 0 .../mcp-sdk/{ajv => }/dist/esm/inMemory.d.ts | 0 .../{ajv => }/dist/esm/inMemory.d.ts.map | 0 .../mcp-sdk/{ajv => }/dist/esm/inMemory.js | 0 .../{ajv => }/dist/esm/inMemory.js.map | 0 .../mcp-sdk/{ajv => }/dist/esm/package.json | 0 .../cjs => dist/esm}/server/auth/clients.d.ts | 2 +- .../dist/esm/server/auth/clients.d.ts.map | 2 +- .../{ajv => }/dist/esm/server/auth/clients.js | 0 .../dist/esm/server/auth/clients.js.map | 0 .../cjs => dist/esm}/server/auth/errors.d.ts | 51 +- .../dist/esm/server/auth/errors.d.ts.map | 1 + .../{ajv => }/dist/esm/server/auth/errors.js | 101 +- .../dist/esm/server/auth/errors.js.map | 1 + .../esm/server/auth/handlers/authorize.d.ts | 0 .../server/auth/handlers/authorize.d.ts.map | 2 +- .../esm/server/auth/handlers/authorize.js | 8 +- .../esm/server/auth/handlers/authorize.js.map | 1 + .../esm/server/auth/handlers/metadata.d.ts | 0 .../server/auth/handlers/metadata.d.ts.map | 0 .../dist/esm/server/auth/handlers/metadata.js | 0 .../esm/server/auth/handlers/metadata.js.map | 0 .../esm/server/auth/handlers/register.d.ts | 8 +- .../server/auth/handlers/register.d.ts.map | 2 +- .../dist/esm/server/auth/handlers/register.js | 10 +- .../esm/server/auth/handlers/register.js.map | 1 + .../dist/esm/server/auth/handlers/revoke.d.ts | 2 +- .../esm/server/auth/handlers/revoke.d.ts.map | 2 +- .../dist/esm/server/auth/handlers/revoke.js | 12 +- .../esm/server/auth/handlers/revoke.js.map | 1 + .../dist/esm/server/auth/handlers/token.d.ts | 0 .../esm/server/auth/handlers/token.d.ts.map | 2 +- .../dist/esm/server/auth/handlers/token.js | 14 +- .../esm/server/auth/handlers/token.js.map | 1 + .../auth/middleware/allowedMethods.d.ts | 0 .../auth/middleware/allowedMethods.d.ts.map | 0 .../server/auth/middleware/allowedMethods.js | 0 .../auth/middleware/allowedMethods.js.map | 0 .../server/auth/middleware/bearerAuth.d.ts | 0 .../auth/middleware/bearerAuth.d.ts.map | 2 +- .../esm/server/auth/middleware/bearerAuth.js | 8 +- .../server/auth/middleware/bearerAuth.js.map | 1 + .../server/auth/middleware/clientAuth.d.ts | 0 .../auth/middleware/clientAuth.d.ts.map | 2 +- .../esm/server/auth/middleware/clientAuth.js | 3 +- .../server/auth/middleware/clientAuth.js.map | 2 +- .../esm}/server/auth/provider.d.ts | 5 +- .../dist/esm/server/auth/provider.d.ts.map | 1 + .../dist/esm/server/auth/provider.js | 0 .../dist/esm/server/auth/provider.js.map | 0 .../server/auth/providers/proxyProvider.d.ts | 10 +- .../auth/providers/proxyProvider.d.ts.map | 1 + .../server/auth/providers/proxyProvider.js | 25 +- .../auth/providers/proxyProvider.js.map | 1 + .../dist/esm/server/auth/router.d.ts | 0 .../dist/esm/server/auth/router.d.ts.map | 0 .../{ajv => }/dist/esm/server/auth/router.js | 2 +- .../dist/esm/server/auth/router.js.map | 2 +- .../{ajv => }/dist/esm/server/auth/types.d.ts | 5 + .../dist/esm/server/auth/types.d.ts.map | 2 +- .../{ajv => }/dist/esm/server/auth/types.js | 0 .../dist/esm/server/auth/types.js.map | 0 .../dist/esm/server/completable.d.ts | 4 +- .../dist/esm/server/completable.d.ts.map | 1 + .../{ajv => }/dist/esm/server/completable.js | 2 +- .../dist/esm/server/completable.js.map | 1 + .../{ajv => }/dist/esm/server/index.d.ts | 39 +- .../mcp-sdk/dist/esm/server/index.d.ts.map | 1 + .../{ajv => }/dist/esm/server/index.js | 78 +- .../mcp-sdk/dist/esm/server/index.js.map | 1 + .../dist/cjs => dist/esm}/server/mcp.d.ts | 43 +- .../mcp-sdk/dist/esm/server/mcp.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/esm/server/mcp.js | 246 +- .../mcp-sdk/dist/esm/server/mcp.js.map | 1 + .../dist/cjs => dist/esm}/server/sse.d.ts | 38 +- .../mcp-sdk/dist/esm/server/sse.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/esm/server/sse.js | 50 +- .../mcp-sdk/dist/esm/server/sse.js.map | 1 + .../{ajv => }/dist/esm/server/stdio.d.ts | 0 .../{ajv => }/dist/esm/server/stdio.d.ts.map | 0 .../{ajv => }/dist/esm/server/stdio.js | 0 .../{ajv => }/dist/esm/server/stdio.js.map | 0 .../esm}/server/streamableHttp.d.ts | 46 +- .../dist/esm/server/streamableHttp.d.ts.map | 1 + .../dist/esm/server/streamableHttp.js | 98 +- .../dist/esm/server/streamableHttp.js.map | 1 + .../mcp-sdk/dist/esm/shared/auth-utils.d.ts | 23 + .../dist/esm/shared/auth-utils.d.ts.map | 1 + .../mcp-sdk/dist/esm/shared/auth-utils.js | 44 + .../mcp-sdk/dist/esm/shared/auth-utils.js.map | 1 + .../mcp-sdk/dist/esm/shared/auth.d.ts | 621 + .../mcp-sdk/dist/esm/shared/auth.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/esm/shared/auth.js | 100 +- .../mcp-sdk/dist/esm/shared/auth.js.map | 1 + .../dist/esm/shared/metadataUtils.d.ts | 12 + .../dist/esm/shared/metadataUtils.d.ts.map | 1 + .../mcp-sdk/dist/esm/shared/metadataUtils.js | 26 + .../dist/esm/shared/metadataUtils.js.map | 1 + .../{ajv => }/dist/esm/shared/protocol.d.ts | 16 +- .../mcp-sdk/dist/esm/shared/protocol.d.ts.map | 1 + .../{ajv => }/dist/esm/shared/protocol.js | 64 +- .../mcp-sdk/dist/esm/shared/protocol.js.map | 1 + .../{ajv => }/dist/esm/shared/stdio.d.ts | 0 .../{ajv => }/dist/esm/shared/stdio.d.ts.map | 0 .../{ajv => }/dist/esm/shared/stdio.js | 0 .../{ajv => }/dist/esm/shared/stdio.js.map | 0 .../{ajv => }/dist/esm/shared/transport.d.ts | 15 +- .../dist/esm/shared/transport.d.ts.map | 1 + .../{ajv => }/dist/esm/shared/transport.js | 0 .../dist/esm/shared/transport.js.map | 0 .../dist/esm/shared/uriTemplate.d.ts | 0 .../dist/esm/shared/uriTemplate.d.ts.map | 0 .../{ajv => }/dist/esm/shared/uriTemplate.js | 0 .../dist/esm/shared/uriTemplate.js.map | 0 .../third_party/mcp-sdk/dist/esm/types.d.ts | 54852 ++++++++++++++++ .../mcp-sdk/dist/esm/types.d.ts.map | 1 + .../mcp-sdk/{ajv => }/dist/esm/types.js | 342 +- .../third_party/mcp-sdk/dist/esm/types.js.map | 1 + .../third_party/mcp-sdk/dist/zod/zod-esm.js | 8 + front_end/third_party/mcp-sdk/lib/ajv.d.ts | 397 + front_end/third_party/mcp-sdk/lib/ajv.js | 506 + front_end/third_party/mcp-sdk/lib/cache.js | 26 + .../third_party/mcp-sdk/lib/compile/async.js | 90 + .../third_party/mcp-sdk/lib/compile/equal.js | 5 + .../mcp-sdk/lib/compile/error_classes.js | 34 + .../mcp-sdk/lib/compile/formats.js | 142 + .../third_party/mcp-sdk/lib/compile/index.js | 387 + .../mcp-sdk/lib/compile/resolve.js | 270 + .../third_party/mcp-sdk/lib/compile/rules.js | 66 + .../mcp-sdk/lib/compile/schema_obj.js | 9 + .../mcp-sdk/lib/compile/ucs2length.js | 20 + .../third_party/mcp-sdk/lib/compile/util.js | 239 + front_end/third_party/mcp-sdk/lib/data.js | 49 + .../mcp-sdk/lib/definition_schema.js | 37 + .../third_party/mcp-sdk/lib/dot/_limit.jst | 113 + .../mcp-sdk/lib/dot/_limitItems.jst | 12 + .../mcp-sdk/lib/dot/_limitLength.jst | 12 + .../mcp-sdk/lib/dot/_limitProperties.jst | 12 + .../third_party/mcp-sdk/lib/dot/allOf.jst | 32 + .../third_party/mcp-sdk/lib/dot/anyOf.jst | 46 + .../third_party/mcp-sdk/lib/dot/coerce.def | 51 + .../third_party/mcp-sdk/lib/dot/comment.jst | 9 + .../third_party/mcp-sdk/lib/dot/const.jst | 11 + .../third_party/mcp-sdk/lib/dot/contains.jst | 55 + .../third_party/mcp-sdk/lib/dot/custom.jst | 191 + .../third_party/mcp-sdk/lib/dot/defaults.def | 47 + .../mcp-sdk/lib/dot/definitions.def | 203 + .../mcp-sdk/lib/dot/dependencies.jst | 79 + .../third_party/mcp-sdk/lib/dot/enum.jst | 30 + .../third_party/mcp-sdk/lib/dot/errors.def | 194 + .../third_party/mcp-sdk/lib/dot/format.jst | 106 + front_end/third_party/mcp-sdk/lib/dot/if.jst | 73 + .../third_party/mcp-sdk/lib/dot/items.jst | 98 + .../third_party/mcp-sdk/lib/dot/missing.def | 39 + .../mcp-sdk/lib/dot/multipleOf.jst | 22 + front_end/third_party/mcp-sdk/lib/dot/not.jst | 43 + .../third_party/mcp-sdk/lib/dot/oneOf.jst | 54 + .../third_party/mcp-sdk/lib/dot/pattern.jst | 14 + .../mcp-sdk/lib/dot/properties.jst | 245 + .../mcp-sdk/lib/dot/propertyNames.jst | 52 + front_end/third_party/mcp-sdk/lib/dot/ref.jst | 85 + .../third_party/mcp-sdk/lib/dot/required.jst | 108 + .../mcp-sdk/lib/dot/uniqueItems.jst | 62 + .../third_party/mcp-sdk/lib/dot/validate.jst | 276 + .../third_party/mcp-sdk/lib/dotjs/README.md | 3 + .../third_party/mcp-sdk/lib/dotjs/_limit.js | 163 + .../mcp-sdk/lib/dotjs/_limitItems.js | 80 + .../mcp-sdk/lib/dotjs/_limitLength.js | 85 + .../mcp-sdk/lib/dotjs/_limitProperties.js | 80 + .../third_party/mcp-sdk/lib/dotjs/allOf.js | 42 + .../third_party/mcp-sdk/lib/dotjs/anyOf.js | 73 + .../third_party/mcp-sdk/lib/dotjs/comment.js | 14 + .../third_party/mcp-sdk/lib/dotjs/const.js | 56 + .../third_party/mcp-sdk/lib/dotjs/contains.js | 81 + .../third_party/mcp-sdk/lib/dotjs/custom.js | 228 + .../mcp-sdk/lib/dotjs/dependencies.js | 168 + .../third_party/mcp-sdk/lib/dotjs/enum.js | 66 + .../third_party/mcp-sdk/lib/dotjs/format.js | 150 + front_end/third_party/mcp-sdk/lib/dotjs/if.js | 103 + .../third_party/mcp-sdk/lib/dotjs/index.js | 33 + .../third_party/mcp-sdk/lib/dotjs/items.js | 140 + .../mcp-sdk/lib/dotjs/multipleOf.js | 80 + .../third_party/mcp-sdk/lib/dotjs/not.js | 84 + .../third_party/mcp-sdk/lib/dotjs/oneOf.js | 73 + .../third_party/mcp-sdk/lib/dotjs/pattern.js | 75 + .../mcp-sdk/lib/dotjs/properties.js | 335 + .../mcp-sdk/lib/dotjs/propertyNames.js | 81 + .../third_party/mcp-sdk/lib/dotjs/ref.js | 124 + .../third_party/mcp-sdk/lib/dotjs/required.js | 270 + .../mcp-sdk/lib/dotjs/uniqueItems.js | 86 + .../third_party/mcp-sdk/lib/dotjs/validate.js | 482 + front_end/third_party/mcp-sdk/lib/keyword.js | 146 + .../third_party/mcp-sdk/lib/refs/data.json | 17 + .../lib/refs/json-schema-draft-04.json | 149 + .../lib/refs/json-schema-draft-06.json | 154 + .../lib/refs/json-schema-draft-07.json | 168 + .../dist => lib}/refs/json-schema-secure.json | 12 +- front_end/third_party/mcp-sdk/mcp-sdk-v2.ts | 979 + front_end/third_party/mcp-sdk/mcp-sdk.ts | 478 +- front_end/third_party/mcp-sdk/package.json | 106 + .../third_party/mcp-sdk/scripts/.eslintrc.yml | 3 + .../third_party/mcp-sdk/scripts/bundle.js | 61 + .../mcp-sdk/scripts/compile-dots.js | 73 + front_end/third_party/mcp-sdk/scripts/info | 10 + .../third_party/mcp-sdk/scripts/prepare-tests | 12 + .../mcp-sdk/scripts/publish-built-version | 32 + .../mcp-sdk/scripts/travis-gh-pages | 23 + .../third_party/mcp-sdk/zod/dist/cli.d.ts | 2 - .../third_party/mcp-sdk/zod/dist/cli.d.ts.map | 1 - front_end/third_party/mcp-sdk/zod/dist/cli.js | 129 - .../third_party/mcp-sdk/zod/dist/cli.js.map | 1 - .../mcp-sdk/zod/dist/client/index.d.ts | 773 - .../mcp-sdk/zod/dist/client/index.d.ts.map | 1 - .../mcp-sdk/zod/dist/client/index.js | 206 - .../mcp-sdk/zod/dist/client/index.js.map | 1 - .../mcp-sdk/zod/dist/client/index.test.d.ts | 2 - .../zod/dist/client/index.test.d.ts.map | 1 - .../mcp-sdk/zod/dist/client/index.test.js | 393 - .../mcp-sdk/zod/dist/client/index.test.js.map | 1 - .../mcp-sdk/zod/dist/client/sse.d.ts | 22 - .../mcp-sdk/zod/dist/client/sse.d.ts.map | 1 - .../mcp-sdk/zod/dist/client/sse.js | 91 - .../mcp-sdk/zod/dist/client/sse.js.map | 1 - .../mcp-sdk/zod/dist/client/stdio.d.ts | 63 - .../mcp-sdk/zod/dist/client/stdio.d.ts.map | 1 - .../mcp-sdk/zod/dist/client/stdio.js | 148 - .../mcp-sdk/zod/dist/client/stdio.js.map | 1 - .../mcp-sdk/zod/dist/client/stdio.test.d.ts | 2 - .../zod/dist/client/stdio.test.d.ts.map | 1 - .../mcp-sdk/zod/dist/client/stdio.test.js | 51 - .../mcp-sdk/zod/dist/client/stdio.test.js.map | 1 - .../mcp-sdk/zod/dist/client/websocket.d.ts | 17 - .../zod/dist/client/websocket.d.ts.map | 1 - .../mcp-sdk/zod/dist/client/websocket.js | 61 - .../mcp-sdk/zod/dist/client/websocket.js.map | 1 - .../mcp-sdk/zod/dist/inMemory.d.ts | 20 - .../mcp-sdk/zod/dist/inMemory.d.ts.map | 1 - .../third_party/mcp-sdk/zod/dist/inMemory.js | 47 - .../mcp-sdk/zod/dist/inMemory.js.map | 1 - .../mcp-sdk/zod/dist/inMemory.test.d.ts | 2 - .../mcp-sdk/zod/dist/inMemory.test.d.ts.map | 1 - .../mcp-sdk/zod/dist/inMemory.test.js | 74 - .../mcp-sdk/zod/dist/inMemory.test.js.map | 1 - .../mcp-sdk/zod/dist/server/index.d.ts | 112 - .../mcp-sdk/zod/dist/server/index.d.ts.map | 1 - .../mcp-sdk/zod/dist/server/index.js | 182 - .../mcp-sdk/zod/dist/server/index.js.map | 1 - .../mcp-sdk/zod/dist/server/index.test.d.ts | 2 - .../zod/dist/server/index.test.d.ts.map | 1 - .../mcp-sdk/zod/dist/server/index.test.js | 408 - .../mcp-sdk/zod/dist/server/index.test.js.map | 1 - .../mcp-sdk/zod/dist/server/sse.d.ts | 46 - .../mcp-sdk/zod/dist/server/sse.d.ts.map | 1 - .../mcp-sdk/zod/dist/server/sse.js | 116 - .../mcp-sdk/zod/dist/server/sse.js.map | 1 - .../mcp-sdk/zod/dist/server/stdio.d.ts | 28 - .../mcp-sdk/zod/dist/server/stdio.d.ts.map | 1 - .../mcp-sdk/zod/dist/server/stdio.js | 69 - .../mcp-sdk/zod/dist/server/stdio.js.map | 1 - .../mcp-sdk/zod/dist/server/stdio.test.d.ts | 2 - .../zod/dist/server/stdio.test.d.ts.map | 1 - .../mcp-sdk/zod/dist/server/stdio.test.js | 87 - .../mcp-sdk/zod/dist/server/stdio.test.js.map | 1 - .../mcp-sdk/zod/dist/shared/protocol.d.ts | 157 - .../mcp-sdk/zod/dist/shared/protocol.d.ts.map | 1 - .../mcp-sdk/zod/dist/shared/protocol.js | 297 - .../mcp-sdk/zod/dist/shared/protocol.js.map | 1 - .../mcp-sdk/zod/dist/shared/stdio.d.ts | 13 - .../mcp-sdk/zod/dist/shared/stdio.d.ts.map | 1 - .../mcp-sdk/zod/dist/shared/stdio.js | 31 - .../mcp-sdk/zod/dist/shared/stdio.js.map | 1 - .../mcp-sdk/zod/dist/shared/stdio.test.d.ts | 2 - .../zod/dist/shared/stdio.test.d.ts.map | 1 - .../mcp-sdk/zod/dist/shared/stdio.test.js | 27 - .../mcp-sdk/zod/dist/shared/stdio.test.js.map | 1 - .../mcp-sdk/zod/dist/shared/transport.d.ts | 39 - .../zod/dist/shared/transport.d.ts.map | 1 - .../mcp-sdk/zod/dist/shared/transport.js | 2 - .../mcp-sdk/zod/dist/shared/transport.js.map | 1 - .../third_party/mcp-sdk/zod/dist/types.d.ts | 26425 -------- .../mcp-sdk/zod/dist/types.d.ts.map | 1 - .../third_party/mcp-sdk/zod/dist/types.js | 997 - .../third_party/mcp-sdk/zod/dist/types.js.map | 1 - front_end/third_party/mcp-sdk/zod/zod-esm.js | 3 + 1335 files changed, 168196 insertions(+), 125378 deletions(-) create mode 100644 front_end/Images/src/asana-mcp.svg create mode 100644 front_end/Images/src/atlassian-mcp.svg create mode 100644 front_end/Images/src/github-mcp.svg create mode 100644 front_end/Images/src/google-drive-mcp.svg create mode 100644 front_end/Images/src/google-sheets-mcp.svg create mode 100644 front_end/Images/src/huggingface-mcp.svg create mode 100644 front_end/Images/src/intercom-mcp.svg create mode 100644 front_end/Images/src/invideo-mcp.svg create mode 100644 front_end/Images/src/linear-mcp.svg create mode 100644 front_end/Images/src/notion-mcp.svg create mode 100644 front_end/Images/src/sentry-mcp.svg create mode 100644 front_end/Images/src/slack-mcp.svg create mode 100644 front_end/Images/src/socket-mcp.svg create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/ActionVerificationAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/AgentVersion.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/ClickActionAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/ContentWriterAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/DirectURLNavigatorAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/EcommerceProductInfoAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/FormFillActionAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/HoverActionAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/KeyboardInputActionAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/ResearchAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/ScrollActionAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/SearchAgent.ts create mode 100644 front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.ts create mode 100644 front_end/panels/ai_chat/core/AgentDescriptorRegistry.ts rename front_end/panels/ai_chat/core/{ => __tests__}/AgentNodes.test.ts (79%) create mode 100644 front_end/panels/ai_chat/core/__tests__/ToolExecutorNode.test.ts create mode 100644 front_end/panels/ai_chat/core/__tests__/ToolNameMap.test.ts rename front_end/panels/ai_chat/core/{ => __tests__}/ToolNameMapping.test.ts (87%) rename front_end/panels/ai_chat/core/{ => __tests__}/ToolSurfaceProvider.test.ts (93%) create mode 100644 front_end/panels/ai_chat/docs/MCP_OAuth_Implementation_Plan.md rename front_end/panels/ai_chat/mcp/{ => __tests__}/MCPClientSDK.test.ts (93%) create mode 100644 front_end/panels/ai_chat/mcp/__tests__/MCPConfig.test.ts create mode 100644 front_end/panels/ai_chat/mcp/__tests__/MCPIntegration.test.ts create mode 100644 front_end/panels/ai_chat/mcp/__tests__/MCPToolRegistration.test.ts rename front_end/panels/ai_chat/ui/{ => __tests__}/AIChatPanel.test.ts (95%) create mode 100644 front_end/panels/ai_chat/ui/mcp/MCPConnectionsDialog.ts create mode 100644 front_end/panels/ai_chat/ui/mcp/MCPConnectorsCatalogDialog.ts create mode 100644 front_end/panels/ai_chat/ui/mcp/mcpConnectorsCatalogDialog.css create mode 100644 front_end/third_party/mcp-sdk/.tonic_example.js create mode 100644 front_end/third_party/mcp-sdk/MCPOAuthProvider.ts create mode 100644 front_end/third_party/mcp-sdk/README.md delete mode 100644 front_end/third_party/mcp-sdk/ajv/.runkit_example.js create mode 100644 front_end/third_party/mcp-sdk/ajv/.tonic_example.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/2019.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/2019.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/2019.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/2020.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/2020.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/2020.js.map create mode 100644 front_end/third_party/mcp-sdk/ajv/dist/ajv-esm.js create mode 100644 front_end/third_party/mcp-sdk/ajv/dist/ajv.bundle.js mode change 100644 => 120000 front_end/third_party/mcp-sdk/ajv/dist/ajv.d.ts mode change 100644 => 120000 front_end/third_party/mcp-sdk/ajv/dist/ajv.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/ajv.js.map create mode 100644 front_end/third_party/mcp-sdk/ajv/dist/ajv.min.js create mode 100644 front_end/third_party/mcp-sdk/ajv/dist/ajv.min.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/auth.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/auth.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/auth.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/auth.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/sse.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/sse.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/stdio.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/streamableHttp.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/client/streamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/examples/client/simpleStreamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/examples/server/demoInMemoryOAuthProvider.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/examples/server/demoInMemoryOAuthProvider.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/examples/server/jsonResponseStreamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/examples/server/simpleSseServer.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/examples/server/simpleStatelessStreamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/examples/server/simpleStreamableHttp.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/examples/server/simpleStreamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/examples/server/sseAndStreamableHttpCompatibleServer.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/errors.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/errors.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/handlers/authorize.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/handlers/register.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/handlers/revoke.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/handlers/token.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/middleware/bearerAuth.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/provider.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/providers/proxyProvider.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/auth/providers/proxyProvider.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/completable.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/completable.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/index.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/mcp.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/mcp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/sse.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/sse.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/streamableHttp.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/server/streamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/shared/auth.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/shared/auth.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/shared/auth.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/shared/protocol.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/shared/protocol.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/shared/transport.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/types.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/types.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/cjs/types.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/codegen/code.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/codegen/code.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/codegen/code.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/codegen/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/codegen/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/codegen/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/codegen/scope.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/codegen/scope.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/codegen/scope.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/errors.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/errors.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/errors.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/jtd/parse.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/jtd/parse.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/jtd/parse.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/jtd/serialize.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/jtd/serialize.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/jtd/serialize.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/jtd/types.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/jtd/types.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/jtd/types.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/names.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/names.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/names.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/ref_error.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/ref_error.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/ref_error.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/resolve.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/resolve.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/resolve.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/rules.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/rules.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/rules.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/util.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/util.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/util.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/applicability.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/applicability.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/applicability.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/boolSchema.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/boolSchema.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/boolSchema.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/dataType.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/dataType.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/dataType.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/defaults.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/defaults.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/defaults.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/keyword.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/keyword.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/keyword.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/subschema.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/subschema.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/compile/validate/subschema.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/core.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/core.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/core.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/auth.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/auth.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/auth.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/auth.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/sse.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/sse.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/stdio.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/streamableHttp.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/client/streamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/examples/client/simpleStreamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/examples/server/demoInMemoryOAuthProvider.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/examples/server/demoInMemoryOAuthProvider.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/examples/server/jsonResponseStreamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/examples/server/simpleSseServer.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/examples/server/simpleStatelessStreamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/examples/server/simpleStreamableHttp.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/examples/server/simpleStreamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/examples/server/sseAndStreamableHttpCompatibleServer.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/errors.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/errors.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/handlers/authorize.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/handlers/register.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/handlers/revoke.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/handlers/token.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/middleware/bearerAuth.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/provider.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/providers/proxyProvider.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/auth/providers/proxyProvider.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/completable.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/completable.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/index.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/mcp.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/mcp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/sse.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/sse.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/streamableHttp.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/server/streamableHttp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/shared/auth.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/shared/auth.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/shared/auth.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/shared/protocol.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/shared/protocol.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/shared/transport.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/types.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/types.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/esm/types.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/jtd.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/jtd.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/jtd.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/data.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/meta/applicator.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/meta/content.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/meta/core.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/meta/format.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/meta/meta-data.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/meta/validation.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2019-09/schema.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/meta/applicator.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/meta/content.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/meta/core.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/meta/format-annotation.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/meta/meta-data.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/meta/unevaluated.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/meta/validation.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-2020-12/schema.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-draft-06.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/json-schema-draft-07.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/jtd-schema.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/jtd-schema.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/refs/jtd-schema.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/equal.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/equal.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/equal.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/parseJson.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/parseJson.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/parseJson.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/quote.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/quote.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/quote.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/re2.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/re2.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/re2.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/timestamp.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/timestamp.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/timestamp.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/ucs2length.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/ucs2length.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/ucs2length.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/uri.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/uri.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/uri.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/validation_error.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/validation_error.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/runtime/validation_error.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/standalone/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/standalone/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/standalone/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/standalone/instance.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/standalone/instance.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/standalone/instance.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/types/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/types/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/types/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/types/json-schema.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/types/json-schema.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/types/json-schema.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/types/jtd-schema.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/types/jtd-schema.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/types/jtd-schema.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/additionalItems.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/additionalItems.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/additionalItems.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/additionalProperties.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/additionalProperties.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/additionalProperties.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/allOf.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/allOf.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/allOf.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/anyOf.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/anyOf.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/anyOf.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/contains.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/contains.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/contains.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/dependencies.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/dependencies.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/dependencies.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/dependentSchemas.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/dependentSchemas.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/dependentSchemas.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/if.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/if.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/if.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/items.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/items.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/items.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/items2020.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/items2020.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/items2020.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/not.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/not.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/not.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/oneOf.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/oneOf.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/oneOf.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/patternProperties.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/patternProperties.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/patternProperties.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/prefixItems.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/prefixItems.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/prefixItems.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/properties.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/properties.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/properties.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/propertyNames.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/propertyNames.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/propertyNames.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/thenElse.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/thenElse.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/applicator/thenElse.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/code.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/code.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/code.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/core/id.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/core/id.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/core/id.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/core/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/core/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/core/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/core/ref.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/core/ref.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/core/ref.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/discriminator/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/discriminator/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/discriminator/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/discriminator/types.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/discriminator/types.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/discriminator/types.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/draft2020.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/draft2020.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/draft2020.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/draft7.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/draft7.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/draft7.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/dynamicAnchor.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/dynamicAnchor.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/dynamicAnchor.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/dynamicRef.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/dynamicRef.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/dynamicRef.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/recursiveAnchor.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/recursiveAnchor.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/recursiveAnchor.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/recursiveRef.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/recursiveRef.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/dynamic/recursiveRef.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/errors.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/errors.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/errors.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/format/format.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/format/format.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/format/format.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/format/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/format/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/format/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/discriminator.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/discriminator.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/discriminator.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/elements.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/elements.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/elements.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/enum.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/enum.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/enum.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/error.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/error.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/error.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/metadata.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/metadata.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/metadata.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/nullable.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/nullable.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/nullable.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/optionalProperties.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/optionalProperties.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/optionalProperties.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/properties.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/properties.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/properties.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/ref.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/ref.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/ref.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/type.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/type.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/type.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/union.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/union.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/union.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/values.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/values.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/jtd/values.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/metadata.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/metadata.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/metadata.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/next.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/next.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/next.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/unevaluated/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/unevaluated/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/unevaluated/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/unevaluated/unevaluatedItems.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/unevaluated/unevaluatedItems.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/unevaluated/unevaluatedItems.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/unevaluated/unevaluatedProperties.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/unevaluated/unevaluatedProperties.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/unevaluated/unevaluatedProperties.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/const.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/const.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/const.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/dependentRequired.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/dependentRequired.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/dependentRequired.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/enum.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/enum.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/enum.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitContains.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitContains.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitContains.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitItems.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitItems.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitItems.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitLength.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitLength.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitLength.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitNumber.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitNumber.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitNumber.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitProperties.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitProperties.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/limitProperties.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/multipleOf.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/multipleOf.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/multipleOf.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/pattern.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/pattern.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/pattern.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/required.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/required.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/required.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/uniqueItems.d.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/uniqueItems.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/dist/vocabularies/validation/uniqueItems.js.map delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/2019.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/2020.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/ajv.d.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/ajv.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/ajv.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/cache.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/async.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/codegen/code.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/codegen/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/codegen/scope.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/equal.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/error_classes.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/errors.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/formats.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/index.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/jtd/parse.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/jtd/serialize.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/jtd/types.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/names.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/ref_error.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/resolve.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/resolve.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/rules.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/rules.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/schema_obj.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/ucs2length.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/util.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/util.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/validate/applicability.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/validate/boolSchema.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/validate/dataType.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/validate/defaults.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/validate/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/validate/keyword.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/compile/validate/subschema.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/core.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/data.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/definition_schema.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/_limit.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/_limitItems.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/_limitLength.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/_limitProperties.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/allOf.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/anyOf.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/coerce.def create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/comment.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/const.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/contains.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/custom.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/defaults.def create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/definitions.def create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/dependencies.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/enum.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/errors.def create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/format.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/if.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/items.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/missing.def create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/multipleOf.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/not.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/oneOf.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/pattern.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/properties.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/propertyNames.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/ref.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/required.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/uniqueItems.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dot/validate.jst create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/README.md create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/_limit.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/_limitItems.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/_limitLength.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/_limitProperties.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/allOf.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/anyOf.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/comment.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/const.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/contains.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/custom.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/dependencies.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/enum.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/format.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/if.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/index.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/items.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/multipleOf.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/not.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/oneOf.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/pattern.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/properties.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/propertyNames.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/ref.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/required.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/uniqueItems.js create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/dotjs/validate.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/jtd.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/keyword.js delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2019-09/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2019-09/meta/applicator.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2019-09/meta/content.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2019-09/meta/core.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2019-09/meta/format.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2019-09/meta/meta-data.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2019-09/meta/validation.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2019-09/schema.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2020-12/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2020-12/meta/applicator.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2020-12/meta/content.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2020-12/meta/core.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2020-12/meta/format-annotation.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2020-12/meta/meta-data.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2020-12/meta/unevaluated.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2020-12/meta/validation.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-2020-12/schema.json create mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/json-schema-draft-04.json delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/refs/jtd-schema.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/runtime/equal.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/runtime/parseJson.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/runtime/quote.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/runtime/re2.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/runtime/timestamp.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/runtime/ucs2length.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/runtime/uri.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/runtime/validation_error.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/standalone/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/standalone/instance.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/types/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/types/json-schema.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/types/jtd-schema.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/additionalItems.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/additionalProperties.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/allOf.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/anyOf.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/contains.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/dependencies.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/dependentSchemas.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/if.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/items.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/items2020.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/not.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/oneOf.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/patternProperties.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/prefixItems.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/properties.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/propertyNames.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/applicator/thenElse.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/code.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/core/id.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/core/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/core/ref.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/discriminator/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/discriminator/types.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/draft2020.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/draft7.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/dynamic/dynamicAnchor.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/dynamic/dynamicRef.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/dynamic/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/dynamic/recursiveAnchor.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/dynamic/recursiveRef.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/errors.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/format/format.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/format/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/discriminator.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/elements.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/enum.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/error.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/metadata.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/nullable.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/optionalProperties.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/properties.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/ref.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/type.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/union.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/jtd/values.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/metadata.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/next.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/unevaluated/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/unevaluated/unevaluatedItems.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/unevaluated/unevaluatedProperties.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/const.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/dependentRequired.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/enum.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/index.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/limitContains.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/limitItems.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/limitLength.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/limitNumber.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/limitProperties.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/multipleOf.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/pattern.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/required.ts delete mode 100644 front_end/third_party/mcp-sdk/ajv/lib/vocabularies/validation/uniqueItems.ts create mode 100644 front_end/third_party/mcp-sdk/ajv/scripts/.eslintrc.yml create mode 100644 front_end/third_party/mcp-sdk/ajv/scripts/bundle.js create mode 100644 front_end/third_party/mcp-sdk/ajv/scripts/compile-dots.js create mode 100644 front_end/third_party/mcp-sdk/ajv/scripts/info create mode 100644 front_end/third_party/mcp-sdk/ajv/scripts/prepare-tests create mode 100644 front_end/third_party/mcp-sdk/ajv/scripts/publish-built-version create mode 100644 front_end/third_party/mcp-sdk/ajv/scripts/travis-gh-pages create mode 100644 front_end/third_party/mcp-sdk/dist/ajv.bundle.js create mode 100644 front_end/third_party/mcp-sdk/dist/ajv.min.js create mode 100644 front_end/third_party/mcp-sdk/dist/ajv.min.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/cli.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/cli.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/cli.js (96%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/cli.js.map (63%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/auth.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/auth.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/auth.js create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/auth.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/index.d.ts rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/index.d.ts.map (55%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/index.js (95%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/index.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/middleware.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/middleware.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/middleware.js create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/middleware.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/sse.d.ts (92%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/sse.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/sse.js (79%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/sse.js.map rename front_end/third_party/mcp-sdk/{ajv/dist/esm => dist/cjs}/client/stdio.d.ts (93%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/stdio.d.ts.map (76%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/stdio.js (86%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/stdio.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/streamableHttp.d.ts (83%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/streamableHttp.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/streamableHttp.js (85%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/client/streamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/websocket.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/websocket.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/websocket.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/client/websocket.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/multipleClientsParallel.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/multipleClientsParallel.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/multipleClientsParallel.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/multipleClientsParallel.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/parallelToolCallsClient.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/parallelToolCallsClient.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/parallelToolCallsClient.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/parallelToolCallsClient.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/simpleOAuthClient.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/simpleOAuthClient.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/simpleOAuthClient.js (99%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/simpleOAuthClient.js.map (59%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/simpleStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/simpleStreamableHttp.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/simpleStreamableHttp.js (57%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/client/simpleStreamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/streamableHttpWithSseFallbackClient.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/streamableHttpWithSseFallbackClient.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/streamableHttpWithSseFallbackClient.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/client/streamableHttpWithSseFallbackClient.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/demoInMemoryOAuthProvider.d.ts (84%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/demoInMemoryOAuthProvider.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/demoInMemoryOAuthProvider.js (82%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/demoInMemoryOAuthProvider.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/jsonResponseStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/jsonResponseStreamableHttp.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/jsonResponseStreamableHttp.js (84%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/jsonResponseStreamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/mcpServerOutputSchema.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/mcpServerOutputSchema.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/mcpServerOutputSchema.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/mcpServerOutputSchema.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/simpleSseServer.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/simpleSseServer.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/simpleSseServer.js (88%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/simpleSseServer.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/simpleStatelessStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/simpleStatelessStreamableHttp.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/simpleStatelessStreamableHttp.js (86%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/simpleStatelessStreamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/simpleStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/simpleStreamableHttp.d.ts.map (100%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/simpleStreamableHttp.js create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/simpleStreamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/sseAndStreamableHttpCompatibleServer.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/sseAndStreamableHttpCompatibleServer.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/sseAndStreamableHttpCompatibleServer.js (93%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/sseAndStreamableHttpCompatibleServer.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/standaloneSseWithGetStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/standaloneSseWithGetStreamableHttp.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/standaloneSseWithGetStreamableHttp.js (97%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/server/standaloneSseWithGetStreamableHttp.js.map (85%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/toolWithSampleServer.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/toolWithSampleServer.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/toolWithSampleServer.js create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/examples/server/toolWithSampleServer.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/shared/inMemoryEventStore.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/shared/inMemoryEventStore.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/shared/inMemoryEventStore.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/examples/shared/inMemoryEventStore.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/inMemory.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/inMemory.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/inMemory.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/inMemory.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/package.json (100%) rename front_end/third_party/mcp-sdk/{ajv/dist/esm => dist/cjs}/server/auth/clients.d.ts (86%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/clients.d.ts.map (70%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/clients.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/clients.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv/dist/esm => dist/cjs}/server/auth/errors.d.ts (77%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/errors.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/errors.js (60%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/errors.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/authorize.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/authorize.d.ts.map (75%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/authorize.js (96%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/handlers/authorize.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/metadata.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/metadata.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/metadata.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/metadata.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/register.d.ts (76%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/register.d.ts.map (70%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/register.js (93%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/handlers/register.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/revoke.d.ts (89%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/revoke.d.ts.map (75%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/revoke.js (88%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/handlers/revoke.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/token.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/token.d.ts.map (76%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/handlers/token.js (92%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/handlers/token.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/allowedMethods.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/allowedMethods.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/allowedMethods.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/allowedMethods.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/bearerAuth.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/bearerAuth.d.ts.map (90%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/bearerAuth.js (90%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/middleware/bearerAuth.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/clientAuth.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/clientAuth.d.ts.map (98%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/clientAuth.js (96%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/middleware/clientAuth.js.map (82%) rename front_end/third_party/mcp-sdk/{ajv/dist/esm => dist/cjs}/server/auth/provider.d.ts (95%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/provider.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/provider.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/provider.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv/dist/esm => dist/cjs}/server/auth/providers/proxyProvider.d.ts (86%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/providers/proxyProvider.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/providers/proxyProvider.js (85%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/auth/providers/proxyProvider.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/router.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/router.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/router.js (99%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/router.js.map (71%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/types.d.ts (76%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/types.d.ts.map (59%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/types.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/auth/types.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/completable.d.ts (89%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/completable.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/completable.js (100%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/completable.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/index.d.ts (60%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/index.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/index.js (68%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/index.js.map rename front_end/third_party/mcp-sdk/{ajv/dist/esm => dist/cjs}/server/mcp.d.ts (87%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/mcp.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/mcp.js (77%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/mcp.js.map rename front_end/third_party/mcp-sdk/{ajv/dist/esm => dist/cjs}/server/sse.d.ts (58%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/sse.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/sse.js (69%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/sse.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/stdio.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/stdio.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/stdio.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/stdio.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv/dist/esm => dist/cjs}/server/streamableHttp.d.ts (74%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/streamableHttp.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/server/streamableHttp.js (84%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/server/streamableHttp.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/auth-utils.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/auth-utils.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/auth-utils.js create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/auth-utils.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/auth.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/auth.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/auth.js (51%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/auth.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/metadataUtils.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/metadataUtils.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/metadataUtils.js create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/metadataUtils.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/protocol.d.ts (92%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/protocol.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/protocol.js (81%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/protocol.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/stdio.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/stdio.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/stdio.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/stdio.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/transport.d.ts (80%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/shared/transport.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/transport.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/transport.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/uriTemplate.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/uriTemplate.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/uriTemplate.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/shared/uriTemplate.js.map (100%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/types.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/types.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/cjs/types.js (72%) create mode 100644 front_end/third_party/mcp-sdk/dist/cjs/types.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/cli.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/cli.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/cli.js (96%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/cli.js.map (66%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/auth.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/auth.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/auth.js create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/auth.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/index.d.ts rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/index.d.ts.map (55%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/index.js (95%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/index.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/middleware.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/middleware.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/middleware.js create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/middleware.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/sse.d.ts (92%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/sse.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/sse.js (78%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/sse.js.map rename front_end/third_party/mcp-sdk/{ajv/dist/cjs => dist/esm}/client/stdio.d.ts (93%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/stdio.d.ts.map (76%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/stdio.js (85%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/stdio.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/streamableHttp.d.ts (83%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/streamableHttp.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/streamableHttp.js (85%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/client/streamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/websocket.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/websocket.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/websocket.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/client/websocket.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/multipleClientsParallel.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/multipleClientsParallel.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/multipleClientsParallel.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/multipleClientsParallel.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/parallelToolCallsClient.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/parallelToolCallsClient.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/parallelToolCallsClient.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/parallelToolCallsClient.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/simpleOAuthClient.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/simpleOAuthClient.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/simpleOAuthClient.js (99%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/simpleOAuthClient.js.map (60%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/simpleStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/simpleStreamableHttp.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/simpleStreamableHttp.js (57%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/client/simpleStreamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/streamableHttpWithSseFallbackClient.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/streamableHttpWithSseFallbackClient.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/streamableHttpWithSseFallbackClient.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/client/streamableHttpWithSseFallbackClient.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/demoInMemoryOAuthProvider.d.ts (84%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/demoInMemoryOAuthProvider.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/demoInMemoryOAuthProvider.js (80%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/demoInMemoryOAuthProvider.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/jsonResponseStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/jsonResponseStreamableHttp.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/jsonResponseStreamableHttp.js (83%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/jsonResponseStreamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/mcpServerOutputSchema.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/mcpServerOutputSchema.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/mcpServerOutputSchema.js (98%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/mcpServerOutputSchema.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/simpleSseServer.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/simpleSseServer.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/simpleSseServer.js (87%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/simpleSseServer.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/simpleStatelessStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/simpleStatelessStreamableHttp.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/simpleStatelessStreamableHttp.js (85%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/simpleStatelessStreamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/simpleStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/simpleStreamableHttp.d.ts.map (100%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/simpleStreamableHttp.js create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/simpleStreamableHttp.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/sseAndStreamableHttpCompatibleServer.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/sseAndStreamableHttpCompatibleServer.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/sseAndStreamableHttpCompatibleServer.js (92%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/sseAndStreamableHttpCompatibleServer.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/standaloneSseWithGetStreamableHttp.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/standaloneSseWithGetStreamableHttp.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/standaloneSseWithGetStreamableHttp.js (96%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/server/standaloneSseWithGetStreamableHttp.js.map (86%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/toolWithSampleServer.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/toolWithSampleServer.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/toolWithSampleServer.js create mode 100644 front_end/third_party/mcp-sdk/dist/esm/examples/server/toolWithSampleServer.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/shared/inMemoryEventStore.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/shared/inMemoryEventStore.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/shared/inMemoryEventStore.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/examples/shared/inMemoryEventStore.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/inMemory.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/inMemory.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/inMemory.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/inMemory.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/package.json (100%) rename front_end/third_party/mcp-sdk/{ajv/dist/cjs => dist/esm}/server/auth/clients.d.ts (86%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/clients.d.ts.map (70%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/clients.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/clients.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv/dist/cjs => dist/esm}/server/auth/errors.d.ts (77%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/errors.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/errors.js (61%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/errors.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/authorize.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/authorize.d.ts.map (75%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/authorize.js (96%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/handlers/authorize.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/metadata.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/metadata.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/metadata.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/metadata.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/register.d.ts (76%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/register.d.ts.map (70%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/register.js (93%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/handlers/register.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/revoke.d.ts (89%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/revoke.d.ts.map (75%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/revoke.js (85%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/handlers/revoke.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/token.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/token.d.ts.map (76%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/handlers/token.js (91%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/handlers/token.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/allowedMethods.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/allowedMethods.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/allowedMethods.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/allowedMethods.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/bearerAuth.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/bearerAuth.d.ts.map (90%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/bearerAuth.js (90%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/middleware/bearerAuth.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/clientAuth.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/clientAuth.d.ts.map (98%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/clientAuth.js (95%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/middleware/clientAuth.js.map (85%) rename front_end/third_party/mcp-sdk/{ajv/dist/cjs => dist/esm}/server/auth/provider.d.ts (95%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/provider.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/provider.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/provider.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv/dist/cjs => dist/esm}/server/auth/providers/proxyProvider.d.ts (86%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/providers/proxyProvider.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/providers/proxyProvider.js (84%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/auth/providers/proxyProvider.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/router.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/router.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/router.js (99%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/router.js.map (73%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/types.d.ts (76%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/types.d.ts.map (59%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/types.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/auth/types.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/completable.d.ts (89%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/completable.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/completable.js (97%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/completable.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/index.d.ts (60%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/index.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/index.js (67%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/index.js.map rename front_end/third_party/mcp-sdk/{ajv/dist/cjs => dist/esm}/server/mcp.d.ts (87%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/mcp.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/mcp.js (76%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/mcp.js.map rename front_end/third_party/mcp-sdk/{ajv/dist/cjs => dist/esm}/server/sse.d.ts (58%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/sse.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/sse.js (67%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/sse.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/stdio.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/stdio.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/stdio.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/stdio.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv/dist/cjs => dist/esm}/server/streamableHttp.d.ts (74%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/streamableHttp.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/server/streamableHttp.js (84%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/server/streamableHttp.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/auth-utils.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/auth-utils.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/auth-utils.js create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/auth-utils.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/auth.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/auth.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/auth.js (50%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/auth.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/metadataUtils.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/metadataUtils.d.ts.map create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/metadataUtils.js create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/metadataUtils.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/protocol.d.ts (92%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/protocol.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/protocol.js (81%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/protocol.js.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/stdio.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/stdio.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/stdio.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/stdio.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/transport.d.ts (80%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/shared/transport.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/transport.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/transport.js.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/uriTemplate.d.ts (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/uriTemplate.d.ts.map (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/uriTemplate.js (100%) rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/shared/uriTemplate.js.map (100%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/types.d.ts create mode 100644 front_end/third_party/mcp-sdk/dist/esm/types.d.ts.map rename front_end/third_party/mcp-sdk/{ajv => }/dist/esm/types.js (77%) create mode 100644 front_end/third_party/mcp-sdk/dist/esm/types.js.map create mode 100644 front_end/third_party/mcp-sdk/dist/zod/zod-esm.js create mode 100644 front_end/third_party/mcp-sdk/lib/ajv.d.ts create mode 100644 front_end/third_party/mcp-sdk/lib/ajv.js create mode 100644 front_end/third_party/mcp-sdk/lib/cache.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/async.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/equal.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/error_classes.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/formats.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/index.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/resolve.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/rules.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/schema_obj.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/ucs2length.js create mode 100644 front_end/third_party/mcp-sdk/lib/compile/util.js create mode 100644 front_end/third_party/mcp-sdk/lib/data.js create mode 100644 front_end/third_party/mcp-sdk/lib/definition_schema.js create mode 100644 front_end/third_party/mcp-sdk/lib/dot/_limit.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/_limitItems.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/_limitLength.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/_limitProperties.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/allOf.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/anyOf.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/coerce.def create mode 100644 front_end/third_party/mcp-sdk/lib/dot/comment.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/const.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/contains.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/custom.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/defaults.def create mode 100644 front_end/third_party/mcp-sdk/lib/dot/definitions.def create mode 100644 front_end/third_party/mcp-sdk/lib/dot/dependencies.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/enum.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/errors.def create mode 100644 front_end/third_party/mcp-sdk/lib/dot/format.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/if.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/items.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/missing.def create mode 100644 front_end/third_party/mcp-sdk/lib/dot/multipleOf.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/not.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/oneOf.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/pattern.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/properties.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/propertyNames.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/ref.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/required.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/uniqueItems.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dot/validate.jst create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/README.md create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/_limit.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/_limitItems.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/_limitLength.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/_limitProperties.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/allOf.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/anyOf.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/comment.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/const.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/contains.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/custom.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/dependencies.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/enum.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/format.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/if.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/index.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/items.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/multipleOf.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/not.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/oneOf.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/pattern.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/properties.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/propertyNames.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/ref.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/required.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/uniqueItems.js create mode 100644 front_end/third_party/mcp-sdk/lib/dotjs/validate.js create mode 100644 front_end/third_party/mcp-sdk/lib/keyword.js create mode 100644 front_end/third_party/mcp-sdk/lib/refs/data.json create mode 100644 front_end/third_party/mcp-sdk/lib/refs/json-schema-draft-04.json create mode 100644 front_end/third_party/mcp-sdk/lib/refs/json-schema-draft-06.json create mode 100644 front_end/third_party/mcp-sdk/lib/refs/json-schema-draft-07.json rename front_end/third_party/mcp-sdk/{ajv/dist => lib}/refs/json-schema-secure.json (88%) create mode 100644 front_end/third_party/mcp-sdk/mcp-sdk-v2.ts create mode 100644 front_end/third_party/mcp-sdk/package.json create mode 100644 front_end/third_party/mcp-sdk/scripts/.eslintrc.yml create mode 100644 front_end/third_party/mcp-sdk/scripts/bundle.js create mode 100644 front_end/third_party/mcp-sdk/scripts/compile-dots.js create mode 100644 front_end/third_party/mcp-sdk/scripts/info create mode 100644 front_end/third_party/mcp-sdk/scripts/prepare-tests create mode 100644 front_end/third_party/mcp-sdk/scripts/publish-built-version create mode 100644 front_end/third_party/mcp-sdk/scripts/travis-gh-pages delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/cli.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/cli.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/cli.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/cli.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/index.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/index.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/index.test.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/index.test.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/index.test.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/index.test.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/sse.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/sse.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/sse.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/sse.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/stdio.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/stdio.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/stdio.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/stdio.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/stdio.test.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/stdio.test.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/stdio.test.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/stdio.test.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/websocket.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/websocket.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/websocket.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/client/websocket.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/inMemory.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/inMemory.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/inMemory.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/inMemory.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/inMemory.test.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/inMemory.test.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/inMemory.test.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/inMemory.test.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/index.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/index.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/index.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/index.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/index.test.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/index.test.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/index.test.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/index.test.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/sse.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/sse.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/sse.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/sse.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/stdio.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/stdio.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/stdio.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/stdio.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/stdio.test.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/stdio.test.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/stdio.test.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/server/stdio.test.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/protocol.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/protocol.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/protocol.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/protocol.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/stdio.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/stdio.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/stdio.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/stdio.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/stdio.test.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/stdio.test.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/stdio.test.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/stdio.test.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/transport.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/transport.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/transport.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/shared/transport.js.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/types.d.ts delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/types.d.ts.map delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/types.js delete mode 100644 front_end/third_party/mcp-sdk/zod/dist/types.js.map create mode 100644 front_end/third_party/mcp-sdk/zod/zod-esm.js diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index 0daec877f6b..e12330a7eed 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni @@ -313,6 +313,19 @@ grd_files_bundled_sources = [ "front_end/Images/whatsnew.svg", "front_end/Images/width.svg", "front_end/Images/zoom-in.svg", + "front_end/Images/asana-mcp.svg", + "front_end/Images/atlassian-mcp.svg", + "front_end/Images/github-mcp.svg", + "front_end/Images/google-drive-mcp.svg", + "front_end/Images/google-sheets-mcp.svg", + "front_end/Images/huggingface-mcp.svg", + "front_end/Images/intercom-mcp.svg", + "front_end/Images/invideo-mcp.svg", + "front_end/Images/linear-mcp.svg", + "front_end/Images/notion-mcp.svg", + "front_end/Images/sentry-mcp.svg", + "front_end/Images/slack-mcp.svg", + "front_end/Images/socket-mcp.svg", "front_end/Tests.js", "front_end/application_tokens.css", "front_end/core/common/common.js", @@ -635,13 +648,17 @@ grd_files_bundled_sources = [ "front_end/panels/ai_chat/ui/AgentSessionHeaderComponent.js", "front_end/panels/ai_chat/ui/ToolDescriptionFormatter.js", "front_end/panels/ai_chat/ui/chatView.css.js", + "front_end/panels/ai_chat/ui/mcp/mcpConnectorsCatalogDialog.css.js", "front_end/panels/ai_chat/ui/HelpDialog.js", "front_end/panels/ai_chat/ui/PromptEditDialog.js", "front_end/panels/ai_chat/ui/SettingsDialog.js", + "front_end/panels/ai_chat/ui/mcp/MCPConnectionsDialog.js", + "front_end/panels/ai_chat/ui/mcp/MCPConnectorsCatalogDialog.js", "front_end/panels/ai_chat/ui/EvaluationDialog.js", "front_end/panels/ai_chat/core/AgentService.js", "front_end/panels/ai_chat/core/State.js", "front_end/panels/ai_chat/core/Graph.js", + "front_end/panels/ai_chat/core/BuildConfig.js", "front_end/panels/ai_chat/core/Types.js", "front_end/panels/ai_chat/core/Constants.js", "front_end/panels/ai_chat/core/ConfigurableGraph.js", @@ -655,6 +672,7 @@ grd_files_bundled_sources = [ "front_end/panels/ai_chat/core/StateGraph.js", "front_end/panels/ai_chat/core/Logger.js", "front_end/panels/ai_chat/core/AgentErrorHandler.js", + "front_end/panels/ai_chat/core/AgentDescriptorRegistry.js", "front_end/panels/ai_chat/core/Version.js", "front_end/panels/ai_chat/core/VersionChecker.js", "front_end/panels/ai_chat/LLM/LLMTypes.js", @@ -720,6 +738,20 @@ grd_files_bundled_sources = [ "front_end/panels/ai_chat/agent_framework/AgentSessionTypes.js", "front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.js", "front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/ActionVerificationAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/AgentVersion.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/ClickActionAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/ContentWriterAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/DirectURLNavigatorAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/EcommerceProductInfoAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/FormFillActionAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/HoverActionAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/KeyboardInputActionAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/ResearchAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/ScrollActionAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.js", + "front_end/panels/ai_chat/agent_framework/implementation/agents/SearchAgent.js", "front_end/panels/ai_chat/common/MarkdownViewerUtil.js", "front_end/panels/ai_chat/evaluation/runner/VisionAgentEvaluationRunner.js", "front_end/panels/ai_chat/evaluation/runner/EvaluationRunner.js", @@ -880,6 +912,7 @@ grd_files_bundled_sources = [ "front_end/third_party/lighthouse/report/report.js", "front_end/third_party/lit/lit.js", "front_end/third_party/mcp-sdk/mcp-sdk.js", + "front_end/third_party/mcp-sdk/mcp-sdk-v2.js", "front_end/third_party/marked/marked.js", "front_end/third_party/puppeteer-replay/puppeteer-replay.js", "front_end/third_party/puppeteer/puppeteer.js", @@ -2239,15 +2272,25 @@ grd_files_unbundled_sources = [ "front_end/third_party/lit/lib/static-html.js", "front_end/third_party/marked/package/lib/marked.esm.js", "front_end/third_party/mcp-sdk/ajv/dist/ajv.js", + "front_end/third_party/mcp-sdk/ajv/dist/ajv.bundle.js", + "front_end/third_party/mcp-sdk/ajv/dist/ajv-esm.js", "front_end/third_party/mcp-sdk/eventsource-parser/package/dist/index.js", "front_end/third_party/mcp-sdk/eventsource-parser/package/dist/stream.js", - "front_end/third_party/mcp-sdk/package/dist/client/index.js", - "front_end/third_party/mcp-sdk/package/dist/client/sse.js", - "front_end/third_party/mcp-sdk/package/dist/shared/protocol.js", - "front_end/third_party/mcp-sdk/package/dist/shared/transport.js", - "front_end/third_party/mcp-sdk/package/dist/types.js", + "front_end/third_party/mcp-sdk/dist/esm/client/auth.js", + "front_end/third_party/mcp-sdk/dist/esm/client/index.js", + "front_end/third_party/mcp-sdk/dist/esm/client/sse.js", + "front_end/third_party/mcp-sdk/dist/esm/client/streamableHttp.js", + "front_end/third_party/mcp-sdk/dist/esm/server/index.js", + "front_end/third_party/mcp-sdk/dist/esm/server/auth/errors.js", + "front_end/third_party/mcp-sdk/dist/esm/shared/auth.js", + "front_end/third_party/mcp-sdk/dist/esm/shared/auth-utils.js", + "front_end/third_party/mcp-sdk/dist/esm/shared/protocol.js", + "front_end/third_party/mcp-sdk/dist/esm/shared/transport.js", + "front_end/third_party/mcp-sdk/dist/esm/types.js", + "front_end/third_party/mcp-sdk/dist/zod/zod-esm.js", "front_end/third_party/mcp-sdk/zod/lib/index.js", "front_end/third_party/mcp-sdk/zod/lib/index.mjs", + "front_end/third_party/mcp-sdk/zod/zod-esm.js", "front_end/third_party/puppeteer-replay/package/lib/main.js", "front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Browser.js", "front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/BrowserContext.js", diff --git a/config/gni/devtools_image_files.gni b/config/gni/devtools_image_files.gni index 46c6e51e7dd..9ac14171838 100644 --- a/config/gni/devtools_image_files.gni +++ b/config/gni/devtools_image_files.gni @@ -308,6 +308,19 @@ devtools_svg_sources = [ "whatsnew.svg", "width.svg", "zoom-in.svg", + "asana-mcp.svg", + "atlassian-mcp.svg", + "github-mcp.svg", + "google-drive-mcp.svg", + "google-sheets-mcp.svg", + "huggingface-mcp.svg", + "intercom-mcp.svg", + "invideo-mcp.svg", + "linear-mcp.svg", + "notion-mcp.svg", + "sentry-mcp.svg", + "slack-mcp.svg", + "socket-mcp.svg", ] devtools_src_svg_files = [] diff --git a/eval-server/nodejs/evals/schema-extractor/amazon-product-001.yaml b/eval-server/nodejs/evals/schema-extractor/amazon-product-001.yaml index bfeb975979c..42e4738f0a9 100644 --- a/eval-server/nodejs/evals/schema-extractor/amazon-product-001.yaml +++ b/eval-server/nodejs/evals/schema-extractor/amazon-product-001.yaml @@ -9,7 +9,7 @@ target: wait_for: "networkidle" wait_timeout: 5000 -tool: "extract_schema_data" +tool: "extract_data" timeout: 60000 input: diff --git a/eval-server/nodejs/evals/schema-extractor/bbc-news-001.yaml b/eval-server/nodejs/evals/schema-extractor/bbc-news-001.yaml index e434d2a874a..68431471596 100644 --- a/eval-server/nodejs/evals/schema-extractor/bbc-news-001.yaml +++ b/eval-server/nodejs/evals/schema-extractor/bbc-news-001.yaml @@ -9,7 +9,7 @@ target: wait_for: "networkidle" wait_timeout: 5000 -tool: "extract_schema_data" +tool: "extract_data" timeout: 30000 input: diff --git a/eval-server/nodejs/evals/schema-extractor/bing-search-001.yaml b/eval-server/nodejs/evals/schema-extractor/bing-search-001.yaml index 8488f341b43..7e7d674c2d6 100644 --- a/eval-server/nodejs/evals/schema-extractor/bing-search-001.yaml +++ b/eval-server/nodejs/evals/schema-extractor/bing-search-001.yaml @@ -9,7 +9,7 @@ target: wait_for: "networkidle" wait_timeout: 5000 -tool: "extract_schema_data" +tool: "extract_data" timeout: 45000 input: diff --git a/eval-server/nodejs/evals/schema-extractor/github-repo-001.yaml b/eval-server/nodejs/evals/schema-extractor/github-repo-001.yaml index 7a01a14043e..669357779b7 100644 --- a/eval-server/nodejs/evals/schema-extractor/github-repo-001.yaml +++ b/eval-server/nodejs/evals/schema-extractor/github-repo-001.yaml @@ -9,7 +9,7 @@ target: wait_for: "networkidle" wait_timeout: 5000 -tool: "extract_schema_data" +tool: "extract_data" timeout: 30000 input: diff --git a/eval-server/nodejs/evals/schema-extractor/google-flights-001.yaml b/eval-server/nodejs/evals/schema-extractor/google-flights-001.yaml index 80da1bb7bb5..ab2e53c91e9 100644 --- a/eval-server/nodejs/evals/schema-extractor/google-flights-001.yaml +++ b/eval-server/nodejs/evals/schema-extractor/google-flights-001.yaml @@ -9,7 +9,7 @@ target: wait_for: "networkidle" wait_timeout: 5000 -tool: "extract_schema_data" +tool: "extract_data" timeout: 60000 input: diff --git a/eval-server/nodejs/evals/schema-extractor/google-search-001.yaml b/eval-server/nodejs/evals/schema-extractor/google-search-001.yaml index 7e6f0e6a4eb..5763ba83a33 100644 --- a/eval-server/nodejs/evals/schema-extractor/google-search-001.yaml +++ b/eval-server/nodejs/evals/schema-extractor/google-search-001.yaml @@ -9,7 +9,7 @@ target: wait_for: "networkidle" wait_timeout: 5000 -tool: "extract_schema_data" +tool: "extract_data" timeout: 45000 input: diff --git a/eval-server/nodejs/evals/schema-extractor/homedepot-001.yaml b/eval-server/nodejs/evals/schema-extractor/homedepot-001.yaml index 4e8b835b66d..2eb4883ffdf 100644 --- a/eval-server/nodejs/evals/schema-extractor/homedepot-001.yaml +++ b/eval-server/nodejs/evals/schema-extractor/homedepot-001.yaml @@ -9,7 +9,7 @@ target: wait_for: "networkidle" wait_timeout: 5000 -tool: "extract_schema_data" +tool: "extract_data" timeout: 60000 input: diff --git a/eval-server/nodejs/evals/schema-extractor/macys-001.yaml b/eval-server/nodejs/evals/schema-extractor/macys-001.yaml index 23a4e37dec0..81e05f9690d 100644 --- a/eval-server/nodejs/evals/schema-extractor/macys-001.yaml +++ b/eval-server/nodejs/evals/schema-extractor/macys-001.yaml @@ -9,7 +9,7 @@ target: wait_for: "networkidle" wait_timeout: 5000 -tool: "extract_schema_data" +tool: "extract_data" timeout: 60000 input: diff --git a/eval-server/nodejs/evals/schema-extractor/wikipedia-search-001.yaml b/eval-server/nodejs/evals/schema-extractor/wikipedia-search-001.yaml index ad5f2f43b82..616f0d6a82c 100644 --- a/eval-server/nodejs/evals/schema-extractor/wikipedia-search-001.yaml +++ b/eval-server/nodejs/evals/schema-extractor/wikipedia-search-001.yaml @@ -9,7 +9,7 @@ target: wait_for: "networkidle" wait_timeout: 5000 -tool: "extract_schema_data" +tool: "extract_data" timeout: 30000 input: diff --git a/eval-server/nodejs/evals/web-task-agent/jobs-001.yaml b/eval-server/nodejs/evals/web-task-agent/jobs-001.yaml index 47df0060949..7a6caa8cee7 100644 --- a/eval-server/nodejs/evals/web-task-agent/jobs-001.yaml +++ b/eval-server/nodejs/evals/web-task-agent/jobs-001.yaml @@ -46,7 +46,7 @@ validation: - "Either used construct_direct_url for LinkedIn job search OR used traditional form interaction" - "If using direct URL: constructed proper LinkedIn job search URL with keywords and location" - "If using forms: delegated keyword and location input to action_agent" - - "Extracted job listings using schema_based_extractor" + - "Extracted job listings using extract_data" - "Returned structured job data in readable text format (not JSON)" - "Each job listing includes title, company, location, and other relevant fields" - "Results are numbered or organized clearly for easy reading" diff --git a/eval-server/nodejs/evals/web-task-agent/realestate-001.yaml b/eval-server/nodejs/evals/web-task-agent/realestate-001.yaml index d8f36d1cc4c..5fd824ea79f 100644 --- a/eval-server/nodejs/evals/web-task-agent/realestate-001.yaml +++ b/eval-server/nodejs/evals/web-task-agent/realestate-001.yaml @@ -49,7 +49,7 @@ validation: - "Delegated price filter setting to action_agent" - "Coordinated property type selection through action_agent" - "Applied search filters through proper action_agent calls" - - "Extracted property listings with schema_based_extractor" + - "Extracted property listings with extract_data" - "Returned structured property data in readable text format (not JSON)" - "Each property includes address, price, bedrooms, bathrooms, and other key details" - "Properties are clearly numbered or organized for easy comparison" diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-jobs-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-jobs-001.yaml index a5f80d377b4..2c72df325aa 100644 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-jobs-001.yaml +++ b/eval-server/nodejs/evals/web-task-agent/web-task-agent-jobs-001.yaml @@ -46,7 +46,7 @@ validation: - "Either used construct_direct_url for LinkedIn job search OR used traditional form interaction" - "If using direct URL: constructed proper LinkedIn job search URL with keywords and location" - "If using forms: delegated keyword and location input to action_agent" - - "Extracted job listings using schema_based_extractor" + - "Extracted job listings using extract_data" - "Returned structured job data in readable text format (not JSON)" - "Each job listing includes title, company, location, and other relevant fields" - "Results are numbered or organized clearly for easy reading" diff --git a/eval-server/nodejs/evals/web-task-agent/web-task-agent-realestate-001.yaml b/eval-server/nodejs/evals/web-task-agent/web-task-agent-realestate-001.yaml index 69757d302a4..f22bc139b96 100644 --- a/eval-server/nodejs/evals/web-task-agent/web-task-agent-realestate-001.yaml +++ b/eval-server/nodejs/evals/web-task-agent/web-task-agent-realestate-001.yaml @@ -49,7 +49,7 @@ validation: - "Delegated price filter setting to action_agent" - "Coordinated property type selection through action_agent" - "Applied search filters through proper action_agent calls" - - "Extracted property listings with schema_based_extractor" + - "Extracted property listings with extract_data" - "Returned structured property data in readable text format (not JSON)" - "Each property includes address, price, bedrooms, bathrooms, and other key details" - "Properties are clearly numbered or organized for easy comparison" diff --git a/eval-server/nodejs/schemas/client.schema.json b/eval-server/nodejs/schemas/client.schema.json index e23faab01f3..8dfdd3b172a 100644 --- a/eval-server/nodejs/schemas/client.schema.json +++ b/eval-server/nodejs/schemas/client.schema.json @@ -130,7 +130,7 @@ "tool": { "type": "string", "enum": [ - "extract_schema_data", + "extract_data", "extract_schema_streamlined", "research_agent", "action_agent", diff --git a/eval-server/nodejs/templates/default-client.yaml b/eval-server/nodejs/templates/default-client.yaml index 82cf18ee6ed..6ada1309532 100644 --- a/eval-server/nodejs/templates/default-client.yaml +++ b/eval-server/nodejs/templates/default-client.yaml @@ -27,7 +27,7 @@ evaluations: wait_for: "networkidle" wait_timeout: 5000 - tool: "extract_schema_data" + tool: "extract_data" timeout: 30000 input: diff --git a/front_end/Images/src/asana-mcp.svg b/front_end/Images/src/asana-mcp.svg new file mode 100644 index 00000000000..d627b4ea1cc --- /dev/null +++ b/front_end/Images/src/asana-mcp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/front_end/Images/src/atlassian-mcp.svg b/front_end/Images/src/atlassian-mcp.svg new file mode 100644 index 00000000000..a5078add21c --- /dev/null +++ b/front_end/Images/src/atlassian-mcp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/front_end/Images/src/github-mcp.svg b/front_end/Images/src/github-mcp.svg new file mode 100644 index 00000000000..4af7f573f8a --- /dev/null +++ b/front_end/Images/src/github-mcp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/front_end/Images/src/google-drive-mcp.svg b/front_end/Images/src/google-drive-mcp.svg new file mode 100644 index 00000000000..bd14ef66e2a --- /dev/null +++ b/front_end/Images/src/google-drive-mcp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/front_end/Images/src/google-sheets-mcp.svg b/front_end/Images/src/google-sheets-mcp.svg new file mode 100644 index 00000000000..2444a02398f --- /dev/null +++ b/front_end/Images/src/google-sheets-mcp.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/front_end/Images/src/huggingface-mcp.svg b/front_end/Images/src/huggingface-mcp.svg new file mode 100644 index 00000000000..2defcde95d2 --- /dev/null +++ b/front_end/Images/src/huggingface-mcp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/front_end/Images/src/intercom-mcp.svg b/front_end/Images/src/intercom-mcp.svg new file mode 100644 index 00000000000..098eeffdb1b --- /dev/null +++ b/front_end/Images/src/intercom-mcp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front_end/Images/src/invideo-mcp.svg b/front_end/Images/src/invideo-mcp.svg new file mode 100644 index 00000000000..470fd078b14 --- /dev/null +++ b/front_end/Images/src/invideo-mcp.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/front_end/Images/src/linear-mcp.svg b/front_end/Images/src/linear-mcp.svg new file mode 100644 index 00000000000..bb830fd5ead --- /dev/null +++ b/front_end/Images/src/linear-mcp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front_end/Images/src/notion-mcp.svg b/front_end/Images/src/notion-mcp.svg new file mode 100644 index 00000000000..1cf62d78ac8 --- /dev/null +++ b/front_end/Images/src/notion-mcp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front_end/Images/src/sentry-mcp.svg b/front_end/Images/src/sentry-mcp.svg new file mode 100644 index 00000000000..657bc83c518 --- /dev/null +++ b/front_end/Images/src/sentry-mcp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front_end/Images/src/slack-mcp.svg b/front_end/Images/src/slack-mcp.svg new file mode 100644 index 00000000000..5a9fb8ba2b6 --- /dev/null +++ b/front_end/Images/src/slack-mcp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/front_end/Images/src/socket-mcp.svg b/front_end/Images/src/socket-mcp.svg new file mode 100644 index 00000000000..003ece6495b --- /dev/null +++ b/front_end/Images/src/socket-mcp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/front_end/panels/ai_chat/BUILD.gn b/front_end/panels/ai_chat/BUILD.gn index b6182d922f7..464fde0fa98 100644 --- a/front_end/panels/ai_chat/BUILD.gn +++ b/front_end/panels/ai_chat/BUILD.gn @@ -11,6 +11,7 @@ import("../visibility.gni") generate_css("css_files") { sources = [ "ui/chatView.css", + "ui/mcp/mcpConnectorsCatalogDialog.css", ] } @@ -53,6 +54,7 @@ devtools_module("ai_chat") { "core/GraphConfigs.ts", "core/ConfigurableGraph.ts", "core/BaseOrchestratorAgent.ts", + "core/AgentDescriptorRegistry.ts", "core/PageInfoManager.ts", "core/AgentNodes.ts", "core/GraphHelpers.ts", @@ -95,6 +97,20 @@ devtools_module("ai_chat") { "agent_framework/AgentRunner.ts", "agent_framework/AgentRunnerEventBus.ts", "agent_framework/AgentSessionTypes.ts", + "agent_framework/implementation/agents/AgentVersion.ts", + "agent_framework/implementation/agents/DirectURLNavigatorAgent.ts", + "agent_framework/implementation/agents/ResearchAgent.ts", + "agent_framework/implementation/agents/ContentWriterAgent.ts", + "agent_framework/implementation/agents/ActionAgent.ts", + "agent_framework/implementation/agents/ActionVerificationAgent.ts", + "agent_framework/implementation/agents/ClickActionAgent.ts", + "agent_framework/implementation/agents/FormFillActionAgent.ts", + "agent_framework/implementation/agents/KeyboardInputActionAgent.ts", + "agent_framework/implementation/agents/HoverActionAgent.ts", + "agent_framework/implementation/agents/ScrollActionAgent.ts", + "agent_framework/implementation/agents/WebTaskAgent.ts", + "agent_framework/implementation/agents/EcommerceProductInfoAgent.ts", + "agent_framework/implementation/agents/SearchAgent.ts", "agent_framework/implementation/ConfiguredAgents.ts", "evaluation/framework/types.ts", "evaluation/framework/judges/LLMEvaluator.ts", @@ -130,6 +146,8 @@ devtools_module("ai_chat") { "mcp/MCPToolAdapter.ts", "mcp/MCPRegistry.ts", "mcp/MCPMetaTools.ts", + "ui/mcp/MCPConnectionsDialog.ts", + "ui/mcp/MCPConnectorsCatalogDialog.ts", ] deps = [ @@ -140,6 +158,7 @@ devtools_module("ai_chat") { "../../generated:protocol", "../../models/logs:bundle", "../../third_party/mcp-sdk:bundle", + "../../third_party/mcp-sdk:bundle_v2", "../../ui/components/helpers:bundle", "../../ui/components/markdown_view:bundle", "../../ui/components/snackbars:bundle", @@ -178,6 +197,8 @@ _ai_chat_sources = [ "ui/PromptEditDialog.ts", "ui/SettingsDialog.ts", "ui/EvaluationDialog.ts", + "ui/mcp/MCPConnectionsDialog.ts", + "ui/mcp/MCPConnectorsCatalogDialog.ts", "ai_chat_impl.ts", "models/ChatTypes.ts", "core/Graph.ts", @@ -190,6 +211,7 @@ _ai_chat_sources = [ "core/GraphConfigs.ts", "core/ConfigurableGraph.ts", "core/BaseOrchestratorAgent.ts", + "core/AgentDescriptorRegistry.ts", "core/PageInfoManager.ts", "core/AgentNodes.ts", "core/GraphHelpers.ts", @@ -233,6 +255,20 @@ _ai_chat_sources = [ "agent_framework/AgentRunnerEventBus.ts", "agent_framework/AgentSessionTypes.ts", "agent_framework/implementation/ConfiguredAgents.ts", + "agent_framework/implementation/agents/ActionAgent.ts", + "agent_framework/implementation/agents/ActionVerificationAgent.ts", + "agent_framework/implementation/agents/AgentVersion.ts", + "agent_framework/implementation/agents/ClickActionAgent.ts", + "agent_framework/implementation/agents/ContentWriterAgent.ts", + "agent_framework/implementation/agents/DirectURLNavigatorAgent.ts", + "agent_framework/implementation/agents/EcommerceProductInfoAgent.ts", + "agent_framework/implementation/agents/FormFillActionAgent.ts", + "agent_framework/implementation/agents/HoverActionAgent.ts", + "agent_framework/implementation/agents/KeyboardInputActionAgent.ts", + "agent_framework/implementation/agents/ResearchAgent.ts", + "agent_framework/implementation/agents/ScrollActionAgent.ts", + "agent_framework/implementation/agents/SearchAgent.ts", + "agent_framework/implementation/agents/WebTaskAgent.ts", "evaluation/framework/types.ts", "evaluation/framework/judges/LLMEvaluator.ts", "evaluation/framework/GenericToolEvaluator.ts", @@ -306,6 +342,7 @@ generated_file("ai_chat_release_js_metadata") { _css_module_target_gen_dir = get_label_info(":css_files", "target_gen_dir") _css_outputs_for_metadata = [ "$_css_module_target_gen_dir/ui/chatView.css.js", + "$_css_module_target_gen_dir/ui/mcp/mcpConnectorsCatalogDialog.css.js", ] generated_file("ai_chat_release_css_metadata") { @@ -363,8 +400,19 @@ ts_library("unittests") { "agent_framework/__tests__/AgentRunner.sanitizeToolResult.test.ts", "agent_framework/__tests__/AgentRunner.computeToolResultText.test.ts", "agent_framework/__tests__/AgentRunner.run.flows.test.ts", - "mcp/MCPClientSDK.test.ts", - "core/ToolSurfaceProvider.test.ts", + "mcp/__tests__/MCPClientSDK.test.ts", + "mcp/__tests__/MCPConfig.test.ts", + "mcp/__tests__/MCPIntegration.test.ts", + "mcp/__tests__/MCPToolRegistration.test.ts", + "core/__tests__/AgentNodes.test.ts", + "core/__tests__/AgentNodesSanitize.test.ts", + "core/__tests__/ToolExecutorNode.test.ts", + "core/__tests__/ToolNameMap.test.ts", + "core/__tests__/ToolNameMapping.test.ts", + "core/__tests__/ToolSurfaceProvider.test.ts", + "ui/__tests__/AIChatPanel.test.ts", + "ui/__tests__/LiveAgentSessionComponent.test.ts", + "ui/message/__tests__/MessageList.test.ts", ] deps = [ diff --git a/front_end/panels/ai_chat/agent_framework/AgentRunner.ts b/front_end/panels/ai_chat/agent_framework/AgentRunner.ts index a0ff010650a..fc05dba4798 100644 --- a/front_end/panels/ai_chat/agent_framework/AgentRunner.ts +++ b/front_end/panels/ai_chat/agent_framework/AgentRunner.ts @@ -3,6 +3,7 @@ // found in the LICENSE file. import { enhancePromptWithPageContext } from '../core/PageInfoManager.js'; +import type { AgentDescriptor } from '../core/AgentDescriptorRegistry.js'; import { LLMClient } from '../LLM/LLMClient.js'; import type { LLMResponse, LLMMessage, LLMProvider } from '../LLM/LLMTypes.js'; import type { Tool } from '../tools/Tools.js'; @@ -38,6 +39,8 @@ export interface AgentRunnerConfig { miniModel?: string; /** Nano model for smallest/fastest operations */ nanoModel?: string; + /** Descriptor describing this agent configuration */ + agentDescriptor?: AgentDescriptor; } /** @@ -397,11 +400,12 @@ export class AgentRunner { hooks: AgentRunnerHooks, executingAgent: ConfigurableAgentTool | null, parentSession?: AgentSession, // For natural nesting - overrides?: { sessionId?: string; parentSessionId?: string; traceId?: string } + overrides?: { sessionId?: string; parentSessionId?: string; traceId?: string }, + abortSignal?: AbortSignal ): Promise { const agentName = executingAgent?.name || 'Unknown'; logger.info(`Starting execution loop for agent: ${agentName}`); - const { apiKey, modelName, systemPrompt, tools, maxIterations, temperature } = config; + const { apiKey, modelName, systemPrompt, tools, maxIterations, temperature, agentDescriptor } = config; const { prepareInitialMessages, createSuccessResult, createErrorResult } = hooks; @@ -422,7 +426,8 @@ export class AgentRunner { config: executingAgent?.config, maxIterations, modelUsed: modelName, - iterationCount: 0 + iterationCount: 0, + descriptor: agentDescriptor }; // Use local session variable instead of static @@ -547,6 +552,13 @@ export class AgentRunner { let iteration = 0; // Initialize iteration counter for (iteration = 0; iteration < maxIterations; iteration++) { + // Check if execution has been aborted + if (abortSignal?.aborted) { + logger.info(`${agentName} execution aborted at iteration ${iteration + 1}/${maxIterations}`); + const abortResult = createErrorResult('Execution was cancelled', messages, 'error'); + return { ...abortResult, agentSession: currentSession }; + } + // Update session iteration count if (currentSession) { currentSession.iterationCount = iteration + 1; @@ -609,7 +621,12 @@ export class AgentRunner { agentName, iteration: iteration + 1, maxIterations, - phase: 'llm_generation' + phase: 'llm_generation', + ...(agentDescriptor ? { + agentVersion: agentDescriptor.version, + promptHash: agentDescriptor.promptHash, + toolsetHash: agentDescriptor.toolsetHash + } : {}) } }, tracingContext.traceId); @@ -675,7 +692,12 @@ export class AgentRunner { agentName, iteration: iteration + 1, phase: 'completed', - duration: Date.now() - generationStartTime.getTime() + duration: Date.now() - generationStartTime.getTime(), + ...(agentDescriptor ? { + agentVersion: agentDescriptor.version, + promptHash: agentDescriptor.promptHash, + toolsetHash: agentDescriptor.toolsetHash + } : {}) } }); @@ -702,7 +724,12 @@ export class AgentRunner { source: 'AgentRunner', agentName, iteration: iteration + 1, - phase: 'error' + phase: 'error', + ...(agentDescriptor ? { + agentVersion: agentDescriptor.version, + promptHash: agentDescriptor.promptHash, + toolsetHash: agentDescriptor.toolsetHash + } : {}) } }); } @@ -983,6 +1010,7 @@ export class AgentRunner { miniModel: config.miniModel, nanoModel: config.nanoModel, getVisionCapability: config.getVisionCapability, + abortSignal: abortSignal, overrideSessionId: preallocatedChildId, overrideParentSessionId: currentSession.sessionId, overrideTraceId: execTracingContext?.traceId, diff --git a/front_end/panels/ai_chat/agent_framework/AgentSessionTypes.ts b/front_end/panels/ai_chat/agent_framework/AgentSessionTypes.ts index 881146a3cfc..41aae1eba09 100644 --- a/front_end/panels/ai_chat/agent_framework/AgentSessionTypes.ts +++ b/front_end/panels/ai_chat/agent_framework/AgentSessionTypes.ts @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import type { AgentDescriptor } from '../core/AgentDescriptorRegistry.js'; import type { AgentToolConfig } from './ConfigurableAgentTool.js'; /** @@ -30,6 +31,7 @@ export interface AgentSession { reasoning?: string; tools: string[]; config?: AgentToolConfig; + descriptor?: AgentDescriptor; // Execution metadata iterationCount?: number; @@ -159,4 +161,4 @@ export function getAgentUIConfig(agentName: string, config?: AgentToolConfig) { color: config?.ui?.color || DEFAULT_AGENT_UI.color, backgroundColor: config?.ui?.backgroundColor || DEFAULT_AGENT_UI.backgroundColor }; -} \ No newline at end of file +} diff --git a/front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts b/front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts index f13079556c3..6ef74bf40a9 100644 --- a/front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts +++ b/front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts @@ -5,12 +5,14 @@ import type { Tool } from '../tools/Tools.js'; import { ChatMessageEntity, type ChatMessage } from '../models/ChatTypes.js'; import { createLogger } from '../core/Logger.js'; +import { AgentDescriptorRegistry, type AgentDescriptor } from '../core/AgentDescriptorRegistry.js'; import { getCurrentTracingContext } from '../tracing/TracingConfig.js'; import { MODEL_SENTINELS } from '../core/Constants.js'; import type { AgentSession } from './AgentSessionTypes.js'; import type { LLMProvider } from '../LLM/LLMTypes.js'; const logger = createLogger('ConfigurableAgentTool'); +const DEFAULT_AGENT_TOOL_VERSION = '2025-09-17'; import { AgentRunner, type AgentRunnerConfig, type AgentRunnerHooks } from './AgentRunner.js'; @@ -26,6 +28,8 @@ export interface CallCtx { overrideSessionId?: string, overrideParentSessionId?: string, overrideTraceId?: string, + abortSignal?: AbortSignal, + agentDescriptor?: AgentDescriptor, } /** @@ -111,6 +115,11 @@ export interface AgentToolConfig { */ tools: string[]; + /** + * Semantic version identifier for this agent configuration + */ + version?: string; + /** * Defines potential handoffs to other agents. * Handoffs triggered by 'llm_tool_call' are presented as tools to the LLM. @@ -315,6 +324,21 @@ export class ConfigurableAgentTool implements Tool config.systemPrompt, + toolNamesProvider: () => [...config.tools], + metadataProvider: () => ({ + handoffs: (config.handoffs || []).map(handoff => ({ + targetAgentName: handoff.targetAgentName, + trigger: handoff.trigger || 'llm_tool_call', + includeToolResults: handoff.includeToolResults ? [...handoff.includeToolResults] : undefined + })) + }) + }); + // Call custom init function directly if provided if (config.init) { config.init(this); @@ -481,6 +505,12 @@ export class ConfigurableAgentTool implements Tool new NavigateBackTool()); ToolRegistry.registerToolFactory('node_ids_to_urls', () => new NodeIDsToURLsTool()); ToolRegistry.registerToolFactory('fetcher_tool', () => new FetcherTool()); - ToolRegistry.registerToolFactory('schema_based_extractor', () => new SchemaBasedExtractorTool()); - ToolRegistry.registerToolFactory('extract_schema_data', () => new SchemaBasedExtractorTool()); + ToolRegistry.registerToolFactory('extract_data', () => new SchemaBasedExtractorTool()); ToolRegistry.registerToolFactory('extract_schema_streamlined', () => new StreamlinedSchemaExtractorTool()); ToolRegistry.registerToolFactory('finalize_with_critique', () => new FinalizeWithCritiqueTool()); ToolRegistry.registerToolFactory('perform_action', () => new PerformActionTool()); @@ -128,6 +64,11 @@ export function initializeConfiguredAgents(): void { const researchAgent = new ConfigurableAgentTool(researchAgentConfig); ToolRegistry.registerToolFactory('research_agent', () => researchAgent); + // Create and register Search Agent + const searchAgentConfig = createSearchAgentConfig(); + const searchAgent = new ConfigurableAgentTool(searchAgentConfig); + ToolRegistry.registerToolFactory('search_agent', () => searchAgent); + // Create and register Content Writer Agent const contentWriterAgentConfig = createContentWriterAgentConfig(); const contentWriterAgent = new ConfigurableAgentTool(contentWriterAgentConfig); @@ -175,1278 +116,3 @@ export function initializeConfiguredAgents(): void { ToolRegistry.registerToolFactory('ecommerce_product_info_fetcher_tool', () => ecommerceProductInfoAgent); } - -/** - * Create the configuration for the Research Agent - */ -function createResearchAgentConfig(): AgentToolConfig { - return { - name: 'research_agent', - description: 'Performs in-depth research on a specific query autonomously using multiple steps and internal tool calls (navigation, fetching, extraction). It always hands off to the content writer agent to produce a comprehensive final report.', - ui: { - displayName: 'Research Agent', - avatar: '๐Ÿ”', - color: '#3b82f6', - backgroundColor: '#f8fafc' - }, - systemPrompt: `You are a research subagent working as part of a team. You have been given a specific research task with clear requirements. Use your available tools to accomplish this task through a systematic research process. - -## Understanding Your Task - -You will receive: -- **task**: The specific research objective to accomplish -- **reasoning**: Why this research is being conducted (shown to the user) -- **context**: Additional details about constraints or focus areas (optional) -- **scope**: Whether this is a focused, comprehensive, or exploratory investigation -- **priority_sources**: Specific sources to prioritize if provided - -Adapt your research approach based on the scope: -- **Focused**: 3-5 tool calls, quick specific answers -- **Comprehensive**: 5-10 tool calls, in-depth analysis from multiple sources -- **Exploratory**: 10-15 tool calls, broad investigation of the topic landscape - -## Research Process - -### 1. Planning Phase -First, think through the task thoroughly: -- Review the task requirements and any provided context -- Note the scope (focused/comprehensive/exploratory) to determine effort level -- Check for priority_sources to guide your search strategy -- Determine your research budget based on scope: - - Focused scope: 3-5 tool calls for quick, specific answers - - Comprehensive scope: 5-10 tool calls for detailed analysis - - Exploratory scope: 10-15 tool calls for broad investigation -- Identify which tools are most relevant for the task - -### 2. Tool Selection Strategy -Choose tools based on task requirements: -- **navigate_url** + **fetcher_tool**: Core research loop - navigate to search engines, then fetch complete content -- **schema_based_extractor**: Extract structured data from search results (URLs, titles, snippets) -- **fetcher_tool**: BATCH PROCESS multiple URLs at once - accepts an array of URLs to save tool calls -- **document_search**: Search within documents for specific information -- **bookmark_store**: Save important sources for reference - -**CRITICAL - Batch URL Fetching**: -- The fetcher_tool accepts an ARRAY of URLs: {urls: [url1, url2, url3], reasoning: "..."} -- ALWAYS batch multiple URLs together instead of calling fetcher_tool multiple times -- Example: After extracting 5 URLs from search results, call fetcher_tool ONCE with all 5 URLs -- This dramatically reduces tool calls and improves efficiency - -### 3. Research Loop (OODA) -Execute an excellent Observe-Orient-Decide-Act loop: - -**Observe**: What information has been gathered? What's still needed? -**Orient**: What tools/queries would best gather needed information? -**Decide**: Make informed decisions on specific tool usage -**Act**: Execute the tool call - -**Efficient Research Workflow**: -1. Use navigate_url to search for your topic -2. Use schema_based_extractor to collect ALL URLs from search results -3. Call fetcher_tool ONCE with the array of all extracted URLs -4. Analyze the batch results and determine if more searches are needed -5. Repeat with different search queries if necessary - -- Execute a MINIMUM of 5 distinct tool calls for comprehensive research -- Maximum of 15 tool calls to prevent system overload -- Batch processing URLs counts as ONE tool call, making research much more efficient -- NEVER repeat the same query - adapt based on findings -- If hitting diminishing returns, complete the task immediately - -### 4. Source Quality Evaluation -Think critically about sources: -- Distinguish facts from speculation (watch for "could", "may", future tense) -- Identify problematic sources (aggregators vs. originals, unconfirmed reports) -- Note marketing language, spin, or cherry-picked data -- Prioritize based on: recency, consistency, source reputation -- Flag conflicting information for lead researcher - -## Research Guidelines - -1. **Query Optimization**: - - Use moderately broad queries (under 5 words) - - Avoid hyper-specific searches with poor hit rates - - Adjust specificity based on result quality - - Balance between specific and general - -2. **Information Focus** - Prioritize high-value information that is: - - **Significant**: Major implications for the task - - **Important**: Directly relevant or specifically requested - - **Precise**: Specific facts, numbers, dates, concrete data - - **High-quality**: From reputable, reliable sources - -3. **Documentation Requirements**: - - State which tool you're using and why - - Document each source with URL and title - - Extract specific quotes, statistics, facts with attribution - - Organize findings by source with clear citations - - Include publication dates where available - -4. **Efficiency Principles**: - - BATCH PROCESS URLs: Always use fetcher_tool with multiple URLs at once - - Use parallel tool calls when possible (2 tools simultaneously) - - Complete task as soon as sufficient information is gathered - - Stop at ~15 tool calls or when hitting diminishing returns - - Be detailed in process but concise in reporting - - Remember: Fetching 10 URLs in one batch = 1 tool call vs 10 individual calls - -## Output Structure -Structure findings as: -- Source 1: [Title] (URL) - [Date if available] - - Key facts: [specific quotes/data] - - Statistics: [numbers with context] - - Expert opinions: [attributed quotes] -- Source 2: [Title] (URL) - - [Continue pattern...] - -## Critical Reminders -- This is autonomous tool execution - complete the full task in one run -- NO conversational elements - execute research automatically -- Gather from 3-5+ diverse sources minimum -- DO NOT generate markdown reports or final content yourself -- Focus on gathering raw research data with proper citations - -## IMPORTANT: Handoff Protocol -When your research is complete: -1. NEVER generate markdown content or final reports yourself -2. Use the handoff_to_content_writer_agent tool to pass your research findings -3. The handoff tool expects: {query: "research topic", reasoning: "explanation for user"} -4. The content_writer_agent will create the final report from your research data - -Remember: You gather data, content_writer_agent writes the report. Always hand off when research is complete.`, - tools: [ - 'navigate_url', - 'navigate_back', - 'fetcher_tool', - 'schema_based_extractor', - 'node_ids_to_urls', - 'bookmark_store', - 'document_search' - ], - maxIterations: 15, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0, - schema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'The specific research task to accomplish, including clear requirements and expected deliverables.' - }, - reasoning: { - type: 'string', - description: 'Clear explanation for the user about why this research is being conducted and what to expect.' - }, - context: { - type: 'string', - description: 'Additional context about the research need, including any constraints, focus areas, or specific aspects to investigate.' - }, - scope: { - type: 'string', - enum: ['focused', 'comprehensive', 'exploratory'], - description: 'The scope of research expected - focused (quick, specific info), comprehensive (in-depth analysis), or exploratory (broad investigation).', - default: 'comprehensive' - }, - }, - required: ['query', 'reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - // For the action agent, we use the objective as the primary input, not the query field - return [{ - entity: ChatMessageEntity.USER, - text: `Task: ${args.query? `${args.query}` : ''} - ${args.task? `${args.task}` : ''} - ${args.objective? `${args.objective}` : ''} -${args.context ? `Context: ${args.context}` : ''} -${args.scope ? `The scope of research expected: ${args.scope}` : ''} -`, - }]; - }, - handoffs: [ - { - targetAgentName: 'content_writer_agent', - trigger: 'llm_tool_call', - includeToolResults: ['fetcher_tool', 'schema_based_extractor'] - }, - { - targetAgentName: 'content_writer_agent', - trigger: 'max_iterations', - includeToolResults: ['fetcher_tool', 'schema_based_extractor'] - } - ], - }; -} - -/** - * Create the configuration for the Content Writer Agent - */ -function createContentWriterAgentConfig(): AgentToolConfig { - return { - name: 'content_writer_agent', - description: 'Writes detailed, well-structured reports based on research data. Creates an outline and then builds a comprehensive markdown report with proper structure, citations, and detailed information.', - ui: { - displayName: 'Documentation Agent', - avatar: '๐Ÿ“', - color: '#059669', - backgroundColor: '#f0fdf4' - }, - systemPrompt: `You are a senior researcher tasked with writing a cohesive report for a research query. -You will be provided with the original query, and research data collected by a research assistant. - -## Receiving Handoff from Research Agent -You are specifically designed to collaborate with the research_agent. When you receive a handoff, you'll be provided with: -- The original research query -- Collected research data, which may include web content, extractions, analysis, and other information -- Your job is to organize this information into a comprehensive, well-structured report - -Your process should follow these steps: -1. Carefully analyze all the research data provided during the handoff -2. Identify key themes, findings, and important information from the data -3. Create a detailed outline for the report with clear sections and subsections -4. Generate a comprehensive report following your outline - -## Here is an example of the final report structure (you can come up with your own structure that is better for the user's query): - -1. **Title**: A concise, descriptive title for the report -2. **Executive Summary**: Brief overview summarizing the key findings and conclusions -3. **Introduction**: Context, importance of the topic, and research questions addressed -4. **Methodology**: How the research was conducted (when applicable) -5. **Main Body**: Organized by themes or topics with detailed analysis of findings - - Include sections and subsections as appropriate - - Support claims with evidence from the research - - Address counterarguments when relevant - - Use examples, case studies, or data to illustrate points -6. **Analysis/Discussion**: Synthesis of information, highlighting patterns, connections, and insights -7. **Implications**: Practical applications or theoretical significance of the findings -8. **Limitations**: Acknowledge limitations of the research or data -9. **Conclusion**: Summary of key points and final thoughts -10. **References**: Properly formatted citations for all sources used - -The final output should be in markdown format, and it should be lengthy and detailed. Aim for 5-10 pages of content, at least 1000 words.`, - tools: [], - maxIterations: 3, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0.3, - schema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'The original research question or topic that was investigated.' - }, - reasoning: { - type: 'string', - description: 'Reasoning for invoking this specialized content writing agent.' - }, - }, - required: ['query', 'reasoning'] - }, - handoffs: [], - }; -} - -/** - * Create the configuration for the Action Agent - */ -function createActionAgentConfig(): AgentToolConfig { - return { - name: 'action_agent', - description: 'Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.', - systemPrompt: `You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to: - -1. Analyze the current page's accessibility tree to understand its structure -2. Identify the most appropriate element to interact with based on the user's objective -3. Determine the correct action to perform (click, fill, type, etc.) -4. Execute that action precisely -5. **Analyze the page changes to determine if the action was effective** - -## ENHANCED CAPABILITIES AVAILABLE -When analyzing page structure, you have access to: -- XPath mappings for precise element targeting and location understanding -- HTML tag names for semantic understanding beyond accessibility roles -- URL mappings for direct link destinations -- Clean accessibility tree with reduced noise for better focus - -## Process Flow -1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use schema_based_extractor to extract the specific element you need to interact with -2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective -3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist -4. Determine the appropriate action method based on the element type and objective: - - For links, buttons: use 'click' - - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state) - - For radio buttons: use 'click' - - For input fields: use 'fill' with appropriate text - - For dropdown/select elements: use 'selectOption' with the option value or text -5. Execute the action using perform_action tool -6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness** - -## EVALUATING ACTION EFFECTIVENESS -After executing an action, the perform_action tool returns objective evidence in pageChange: - -**If pageChange.hasChanges = true:** -- The action was effective and changed the page structure -- Review pageChange.summary to understand what changed -- Check pageChange.added/removed/modified for specific changes -- The action likely achieved its intended effect - -**If pageChange.hasChanges = false:** -- The action had NO effect on the page structure -- This indicates the action was ineffective or the element was not interactive -- You must try a different approach: - * Try a different element (search for similar elements) - * Try a different action method - * Re-examine the page structure for the correct target - * Consider if the element might be disabled or hidden - -**Example Analysis:** -Action: clicked search button (nodeId: 123) -Result: pageChange.hasChanges = false, summary = "No changes detected" -Conclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field. - -**Example Tool Error:** -Action: attempted to fill input field -Error: "Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { "text": "your value" }" -Conclusion: Fix the args format and retry with proper syntax: { "method": "fill", "nodeId": 22132, "args": { "text": "search query" } } - -## Important Considerations -- **NEVER claim success unless pageChange.hasChanges = true** -- Be precise in your element selection, using the exact nodeId from the accessibility tree -- Leverage XPath information when available for more precise element targeting -- Use HTML tag context to better understand element semantics -- Use URL mappings to identify link destinations when relevant to the objective -- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element) -- When filling forms, ensure the data format matches what the field expects -- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability -- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select -- If pageChange shows no changes, immediately try an alternative approach - -## Method Examples -- perform_action with method='check' for checkboxes: { "method": "check", "nodeId": 123 } -- perform_action with method='selectOption' for dropdowns: { "method": "selectOption", "nodeId": 456, "args": { "text": "United States" } } -- perform_action with method='setChecked' for specific checkbox state: { "method": "setChecked", "nodeId": 789, "args": { "checked": true } }`, - tools: [ - 'get_page_content', - 'perform_action', - 'schema_based_extractor', - 'node_ids_to_urls', - 'scroll_page', - 'take_screenshot', - ], - maxIterations: 10, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0.5, - schema: { - type: 'object', - properties: { - objective: { - type: 'string', - description: 'The natural language description of the desired action (e.g., "click the login button", "fill the search box with \'query\'").' - }, - reasoning: { - type: 'string', - description: 'Reasoning for invoking this specialized action agent.' - }, - hint: { - type: 'string', - description: 'Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.' - }, - input_data: { - type: 'string', - description: 'Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.' - } - }, - required: ['objective', 'reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - // For the action agent, we use the objective as the primary input, not the query field - return [{ - entity: ChatMessageEntity.USER, - text: `Objective: ${args.objective}\n -Reasoning: ${args.reasoning}\n -${args.hint ? `Hint: ${args.hint}` : ''} -${args.input_data ? `Input Data: ${args.input_data}` : ''} -`, - }]; - }, - handoffs: [ - { - targetAgentName: 'action_verification_agent', - trigger: 'llm_tool_call', - includeToolResults: ['perform_action', 'get_page_content'] - } - ], - }; -} - -/** - * Create the configuration for the Action Verification Agent - */ -function createActionVerificationAgentConfig(): AgentToolConfig { - return { - name: 'action_verification_agent', - description: 'Verifies that actions performed by the action agent were successful by analyzing the page state after action execution and confirming expected outcomes.', - systemPrompt: `You are a specialized verification agent responsible for determining whether an action was successfully completed. Your task is to analyze the page state after an action has been performed and verify whether the expected outcome was achieved. - -## Verification Process -1. Review the original objective that was given to the action agent -2. Understand what action was attempted (click, fill, etc.) and on which element -3. Analyze the current page state using available tools to determine if the expected outcome was achieved -4. Provide a clear verification result with supporting evidence - -## Verification Methods -Based on the action type, use different verification strategies: - -### For Click Actions: -- Check if a new page loaded or the URL changed -- Verify if expected elements appeared or disappeared -- Look for confirmation messages or success indicators -- Check if any error messages appeared - -### For Form Fill Actions: -- Verify the field contains the expected value -- Look for validation messages (success or error) -- Check if form was successfully submitted -- Monitor for any error messages - -### For Navigation Actions: -- Confirm the URL matches the expected destination -- Verify page title or key content matches expectations -- Check for any navigation errors in console logs - -### Visual Verification: -- Use take_screenshot tool to capture the current page state -- Compare visual elements to expected outcomes -- Document any visual anomalies or unexpected UI states - -## Tools to Use -- get_page_content: Examine the updated page structure -- search_content: Look for specific text indicating success/failure -- inspect_element: Check properties of specific elements -- get_console_logs: Check for errors or success messages in the console -- schema_based_extractor: Extract structured data to verify expected outcomes - -## Output Format -Provide a clear verification report with: -1. Action Summary: Brief description of the action that was attempted -2. Verification Result: Clear SUCCESS or FAILURE classification -3. Confidence Level: High, Medium, or Low confidence in your verification -4. Evidence: Specific observations that support your conclusion -5. Explanation: Reasoning behind your verification result - -Remember that verification is time-sensitive - the page state might change during your analysis, so perform verifications promptly and efficiently.`, - tools: [ - 'search_content', - 'inspect_element', - 'get_console_logs', - 'schema_based_extractor', - 'take_screenshot' - ], - maxIterations: 3, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0.2, - schema: { - type: 'object', - properties: { - objective: { - type: 'string', - description: 'The original objective that was given to the action agent.' - }, - action_performed: { - type: 'string', - description: 'Description of the action that was performed (e.g., "clicked login button", "filled search field").' - }, - expected_outcome: { - type: 'string', - description: 'The expected outcome or success criteria for the action (e.g., "form submitted", "new page loaded").' - }, - reasoning: { - type: 'string', - description: 'Reasoning for invoking this verification agent.' - }, - }, - required: ['objective', 'reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - return [{ - entity: ChatMessageEntity.USER, - text: `Verification Request: -Objective: ${args.objective} -${args.action_performed ? `Action Performed: ${args.action_performed}` : ''} -${args.expected_outcome ? `Expected Outcome: ${args.expected_outcome}` : ''} -Reasoning: ${args.reasoning} - -Please verify if the action was successfully completed and achieved its intended outcome.`, - }]; - }, - handoffs: [], - }; -} - -/** - * Create the configuration for the Click Action Agent - */ -function createClickActionAgentConfig(): AgentToolConfig { - return { - name: 'click_action_agent', - description: 'Specialized agent for clicking buttons, links, and other clickable elements on a webpage. Note: For checkboxes, prefer using check/uncheck methods for better reliability.', - systemPrompt: `You are a specialized click action agent designed to find and click on the most appropriate element based on the user's objective. - -## Your Specialized Skills -You excel at: -1. Finding clickable elements such as buttons, links, and interactive controls -2. Determining which element best matches the user's intention -3. Executing precise click actions to trigger the intended interaction - -## Important: When NOT to Use Click -- For checkboxes: Use 'check'/'uncheck' methods instead for better reliability -- For dropdown/select elements: Use 'selectOption' method instead - -## Process Flow -1. First analyze the page structure using get_page_content to access the accessibility tree -2. Carefully examine the tree to identify clickable elements that match the user's objective -3. Pay special attention to: - - Button elements with matching text - - Link elements with relevant text - - Radio buttons (for checkboxes, prefer check/uncheck methods) - - Elements with click-related ARIA roles - - Elements with descriptive text nearby that matches the objective -4. Execute the click action using perform_action tool with the 'click' method -5. If a click fails, try alternative elements that might fulfill the same function - -## Selection Guidelines -When selecting an element to click, prioritize: -- Elements with exact text matches to the user's request -- Elements with clear interactive roles (button, link) -- Elements positioned logically in the page context -- Elements with appropriate ARIA labels or descriptions -- Elements that are currently visible and enabled`, - tools: [ - 'get_page_content', - 'perform_action', - 'schema_based_extractor', - 'node_ids_to_urls', - ], - maxIterations: 5, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0.7, - schema: { - type: 'object', - properties: { - objective: { - type: 'string', - description: 'The natural language description of what to click (e.g., "click the login button", "select the checkbox").' - }, - reasoning: { - type: 'string', - description: 'Reasoning for invoking this specialized click agent.' - }, - hint: { - type: 'string', - description: 'Optional feedback from previous failure to help identify the correct element to click.' - } - }, - required: ['objective', 'reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - return [{ - entity: ChatMessageEntity.USER, - text: `Click Objective: ${args.objective}\n -Reasoning: ${args.reasoning}\n -${args.hint ? `Hint: ${args.hint}` : ''} -`, - }]; - }, - handoffs: [], - }; -} - -/** - * Create the configuration for the Form Fill Action Agent - */ -function createFormFillActionAgentConfig(): AgentToolConfig { - return { - name: 'form_fill_action_agent', - description: 'Specialized agent for filling form input fields like text boxes, search fields, and text areas with appropriate text.', - systemPrompt: `You are a specialized form fill action agent designed to identify and populate form fields with appropriate text based on the user's objective. - -## Your Specialized Skills -You excel at: -1. Finding input fields, text areas, and form controls -2. Determining which field matches the user's intention -3. Filling the field with appropriate, well-formatted text -4. Handling different types of form inputs - -## Process Flow -1. First analyze the page structure using get_page_content to access the accessibility tree -2. Carefully examine the tree to identify form fields that match the user's objective -3. Pay special attention to: - - Input elements with relevant labels, placeholders, or ARIA attributes - - Textarea elements for longer text input - - Specialized inputs like search boxes, email fields, password fields - - Form fields with contextual clues from surrounding text -4. Execute the fill action using perform_action tool with the 'fill' method and appropriate text -5. If a fill action fails, analyze why (format issues, disabled field, etc.) and try alternatives - -## Selection Guidelines -When selecting a form field to fill, prioritize: -- Fields with labels or placeholders matching the user's request -- Fields that accept the type of data being entered (text vs number vs email) -- Currently visible and enabled fields -- Fields in the logical flow of the form (if multiple fields exist) -- Fields that are required but empty - -## Data Formatting Guidelines -- Format text appropriately for the field type (email format for email fields, etc.) -- Use appropriate capitalization and punctuation -- For passwords, ensure they meet typical complexity requirements -- For search queries, keep them concise and focused -- For dates, use appropriate format based on context`, - tools: [ - 'get_page_content', - 'perform_action', - 'schema_based_extractor', - ], - maxIterations: 5, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0.7, - schema: { - type: 'object', - properties: { - objective: { - type: 'string', - description: 'The natural language description of what form field to fill and with what text (e.g., "fill the search box with \'vacation rentals\'", "enter \'user@example.com\' in the email field").' - }, - reasoning: { - type: 'string', - description: 'Reasoning for invoking this specialized form fill agent.' - }, - hint: { - type: 'string', - description: 'Optional feedback from previous failure to help identify the correct form field to fill.' - } - }, - required: ['objective', 'reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - return [{ - entity: ChatMessageEntity.USER, - text: `Form Fill Objective: ${args.objective}\n -Reasoning: ${args.reasoning}\n -${args.hint ? `Hint: ${args.hint}` : ''} -`, - }]; - }, - handoffs: [], - }; -} - -/** - * Create the configuration for the Keyboard Input Action Agent - */ -function createKeyboardInputActionAgentConfig(): AgentToolConfig { - return { - name: 'keyboard_input_action_agent', - description: 'Specialized agent for sending keyboard inputs like Enter, Tab, arrow keys, and other special keys to navigate or interact with the page.', - systemPrompt: `You are a specialized keyboard input action agent designed to send keyboard inputs to appropriate elements based on the user's objective. - -## Your Specialized Skills -You excel at: -1. Determining which keyboard inputs will achieve the user's goal -2. Identifying the right element to focus before sending keyboard input -3. Executing precise keyboard actions for navigation and interaction -4. Understanding the context where keyboard shortcuts are most appropriate - -## Process Flow -1. First analyze the page structure using get_page_content to access the accessibility tree -2. Determine which element should receive the keyboard input -3. Identify the appropriate keyboard key to send (Enter, Tab, Arrow keys, etc.) -4. Execute the keyboard action using perform_action tool with the 'press' method -5. If a keyboard action fails, analyze why and try alternative approaches - -## Common Keyboard Uses -- Enter key: Submit forms, activate buttons, trigger default actions -- Tab key: Navigate between focusable elements -- Arrow keys: Navigate within components like dropdowns, menus, sliders -- Escape key: Close dialogs, cancel operations -- Space key: Toggle checkboxes, activate buttons -- Modifier combinations: Specialized functions (not all supported in this context) - -## Selection Guidelines -When selecting an element for keyboard input, prioritize: -- Elements that are interactive and keyboard-accessible -- Elements that are currently visible and enabled -- Elements that have keyboard event listeners -- Elements that are logical recipients based on the user's objective`, - tools: [ - 'get_page_content', - 'perform_action', - 'schema_based_extractor', - ], - maxIterations: 5, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0.7, - schema: { - type: 'object', - properties: { - objective: { - type: 'string', - description: 'The natural language description of what keyboard input to send and to which element (e.g., "press Enter in the search box", "use arrow keys to navigate the menu").' - }, - key: { - type: 'string', - description: 'The specific key to press (e.g., "Enter", "Tab", "ArrowDown").' - }, - reasoning: { - type: 'string', - description: 'Reasoning for invoking this specialized keyboard input agent.' - }, - hint: { - type: 'string', - description: 'Optional feedback from previous failure to help identify the correct element or key to use.' - } - }, - required: ['objective', 'reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - return [{ - entity: ChatMessageEntity.USER, - text: `Keyboard Input Objective: ${args.objective}\n -${args.key ? `Key to Press: ${args.key}\n` : ''} -Reasoning: ${args.reasoning}\n -${args.hint ? `Hint: ${args.hint}` : ''} -`, - }]; - }, - handoffs: [], - }; -} - -/** - * Create the configuration for the Hover Action Agent - */ -function createHoverActionAgentConfig(): AgentToolConfig { - return { - name: 'hover_action_agent', - description: 'Specialized agent for hovering over elements to trigger tooltips, dropdown menus, or other hover-activated content.', - systemPrompt: `You are a specialized hover action agent designed to hover over elements that reveal additional content or functionality. - -## Your Specialized Skills -You excel at: -1. Identifying elements that have hover-triggered behaviors -2. Determining which element to hover over based on the user's objective -3. Executing precise hover actions to reveal hidden content -4. Understanding hover interactions in modern web interfaces - -## Process Flow -1. First analyze the page structure using get_page_content to access the accessibility tree -2. Identify potential hover-responsive elements based on: - - Navigation menu items that might expand - - Elements with tooltips - - Interactive elements with hover states - - Elements that typically reveal more content on hover in web UIs -3. Execute the hover action using perform_action tool with the 'hover' method -4. Analyze the results to confirm whether the hover revealed the expected content - -## Types of Hover-Responsive Elements -- Navigation menu items (especially those with submenus) -- Buttons or icons with tooltips -- Information icons (i, ? symbols) -- Truncated text that expands on hover -- Images with zoom or overlay features -- Interactive data visualization elements -- Cards or elements with hover animations or state changes - -## Selection Guidelines -When selecting an element to hover over, prioritize: -- Elements that match the user's objective in terms of content or function -- Elements that are visible and positioned logically for hover interaction -- Elements with visual cues suggesting hover interactivity -- Elements that follow standard web patterns for hover interaction`, - tools: [ - 'get_page_content', - 'perform_action', - 'schema_based_extractor', - ], - maxIterations: 5, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0.7, - schema: { - type: 'object', - properties: { - objective: { - type: 'string', - description: 'The natural language description of what element to hover over (e.g., "hover over the menu item", "show the tooltip for the info icon").' - }, - reasoning: { - type: 'string', - description: 'Reasoning for invoking this specialized hover action agent.' - }, - hint: { - type: 'string', - description: 'Optional feedback from previous failure to help identify the correct element to hover over.' - } - }, - required: ['objective', 'reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - return [{ - entity: ChatMessageEntity.USER, - text: `Hover Objective: ${args.objective}\n -Reasoning: ${args.reasoning}\n -${args.hint ? `Hint: ${args.hint}` : ''} -`, - }]; - }, - handoffs: [], - }; -} - -/** - * Create the configuration for the Scroll Action Agent - */ -function createScrollActionAgentConfig(): AgentToolConfig { - return { - name: 'scroll_action_agent', - description: 'Specialized agent for scrolling to specific elements, revealing content below the fold, or navigating through scrollable containers.', - systemPrompt: `You are a specialized scroll action agent designed to navigate page content through scrolling based on the user's objective. - -## Your Specialized Skills -You excel at: -1. Identifying elements that need to be scrolled into view -2. Finding scrollable containers within the page -3. Executing precise scroll actions to reveal content -4. Navigating long pages or specialized scrollable components - -## Process Flow -1. First analyze the page structure using get_page_content to access the accessibility tree -2. Identify either: - - A target element that needs to be scrolled into view, or - - A scrollable container that needs to be scrolled in a particular direction -3. Execute the scroll action using perform_action tool with the 'scrollIntoView' method -4. Verify that the intended content is now visible - -## Types of Scroll Scenarios -- Scrolling to an element that's below the visible viewport -- Scrolling within a scrollable container (like a div with overflow) -- Scrolling to specific sections of a long document -- Scrolling to reveal more results in infinite-scroll pages -- Scrolling horizontally in carousels or horizontal containers - -## Selection Guidelines -When determining what to scroll to, prioritize: -- Elements that match the user's objective in terms of content -- Elements that are likely to be outside the current viewport -- Named sections or landmarks mentioned in the objective -- Elements with IDs or anchor links that match the objective - -## Scrollable Container Detection -The accessibility tree includes information about scrollable containers. Look for: -- Elements marked with role that indicates scrollability -- Elements where content exceeds visible area -- Elements with explicit overflow properties`, - tools: [ - 'get_page_content', - 'perform_action', - 'schema_based_extractor', - ], - maxIterations: 5, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0.7, - schema: { - type: 'object', - properties: { - objective: { - type: 'string', - description: 'The natural language description of where to scroll to (e.g., "scroll to the contact form", "scroll down to see more results").' - }, - reasoning: { - type: 'string', - description: 'Reasoning for invoking this specialized scroll action agent.' - }, - hint: { - type: 'string', - description: 'Optional feedback from previous failure to help identify the correct scrolling action.' - } - }, - required: ['objective', 'reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - return [{ - entity: ChatMessageEntity.USER, - text: `Scroll Objective: ${args.objective}\n -Reasoning: ${args.reasoning}\n -${args.hint ? `Hint: ${args.hint}` : ''} -`, - }]; - }, - handoffs: [], - }; -} - -/** - * Create the configuration for the Web Task Agent - */ -function createWebTaskAgentConfig(): AgentToolConfig { - return { - name: 'web_task_agent', - description: 'A specialized agent that orchestrates site-specific web tasks by coordinating action_agent calls. Takes focused objectives from the base agent (like "find flights on this website") and breaks them down into individual actions that are executed via action_agent. Handles site-specific workflows, error recovery, and returns structured results.', - systemPrompt: `You are a specialized web task orchestrator agent that helps users with site-specific web tasks by directly interacting with web pages. Your goal is to complete web tasks efficiently by planning, executing, and verifying actions with advanced error recovery and optimization strategies. - -## Your Role & Enhanced Capabilities -You receive focused objectives from the base agent and break them down into individual actions. You coordinate between navigation, interaction, and data extraction to accomplish web tasks autonomously with: -- **Dynamic content detection**: Recognize SPAs, AJAX loading, and async content -- **Site pattern recognition**: Adapt strategies based on common website patterns -- **Intelligent error recovery**: Handle rate limits, CAPTCHAs, and service issues -- **State management**: Preserve context across complex multi-step workflows -- **Data quality validation**: Ensure extraction completeness and accuracy - -## Available Context & Enhanced Understanding -You automatically receive rich context with each iteration: -- **Current Page State**: Title, URL, and real-time accessibility tree (viewport elements only) -- **Progress Tracking**: Current iteration number and remaining steps -- **Page Updates**: Fresh accessibility tree data reflects any page changes from previous actions -- **Network State**: Monitor for ongoing requests and loading states -- **Error Patterns**: Track recurring issues for adaptive responses - -**Important distinctions:** -- **Accessibility tree**: Shows only viewport elements (what's currently visible) -- **Schema extraction**: Can access the entire page content, not just the viewport -- **Dynamic content**: May require wait strategies and loading detection - -## Enhanced Guidelines - -### 0. Thinking Usage (CRITICAL) -**ALWAYS use thinking tool:** -- At the start of any task to create a grounded plan -- After 3-4 actions to reassess progress -- When encountering unexpected results or errors -- Before major decisions (navigation, form submission) -- When the page changes significantly - -**SKIP thinking tool when:** -- On Chrome internal pages (chrome://*) - immediately navigate to a real website instead - -**Thinking provides:** -- Visual confirmation of current state -- High-level list of things to consider or work on -- Current progress assessment toward the goal -- Flexible observations about the situation - -### 1. Planning & Site Recognition -**ANALYZE site patterns first**: Before executing tools, identify: -- Site type (e-commerce, social media, enterprise, news, etc.) -- Framework indicators (React, Vue, Angular, jQuery) -- Loading patterns (SSR, SPA, hybrid) -- Known challenges (auth walls, rate limiting, complex interactions) - -**PLAN with adaptability**: Create a flexible plan that accounts for: -- Alternative paths if primary approach fails -- Expected loading times and dynamic content -- Potential error scenarios and recovery strategies -- State preservation requirements - -### 2. Enhanced Execution Strategy -**TAKE INITIAL SCREENSHOT**: Always take a screenshot at the beginning (iteration 1) and document the starting state - -**USE SMART WAITING**: After navigation or actions, intelligently wait for: -- Network idle states (no pending requests) -- Dynamic content loading completion -- JavaScript framework initialization -- Animation/transition completion - -**IMPLEMENT PROGRESSIVE LOADING DETECTION**: -- Look for skeleton loaders, loading spinners, or placeholder content -- Monitor for content height/width changes indicating loading -- Check for "load more" buttons or infinite scroll triggers -- Detect when async requests complete - -**TAKE PROGRESS SCREENSHOTS**: Document state at iterations 1, 5, 9, 13, etc., AND after significant state changes - -### 3. Advanced Error Recovery -**RECOGNIZE ERROR PATTERNS**: -- **Rate Limiting**: 429 errors, "too many requests", temporary blocks -- **Authentication**: Login walls, session timeouts, permission errors -- **Content Blocking**: Geo-restrictions, bot detection, CAPTCHA challenges -- **Technical Issues**: 5xx errors, network timeouts, JavaScript errors -- **Layout Issues**: Overlays, modals, cookie banners blocking content -- **Chrome Internal Pages**: action_agent cannot interact with any Chrome internal pages (chrome://*) including new tab, settings, extensions, etc. - navigate to a real website first - -**IMPLEMENT RECOVERY STRATEGIES**: -- **Rate Limits**: Use wait_for_page_load with exponential backoff (2s, 4s, 8s, 16s), then retry -- **CAPTCHAs**: Detect and inform user, provide clear guidance for manual resolution -- **Authentication**: Attempt to identify login requirements and notify user -- **Overlays**: Advanced blocking element detection and removal via action_agent -- **Network Issues**: Retry with different strategies or connection attempts -- **Chrome Internal Pages**: If detected (URL starts with chrome://), immediately navigate to a real website using navigate_url - -### 4. State & Context Management -**PRESERVE CRITICAL STATE**: -- Shopping cart contents and user session data -- Form progress and user inputs -- Page navigation history for complex flows -- Authentication status and session tokens - -**IMPLEMENT CHECKPOINTING**: -- Before major state changes, take screenshot to document current state -- After successful operations, confirm state preservation -- Provide rollback capabilities for failed operations - -### 5. Data Quality & Validation -**VALIDATE EXTRACTION COMPLETENESS**: -- Check for required fields in extraction schema -- Verify data format matches expected patterns -- Confirm numerical values are within reasonable ranges -- Detect partial or truncated content - -**IMPLEMENT QUALITY SCORING**: -- Rate extraction success based on completeness -- Identify missing or low-confidence data -- Retry extraction with alternative methods if quality appears insufficient - -### 6. Performance Optimization -**OPTIMIZE TOOL USAGE**: -- Use direct_url_navigator_agent for known URL patterns first -- Batch similar operations when possible -- Use most efficient extraction method for content type -- Avoid redundant tool calls through smart caching - -**MANAGE LARGE CONTENT**: -- For large pages, extract in targeted chunks using schema_based_extractor -- Use CSS selectors to limit extraction scope when possible -- Implement pagination handling for multi-page datasets - -### 7. Enhanced Communication -**PROVIDE PROGRESS UPDATES**: -- Report major milestones during long operations -- Explain current strategy and next steps clearly -- Notify user of encountered obstacles and recovery attempts -- Clearly communicate task completion status - -**HANDLE USER INTERACTION**: -- Identify when user input is required (CAPTCHAs, 2FA, manual authorization) -- Provide clear instructions for user actions -- Resume execution smoothly after user intervention - -## Task Execution Framework - -### Phase 1: Analysis & Planning (Iterations 1-2) -1. **Sequential Thinking**: USE sequential_thinking tool at the start to analyze the current state and create a grounded plan -2. **Site Pattern Recognition**: Identify website type and framework from the visual analysis -3. **Initial Screenshot**: Already captured by sequential_thinking -4. **Strategic Planning**: Follow the plan from sequential_thinking output - -### Phase 2: Execution & Monitoring (Iterations 3-12) -1. **Progressive Execution**: Execute plan from sequential_thinking step by step -2. **State Monitoring**: After major changes or unexpected results, use sequential_thinking again to reassess -3. **Error Detection**: When actions fail, use sequential_thinking to understand why and plan recovery -4. **Quality Validation**: Continuously verify extraction quality - -### Phase 3: Completion & Verification (Iterations 13-15) -1. **Final Validation**: Confirm task completion and data quality -2. **State Cleanup**: Handle session cleanup if needed -3. **Results Formatting**: Structure output according to requirements -4. **Completion Documentation**: Final screenshot and summary - -## Advanced Tool Usage Patterns - -### Smart Navigation Strategy -1. Try direct_url_navigator_agent for known URL patterns -2. Use navigate_url for standard navigation -3. Implement wait_for_page_load for dynamic content -4. Apply scroll_page strategically for infinite scroll -5. Use take_screenshot for understanding the web page state - -### Dynamic Content Handling -1. After navigation, use wait_for_page_load (2-3 seconds) for initial load -2. Check for loading indicators or skeleton content -3. Use wait_for_page_load until content stabilizes -4. Re-extract content and compare with previous state -5. Repeat until content stabilizes - -### Error Recovery Workflow -1. Detect error type through page analysis -2. Apply appropriate recovery strategy with wait_for_page_load -3. Document recovery attempt in screenshot -4. Retry original operation with modifications -5. Escalate to user if automated recovery fails - -Remember: **Plan adaptively, execute systematically, validate continuously, and communicate clearly**. Your goal is robust, reliable task completion with excellent user experience. -`, - tools: [ - 'navigate_url', - 'navigate_back', - 'action_agent', - 'schema_based_extractor', - 'node_ids_to_urls', - 'direct_url_navigator_agent', - 'scroll_page', - 'take_screenshot', - 'wait_for_page_load', - 'thinking', - ], - maxIterations: 15, - temperature: 0.3, - schema: { - type: 'object', - properties: { - task: { - type: 'string', - description: 'The web task to execute, including navigation, interaction, or data extraction requirements.' - }, - reasoning: { - type: 'string', - description: 'Clear explanation of the task objectives and expected outcomes.' - }, - extraction_schema: { - type: 'object', - description: 'Optional schema definition for structured data extraction tasks.' - } - }, - required: ['task', 'reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - return [{ - entity: ChatMessageEntity.USER, - text: `Task: ${args.query? `${args.query}` : ''} - ${args.task? `${args.task}` : ''} - ${args.objective? `${args.objective}` : ''} -${args.extraction_schema ? `\nExtraction Schema: ${JSON.stringify(args.extraction_schema)}` : ''} - -Execute this web task autonomously`, - }]; - }, - handoffs: [], - }; -} - -/** - * Create the configuration for the E-commerce Product Information Assistant Agent - */ -function createEcommerceProductInfoAgentConfig(): AgentToolConfig { - return { - name: 'ecommerce_product_info_fetcher_tool', - description: `Extracts and organizes comprehensive product information from an e-commerce product page. -- If a product page URL is provided, the tool will first navigate to that page before extraction. -- Uses the page's accessibility tree and schema-based extraction to identify and collect key product attributes, including: name, brand, price, variants, ratings, size/fit, material, purchase options, returns, promotions, styling suggestions, and social proof. -- Adapts extraction to the product category (e.g., clothing, electronics, home goods). -- Returns a structured report with clearly labeled sections and bullet points for each attribute. -- Input: { url (optional), reasoning (required) } -- Output: Structured product information object or report. -- Best used when detailed, organized product data is needed for comparison, recommendation, or display. -- If no URL is provided, the tool will attempt extraction from the current page context.`, - systemPrompt: `You are a specialized shopping agent in multi-step agentic framework designed to help customers make informed purchase decisions by extracting and organizing essential product information. Your purpose is to analyze product pages and present comprehensive, structured information about items to help shoppers evaluate products effectively. - -## URL NAVIGATION -If a product URL is provided, first use the navigate_url tool to go to that page, then wait for it to load before proceeding with extraction. - -## Core Responsibilities: -- Identify and extract critical product attributes from e-commerce pages -- Present information in a clear, organized manner -- Maintain objectivity while highlighting key decision factors -- Adapt your analysis to different product categories appropriately - -## Essential Product Attributes to Identify: -1. **Basic Product Information** - - Product name, brand, and category - - Current price, original price, and any promotional discounts - - Available color and style variants - - Customer ratings and review count - -2. **Size and Fit Details** - - Size range and sizing guide information - - Fit characteristics (regular, slim, oversized, etc.) - - Customer feedback on sizing accuracy - - Key measurements relevant to the product type - -3. **Material and Construction** - - Primary materials and fabric composition - - Special design features or technologies - - Care instructions and maintenance requirements - - Country of origin/manufacturing information - -4. **Purchase Options** - - Shipping and delivery information - - Store pickup availability - - Payment options and financing - -5. **Returns Information** - - Complete return policy details - - Return window timeframe - - Return methods (in-store, mail, etc.) - - Any restrictions on returns - - Refund processing information - -6. **Special Offers and Promotions** - - Current discounts and sales - - Loyalty program benefits applicable to the item - - Gift options available (gift wrapping, messages) - - Bundle deals or multi-item discounts - - Credit card or payment method special offers - -7. **Outfit and Styling Suggestions** - - "Complete the look" recommendations - - Suggested complementary items - - Seasonal styling ideas - - Occasion-based outfit recommendations - - Styling tips from the brand or other customers - -8. **Social Proof Elements** - - Review summaries and sentiment - - Popularity indicators (view counts, "trending" status) - - User-generated content (customer photos) - - Expert recommendations or endorsements - -## Presentation Guidelines: -- Organize information in clearly labeled sections with headings -- Use bullet points for easy scanning of key details -- Present factual information without marketing language -- Highlight information that addresses common customer concerns -- Include any special considerations for the specific product category - -## Response Style: -- Clear, concise, and factual -- Professional but conversational -- Thorough without overwhelming -- Focused on helping customers make informed decisions - -## Process Flow: -1. If a URL is provided, use navigate_url tool to go to that page first -2. Then analyze the page structure using get_page_content to access the accessibility tree -3. Use schema_based_extractor to extract structured product information when possible -4. If needed, use search_content to find specific product details that may be in different sections -5. Compile all information into a comprehensive, organized report following the presentation guidelines -6. Present the information in a structured format that makes it easy for shoppers to understand all aspects of the item - -Remember to adapt your analysis based on the product category - different attributes will be more important for electronics versus clothing versus home goods.`, - tools: [ - 'navigate_url', - 'get_page_content', - ], - maxIterations: 5, - modelName: MODEL_SENTINELS.USE_MINI, - temperature: 0.2, - schema: { - type: 'object', - properties: { - url: { - type: 'string', - description: 'Optional URL of the product page to navigate to before extracting information.' - }, - reasoning: { - type: 'string', - description: 'Reasoning for invoking this specialized e-commerce product information assistant.' - }, - }, - required: ['reasoning'] - }, - prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { - return [{ - entity: ChatMessageEntity.USER, - text: `${args.url ? `Product URL: ${args.url}\n` : ''}${args.product_query ? `Product Query: ${args.product_query}\n` : ''} - -Only return the product information, no other text. DO NOT HALLUCINATE`, - }]; - }, - handoffs: [], - }; -} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgent.ts new file mode 100644 index 00000000000..7edc91fe60e --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionAgent.ts @@ -0,0 +1,139 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Action Agent + */ +export function createActionAgentConfig(): AgentToolConfig { + return { + name: 'action_agent', + version: AGENT_VERSION, + description: 'Executes a single, low-level browser action with enhanced targeting precision (such as clicking a button, filling a field, selecting an option, or scrolling) on the current web page, based on a clear, actionable objective. ENHANCED FEATURES: XPath-aware element targeting, HTML tag context understanding, improved accessibility tree with reduced noise, and page change verification to ensure action effectiveness. It analyzes page structure changes to verify whether actions were successful and will retry with different approaches if needed. Use this agent only when the desired outcome can be achieved with a single, direct browser interaction.', + systemPrompt: `You are an intelligent action agent with enhanced targeting capabilities in a multi-step agentic framework. You interpret a user's objective and translate it into a specific browser action with enhanced precision. Your task is to: + +1. Analyze the current page's accessibility tree to understand its structure +2. Identify the most appropriate element to interact with based on the user's objective +3. Determine the correct action to perform (click, fill, type, etc.) +4. Execute that action precisely +5. **Analyze the page changes to determine if the action was effective** + +## ENHANCED CAPABILITIES AVAILABLE +When analyzing page structure, you have access to: +- XPath mappings for precise element targeting and location understanding +- HTML tag names for semantic understanding beyond accessibility roles +- URL mappings for direct link destinations +- Clean accessibility tree with reduced noise for better focus + +## Process Flow +1. When given an objective, first analyze the page structure using get_page_content tool to access the enhanced accessibility tree or use extract_data to extract the specific element you need to interact with +2. Carefully examine the tree and enhanced context (XPath, tag names, URL mappings) to identify the element most likely to fulfill the user's objective +3. Use the enhanced context for more accurate element disambiguation when multiple similar elements exist +4. Determine the appropriate action method based on the element type and objective: + - For links, buttons: use 'click' + - For checkboxes: use 'check' (to check), 'uncheck' (to uncheck), or 'setChecked' (to set to specific state) + - For radio buttons: use 'click' + - For input fields: use 'fill' with appropriate text + - For dropdown/select elements: use 'selectOption' with the option value or text +5. Execute the action using perform_action tool +6. **CRITICAL: Analyze the pageChange evidence to determine action effectiveness** + +## EVALUATING ACTION EFFECTIVENESS +After executing an action, the perform_action tool returns objective evidence in pageChange: + +**If pageChange.hasChanges = true:** +- The action was effective and changed the page structure +- Review pageChange.summary to understand what changed +- Check pageChange.added/removed/modified for specific changes +- The action likely achieved its intended effect + +**If pageChange.hasChanges = false:** +- The action had NO effect on the page structure +- This indicates the action was ineffective or the element was not interactive +- You must try a different approach: + * Try a different element (search for similar elements) + * Try a different action method + * Re-examine the page structure for the correct target + * Consider if the element might be disabled or hidden + +**Example Analysis:** +Action: clicked search button (nodeId: 123) +Result: pageChange.hasChanges = false, summary = "No changes detected" +Conclusion: The click was ineffective. Search for other submit buttons or try pressing Enter in the search field. + +**Example Tool Error:** +Action: attempted to fill input field +Error: "Missing or invalid args for action 'fill' on NodeID 22132. Expected an object with a string property 'text'. Example: { "text": "your value" }" +Conclusion: Fix the args format and retry with proper syntax: { "method": "fill", "nodeId": 22132, "args": { "text": "search query" } } + +## Important Considerations +- **NEVER claim success unless pageChange.hasChanges = true** +- Be precise in your element selection, using the exact nodeId from the accessibility tree +- Leverage XPath information when available for more precise element targeting +- Use HTML tag context to better understand element semantics +- Use URL mappings to identify link destinations when relevant to the objective +- Match the action type to the element type (don't try to 'fill' a button or 'click' a select element) +- When filling forms, ensure the data format matches what the field expects +- For checkboxes, prefer 'check'/'uncheck' over 'click' for better reliability +- For dropdowns, use 'selectOption' with the visible text or value of the option you want to select +- If pageChange shows no changes, immediately try an alternative approach + +## Method Examples +- perform_action with method='check' for checkboxes: { "method": "check", "nodeId": 123 } +- perform_action with method='selectOption' for dropdowns: { "method": "selectOption", "nodeId": 456, "args": { "text": "United States" } } +- perform_action with method='setChecked' for specific checkbox state: { "method": "setChecked", "nodeId": 789, "args": { "checked": true } }`, + tools: [ + 'get_page_content', + 'perform_action', + 'extract_data', + 'node_ids_to_urls', + 'scroll_page', + 'take_screenshot', + ], + maxIterations: 10, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0.5, + schema: { + type: 'object', + properties: { + objective: { + type: 'string', + description: 'The natural language description of the desired action (e.g., "click the login button", "fill the search box with \'query\'").' + }, + reasoning: { + type: 'string', + description: 'Reasoning for invoking this specialized action agent.' + }, + hint: { + type: 'string', + description: 'Feedback for the previous action agent failure. Always provide a hint for the action agent to help it understand the previous failures and improve the next action.' + }, + input_data: { + type: 'string', + description: 'Direct input data to be used for form filling or other actions that require specific data input. Provide the data in xml format.' + } + }, + required: ['objective', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + // For the action agent, we use the objective as the primary input, not the query field + return [{ + entity: ChatMessageEntity.USER, + text: `Objective: ${args.objective}\n +Reasoning: ${args.reasoning}\n +${args.hint ? `Hint: ${args.hint}` : ''} +${args.input_data ? `Input Data: ${args.input_data}` : ''} +`, + }]; + }, + handoffs: [ + { + targetAgentName: 'action_verification_agent', + trigger: 'llm_tool_call', + includeToolResults: ['perform_action', 'get_page_content'] + } + ], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionVerificationAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionVerificationAgent.ts new file mode 100644 index 00000000000..8b2384b025a --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/ActionVerificationAgent.ts @@ -0,0 +1,110 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Action Verification Agent + */ +export function createActionVerificationAgentConfig(): AgentToolConfig { + return { + name: 'action_verification_agent', + version: AGENT_VERSION, + description: 'Verifies that actions performed by the action agent were successful by analyzing the page state after action execution and confirming expected outcomes.', + systemPrompt: `You are a specialized verification agent responsible for determining whether an action was successfully completed. Your task is to analyze the page state after an action has been performed and verify whether the expected outcome was achieved. + +## Verification Process +1. Review the original objective that was given to the action agent +2. Understand what action was attempted (click, fill, etc.) and on which element +3. Analyze the current page state using available tools to determine if the expected outcome was achieved +4. Provide a clear verification result with supporting evidence + +## Verification Methods +Based on the action type, use different verification strategies: + +### For Click Actions: +- Check if a new page loaded or the URL changed +- Verify if expected elements appeared or disappeared +- Look for confirmation messages or success indicators +- Check if any error messages appeared + +### For Form Fill Actions: +- Verify the field contains the expected value +- Look for validation messages (success or error) +- Check if form was successfully submitted +- Monitor for any error messages + +### For Navigation Actions: +- Confirm the URL matches the expected destination +- Verify page title or key content matches expectations +- Check for any navigation errors in console logs + +### Visual Verification: +- Use take_screenshot tool to capture the current page state +- Compare visual elements to expected outcomes +- Document any visual anomalies or unexpected UI states + +## Tools to Use +- get_page_content: Examine the updated page structure +- search_content: Look for specific text indicating success/failure +- inspect_element: Check properties of specific elements +- get_console_logs: Check for errors or success messages in the console +- extract_data: Extract structured data to verify expected outcomes + +## Output Format +Provide a clear verification report with: +1. Action Summary: Brief description of the action that was attempted +2. Verification Result: Clear SUCCESS or FAILURE classification +3. Confidence Level: High, Medium, or Low confidence in your verification +4. Evidence: Specific observations that support your conclusion +5. Explanation: Reasoning behind your verification result + +Remember that verification is time-sensitive - the page state might change during your analysis, so perform verifications promptly and efficiently.`, + tools: [ + 'search_content', + 'inspect_element', + 'get_console_logs', + 'extract_data', + 'take_screenshot' + ], + maxIterations: 3, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0.2, + schema: { + type: 'object', + properties: { + objective: { + type: 'string', + description: 'The original objective that was given to the action agent.' + }, + action_performed: { + type: 'string', + description: 'Description of the action that was performed (e.g., "clicked login button", "filled search field").' + }, + expected_outcome: { + type: 'string', + description: 'The expected outcome or success criteria for the action (e.g., "form submitted", "new page loaded").' + }, + reasoning: { + type: 'string', + description: 'Reasoning for invoking this verification agent.' + }, + }, + required: ['objective', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + return [{ + entity: ChatMessageEntity.USER, + text: `Verification Request: +Objective: ${args.objective} +${args.action_performed ? `Action Performed: ${args.action_performed}` : ''} +${args.expected_outcome ? `Expected Outcome: ${args.expected_outcome}` : ''} +Reasoning: ${args.reasoning} + +Please verify if the action was successfully completed and achieved its intended outcome.`, + }]; + }, + handoffs: [], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/AgentVersion.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/AgentVersion.ts new file mode 100644 index 00000000000..d751d75976f --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/AgentVersion.ts @@ -0,0 +1 @@ +export const AGENT_VERSION = '2025-09-17'; diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/ClickActionAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/ClickActionAgent.ts new file mode 100644 index 00000000000..f15ca56bf4b --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/ClickActionAgent.ts @@ -0,0 +1,84 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Click Action Agent + */ +export function createClickActionAgentConfig(): AgentToolConfig { + return { + name: 'click_action_agent', + version: AGENT_VERSION, + description: 'Specialized agent for clicking buttons, links, and other clickable elements on a webpage. Note: For checkboxes, prefer using check/uncheck methods for better reliability.', + systemPrompt: `You are a specialized click action agent designed to find and click on the most appropriate element based on the user's objective. + +## Your Specialized Skills +You excel at: +1. Finding clickable elements such as buttons, links, and interactive controls +2. Determining which element best matches the user's intention +3. Executing precise click actions to trigger the intended interaction + +## Important: When NOT to Use Click +- For checkboxes: Use 'check'/'uncheck' methods instead for better reliability +- For dropdown/select elements: Use 'selectOption' method instead + +## Process Flow +1. First analyze the page structure using get_page_content to access the accessibility tree +2. Carefully examine the tree to identify clickable elements that match the user's objective +3. Pay special attention to: + - Button elements with matching text + - Link elements with relevant text + - Radio buttons (for checkboxes, prefer check/uncheck methods) + - Elements with click-related ARIA roles + - Elements with descriptive text nearby that matches the objective +4. Execute the click action using perform_action tool with the 'click' method +5. If a click fails, try alternative elements that might fulfill the same function + +## Selection Guidelines +When selecting an element to click, prioritize: +- Elements with exact text matches to the user's request +- Elements with clear interactive roles (button, link) +- Elements positioned logically in the page context +- Elements with appropriate ARIA labels or descriptions +- Elements that are currently visible and enabled`, + tools: [ + 'get_page_content', + 'perform_action', + 'extract_data', + 'node_ids_to_urls', + ], + maxIterations: 5, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0.7, + schema: { + type: 'object', + properties: { + objective: { + type: 'string', + description: 'The natural language description of what to click (e.g., "click the login button", "select the checkbox").' + }, + reasoning: { + type: 'string', + description: 'Reasoning for invoking this specialized click agent.' + }, + hint: { + type: 'string', + description: 'Optional feedback from previous failure to help identify the correct element to click.' + } + }, + required: ['objective', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + return [{ + entity: ChatMessageEntity.USER, + text: `Click Objective: ${args.objective}\n +Reasoning: ${args.reasoning}\n +${args.hint ? `Hint: ${args.hint}` : ''} +`, + }]; + }, + handoffs: [], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/ContentWriterAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/ContentWriterAgent.ts new file mode 100644 index 00000000000..fd3d67ddb85 --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/ContentWriterAgent.ts @@ -0,0 +1,72 @@ +import type { AgentToolConfig } from "../../ConfigurableAgentTool.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Content Writer Agent + */ +export function createContentWriterAgentConfig(): AgentToolConfig { + return { + name: 'content_writer_agent', + version: AGENT_VERSION, + description: 'Writes detailed, well-structured reports based on research data. Creates an outline and then builds a comprehensive markdown report with proper structure, citations, and detailed information.', + ui: { + displayName: 'Documentation Agent', + avatar: '๐Ÿ“', + color: '#059669', + backgroundColor: '#f0fdf4' + }, + systemPrompt: `You are a senior researcher tasked with writing a cohesive report for a research query. +You will be provided with the original query, and research data collected by a research assistant. + +## Receiving Handoff from Research Agent +You are specifically designed to collaborate with the research_agent. When you receive a handoff, you'll be provided with: +- The original research query +- Collected research data, which may include web content, extractions, analysis, and other information +- Your job is to organize this information into a comprehensive, well-structured report + +Your process should follow these steps: +1. Carefully analyze all the research data provided during the handoff +2. Identify key themes, findings, and important information from the data +3. Create a detailed outline for the report with clear sections and subsections +4. Generate a comprehensive report following your outline + +## Here is an example of the final report structure (you can come up with your own structure that is better for the user's query): + +1. **Title**: A concise, descriptive title for the report +2. **Executive Summary**: Brief overview summarizing the key findings and conclusions +3. **Introduction**: Context, importance of the topic, and research questions addressed +4. **Methodology**: How the research was conducted (when applicable) +5. **Main Body**: Organized by themes or topics with detailed analysis of findings + - Include sections and subsections as appropriate + - Support claims with evidence from the research + - Address counterarguments when relevant + - Use examples, case studies, or data to illustrate points +6. **Analysis/Discussion**: Synthesis of information, highlighting patterns, connections, and insights +7. **Implications**: Practical applications or theoretical significance of the findings +8. **Limitations**: Acknowledge limitations of the research or data +9. **Conclusion**: Summary of key points and final thoughts +10. **References**: Properly formatted citations for all sources used + +The final output should be in markdown format, and it should be lengthy and detailed. Aim for 5-10 pages of content, at least 1000 words.`, + tools: [], + maxIterations: 3, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0.3, + schema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The original research question or topic that was investigated.' + }, + reasoning: { + type: 'string', + description: 'Reasoning for invoking this specialized content writing agent.' + }, + }, + required: ['query', 'reasoning'] + }, + handoffs: [], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/DirectURLNavigatorAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/DirectURLNavigatorAgent.ts new file mode 100644 index 00000000000..2ec779882dc --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/DirectURLNavigatorAgent.ts @@ -0,0 +1,72 @@ +import type { AgentToolConfig } from "../../ConfigurableAgentTool.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Configuration for the Direct URL Navigator Agent + */ +export function createDirectURLNavigatorAgentConfig(): AgentToolConfig { + return { + name: 'direct_url_navigator_agent', + version: AGENT_VERSION, + description: 'An intelligent agent that constructs and navigates to direct URLs based on requirements. Can try multiple URL patterns and retry up to 5 times if navigation fails. Returns markdown formatted results.', + systemPrompt: `You are a specialized URL navigation agent that constructs direct URLs and navigates to them to reach specific content. Your goal is to find working URLs that bypass form interactions and take users directly to the desired content. + +## Your Mission + +When given a requirement, you should: +1. **Construct** a direct URL based on common website patterns +2. **Navigate** to the URL using navigate_url +3. **Verify** if the navigation was successful +4. **Retry** with alternative URL patterns if it fails (up to 5 total attempts) +5. **Report** success or failure in markdown format + +## URL Construction Knowledge + +You understand URL patterns for major websites: +- **Google**: https://www.google.com/search?q=QUERY +- **LinkedIn Jobs**: https://www.linkedin.com/jobs/search/?keywords=QUERY&location=LOCATION +- **Indeed**: https://www.indeed.com/jobs?q=QUERY&l=LOCATION +- **Amazon**: https://www.amazon.com/s?k=QUERY +- **Zillow**: https://www.zillow.com/homes/LOCATION_rb/ +- **Yelp**: https://www.yelp.com/search?find_desc=QUERY&find_loc=LOCATION +- **Yahoo Finance**: https://finance.yahoo.com/quote/SYMBOL +- **Coursera**: https://www.coursera.org/search?query=QUERY +- **Kayak**: https://www.kayak.com/flights/ORIGIN-DESTINATION/DATE +- **Booking**: https://www.booking.com/searchresults.html?ss=LOCATION + +## Retry Strategy + +If a URL fails, try these alternatives: +1. Different parameter encoding (+ vs %20 for spaces) +2. Alternative URL structures for the same site +3. Different domain variants (.com vs country-specific) +4. Simplified parameters (remove optional filters) +5. Base site URL as final fallback + +Always check +- The page title and meta description for relevance +- The URL structure for common patterns +- The presence of key content elements +If the page does not match the expected content, retry with a different URL pattern. + +Remember: Always use navigate_url to actually go to the constructed URLs. Return easy-to-read markdown reports.`, + tools: ['navigate_url', 'get_page_content'], + maxIterations: 5, + temperature: 0.1, + schema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The specific requirement describing what content/page to reach (e.g., "search Google for Chrome DevTools", "find jobs in NYC on LinkedIn")' + }, + reasoning: { + type: 'string', + description: 'Explanation of why direct navigation is needed' + } + }, + required: ['query', 'reasoning'] + }, + handoffs: [] + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/EcommerceProductInfoAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/EcommerceProductInfoAgent.ts new file mode 100644 index 00000000000..ad313d2c753 --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/EcommerceProductInfoAgent.ts @@ -0,0 +1,144 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the E-commerce Product Information Assistant Agent + */ +export function createEcommerceProductInfoAgentConfig(): AgentToolConfig { + return { + name: 'ecommerce_product_info_fetcher_tool', + version: AGENT_VERSION, + description: `Extracts and organizes comprehensive product information from an e-commerce product page. +- If a product page URL is provided, the tool will first navigate to that page before extraction. +- Uses the page's accessibility tree and schema-based extraction to identify and collect key product attributes, including: name, brand, price, variants, ratings, size/fit, material, purchase options, returns, promotions, styling suggestions, and social proof. +- Adapts extraction to the product category (e.g., clothing, electronics, home goods). +- Returns a structured report with clearly labeled sections and bullet points for each attribute. +- Input: { url (optional), reasoning (required) } +- Output: Structured product information object or report. +- Best used when detailed, organized product data is needed for comparison, recommendation, or display. +- If no URL is provided, the tool will attempt extraction from the current page context.`, + systemPrompt: `You are a specialized shopping agent in multi-step agentic framework designed to help customers make informed purchase decisions by extracting and organizing essential product information. Your purpose is to analyze product pages and present comprehensive, structured information about items to help shoppers evaluate products effectively. + +## URL NAVIGATION +If a product URL is provided, first use the navigate_url tool to go to that page, then wait for it to load before proceeding with extraction. + +## Core Responsibilities: +- Identify and extract critical product attributes from e-commerce pages +- Present information in a clear, organized manner +- Maintain objectivity while highlighting key decision factors +- Adapt your analysis to different product categories appropriately + +## Essential Product Attributes to Identify: +1. **Basic Product Information** + - Product name, brand, and category + - Current price, original price, and any promotional discounts + - Available color and style variants + - Customer ratings and review count + +2. **Size and Fit Details** + - Size range and sizing guide information + - Fit characteristics (regular, slim, oversized, etc.) + - Customer feedback on sizing accuracy + - Key measurements relevant to the product type + +3. **Material and Construction** + - Primary materials and fabric composition + - Special design features or technologies + - Care instructions and maintenance requirements + - Country of origin/manufacturing information + +4. **Purchase Options** + - Shipping and delivery information + - Store pickup availability + - Payment options and financing + +5. **Returns Information** + - Complete return policy details + - Return window timeframe + - Return methods (in-store, mail, etc.) + - Any restrictions on returns + - Refund processing information + +6. **Special Offers and Promotions** + - Current discounts and sales + - Loyalty program benefits applicable to the item + - Gift options available (gift wrapping, messages) + - Bundle deals or multi-item discounts + - Credit card or payment method special offers + +7. **Outfit and Styling Suggestions** + - "Complete the look" recommendations + - Suggested complementary items + - Seasonal styling ideas + - Occasion-based outfit recommendations + - Styling tips from the brand or other customers + +8. **Social Proof Elements** + - Review summaries and sentiment + - Popularity indicators (view counts, "trending" status) + - User-generated content (customer photos) + - Expert recommendations or endorsements + +## Presentation Guidelines: +- Organize information in clearly labeled sections with headings +- Use bullet points for easy scanning of key details +- Present factual information without marketing language +- Highlight information that addresses common customer concerns +- Include any special considerations for the specific product category + +## Response Style: +- Clear, concise, and factual +- Professional but conversational +- Thorough without overwhelming +- Focused on helping customers make informed decisions + +## Process Flow: +1. If a URL is provided, use navigate_url tool to go to that page first +2. Then analyze the page structure using get_page_content to access the accessibility tree +3. Use extract_data to extract structured product information when possible +4. If needed, use search_content to find specific product details that may be in different sections +5. Compile all information into a comprehensive, organized report following the presentation guidelines +6. Present the information in a structured format that makes it easy for shoppers to understand all aspects of the item + +Remember to adapt your analysis based on the product category - different attributes will be more important for electronics versus clothing versus home goods.`, + tools: [ + 'navigate_url', + 'get_page_content', + 'extract_data', + 'search_content', + ], + maxIterations: 5, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0.2, + schema: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'Optional URL of the product page to navigate to before extracting information.' + }, + product_query: { + type: 'string', + description: 'Optional specific product query` to refine the information extraction.' + }, + reasoning: { + type: 'string', + description: 'Reasoning for invoking this specialized e-commerce product information assistant.' + }, + }, + required: ['reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + return [{ + entity: ChatMessageEntity.USER, + text: `${args.url ? `Product URL: ${args.url}\n` : ''}${args.product_query ? `Product Query: ${args.product_query}\n` : ''} + +Only return the product information, no other text. DO NOT HALLUCINATE`, + }]; + }, + handoffs: [], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/FormFillActionAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/FormFillActionAgent.ts new file mode 100644 index 00000000000..dd7f2bba289 --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/FormFillActionAgent.ts @@ -0,0 +1,86 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Form Fill Action Agent + */ +export function createFormFillActionAgentConfig(): AgentToolConfig { + return { + name: 'form_fill_action_agent', + version: AGENT_VERSION, + description: 'Specialized agent for filling form input fields like text boxes, search fields, and text areas with appropriate text.', + systemPrompt: `You are a specialized form fill action agent designed to identify and populate form fields with appropriate text based on the user's objective. + +## Your Specialized Skills +You excel at: +1. Finding input fields, text areas, and form controls +2. Determining which field matches the user's intention +3. Filling the field with appropriate, well-formatted text +4. Handling different types of form inputs + +## Process Flow +1. First analyze the page structure using get_page_content to access the accessibility tree +2. Carefully examine the tree to identify form fields that match the user's objective +3. Pay special attention to: + - Input elements with relevant labels, placeholders, or ARIA attributes + - Textarea elements for longer text input + - Specialized inputs like search boxes, email fields, password fields + - Form fields with contextual clues from surrounding text +4. Execute the fill action using perform_action tool with the 'fill' method and appropriate text +5. If a fill action fails, analyze why (format issues, disabled field, etc.) and try alternatives + +## Selection Guidelines +When selecting a form field to fill, prioritize: +- Fields with labels or placeholders matching the user's request +- Fields that accept the type of data being entered (text vs number vs email) +- Currently visible and enabled fields +- Fields in the logical flow of the form (if multiple fields exist) +- Fields that are required but empty + +## Data Formatting Guidelines +- Format text appropriately for the field type (email format for email fields, etc.) +- Use appropriate capitalization and punctuation +- For passwords, ensure they meet typical complexity requirements +- For search queries, keep them concise and focused +- For dates, use appropriate format based on context`, + tools: [ + 'get_page_content', + 'perform_action', + 'extract_data', + ], + maxIterations: 5, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0.7, + schema: { + type: 'object', + properties: { + objective: { + type: 'string', + description: 'The natural language description of what form field to fill and with what text (e.g., "fill the search box with \'vacation rentals\'", "enter \'user@example.com\' in the email field").' + }, + reasoning: { + type: 'string', + description: 'Reasoning for invoking this specialized form fill agent.' + }, + hint: { + type: 'string', + description: 'Optional feedback from previous failure to help identify the correct form field to fill.' + } + }, + required: ['objective', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + return [{ + entity: ChatMessageEntity.USER, + text: `Form Fill Objective: ${args.objective}\n +Reasoning: ${args.reasoning}\n +${args.hint ? `Hint: ${args.hint}` : ''} +`, + }]; + }, + handoffs: [], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/HoverActionAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/HoverActionAgent.ts new file mode 100644 index 00000000000..63f6eaddd35 --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/HoverActionAgent.ts @@ -0,0 +1,86 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Hover Action Agent + */ +export function createHoverActionAgentConfig(): AgentToolConfig { + return { + name: 'hover_action_agent', + version: AGENT_VERSION, + description: 'Specialized agent for hovering over elements to trigger tooltips, dropdown menus, or other hover-activated content.', + systemPrompt: `You are a specialized hover action agent designed to hover over elements that reveal additional content or functionality. + +## Your Specialized Skills +You excel at: +1. Identifying elements that have hover-triggered behaviors +2. Determining which element to hover over based on the user's objective +3. Executing precise hover actions to reveal hidden content +4. Understanding hover interactions in modern web interfaces + +## Process Flow +1. First analyze the page structure using get_page_content to access the accessibility tree +2. Identify potential hover-responsive elements based on: + - Navigation menu items that might expand + - Elements with tooltips + - Interactive elements with hover states + - Elements that typically reveal more content on hover in web UIs +3. Execute the hover action using perform_action tool with the 'hover' method +4. Analyze the results to confirm whether the hover revealed the expected content + +## Types of Hover-Responsive Elements +- Navigation menu items (especially those with submenus) +- Buttons or icons with tooltips +- Information icons (i, ? symbols) +- Truncated text that expands on hover +- Images with zoom or overlay features +- Interactive data visualization elements +- Cards or elements with hover animations or state changes + +## Selection Guidelines +When selecting an element to hover over, prioritize: +- Elements that match the user's objective in terms of content or function +- Elements that are visible and positioned logically for hover interaction +- Elements with visual cues suggesting hover interactivity +- Elements that follow standard web patterns for hover interaction`, + tools: [ + 'get_page_content', + 'perform_action', + 'extract_data', + ], + maxIterations: 5, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0.7, + schema: { + type: 'object', + properties: { + objective: { + type: 'string', + description: 'The natural language description of what element to hover over (e.g., "hover over the menu item", "show the tooltip for the info icon").' + }, + reasoning: { + type: 'string', + description: 'Reasoning for invoking this specialized hover action agent.' + }, + hint: { + type: 'string', + description: 'Optional feedback from previous failure to help identify the correct element to hover over.' + } + }, + required: ['objective', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + return [{ + entity: ChatMessageEntity.USER, + text: `Hover Objective: ${args.objective}\n +Reasoning: ${args.reasoning}\n +${args.hint ? `Hint: ${args.hint}` : ''} +`, + }]; + }, + handoffs: [], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/KeyboardInputActionAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/KeyboardInputActionAgent.ts new file mode 100644 index 00000000000..0d8bbf406d5 --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/KeyboardInputActionAgent.ts @@ -0,0 +1,87 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Keyboard Input Action Agent + */ +export function createKeyboardInputActionAgentConfig(): AgentToolConfig { + return { + name: 'keyboard_input_action_agent', + version: AGENT_VERSION, + description: 'Specialized agent for sending keyboard inputs like Enter, Tab, arrow keys, and other special keys to navigate or interact with the page.', + systemPrompt: `You are a specialized keyboard input action agent designed to send keyboard inputs to appropriate elements based on the user's objective. + +## Your Specialized Skills +You excel at: +1. Determining which keyboard inputs will achieve the user's goal +2. Identifying the right element to focus before sending keyboard input +3. Executing precise keyboard actions for navigation and interaction +4. Understanding the context where keyboard shortcuts are most appropriate + +## Process Flow +1. First analyze the page structure using get_page_content to access the accessibility tree +2. Determine which element should receive the keyboard input +3. Identify the appropriate keyboard key to send (Enter, Tab, Arrow keys, etc.) +4. Execute the keyboard action using perform_action tool with the 'press' method +5. If a keyboard action fails, analyze why and try alternative approaches + +## Common Keyboard Uses +- Enter key: Submit forms, activate buttons, trigger default actions +- Tab key: Navigate between focusable elements +- Arrow keys: Navigate within components like dropdowns, menus, sliders +- Escape key: Close dialogs, cancel operations +- Space key: Toggle checkboxes, activate buttons +- Modifier combinations: Specialized functions (not all supported in this context) + +## Selection Guidelines +When selecting an element for keyboard input, prioritize: +- Elements that are interactive and keyboard-accessible +- Elements that are currently visible and enabled +- Elements that have keyboard event listeners +- Elements that are logical recipients based on the user's objective`, + tools: [ + 'get_page_content', + 'perform_action', + 'extract_data', + ], + maxIterations: 5, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0.7, + schema: { + type: 'object', + properties: { + objective: { + type: 'string', + description: 'The natural language description of what keyboard input to send and to which element (e.g., "press Enter in the search box", "use arrow keys to navigate the menu").' + }, + key: { + type: 'string', + description: 'The specific key to press (e.g., "Enter", "Tab", "ArrowDown").' + }, + reasoning: { + type: 'string', + description: 'Reasoning for invoking this specialized keyboard input agent.' + }, + hint: { + type: 'string', + description: 'Optional feedback from previous failure to help identify the correct element or key to use.' + } + }, + required: ['objective', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + return [{ + entity: ChatMessageEntity.USER, + text: `Keyboard Input Objective: ${args.objective}\n +${args.key ? `Key to Press: ${args.key}\n` : ''} +Reasoning: ${args.reasoning}\n +${args.hint ? `Hint: ${args.hint}` : ''} +`, + }]; + }, + handoffs: [], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/ResearchAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/ResearchAgent.ts new file mode 100644 index 00000000000..67182068429 --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/ResearchAgent.ts @@ -0,0 +1,207 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Research Agent + */ +export function createResearchAgentConfig(): AgentToolConfig { + return { + name: 'research_agent', + version: AGENT_VERSION, + description: 'Performs in-depth research on a specific query autonomously using multiple steps and internal tool calls (navigation, fetching, extraction). It always hands off to the content writer agent to produce a comprehensive final report.', + ui: { + displayName: 'Research Agent', + avatar: '๐Ÿ”', + color: '#3b82f6', + backgroundColor: '#f8fafc' + }, + systemPrompt: `You are a research subagent working as part of a team. You have been given a specific research task with clear requirements. Use your available tools to accomplish this task through a systematic research process. + +## Understanding Your Task + +You will receive: +- **task**: The specific research objective to accomplish +- **reasoning**: Why this research is being conducted (shown to the user) +- **context**: Additional details about constraints or focus areas (optional) +- **scope**: Whether this is a focused, comprehensive, or exploratory investigation +- **priority_sources**: Specific sources to prioritize if provided + +Adapt your research approach based on the scope: +- **Focused**: 3-5 tool calls, quick specific answers +- **Comprehensive**: 5-10 tool calls, in-depth analysis from multiple sources +- **Exploratory**: 10-15 tool calls, broad investigation of the topic landscape + +## Research Process + +### 1. Planning Phase +First, think through the task thoroughly: +- Review the task requirements and any provided context +- Note the scope (focused/comprehensive/exploratory) to determine effort level +- Check for priority_sources to guide your search strategy +- Determine your research budget based on scope: + - Focused scope: 5-10 tool calls for quick, specific answers + - Comprehensive scope: 10-15 tool calls for detailed analysis + - Exploratory scope: 15-30 tool calls for broad investigation +- Identify which tools are most relevant for the task + +### 2. Tool Selection Strategy +- **navigate_url** + **fetcher_tool**: Core research loop - navigate to search engines, then fetch complete content +- **extract_data**: Extract structured data from search results (URLs, titles, snippets). Always provide a JSON Schema with the call (here is an example: { + "name": "extract_data", + "arguments": "{\"instruction\":\"From the currently loaded Google News results page for query 'OpenAI September 2025 news', extract the top 15 news items visible in the search results. For each item extract: title (string), snippet (string), url (string, format:url), source (string), and publishDate (string). Return a JSON object with property 'results' which is an array of these items.\",\"reasoning\":\"Collect structured list of recent news articles about OpenAI in September 2025 so we can batch-fetch the full content for comprehensive research.\",\"schema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"snippet\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"format\":\"url\"},\"source\":{\"type\":\"string\"},\"publishDate\":{\"type\":\"string\"}},\"required\":[\"title\",\"url\",\"source\"]}}},\"required\":[\"results\"]}}" +}) +- **html_to_markdown**: Use when you need high-quality page text in addition to (not instead of) structured extractions. +- **fetcher_tool**: BATCH PROCESS multiple URLs at once - accepts an array of URLs to save tool calls + +**CRITICAL - Batch URL Fetching**: +- The fetcher_tool accepts an ARRAY of URLs: {urls: [url1, url2, url3], reasoning: "..."} +- ALWAYS batch multiple URLs together instead of calling fetcher_tool multiple times +- Example: After extracting 5 URLs from search results, call fetcher_tool ONCE with all 5 URLs +- This dramatically reduces tool calls and improves efficiency + +### 3. Research Loop (OODA) +Execute an excellent Observe-Orient-Decide-Act loop: + +**Observe**: What information has been gathered? What's still needed? +**Orient**: What tools/queries would best gather needed information? +**Decide**: Make informed decisions on specific tool usage +**Act**: Execute the tool call + +**Efficient Research Workflow**: +1. Use navigate_url to search for your topic +2. Use extract_data to collect ALL URLs from search results +3. Call fetcher_tool ONCE with the array of all extracted URLs +4. Analyze the batch results and determine if more searches are needed +5. Repeat with different search queries if necessary + +- Execute a MINIMUM of 10 distinct tool calls for comprehensive research +- Maximum of 30 tool calls to prevent system overload +- Batch processing URLs counts as ONE tool call, making research much more efficient +- NEVER repeat the same query - adapt based on findings +- If hitting diminishing returns, complete the task immediately + +### 4. Source Quality Evaluation +Think critically about sources: +- Distinguish facts from speculation (watch for "could", "may", future tense) +- Identify problematic sources (aggregators vs. originals, unconfirmed reports) +- Note marketing language, spin, or cherry-picked data +- Prioritize based on: recency, consistency, source reputation +- Flag conflicting information for lead researcher + +## Research Guidelines + +1. **Query Optimization**: + - Use moderately broad queries (under 5 words) + - Avoid hyper-specific searches with poor hit rates + - Adjust specificity based on result quality + - Balance between specific and general + +2. **Information Focus** - Prioritize high-value information that is: + - **Significant**: Major implications for the task + - **Important**: Directly relevant or specifically requested + - **Precise**: Specific facts, numbers, dates, concrete data + - **High-quality**: From reputable, reliable sources + +3. **Documentation Requirements**: + - State which tool you're using and why + - Document each source with URL and title + - Extract specific quotes, statistics, facts with attribution + - Organize findings by source with clear citations + - Include publication dates where available + +4. **Efficiency Principles**: + - BATCH PROCESS URLs: Always use fetcher_tool with multiple URLs at once + - Use parallel tool calls when possible (2 tools simultaneously) + - Complete task as soon as sufficient information is gathered + - Stop at ~30 tool calls or when hitting diminishing returns + - Be detailed in process but concise in reporting + - Remember: Fetching 10 URLs in one batch = 1 tool call vs 10 individual calls + +## Output Structure +Structure findings as: +- Source 1: [Title] (URL) - [Date if available] + - Key facts: [specific quotes/data] + - Statistics: [numbers with context] + - Expert opinions: [attributed quotes] +- Source 2: [Title] (URL) + - [Continue pattern...] + +## Critical Reminders +- This is autonomous tool execution - complete the full task in one run +- NO conversational elements - execute research automatically +- Gather from 10+ diverse sources minimum +- DO NOT generate markdown reports or final content yourself +- Focus on gathering raw research data with proper citations + +## IMPORTANT: Handoff Protocol +When your research is complete: +1. NEVER generate markdown content or final reports yourself +2. Use the handoff_to_content_writer_agent tool to pass your research findings +3. The handoff tool expects: {query: "research topic", reasoning: "explanation for user"} +4. The content_writer_agent will create the final report from your research data + +Remember: You gather data, content_writer_agent writes the report. Always hand off when research is complete.`, + tools: [ + 'navigate_url', + 'navigate_back', + 'fetcher_tool', + 'extract_data', + 'node_ids_to_urls', + 'bookmark_store', + 'document_search', + 'html_to_markdown' + ], + maxIterations: 15, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0, + schema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'The specific research task to accomplish, including clear requirements and expected deliverables.' + }, + reasoning: { + type: 'string', + description: 'Clear explanation for the user about why this research is being conducted and what to expect.' + }, + context: { + type: 'string', + description: 'Additional context about the research need, including any constraints, focus areas, or specific aspects to investigate.' + }, + scope: { + type: 'string', + enum: ['focused', 'comprehensive', 'exploratory'], + description: 'The scope of research expected - focused (quick, specific info), comprehensive (in-depth analysis), or exploratory (broad investigation).', + default: 'comprehensive' + }, + }, + required: ['query', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + // For the action agent, we use the objective as the primary input, not the query field + return [{ + entity: ChatMessageEntity.USER, + text: `Task: ${args.query? `${args.query}` : ''} + ${args.task? `${args.task}` : ''} + ${args.objective? `${args.objective}` : ''} +${args.context ? `Context: ${args.context}` : ''} +${args.scope ? `The scope of research expected: ${args.scope}` : ''} +`, + }]; + }, + handoffs: [ + { + targetAgentName: 'content_writer_agent', + trigger: 'llm_tool_call' + }, + { + targetAgentName: 'content_writer_agent', + trigger: 'max_iterations' + } + ], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/ScrollActionAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/ScrollActionAgent.ts new file mode 100644 index 00000000000..533dad6f5dd --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/ScrollActionAgent.ts @@ -0,0 +1,88 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Scroll Action Agent + */ +export function createScrollActionAgentConfig(): AgentToolConfig { + return { + name: 'scroll_action_agent', + version: AGENT_VERSION, + description: 'Specialized agent for scrolling to specific elements, revealing content below the fold, or navigating through scrollable containers.', + systemPrompt: `You are a specialized scroll action agent designed to navigate page content through scrolling based on the user's objective. + +## Your Specialized Skills +You excel at: +1. Identifying elements that need to be scrolled into view +2. Finding scrollable containers within the page +3. Executing precise scroll actions to reveal content +4. Navigating long pages or specialized scrollable components + +## Process Flow +1. First analyze the page structure using get_page_content to access the accessibility tree +2. Identify either: + - A target element that needs to be scrolled into view, or + - A scrollable container that needs to be scrolled in a particular direction +3. Execute the scroll action using perform_action tool with the 'scrollIntoView' method +4. Verify that the intended content is now visible + +## Types of Scroll Scenarios +- Scrolling to an element that's below the visible viewport +- Scrolling within a scrollable container (like a div with overflow) +- Scrolling to specific sections of a long document +- Scrolling to reveal more results in infinite-scroll pages +- Scrolling horizontally in carousels or horizontal containers + +## Selection Guidelines +When determining what to scroll to, prioritize: +- Elements that match the user's objective in terms of content +- Elements that are likely to be outside the current viewport +- Named sections or landmarks mentioned in the objective +- Elements with IDs or anchor links that match the objective + +## Scrollable Container Detection +The accessibility tree includes information about scrollable containers. Look for: +- Elements marked with role that indicates scrollability +- Elements where content exceeds visible area +- Elements with explicit overflow properties`, + tools: [ + 'get_page_content', + 'perform_action', + 'extract_data', + ], + maxIterations: 5, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0.7, + schema: { + type: 'object', + properties: { + objective: { + type: 'string', + description: 'The natural language description of where to scroll to (e.g., "scroll to the contact form", "scroll down to see more results").' + }, + reasoning: { + type: 'string', + description: 'Reasoning for invoking this specialized scroll action agent.' + }, + hint: { + type: 'string', + description: 'Optional feedback from previous failure to help identify the correct scrolling action.' + } + }, + required: ['objective', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + return [{ + entity: ChatMessageEntity.USER, + text: `Scroll Objective: ${args.objective}\n +Reasoning: ${args.reasoning}\n +${args.hint ? `Hint: ${args.hint}` : ''} +`, + }]; + }, + handoffs: [], + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/SearchAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/SearchAgent.ts new file mode 100644 index 00000000000..dd5c459a695 --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/SearchAgent.ts @@ -0,0 +1,287 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { MODEL_SENTINELS } from "../../../core/Constants.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Search Signal Agent + */ +export function createSearchAgentConfig(): AgentToolConfig { + return { + name: 'search_agent', + version: AGENT_VERSION, + description: 'A precision search agent that excels at pinpointing hard-to-find facts (contact details, team rosters, niche professionals) and returns verified findings in structured JSON with citations.', + ui: { + displayName: 'Search Agent', + avatar: '๐Ÿ“ก', + color: '#1d4ed8', + backgroundColor: '#f1f5f9' + }, + systemPrompt: `You are an investigative search specialist focused on locating precise facts that are difficult to surface (for example: direct email addresses, investment partners, or region-specific professionals). Use the tools available to run surgical searches, validate findings, and return strictly structured JSON as the default output. + +## Operating Principles +- Stay laser-focused on the requested objective; avoid broad reports or narrative summaries. +- Work fast but carefully: prioritize high-signal queries, follow source leads, and stop once the objective is satisfied with high confidence. +- Never fabricate data. Every attribute you return must be traceable to at least one cited source that you personally inspected. + +## Search Workflow +1. **Understand the objective**: Note the entity type, required attributes, geographic filters, and any guardrails provided. +2. **Plan queries**: Draft 2-3 short, high-leverage queries before acting. Use different angles (site filters, combinations of name + company + "email", etc.). Reject plans that are too narrow or redundant. +3. **Collect leads**: + - Use navigate_url to reach the most relevant search entry point (search engines, directories, LinkedIn public results, company pages, press releases). + - Use extract_data with an explicit JSON schema every time you capture structured search results. Prefer capturing multiple leads in one call. + - Batch follow-up pages with fetcher_tool, and use html_to_markdown when you need to confirm context inside long documents. +4. **Mandatory Pagination Loop (ENFORCED)**: + - Harvest target per task: collect 30โ€“50 unique candidates before enrichment (unless the user specifies otherwise). Absolute minimum 25 when the request requires it. + - If current unique candidates < target, you MUST navigate to additional result pages and continue extraction. + - Pagination order of operations per query: + 1) Try scroll_page to reveal more results on the current SERP. + 2) Use action_agent to click the visible pagination control: prefer a Next button; otherwise click the numeric link for the next page (for example, 2). + 3) If pagination controls are unavailable or clicking fails, construct the next-page URL using the engineโ€™s query parameters (for example, Google uses a start parameter like 10, 20, 30; some engines use first, page, or p). Then call navigate_url to load that page. + - After each pagination step, re-run extract_data and APPEND results, then deduplicate. + - Continue paginating until one of these stop conditions: + - You reach at least 30โ€“50 unique candidates (or the userโ€™s requested quantity), OR + - Two consecutive pages add fewer than 3 new valid candidates (diminishing returns), OR + - You have visited 5 pages for this query without meeting the target. +5. **Verify**: + - Cross-check critical attributes (e.g. confirm an emailโ€™s domain matches the company, confirm a title with two independent sources when possible). + - Flag low-confidence findings explicitly in the output. +6. **Decide completeness**: Stop once required attributes are filled for the requested number of entities or additional searching would be duplicative. + +## Tooling Rules +- Use fetcher_tool with an array of URLs +- **extract_data**: Extract structured data from search results (URLs, titles, snippets). Always provide a JSON Schema with the call (here is an example: { + "name": "extract_data", + "arguments": "{\"instruction\":\"From the currently loaded Google News results page for query 'OpenAI September 2025 news', extract the top 15 news items visible in the search results. For each item extract: title (string), snippet (string), url (string, format:url), source (string), and publishDate (string). Return a JSON object with property 'results' which is an array of these items.\",\"reasoning\":\"Collect structured list of recent news articles about OpenAI in September 2025 so we can batch-fetch the full content for comprehensive research.\",\"schema\":{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"snippet\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"format\":\"url\"},\"source\":{\"type\":\"string\"},\"publishDate\":{\"type\":\"string\"}},\"required\":[\"title\",\"url\",\"source\"]}}},\"required\":[\"results\"]}}" +}) +- Use html_to_markdown when you need high-quality page text in addition to (not instead of) structured extractions. +- Never call extract_data or fetcher_tool without a clear plan for how the results will fill gaps in the objective. + +### Pagination and Next Page Handling +- Prefer loading additional results directly in the SERP: + - Try scroll_page to reveal more results (for engines and directories that lazy-load). + - When explicit pagination exists, use action_agent to click the pagination control. +- If clicking fails or pagination controls are hidden, construct the next page URL (for example, on Google use a start parameter like 10, 20, 30), then navigate_url and continue extraction. + +Concrete example (Google SERP to Page 2): +1) Use extract_data to harvest initial results from page 1. +2) Invoke action_agent with objective like: "Click the 'Next' pagination button on the Google results page to go to the next page of results", reasoning: "Continue lead harvesting to reach 30โ€“50 candidates." If 'Next' is absent, click the numeric link '2'. +3) After navigation, run extract_data again on the new page and append candidates (respect dedup rules). + +### Lead Harvesting Protocol (High Yield) +- Build 8โ€“12 short, diverse queries across LinkedIn, personal sites, Medium/Dev.to/Substack. +- SERP routine per query: extract results; if < 15 valid items, paginate (action_agent click or query param) and re-extract until yield is sufficient. +- Deduplicate strictly by normalized name+hostname and canonical URL; merge crossโ€‘platform duplicates. +- Prefer authoritative domains (linkedin.com/in, company sites, personal domains, medium.com/@โ€ฆ); downโ€‘rank aggregators (job boards, marketplaces) unless linking to an identifiable person profile. + +### 404/Invalid URL Recovery (CRITICAL) +- After any fetcher_tool call, scan its result: for each entry in result.sources where success=false OR error contains any of ["404", "not found", "invalid", "Failed to navigate", "Navigation invocation failed"], treat the URL as a dead or invalid link. +- Immediately pivot to a search engine to locate authoritative alternatives: + - Build targeted queries using combinations of: entity name, company, role, plus modifiers like email, contact, team, partners, leadership; also try site filters such as site:company.com/team, site:company.com/contact, or site:linkedin.com/in NAME COMPANY. + - Prefer Google; if blocked, use Bing or DuckDuckGo. +- Use extract_data on the search results page with a schema that captures: title, snippet, url, source/domain. Collect at least 10 candidates. +- Filter to authoritative domains first (official company site, public LinkedIn profile/company page, press releases). Avoid low-signal aggregators if an official source exists. +- Re-run fetcher_tool in a single batch on the shortlisted candidate URLs and continue verification. +- Document dead-link recovery actions in notes and update gaps only if no authoritative alternative can be found. + +## Output Requirements +Return only JSON unless the user explicitly asked for another format. The JSON must conform to this schema: +{ + "status": "complete" | "partial" | "failed", + "objective": string, + "results": [ + { + "entity": string, + "confidence": number, + "attributes": object, + "sources": [ + { + "title": string, + "url": string, + "last_verified": string + } + ], + "notes": string[] + } + ], + "gaps": string[], + "next_actions": string[] +} +- confidence is between 0 and 1. Use 0.9+ only for attributes checked across multiple indicators. +- sources[*].last_verified must be an ISO 8601 date string representing when you verified the information (use the current date). +- Use gaps to explain which requested attributes you could not verify. +- Use next_actions sparingly for recommended manual follow-ups (for instance, if a LinkedIn login wall blocked you). + +If you absolutely cannot find any reliable leads, return status "failed" with gaps detailing everything you attempted. + +## Style Guardrails +- No markdown tables, bullet lists, or prose unless the user specifically overrides the default. +- Always include citations in sources for every result entry. +- Keep analysis internal; the user should only see the structured payload. +`, + tools: [ + 'navigate_url', + 'navigate_back', + 'node_ids_to_urls', + 'fetcher_tool', + 'extract_data', + 'scroll_page', + 'action_agent', + 'html_to_markdown' + ], + maxIterations: 12, + modelName: MODEL_SENTINELS.USE_MINI, + temperature: 0, + schema: { + type: 'object', + properties: { + objective: { + type: 'string', + description: 'Exact statement of the fact-finding mission (e.g. "find direct email for John Doe at Example Capital").' + }, + entity_type: { + type: 'string', + description: 'Type of entity being searched (individual, firm, team, etc.).' + }, + attributes: { + type: 'array', + items: { type: 'string' }, + description: 'List of attributes that must be returned for each result (e.g. ["email", "role", "linkedin_url"]).' + }, + territory: { + type: 'string', + description: 'Optional geographic or domain filters (e.g. "Seattle", "APAC").' + }, + quantity: { + type: 'number', + description: 'Desired number of matching entities (defaults to 1).' + }, + reasoning: { + type: 'string', + description: 'Short explanation of why this search is being run; surfaced to the user.' + } + }, + required: ['objective', 'attributes', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + const quantityText = args.quantity ? `Quantity: ${args.quantity}\n` : ''; + const territoryText = args.territory ? `Territory: ${args.territory}\n` : ''; + const entityTypeText = args.entity_type ? `Entity Type: ${args.entity_type}\n` : ''; + const attributesText = Array.isArray(args.attributes) ? `Attributes: ${JSON.stringify(args.attributes)}\n` : ''; + return [{ + entity: ChatMessageEntity.USER, + text: `Objective: ${args.objective}\n${entityTypeText}${territoryText}${quantityText}${attributesText}Reasoning: ${args.reasoning}\nRespond STRICTLY with JSON following the required schema. Do not include markdown or narrative text.`, + }]; + }, + handoffs: [], + includeIntermediateStepsOnReturn: false, + createErrorResult: (error: string, steps: ChatMessage[], reason: any) => { + // If we hit max iterations, synthesize a partial JSON payload from what we gathered + if (reason === 'max_iterations') { + const now = new Date().toISOString(); + const seen = new Set(); + const results: any[] = []; + + const addOrUpdate = (url: string, item: any) => { + if (!url) return; + const key = url.trim(); + if (seen.has(key)) return; + seen.add(key); + results.push(item); + }; + + // Try to recover the user's objective from the first USER message + const firstUser = steps.find(m => m.entity === ChatMessageEntity.USER && 'text' in m) as any; + const objectiveMatch = firstUser?.text?.match(/Objective:\s*(.*)/i); + const objective = objectiveMatch ? objectiveMatch[1].trim() : ''; + + // Scan tool results for extract_data (SERP) and fetcher_tool (content) + for (const msg of steps) { + if (msg.entity !== ChatMessageEntity.TOOL_RESULT) continue; + const tr = msg as any; + const tool = tr.toolName; + const data = tr.resultData ?? {}; + + if (tool === 'extract_data') { + const arr = (data?.data?.results || data?.results || []) as any[]; + for (const r of arr) { + const url = r?.url || ''; + if (!url) continue; + const title = r?.title || ''; + const source = r?.source || ''; + const snippet = r?.snippet || ''; + const entity = title || source || url; + addOrUpdate(url, { + entity, + confidence: 0.3, + attributes: { + source, + snippet + }, + sources: [ + { title: title || source || url, url, last_verified: now } + ], + notes: [ + 'SERP lead only; enrichment required to verify attributes.' + ] + }); + } + } + + if (tool === 'fetcher_tool') { + const sources = (data?.sources || []) as any[]; + for (const s of sources) { + const url = s?.url || ''; + if (!url) continue; + const title = s?.title || ''; + if (s?.success) { + addOrUpdate(url, { + entity: title || url, + confidence: 0.6, + attributes: { + content_fetched: true + }, + sources: [ + { title: title || url, url, last_verified: now } + ], + notes: [ + 'Fetched page content; run targeted extraction to fill required attributes.' + ] + }); + } + } + } + } + + const payload = { + status: 'partial', + objective: objective || 'Search task', + results, + gaps: [ + 'Reached maximum iterations before filling all required attributes.', + 'Many candidates may only be SERP leads; enrichment (profile/portfolio fetch + extraction) still needed.' + ], + next_actions: [ + 'Continue pagination on current queries (Next/numeric page or query params).', + 'Batch fetcher_tool on shortlisted URLs; use html_to_markdown + document_search to extract location, availability, portfolio, and contact.', + 'Deduplicate by normalized name + hostname and canonical URL.' + ] + }; + + return { + success: true, + output: JSON.stringify(payload, null, 2), + terminationReason: reason + }; + } + + // Fallback to a simple error result + return { + success: false, + error, + terminationReason: reason + }; + }, + }; +} diff --git a/front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.ts b/front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.ts new file mode 100644 index 00000000000..23b49ad328d --- /dev/null +++ b/front_end/panels/ai_chat/agent_framework/implementation/agents/WebTaskAgent.ts @@ -0,0 +1,241 @@ +import type { AgentToolConfig, ConfigurableAgentArgs } from "../../ConfigurableAgentTool.js"; +import type { ChatMessage } from "../../../models/ChatTypes.js"; +import { ChatMessageEntity } from "../../../models/ChatTypes.js"; +import { AGENT_VERSION } from "./AgentVersion.js"; + +/** + * Create the configuration for the Web Task Agent + */ +export function createWebTaskAgentConfig(): AgentToolConfig { + return { + name: 'web_task_agent', + version: AGENT_VERSION, + description: `A specialized agent that controls the browser to navigate web pages, reads contents, and orchestrates site-specific web tasks. Takes focused objectives from the base agent (like "find flights on this website") and breaks them down into individual actions that are executed. Handles site-specific workflows, error recovery, and returns structured results. Example tasks include navigating website, booking appointments, filling forms, extracting data, and interacting with dynamic content.`, + systemPrompt: `You are a specialized web task orchestrator agent that helps users with site-specific web tasks by directly interacting with web pages. Your goal is to complete web tasks efficiently by planning, executing, and verifying actions with advanced error recovery and optimization strategies. + +## Your Role & Enhanced Capabilities +You receive focused objectives from the base agent and break them down into individual actions. You coordinate between navigation, interaction, and data extraction to accomplish web tasks autonomously with: +- **Dynamic content detection**: Recognize SPAs, AJAX loading, and async content +- **Site pattern recognition**: Adapt strategies based on common website patterns +- **Intelligent error recovery**: Handle rate limits, CAPTCHAs, and service issues +- **State management**: Preserve context across complex multi-step workflows +- **Data quality validation**: Ensure extraction completeness and accuracy + +## Available Context & Enhanced Understanding +You automatically receive rich context with each iteration: +- **Current Page State**: Title, URL, and real-time accessibility tree (viewport elements only) +- **Progress Tracking**: Current iteration number and remaining steps +- **Page Updates**: Fresh accessibility tree data reflects any page changes from previous actions +- **Network State**: Monitor for ongoing requests and loading states +- **Error Patterns**: Track recurring issues for adaptive responses + +**Important distinctions:** +- **Accessibility tree**: Shows only viewport elements (what's currently visible) +- **Schema extraction**: Can access the entire page content, not just the viewport +- **Dynamic content**: May require wait strategies and loading detection + +## Enhanced Guidelines + +### 0. Thinking Usage (CRITICAL) +**ALWAYS use thinking tool:** +- At the start of any task to create a grounded plan +- After 3-4 actions to reassess progress +- When encountering unexpected results or errors +- Before major decisions (navigation, form submission) +- When the page changes significantly + +**SKIP thinking tool when:** +- On Chrome internal pages (chrome://*) - immediately navigate to a real website instead + +**Thinking provides:** +- Visual confirmation of current state +- High-level list of things to consider or work on +- Current progress assessment toward the goal +- Flexible observations about the situation + +### 1. Planning & Site Recognition +**ANALYZE site patterns first**: Before executing tools, identify: +- Site type (e-commerce, social media, enterprise, news, etc.) +- Framework indicators (React, Vue, Angular, jQuery) +- Loading patterns (SSR, SPA, hybrid) +- Known challenges (auth walls, rate limiting, complex interactions) + +**PLAN with adaptability**: Create a flexible plan that accounts for: +- Alternative paths if primary approach fails +- Expected loading times and dynamic content +- Potential error scenarios and recovery strategies +- State preservation requirements + +### 2. Enhanced Execution Strategy +**TAKE INITIAL SCREENSHOT**: Always take a screenshot at the beginning (iteration 1) and document the starting state + +**USE SMART WAITING**: After navigation or actions, intelligently wait for: +- Network idle states (no pending requests) +- Dynamic content loading completion +- JavaScript framework initialization +- Animation/transition completion + +**IMPLEMENT PROGRESSIVE LOADING DETECTION**: +- Look for skeleton loaders, loading spinners, or placeholder content +- Monitor for content height/width changes indicating loading +- Check for "load more" buttons or infinite scroll triggers +- Detect when async requests complete + +**TAKE PROGRESS SCREENSHOTS**: Document state at iterations 1, 5, 9, 13, etc., AND after significant state changes + +### 3. Advanced Error Recovery +**RECOGNIZE ERROR PATTERNS**: +- **Rate Limiting**: 429 errors, "too many requests", temporary blocks +- **Authentication**: Login walls, session timeouts, permission errors +- **Content Blocking**: Geo-restrictions, bot detection, CAPTCHA challenges +- **Technical Issues**: 5xx errors, network timeouts, JavaScript errors +- **Layout Issues**: Overlays, modals, cookie banners blocking content +- **Chrome Internal Pages**: action_agent cannot interact with any Chrome internal pages (chrome://*) including new tab, settings, extensions, etc. - navigate to a real website first + +**IMPLEMENT RECOVERY STRATEGIES**: +- **Rate Limits**: Use wait_for_page_load with exponential backoff (2s, 4s, 8s, 16s), then retry +- **CAPTCHAs**: Detect and inform user, provide clear guidance for manual resolution +- **Authentication**: Attempt to identify login requirements and notify user +- **Overlays**: Advanced blocking element detection and removal via action_agent +- **Network Issues**: Retry with different strategies or connection attempts +- **Chrome Internal Pages**: If detected (URL starts with chrome://), immediately navigate to a real website using navigate_url + +### 4. State & Context Management +**PRESERVE CRITICAL STATE**: +- Shopping cart contents and user session data +- Form progress and user inputs +- Page navigation history for complex flows +- Authentication status and session tokens + +**IMPLEMENT CHECKPOINTING**: +- Before major state changes, take screenshot to document current state +- After successful operations, confirm state preservation +- Provide rollback capabilities for failed operations + +### 5. Data Quality & Validation +**VALIDATE EXTRACTION COMPLETENESS**: +- Check for required fields in extraction schema +- Verify data format matches expected patterns +- Confirm numerical values are within reasonable ranges +- Detect partial or truncated content + +**IMPLEMENT QUALITY SCORING**: +- Rate extraction success based on completeness +- Identify missing or low-confidence data +- Retry extraction with alternative methods if quality appears insufficient + +### 6. Performance Optimization +**OPTIMIZE TOOL USAGE**: +- Use direct_url_navigator_agent for known URL patterns first +- Batch similar operations when possible +- Use most efficient extraction method for content type +- Avoid redundant tool calls through smart caching + +**MANAGE LARGE CONTENT**: +- For large pages, extract in targeted chunks using extract_data +- Use CSS selectors to limit extraction scope when possible +- Implement pagination handling for multi-page datasets + +### 7. Enhanced Communication +**PROVIDE PROGRESS UPDATES**: +- Report major milestones during long operations +- Explain current strategy and next steps clearly +- Notify user of encountered obstacles and recovery attempts +- Clearly communicate task completion status + +**HANDLE USER INTERACTION**: +- Identify when user input is required (CAPTCHAs, 2FA, manual authorization) +- Provide clear instructions for user actions +- Resume execution smoothly after user intervention + +## Task Execution Framework + +### Phase 1: Analysis & Planning (Iterations 1-2) +1. **Sequential Thinking**: USE sequential_thinking tool at the start to analyze the current state and create a grounded plan +2. **Site Pattern Recognition**: Identify website type and framework from the visual analysis +3. **Initial Screenshot**: Already captured by sequential_thinking +4. **Strategic Planning**: Follow the plan from sequential_thinking output + +### Phase 2: Execution & Monitoring (Iterations 3-12) +1. **Progressive Execution**: Execute plan from sequential_thinking step by step +2. **State Monitoring**: After major changes or unexpected results, use sequential_thinking again to reassess +3. **Error Detection**: When actions fail, use sequential_thinking to understand why and plan recovery +4. **Quality Validation**: Continuously verify extraction quality + +### Phase 3: Completion & Verification (Iterations 13-15) +1. **Final Validation**: Confirm task completion and data quality +2. **State Cleanup**: Handle session cleanup if needed +3. **Results Formatting**: Structure output according to requirements +4. **Completion Documentation**: Final screenshot and summary + +## Advanced Tool Usage Patterns + +### Smart Navigation Strategy +1. Try direct_url_navigator_agent for known URL patterns +2. Use navigate_url for standard navigation +3. Implement wait_for_page_load for dynamic content +4. Apply scroll_page strategically for infinite scroll +5. Use take_screenshot for understanding the web page state + +### Dynamic Content Handling +1. After navigation, use wait_for_page_load (2-3 seconds) for initial load +2. Check for loading indicators or skeleton content +3. Use wait_for_page_load until content stabilizes +4. Re-extract content and compare with previous state +5. Repeat until content stabilizes + +### Error Recovery Workflow +1. Detect error type through page analysis +2. Apply appropriate recovery strategy with wait_for_page_load +3. Document recovery attempt in screenshot +4. Retry original operation with modifications +5. Escalate to user if automated recovery fails + +Remember: **Plan adaptively, execute systematically, validate continuously, and communicate clearly**. Your goal is robust, reliable task completion with excellent user experience. +`, + tools: [ + 'navigate_url', + 'navigate_back', + 'action_agent', + 'extract_data', + 'node_ids_to_urls', + 'direct_url_navigator_agent', + 'scroll_page', + 'take_screenshot', + 'wait_for_page_load', + 'thinking', + ], + maxIterations: 15, + temperature: 0.3, + schema: { + type: 'object', + properties: { + task: { + type: 'string', + description: 'The web task to execute, including navigation, interaction, or data extraction requirements.' + }, + reasoning: { + type: 'string', + description: 'Clear explanation of the task objectives and expected outcomes.' + }, + extraction_schema: { + type: 'object', + description: 'Optional schema definition for structured data extraction tasks.' + } + }, + required: ['task', 'reasoning'] + }, + prepareMessages: (args: ConfigurableAgentArgs): ChatMessage[] => { + return [{ + entity: ChatMessageEntity.USER, + text: `Task: ${args.query? `${args.query}` : ''} + ${args.task? `${args.task}` : ''} + ${args.objective? `${args.objective}` : ''} +${args.extraction_schema ? `\nExtraction Schema: ${JSON.stringify(args.extraction_schema)}` : ''} + +Execute this web task autonomously`, + }]; + }, + handoffs: [], + }; +} diff --git a/front_end/panels/ai_chat/core/AgentDescriptorRegistry.ts b/front_end/panels/ai_chat/core/AgentDescriptorRegistry.ts new file mode 100644 index 00000000000..7b309708e4b --- /dev/null +++ b/front_end/panels/ai_chat/core/AgentDescriptorRegistry.ts @@ -0,0 +1,138 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as Platform from '../../../core/platform/platform.js'; +import { createLogger } from './Logger.js'; + +const logger = createLogger('AgentDescriptorRegistry'); + +export interface AgentDescriptor { + name: string; + type?: string; + version: string; + promptHash: string; + toolsetHash: string; + generatedAt: string; + metadata?: Record; +} + +export interface AgentDescriptorSource { + name: string; + type?: string; + version?: string; + promptProvider: () => string | Promise; + toolNamesProvider: () => string[] | Promise; + metadataProvider?: () => Record | Promise>; +} + +const descriptorSources = new Map(); +const descriptorCache = new Map>(); + +async function computeHash(value: string): Promise { + const normalized = value.replace(/\r\n/g, '\n'); + + try { + if (typeof globalThis.crypto?.subtle !== 'undefined') { + const encoder = new TextEncoder(); + const data = encoder.encode(normalized); + const digest = await globalThis.crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(digest)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + } + } catch (error) { + logger.warn('Falling back to hashCode due to subtle crypto failure', error); + } + + const fallback = Platform.StringUtilities.hashCode(normalized); + return fallback.toString(16); +} + +async function computeDescriptorFromSource(source: AgentDescriptorSource): Promise { + const prompt = await source.promptProvider(); + const toolNames = await source.toolNamesProvider(); + const metadata = source.metadataProvider ? await source.metadataProvider() : undefined; + + const promptHash = await computeHash(prompt); + const toolsetPayload = JSON.stringify({ + tools: [...toolNames].sort(), + metadata: metadata ?? {} + }); + const toolsetHash = await computeHash(toolsetPayload); + + return { + name: source.name, + type: source.type, + version: source.version ?? 'unversioned', + promptHash, + toolsetHash, + generatedAt: new Date().toISOString(), + ...(metadata ? { metadata } : {}), + }; +} + +function invalidateCache(name: string): void { + descriptorCache.delete(name); +} + +export class AgentDescriptorRegistry { + static registerSource(source: AgentDescriptorSource): void { + const existing = descriptorSources.get(source.name); + if (existing) { + logger.warn('Agent descriptor source already registered, overwriting', { + name: source.name, + previousType: existing.type, + newType: source.type + }); + } + descriptorSources.set(source.name, source); + invalidateCache(source.name); + } + + static async getDescriptor(name: string): Promise { + if (!descriptorSources.has(name)) { + return null; + } + + if (!descriptorCache.has(name)) { + const source = descriptorSources.get(name)!; + const promise = computeDescriptorFromSource(source); + descriptorCache.set(name, promise); + } + + try { + return await descriptorCache.get(name)!; + } catch (error) { + logger.error('Failed to compute descriptor', { name, error }); + descriptorCache.delete(name); + return null; + } + } + + static async listDescriptors(): Promise { + const list = Array.from(descriptorSources.keys()).map(name => this.getDescriptor(name)); + const results = await Promise.all(list); + return results.filter((descriptor): descriptor is AgentDescriptor => Boolean(descriptor)); + } + + static hasDescriptor(name: string): boolean { + return descriptorSources.has(name); + } +} + +// Convenience helpers exposed globally for debugging during development. +if (typeof window !== 'undefined') { + (window as any).listAgentDescriptors = () => AgentDescriptorRegistry.listDescriptors(); + (window as any).getAgentDescriptor = (name: string) => AgentDescriptorRegistry.getDescriptor(name); +} + +export async function ensureDescriptor(name: string, fallbackSource: AgentDescriptorSource): Promise { + if (!AgentDescriptorRegistry.hasDescriptor(name)) { + AgentDescriptorRegistry.registerSource(fallbackSource); + } + const descriptor = await AgentDescriptorRegistry.getDescriptor(name); + if (!descriptor) { + throw new Error(`Failed to compute agent descriptor for ${name}`); + } + return descriptor; +} diff --git a/front_end/panels/ai_chat/core/AgentNodes.ts b/front_end/panels/ai_chat/core/AgentNodes.ts index a0f3efb1d54..cbfe05c52b8 100644 --- a/front_end/panels/ai_chat/core/AgentNodes.ts +++ b/front_end/panels/ai_chat/core/AgentNodes.ts @@ -20,6 +20,7 @@ import { AgentErrorHandler } from './AgentErrorHandler.js'; import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js'; import * as ToolNameMap from './ToolNameMap.js'; import type { TracingProvider } from '../tracing/TracingProvider.js'; +import { AgentDescriptorRegistry } from './AgentDescriptorRegistry.js'; const logger = createLogger('AgentNodes'); @@ -35,8 +36,14 @@ export function createAgentNode(modelName: string, provider: LLMProvider, temper constructor() { this.tracingProvider = createTracingProvider(); } - async invoke(state: AgentState): Promise { - console.log('[AGENT NODE DEBUG] AgentNode invoke called, messages count:', state.messages.length); + async invoke(state: AgentState, signal?: AbortSignal): Promise { + // Check if execution has been aborted + if (signal?.aborted) { + logger.info('AgentNode execution aborted'); + throw new DOMException('Agent execution was cancelled', 'AbortError'); + } + + logger.debug('AgentNode invoke called, messages count:', state.messages.length); logger.debug('AgentNode: Invoked with state. Last message:', state.messages.length > 0 ? state.messages[state.messages.length - 1] : 'No messages'); @@ -97,7 +104,13 @@ export function createAgentNode(modelName: string, provider: LLMProvider, temper // 2. Call the LLM with the message array this.callCount++; - + + // Check for abort before potentially long-running LLM call + if (signal?.aborted) { + logger.info('AgentNode execution aborted before LLM call'); + throw new DOMException('Agent execution was cancelled', 'AbortError'); + } + if (this.callCount > this.MAX_CALLS_PER_INTERACTION) { logger.warn('Max calls per interaction reached:', this.callCount); throw new Error(`Maximum calls (${this.MAX_CALLS_PER_INTERACTION}) per interaction exceeded. This might be an infinite loop.`); @@ -111,6 +124,7 @@ export function createAgentNode(modelName: string, provider: LLMProvider, temper // Create generation observation for LLM call const tracingContext = state.context?.tracingContext; + const agentDescriptor = state.context?.agentDescriptor; let generationId: string | undefined; const generationStartTime = new Date(); @@ -135,6 +149,17 @@ export function createAgentNode(modelName: string, provider: LLMProvider, temper entity: state.messages[state.messages.length - 1].entity, content: JSON.stringify(state.messages[state.messages.length - 1]).substring(0, 500) } : null + }, + metadata: { + executionLevel: 'stategraph', + source: 'AgentNode', + selectedAgentType: state.selectedAgentType ?? 'default', + ...(agentDescriptor ? { + agentName: agentDescriptor.name, + agentVersion: agentDescriptor.version, + promptHash: agentDescriptor.promptHash, + toolsetHash: agentDescriptor.toolsetHash + } : {}) } }, tracingContext.traceId); @@ -150,7 +175,7 @@ export function createAgentNode(modelName: string, provider: LLMProvider, temper // Get tools for the current agent type const baseTools = BaseOrchestratorAgent.getAgentTools(state.selectedAgentType ?? '') as any; - const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 }); + const selection = await ToolSurfaceProvider.select(state, baseTools); // Persist selection in context so ToolExecutorNode can resolve the same set if (!state.context) { (state as any).context = {}; } (state.context as any).selectedToolNames = selection.selectedNames; @@ -174,9 +199,16 @@ export function createAgentNode(modelName: string, provider: LLMProvider, temper logger.warn('Failed to update generation observation with tools list', err); } } - + + // Build mapping from original tool names to sanitized names for message conversion + const originalToSanitized: Record = {}; + tools.forEach(tool => { + const sanitized = ToolNameMap.getSanitized(tool.name); + originalToSanitized[tool.name] = sanitized; + }); + // Convert ChatMessage[] to LLMMessage[] - const llmMessages = this.convertChatMessagesToLLMMessages(state.messages); + const llmMessages = this.convertChatMessagesToLLMMessages(state.messages, originalToSanitized); // Create error handler for retry logic const errorHandler = AgentErrorHandler.createErrorHandler({ @@ -242,7 +274,18 @@ export function createAgentNode(modelName: string, provider: LLMProvider, temper await this.tracingProvider.updateObservation(generationId, { endTime: new Date(), output: parsedAction, - ...(usage && { usage }) + ...(usage && { usage }), + metadata: { + executionLevel: 'stategraph', + source: 'AgentNode', + selectedAgentType: state.selectedAgentType ?? 'default', + ...(agentDescriptor ? { + agentName: agentDescriptor.name, + agentVersion: agentDescriptor.version, + promptHash: agentDescriptor.promptHash, + toolsetHash: agentDescriptor.toolsetHash + } : {}) + } }); } @@ -250,7 +293,10 @@ export function createAgentNode(modelName: string, provider: LLMProvider, temper let newModelMessage: ModelChatMessage; if (parsedAction.type === 'tool_call') { const toolCallId = crypto.randomUUID(); // Generate unique ID for OpenAI format - const resolvedToolName = ToolNameMap.resolveOriginal(parsedAction.name) || parsedAction.name; + const sanitizedToolName = ToolNameMap.getSanitized(parsedAction.name); + const resolvedToolName = ToolNameMap.resolveOriginal(parsedAction.name) + || ToolNameMap.resolveOriginal(sanitizedToolName) + || parsedAction.name; // Create tool-call event observation const tracingContext = state.context?.tracingContext; @@ -337,7 +383,18 @@ export function createAgentNode(modelName: string, provider: LLMProvider, temper if (generationId && tracingContext?.traceId) { await this.tracingProvider.updateObservation(generationId, { endTime: new Date(), - error: error instanceof Error ? error.message : String(error) + error: error instanceof Error ? error.message : String(error), + metadata: { + executionLevel: 'stategraph', + source: 'AgentNode', + selectedAgentType: state.selectedAgentType ?? 'default', + ...(agentDescriptor ? { + agentName: agentDescriptor.name, + agentVersion: agentDescriptor.version, + promptHash: agentDescriptor.promptHash, + toolsetHash: agentDescriptor.toolsetHash + } : {}) + } }); } @@ -457,7 +514,33 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, tools = [] as unknown as ReturnType; } const toolMap = new Map[number]>(); - (tools as any[]).forEach((tool: any) => toolMap.set(tool.name, tool)); + (tools as any[]).forEach((tool: any) => { + // Map original name + toolMap.set(tool.name, tool); + + // Map sanitized name + const sanitized = ToolNameMap.getSanitized(tool.name); + if (sanitized && sanitized !== tool.name) { + toolMap.set(sanitized, tool); + } + + // Also try to resolve any existing mapping from ToolNameMap + const resolvedOriginal = ToolNameMap.resolveOriginal(sanitized); + if (resolvedOriginal && resolvedOriginal !== tool.name && resolvedOriginal !== sanitized) { + toolMap.set(resolvedOriginal, tool); + } + + // Debug logging for MCP tools + if (tool.name.includes('mcp:')) { + logger.debug('ToolExecutorNode: Mapped MCP tool', { + original: tool.name, + sanitized: sanitized, + resolvedOriginal: resolvedOriginal + }); + } + }); + + logger.debug('ToolExecutorNode: Created toolMap with keys', Array.from(toolMap.keys())); const toolExecutorNode = new class ToolExecutorNode implements Runnable { private toolMap: Map[number]>; @@ -476,7 +559,13 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, this.nanoModel = nanoModel; } - async invoke(state: AgentState): Promise { + async invoke(state: AgentState, signal?: AbortSignal): Promise { + // Check if execution has been aborted + if (signal?.aborted) { + logger.info('ToolExecutorNode execution aborted'); + throw new DOMException('Tool execution was cancelled', 'AbortError'); + } + const lastMessage = state.messages[state.messages.length - 1]; // Expect the last message to be the MODEL action requesting the tool @@ -495,9 +584,129 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, // Initialize messages array with current state const messages = [...state.messages]; - const selectedTool = this.toolMap.get(toolName); + const sanitizedToolName = ToolNameMap.getSanitized(toolName); + const resolvedOriginalName = ToolNameMap.resolveOriginal(toolName) || ToolNameMap.resolveOriginal(sanitizedToolName); + + // Extract tool name for smart naming (declared here for reuse in fallback) + let extractedToolName: string | null = null; + if (toolName.startsWith('mcp:') && toolName.includes(':')) { + // Extract tool name from "mcp:server_id:tool_name" format + const parts = toolName.split(':'); + if (parts.length >= 3) { + extractedToolName = parts.slice(2).join(':'); // Handle tool names with colons + } + } else if (toolName.includes('mcp_')) { + // Handle sanitized format "mcp_server_id_tool_name" + const mcpPrefix = toolName.match(/^mcp_[^_]+_(.+)$/); + if (mcpPrefix) { + extractedToolName = mcpPrefix[1].replace(/_/g, ':'); // Convert back underscores in tool name + } + } + + // Debug logging for tool resolution + logger.debug('ToolExecutorNode: Resolving tool', { + requested: toolName, + sanitized: sanitizedToolName, + resolved: resolvedOriginalName, + availableTools: Array.from(this.toolMap.keys()) + }); + + // Try multiple resolution strategies + let selectedTool = this.toolMap.get(toolName) + || (resolvedOriginalName ? this.toolMap.get(resolvedOriginalName) : undefined) + || this.toolMap.get(sanitizedToolName); + + // Last resort: try to get tool directly from ToolRegistry if (!selectedTool) { - throw new Error(`Tool ${toolName} not found`); + logger.debug('ToolExecutorNode: Trying direct ToolRegistry lookup as fallback'); + + // Try all possible name variants + const namesToTry = [ + toolName, + sanitizedToolName, + resolvedOriginalName, + extractedToolName + ].filter(Boolean) as string[]; + + for (const name of namesToTry) { + try { + const registryTool = ToolRegistry.getRegisteredTool(name as any); + if (registryTool) { + selectedTool = registryTool; + logger.debug('ToolExecutorNode: Found tool via direct registry lookup', { + requested: toolName, + foundAs: name, + toolType: registryTool.constructor.name + }); + break; + } + } catch (error) { + // Ignore registry lookup errors + } + } + } + + if (!selectedTool) { + // Gracefully handle missing tools: satisfy the tool call with an error result + // and route back to the Agent for a fresh LLM decision (retry without crashing the graph). + logger.error('ToolExecutorNode: Tool not found', { + requested: toolName, + sanitized: sanitizedToolName, + resolved: resolvedOriginalName, + availableTools: Array.from(this.toolMap.keys()) + }); + + // Tracing: emit an event so we can diagnose missing tool issues in traces + try { + const tracingContext = state.context?.tracingContext; + if (tracingContext?.traceId) { + const available = Array.from(this.toolMap.keys()); + await this.tracingProvider.createObservation({ + id: `event-missing-tool-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + name: `Missing Tool: ${toolName}`, + type: 'event', + startTime: new Date(), + parentObservationId: tracingContext.currentToolCallId || tracingContext.parentObservationId, + error: `Tool ${toolName} not found`, + input: { + requested: toolName, + sanitized: sanitizedToolName, + resolvedOriginal: resolvedOriginalName, + availableCount: available.length, + // Avoid huge payloads: include first 25 names + availableSample: available.slice(0, 25), + }, + metadata: { + source: 'ToolExecutorNode', + phase: 'error', + category: 'missing-tool', + executionLevel: 'tool', + } + }, tracingContext.traceId); + } + } catch (traceErr) { + console.error('[HIERARCHICAL_TRACING] ToolExecutorNode: Failed to record missing-tool event', traceErr); + } + + const resultText = `Error: Tool "${toolName}" is not available. Please choose another tool or provide a direct answer.`; + const toolResultMessage: ToolResultMessage = { + entity: ChatMessageEntity.TOOL_RESULT, + toolName, + resultText, + isError: true, + toolCallId, + error: resultText, + uiLane: 'chat', + }; + messages.push(toolResultMessage); + + const newState = { + ...state, + messages: [...messages], + error: resultText, + }; + // Returning here makes the last message a TOOL_RESULT so routeNextNode() sends execution back to the AgentNode. + return newState; } // Create span for tool execution @@ -505,8 +714,9 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, let spanId: string | undefined; const spanStartTime = new Date(); const isConfigurableAgent = selectedTool instanceof ConfigurableAgentTool; + const configurableDescriptor = isConfigurableAgent ? await AgentDescriptorRegistry.getDescriptor(selectedTool.name) : null; - console.log(`[HIERARCHICAL_TRACING] ToolExecutorNode: Creating span for ${toolName}:`, { + logger.debug(`ToolExecutorNode: Creating span for ${toolName}:`, { hasTracingContext: !!tracingContext, traceId: tracingContext?.traceId, currentToolCallId: tracingContext?.currentToolCallId, @@ -534,10 +744,16 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, phase: 'execution', executionLevel: isConfigurableAgent ? 'agentrunner' : 'tool', source: 'ToolExecutorNode', - isConfigurableAgent + isConfigurableAgent, + ...(configurableDescriptor ? { + agentVersion: configurableDescriptor.version, + agentName: configurableDescriptor.name, + promptHash: configurableDescriptor.promptHash, + toolsetHash: configurableDescriptor.toolsetHash + } : {}) } }, tracingContext.traceId); - console.log(`[HIERARCHICAL_TRACING] ToolExecutorNode: Successfully created span:`, { + logger.debug(`ToolExecutorNode: Successfully created span:`, { spanId, toolName, isConfigurableAgent, @@ -547,7 +763,7 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, console.error(`[HIERARCHICAL_TRACING] ToolExecutorNode: Failed to create span:`, error); } } else { - console.log(`[HIERARCHICAL_TRACING] ToolExecutorNode: No tracing context or traceId available`); + logger.debug(`ToolExecutorNode: No tracing context or traceId available`); } try { @@ -558,13 +774,13 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, traceId: tracingContext?.traceId, toolName }); - console.log(`[TRACING DEBUG] Executing tool ${toolName} with tracing context:`, { - hasTracingContext: !!tracingContext, + logger.debug(`Executing tool ${toolName} with tracing context:`, { + hasTracingContext: !!tracingContext, traceId: tracingContext?.traceId, - toolName + toolName }); - console.log(`[TOOL EXECUTION PATH 1] ToolExecutorNode about to execute tool: ${toolName}`); + logger.debug(`ToolExecutorNode about to execute tool: ${toolName}`); // Create enhanced tracing context for ConfigurableAgentTool execution let executionContext = tracingContext || null; @@ -580,7 +796,7 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, iterationCount: 0 } }; - console.log(`[HIERARCHICAL_TRACING] ToolExecutorNode: Created enhanced tracing context for agent:`, { + logger.debug(`ToolExecutorNode: Created enhanced tracing context for agent:`, { agentSpanId: spanId, agentName: toolName, executionLevel: executionContext.executionLevel, @@ -589,8 +805,14 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, }); } + // Check for abort before tool execution + if (signal?.aborted) { + logger.info('ToolExecutorNode execution aborted before tool execution'); + throw new DOMException('Tool execution was cancelled', 'AbortError'); + } + const result = await withTracingContext(executionContext, async () => { - console.log(`[TOOL EXECUTION PATH 1] Inside withTracingContext for tool: ${toolName}`); + logger.debug(`Inside withTracingContext for tool: ${toolName}`); // Get configuration from manager (supports overrides) const configManager = LLMConfigurationManager.getInstance(); @@ -601,18 +823,20 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, provider: config.provider, model: config.mainModel, miniModel: config.miniModel, - nanoModel: config.nanoModel + nanoModel: config.nanoModel, + abortSignal: signal || state.context.abortSignal, + ...(configurableDescriptor ? { agentDescriptor: configurableDescriptor } : {}) }); }); - console.log(`[TOOL EXECUTION PATH 1] ToolExecutorNode completed tool: ${toolName}`); + logger.debug(`ToolExecutorNode completed tool: ${toolName}`); // Check if result contains agentSession (ConfigurableAgentTool result) if (selectedTool instanceof ConfigurableAgentTool && result && typeof result === 'object' && 'agentSession' in result) { const agentSession = result.agentSession as any; - console.log(`[AGENT SESSION] Captured agent session from ${toolName}:`, agentSession); + logger.debug(`Captured agent session from ${toolName}:`, agentSession); // Log detailed session information - console.log(`[AGENT SESSION] Session Details:`, { + logger.debug(`Session Details:`, { sessionId: agentSession.sessionId, agentName: agentSession.agentName, status: agentSession.status, @@ -630,7 +854,7 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, if (agentSession.messages) { const toolCalls = agentSession.messages.filter((msg: any) => msg.type === 'tool_call'); const toolResults = agentSession.messages.filter((msg: any) => msg.type === 'tool_result'); - console.log(`[AGENT SESSION] Tool Analysis:`, { + logger.debug(`Tool Analysis:`, { totalMessages: agentSession.messages.length, toolCallCount: toolCalls.length, toolResultCount: toolResults.length, @@ -648,14 +872,14 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, agentSession: result.agentSession as any, summary: `Agent ${toolName} execution completed` }; - console.log(`[AGENT SESSION] Created top-level AgentSessionMessage:`, { + logger.debug(`Created top-level AgentSessionMessage:`, { sessionId: (result.agentSession as any).sessionId, agentName: (result.agentSession as any).agentName, status: (result.agentSession as any).status }); messages.push(agentSessionMessage); } else { - console.log(`[AGENT SESSION] Skipping top-level AgentSessionMessage for nested child`, { + logger.debug(`Skipping top-level AgentSessionMessage for nested child`, { sessionId: (result.agentSession as any).sessionId, parentSessionId }); @@ -668,7 +892,7 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, // For ConfigurableAgentTool, only send the output/error fields to the LLM, never intermediateSteps const agentResult = result as any; // Cast to any to access ConfigurableAgentResult properties resultText = agentResult.output || (agentResult.error ? `Error: ${agentResult.error}` : 'No output'); - console.log(`[AGENT SESSION] Filtered ConfigurableAgentTool result for LLM:`, { + logger.debug(`Filtered ConfigurableAgentTool result for LLM:`, { toolName, originalResult: result, filteredResult: resultText @@ -705,7 +929,12 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, agentName: toolName, agentType: toolName, resultType: result && typeof result === 'object' && 'agentSession' in result ? 'agent_result' : 'unknown' - }) + }), + ...(configurableDescriptor ? { + agentVersion: configurableDescriptor.version, + promptHash: configurableDescriptor.promptHash, + toolsetHash: configurableDescriptor.toolsetHash + } : {}) }; await this.tracingProvider.updateObservation(spanId, { @@ -715,7 +944,7 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, : result, metadata: completionMetadata }); - console.log(`[HIERARCHICAL_TRACING] ToolExecutorNode: Successfully completed span:`, { + logger.debug(`ToolExecutorNode: Successfully completed span:`, { spanId, toolName, success: !isError, @@ -728,6 +957,10 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, } } catch (err) { + // Propagate cancellation so the graph can handle AbortError cleanly + if (err instanceof DOMException && err.name === 'AbortError') { + throw err; + } resultText = `Error during tool execution: ${err instanceof Error ? err.message : String(err)}`; logger.error(resultText, { tool: toolName, args: toolArgs }); isError = true; @@ -747,7 +980,12 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, ...(isConfigurableAgent && { agentName: toolName, agentType: toolName - }) + }), + ...(configurableDescriptor ? { + agentVersion: configurableDescriptor.version, + promptHash: configurableDescriptor.promptHash, + toolsetHash: configurableDescriptor.toolsetHash + } : {}) }; await this.tracingProvider.updateObservation(spanId, { @@ -755,7 +993,7 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, error: err instanceof Error ? err.message : String(err), metadata: errorMetadata }); - console.log(`[HIERARCHICAL_TRACING] ToolExecutorNode: Successfully completed span with error:`, { + logger.debug(`ToolExecutorNode: Successfully completed span with error:`, { spanId, toolName, error: err instanceof Error ? err.message : String(err), @@ -791,7 +1029,7 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, error: isError ? resultText : undefined, }; - console.log(`[AGENT SESSION] Returning state with ${newState.messages.length} messages`); + logger.debug(`Returning state with ${newState.messages.length} messages`); return newState; } @@ -801,7 +1039,13 @@ export function createToolExecutorNode(state: AgentState, provider: LLMProvider, export function createFinalNode(): Runnable { const finalNode = new class FinalNode implements Runnable { - async invoke(state: AgentState): Promise { + async invoke(state: AgentState, signal?: AbortSignal): Promise { + // Check if execution has been aborted (for consistency) + if (signal?.aborted) { + logger.info('FinalNode execution aborted'); + throw new DOMException('Execution was cancelled', 'AbortError'); + } + const lastMessage = state.messages[state.messages.length - 1]; if (lastMessage?.entity !== ChatMessageEntity.MODEL || !lastMessage.isFinalAnswer) { logger.warn('FinalNode: Invoked, but last message was not a final MODEL answer as expected.'); diff --git a/front_end/panels/ai_chat/core/AgentService.ts b/front_end/panels/ai_chat/core/AgentService.ts index 4c9b2f842e9..b244be45583 100644 --- a/front_end/panels/ai_chat/core/AgentService.ts +++ b/front_end/panels/ai_chat/core/AgentService.ts @@ -11,6 +11,7 @@ import { type ChatMessage, ChatMessageEntity, type ImageInputData, type ModelCha import {createAgentGraph} from './Graph.js'; import { createLogger } from './Logger.js'; +import { AgentDescriptorRegistry } from './AgentDescriptorRegistry.js'; import {type AgentState, createInitialState, createUserMessage} from './State.js'; import type {CompiledGraph} from './Types.js'; import { LLMClient } from '../LLM/LLMClient.js'; @@ -56,11 +57,48 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ #apiKey: string|null = null; #isInitialized = false; #runningGraphStatePromise?: AsyncGenerator; + #abortController?: AbortController; + #executionId?: string; #tracingProvider!: TracingProvider; #sessionId: string; #activeAgentSessions = new Map(); #configManager: LLMConfigurationManager; + // Global registry for all active executions + private static activeExecutions = new Map(); + + /** + * Register an abort controller for execution tracking + */ + static registerExecution(executionId: string, controller: AbortController): void { + AgentService.activeExecutions.set(executionId, controller); + } + + /** + * Unregister an execution + */ + static unregisterExecution(executionId: string): void { + AgentService.activeExecutions.delete(executionId); + } + + /** + * Abort all active executions + */ + static abortAllExecutions(): void { + for (const [executionId, controller] of AgentService.activeExecutions) { + logger.info(`Aborting execution: ${executionId}`); + controller.abort(); + } + AgentService.activeExecutions.clear(); + } + + /** + * Get abort controller for execution + */ + static getExecutionController(executionId: string): AbortController | undefined { + return AgentService.activeExecutions.get(executionId); + } + constructor() { super(); @@ -362,6 +400,13 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ const currentPageUrl = await this.#getCurrentPageUrl(); const currentPageTitle = await this.#getCurrentPageTitle(); + const orchestratorKey = selectedAgentType ? `orchestrator:${selectedAgentType}` : 'orchestrator:default'; + const orchestratorDescriptor = await AgentDescriptorRegistry.getDescriptor(orchestratorKey) || + await AgentDescriptorRegistry.getDescriptor('orchestrator:default'); + if (orchestratorDescriptor) { + this.#state.context.agentDescriptor = orchestratorDescriptor; + } + // Check if there's an existing tracing context (e.g., from evaluation) const existingContext = getCurrentTracingContext() as TracingContext | null; @@ -397,7 +442,13 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ { selectedAgentType, currentPageUrl, - currentPageTitle + currentPageTitle, + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) }, undefined, // userId [selectedAgentType || 'default'].filter(Boolean) @@ -431,7 +482,13 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ currentPageUrl, currentPageTitle, messageCount: this.#state.messages.length, - isEvaluationContext: !!existingContext + isEvaluationContext: !!existingContext, + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) }, ...(parentObservationId && { parentObservationId }) }, traceId); @@ -445,7 +502,10 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ sessionId: existingContext?.sessionId || this.#sessionId, traceId, parentObservationId: parentObservationId - } + }, + executionId: this.#executionId, + abortSignal: this.#abortController?.signal, + ...(orchestratorDescriptor ? { agentDescriptor: orchestratorDescriptor } : {}) }, selectedAgentType: selectedAgentType ?? null, // Set the agent type for this run currentPageUrl, @@ -463,13 +523,23 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ messageCount: this.#state.messages.length }); + // Create AbortController for this execution + this.#executionId = `execution-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + this.#abortController = new AbortController(); + AgentService.registerExecution(this.#executionId, this.#abortController); + // Ensure the abort signal is present in the state context for tools + try { + (state as any).context.abortSignal = this.#abortController.signal; + (state as any).context.executionId = this.#executionId; + } catch {} + // Run the agent graph on the state console.warn('[AGENT SERVICE DEBUG] About to invoke graph with state:', { traceId, messagesCount: state.messages.length, hasTracingContext: !!state.context?.tracingContext }); - this.#runningGraphStatePromise = this.#graph?.invoke(state); + this.#runningGraphStatePromise = this.#graph?.invoke(state, this.#abortController.signal); // Wait for the result if (!this.#runningGraphStatePromise) { @@ -504,7 +574,13 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ }, metadata: { totalMessages: this.#state.messages.length, - responseType: 'success' + responseType: 'success', + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) } }, traceId); @@ -520,6 +596,14 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ ); } + // Clean up execution state + if (this.#executionId) { + AgentService.unregisterExecution(this.#executionId); + this.#executionId = undefined; + } + this.#abortController = undefined; + this.#runningGraphStatePromise = undefined; + // Return the most recent message (could be final answer, tool call, or error) return finalMessage; @@ -550,7 +634,13 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ error: error instanceof Error ? error.message : String(error), metadata: { totalMessages: this.#state.messages.length, - responseType: 'error' + responseType: 'error', + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) } }, traceId); @@ -563,6 +653,14 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ ); } + // Clean up execution state + if (this.#executionId) { + AgentService.unregisterExecution(this.#executionId); + this.#executionId = undefined; + } + this.#abortController = undefined; + this.#runningGraphStatePromise = undefined; + return errorMessage; } } @@ -571,6 +669,15 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ * Clears the conversation history */ clearConversation(): void { + // Abort ALL running agent executions globally + logger.info('Aborting all running agent executions due to conversation clear'); + AgentService.abortAllExecutions(); + + // Clear local state + this.#abortController = undefined; + this.#runningGraphStatePromise = undefined; + this.#executionId = undefined; + // Create a fresh state this.#state = createInitialState(); @@ -586,6 +693,22 @@ export class AgentService extends Common.ObjectWrapper.ObjectWrapper<{ this.dispatchEventToListeners(Events.MESSAGES_CHANGED, [...this.#state.messages]); } + /** + * Cancels any in-flight agent execution without clearing conversation state. + */ + cancelRun(): void { + logger.info('Cancelling current agent execution (without clearing messages)'); + if (this.#executionId) { + const controller = AgentService.getExecutionController(this.#executionId); + try { controller?.abort(); } catch {} + AgentService.unregisterExecution(this.#executionId); + this.#executionId = undefined; + } + try { this.#abortController?.abort(); } catch {} + this.#abortController = undefined; + this.#runningGraphStatePromise = undefined; + } + /** * Sets the API key for the agent and re-initializes the graph * @param apiKey The new API key diff --git a/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts b/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts index c168a6a1831..52399d0e3f2 100644 --- a/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts +++ b/front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts @@ -32,6 +32,7 @@ import { initializeConfiguredAgents(); const logger = createLogger('BaseOrchestratorAgent'); +const DEFAULT_ORCHESTRATOR_VERSION = '2025-09-17'; // Define available agent types export enum BaseOrchestratorAgentType { @@ -42,9 +43,15 @@ export enum BaseOrchestratorAgentType { // System prompts for each agent type export const SYSTEM_PROMPTS = { - [BaseOrchestratorAgentType.SEARCH]: `You are an AI assistant focused on searching the web to answer user questions. -Use the 'navigate_url' and 'fetcher_tool' tools whenever the user asks a question that requires up-to-date information -or knowledge beyond your training data. Prioritize concise and direct answers based on search results.`, + [BaseOrchestratorAgentType.SEARCH]: `You are an search browser agent specialized in pinpoint web fact-finding. +Always delegate investigative work to the 'search_agent' tool so it can gather verified, structured results (emails, team rosters, niche professionals, etc.). + +- Launch search_agent with a clear objective, attribute list, filters, and quantity requirement. +- Review the JSON output, double-check confidence values and citations, and surface the most credible findings. +- If the user pivots into broad synthesis or long-form reporting, switch to the 'research_agent'. +- Keep responses concise, cite the strongest sources, and present the structured findings provided by the agent. + +Clarify ambiguous requests before delegating.`, [BaseOrchestratorAgentType.DEEP_RESEARCH]: `You are an expert research browser agent focused on high-level research strategy, planning, efficient delegation to sub-research agents, and final report synthesis. Your core goal is to provide maximally helpful, comprehensive research reports by orchestrating an effective research process. @@ -275,55 +282,79 @@ export interface AgentConfig { description?: string; systemPrompt: string; availableTools: Array>; + version?: string; } // Agent configurations export const AGENT_CONFIGS: {[key: string]: AgentConfig} = { - // [BaseOrchestratorAgentType.SEARCH]: { - // type: BaseOrchestratorAgentType.SEARCH, - // icon: '๐Ÿ”', - // label: 'Search', - // description: 'General web search', - // systemPrompt: SYSTEM_PROMPTS[BaseOrchestratorAgentType.SEARCH], - // availableTools: [ - // new CombinedExtractionTool(), - // new NavigateBackTool(), - // new HTMLToMarkdownTool(), - // new SchemaBasedExtractorTool(), - // new NodeIDsToURLsTool(), - // new FinalizeWithCritiqueTool(), - // ] - // }, + [BaseOrchestratorAgentType.SEARCH]: { + type: BaseOrchestratorAgentType.SEARCH, + icon: '๐Ÿ”Ž', + label: 'Search', + description: 'Precision fact finding with structured output', + systemPrompt: SYSTEM_PROMPTS[BaseOrchestratorAgentType.SEARCH], + version: '2025-09-17', + availableTools: [ + ToolRegistry.getToolInstance('search_agent') || (() => { throw new Error('search_agent tool not found'); })(), + ToolRegistry.getToolInstance('web_task_agent') || (() => { throw new Error('web_task_agent tool not found'); })(), + ToolRegistry.getToolInstance('research_agent') || (() => { throw new Error('research_agent tool not found'); })(), + new FinalizeWithCritiqueTool(), + new SearchVisitHistoryTool(), + ] + }, [BaseOrchestratorAgentType.DEEP_RESEARCH]: { type: BaseOrchestratorAgentType.DEEP_RESEARCH, icon: '๐Ÿ“š', label: 'Deep Research', description: 'In-depth research on a topic', systemPrompt: SYSTEM_PROMPTS[BaseOrchestratorAgentType.DEEP_RESEARCH], + version: '2025-09-17', availableTools: [ ToolRegistry.getToolInstance('research_agent') || (() => { throw new Error('research_agent tool not found'); })(), ToolRegistry.getToolInstance('web_task_agent') || (() => { throw new Error('web_task_agent tool not found'); })(), ToolRegistry.getToolInstance('document_search') || (() => { throw new Error('document_search tool not found'); })(), ToolRegistry.getToolInstance('bookmark_store') || (() => { throw new Error('bookmark_store tool not found'); })(), + ToolRegistry.getToolInstance('search_agent') || (() => { throw new Error('search_agent tool not found'); })(), new FinalizeWithCritiqueTool(), ] }, - [BaseOrchestratorAgentType.SHOPPING]: { - type: BaseOrchestratorAgentType.SHOPPING, - icon: '๐Ÿ›’', - label: 'Shopping', - description: 'Find products and compare options', - systemPrompt: SYSTEM_PROMPTS[BaseOrchestratorAgentType.SHOPPING], - availableTools: [ - ToolRegistry.getToolInstance('web_task_agent') || (() => { throw new Error('web_task_agent tool not found'); })(), - ToolRegistry.getToolInstance('document_search') || (() => { throw new Error('document_search tool not found'); })(), - ToolRegistry.getToolInstance('bookmark_store') || (() => { throw new Error('bookmark_store tool not found'); })(), - new FinalizeWithCritiqueTool(), - ToolRegistry.getToolInstance('research_agent') || (() => { throw new Error('research_agent tool not found'); })(), - ToolRegistry.getToolInstance('ecommerce_product_info_fetcher_tool') || (() => { throw new Error('ecommerce_product_info_fetcher_tool tool not found'); })(), - ] - } + // [BaseOrchestratorAgentType.SHOPPING]: { + // type: BaseOrchestratorAgentType.SHOPPING, + // icon: '๐Ÿ›’', + // label: 'Shopping', + // description: 'Find products and compare options', + // systemPrompt: SYSTEM_PROMPTS[BaseOrchestratorAgentType.SHOPPING], + // version: '2025-09-17', + // availableTools: [ + // ToolRegistry.getToolInstance('web_task_agent') || (() => { throw new Error('web_task_agent tool not found'); })(), + // ToolRegistry.getToolInstance('document_search') || (() => { throw new Error('document_search tool not found'); })(), + // ToolRegistry.getToolInstance('bookmark_store') || (() => { throw new Error('bookmark_store tool not found'); })(), + // new FinalizeWithCritiqueTool(), + // ToolRegistry.getToolInstance('research_agent') || (() => { throw new Error('research_agent tool not found'); })(), + // ToolRegistry.getToolInstance('ecommerce_product_info_fetcher_tool') || (() => { throw new Error('ecommerce_product_info_fetcher_tool tool not found'); })(), + // ] + // } }; +// Register orchestrator descriptors for version tracking +for (const config of Object.values(AGENT_CONFIGS)) { + AgentDescriptorRegistry.registerSource({ + name: `orchestrator:${config.type}`, + type: config.type, + version: config.version ?? DEFAULT_ORCHESTRATOR_VERSION, + promptProvider: () => config.systemPrompt, + toolNamesProvider: () => config.availableTools.map(tool => tool.name) + }); +} + +// Register a default orchestrator descriptor for the fallback configuration +AgentDescriptorRegistry.registerSource({ + name: 'orchestrator:default', + type: 'default', + version: DEFAULT_ORCHESTRATOR_VERSION, + promptProvider: () => getSystemPrompt(''), + toolNamesProvider: () => getAgentTools('').map(tool => tool.name) +}); + /** * Get the system prompt for a specific agent type */ @@ -355,7 +386,7 @@ You automatically receive rich context with each iteration: - **Never let web_task_agent ask for accessibility trees**: If it reports it cannot extract data, instruct it to try different approach - **Always provide extraction_schema**: For any data extraction task, include a clear schema defining the fields to extract -- **Use proper agent delegation**: Don't try to access web pages directly - always use web_task_agent or research_agent +- **Use proper agent delegation**: Don't try to access web pages directly - always use web_task_agent, search_agent, or research_agent - **Handle extraction failures gracefully**: If initial task fails, try alternative approaches rather than asking users for help ## Task Execution Process @@ -390,7 +421,7 @@ Classify the task type to optimize execution strategy: 3. web_task_agent("Apply to selected jobs on LinkedIn with cover letter") **Information gathering**: Research-focused tasks requiring data collection -- Use research_agent for broad information gathering, web_task_agent for specific site data +- Use search_agent for targeted fact-finding, research_agent for broad information gathering, and web_task_agent for specific site data - Example: "Research renewable energy trends" โ†’ research_agent + specific site data from government/industry sites ### 3. Execution Plan Development @@ -408,7 +439,7 @@ Based on task type, develop a specific execution plan: - Identify potential failure points and alternative approaches **For information gathering:** -- Determine if research_agent or web_task_agent is more appropriate +- Determine if search_agent, research_agent, or web_task_agent is more appropriate - Plan authoritative sources and verification methods - Define data collection requirements and output format @@ -416,6 +447,7 @@ Based on task type, develop a specific execution plan: **IMPORTANT**: Always delegate site-specific work to the appropriate specialized agent: - Use 'web_task_agent' for any website interaction, navigation, or data extraction +- Use 'search_agent' for targeted fact-finding (contacts, team rosters, granular attribute checks) - Use 'research_agent' for broad information research across multiple sources - As the orchestrator, focus on: - Planning and strategy @@ -461,6 +493,7 @@ After specialized agents complete their tasks: */ export function getAgentTools(agentType: string): Array> { return AGENT_CONFIGS[agentType]?.availableTools || [ + ToolRegistry.getToolInstance('search_agent') || (() => { throw new Error('search_agent tool not found'); })(), ToolRegistry.getToolInstance('web_task_agent') || (() => { throw new Error('web_task_agent tool not found'); })(), ToolRegistry.getToolInstance('document_search') || (() => { throw new Error('document_search tool not found'); })(), ToolRegistry.getToolInstance('bookmark_store') || (() => { throw new Error('bookmark_store tool not found'); })(), @@ -675,4 +708,4 @@ declare global { [AgentTypeSelectionEvent.eventName]: AgentTypeSelectionEvent; } } - +import { AgentDescriptorRegistry } from './AgentDescriptorRegistry.js'; diff --git a/front_end/panels/ai_chat/core/ConfigurableGraph.ts b/front_end/panels/ai_chat/core/ConfigurableGraph.ts index 0dc52f54bc0..d24f8788191 100644 --- a/front_end/panels/ai_chat/core/ConfigurableGraph.ts +++ b/front_end/panels/ai_chat/core/ConfigurableGraph.ts @@ -68,7 +68,11 @@ export function createAgentGraphFromConfig( final: () => createFinalNode(), toolExecutor: (nodeCfg) => { return { - invoke: async (state: AgentState) => { + invoke: async (state: AgentState, signal?: AbortSignal) => { + if (signal?.aborted) { + logger.info('ToolExecutorNode dummy implementation aborted'); + throw new DOMException('Tool execution was cancelled', 'AbortError'); + } logger.warn(`ToolExecutorNode "${nodeCfg.name}" invoked without being dynamically replaced. This indicates an issue.`); return { ...state, error: `ToolExecutor ${nodeCfg.name} not properly initialized.` }; } @@ -85,7 +89,11 @@ export function createAgentGraphFromConfig( } else { logger.warn(`Unknown node type: ${nodeConfig.type} for node ${nodeConfig.name}. Adding a dummy error node.`); graph.addNode(nodeConfig.name, { - invoke: async (state: AgentState) => { + invoke: async (state: AgentState, signal?: AbortSignal) => { + if (signal?.aborted) { + logger.info('Dummy error node aborted'); + throw new DOMException('Execution was cancelled', 'AbortError'); + } logger.error(`Dummy node ${nodeConfig.name} invoked due to unknown type ${nodeConfig.type}`); return { ...state, error: `Unknown node type ${nodeConfig.type} for ${nodeConfig.name}` }; } diff --git a/front_end/panels/ai_chat/core/Logger.ts b/front_end/panels/ai_chat/core/Logger.ts index fa41887bf63..660e3000e40 100644 --- a/front_end/panels/ai_chat/core/Logger.ts +++ b/front_end/panels/ai_chat/core/Logger.ts @@ -97,7 +97,7 @@ export class Logger { /** * Check if we're in development mode */ - private static isDevelopment(): boolean { + static isDevelopment(): boolean { // Check for development indicators return location.hostname === 'localhost' || location.hostname.includes('127.0.0.1') || diff --git a/front_end/panels/ai_chat/core/PageInfoManager.ts b/front_end/panels/ai_chat/core/PageInfoManager.ts index cb08c97dc36..92b8126c658 100644 --- a/front_end/panels/ai_chat/core/PageInfoManager.ts +++ b/front_end/panels/ai_chat/core/PageInfoManager.ts @@ -230,7 +230,7 @@ Instructions: ${iframeContent && iframeContent.length > 0 ? '- The page contains embedded iframes with their own content, which is included above.' : ''} - If the user asks about the page, refer to this context. - If the partial accessibility tree is present, use it to answer questions about visible page structure, elements, or accessibility. -- If you need to extract any data from the entire page, you must always use the extract_schema_data tool to do so. Do not attempt to extract data from the full page by any other means. +- If you need to extract any data from the entire page, you must always use the extract_data tool to do so. Do not attempt to extract data from the full page by any other means. - If information is missing, answer based on what is available, or request the full page accessibility tree if necessary. - Always be concise, accurate, and helpful. diff --git a/front_end/panels/ai_chat/core/State.ts b/front_end/panels/ai_chat/core/State.ts index c60bef44f35..50a38ca465d 100644 --- a/front_end/panels/ai_chat/core/State.ts +++ b/front_end/panels/ai_chat/core/State.ts @@ -5,6 +5,7 @@ import * as i18n from '../../../core/i18n/i18n.js'; import {type ChatMessage, ChatMessageEntity, type ImageInputData} from '../models/ChatTypes.js'; import type {TracingContext} from '../tracing/TracingProvider.js'; +import type { AgentDescriptor } from './AgentDescriptorRegistry.js'; const UIStrings = { } as const; @@ -54,6 +55,11 @@ export interface DevToolsContext { intermediateStepsCount?: number; // Tracing context for distributed tracing tracingContext?: TracingContext; + // Descriptor describing the active agent configuration + agentDescriptor?: AgentDescriptor; + // Execution tracking for cancellation support + executionId?: string; + abortSignal?: AbortSignal; } /** diff --git a/front_end/panels/ai_chat/core/StateGraph.ts b/front_end/panels/ai_chat/core/StateGraph.ts index ebf8e87fb29..4bcb32eedd0 100644 --- a/front_end/panels/ai_chat/core/StateGraph.ts +++ b/front_end/panels/ai_chat/core/StateGraph.ts @@ -50,7 +50,7 @@ export class StateGraph { + async *invoke(state: TState, signal?: AbortSignal): AsyncGenerator { logger.debug(`Starting graph execution from entry point: ${this.entryPoint}`); console.warn(`Graph "${this.name}" started with entry point "${this.entryPoint}"`); @@ -59,6 +59,12 @@ export class StateGraph(); function sanitize(original: string): string { let name = original.replace(/[^a-zA-Z0-9_-]/g, '_'); if (!name) name = 'tool'; - if (name.length > 64) name = name.slice(0, 64); + if (name.length > 64) { + logger.warn(`Tool name too long (${name.length} chars), truncating to 64: ${original}`); + name = name.slice(0, 64); + } return name; } @@ -40,15 +47,13 @@ export function addMapping(original: string): string { if (sanitizedToOriginal.has(candidate) && sanitizedToOriginal.get(candidate) !== original) { const suffix = shortHash(original); const base = candidate.replace(/_+$/g, ''); - const maxBase = Math.max(1, 64 - 1 - suffix.length); - candidate = (base.length > maxBase ? base.slice(0, maxBase) : base) + '-' + suffix; + candidate = base + '-' + suffix; } let unique = candidate; let counter = 1; while (sanitizedToOriginal.has(unique) && sanitizedToOriginal.get(unique) !== original) { const add = `_${counter++}`; - const maxBase = Math.max(1, 64 - add.length); - unique = (candidate.length > maxBase ? candidate.slice(0, maxBase) : candidate) + add; + unique = candidate + add; } originalToSanitized.set(original, unique); sanitizedToOriginal.set(unique, original); diff --git a/front_end/panels/ai_chat/core/ToolSurfaceProvider.ts b/front_end/panels/ai_chat/core/ToolSurfaceProvider.ts index f16b6903ffa..3aff0b9055a 100644 --- a/front_end/panels/ai_chat/core/ToolSurfaceProvider.ts +++ b/front_end/panels/ai_chat/core/ToolSurfaceProvider.ts @@ -9,6 +9,8 @@ import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; import { MCPRegistry } from '../mcp/MCPRegistry.js'; import { getMCPConfig } from '../mcp/MCPConfig.js'; import { MCPToolAdapter } from '../mcp/MCPToolAdapter.js'; +import { LLMClient } from '../LLM/LLMClient.js'; +import { AIChatPanel } from '../ui/AIChatPanel.js'; const logger = createLogger('ToolSurfaceProvider'); @@ -29,10 +31,12 @@ function uniqByName(tools: Tool[]): Tool[] { return out; } -function getAllMcpTools(): Tool[] { +async function getAllMcpTools(): Promise[]> { try { + // Ensure tools are registered before getting status + await MCPRegistry.ensureToolsRegistered(); const status = MCPRegistry.getStatus(); - console.log('[TOOL_SELECTION_DEBUG] MCPRegistry status:', { + logger.debug('MCPRegistry status:', { enabled: status.enabled, serverCount: status.servers.length, servers: status.servers, @@ -47,45 +51,140 @@ function getAllMcpTools(): Tool[] { if (tool) { tools.push(tool); } else { - console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name); + logger.debug('Tool registered but not found:', name); } } - console.log('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', { + logger.debug('getAllMcpTools result:', { availableToolsCount: tools.length, availableToolNames: tools.map(t => t.name) }); return tools; } catch (error) { - console.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error); + logger.error('Error in getAllMcpTools:', error); return []; } } -function scoreTool(query: string, agentType: string | null | undefined, tool: Tool): number { - const q = (query || '').toLowerCase(); - const a = (agentType || '').toLowerCase(); - const name = (tool.name || '').toLowerCase(); - const desc = (tool.description || '').toLowerCase(); - let score = 0; - if (q && name.includes(q)) score += 10; - if (q && desc.includes(q)) score += 3; - if (a && name.includes(a)) score += 2; - if (a && desc.includes(a)) score += 1; - // Prefer MCP tools only slightly lower than strong name matches - if (tool instanceof MCPToolAdapter) score += 0.5; - return score; +async function selectToolsWithLLM( + query: string, + agentType: string | null | undefined, + mcpTools: Tool[], + maxMcpPerTurn: number +): Promise[]> { + try { + // Early return for empty tool list - avoid unnecessary LLM call + if (mcpTools.length === 0) { + logger.debug('No MCP tools provided to LLM selector'); + return []; + } + + const miniModel = AIChatPanel.getMiniModel(); + const miniProvider = AIChatPanel.getMiniModelWithProvider(); + if (!miniModel || !miniProvider) { + logger.debug('Mini model not available, falling back to first N tools'); + return mcpTools.slice(0, maxMcpPerTurn); + } + + const toolDescriptions = mcpTools.map(tool => + `- ${tool.name}: ${tool.description}` + ).join('\n'); + + const systemPrompt = `You are an intelligent tool selector. Given a user query and agent type, select the most relevant MCP tools. + +Select up to ${maxMcpPerTurn} most relevant tools for this query. Consider: +1. Direct keyword matches between query and tool names/descriptions +2. Semantic relevance to the task +3. Agent type preferences (e.g., research agents prefer search/analysis tools) + +CRITICAL RULES: +- You MUST ONLY select from the exact tool names provided in the "Available MCP tools" list +- Do NOT invent, create, or hallucinate any tool names +- Return ONLY the exact tool names as they appear in the list +- Respond with a JSON array containing only the selected tool names + +Response format: JSON array of exact tool names from the provided list.`; + + const userMessage = `User query: "${query}" +Agent type: ${agentType || 'general'} + +Available MCP tools: +${toolDescriptions}`; + + logger.debug('LLM prompt:', userMessage); + + const llmClient = LLMClient.getInstance(); + const response = await llmClient.call({ + provider: miniProvider.provider, + model: miniModel, + messages: [{ role: 'user', content: userMessage }], + systemPrompt, + temperature: 0.1 + }); + + logger.debug('LLM response:', response.text); + + // Parse the JSON response + let selectedToolNames: string[]; + try { + // Check if response.text is defined + if (!response.text) { + throw new Error('LLM response text is undefined'); + } + + // Try to extract JSON from the response + const jsonMatch = response.text.match(/\[[\s\S]*?\]/); + if (jsonMatch) { + selectedToolNames = JSON.parse(jsonMatch[0]); + } else { + throw new Error('No JSON array found in response'); + } + } catch (parseError) { + logger.debug('Failed to parse LLM response, falling back to first N tools:', parseError); + return mcpTools.slice(0, maxMcpPerTurn); + } + + // Map selected tool names back to tool objects + const selectedTools: Tool[] = []; + const toolMap = new Map(mcpTools.map(tool => [tool.name, tool])); + + for (const toolName of selectedToolNames) { + const tool = toolMap.get(toolName); + if (tool && selectedTools.length < maxMcpPerTurn) { + selectedTools.push(tool); + } + } + + // Fill remaining slots if LLM didn't select enough tools + if (selectedTools.length < maxMcpPerTurn) { + const remainingTools = mcpTools.filter(tool => !selectedTools.includes(tool)); + selectedTools.push(...remainingTools.slice(0, maxMcpPerTurn - selectedTools.length)); + } + + logger.debug('LLM selected tools:', { + selectedToolNames, + selectedCount: selectedTools.length, + finalToolNames: selectedTools.map(t => t.name) + }); + + return selectedTools; + + } catch (error) { + logger.error('Error in LLM tool selection:', error); + return mcpTools.slice(0, maxMcpPerTurn); + } } + // DEBUG: Add a utility function to test MCP modes from console (globalThis as any).debugToolSelection = { getCurrentMCPConfig: () => { const cfg = getMCPConfig(); - console.log('Current MCP Config:', cfg); + logger.debug('Current MCP Config:', cfg); return cfg; }, testMode: async (mode: 'all' | 'router' | 'meta') => { const originalConfig = getMCPConfig(); - console.log(`Testing mode: ${mode}`); + logger.debug(`Testing mode: ${mode}`); // Temporarily set the mode localStorage.setItem('ai_chat_mcp_tool_mode', mode); // Test with mock state @@ -99,24 +198,24 @@ function scoreTool(query: string, agentType: string | null | undefined, tool: To if (originalConfig.toolMode) { localStorage.setItem('ai_chat_mcp_tool_mode', originalConfig.toolMode); } - console.log(`Mode ${mode} result:`, result); + logger.debug(`Mode ${mode} result:`, result); return result; }, getMCPRegistryStatus: () => { const status = MCPRegistry.getStatus(); - console.log('MCP Registry Status:', status); + logger.debug('MCP Registry Status:', status); return status; } }; export const ToolSurfaceProvider = { async select(state: AgentState, baseTools: Tool[], opts?: ToolSelectionOptions): Promise<{ tools: Tool[]; selectedNames: string[] }> { - const { maxToolsPerTurn = 20, maxMcpPerTurn = 8 } = opts || {}; const cfg = getMCPConfig(); - const mode = cfg.toolMode || 'router'; + const { maxToolsPerTurn = cfg.maxToolsPerTurn || 50, maxMcpPerTurn = cfg.maxMcpPerTurn || 50 } = opts || {}; + const mode = cfg.toolMode || 'all'; // DEBUG: Log current MCP configuration and tool selection parameters - console.log('[TOOL_SELECTION_DEBUG] ToolSurfaceProvider.select called with:', { + logger.debug('ToolSurfaceProvider.select called with:', { maxToolsPerTurn, maxMcpPerTurn, mcpConfig: cfg, @@ -130,16 +229,16 @@ export const ToolSurfaceProvider = { let resultTools: Tool[] = uniqByName([...baseTools]); const selectedNames: string[] = []; - console.log('[TOOL_SELECTION_DEBUG] Base tools provided:', { + logger.debug('Base tools provided:', { agentType: state.selectedAgentType, baseToolsCount: baseTools.length, baseToolNames: baseTools.map(t => t.name) }); if (!cfg.enabled) { - console.log('[TOOL_SELECTION_DEBUG] MCP disabled, returning core tools only'); + logger.debug('MCP disabled, returning core tools only'); const uniq = uniqByName(resultTools).slice(0, maxToolsPerTurn); - console.log('[TOOL_SELECTION_DEBUG] Final result (MCP disabled):', { + logger.debug('Final result (MCP disabled):', { toolCount: uniq.length, toolNames: uniq.map(t => t.name) }); @@ -147,14 +246,14 @@ export const ToolSurfaceProvider = { } if (mode === 'all') { - console.log('[TOOL_SELECTION_DEBUG] Using ALL mode'); - const mcpTools = getAllMcpTools(); - console.log('[TOOL_SELECTION_DEBUG] MCP tools found:', { + logger.debug('Using ALL mode'); + const mcpTools = await getAllMcpTools(); + logger.debug('MCP tools found:', { mcpToolsCount: mcpTools.length, mcpToolNames: mcpTools.map(t => t.name) }); - resultTools = uniqByName([...resultTools, ...mcpTools]).slice(0, maxToolsPerTurn); - console.log('[TOOL_SELECTION_DEBUG] Final result (ALL mode):', { + resultTools = uniqByName([...resultTools, ...mcpTools]); + logger.debug('Final result (ALL mode):', { toolCount: resultTools.length, toolNames: resultTools.map(t => t.name) }); @@ -162,51 +261,84 @@ export const ToolSurfaceProvider = { } if (mode === 'meta') { - console.log('[TOOL_SELECTION_DEBUG] Using META mode'); + logger.debug('Using META mode'); // Include only meta-tools for MCP alongside core tools const search = ToolRegistry.getRegisteredTool('mcp.search'); const invoke = ToolRegistry.getRegisteredTool('mcp.invoke'); const metaTools = [search, invoke].filter(Boolean) as Tool[]; - console.log('[TOOL_SELECTION_DEBUG] Meta tools found:', { + logger.debug('Meta tools found:', { metaToolsCount: metaTools.length, metaToolNames: metaTools.map(t => t.name), searchTool: !!search, invokeTool: !!invoke }); resultTools = uniqByName([...resultTools, ...metaTools]).slice(0, maxToolsPerTurn); - console.log('[TOOL_SELECTION_DEBUG] Final result (META mode):', { + logger.debug('Final result (META mode):', { toolCount: resultTools.length, toolNames: resultTools.map(t => t.name) }); return { tools: resultTools, selectedNames: resultTools.map(t => t.name) }; } - // Router mode (heuristic pre-call selection) - console.log('[TOOL_SELECTION_DEBUG] Using ROUTER mode'); - const mcpTools = getAllMcpTools(); - console.log('[TOOL_SELECTION_DEBUG] MCP tools available for scoring:', { + // Router mode (LLM-based intelligent selection) + logger.debug('Using ROUTER mode with LLM selection'); + const mcpTools = await getAllMcpTools(); + logger.debug('MCP tools available for LLM selection:', { mcpToolsCount: mcpTools.length, mcpToolNames: mcpTools.map(t => t.name) }); - - const lastUserMsg = [...state.messages].reverse().find(m => m.entity === 'user' || (m as any).entity === 0) as any; - const queryText = lastUserMsg?.text || ''; - console.log('[TOOL_SELECTION_DEBUG] Query text for scoring:', queryText); - - const scored = mcpTools - .map(t => ({ t, s: scoreTool(queryText, state.selectedAgentType, t) })) - .sort((a, b) => b.s - a.s) - .slice(0, maxMcpPerTurn) - .map(({ t }) => t); - - console.log('[TOOL_SELECTION_DEBUG] Top scored MCP tools:', { - scoredToolsCount: scored.length, - scoredToolNames: scored.map(t => t.name), + + // Early return if no MCP tools available - avoid unnecessary LLM call + if (mcpTools.length === 0) { + logger.debug('No MCP tools available, skipping LLM selection'); + logger.debug('Final result (ROUTER mode - no MCP tools):', { + toolCount: resultTools.length, + toolNames: resultTools.map(t => t.name), + maxToolsPerTurn + }); + return { tools: resultTools, selectedNames: resultTools.map(t => t.name) }; + } + + // Gate LLM tool selection to only run on fresh user input + const lastMsg = state.messages[state.messages.length - 1] as any; + const isUserTurn = lastMsg?.entity === 'user' || lastMsg?.entity === 0; // 0 is ChatMessageEntity.USER in some compiled forms + + if (!isUserTurn) { + logger.debug('Not a user turn; skipping LLM tool selection. Attempting to reuse previous selection.'); + // Try to reuse the previous selection from state.context if available + const prevSelectedNames = (state.context as any)?.selectedToolNames as string[] | undefined; + if (Array.isArray(prevSelectedNames) && prevSelectedNames.length > 0) { + const mcpMap = new Map(mcpTools.map(t => [t.name, t] as const)); + const reused = prevSelectedNames.map(n => mcpMap.get(n)).filter(Boolean) as Tool[]; + const combined = uniqByName([...resultTools, ...reused]).slice(0, maxToolsPerTurn); + logger.debug('Reused previous MCP tool selection for non-user turn:', { + prevSelectedCount: prevSelectedNames.length, + reusedToolNames: reused.map(t => t.name), + finalToolNames: combined.map(t => t.name) + }); + return { tools: combined, selectedNames: prevSelectedNames }; + } + // No previous selection to reuse; return current (base) tools only + logger.debug('No previous selection found; returning base tools only for non-user turn.', { + toolCount: resultTools.length, + toolNames: resultTools.map(t => t.name) + }); + return { tools: resultTools, selectedNames: resultTools.map(t => t.name) }; + } + + // User input detected โ€” run intelligent selection + const queryText = lastMsg?.text || ''; + logger.debug('User turn detected. Query text for LLM selection:', queryText); + const selectedMcpTools = await selectToolsWithLLM(queryText, state.selectedAgentType, mcpTools, maxMcpPerTurn); + + logger.debug('LLM selected MCP tools:', { + selectedToolsCount: selectedMcpTools.length, + selectedToolNames: selectedMcpTools.map(t => t.name), maxMcpPerTurn }); - resultTools = uniqByName([...resultTools, ...scored]).slice(0, maxToolsPerTurn); - console.log('[TOOL_SELECTION_DEBUG] Final result (ROUTER mode):', { + resultTools = uniqByName([...resultTools, ...selectedMcpTools]).slice(0, maxToolsPerTurn); + logger.debug('Final result (ROUTER mode):', { toolCount: resultTools.length, toolNames: resultTools.map(t => t.name), maxToolsPerTurn diff --git a/front_end/panels/ai_chat/core/Types.ts b/front_end/panels/ai_chat/core/Types.ts index e7c85906d0c..1ed93d9cdb7 100644 --- a/front_end/panels/ai_chat/core/Types.ts +++ b/front_end/panels/ai_chat/core/Types.ts @@ -8,7 +8,7 @@ import type {AgentState} from './State.js'; * Interface for a runnable unit of work. */ export interface Runnable { - invoke(input: TInput): Promise; + invoke(input: TInput, signal?: AbortSignal): Promise; } /** @@ -24,6 +24,6 @@ export enum NodeType { * Interface for the compiled orchestrator agent */ export interface CompiledGraph { - invoke(state: AgentState): AsyncGenerator; + invoke(state: AgentState, signal?: AbortSignal): AsyncGenerator; onStep?: (stepData: {text: string, type: string}) => void; } diff --git a/front_end/panels/ai_chat/core/Version.ts b/front_end/panels/ai_chat/core/Version.ts index d2bcc5f2142..fae99706c54 100644 --- a/front_end/panels/ai_chat/core/Version.ts +++ b/front_end/panels/ai_chat/core/Version.ts @@ -3,8 +3,8 @@ // found in the LICENSE file. export const VERSION_INFO = { - version: '0.3.1', - buildDate: '2025-08-07', + version: '0.3.4', + buildDate: '2025-09-19', channel: 'stable' } as const; diff --git a/front_end/panels/ai_chat/core/AgentNodes.test.ts b/front_end/panels/ai_chat/core/__tests__/AgentNodes.test.ts similarity index 79% rename from front_end/panels/ai_chat/core/AgentNodes.test.ts rename to front_end/panels/ai_chat/core/__tests__/AgentNodes.test.ts index 0cfc2cdf83c..0b9b23d769d 100644 --- a/front_end/panels/ai_chat/core/AgentNodes.test.ts +++ b/front_end/panels/ai_chat/core/__tests__/AgentNodes.test.ts @@ -2,25 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { createToolExecutorNode } from './AgentNodes.js'; -import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js'; -import { ChatMessageEntity } from '../ui/ChatView.js'; -import type { AgentState } from './State.js'; -import type { ConfigurableAgentResult } from '../agent_framework/ConfigurableAgentTool.js'; - -declare global { - function describe(name: string, fn: () => void): void; - function it(name: string, fn: () => void): void; - function beforeEach(fn: () => void): void; - function afterEach(fn: () => void): void; - namespace assert { - function strictEqual(actual: unknown, expected: unknown): void; - function deepEqual(actual: unknown, expected: unknown): void; - function isTrue(value: unknown): void; - function isFalse(value: unknown): void; - function doesNotMatch(actual: string, regexp: RegExp): void; - } -} +import { createToolExecutorNode } from '../AgentNodes.js'; +import { ConfigurableAgentTool } from '../../agent_framework/ConfigurableAgentTool.js'; +import { ChatMessageEntity } from '../../models/ChatTypes.js'; +import type { AgentState } from '../State.js'; +import type { ConfigurableAgentResult } from '../../agent_framework/ConfigurableAgentTool.js'; + describe('AgentNodes ToolExecutorNode', () => { describe('ConfigurableAgentTool result filtering', () => { @@ -66,7 +53,7 @@ describe('AgentNodes ToolExecutorNode', () => { }); } - async execute(): Promise { + override async execute(): Promise { return errorResultWithSession; } } @@ -85,7 +72,6 @@ describe('AgentNodes ToolExecutorNode', () => { isFinalAnswer: false } ], - agentType: 'web_task', context: {} }; @@ -97,7 +83,8 @@ describe('AgentNodes ToolExecutorNode', () => { }; // Create ToolExecutorNode - const toolExecutorNode = createToolExecutorNode(stateWithMockTool); + const mockProvider = { name: 'test-provider' } as any; + const toolExecutorNode = createToolExecutorNode(stateWithMockTool, mockProvider, 'test-model'); // Execute the node const result = await toolExecutorNode.invoke(stateWithMockTool); @@ -113,11 +100,11 @@ describe('AgentNodes ToolExecutorNode', () => { assert.strictEqual(resultText, 'Error: Agent reached maximum iterations'); // Verify that the resultText does not contain session data - assert.doesNotMatch(resultText, /sessionId/); - assert.doesNotMatch(resultText, /test-session-123/); - assert.doesNotMatch(resultText, /intermediateSteps/); - assert.doesNotMatch(resultText, /agentSession/); - assert.doesNotMatch(resultText, /nestedSessions/); + assert.notMatch(resultText, /sessionId/); + assert.notMatch(resultText, /test-session-123/); + assert.notMatch(resultText, /intermediateSteps/); + assert.notMatch(resultText, /agentSession/); + assert.notMatch(resultText, /nestedSessions/); // The resultText should be clean and only contain the error message assert.strictEqual(resultText.includes('test-session-123'), false); @@ -159,7 +146,7 @@ describe('AgentNodes ToolExecutorNode', () => { }); } - async execute(): Promise { + override async execute(): Promise { return successResultWithSession; } } @@ -177,7 +164,6 @@ describe('AgentNodes ToolExecutorNode', () => { isFinalAnswer: false } ], - agentType: 'web_task', context: {} }; @@ -186,7 +172,8 @@ describe('AgentNodes ToolExecutorNode', () => { tools: [mockTool] }; - const toolExecutorNode = createToolExecutorNode(stateWithMockTool); + const mockProvider = { name: 'test-provider' } as any; + const toolExecutorNode = createToolExecutorNode(stateWithMockTool, mockProvider, 'test-model'); const result = await toolExecutorNode.invoke(stateWithMockTool); const toolResultMessage = result.messages[result.messages.length - 1]; @@ -196,9 +183,9 @@ describe('AgentNodes ToolExecutorNode', () => { assert.strictEqual(resultText, 'Task completed successfully'); // Should NOT contain session data - assert.doesNotMatch(resultText, /success-session-456/); - assert.doesNotMatch(resultText, /intermediateSteps/); - assert.doesNotMatch(resultText, /agentSession/); + assert.notMatch(resultText, /success-session-456/); + assert.notMatch(resultText, /intermediateSteps/); + assert.notMatch(resultText, /agentSession/); }); }); }); \ No newline at end of file diff --git a/front_end/panels/ai_chat/core/__tests__/ToolExecutorNode.test.ts b/front_end/panels/ai_chat/core/__tests__/ToolExecutorNode.test.ts new file mode 100644 index 00000000000..d73bc281738 --- /dev/null +++ b/front_end/panels/ai_chat/core/__tests__/ToolExecutorNode.test.ts @@ -0,0 +1,278 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { createToolExecutorNode } from '../AgentNodes.js'; +import type { AgentState } from '../State.js'; +import { ChatMessageEntity } from '../../models/ChatTypes.js'; +import * as ToolNameMap from '../ToolNameMap.js'; +import { ToolRegistry } from '../../agent_framework/ConfigurableAgentTool.js'; + +/* eslint-env mocha */ + +// Mock tool class +class MockTool { + constructor(public name: string, public description: string = `Mock tool ${name}`) {} + + get schema() { + return { + type: 'object', + properties: {}, + }; + } + + async execute(_args: Record): Promise { + return `Executed ${this.name}`; + } +} + +describe('ToolExecutorNode Resolution', () => { + beforeEach(() => { + ToolNameMap.clear(); + }); + + afterEach(() => { + ToolNameMap.clear(); + }); + + describe('MCP tool resolution', () => { + it('should resolve MCP tools by different name variants', () => { + // Setup: simulate an MCP tool registered with smart naming + const serverId = 'mcp-lusl1248if'; + const toolName = 'search'; + const namespacedName = `mcp:${serverId}:${toolName}`; + const smartName = toolName; + + console.log('Test 1 - MCP tool resolution variants:'); + console.log(' Namespaced name:', namespacedName); + console.log(' Smart name:', smartName); + + // Add mappings like MCPRegistry would + ToolNameMap.addMapping(namespacedName); + ToolNameMap.addMapping(smartName); + + // Register with smart name (this is what our updated MCPRegistry does) + const mockTool = new MockTool(smartName); + ToolRegistry.registerToolFactory(smartName, () => mockTool); + + // Create state with the tool selected + const state: AgentState = { + messages: [ + { + entity: ChatMessageEntity.USER, + text: 'Test user message', + }, + { + entity: ChatMessageEntity.MODEL, + action: 'tool', + toolName: namespacedName, // This is what comes from the LLM message + toolArgs: {}, + toolCallId: 'test-call-id', + isFinalAnswer: false, + } + ], + selectedAgentType: 'test', + context: { + selectedToolNames: [smartName], // This is what MCPRegistry puts in context + } as any, + }; + + // Create ToolExecutorNode + const toolExecutor = createToolExecutorNode(state, 'openai', 'gpt-4'); + + console.log(' State setup complete'); + console.log(' Tool message name:', namespacedName); + console.log(' Selected tool names:', (state.context as any)?.selectedToolNames); + + // Test that the tool executor can find the tool + // This should work with our enhanced resolution logic + return toolExecutor.invoke(state).then(result => { + console.log(' Resolution successful!'); + console.log(' Result messages:', result.messages.length); + + // Should have added a tool result message + assert.isTrue(result.messages.length > state.messages.length, 'Should add tool result message'); + + const lastMessage = result.messages[result.messages.length - 1]; + assert.strictEqual(lastMessage.entity, ChatMessageEntity.TOOL_RESULT, 'Last message should be tool result'); + }).catch(error => { + console.log(' Resolution failed with error:', error.message); + throw error; + }); + }); + + it('should test the exact error scenario from logs', () => { + // Recreate the exact scenario from the error logs + const requestedTool = 'mcp:mcp-lusl1248if:search'; + const smartName = 'search'; + + console.log('Test 2 - Exact error scenario:'); + console.log(' Requested tool (from error):', requestedTool); + console.log(' Smart name (how it should be registered):', smartName); + + // Register tool with smart name (correct way) + const mockTool = new MockTool(smartName); + ToolRegistry.registerToolFactory(smartName, () => mockTool); + + // Add mappings + ToolNameMap.addMapping(requestedTool); + ToolNameMap.addMapping(smartName); + + // Create state that requests the namespaced name + const state: AgentState = { + messages: [ + { + entity: ChatMessageEntity.USER, + text: 'Test search', + }, + { + entity: ChatMessageEntity.MODEL, + action: 'tool', + toolName: requestedTool, // Requesting namespaced name + toolArgs: {}, + toolCallId: 'test-call-id', + isFinalAnswer: false, + } + ], + selectedAgentType: 'test', + context: { + selectedToolNames: [smartName], // Only smart name in selection + } as any, + }; + + console.log(' Creating ToolExecutorNode...'); + const toolExecutor = createToolExecutorNode(state, 'openai', 'gpt-4'); + + console.log(' Testing resolution...'); + + // This should work with our enhanced fuzzy matching + return toolExecutor.invoke(state).then(result => { + console.log(' SUCCESS: Tool resolved successfully!'); + assert.isTrue(result.messages.length > state.messages.length); + + const toolResult = result.messages[result.messages.length - 1]; + assert.strictEqual(toolResult.entity, ChatMessageEntity.TOOL_RESULT); + + console.log(' Tool result:', (toolResult as any).resultText); + }).catch(error => { + console.log(' FAILED: Tool resolution failed:', error.message); + + // This tells us exactly what the issue is + if (error.message.includes('not found')) { + console.log(' Diagnosis: Tool name mismatch between registration and request'); + console.log(' - Tool registered as:', smartName); + console.log(' - Tool requested as:', requestedTool); + console.log(' - Need better name mapping or registration strategy'); + } + + throw error; + }); + }); + + it('should test fuzzy matching for MCP tools', () => { + const originalName = 'mcp:mcp-test123:notion_create_page'; + const smartName = 'notion_create_page'; + + console.log('Test 3 - Fuzzy matching:'); + console.log(' Original name:', originalName); + console.log(' Smart name:', smartName); + + // Register with smart name + const mockTool = new MockTool(smartName); + ToolRegistry.registerToolFactory(smartName, () => mockTool); + + // Add mappings + ToolNameMap.addMapping(originalName); + ToolNameMap.addMapping(smartName); + + // Try to request with sanitized version + const sanitizedName = ToolNameMap.getSanitized(originalName); + console.log(' Sanitized name:', sanitizedName); + + const state: AgentState = { + messages: [ + { + entity: ChatMessageEntity.USER, + text: 'Test create page', + }, + { + entity: ChatMessageEntity.MODEL, + action: 'tool', + toolName: sanitizedName, // Request sanitized version + toolArgs: {}, + toolCallId: 'test-call-id', + isFinalAnswer: false, + } + ], + selectedAgentType: 'test', + context: { + selectedToolNames: [smartName], + } as any, + }; + + const toolExecutor = createToolExecutorNode(state, 'openai', 'gpt-4'); + + return toolExecutor.invoke(state).then(result => { + console.log(' Fuzzy matching SUCCESS!'); + assert.isTrue(result.messages.length > state.messages.length); + }).catch(error => { + console.log(' Fuzzy matching FAILED:', error.message); + throw error; + }); + }); + }); + + describe('Tool map construction', () => { + it('should verify toolMap includes all name variants', () => { + const originalName = 'mcp:server:tool'; + const smartName = 'tool'; + + console.log('Test 4 - Tool map construction:'); + + // Register tool + const mockTool = new MockTool(smartName); + ToolRegistry.registerToolFactory(smartName, () => mockTool); + + // Add mappings + ToolNameMap.addMapping(originalName); + ToolNameMap.addMapping(smartName); + + const state: AgentState = { + messages: [], + selectedAgentType: 'test', + context: { + selectedToolNames: [smartName], + } as any, + }; + + // Create tool executor to see what gets added to toolMap + const toolExecutor = createToolExecutorNode(state, 'openai', 'gpt-4'); + + // The debug logs we added should show what's in the toolMap + console.log(' Tool executor created - check debug logs for toolMap contents'); + + // Test that it can handle the tool request + const testState: AgentState = { + ...state, + messages: [ + { + entity: ChatMessageEntity.MODEL, + action: 'tool', + toolName: originalName, + toolArgs: {}, + toolCallId: 'test', + isFinalAnswer: false, + } + ], + }; + + return toolExecutor.invoke(testState).then(result => { + console.log(' Tool map construction test PASSED'); + assert.isTrue(result.messages.length > 0); + }).catch(error => { + console.log(' Tool map construction test FAILED:', error.message); + throw error; + }); + }); + }); +}); \ No newline at end of file diff --git a/front_end/panels/ai_chat/core/__tests__/ToolNameMap.test.ts b/front_end/panels/ai_chat/core/__tests__/ToolNameMap.test.ts new file mode 100644 index 00000000000..aa7db412269 --- /dev/null +++ b/front_end/panels/ai_chat/core/__tests__/ToolNameMap.test.ts @@ -0,0 +1,142 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as ToolNameMap from '../ToolNameMap.js'; + +/* eslint-env mocha */ + +describe('ToolNameMap', () => { + beforeEach(() => { + ToolNameMap.clear(); + }); + + afterEach(() => { + ToolNameMap.clear(); + }); + + describe('MCP tool name handling', () => { + it('should handle MCP tool names with colons and hyphens', () => { + const original = 'mcp:mcp-lusl1248if:search'; + const sanitized = ToolNameMap.getSanitized(original); + + console.log('Test 1 - Basic MCP name:'); + console.log(' Original:', original); + console.log(' Sanitized:', sanitized); + console.log(' Expected: mcp_mcp-lusl1248if_search'); + + // Should replace colons with underscores but keep hyphens + assert.strictEqual(sanitized, 'mcp_mcp-lusl1248if_search'); + + // Should be able to resolve back + const resolved = ToolNameMap.resolveOriginal(sanitized); + console.log(' Resolved back:', resolved); + assert.strictEqual(resolved, original); + }); + + it('should handle long MCP tool names without truncation', () => { + const original = 'mcp:80a92942-4bf9-4c02-ab11-422151bec3a2:notion_find_page_by_title'; + const sanitized = ToolNameMap.getSanitized(original); + + console.log('Test 2 - Long MCP name:'); + console.log(' Original:', original); + console.log(' Sanitized:', sanitized); + console.log(' Length:', sanitized.length); + + // Should not be truncated (our fix removed 64-char limit) + const expected = 'mcp_80a92942-4bf9-4c02-ab11-422151bec3a2_notion_find_page_by_title'; + assert.strictEqual(sanitized, expected); + assert.isTrue(sanitized.length > 64, 'Should not be truncated at 64 characters'); + + // Should be able to resolve back + const resolved = ToolNameMap.resolveOriginal(sanitized); + console.log(' Resolved back:', resolved); + assert.strictEqual(resolved, original); + }); + + it('should handle conflicts correctly', () => { + const original1 = 'mcp:server1:search'; + const original2 = 'mcp:server2:search'; + + const sanitized1 = ToolNameMap.getSanitized(original1); + const sanitized2 = ToolNameMap.getSanitized(original2); + + console.log('Test 3 - Conflicts:'); + console.log(' Original1:', original1, '-> Sanitized1:', sanitized1); + console.log(' Original2:', original2, '-> Sanitized2:', sanitized2); + + // Both should get unique sanitized names + assert.notStrictEqual(sanitized1, sanitized2); + + // Both should resolve back correctly + assert.strictEqual(ToolNameMap.resolveOriginal(sanitized1), original1); + assert.strictEqual(ToolNameMap.resolveOriginal(sanitized2), original2); + }); + + it('should handle smart tool names (without server IDs)', () => { + const smartName = 'search'; + const sanitized = ToolNameMap.getSanitized(smartName); + + console.log('Test 4 - Smart name:'); + console.log(' Original:', smartName); + console.log(' Sanitized:', sanitized); + + // Simple names should remain unchanged + assert.strictEqual(sanitized, smartName); + + // Should resolve back to itself + const resolved = ToolNameMap.resolveOriginal(sanitized); + console.log(' Resolved back:', resolved); + assert.strictEqual(resolved, smartName); + }); + + it('should bidirectionally map both namespaced and smart names', () => { + const namespacedName = 'mcp:mcp-lusl1248if:search'; + const smartName = 'search'; + + // Add both mappings + const sanitizedNamespaced = ToolNameMap.getSanitized(namespacedName); + const sanitizedSmart = ToolNameMap.getSanitized(smartName); + + console.log('Test 5 - Bidirectional mapping:'); + console.log(' Namespaced:', namespacedName, '-> Sanitized:', sanitizedNamespaced); + console.log(' Smart:', smartName, '-> Sanitized:', sanitizedSmart); + + // Both should be resolvable + assert.strictEqual(ToolNameMap.resolveOriginal(sanitizedNamespaced), namespacedName); + assert.strictEqual(ToolNameMap.resolveOriginal(sanitizedSmart), smartName); + }); + }); + + describe('Edge cases', () => { + it('should handle special characters correctly', () => { + const original = 'mcp:test-server_123:tool@name.ext'; + const sanitized = ToolNameMap.getSanitized(original); + + console.log('Test 6 - Special characters:'); + console.log(' Original:', original); + console.log(' Sanitized:', sanitized); + + // Should replace non-alphanumeric characters except hyphens and underscores + const expected = 'mcp_test-server_123_tool_name_ext'; + assert.strictEqual(sanitized, expected); + + // Should resolve back + assert.strictEqual(ToolNameMap.resolveOriginal(sanitized), original); + }); + + it('should handle empty and invalid names', () => { + console.log('Test 7 - Edge cases:'); + + const empty = ''; + const sanitizedEmpty = ToolNameMap.getSanitized(empty); + console.log(' Empty string -> Sanitized:', sanitizedEmpty); + assert.strictEqual(sanitizedEmpty, 'tool'); + + const onlySpecialChars = '@#$%^&*()'; + const sanitizedSpecial = ToolNameMap.getSanitized(onlySpecialChars); + console.log(' Special chars only -> Sanitized:', sanitizedSpecial); + assert.strictEqual(sanitizedSpecial, '_________'); // 9 underscores for 9 special chars + }); + }); +}); \ No newline at end of file diff --git a/front_end/panels/ai_chat/core/ToolNameMapping.test.ts b/front_end/panels/ai_chat/core/__tests__/ToolNameMapping.test.ts similarity index 87% rename from front_end/panels/ai_chat/core/ToolNameMapping.test.ts rename to front_end/panels/ai_chat/core/__tests__/ToolNameMapping.test.ts index 7b835d16ce7..09b02d3756d 100644 --- a/front_end/panels/ai_chat/core/ToolNameMapping.test.ts +++ b/front_end/panels/ai_chat/core/__tests__/ToolNameMapping.test.ts @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { createAgentNode, createToolExecutorNode } from './AgentNodes.js'; -import type { AgentState } from './State.js'; -import type { Tool } from '../tools/Tools.js'; -import { ChatMessageEntity } from '../models/ChatTypes.js'; -import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; -import '../agent_framework/ConfigurableAgentTool.js'; -import { LLMClient } from '../LLM/LLMClient.js'; +import { createAgentNode, createToolExecutorNode } from '../AgentNodes.js'; +import type { AgentState } from '../State.js'; +import type { Tool } from '../../tools/Tools.js'; +import { ChatMessageEntity } from '../../models/ChatTypes.js'; +import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js'; +import '../../agent_framework/ConfigurableAgentTool.js'; +import { LLMClient } from '../../LLM/LLMClient.js'; /* eslint-env mocha */ diff --git a/front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts b/front_end/panels/ai_chat/core/__tests__/ToolSurfaceProvider.test.ts similarity index 93% rename from front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts rename to front_end/panels/ai_chat/core/__tests__/ToolSurfaceProvider.test.ts index 6b6d7c90a45..d7143e78513 100644 --- a/front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts +++ b/front_end/panels/ai_chat/core/__tests__/ToolSurfaceProvider.test.ts @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { ToolSurfaceProvider } from './ToolSurfaceProvider.js'; -import type { AgentState } from './State.js'; -import type { Tool } from '../tools/Tools.js'; -import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; -import { MCPRegistry } from '../mcp/MCPRegistry.js'; -import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js'; +import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js'; +import type { AgentState } from '../State.js'; +import type { Tool } from '../../tools/Tools.js'; +import { ToolRegistry } from '../../agent_framework/ConfigurableAgentTool.js'; +import { MCPRegistry } from '../../mcp/MCPRegistry.js'; +import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js'; /* eslint-env mocha */ diff --git a/front_end/panels/ai_chat/docs/MCP_OAuth_Implementation_Plan.md b/front_end/panels/ai_chat/docs/MCP_OAuth_Implementation_Plan.md new file mode 100644 index 00000000000..187f4f154fc --- /dev/null +++ b/front_end/panels/ai_chat/docs/MCP_OAuth_Implementation_Plan.md @@ -0,0 +1,333 @@ +# MCP OAuth Implementation Plan + +## Overview + +This document outlines the implementation plan for adding OAuth 2.0 authentication support to the DevTools MCP (Model Context Protocol) client. This will enable secure connections to MCP servers like Zapier without requiring users to manually manage API keys. + +## Background + +### Current MCP Implementation +- **MCPClientSDK**: Wrapper around the MCP SDK with basic bearer token auth +- **MCPRegistry**: Manages server connections and tool registration +- **MCPConfig**: Configuration storage using localStorage/sessionStorage +- **SettingsDialog**: UI for MCP configuration (currently hidden) + +### OAuth URL Context +When services like Zapier provide an "OAuth Server URL", they're offering a standardized OAuth 2.0 endpoint for MCP client authentication. The URL format typically follows: +``` +https://nla.zapier.com/oauth/mcp/authorize?client_id= +``` + +## Architecture Overview + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ SettingsDialog โ”‚โ”€โ”€โ”€โ”€โ”‚ MCPOAuthFlow โ”‚โ”€โ”€โ”€โ”€โ”‚ MCPOAuthProviderโ”‚ +โ”‚ (UI) โ”‚ โ”‚ (Orchestrator) โ”‚ โ”‚ (Storage) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ MCPConfig โ”‚ โ”‚ MCPClientSDK โ”‚ โ”‚ MCP SDK OAuth โ”‚ +โ”‚ (Config) โ”‚ โ”‚ (Connection) โ”‚ โ”‚ (Protocol) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Implementation Components + +### 1. MCPOAuthProvider (`mcp/MCPOAuthProvider.ts`) + +**Purpose**: Implements the `OAuthClientProvider` interface from the MCP SDK for browser environments. + +**Key Methods**: +```typescript +interface MCPOAuthProvider extends OAuthClientProvider { + // Required by MCP SDK + get redirectUrl(): string; + get clientMetadata(): OAuthClientMetadata; + clientInformation(): OAuthClientInformation | undefined; + saveClientInformation(info: OAuthClientInformationFull): void; + tokens(): OAuthTokens | undefined; + saveTokens(tokens: OAuthTokens): void; + redirectToAuthorization(url: URL): void; + saveCodeVerifier(verifier: string): void; + codeVerifier(): string; + + // Custom methods for browser environment + initializeForServer(serverUrl: string): Promise; + handleAuthCallback(authCode: string): Promise; + clearCredentials(): void; +} +``` + +**Storage Strategy**: +- **sessionStorage**: OAuth tokens (cleared on tab close) +- **localStorage**: Client metadata, configuration +- **In-memory**: Code verifiers (security best practice) + +**Security Features**: +- PKCE (Proof Key for Code Exchange) for all flows +- State parameter validation for CSRF protection +- Automatic token refresh before expiry +- Secure token storage patterns + +### 2. MCPOAuthFlow (`mcp/MCPOAuthFlow.ts`) + +**Purpose**: Orchestrates the complete OAuth flow in the browser environment. + +**Key Methods**: +```typescript +class MCPOAuthFlow { + async startOAuthFlow(serverUrl: string, clientMetadata: OAuthClientMetadata): Promise; + async handlePopupCallback(popup: Window): Promise; // auth code + async completeTokenExchange(authCode: string, provider: MCPOAuthProvider): Promise; + async refreshTokens(provider: MCPOAuthProvider): Promise; +} + +interface OAuthFlowResult { + success: boolean; + tokens?: OAuthTokens; + error?: string; +} +``` + +**Flow Strategy**: +1. **Popup-based flow** (preferred for DevTools) + - Opens OAuth provider in popup window + - Monitors popup for redirect with auth code + - Automatically closes popup on completion + +2. **Fallback options**: + - Redirect flow (if popup blocked) + - Manual auth code entry + +### 3. Extended MCPConfig (`mcp/MCPConfig.ts`) + +**New Configuration Fields**: +```typescript +interface MCPConfigData { + // Existing fields... + enabled: boolean; + endpoint?: string; + token?: string; + + // New OAuth fields + authType: 'bearer' | 'oauth'; + oauthServerUrl?: string; + oauthClientId?: string; + oauthScope?: string; + oauthRedirectUrl?: string; + + // Runtime OAuth state (not persisted) + oauthTokens?: OAuthTokens; + oauthClientInfo?: OAuthClientInformation; +} +``` + +**Storage Mapping**: +```typescript +const OAUTH_KEYS = { + authType: 'ai_chat_mcp_auth_type', + oauthServerUrl: 'ai_chat_mcp_oauth_server_url', + oauthClientId: 'ai_chat_mcp_oauth_client_id', + oauthScope: 'ai_chat_mcp_oauth_scope', + oauthRedirectUrl: 'ai_chat_mcp_oauth_redirect_url', +} as const; +``` + +### 4. Updated MCPClientSDK (`third_party/mcp-sdk/mcp-sdk.ts`) + +**Enhanced Connection Logic**: +```typescript +class MCPClientSDK { + async connectWithOAuth(server: MCPServer, provider: MCPOAuthProvider): Promise; + async connectWithBearer(server: MCPServer): Promise; // existing + + private async createOAuthTransport(server: MCPServer, provider: MCPOAuthProvider): Promise; + private async handleOAuthErrors(error: Error, provider: MCPOAuthProvider): Promise; +} +``` + +**OAuth Integration Points**: +- Use `StreamableHTTPClientTransport` with `authProvider` +- Handle `UnauthorizedError` with automatic token refresh +- Support both OAuth and bearer token authentication + +### 5. OAuth UI Components (`ui/SettingsDialog.ts`) + +**New UI Elements**: + +1. **Authentication Type Toggle**: + ``` + โ—‹ Bearer Token โ— OAuth 2.0 + ``` + +2. **OAuth Configuration Section**: + ``` + OAuth Server URL: [https://nla.zapier.com/oauth/...] + Client ID: [optional field] + Scope: [mcp:tools (default)] + + [Connect with OAuth] [Disconnect] + + Status: โ— Connected (expires in 2h 15m) + Last connected: Dec 15, 2024 at 3:42 PM + ``` + +3. **OAuth Flow Indicators**: + - Loading spinner during authorization + - Success/error messages + - Connection status with token expiry + +**User Experience Flow**: +1. User enters OAuth Server URL from Zapier/other provider +2. Clicks "Connect with OAuth" +3. Popup opens with OAuth provider's authorization page +4. User authorizes in popup +5. Popup closes automatically, connection established +6. UI shows connected status with token info + +### 6. OAuth Utilities (`mcp/MCPOAuthUtils.ts`) + +**Helper Functions**: +```typescript +// PKCE utilities +export function generateCodeVerifier(): string; +export function generateCodeChallenge(verifier: string): Promise; + +// State management +export function generateState(): string; +export function validateState(received: string, expected: string): boolean; + +// Token utilities +export function isTokenExpired(token: OAuthTokens): boolean; +export function shouldRefreshToken(token: OAuthTokens, bufferMinutes: number): boolean; + +// URL utilities +export function parseAuthCallbackUrl(url: string): { code?: string; state?: string; error?: string }; +export function buildRedirectUrl(): string; + +// Browser utilities +export function openOAuthPopup(authUrl: string): Promise; +export function waitForPopupCallback(popup: Window): Promise; +``` + +## Implementation Phases + +### Phase 1: Core OAuth Infrastructure +1. Implement `MCPOAuthProvider` with basic functionality +2. Create `MCPOAuthUtils` with PKCE and state management +3. Add OAuth configuration fields to `MCPConfig` +4. Unit tests for OAuth utilities + +### Phase 2: OAuth Flow Implementation +1. Implement `MCPOAuthFlow` with popup-based authorization +2. Update `MCPClientSDK` to support OAuth connections +3. Add OAuth error handling and token refresh +4. Integration tests with mock OAuth server + +### Phase 3: UI Integration +1. Update `SettingsDialog` with OAuth configuration UI +2. Add OAuth connection status display +3. Implement connect/disconnect flows +4. Handle OAuth errors in UI + +### Phase 4: Testing & Polish +1. Test with real OAuth providers (Zapier, etc.) +2. Error handling improvements +3. Documentation updates +4. Performance optimizations + +## Security Considerations + +### Token Security +- **Short-lived access tokens**: Request tokens with reasonable expiry +- **Secure storage**: Use sessionStorage for tokens (cleared on tab close) +- **Token rotation**: Always use refresh tokens when available +- **Revocation**: Support token revocation on disconnect + +### Flow Security +- **PKCE**: Use for all flows, even with confidential clients +- **State validation**: Prevent CSRF attacks +- **Popup security**: Validate popup origin and URLs +- **Input validation**: Sanitize all OAuth URLs and parameters + +### Client Security +- **No client secrets**: Never store secrets in frontend code +- **Dynamic registration**: Use when supported by server +- **Scope limitation**: Request minimal necessary scopes +- **Redirect validation**: Validate all redirect URLs + +## Testing Strategy + +### Unit Tests +- `MCPOAuthUtils`: PKCE generation, state validation +- `MCPOAuthProvider`: Token storage, client metadata handling +- `MCPConfig`: OAuth configuration persistence + +### Integration Tests +- `MCPOAuthFlow`: Complete OAuth flow simulation +- `MCPClientSDK`: OAuth connection with mock transport +- UI components: OAuth settings and connection flows + +### End-to-End Tests +- Real OAuth provider integration (Zapier) +- Token refresh scenarios +- Error handling (expired tokens, revoked access) +- Multi-tab behavior with sessionStorage + +## Error Handling + +### OAuth-Specific Errors +```typescript +interface OAuthErrorType { + 'oauth_authorization_pending': 'User has not completed authorization', + 'oauth_access_denied': 'User denied authorization', + 'oauth_invalid_client': 'Client credentials invalid', + 'oauth_invalid_grant': 'Authorization code invalid/expired', + 'oauth_expired_token': 'Access token expired', + 'oauth_insufficient_scope': 'Token lacks required scope', +} +``` + +### Recovery Strategies +- **Expired tokens**: Automatic refresh with retry +- **Revoked access**: Clear tokens, prompt re-authorization +- **Network errors**: Exponential backoff with retry +- **Popup blocked**: Fallback to redirect flow +- **Invalid configuration**: Clear state, show configuration UI + +## Future Enhancements + +### Phase 2 Features +1. **Multiple OAuth servers**: Support multiple simultaneous connections +2. **Client certificate auth**: For enterprise OAuth flows +3. **OpenID Connect**: Enhanced identity and metadata +4. **Advanced scoping**: Fine-grained permission management + +### Integration Opportunities +1. **Browser credential storage**: Integration with browser password manager +2. **Single sign-on**: OAuth provider discovery and preference +3. **Audit logging**: OAuth connection and usage tracking +4. **Admin controls**: Enterprise policy enforcement + +## Success Metrics + +### Technical Metrics +- OAuth flow completion rate > 95% +- Token refresh success rate > 99% +- Connection establishment time < 5 seconds +- Zero security vulnerabilities in OAuth implementation + +### User Experience Metrics +- Reduction in support tickets related to API key management +- User adoption of OAuth vs. bearer token authentication +- Time to first successful MCP connection +- User satisfaction with OAuth flow simplicity + +## Conclusion + +This OAuth implementation will significantly improve the security and user experience of MCP connections in DevTools. By supporting industry-standard OAuth 2.0 flows, users can securely connect to services like Zapier without managing API keys manually. + +The phased implementation approach ensures we can validate the architecture early and iterate based on real-world usage patterns. The security-first design protects user credentials while maintaining the flexibility needed for various OAuth providers. \ No newline at end of file diff --git a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts index 283e50e8148..9e83880164b 100644 --- a/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts +++ b/front_end/panels/ai_chat/evaluation/remote/EvaluationAgent.ts @@ -7,11 +7,13 @@ import { BUILD_CONFIG } from '../../core/BuildConfig.js'; import { WebSocketRPCClient } from '../../common/WebSocketRPCClient.js'; import { LLMConfigurationManager } from '../../core/LLMConfigurationManager.js'; import { getEvaluationConfig, getEvaluationClientId } from '../../common/EvaluationConfig.js'; -import { ToolRegistry } from '../../agent_framework/ConfigurableAgentTool.js'; +import { ToolRegistry, ConfigurableAgentTool } from '../../agent_framework/ConfigurableAgentTool.js'; import { AgentService } from '../../core/AgentService.js'; import { AIChatPanel } from '../../ui/AIChatPanel.js'; import { createLogger } from '../../core/Logger.js'; import { createTracingProvider, withTracingContext, isTracingEnabled, getTracingConfig } from '../../tracing/TracingConfig.js'; +import { AgentDescriptorRegistry, type AgentDescriptor } from '../../core/AgentDescriptorRegistry.js'; +import '../../core/BaseOrchestratorAgent.js'; import type { TracingProvider, TracingContext } from '../../tracing/TracingProvider.js'; import type { ChatMessage } from '../../models/ChatTypes.js'; import { @@ -70,6 +72,7 @@ export class EvaluationAgent { private judgeModel: string; private miniModel: string; private nanoModel: string; + private orchestratorDescriptorPromise: Promise; constructor(options: EvaluationAgentOptions) { this.clientId = options.clientId; @@ -79,6 +82,7 @@ export class EvaluationAgent { this.miniModel = options.miniModel; this.nanoModel = options.nanoModel; this.tracingProvider = createTracingProvider(); + this.orchestratorDescriptorPromise = AgentDescriptorRegistry.getDescriptor('orchestrator:default'); logger.info('EvaluationAgent created with tracing provider', { clientId: this.clientId, @@ -425,6 +429,7 @@ export class EvaluationAgent { sessionId, parentObservationId: undefined }; + const orchestratorDescriptor = await this.orchestratorDescriptorPromise; try { // Initialize tracing provider if not already done @@ -447,7 +452,13 @@ export class EvaluationAgent { evaluationId: params.evaluationId, tool: params.tool, url: params.url, - source: 'evaluation-server' + source: 'evaluation-server', + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) }, 'evaluation-agent', ['evaluation', params.tool] @@ -620,7 +631,13 @@ export class EvaluationAgent { statusMessage: 'completed', metadata: { executionTime, - evaluationId: params.evaluationId + evaluationId: params.evaluationId, + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) } }); } catch (error) { @@ -664,7 +681,13 @@ export class EvaluationAgent { statusMessage: 'failed', metadata: { executionTime, - evaluationId: params.evaluationId + evaluationId: params.evaluationId, + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) } }); } catch (updateError) { @@ -696,6 +719,7 @@ export class EvaluationAgent { ): Promise { const spanId = `tool-exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const startTime = new Date(); + const toolDescriptor = tool instanceof ConfigurableAgentTool ? await AgentDescriptorRegistry.getDescriptor(tool.name) : null; // Create tool execution span if tracing context is provided if (tracingContext) { @@ -708,7 +732,13 @@ export class EvaluationAgent { input, metadata: { tool: toolName, - timeout + timeout, + ...(toolDescriptor ? { + agentVersion: toolDescriptor.version, + agentName: toolDescriptor.name, + promptHash: toolDescriptor.promptHash, + toolsetHash: toolDescriptor.toolsetHash + } : {}) } }, tracingContext.traceId); } catch (error) { @@ -722,7 +752,13 @@ export class EvaluationAgent { if (tracingContext) { this.tracingProvider.updateObservation(spanId, { endTime: new Date(), - error: `Tool execution timeout after ${timeout}ms` + error: `Tool execution timeout after ${timeout}ms`, + metadata: toolDescriptor ? { + agentVersion: toolDescriptor.version, + agentName: toolDescriptor.name, + promptHash: toolDescriptor.promptHash, + toolsetHash: toolDescriptor.toolsetHash + } : undefined }).catch(err => logger.warn('Failed to update span with timeout:', err)); } reject(new Error(`Tool execution timeout after ${timeout}ms`)); @@ -741,7 +777,13 @@ export class EvaluationAgent { if (tracingContext) { this.tracingProvider.updateObservation(spanId, { endTime: new Date(), - output: result + output: result, + metadata: toolDescriptor ? { + agentVersion: toolDescriptor.version, + agentName: toolDescriptor.name, + promptHash: toolDescriptor.promptHash, + toolsetHash: toolDescriptor.toolsetHash + } : undefined }).catch(err => logger.warn('Failed to update span with result:', err)); } @@ -754,7 +796,13 @@ export class EvaluationAgent { if (tracingContext) { this.tracingProvider.updateObservation(spanId, { endTime: new Date(), - error: error.message + error: error.message, + metadata: toolDescriptor ? { + agentVersion: toolDescriptor.version, + agentName: toolDescriptor.name, + promptHash: toolDescriptor.promptHash, + toolsetHash: toolDescriptor.toolsetHash + } : undefined }).catch(err => logger.warn('Failed to update span with error:', err)); } @@ -834,6 +882,7 @@ export class EvaluationAgent { let chatObservationId: string | undefined; // Get configuration manager for override support (defined outside try-catch for finally block) const configManager = LLMConfigurationManager.getInstance(); + const orchestratorDescriptor = await this.orchestratorDescriptorPromise; try { @@ -887,7 +936,13 @@ export class EvaluationAgent { startTime: new Date(), input: { message: input.message, model: mainModel }, metadata: { - evaluationType: 'chat' + evaluationType: 'chat', + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) } }, tracingContext.traceId); } catch (error) { @@ -910,7 +965,18 @@ export class EvaluationAgent { try { await this.tracingProvider.updateObservation(chatObservationId, { endTime: new Date(), - output: { response: responseText, messageCount: agentService.getMessages().length } + output: { response: responseText, messageCount: agentService.getMessages().length }, + metadata: orchestratorDescriptor ? { + evaluationType: 'chat', + status: 'completed', + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : { + evaluationType: 'chat', + status: 'completed' + } }); } catch (error) { logger.warn('Failed to update chat execution observation:', error); @@ -946,7 +1012,18 @@ export class EvaluationAgent { try { await this.tracingProvider.updateObservation(chatObservationId, { endTime: new Date(), - error: error instanceof Error ? error.message : String(error) + error: error instanceof Error ? error.message : String(error), + metadata: orchestratorDescriptor ? { + evaluationType: 'chat', + status: 'failed', + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : { + evaluationType: 'chat', + status: 'failed' + } }); } catch (updateError) { logger.warn('Failed to update chat execution observation with error:', updateError); diff --git a/front_end/panels/ai_chat/evaluation/runner/EvaluationRunner.ts b/front_end/panels/ai_chat/evaluation/runner/EvaluationRunner.ts index 3dd3cd0a897..df9da41fae8 100644 --- a/front_end/panels/ai_chat/evaluation/runner/EvaluationRunner.ts +++ b/front_end/panels/ai_chat/evaluation/runner/EvaluationRunner.ts @@ -8,6 +8,8 @@ import { AgentService } from '../../core/AgentService.js'; import { ToolRegistry } from '../../agent_framework/ConfigurableAgentTool.js'; import type { EvaluationConfig, TestResult, TestCase } from '../framework/types.js'; import { createLogger } from '../../core/Logger.js'; +import { AgentDescriptorRegistry, type AgentDescriptor } from '../../core/AgentDescriptorRegistry.js'; +import '../../core/BaseOrchestratorAgent.js'; import { LLMClient } from '../../LLM/LLMClient.js'; import type { LLMProviderConfig } from '../../LLM/LLMClient.js'; import { TIMING_CONSTANTS } from '../../core/Constants.js'; @@ -33,6 +35,7 @@ export class EvaluationRunner { private tracingProvider: TracingProvider; private sessionId: string; #llmInitPromise: Promise | null = null; + #orchestratorDescriptorPromise: Promise; constructor(options: EvaluationRunnerOptions) { // Get API key from AgentService @@ -71,6 +74,7 @@ export class EvaluationRunner { // Initialize tracing this.tracingProvider = createTracingProvider(); this.sessionId = `evaluation-session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + this.#orchestratorDescriptorPromise = AgentDescriptorRegistry.getDescriptor('orchestrator:default'); logger.info('EvaluationRunner created with tracing provider', { sessionId: this.sessionId, @@ -169,6 +173,8 @@ export class EvaluationRunner { parentObservationId: undefined }; + const orchestratorDescriptor = await this.#orchestratorDescriptorPromise; + // Create trace for this evaluation if (isTracingEnabled()) { try { @@ -197,7 +203,13 @@ export class EvaluationRunner { type: 'evaluation', tool: testCase.tool, url: testCase.url, - testId: testCase.id || testCase.name + testId: testCase.id || testCase.name, + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) }, 'evaluation-runner', ['evaluation', testCase.tool, 'test'] @@ -243,7 +255,13 @@ export class EvaluationRunner { metadata: { tool: testCase.tool, testId: testCase.id || testCase.name, - phase: 'llm-evaluation' + phase: 'llm-evaluation', + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) } }, traceId); } @@ -262,7 +280,13 @@ export class EvaluationRunner { metadata: { score: llmJudgment.score, passed: llmJudgment.passed, - explanation: llmJudgment.explanation + explanation: llmJudgment.explanation, + ...(orchestratorDescriptor ? { + agentVersion: orchestratorDescriptor.version, + agentName: orchestratorDescriptor.name, + promptHash: orchestratorDescriptor.promptHash, + toolsetHash: orchestratorDescriptor.toolsetHash + } : {}) } }); } diff --git a/front_end/panels/ai_chat/evaluation/test-cases/schema-extractor-tests.ts b/front_end/panels/ai_chat/evaluation/test-cases/schema-extractor-tests.ts index a2ff50c0ce7..e31b9f48d20 100644 --- a/front_end/panels/ai_chat/evaluation/test-cases/schema-extractor-tests.ts +++ b/front_end/panels/ai_chat/evaluation/test-cases/schema-extractor-tests.ts @@ -27,7 +27,7 @@ function createSchemaTest( ): TestCase { return { ...baseConfig, - tool: useStreamlined ? 'extract_schema_streamlined' : 'extract_schema_data' + tool: useStreamlined ? 'extract_schema_streamlined' : 'extract_data' }; } @@ -122,7 +122,7 @@ export const ecommerceTest: TestCase = { name: 'Extract Amazon Product Details', description: 'Extract product information from an Amazon product page', url: 'https://www.amazon.com/Obelisk-Climbing-Rustproof-Trellises-Clematis/dp/B0B4SBY6QD/', - tool: 'extract_schema_data', + tool: 'extract_data', input: { schema: { type: 'object', @@ -194,7 +194,7 @@ export const newsTest: TestCase = { name: 'Extract BBC News Article', description: 'Extract article content and metadata from a BBC News page', url: 'https://www.bbc.com/news/technology', - tool: 'extract_schema_data', + tool: 'extract_data', input: { schema: { type: 'object', @@ -255,7 +255,7 @@ export const googleSearchTest: TestCase = { name: 'Extract Google Search Results', description: 'Extract search results from Google search page', url: 'https://www.google.com/search?q=chrome+devtools+tutorial', - tool: 'extract_schema_data', + tool: 'extract_data', input: { schema: { type: 'object', @@ -321,7 +321,7 @@ export const bingSearchTest: TestCase = { name: 'Extract Bing Search Results', description: 'Extract search results from Bing search page', url: 'https://www.bing.com/search?q=web+scraping+best+practices', - tool: 'extract_schema_data', + tool: 'extract_data', input: { schema: { type: 'object', @@ -382,7 +382,7 @@ export const wikipediaSearchTest: TestCase = { name: 'Extract Wikipedia Search Results', description: 'Extract search results from Wikipedia search', url: 'https://en.wikipedia.org/w/index.php?search=artificial+intelligence&title=Special:Search', - tool: 'extract_schema_data', + tool: 'extract_data', input: { schema: { type: 'object', @@ -448,7 +448,7 @@ export const homeDepotTest: TestCase = { name: 'Extract Home Depot Product Search', description: 'Extract product listings from Home Depot search results', url: 'https://www.homedepot.com/s/power%2520drill', - tool: 'extract_schema_data', + tool: 'extract_data', input: { schema: { type: 'object', @@ -526,7 +526,7 @@ export const macysTest: TestCase = { name: 'Extract Macy\'s Product Listings', description: 'Extract fashion products from Macy\'s category page', url: 'https://www.macys.com/shop/womens-clothing/womens-dresses', - tool: 'extract_schema_data', + tool: 'extract_data', input: { schema: { type: 'object', @@ -619,7 +619,7 @@ export const googleFlightsTest: TestCase = { name: 'Extract Google Flights Search Results', description: 'Extract flight options from Google Flights search', url: 'https://www.google.com/travel/flights/search?tfs=CBwQAhojEgoyMDI1LTEyLTI0agwIAhIIL20vMGQ5anJyBwgBEgNTRk8aIxIKMjAyNS0xMi0zMWoHCAESA1NGT3IMCAISCC9tLzBkOWpyQAFIAXABggELCP___________wGYAQE', - tool: 'extract_schema_data', + tool: 'extract_data', input: { schema: { type: 'object', @@ -707,7 +707,7 @@ export const simpleTest: TestCase = { name: 'Extract GitHub Repository Info', description: 'Extract basic repository information from a GitHub page', url: 'https://github.com/microsoft/TypeScript', - tool: 'extract_schema_data', + tool: 'extract_data', input: { schema: { type: 'object', diff --git a/front_end/panels/ai_chat/evaluation/test-cases/web-task-agent-tests.ts b/front_end/panels/ai_chat/evaluation/test-cases/web-task-agent-tests.ts index 0d1c7dc2440..14811e9f893 100644 --- a/front_end/panels/ai_chat/evaluation/test-cases/web-task-agent-tests.ts +++ b/front_end/panels/ai_chat/evaluation/test-cases/web-task-agent-tests.ts @@ -364,7 +364,7 @@ export const jobSearchTest: TestCase = { 'Either used construct_direct_url for LinkedIn job search OR used traditional form interaction', 'If using direct URL: constructed proper LinkedIn job search URL with keywords and location', 'If using forms: delegated keyword and location input to action_agent', - 'Extracted job listings using schema_based_extractor', + 'Extracted job listings using extract_data', 'Returned structured job data in readable text format (not JSON)', 'Each job listing includes title, company, location, and other relevant fields', 'Results are numbered or organized clearly for easy reading', @@ -485,7 +485,7 @@ export const realEstateSearchTest: TestCase = { 'Delegated price filter setting to action_agent', 'Coordinated property type selection through action_agent', 'Applied search filters through proper action_agent calls', - 'Extracted property listings with schema_based_extractor', + 'Extracted property listings with extract_data', 'Returned structured property data in readable text format (not JSON)', 'Each property includes address, price, bedrooms, bathrooms, and other key details', 'Properties are clearly numbered or organized for easy comparison', diff --git a/front_end/panels/ai_chat/mcp/MCPConfig.ts b/front_end/panels/ai_chat/mcp/MCPConfig.ts index 8753a7c5e90..f17f63030ca 100644 --- a/front_end/panels/ai_chat/mcp/MCPConfig.ts +++ b/front_end/panels/ai_chat/mcp/MCPConfig.ts @@ -2,66 +2,342 @@ import { createLogger } from '../core/Logger.js'; const logger = createLogger('MCPConfig'); -export interface MCPConfigData { +export interface MCPProviderConfig { + id: string; + name?: string; + endpoint: string; + authType: 'bearer' | 'oauth'; enabled: boolean; - endpoint?: string; // MVP: single endpoint; Phase 2 can support multiple token?: string; + oauthClientId?: string; + oauthRedirectUrl?: string; + oauthScope?: string; +} + +export interface MCPConfigData { + enabled: boolean; + providers: MCPProviderConfig[]; toolAllowlist?: string[]; autostart?: boolean; toolMode?: 'all' | 'router' | 'meta'; maxToolsPerTurn?: number; maxMcpPerTurn?: number; + autoRefreshTokens?: boolean; + maxConnectionRetries?: number; + retryDelayMs?: number; + proactiveRefreshThresholdMs?: number; } +export type MCPConfigUpdate = Partial>; + const KEYS = { enabled: 'ai_chat_mcp_enabled', - endpoint: 'ai_chat_mcp_endpoint', - token: 'ai_chat_mcp_token', + providers: 'ai_chat_mcp_providers', + tokenMap: 'ai_chat_mcp_tokens_by_provider', allowlist: 'ai_chat_mcp_tool_allowlist', autostart: 'ai_chat_mcp_autostart', toolMode: 'ai_chat_mcp_tool_mode', maxToolsPerTurn: 'ai_chat_mcp_max_tools_per_turn', maxMcpPerTurn: 'ai_chat_mcp_max_mcp_per_turn', + autoRefreshTokens: 'ai_chat_mcp_auto_refresh_tokens', + maxConnectionRetries: 'ai_chat_mcp_max_connection_retries', + retryDelayMs: 'ai_chat_mcp_retry_delay_ms', + proactiveRefreshThresholdMs: 'ai_chat_mcp_proactive_refresh_threshold_ms', } as const; -export function getMCPConfig(): MCPConfigData { +interface StoredProvider { + id: string; + name?: string; + endpoint: string; + authType: 'bearer' | 'oauth'; + enabled?: boolean; + oauthClientId?: string; + oauthRedirectUrl?: string; + oauthScope?: string; +} + +type TokenMap = Record; + +function sanitizeProvider(provider: StoredProvider, index: number): StoredProvider | null { + if (!provider || typeof provider !== 'object') { + return null; + } + const id = typeof provider.id === 'string' && provider.id.trim() ? provider.id.trim() : undefined; + const endpoint = typeof provider.endpoint === 'string' ? provider.endpoint.trim() : ''; + const authType = provider.authType === 'oauth' ? 'oauth' : 'bearer'; + if (!id || !endpoint) { + return null; + } + return { + id, + name: typeof provider.name === 'string' ? provider.name.trim() || undefined : undefined, + endpoint, + authType, + enabled: provider.enabled !== false, + oauthClientId: typeof provider.oauthClientId === 'string' ? provider.oauthClientId.trim() || undefined : undefined, + oauthRedirectUrl: typeof provider.oauthRedirectUrl === 'string' ? provider.oauthRedirectUrl.trim() || undefined : undefined, + oauthScope: typeof provider.oauthScope === 'string' ? provider.oauthScope.trim() || undefined : undefined, + }; +} + +function loadProviders(): StoredProvider[] { try { - const enabled = localStorage.getItem(KEYS.enabled) === 'true'; - const endpoint = localStorage.getItem(KEYS.endpoint) || undefined; - const token = sessionStorage.getItem(KEYS.token) || undefined; - let toolAllowlist: string[] | undefined; - const raw = localStorage.getItem(KEYS.allowlist); - if (raw) { - try { toolAllowlist = JSON.parse(raw); } catch { toolAllowlist = undefined; } + const raw = localStorage.getItem(KEYS.providers); + if (!raw) { + return []; } - const autostart = localStorage.getItem(KEYS.autostart) === 'true'; - const toolMode = (localStorage.getItem(KEYS.toolMode) as MCPConfigData['toolMode']) || 'router'; - const maxToolsPerTurn = parseInt(localStorage.getItem(KEYS.maxToolsPerTurn) || '20', 10); - const maxMcpPerTurn = parseInt(localStorage.getItem(KEYS.maxMcpPerTurn) || '8', 10); - return { enabled, endpoint, token, toolAllowlist, autostart, toolMode, maxToolsPerTurn, maxMcpPerTurn }; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) { + return []; + } + const seen = new Set(); + const result: StoredProvider[] = []; + for (let i = 0; i < parsed.length; ++i) { + const sanitized = sanitizeProvider(parsed[i] as StoredProvider, i); + if (!sanitized) { + continue; + } + if (seen.has(sanitized.id)) { + continue; + } + seen.add(sanitized.id); + result.push(sanitized); + } + return result; } catch (err) { - logger.error('Failed to load MCP config', err); - return { enabled: false }; + logger.warn('Failed to parse MCP providers', err); + return []; + } +} + +function saveProvidersInternal(providers: StoredProvider[]): void { + try { + if (!providers.length) { + localStorage.removeItem(KEYS.providers); + } else { + localStorage.setItem(KEYS.providers, JSON.stringify(providers)); + } + } catch (err) { + logger.error('Failed to persist MCP providers', err); } } -export function setMCPConfig(config: MCPConfigData): void { +function loadTokenMap(): TokenMap { try { - localStorage.setItem(KEYS.enabled, String(!!config.enabled)); - if (config.endpoint !== undefined) { - localStorage.setItem(KEYS.endpoint, config.endpoint); + const raw = sessionStorage.getItem(KEYS.tokenMap); + if (!raw) { + return {}; + } + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + return parsed as TokenMap; + } + } catch (err) { + logger.warn('Failed to parse MCP token map', err); + } + sessionStorage.removeItem(KEYS.tokenMap); + return {}; +} + +function saveTokenMap(tokenMap: TokenMap): void { + try { + if (Object.keys(tokenMap).length === 0) { + sessionStorage.removeItem(KEYS.tokenMap); + } else { + sessionStorage.setItem(KEYS.tokenMap, JSON.stringify(tokenMap)); + } + } catch (err) { + logger.error('Failed to persist MCP token map', err); + } +} + +function sanitizeIdBase(input: string): string { + let sanitized = input.trim().toLowerCase(); + sanitized = sanitized.replace(/[^a-z0-9_-]+/g, '-'); + sanitized = sanitized.replace(/-+/g, '-'); + sanitized = sanitized.replace(/^[-_]+|[-_]+$/g, ''); + return sanitized || 'mcp'; +} + +function ensureMcpPrefix(id: string): string { + return id.startsWith('mcp-') ? id : `mcp-${id}`; +} + +function extractDomainBase(host: string): string { + const cleanedHost = host.replace(/\.+$/, '').toLowerCase(); + if (/^\d+\.\d+\.\d+\.\d+$/.test(cleanedHost)) { + return cleanedHost; + } + const parts = cleanedHost.split('.').filter(Boolean); + if (parts.length === 0) { + return host; + } + if (parts.length === 1) { + return parts[0]; + } + + const commonSecondLevel = new Set(['co', 'com', 'net', 'org', 'gov', 'edu', 'ac']); + const topLevel = parts[parts.length - 1]; + const secondLevel = parts[parts.length - 2]; + + // Handle common country-code second-level domains like *.co.uk + if (topLevel.length === 2 && parts.length >= 3 && commonSecondLevel.has(secondLevel)) { + return parts[parts.length - 3]; + } + + const commonTlds = new Set(['com', 'org', 'net', 'gov', 'edu', 'io', 'ai', 'app', 'dev', 'cloud', 'info', 'biz', 'co']); + if (commonTlds.has(topLevel) && parts.length >= 2) { + return secondLevel; + } + + return secondLevel; +} + +export function generateMCPProviderId(provider?: { id?: string; name?: string; endpoint?: string }): string { + // Prefer explicit ID if provided + const explicitId = provider?.id?.trim(); + if (explicitId) { + return ensureMcpPrefix(sanitizeIdBase(explicitId)); + } + + const name = provider?.name?.trim(); + if (name) { + return ensureMcpPrefix(sanitizeIdBase(name)); + } + + const endpoint = provider?.endpoint?.trim(); + if (endpoint) { + try { + const host = new URL(endpoint).hostname; + if (host) { + return ensureMcpPrefix(sanitizeIdBase(extractDomainBase(host))); + } + } catch { + // Fall through to using the raw endpoint if URL parsing fails + return ensureMcpPrefix(sanitizeIdBase(endpoint)); + } + } + + // Fallback: generate a random ID if neither name nor endpoint is available yet + const fallback = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`; + return ensureMcpPrefix(sanitizeIdBase(fallback)); +} + +export function getMCPProviders(): MCPProviderConfig[] { + const providers = loadProviders(); + const tokenMap = loadTokenMap(); + return providers.map(provider => ({ + ...provider, + enabled: provider.enabled !== false, + token: tokenMap[provider.id], + })); +} + +export function saveMCPProviders(providers: MCPProviderConfig[]): void { + // Get existing providers to identify which ones are being removed + const existingProviders = loadProviders(); + const existingIds = new Set(existingProviders.map(p => p.id)); + + const sanitizedProviders: StoredProvider[] = []; + const newTokenMap: TokenMap = {}; + const newIds = new Set(); + const seenIds = new Set(); + + for (const provider of providers) { + const id = generateMCPProviderId(provider); + const endpoint = provider.endpoint?.trim(); + if (!endpoint) { + continue; + } + const authType: 'bearer' | 'oauth' = provider.authType === 'oauth' ? 'oauth' : 'bearer'; + + if (seenIds.has(id)) { + throw new Error(`Duplicate MCP connection identifier: ${id}. Please use unique names or endpoints.`); + } + seenIds.add(id); + newIds.add(id); + + sanitizedProviders.push({ + id, + name: provider.name?.trim() || undefined, + endpoint, + authType, + enabled: provider.enabled !== false, + oauthClientId: provider.oauthClientId?.trim() || undefined, + oauthRedirectUrl: provider.oauthRedirectUrl?.trim() || undefined, + oauthScope: provider.oauthScope?.trim() || undefined, + }); + + if (authType === 'bearer' && provider.token) { + newTokenMap[id] = provider.token; + } + } + + // Clean up OAuth data for removed providers + for (const existingId of existingIds) { + if (!newIds.has(existingId)) { + cleanupOAuthData(existingId); } - if (config.token !== undefined) { + } + + saveProvidersInternal(sanitizedProviders); + saveTokenMap(newTokenMap); + dispatchMCPConfigChanged(); +} + +export function getMCPConfig(): MCPConfigData { + try { + const enabled = localStorage.getItem(KEYS.enabled) !== 'false'; // Default: true + const providers = getMCPProviders(); + + let toolAllowlist: string[] | undefined; + const rawAllowlist = localStorage.getItem(KEYS.allowlist); + if (rawAllowlist) { try { - if (config.token) { - sessionStorage.setItem(KEYS.token, config.token); - } else { - sessionStorage.removeItem(KEYS.token); + const parsed = JSON.parse(rawAllowlist); + if (Array.isArray(parsed)) { + toolAllowlist = parsed.filter(item => typeof item === 'string'); } - } catch (e) { - logger.error('Failed to persist MCP token to sessionStorage', e); + } catch { + toolAllowlist = undefined; } } + + const autostart = localStorage.getItem(KEYS.autostart) === 'true'; + const toolMode = (localStorage.getItem(KEYS.toolMode) as MCPConfigData['toolMode']) || 'all'; + const maxToolsPerTurn = parseInt(localStorage.getItem(KEYS.maxToolsPerTurn) || '50', 10); + const maxMcpPerTurn = parseInt(localStorage.getItem(KEYS.maxMcpPerTurn) || '50', 10); + + // New auto-refresh and retry configuration options with defaults + const autoRefreshTokens = localStorage.getItem(KEYS.autoRefreshTokens) !== 'false'; // Default: true + const maxConnectionRetries = parseInt(localStorage.getItem(KEYS.maxConnectionRetries) || '3', 10); + const retryDelayMs = parseInt(localStorage.getItem(KEYS.retryDelayMs) || '1000', 10); + const proactiveRefreshThresholdMs = parseInt(localStorage.getItem(KEYS.proactiveRefreshThresholdMs) || '300000', 10); // 5 minutes + + return { + enabled, + providers, + toolAllowlist, + autostart, + toolMode, + maxToolsPerTurn, + maxMcpPerTurn, + autoRefreshTokens, + maxConnectionRetries, + retryDelayMs, + proactiveRefreshThresholdMs, + }; + } catch (err) { + logger.error('Failed to load MCP config', err); + return { enabled: false, providers: [] }; + } +} + +export function setMCPConfig(config: MCPConfigUpdate): void { + try { + if (config.enabled !== undefined) { + localStorage.setItem(KEYS.enabled, String(!!config.enabled)); + } if (config.toolAllowlist) { localStorage.setItem(KEYS.allowlist, JSON.stringify(config.toolAllowlist)); } @@ -77,6 +353,18 @@ export function setMCPConfig(config: MCPConfigData): void { if (config.maxMcpPerTurn !== undefined) { localStorage.setItem(KEYS.maxMcpPerTurn, String(config.maxMcpPerTurn)); } + if (config.autoRefreshTokens !== undefined) { + localStorage.setItem(KEYS.autoRefreshTokens, String(!!config.autoRefreshTokens)); + } + if (config.maxConnectionRetries !== undefined) { + localStorage.setItem(KEYS.maxConnectionRetries, String(config.maxConnectionRetries)); + } + if (config.retryDelayMs !== undefined) { + localStorage.setItem(KEYS.retryDelayMs, String(config.retryDelayMs)); + } + if (config.proactiveRefreshThresholdMs !== undefined) { + localStorage.setItem(KEYS.proactiveRefreshThresholdMs, String(config.proactiveRefreshThresholdMs)); + } } catch (err) { logger.error('Failed to save MCP config', err); } finally { @@ -101,3 +389,97 @@ function dispatchMCPConfigChanged(): void { logger.warn('Failed to dispatch MCP config change event', err); } } + +/** + * Interface for stored authentication error details + */ +export interface StoredAuthError { + message: string; + type: 'authentication' | 'network' | 'configuration' | 'server_error' | 'unknown'; + timestamp: number; + serverId: string; +} + +/** + * Get stored authentication errors for all MCP providers + */ +export function getStoredAuthErrors(): StoredAuthError[] { + const errors: StoredAuthError[] = []; + const providers = getMCPProviders(); + + for (const provider of providers) { + if (provider.authType === 'oauth') { + try { + const prefix = `mcp_oauth:${provider.id}:`; + const message = localStorage.getItem(`${prefix}last_auth_error`); + const timestampStr = localStorage.getItem(`${prefix}auth_error_timestamp`); + const type = localStorage.getItem(`${prefix}auth_error_type`); + + if (message && timestampStr && type) { + const timestamp = parseInt(timestampStr, 10); + if (!isNaN(timestamp)) { + errors.push({ + message, + type: type as StoredAuthError['type'], + timestamp, + serverId: provider.id, + }); + } + } + } catch (err) { + logger.warn('Failed to retrieve stored auth error for provider', { providerId: provider.id, err }); + } + } + } + + return errors; +} + +/** + * Clear stored authentication error for a specific provider + */ +export function clearStoredAuthError(serverId: string): void { + try { + const prefix = `mcp_oauth:${serverId}:`; + localStorage.removeItem(`${prefix}last_auth_error`); + localStorage.removeItem(`${prefix}auth_error_timestamp`); + localStorage.removeItem(`${prefix}auth_error_type`); + } catch (err) { + logger.warn('Failed to clear stored auth error', { serverId, err }); + } +} + +/** + * Check if any OAuth providers have stored authentication errors + */ +export function hasStoredAuthErrors(): boolean { + return getStoredAuthErrors().length > 0; +} + +/** + * Clean up all OAuth-related data for a specific provider + */ +export function cleanupOAuthData(serverId: string): void { + try { + const prefix = `mcp_oauth:${serverId}:`; + const keysToRemove = [ + 'tokens', + 'client_info', + 'code_verifier', + 'state', + 'original_url', + 'last_auth_error', + 'auth_error_timestamp', + 'auth_error_type', + 'token_expiration', + ]; + + for (const key of keysToRemove) { + localStorage.removeItem(`${prefix}${key}`); + } + + logger.info('Cleaned up OAuth data for provider', { serverId }); + } catch (err) { + logger.warn('Failed to clean up OAuth data', { serverId, err }); + } +} diff --git a/front_end/panels/ai_chat/mcp/MCPRegistry.ts b/front_end/panels/ai_chat/mcp/MCPRegistry.ts index d095f754b4d..1480e11d7b7 100644 --- a/front_end/panels/ai_chat/mcp/MCPRegistry.ts +++ b/front_end/panels/ai_chat/mcp/MCPRegistry.ts @@ -1,40 +1,125 @@ import { createLogger } from '../core/Logger.js'; import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js'; import * as ToolNameMap from '../core/ToolNameMap.js'; -import type { MCPToolDef, MCPServer } from '../../../third_party/mcp-sdk/mcp-sdk.js'; -import { MCPClient } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import type { MCPToolDef, MCPServer } from '../../../third_party/mcp-sdk/mcp-sdk-v2.js'; +import { MCPClient } from '../../../third_party/mcp-sdk/mcp-sdk-v2.js'; import { getMCPConfig } from './MCPConfig.js'; import { MCPToolAdapter } from './MCPToolAdapter.js'; const logger = createLogger('MCPRegistry'); +interface RegistryServer extends MCPServer { + name?: string; + authType: 'bearer' | 'oauth'; +} + +export interface ConnectionResult { + serverId: string; + name?: string; + endpoint: string; + connected: boolean; + error?: Error; + errorType?: 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown'; + retryAttempts?: number; +} + +interface RetryConfig { + maxRetries: number; + baseDelayMs: number; + maxDelayMs: number; + backoffMultiplier: number; + jitterMs: number; +} + +export interface ConnectionEvent { + timestamp: Date; + serverId: string; + eventType: 'connected' | 'disconnected' | 'auth_error' | 'retry_attempt' | 'connection_failed'; + details?: string; + retryAttempt?: number; +} + export interface MCPRegistryStatus { enabled: boolean; - servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number }>; + servers: Array<{ id: string; name?: string; endpoint: string; authType: 'bearer' | 'oauth'; connected: boolean; toolCount: number }>; registeredToolNames: string[]; lastError?: string; lastErrorType?: 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown'; lastConnected?: Date; lastDisconnected?: Date; + connectionEvents: ConnectionEvent[]; } class RegistryImpl { private client = new MCPClient(); - private servers: MCPServer[] = []; + private servers: RegistryServer[] = []; private registeredTools: string[] = []; private lastError?: string; private lastErrorType?: 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown'; private lastConnected?: Date; private lastDisconnected?: Date; + private connectionEvents: ConnectionEvent[] = []; + private readonly maxConnectionEvents = 50; // Keep last 50 events + + private getRetryConfig(): RetryConfig { + const cfg = getMCPConfig(); + return { + maxRetries: cfg.maxConnectionRetries || 3, + baseDelayMs: cfg.retryDelayMs || 1000, + maxDelayMs: Math.max((cfg.retryDelayMs || 1000) * 10, 10000), // 10x base delay or 10s minimum + backoffMultiplier: 2, + jitterMs: 500, + }; + } private categorizeError(error: unknown): 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown' { const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase(); - + + // Check for SSE-specific error context + if (error instanceof Error && 'context' in error) { + const context = (error as any).context; + + // OAuth-related failures + if (context?.authState === 'oauth_required' || context?.httpStatus === 401) { + return 'authentication'; + } + + // Network/connection failures with specific status codes + if (context?.httpStatus === 404) { + return 'configuration'; // Endpoint not found + } + if (context?.httpStatus === 403) { + return 'authentication'; // Forbidden - likely auth issue + } + if (context?.httpStatus >= 500) { + return 'server_error'; + } + + // CORS or connection timeouts + if (context?.readyState === 2) { // EventSource CLOSED state + return 'network'; + } + } + + // Check for CORS errors (common with SSE) + if (message.includes('cors') || message.includes('cross-origin') || message.includes('fetch')) { + return 'network'; + } + + // SSE-specific errors + if (message.includes('sse error') || message.includes('eventsource')) { + if (message.includes('oauth') || message.includes('401') || message.includes('unauthorized')) { + return 'authentication'; + } + return 'connection'; + } + + // Original categorization logic if (message.includes('unauthorized') || message.includes('authentication') || message.includes('auth') || message.includes('token')) { return 'authentication'; } if (message.includes('network') || message.includes('timeout') || message.includes('connection reset') || message.includes('econnreset')) { - return 'network'; + return 'network'; } if (message.includes('connection') || message.includes('connect') || message.includes('econnrefused') || message.includes('websocket')) { return 'connection'; @@ -53,110 +138,545 @@ class RegistryImpl { this.lastErrorType = this.categorizeError(error); } - async init(): Promise { + /** + * Check if an error type is worth retrying + */ + private shouldRetryError(errorType: string): boolean { + // Only retry network errors and server errors, not authentication or configuration errors + return errorType === 'network' || errorType === 'server_error' || errorType === 'connection'; + } + + /** + * Calculate delay for retry attempt with exponential backoff and jitter + */ + private calculateRetryDelay(attempt: number, config: RetryConfig): number { + const exponentialDelay = config.baseDelayMs * Math.pow(config.backoffMultiplier, attempt); + const jitter = Math.random() * config.jitterMs; + return Math.min(exponentialDelay + jitter, config.maxDelayMs); + } + + /** + * Sleep for specified milliseconds + */ + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Refresh tools for a specific server with retry logic + */ + private async refreshToolsForServer(serverId: string, retryCount = 0): Promise { + const maxRetries = 2; + const retryDelayMs = 2000; // 2 seconds + + if (!this.client.isConnected(serverId)) { + return; + } + + const server = this.servers.find(s => s.id === serverId); + if (!server) { + logger.warn('MCPRegistry: Server not found for tool refresh', { serverId }); + return; + } + + try { + + const tools = await this.client.listTools(serverId); + + // If we got tools, we're done - tools exist and will be registered by the next refresh + if (tools.length > 0) { + return; + } else if (retryCount < maxRetries) { + // No tools found, but maybe the server needs more time + + setTimeout(async () => { + try { + await this.refreshToolsForServer(serverId, retryCount + 1); + } catch (error) { + logger.warn('MCPRegistry: Delayed tool refresh failed', { serverId, error }); + } + }, retryDelayMs); + } else { + } + } catch (error) { + logger.warn('MCPRegistry: Failed to refresh tools for server', { + serverId, + attempt: retryCount + 1, + maxRetries, + error + }); + + if (retryCount < maxRetries) { + + setTimeout(async () => { + try { + await this.refreshToolsForServer(serverId, retryCount + 1); + } catch (retryError) { + logger.warn('MCPRegistry: Delayed tool refresh retry failed', { serverId, error: retryError }); + } + }, retryDelayMs); + } + } + } + + /** + * Track a connection event + */ + private trackConnectionEvent(event: Omit): void { + const connectionEvent: ConnectionEvent = { + ...event, + timestamp: new Date(), + }; + + this.connectionEvents.push(connectionEvent); + + // Keep only the last N events + if (this.connectionEvents.length > this.maxConnectionEvents) { + this.connectionEvents = this.connectionEvents.slice(-this.maxConnectionEvents); + } + + // Also persist to localStorage for persistence across sessions + this.saveConnectionEvents(); + + } + + /** + * Save connection events to localStorage + */ + private saveConnectionEvents(): void { + try { + const serialized = this.connectionEvents.map(event => ({ + ...event, + timestamp: event.timestamp.toISOString(), + })); + localStorage.setItem('ai_chat_mcp_connection_events', JSON.stringify(serialized)); + } catch (error) { + logger.warn('Failed to save connection events', error); + } + } + + /** + * Load connection events from localStorage + */ + private loadConnectionEvents(): void { + try { + const raw = localStorage.getItem('ai_chat_mcp_connection_events'); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + this.connectionEvents = parsed.map(event => ({ + ...event, + timestamp: new Date(event.timestamp), + })).slice(-this.maxConnectionEvents); // Ensure we don't exceed max + } + } + } catch (error) { + logger.warn('Failed to load connection events', error); + this.connectionEvents = []; + } + } + + /** + * Get connection events for display + */ + getConnectionEvents(): ConnectionEvent[] { + return [...this.connectionEvents].reverse(); // Most recent first + } + + /** + * Clear stored authentication error for a specific server + */ + private clearStoredAuthErrorForServer(serverId: string): void { + try { + const prefix = `mcp_oauth:${serverId}:`; + localStorage.removeItem(`${prefix}last_auth_error`); + localStorage.removeItem(`${prefix}auth_error_timestamp`); + localStorage.removeItem(`${prefix}auth_error_type`); + } catch (err) { + logger.warn('Failed to clear stored auth error', { serverId, err }); + } + } + + /** + * Attempt to connect to a server with retry logic + */ + private async connectWithRetry(server: RegistryServer, config?: RetryConfig, interactive: boolean = true): Promise { + const retryConfig = config || this.getRetryConfig(); + let lastError: unknown; + let retryAttempts = 0; + + for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) { + try { + await this.client.connect({ ...server, interactive }); + this.lastConnected = new Date(); + + + // Clear any stored authentication errors on successful connection + this.clearStoredAuthErrorForServer(server.id); + + // Track successful connection + this.trackConnectionEvent({ + serverId: server.id, + eventType: 'connected', + details: `Connected on attempt ${attempt + 1}`, + retryAttempt: attempt, + }); + + // Automatically refresh tools after successful connection + try { + await this.refreshToolsForServer(server.id); + } catch (refreshError) { + logger.warn('MCPRegistry: Auto-refresh failed after connection', { + serverId: server.id, + error: refreshError + }); + } + + return { + serverId: server.id, + name: server.name, + endpoint: server.endpoint, + connected: true, + retryAttempts, + }; + } catch (error) { + lastError = error; + retryAttempts = attempt; + const errorType = this.categorizeError(error); + + // Enhanced logging with error details + const logContext: any = { + serverId: server.id, + endpoint: server.endpoint, + authType: server.authType, + attempt: attempt + 1, + errorType + }; + if (error instanceof Error && 'context' in error) { + const context = (error as any).context; + logContext.errorContext = context; + } + + logger.warn('MCP connect attempt failed', { ...logContext, error }); + + // Track authentication errors specifically + if (errorType === 'authentication') { + this.trackConnectionEvent({ + serverId: server.id, + eventType: 'auth_error', + details: error instanceof Error ? error.message : String(error), + retryAttempt: attempt, + }); + } else { + // Track retry attempts for other errors + this.trackConnectionEvent({ + serverId: server.id, + eventType: 'retry_attempt', + details: `${errorType}: ${error instanceof Error ? error.message : String(error)}`, + retryAttempt: attempt, + }); + } + + // Don't retry for authentication or configuration errors + if (!this.shouldRetryError(errorType)) { + logger.info('MCP connection error not retryable', { + serverId: server.id, + errorType, + finalAttempt: attempt + 1 + }); + break; + } + + // Don't sleep after the last attempt + if (attempt < retryConfig.maxRetries) { + const delay = this.calculateRetryDelay(attempt, retryConfig); + logger.info('MCP connection retry scheduled', { + serverId: server.id, + attempt: attempt + 1, + nextAttemptIn: `${delay}ms` + }); + await this.sleep(delay); + } + } + } + + // All attempts failed + this.setError(lastError); + + logger.error('MCP connect failed after all retries', { + serverId: server.id, + endpoint: server.endpoint, + totalAttempts: retryAttempts + 1, + error: lastError + }); + + // Track final connection failure + this.trackConnectionEvent({ + serverId: server.id, + eventType: 'connection_failed', + details: `Failed after ${retryAttempts + 1} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`, + retryAttempt: retryAttempts, + }); + + return { + serverId: server.id, + name: server.name, + endpoint: server.endpoint, + connected: false, + error: lastError instanceof Error ? lastError : new Error(String(lastError)), + errorType: this.categorizeError(lastError), + retryAttempts, + }; + } + + async init(interactive: boolean = false): Promise { const cfg = getMCPConfig(); this.registeredTools = []; this.lastError = undefined; this.lastErrorType = undefined; - // Reset mappings on reconnect ToolNameMap.clear(); + // Load connection events from previous sessions + this.loadConnectionEvents(); + if (!cfg.enabled) { - logger.info('MCP disabled'); - return; + return []; } - if (!cfg.endpoint) { - logger.warn('MCP endpoint not configured'); - return; + + const providers = cfg.providers.filter(provider => provider.enabled); + if (providers.length === 0) { + logger.warn('No MCP providers configured'); + return []; } - const server: MCPServer = { - id: 'default', - endpoint: cfg.endpoint, - token: cfg.token, - }; - this.servers = [server]; + const configuredIds = new Set(providers.map(provider => provider.id)); + for (const existing of this.servers) { + if (!configuredIds.has(existing.id)) { + try { + this.client.disconnect(existing.id); + } catch (error) { + logger.warn('Failed to disconnect MCP server', { serverId: existing.id, error }); + } + } + } - try { - await this.client.connect(server); - this.lastConnected = new Date(); - logger.info('MCP connected', { endpoint: server.endpoint }); - } catch (err) { - this.setError(err); - logger.error('MCP connect failed', err); + this.servers = providers.map(provider => ({ + id: provider.id, + name: provider.name, + endpoint: provider.endpoint, + authType: provider.authType, + token: provider.authType === 'bearer' ? provider.token : undefined, + oauth: provider.authType === 'oauth' ? { + clientId: provider.oauthClientId, + scope: provider.oauthScope, + redirectUri: provider.oauthRedirectUrl, + } : undefined, + })); + + // In non-interactive mode, let the OAuth provider handle token checks internally + // This allows auto-connection with refresh tokens while avoiding popups for new auth + + const results: ConnectionResult[] = []; + + for (const server of this.servers) { + const result = await this.connectWithRetry(server, undefined, interactive); + results.push(result); } + + return results; } + async refresh(): Promise { const cfg = getMCPConfig(); + if (!cfg.enabled || this.servers.length === 0) { return; } - - // Clear previously registered tools (ToolRegistry will overwrite on re-registration) + this.registeredTools = []; - const allow = new Set(cfg.toolAllowlist || []); + // Track tool names across all servers for conflict detection + const toolNameRegistry = new Map(); + const allServerTools: Array<{ srv: RegistryServer; def: MCPToolDef }> = []; + + // First pass: collect all tools from all servers for (const srv of this.servers) { - if (!this.client.isConnected(srv.id)) { + const isConnected = this.client.isConnected(srv.id); + + if (!isConnected) { continue; } + let tools: MCPToolDef[] = []; try { tools = await this.client.listTools(srv.id); - } catch (err) { - this.setError(err); - logger.error('listTools failed', err); + } catch (error) { + this.setError(error); + logger.error('MCPRegistry: listTools failed', { serverId: srv.id, error }); continue; } for (const def of tools) { - const namespaced = `mcp:${srv.id}:${def.name}`; - // Create or reuse a stable sanitized mapping for LLM function names - ToolNameMap.addMapping(namespaced); - if (allow.size > 0 && !allow.has(namespaced) && !allow.has(def.name)) { - continue; - } - try { - const factoryName = namespaced; - ToolRegistry.registerToolFactory(factoryName, () => new MCPToolAdapter(srv.id, this.client, def, namespaced)); - this.registeredTools.push(factoryName); - } catch (err) { - logger.error('Failed to register MCP tool', { tool: def.name, err }); + allServerTools.push({ srv, def }); + + // Track tool name occurrences + if (toolNameRegistry.has(def.name)) { + const existing = toolNameRegistry.get(def.name)!; + existing.count++; + } else { + toolNameRegistry.set(def.name, { serverId: srv.id, originalName: def.name, count: 1 }); } } } + + // Second pass: register tools with smart naming + const usedNames = new Map(); // Track occurrence count per base name (starts at 0) + + for (const { srv, def } of allServerTools) { + // Generate smart tool name + const baseName = def.name; + + // Get current occurrence count (starts at 0) + const occurrenceCount = usedNames.get(baseName) || 0; + + // Increment occurrence count for this tool + const newCount = occurrenceCount + 1; + usedNames.set(baseName, newCount); + + // Generate tool name with suffix only for 2nd+ occurrences + let toolName = baseName; + if (newCount > 1) { + toolName = `${baseName}_${newCount}`; + } + + // Create namespaced name for internal tracking but use smart name for registration + const namespacedName = `mcp:${srv.id}:${def.name}`; + ToolNameMap.addMapping(namespacedName); + ToolNameMap.addMapping(toolName); // Also map the smart name + + // Check allowlist using both names + if (allow.size > 0 && !allow.has(namespacedName) && !allow.has(def.name) && !allow.has(toolName)) { + continue; + } + + try { + const factoryName = toolName; // Use smart name as factory name + ToolRegistry.registerToolFactory(factoryName, () => new MCPToolAdapter(srv.id, this.client, def, namespacedName)); + this.registeredTools.push(factoryName); + } catch (error) { + logger.error('MCPRegistry: Failed to register MCP tool', { tool: def.name, smartName: toolName, error }); + } + } + + if (allServerTools.length > 0 && this.registeredTools.length === 0) { + logger.warn('MCPRegistry: Found tools but none were registered - check allowlist configuration', { + foundTools: allServerTools.map(t => t.def.name), + allowlist: Array.from(allow) + }); + } + } + + async reconnect(serverId: string): Promise { + const server = this.servers.find(srv => srv.id === serverId); + if (!server) { + throw new Error(`Unknown MCP server: ${serverId}`); + } + + try { + this.client.disconnect(serverId); + } catch (error) { + logger.debug('Error disconnecting MCP server before reconnect', { serverId, error }); + } + + try { + await this.client.connect({ ...server, interactive: true }); + this.lastConnected = new Date(); + this.lastError = undefined; + this.lastErrorType = undefined; + + // Clear stored authentication errors on successful reconnection + this.clearStoredAuthErrorForServer(serverId); + + await this.refresh(); + } catch (error) { + this.setError(error); + logger.error('Failed to reconnect MCP server', { serverId, error }); + throw error; + } } dispose(): void { for (const srv of this.servers) { - try { this.client.disconnect(srv.id); } catch {} + try { + this.client.disconnect(srv.id); + // Track disconnection + this.trackConnectionEvent({ + serverId: srv.id, + eventType: 'disconnected', + details: 'Manual disconnect', + }); + } catch { + // ignore errors during cleanup + } } this.lastDisconnected = new Date(); this.servers = []; } + async ensureToolsRegistered(): Promise { + // Auto-refresh if no tools are registered but servers are configured + if (this.registeredTools.length === 0 && this.servers.length > 0) { + try { + await this.refresh(); + } catch (error) { + logger.error('MCPRegistry: Auto-refresh failed', { error }); + } + } + } + getStatus(): MCPRegistryStatus { return { enabled: getMCPConfig().enabled, servers: this.servers.map(s => ({ id: s.id, + name: s.name, endpoint: s.endpoint, + authType: s.authType, connected: this.client.isConnected(s.id), - toolCount: 0, + toolCount: (() => { + // Count tools for this server by checking if each registered tool belongs to this server + let count = 0; + for (const toolName of this.registeredTools) { + try { + const tool = ToolRegistry.getRegisteredTool(toolName); + if (tool && tool instanceof MCPToolAdapter && tool.getServerId() === s.id) { + count++; + } + } catch (error) { + // Ignore tool registry errors + } + } + return count; + })(), })), registeredToolNames: [...this.registeredTools], lastError: this.lastError, lastErrorType: this.lastErrorType, lastConnected: this.lastConnected, lastDisconnected: this.lastDisconnected, + connectionEvents: this.getConnectionEvents(), }; } - getSanitizedFunctionName(original: string): string { return ToolNameMap.getSanitized(original); } + getSanitizedFunctionName(original: string): string { + return ToolNameMap.getSanitized(original); + } - resolveOriginalFunctionName(sanitized: string): string | undefined { return ToolNameMap.resolveOriginal(sanitized); } + resolveOriginalFunctionName(sanitized: string): string | undefined { + return ToolNameMap.resolveOriginal(sanitized); + } } export const MCPRegistry = new RegistryImpl(); diff --git a/front_end/panels/ai_chat/mcp/MCPToolAdapter.ts b/front_end/panels/ai_chat/mcp/MCPToolAdapter.ts index 5c83debee00..c14bb93c048 100644 --- a/front_end/panels/ai_chat/mcp/MCPToolAdapter.ts +++ b/front_end/panels/ai_chat/mcp/MCPToolAdapter.ts @@ -1,6 +1,6 @@ import type { Tool } from '../tools/Tools.js'; import { createLogger } from '../core/Logger.js'; -import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk-v2.js'; const logger = createLogger('MCPToolAdapter'); diff --git a/front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts b/front_end/panels/ai_chat/mcp/__tests__/MCPClientSDK.test.ts similarity index 93% rename from front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts rename to front_end/panels/ai_chat/mcp/__tests__/MCPClientSDK.test.ts index 7298942d783..eb7efb0e459 100644 --- a/front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts +++ b/front_end/panels/ai_chat/mcp/__tests__/MCPClientSDK.test.ts @@ -2,14 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { MCPClientSDK } from '../../../third_party/mcp-sdk/mcp-sdk.js'; -import type { MCPServer } from '../../../third_party/mcp-sdk/mcp-sdk.js'; +import { MCPClientSDKv2 } from '../../../../third_party/mcp-sdk/mcp-sdk-v2.js'; +import type { MCPServer } from '../../../../third_party/mcp-sdk/mcp-sdk-v2.js'; describe('MCPClientSDK', () => { - let client: MCPClientSDK; + let client: MCPClientSDKv2; beforeEach(() => { - client = new MCPClientSDK(); + client = new MCPClientSDKv2(); }); afterEach(() => { @@ -22,7 +22,7 @@ describe('MCPClientSDK', () => { }); it('can be instantiated', () => { - assert.ok(client instanceof MCPClientSDK); + assert.ok(client instanceof MCPClientSDKv2); }); it('reports not connected initially', () => { @@ -55,7 +55,7 @@ describe('MCPClientSDK', () => { const HACKER_NEWS_SERVER: MCPServer = { id: 'local-hn-sdk', - endpoint: 'http://localhost:5001/sse', + endpoint: 'http://localhost:5001/mcp', }; it('connects and lists tools', async function() { @@ -133,4 +133,4 @@ describe('MCPClientSDK', () => { } }); }); -}); \ No newline at end of file +}); diff --git a/front_end/panels/ai_chat/mcp/__tests__/MCPConfig.test.ts b/front_end/panels/ai_chat/mcp/__tests__/MCPConfig.test.ts new file mode 100644 index 00000000000..a5f223a74b6 --- /dev/null +++ b/front_end/panels/ai_chat/mcp/__tests__/MCPConfig.test.ts @@ -0,0 +1,173 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { generateMCPProviderId, saveMCPProviders, cleanupOAuthData, getMCPProviders, type MCPProviderConfig } from '../MCPConfig.js'; + + +class MemoryStorage { + private readonly store = new Map(); + + getItem(key: string): string | null { + return this.store.has(key) ? this.store.get(key)! : null; + } + + setItem(key: string, value: string): void { + this.store.set(key, value); + } + + removeItem(key: string): void { + this.store.delete(key); + } + + clear(): void { + this.store.clear(); + } +} + +describe('MCPConfig ID generation', () => { + let localStorageStub: MemoryStorage; + let sessionStorageStub: MemoryStorage; + + beforeEach(() => { + localStorageStub = new MemoryStorage(); + sessionStorageStub = new MemoryStorage(); + + Object.defineProperty(window, 'localStorage', { + value: localStorageStub, + configurable: true, + }); + Object.defineProperty(window, 'sessionStorage', { + value: sessionStorageStub, + configurable: true, + }); + }); + + afterEach(() => { + localStorageStub.clear(); + sessionStorageStub.clear(); + }); + + describe('generateMCPProviderId', () => { + it('prefers explicit IDs and sanitizes them', () => { + const id = generateMCPProviderId({ id: ' Custom-ID ' }); + assert.strictEqual(id, 'mcp-custom-id'); + }); + + it('derives IDs from provider names', () => { + const id = generateMCPProviderId({ name: ' OpenRouter ++ ' }); + assert.strictEqual(id, 'mcp-openrouter'); + }); + + it('derives IDs from subdomain hosts by using the domain base', () => { + const id = generateMCPProviderId({ endpoint: 'https://mcp.notion.com/mcp' }); + assert.strictEqual(id, 'mcp-notion'); + }); + + it('handles common country-code second-level domains', () => { + const id = generateMCPProviderId({ endpoint: 'https://api.tools.co.uk/service' }); + assert.strictEqual(id, 'mcp-api'); + }); + + it('falls back to sanitized endpoint when URL parsing fails', () => { + const id = generateMCPProviderId({ endpoint: 'invalid host value' }); + assert.strictEqual(id.startsWith('mcp-'), true); + }); + }); + + describe('saveMCPProviders duplicate detection', () => { + it('throws when two providers resolve to the same derived ID', () => { + const providers: MCPProviderConfig[] = [ + { + id: '', + name: 'Notion', + endpoint: 'https://mcp.notion.com/api', + authType: 'oauth', + enabled: true, + }, + { + id: '', + name: undefined, + endpoint: 'https://another.notion.com/v1', + authType: 'oauth', + enabled: true, + }, + ]; + + assert.throws(() => saveMCPProviders(providers), /Duplicate MCP connection identifier: mcp-notion/); + }); + }); + + describe('OAuth cleanup', () => { + it('cleanupOAuthData removes all OAuth-related localStorage keys', () => { + const serverId = 'mcp-test'; + const prefix = `mcp_oauth:${serverId}:`; + + // Set up some OAuth data in localStorage + localStorageStub.setItem(`${prefix}tokens`, '{"access_token":"test"}'); + localStorageStub.setItem(`${prefix}client_info`, '{"client_id":"test"}'); + localStorageStub.setItem(`${prefix}last_auth_error`, 'test error'); + localStorageStub.setItem(`${prefix}auth_error_timestamp`, '123456789'); + + // Verify data exists + assert.strictEqual(localStorageStub.getItem(`${prefix}tokens`), '{"access_token":"test"}'); + assert.strictEqual(localStorageStub.getItem(`${prefix}client_info`), '{"client_id":"test"}'); + + // Clean up OAuth data + cleanupOAuthData(serverId); + + // Verify all OAuth data is removed + assert.strictEqual(localStorageStub.getItem(`${prefix}tokens`), null); + assert.strictEqual(localStorageStub.getItem(`${prefix}client_info`), null); + assert.strictEqual(localStorageStub.getItem(`${prefix}last_auth_error`), null); + assert.strictEqual(localStorageStub.getItem(`${prefix}auth_error_timestamp`), null); + }); + + it('saveMCPProviders cleans up OAuth data for removed providers', () => { + // Set up existing providers + const existingProviders: MCPProviderConfig[] = [ + { + id: 'mcp-provider1', + endpoint: 'https://api1.example.com', + authType: 'oauth', + enabled: true, + }, + { + id: 'mcp-provider2', + endpoint: 'https://api2.example.com', + authType: 'oauth', + enabled: true, + }, + ]; + + // Save existing providers to set up state + saveMCPProviders(existingProviders); + + // Add OAuth data for both providers + localStorageStub.setItem('mcp_oauth:mcp-provider1:tokens', '{"access_token":"token1"}'); + localStorageStub.setItem('mcp_oauth:mcp-provider2:tokens', '{"access_token":"token2"}'); + + // Verify OAuth data exists + assert.strictEqual(localStorageStub.getItem('mcp_oauth:mcp-provider1:tokens'), '{"access_token":"token1"}'); + assert.strictEqual(localStorageStub.getItem('mcp_oauth:mcp-provider2:tokens'), '{"access_token":"token2"}'); + + // Save providers with only provider1 (remove provider2) + const newProviders: MCPProviderConfig[] = [ + { + id: 'mcp-provider1', + endpoint: 'https://api1.example.com', + authType: 'oauth', + enabled: true, + }, + ]; + + saveMCPProviders(newProviders); + + // Verify provider1's OAuth data still exists + assert.strictEqual(localStorageStub.getItem('mcp_oauth:mcp-provider1:tokens'), '{"access_token":"token1"}'); + + // Verify provider2's OAuth data was cleaned up + assert.strictEqual(localStorageStub.getItem('mcp_oauth:mcp-provider2:tokens'), null); + }); + }); +}); diff --git a/front_end/panels/ai_chat/mcp/__tests__/MCPIntegration.test.ts b/front_end/panels/ai_chat/mcp/__tests__/MCPIntegration.test.ts new file mode 100644 index 00000000000..a1dbdd45cd7 --- /dev/null +++ b/front_end/panels/ai_chat/mcp/__tests__/MCPIntegration.test.ts @@ -0,0 +1,321 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { MCPRegistry } from '../MCPRegistry.js'; +import { createToolExecutorNode } from '../../core/AgentNodes.js'; +import * as ToolNameMap from '../../core/ToolNameMap.js'; +import { ToolRegistry } from '../../agent_framework/ConfigurableAgentTool.js'; +import type { AgentState } from '../../core/State.js'; +import { ChatMessageEntity } from '../../models/ChatTypes.js'; +import { createLogger } from '../../core/Logger.js'; + +const logger = createLogger('MCPIntegrationTest'); + +/* eslint-env mocha */ + +// Mock MCP client and components +const mockMCPClient = { + isConnected: (serverId: string) => true, + listTools: (serverId: string) => Promise.resolve([ + { + name: 'search', + description: 'Search for content', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string' } + } + } + }, + { + name: 'notion_find_page_by_title', + description: 'Find Notion page by title', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string' } + } + } + } + ]), + call: () => Promise.resolve({ success: true }), + connect: () => Promise.resolve(), + disconnect: () => {}, +}; + +// Mock MCPConfig +const mockMCPConfig = { + enabled: true, + providers: [ + { + id: 'mcp-lusl1248if', + name: 'Test MCP Server', + endpoint: 'test://server', + enabled: true, + authType: 'bearer' as const, + } + ], + toolAllowlist: [], + autostart: true, +}; + +describe('MCP Integration Test', () => { + beforeEach(() => { + ToolNameMap.clear(); + // Mock the config + (globalThis as any).getMCPConfig = () => mockMCPConfig; + }); + + afterEach(() => { + ToolNameMap.clear(); + MCPRegistry.dispose(); + }); + + describe('Full MCP tool lifecycle', () => { + it('should register MCP tools and resolve them correctly', async () => { + console.log('Integration Test - Full MCP Lifecycle'); + + // Step 1: Initialize MCP Registry with mock client + console.log('Step 1: Initialize MCP Registry'); + + // Replace the internal client with our mock + (MCPRegistry as any).client = mockMCPClient; + (MCPRegistry as any).servers = mockMCPConfig.providers.map(p => ({ + id: p.id, + name: p.name, + endpoint: p.endpoint, + authType: p.authType, + })); + + // Step 2: Refresh to register tools + logger.info('Step 2: Refresh registry to register tools'); + await MCPRegistry.refresh(); + + const status = MCPRegistry.getStatus(); + logger.debug('Registry status:', { + enabled: status.enabled, + serverCount: status.servers.length, + toolCount: status.registeredToolNames.length, + tools: status.registeredToolNames, + }); + + // Verify tools were registered with smart names + assert.isTrue(status.registeredToolNames.length > 0, 'Should register some tools'); + + // Should have smart names, not long namespaced names + const hasShortNames = status.registeredToolNames.some(name => !name.includes('mcp:')); + logger.debug('Has short names:', hasShortNames); + logger.debug('Registered tools:', status.registeredToolNames); + + // Step 3: Simulate tool execution request + logger.info('Step 3: Test tool resolution and execution'); + + // Try both namespaced and smart names + const testCases = [ + { + name: 'Smart name request', + toolName: 'search', // Simple smart name + }, + { + name: 'Namespaced name request', + toolName: 'mcp:mcp-lusl1248if:search', // Full namespaced name + }, + { + name: 'Sanitized name request', + toolName: ToolNameMap.getSanitized('mcp:mcp-lusl1248if:search'), // Sanitized version + } + ]; + + for (const testCase of testCases) { + logger.info(`Testing: ${testCase.name}`); + logger.debug(`Requesting tool: ${testCase.toolName}`); + + const state: AgentState = { + messages: [ + { + entity: ChatMessageEntity.USER, + text: 'Test message', + }, + { + entity: ChatMessageEntity.MODEL, + action: 'tool', + toolName: testCase.toolName, + toolArgs: { query: 'test' }, + toolCallId: `test-${Date.now()}`, + isFinalAnswer: false, + } + ], + selectedAgentType: 'test', + context: { + selectedToolNames: status.registeredToolNames, // Use all registered tools + } as any, + }; + + try { + const toolExecutor = createToolExecutorNode(state, 'openai', 'gpt-4'); + const result = await toolExecutor.invoke(state); + + console.log(` โœ“ SUCCESS: ${testCase.name} worked`); + console.log(` Result messages: ${result.messages.length}`); + + // Verify tool result was added + const lastMessage = result.messages[result.messages.length - 1]; + assert.strictEqual(lastMessage.entity, ChatMessageEntity.TOOL_RESULT); + + } catch (error) { + console.log(` โœ— FAILED: ${testCase.name} failed with: ${error.message}`); + + // Don't fail the test, just log the issue + console.log(` This reveals the resolution issue for: ${testCase.toolName}`); + } + } + }); + + it('should demonstrate the exact error from logs', async () => { + console.log('\nDemonstration of Exact Error from Logs'); + + // Setup registry + (MCPRegistry as any).client = mockMCPClient; + (MCPRegistry as any).servers = [{ + id: 'mcp-lusl1248if', + name: 'Test Server', + endpoint: 'test://server', + authType: 'bearer', + }]; + + await MCPRegistry.refresh(); + const status = MCPRegistry.getStatus(); + + console.log(' Available tools:', status.registeredToolNames); + + // The exact error: Tool mcp:mcp-lusl1248if:search not found + const problematicToolName = 'mcp:mcp-lusl1248if:search'; + console.log(' Problematic request:', problematicToolName); + + // Check what we actually have + const hasExactMatch = status.registeredToolNames.includes(problematicToolName); + const hasSmartName = status.registeredToolNames.includes('search'); + + console.log(' Has exact match:', hasExactMatch); + console.log(' Has smart name:', hasSmartName); + + // Check ToolNameMap + const sanitized = ToolNameMap.getSanitized(problematicToolName); + const resolved = ToolNameMap.resolveOriginal(sanitized); + + console.log(' Sanitized version:', sanitized); + console.log(' Resolves back to:', resolved); + + // Check ToolRegistry + const toolByExact = ToolRegistry.getRegisteredTool(problematicToolName as any); + const toolBySmartName = ToolRegistry.getRegisteredTool('search' as any); + const toolBySanitized = ToolRegistry.getRegisteredTool(sanitized as any); + + console.log(' ToolRegistry results:'); + console.log(' By exact name:', !!toolByExact); + console.log(' By smart name:', !!toolBySmartName); + console.log(' By sanitized:', !!toolBySanitized); + + // This should reveal exactly what the mismatch is + if (!toolByExact && toolBySmartName) { + console.log(' ROOT CAUSE: Tool registered with smart name but requested with namespaced name'); + console.log(' SOLUTION: Need proper name mapping in message flow'); + } + + // Try to resolve with our enhanced logic + const state: AgentState = { + messages: [ + { + entity: ChatMessageEntity.MODEL, + action: 'tool', + toolName: problematicToolName, + toolArgs: {}, + toolCallId: 'test-exact-error', + isFinalAnswer: false, + } + ], + selectedAgentType: 'test', + context: { + selectedToolNames: status.registeredToolNames, + } as any, + }; + + try { + const toolExecutor = createToolExecutorNode(state, 'openai', 'gpt-4'); + const result = await toolExecutor.invoke(state); + + console.log(' FIXED: Our enhanced resolution worked!'); + assert.isTrue(result.messages.length > 0); + + } catch (error) { + console.log(' STILL FAILING:', error.message); + console.log(' Need to debug the enhanced resolution logic'); + + // Don't fail the test, this is diagnostic + console.log(' Available tools in toolMap should be logged above'); + } + }); + }); + + describe('Smart naming validation', () => { + it('should validate smart naming produces clean tool names', () => { + console.log('\nSmart Naming Validation'); + + const testTools = [ + { server: 'mcp-short123', tool: 'search', expected: 'search' }, + { server: 'mcp-short123', tool: 'notion_create', expected: 'notion_create' }, + { server: 'mcp-other456', tool: 'search', expected: 'search_2' }, // Conflict + ]; + + console.log(' Testing smart naming logic:'); + + // Simulate the smart naming process + const toolNameRegistry = new Map(); + const results: string[] = []; + + // First pass: count occurrences + for (const test of testTools) { + if (toolNameRegistry.has(test.tool)) { + const existing = toolNameRegistry.get(test.tool)!; + existing.count++; + } else { + toolNameRegistry.set(test.tool, { + serverId: test.server, + originalName: test.tool, + count: 1 + }); + } + } + + // Second pass: assign names + const usedNames = new Map(); + + for (const test of testTools) { + let toolName = test.tool; + const toolInfo = toolNameRegistry.get(test.tool)!; + + if (toolInfo.count > 1) { + const baseName = test.tool; + const currentCount = usedNames.get(baseName) || 1; + + if (toolInfo.serverId === test.server && currentCount === 1) { + toolName = baseName; + } else { + const suffix = currentCount + 1; + toolName = `${baseName}_${suffix}`; + } + + usedNames.set(baseName, currentCount + 1); + } + + results.push(toolName); + + console.log(` ${test.server}:${test.tool} -> ${toolName} (expected: ${test.expected})`); + assert.strictEqual(toolName, test.expected, `Smart naming failed for ${test.server}:${test.tool}`); + } + + console.log(' โœ“ Smart naming validation passed'); + }); + }); +}); \ No newline at end of file diff --git a/front_end/panels/ai_chat/mcp/__tests__/MCPToolRegistration.test.ts b/front_end/panels/ai_chat/mcp/__tests__/MCPToolRegistration.test.ts new file mode 100644 index 00000000000..1aa36804c6b --- /dev/null +++ b/front_end/panels/ai_chat/mcp/__tests__/MCPToolRegistration.test.ts @@ -0,0 +1,195 @@ +// Copyright 2025 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import { ToolRegistry } from '../../agent_framework/ConfigurableAgentTool.js'; +import * as ToolNameMap from '../../core/ToolNameMap.js'; +import { MCPToolAdapter } from '../MCPToolAdapter.js'; + +/* eslint-env mocha */ + +// Mock the MCP client and tool definition +const mockMCPClient = { + call: () => Promise.resolve({}), + isConnected: () => true, + listTools: () => Promise.resolve([]), + disconnect: () => {}, +}; + +const createMockToolDef = (name: string) => ({ + name, + description: `Test tool ${name}`, + inputSchema: { + type: 'object', + properties: {}, + }, +}); + +describe('MCP Tool Registration', () => { + beforeEach(() => { + ToolNameMap.clear(); + // Clear any existing MCP tools from registry + // Note: ToolRegistry doesn't have an unregister method, + // so we'll just track what we register in tests + }); + + afterEach(() => { + ToolNameMap.clear(); + }); + + describe('Smart naming for MCP tools', () => { + it('should register MCP tools with smart names (no conflicts)', () => { + const serverId = 'mcp-test123'; + const toolDef = createMockToolDef('search'); + const namespacedName = `mcp:${serverId}:${toolDef.name}`; + + // Simulate what MCPRegistry does with smart naming + const smartName = toolDef.name; // No conflicts, so use simple name + + console.log('Test 1 - Smart naming without conflicts:'); + console.log(' Server ID:', serverId); + console.log(' Tool name:', toolDef.name); + console.log(' Namespaced name:', namespacedName); + console.log(' Smart name:', smartName); + + // Add mappings like MCPRegistry would + ToolNameMap.addMapping(namespacedName); + ToolNameMap.addMapping(smartName); + + // Register tool with smart name + ToolRegistry.registerToolFactory(smartName, () => new MCPToolAdapter(serverId, mockMCPClient as any, toolDef, namespacedName)); + + // Verify tool is registered with smart name + const registeredTool = ToolRegistry.getRegisteredTool(smartName as any); + console.log(' Tool registered:', !!registeredTool); + assert.isNotNull(registeredTool, 'Tool should be registered with smart name'); + + // Verify name mappings work + const sanitizedNamespaced = ToolNameMap.getSanitized(namespacedName); + const resolvedFromSanitized = ToolNameMap.resolveOriginal(sanitizedNamespaced); + + console.log(' Namespaced sanitized:', sanitizedNamespaced); + console.log(' Resolved from sanitized:', resolvedFromSanitized); + + assert.strictEqual(resolvedFromSanitized, namespacedName); + }); + + it('should handle conflicts with numeric suffixes', () => { + const server1Id = 'mcp-server1'; + const server2Id = 'mcp-server2'; + const toolName = 'search'; + + const namespacedName1 = `mcp:${server1Id}:${toolName}`; + const namespacedName2 = `mcp:${server2Id}:${toolName}`; + + // Simulate conflict resolution + const smartName1 = toolName; // First occurrence gets simple name + const smartName2 = `${toolName}_2`; // Second occurrence gets suffix + + console.log('Test 2 - Smart naming with conflicts:'); + console.log(' Server 1:', server1Id, 'Tool:', toolName, 'Smart name:', smartName1); + console.log(' Server 2:', server2Id, 'Tool:', toolName, 'Smart name:', smartName2); + + // Add mappings + ToolNameMap.addMapping(namespacedName1); + ToolNameMap.addMapping(smartName1); + ToolNameMap.addMapping(namespacedName2); + ToolNameMap.addMapping(smartName2); + + // Register both tools + const toolDef = createMockToolDef(toolName); + ToolRegistry.registerToolFactory(smartName1, () => new MCPToolAdapter(server1Id, mockMCPClient as any, toolDef, namespacedName1)); + ToolRegistry.registerToolFactory(smartName2, () => new MCPToolAdapter(server2Id, mockMCPClient as any, toolDef, namespacedName2)); + + // Verify both tools are registered with different names + const tool1 = ToolRegistry.getRegisteredTool(smartName1 as any); + const tool2 = ToolRegistry.getRegisteredTool(smartName2 as any); + + console.log(' Tool1 registered:', !!tool1); + console.log(' Tool2 registered:', !!tool2); + + assert.isNotNull(tool1, 'First tool should be registered'); + assert.isNotNull(tool2, 'Second tool should be registered'); + }); + + it('should handle the exact error case from logs', () => { + const serverId = 'mcp-lusl1248if'; + const toolName = 'search'; + const namespacedName = `mcp:${serverId}:${toolName}`; + const smartName = toolName; + + console.log('Test 3 - Exact error case:'); + console.log(' Requested in error:', namespacedName); + console.log(' Server ID:', serverId); + console.log(' Tool name:', toolName); + console.log(' Expected smart name:', smartName); + + // Add mappings + ToolNameMap.addMapping(namespacedName); + ToolNameMap.addMapping(smartName); + + // Register with smart name (like our new logic should do) + const toolDef = createMockToolDef(toolName); + ToolRegistry.registerToolFactory(smartName, () => new MCPToolAdapter(serverId, mockMCPClient as any, toolDef, namespacedName)); + + // Now test resolution - what happens when we try to find the tool? + console.log(' --- Resolution test ---'); + + // Try to find by namespaced name (what the error shows) + const toolByNamespaced = ToolRegistry.getRegisteredTool(namespacedName as any); + console.log(' Find by namespaced name:', !!toolByNamespaced); + + // Try to find by smart name (what should work) + const toolBySmart = ToolRegistry.getRegisteredTool(smartName as any); + console.log(' Find by smart name:', !!toolBySmart); + + // Try sanitized version + const sanitized = ToolNameMap.getSanitized(namespacedName); + const toolBySanitized = ToolRegistry.getRegisteredTool(sanitized as any); + console.log(' Sanitized version:', sanitized); + console.log(' Find by sanitized:', !!toolBySanitized); + + // The issue: tool is registered with smart name but being requested by namespaced name + assert.isNull(toolByNamespaced, 'Tool should NOT be found by namespaced name'); + assert.isNotNull(toolBySmart, 'Tool SHOULD be found by smart name'); + + console.log(' ISSUE IDENTIFIED: Tool registered as "' + smartName + '" but requested as "' + namespacedName + '"'); + }); + }); + + describe('Name mapping verification', () => { + it('should verify ToolNameMap handles MCP names correctly', () => { + const testCases = [ + { + original: 'mcp:mcp-lusl1248if:search', + expectedSanitized: 'mcp_mcp-lusl1248if_search' + }, + { + original: 'mcp:80a92942-4bf9-4c02-ab11-422151bec3a2:notion_find_page_by_title', + expectedSanitized: 'mcp_80a92942-4bf9-4c02-ab11-422151bec3a2_notion_find_page_by_title' + }, + { + original: 'search', + expectedSanitized: 'search' + } + ]; + + console.log('Test 4 - Name mapping verification:'); + + testCases.forEach((testCase, index) => { + const sanitized = ToolNameMap.getSanitized(testCase.original); + const resolved = ToolNameMap.resolveOriginal(sanitized); + + console.log(` Case ${index + 1}:`); + console.log(` Original: ${testCase.original}`); + console.log(` Sanitized: ${sanitized}`); + console.log(` Expected: ${testCase.expectedSanitized}`); + console.log(` Resolved: ${resolved}`); + console.log(` Bidirectional: ${resolved === testCase.original}`); + + assert.strictEqual(sanitized, testCase.expectedSanitized, `Sanitization failed for ${testCase.original}`); + assert.strictEqual(resolved, testCase.original, `Resolution failed for ${testCase.original}`); + }); + }); + }); +}); \ No newline at end of file diff --git a/front_end/panels/ai_chat/tools/FetcherTool.ts b/front_end/panels/ai_chat/tools/FetcherTool.ts index 466c159bc17..2a85897d394 100644 --- a/front_end/panels/ai_chat/tools/FetcherTool.ts +++ b/front_end/panels/ai_chat/tools/FetcherTool.ts @@ -75,6 +75,13 @@ export class FetcherTool implements Tool { async execute(args: FetcherToolArgs, ctx?: LLMContext): Promise { logger.info('Executing with args', { args }); const { urls, reasoning } = args; + const signal = ctx?.abortSignal; + + const throwIfAborted = () => { + if (signal?.aborted) { + throw new DOMException('The operation was aborted', 'AbortError'); + } + }; // Validate input if (!Array.isArray(urls) || urls.length === 0) { @@ -91,6 +98,7 @@ export class FetcherTool implements Tool { // Process each URL sequentially for (const url of urlsToProcess) { + throwIfAborted(); try { logger.info('Processing URL', { url }); const fetchedContent = await this.fetchContentFromUrl(url, reasoning, ctx); @@ -117,9 +125,39 @@ export class FetcherTool implements Tool { * Fetch and extract content from a single URL */ private async fetchContentFromUrl(url: string, reasoning: string, ctx?: LLMContext): Promise { + const signal = ctx?.abortSignal; + const throwIfAborted = () => { + if (signal?.aborted) { + throw new DOMException('The operation was aborted', 'AbortError'); + } + }; + const sleep = (ms: number) => new Promise((resolve, reject) => { + if (!ms) return resolve(); + const timer = setTimeout(() => { + cleanup(); + resolve(); + }, ms); + const onAbort = () => { + clearTimeout(timer); + cleanup(); + reject(new DOMException('The operation was aborted', 'AbortError')); + }; + const cleanup = () => { + signal?.removeEventListener('abort', onAbort); + }; + if (signal) { + if (signal.aborted) { + clearTimeout(timer); + cleanup(); + return reject(new DOMException('The operation was aborted', 'AbortError')); + } + signal.addEventListener('abort', onAbort, { once: true }); + } + }); try { // Step 1: Navigate to the URL logger.info('Navigating to URL', { url }); + throwIfAborted(); // Note: NavigateURLTool requires both url and reasoning parameters const navigationResult = await this.navigateURLTool.execute({ url, @@ -138,13 +176,15 @@ export class FetcherTool implements Tool { } // Wait for 1 second to ensure the page has time to load - await new Promise(resolve => setTimeout(resolve, 1000)); + await sleep(1000); + throwIfAborted(); // Get metadata from navigation result const metadata = navigationResult.metadata ? navigationResult.metadata : { url: '', title: '' }; // Step 2: Extract markdown content using HTMLToMarkdownTool logger.info('Extracting content from URL', { url }); + throwIfAborted(); const extractionResult = await this.htmlToMarkdownTool.execute({ instruction: 'Extract the main content focusing on article text, headings, and important information. Remove ads, navigation, and distracting elements.', reasoning diff --git a/front_end/panels/ai_chat/tools/SchemaBasedExtractorTool.ts b/front_end/panels/ai_chat/tools/SchemaBasedExtractorTool.ts index bc372fdaaef..8008d64eeec 100644 --- a/front_end/panels/ai_chat/tools/SchemaBasedExtractorTool.ts +++ b/front_end/panels/ai_chat/tools/SchemaBasedExtractorTool.ts @@ -36,7 +36,7 @@ export interface SchemaExtractionResult { * Tool for extracting structured data from DOM based on schema definitions */ export class SchemaBasedExtractorTool implements Tool { - name = 'extract_schema_data'; + name = 'extract_data'; description = `Extracts structured data from a web page's DOM using a user-provided JSON schema and natural language instruction. - The schema defines the exact structure and types of data to extract (e.g., text, numbers, URLs). - For fields representing URLs, specify them in the schema as: { type: 'string', format: 'url' }. diff --git a/front_end/panels/ai_chat/tools/StreamlinedSchemaExtractorTool.ts b/front_end/panels/ai_chat/tools/StreamlinedSchemaExtractorTool.ts index 04a2e9603c6..0cac2d20873 100644 --- a/front_end/panels/ai_chat/tools/StreamlinedSchemaExtractorTool.ts +++ b/front_end/panels/ai_chat/tools/StreamlinedSchemaExtractorTool.ts @@ -43,6 +43,23 @@ export class StreamlinedSchemaExtractorTool implements Tool { + return new Promise((resolve, reject) => { + if (!ms) return resolve(); + const timer = setTimeout(() => { cleanup(); resolve(); }, ms); + const onAbort = () => { clearTimeout(timer); cleanup(); reject(new DOMException('The operation was aborted', 'AbortError')); }; + const cleanup = () => { signal?.removeEventListener('abort', onAbort); }; + if (signal) { + if (signal.aborted) { + clearTimeout(timer); + cleanup(); + return reject(new DOMException('The operation was aborted', 'AbortError')); + } + signal.addEventListener('abort', onAbort, { once: true }); + } + }); + } + schema = { type: 'object', properties: { @@ -163,9 +180,9 @@ export class StreamlinedSchemaExtractorTool implements Tool 1) { - await new Promise(resolve => setTimeout(resolve, this.RETRY_DELAY_MS)); + await this.sleep(this.RETRY_DELAY_MS, ctx?.abortSignal); } const retryResult = await this.retryUrlResolution( @@ -287,8 +304,8 @@ IMPORTANT: Only extract data that you can see in the accessibility tree above. D } catch (error) { if (attempt <= maxRetries) { logger.warn(`JSON parsing failed on attempt ${attempt}, retrying...`, error); - // Add delay before next attempt to prevent overloading the LLM - await new Promise(resolve => setTimeout(resolve, this.RETRY_DELAY_MS)); + // Add delay before next attempt to prevent overloading the LLM (abortable) + await this.sleep(this.RETRY_DELAY_MS, ctx?.abortSignal); } else { logger.error(`JSON extraction failed after ${attempt} attempts:`, error instanceof Error ? error.message : String(error)); throw new Error(`Data extraction failed after ${attempt} attempts: ${error instanceof Error ? error.message : String(error)}`); diff --git a/front_end/panels/ai_chat/tools/Tools.ts b/front_end/panels/ai_chat/tools/Tools.ts index bb5f2a7ff31..a6d12b68d41 100644 --- a/front_end/panels/ai_chat/tools/Tools.ts +++ b/front_end/panels/ai_chat/tools/Tools.ts @@ -58,6 +58,7 @@ export interface LLMContext { getVisionCapability?: (model: string) => Promise | boolean; miniModel?: string; nanoModel?: string; + abortSignal?: AbortSignal; } /** @@ -889,7 +890,7 @@ export class NavigateBackTool implements Tool<{ steps: number, reasoning: string required: ['steps', 'reasoning'], }; - async execute(args: { steps: number, reasoning: string }, _ctx?: LLMContext): Promise { + async execute(args: { steps: number, reasoning: string }, ctx?: LLMContext): Promise { logger.error('navigate_back', args); const steps = args.steps; if (typeof steps !== 'number' || steps <= 0) { @@ -939,10 +940,14 @@ export class NavigateBackTool implements Tool<{ steps: number, reasoning: string const timeoutMs = 5000; // 5 second timeout let isNavigationComplete = false; - // Poll until navigation completes or times out + const signal = ctx?.abortSignal; + // Poll until navigation completes, cancels, or times out while (!isNavigationComplete && (Date.now() - startTime) < timeoutMs) { + if (signal?.aborted) { + throw new DOMException('The operation was aborted', 'AbortError'); + } // Short delay between checks - await new Promise(resolve => setTimeout(resolve, 100)); + await abortableSleep(100, signal); // Check if navigation is complete by testing document readyState try { @@ -1424,6 +1429,30 @@ export class WaitTool implements Tool<{ seconds?: number, duration?: number, rea description = 'Waits for a specified number of seconds to allow page content to load, animations to complete, or dynamic content to appear. After waiting, returns a summary of what is currently visible in the viewport to help determine if additional waiting is needed. Provide the number of seconds to wait and an optional reasoning for waiting.'; async execute(args: { seconds?: number, duration?: number, reason?: string, reasoning?: string }, ctx?: LLMContext): Promise { + const signal = ctx?.abortSignal; + const sleep = (ms: number) => new Promise((resolve, reject) => { + if (!ms) return resolve(); + const timer = setTimeout(() => { + cleanup(); + resolve(); + }, ms); + const onAbort = () => { + clearTimeout(timer); + cleanup(); + reject(new DOMException('The operation was aborted', 'AbortError')); + }; + const cleanup = () => { + signal?.removeEventListener('abort', onAbort); + }; + if (signal) { + if (signal.aborted) { + clearTimeout(timer); + cleanup(); + return reject(new DOMException('The operation was aborted', 'AbortError')); + } + signal.addEventListener('abort', onAbort, { once: true }); + } + }); // Handle both 'seconds' and 'duration' parameter names for flexibility const waitTime = args.seconds ?? args.duration; const waitReason = args.reason ?? args.reasoning; @@ -1444,8 +1473,8 @@ export class WaitTool implements Tool<{ seconds?: number, duration?: number, rea // Log the wait reason if provided logger.info(`Waiting for ${waitTime} seconds${waitReason ? `: ${waitReason}` : ''}`); - // Wait for the specified duration - await new Promise(resolve => setTimeout(resolve, waitTime * 1000)); + // Wait for the specified duration (abortable) + await sleep(waitTime * 1000); // Get viewport summary after waiting let viewportSummary: string | undefined; @@ -1946,7 +1975,7 @@ export class PerformActionTool implements Tool<{ method: string, nodeId: number await Utils.performAction(target, method, actionArgsArray, xpath, iframeNodeId); // --- Wait for DOM to stabilize after action --- - await this.waitForDOMStability(target, method, isLikelyNavigationElement); + await this.waitForDOMStability(target, method, isLikelyNavigationElement, (ctx as LLMContext | undefined)?.abortSignal); // --- Capture tree state after action and generate diff --- try { @@ -2027,8 +2056,8 @@ export class PerformActionTool implements Tool<{ method: string, nodeId: number // Check for navigation after 'click' on relevant elements if (method === 'click' && isLikelyNavigationElement && initialUrl !== undefined) { logger.info('Checking for navigation after click'); - // Wait briefly for potential navigation. - await new Promise(resolve => setTimeout(resolve, 1000)); // 1 second wait + // Wait briefly for potential navigation (abortable) + await abortableSleep(1000, ctx?.abortSignal); const urlResult = await target.runtimeAgent().invoke_evaluate({ expression: 'window.location.href', @@ -2129,8 +2158,8 @@ Provide a clear, concise response about what happened.` } } else { try { - // Add some delay to allow UI to refresh - await new Promise(resolve => setTimeout(resolve, 300)); + // Add some delay to allow UI to refresh (abortable) + await abortableSleep(300, (ctx as LLMContext | undefined)?.abortSignal); // Take after screenshot const afterScreenshotResult = await target.pageAgent().invoke_captureScreenshot({ @@ -2383,7 +2412,7 @@ Provide a clear, descriptive response about what you observe and whether the act }; // DOM stability waiting method - private async waitForDOMStability(target: SDK.Target.Target, method: string, isLikelyNavigationElement: boolean): Promise { + private async waitForDOMStability(target: SDK.Target.Target, method: string, isLikelyNavigationElement: boolean, signal?: AbortSignal): Promise { const maxWaitTime = isLikelyNavigationElement ? 5000 : 2000; // 5s for navigation, 2s for other actions const startTime = Date.now(); @@ -2392,24 +2421,27 @@ Provide a clear, descriptive response about what you observe and whether the act try { // For navigation elements, wait for document ready state if (isLikelyNavigationElement) { - await this.waitForDocumentReady(target, maxWaitTime); + await this.waitForDocumentReady(target, maxWaitTime, signal); } // Wait for DOM mutations to settle using polling approach - await this.waitForDOMMutationStability(target, maxWaitTime - (Date.now() - startTime)); + await this.waitForDOMMutationStability(target, maxWaitTime - (Date.now() - startTime), signal); } catch (error) { logger.warn('Error waiting for DOM stability:', error); // Fallback to minimal wait - await new Promise(resolve => setTimeout(resolve, 300)); + await abortableSleep(300, signal); } } - private async waitForDocumentReady(target: SDK.Target.Target, maxWaitTime: number): Promise { + private async waitForDocumentReady(target: SDK.Target.Target, maxWaitTime: number, signal?: AbortSignal): Promise { const startTime = Date.now(); const pollInterval = 100; while (Date.now() - startTime < maxWaitTime) { + if (signal?.aborted) { + throw new DOMException('The operation was aborted', 'AbortError'); + } try { const readyStateResult = await target.runtimeAgent().invoke_evaluate({ expression: 'document.readyState', @@ -2421,7 +2453,7 @@ Provide a clear, descriptive response about what you observe and whether the act return; } - await new Promise(resolve => setTimeout(resolve, pollInterval)); + await abortableSleep(pollInterval, signal); } catch (error) { logger.warn('Error checking document ready state:', error); break; @@ -2429,7 +2461,7 @@ Provide a clear, descriptive response about what you observe and whether the act } } - private async waitForDOMMutationStability(target: SDK.Target.Target, maxWaitTime: number): Promise { + private async waitForDOMMutationStability(target: SDK.Target.Target, maxWaitTime: number, signal?: AbortSignal): Promise { const startTime = Date.now(); const stabilityWindow = 800; // Longer stability window for complex content const pollInterval = 100; @@ -2439,6 +2471,9 @@ Provide a clear, descriptive response about what you observe and whether the act const requiredStableChecks = 3; while (Date.now() - startTime < maxWaitTime) { + if (signal?.aborted) { + throw new DOMException('The operation was aborted', 'AbortError'); + } try { // Generic DOM stability detection const currentTreeResult = await target.runtimeAgent().invoke_evaluate({ @@ -2505,7 +2540,7 @@ Provide a clear, descriptive response about what you observe and whether the act } } - await new Promise(resolve => setTimeout(resolve, pollInterval)); + await abortableSleep(pollInterval, signal); } catch (error) { logger.warn('Error checking DOM stability:', error); break; @@ -3110,7 +3145,7 @@ Important guidelines: * Tool for getting URLs from a list of NodeIDs */ export class NodeIDsToURLsTool implements Tool<{ nodeIds: number[] }, NodeIDsToURLsResult | ErrorResult> { - name = 'get_urls_from_nodeids'; + name = 'node_ids_to_urls'; description = 'Gets URLs associated with DOM elements identified by NodeIDs from accessibility tree.'; async execute(args: { nodeIds: number[] }, _ctx?: LLMContext): Promise { @@ -3209,759 +3244,6 @@ export class NodeIDsToURLsTool implements Tool<{ nodeIds: number[] }, NodeIDsToU }; } -/** - * Tool for structured data extraction based on a provided schema - */ -export class SchemaBasedDataExtractionTool implements Tool<{ - objective: string, - schema: Record, - offset?: number, - chunkSize?: number, - maxRetries?: number, -}, SchemaBasedDataExtractionResult | ErrorResult> { - name = 'schema_based_extraction'; - description = 'Extracts structured data from the page according to a provided schema and objective, returning results in JSON format that resembles JSON structure. Particularly useful for extracting content while maintaining its original structure and relationships. ALWAYS provide a JSON Schema definition that describes the structure of data to extract.'; - - // Create system prompt for SchemaBasedDataExtractionTool - private getSystemPrompt(): string { - return `You are an expert data extraction assistant, specializing in analyzing web page accessibility trees and extracting structured data according to a provided schema and objective. - -Your task is to examine the provided simplified accessibility tree, which contains element structures with their accessibility IDs in brackets, and extract NodeIDs that match both the provided objective and schema. Your output must be valid JSON data that resembles HTML structure using NodeIDs instead of content. The objective will guide you on what kind of data to extract and how to interpret the schema in the context of the page content. - -Guidelines for extraction: -1. The schema will serve as a guide for what to extract, but structure the data in a DOM-like hierarchy -2. Create a JSON object that preserves parent-child relationships similar to HTML -3. Use tags, attributes, but ONLY include NodeIDs instead of actual content -4. Maintain proper nesting relationships (e.g., sections containing headings and paragraphs) -5. Every element that should contain content must have a nodeId property -6. Preserve the document flow and hierarchy similar to a real HTML document -7. NodeIDs should be numerical values (integers), not strings -8. IMPORTANT: You may filter out 'none' and 'generic' nodes that are just structural containers, but PRESERVE any 'none' or 'generic' nodes that have children with meaningful content (e.g., text, headings, links, buttons, etc.) - -Example DOM-like schema structure WITH NODEIDS: - -Schema: { "elements": ["heading", "paragraph", "list", "table", "link"] } - -JSON output: -{ - "type": "document", - "children": [ - { - "type": "section", - "children": [ - { - "type": "heading", - "level": 1, - "nodeId": 123 - }, - { - "type": "paragraph", - "nodeId": 456 - }, - { - "type": "list", - "listType": "unordered", - "items": [ - { "nodeId": 789 }, - { "nodeId": 790 }, - { "nodeId": 791 } - ] - } - ] - }, - { - "type": "section", - "children": [ - { - "type": "heading", - "level": 2, - "nodeId": 555 - }, - { - "type": "paragraph", - "nodeId": 556 - }, - { - "type": "link", - "href": "https://example.com", - "nodeId": 557 - }, - { - "type": "table", - "headers": [ - { "nodeId": 560 }, - { "nodeId": 561 }, - { "nodeId": 562 } - ], - "rows": [ - [ - { "nodeId": 570 }, - { "nodeId": 571 }, - { "nodeId": 572 } - ], - [ - { "nodeId": 580 }, - { "nodeId": 581 }, - { "nodeId": 582 } - ] - ] - } - ] - } - ] -} - -For more structured content types, use appropriate HTML-like nesting and NodeIDs: - -Schema: { "article": { "title": "string", "content": "string", "images": ["string"] } } - -JSON output: -{ - "type": "article", - "children": [ - { - "type": "heading", - "level": 1, - "nodeId": 123 - }, - { - "type": "paragraph", - "nodeId": 456 - }, - { - "type": "image", - "src": "image1.jpg", - "alt": "Description of first image", - "nodeId": 789 - }, - { - "type": "image", - "src": "image2.jpg", - "alt": "Description of second image", - "nodeId": 790 - } - ] -} - -Do not include any explanatory text or markdown syntax in your response. Return only valid JSON. - -CRITICAL: -1. Your response must ONLY contain valid JSON. Do not include any explanatory text outside the JSON structure. -2. ALWAYS include nodeId properties for any element that would normally have content. -3. Do NOT include actual content text - use only the nodeId property instead. -4. All nodeId values must be numbers, not strings.`; - } - - /** - * Get content from a specific node in the accessibility tree with intelligent handling - * of container nodes and their children - * @param nodeId The NodeID to extract content from - * @param nodes The accessibility tree nodes - * @returns The content as a string or a placeholder if not found - */ - private getNodeContent(nodeId: number, nodes: Protocol.Accessibility.AXNode[]): string | null { - const node = nodes.find(n => Number(n.nodeId) === nodeId); - if (!node) { - logger.warn(`SchemaBasedDataExtractionTool: Node not found for nodeId ${nodeId}`); - return null; - } - - // Check if this is a container that might have interesting children - if (this.isContainerNode(node) && node.childIds?.length) { - return this.getAggregatedNodeContent(node, nodes); - } - - // Get the name property which contains the accessible text - const nameProperty = node.properties?.find(p => String(p.name) === 'name'); - if (nameProperty && nameProperty.value?.value !== undefined) { - return String(nameProperty.value.value); - } - - // Some nodes might have text in valueValue - const valueProperty = node.properties?.find(p => String(p.name) === 'value'); - if (valueProperty?.value?.value !== undefined) { - return String(valueProperty.value.value); - } - - // For nodes with no text content, return role as a fallback - return `[${node.role?.value ? String(node.role.value) : 'Element'}]`; - } - - /** - * Check if a node is a container that might have interesting child content - * @param node The accessibility node to check - * @returns True if the node is a container role - */ - /** - * Determines if a node is a container that might have interesting child content - * This is important for intelligent content aggregation from complex elements - * @param node The accessibility node to check - * @returns True if the node is a container role that should have its children processed - */ - private isContainerNode(node: Protocol.Accessibility.AXNode): boolean { - // If node has no children, it's not a container regardless of role - if (!node.childIds?.length) { - return false; - } - - const role = String(node.role?.value || '').toLowerCase(); - - // List of roles that typically act as containers for other content - const containerRoles = [ - 'paragraph', 'section', 'div', 'header', 'footer', 'aside', - 'figure', 'blockquote', 'list', 'listitem', 'table', 'row', 'cell', 'columnheader', - 'rowheader', 'grid', 'document', 'form', 'group', 'region', 'tabpanel' - ]; - - // Special case: if node has a name property with content but also has children, - // we should consider it a container to get the most complete content - const nameProperty = node.properties?.find(p => String(p.name) === 'name'); - const hasDirectContent = nameProperty?.value?.value !== undefined && - String(nameProperty.value.value).trim() !== ''; - - return containerRoles.includes(role) || - (role === 'generic' && node.childIds.length > 0) || - (hasDirectContent && node.childIds.length > 0); - } - - /** - * Intelligently aggregate content from a node and its children - * @param node The node to extract content from - * @param allNodes All nodes in the accessibility tree - * @returns The aggregated content as a string - */ - private getAggregatedNodeContent(node: Protocol.Accessibility.AXNode, allNodes: Protocol.Accessibility.AXNode[]): string { - // First, get this node's direct text content - let directContent = ''; - - // Get the name property which contains the accessible text - const nameProperty = node.properties?.find(p => String(p.name) === 'name'); - if (nameProperty && nameProperty.value?.value !== undefined) { - directContent = String(nameProperty.value.value); - } - - // Some nodes might have text in valueValue - if (!directContent) { - const valueProperty = node.properties?.find(p => String(p.name) === 'value'); - if (valueProperty?.value?.value !== undefined) { - directContent = String(valueProperty.value.value); - } - } - - // If no children, return direct content or role as fallback - if (!node.childIds?.length) { - return directContent || `[${node.role?.value ? String(node.role.value) : 'Element'}]`; - } - - // Process child nodes - const childContents: string[] = []; - const role = String(node.role?.value || '').toLowerCase(); - - // For each child node - for (const childId of node.childIds) { - const childNode = allNodes.find(n => n.nodeId === childId); - if (!childNode) { - continue; - } - - const childRole = String(childNode.role?.value || '').toLowerCase(); - - // Handle different types of child nodes based on their role - if (childRole === 'statictext' || childRole === 'text') { - // Get direct text from text nodes - const childNameProperty = childNode.properties?.find(p => String(p.name) === 'name'); - if (childNameProperty?.value?.value !== undefined) { - childContents.push(String(childNameProperty.value.value)); - } - } else if (childRole === 'linebreak' || childRole === 'br') { - // Handle line breaks - childContents.push('\n'); - } else if (['emphasis', 'strong', 'b', 'i', 'em', 'mark', 'code', 'cite'].includes(childRole)) { - // Special handling for emphasis nodes - recursively get their content - const emphasisContent = this.getAggregatedNodeContent(childNode, allNodes); - if (emphasisContent) { - childContents.push(emphasisContent); - } - } else if (childRole === 'link') { - // Get text content from links - const linkContent = this.getAggregatedNodeContent(childNode, allNodes); - if (linkContent) { - childContents.push(linkContent); - } - } else if (this.isContainerNode(childNode)) { - // Recursively process container nodes - const containerContent = this.getAggregatedNodeContent(childNode, allNodes); - if (containerContent) { - // For certain container types, add appropriate spacing - if (['paragraph', 'div', 'section', 'article'].includes(childRole)) { - childContents.push(containerContent + '\n'); - } else if (['listitem'].includes(childRole)) { - childContents.push('โ€ข ' + containerContent); - } else { - childContents.push(containerContent); - } - } - } else { - // Default handling for other nodes - try to get their direct content - const childContent = this.getNodeContent(Number(childNode.nodeId), allNodes); - if (childContent) { - childContents.push(childContent); - } - } - } - - // Combine all child content with appropriate formatting based on node type - let combinedChildContent = ''; - - // Format based on container role - if (role === 'paragraph') { - // For paragraphs, join with spaces and normalize whitespace - combinedChildContent = childContents.join(' ') - .replace(/\s+/g, ' ') // Normalize whitespace - .replace(/ \n /g, '\n') // Fix spacing around line breaks - .trim(); - } else if (role === 'list') { - // For lists, join with newlines - combinedChildContent = childContents.join('\n'); - } else if (['table', 'grid'].includes(role)) { - // For tables, join with newlines and add extra spacing - combinedChildContent = childContents.join('\n'); - } else { - // Default joining with spaces - combinedChildContent = childContents.join(' ') - .replace(/\s+/g, ' ') - .trim(); - } - - // Return combined content, or direct content if no children had content - return combinedChildContent || directContent || `[${node.role?.value ? String(node.role.value) : 'Element'}]`; - } - - /** - * Process the structure to replace NodeIDs with actual content - * @param structure The structure containing NodeIDs (can be a number, array, or object) - * @param accessibilityNodes The accessibility tree nodes - * @returns The processed structure with content - */ - private async processNodeStructure(structure: unknown, accessibilityNodes: Protocol.Accessibility.AXNode[] = []): Promise { - // Map CDP nodes to AccessibilityNode format for getFormattedSubtreeByNodeId - // Do this mapping once at the top level of the recursion entry point - const mappedNodes = accessibilityNodes.map( - (node: Protocol.Accessibility.AXNode): AccessibilityNode => { - const roleValue = - node.role && typeof node.role === 'object' && 'value' in node.role - ? String(node.role.value) // Ensure string - : ''; - - const nameValue = - node.name && typeof node.name === 'object' && 'value' in node.name - ? String(node.name.value) // Ensure string - : undefined; - - const descriptionValue = - node.description && - typeof node.description === 'object' && - 'value' in node.description - ? String(node.description.value) // Ensure string - : undefined; - - const valueValue = - node.value && typeof node.value === 'object' && 'value' in node.value - ? String(node.value.value) // Ensure string - : undefined; - - const backendNodeId = - typeof node.backendDOMNodeId === 'number' - ? node.backendDOMNodeId - : undefined; - - return { - role: roleValue, - name: nameValue, - description: descriptionValue, - value: valueValue, - nodeId: String(node.nodeId), // Ensure nodeId is string - backendDOMNodeId: backendNodeId, - parentId: node.parentId, - childIds: node.childIds, - }; - }, - ); - - // Call a helper function to handle the actual recursive processing - // Pass both raw nodes (for getNodeContent) and mapped nodes (for getFormattedSubtreeByNodeId) - return await this.processNodeStructureRecursive(structure, accessibilityNodes, mappedNodes); - } - - /** - * Recursive helper for processing the structure - */ - /** - * Check if a node should be skipped based on its role - * @param nodeId The NodeID to check - * @param nodes The accessibility tree nodes - * @returns True if the node should be skipped, false otherwise - */ - private shouldSkipNode(nodeId: number, nodes: Protocol.Accessibility.AXNode[]): boolean { - const node = nodes.find(n => Number(n.nodeId) === nodeId); - if (!node) { - return false; // If node not found, don't skip it - } - - const role = String(node.role?.value || '').toLowerCase(); - - // Check if this is a 'none' or 'generic' node - if (role === 'none' || role === 'generic') { - // Only skip if it doesn't have meaningful children - return !this.hasRelevantChildren(node, nodes); - } - - return false; // Don't skip nodes with specific roles - } - - /** - * Check if a node has children with meaningful content - * @param node The node to check for meaningful children - * @param allNodes All nodes in the accessibility tree - * @returns True if the node has children with meaningful content - */ - private hasRelevantChildren(node: Protocol.Accessibility.AXNode, allNodes: Protocol.Accessibility.AXNode[]): boolean { - // If no children, definitely no meaningful content - if (!node.childIds?.length) { - return false; - } - - // Check direct children first - for (const childId of node.childIds) { - const childNode = allNodes.find(n => n.nodeId === childId); - if (!childNode) { continue; } - - const childRole = String(childNode.role?.value || '').toLowerCase(); - - // Check if child has direct content - if (this.nodeHasDirectContent(childNode)) { - return true; - } - - // Skip checking further if child is also a structural node - if (childRole === 'none' || childRole === 'generic') { - // Recursively check if this child has meaningful descendants - if (this.hasRelevantChildren(childNode, allNodes)) { - return true; - } - } else if (childRole !== 'none' && childRole !== 'generic') { - // Child has a specific role, which is likely meaningful - return true; - } - } - - return false; - } - - /** - * Check if a node has direct text content - * @param node The node to check for content - * @returns True if the node has direct text content - */ - private nodeHasDirectContent(node: Protocol.Accessibility.AXNode): boolean { - // Check for name property with content - const nameProperty = node.properties?.find(p => String(p.name) === 'name'); - if (nameProperty?.value?.value !== undefined && String(nameProperty.value.value).trim() !== '') { - return true; - } - - // Check for value property with content - const valueProperty = node.properties?.find(p => String(p.name) === 'value'); - if (valueProperty?.value?.value !== undefined && String(valueProperty.value.value).trim() !== '') { - return true; - } - - // Check role for text-specific roles - const role = String(node.role?.value || '').toLowerCase(); - if (['statictext', 'text', 'heading', 'link', 'button'].includes(role)) { - return true; - } - - return false; - } - - private async processNodeStructureRecursive(structure: unknown, accessibilityNodes: Protocol.Accessibility.AXNode[], mappedNodes: AccessibilityNode[]): Promise { - if (this.isEmptyOrUndefined(structure)) { - return structure; - } - - // Handle direct NodeID (a number) - if (typeof structure === 'number') { - const nodeId = structure; - - // Skip nodes with role 'none' or 'generic' - if (this.shouldSkipNode(nodeId, accessibilityNodes)) { - return null; // Return null for skipped nodes - } - - const content = this.getNodeContent(nodeId, accessibilityNodes); - const simplifiedRepresentation = Utils.getFormattedSubtreeByNodeId(String(nodeId), mappedNodes); - return { - originalNodeId: nodeId, - content: content ?? '[Content not found]', // Provide fallback - simplifiedRepresentation: simplifiedRepresentation ?? '[Simplified representation not found]', // Provide fallback - }; - } - - if (Array.isArray(structure)) { - // Process array of nodes or values - const processedArray = []; - for (const item of structure) { - // Pass mappedNodes down recursively - const processedItem = await this.processNodeStructureRecursive(item, accessibilityNodes, mappedNodes); - // Only add non-null items (skip null items which are 'none' or 'generic' nodes) - if (processedItem !== null) { - processedArray.push(processedItem); - } - } - return processedArray; - } - - if (typeof structure === 'object' && structure !== null) { - // Process object node - const processedObject: Record = {}; - - for (const [key, value] of Object.entries(structure)) { - // Handle standard nodeId property - if (key === 'nodeId' && typeof value === 'number') { - const nodeId = value; - - // Skip nodes with role 'none' or 'generic' - if (this.shouldSkipNode(nodeId, accessibilityNodes)) { - // For nodeId properties, we'll keep the object but mark it as skipped - processedObject.skipped = true; - processedObject[key] = nodeId; // Keep the original nodeId - continue; - } - - const content = this.getNodeContent(nodeId, accessibilityNodes); - const simplifiedRepresentation = Utils.getFormattedSubtreeByNodeId(String(nodeId), mappedNodes); - - processedObject[key] = nodeId; // Keep the original nodeId - processedObject.content = content ?? '[Content not found]'; // Add the content with fallback - processedObject.simplifiedRepresentation = simplifiedRepresentation ?? '[Simplified representation not found]'; // Add simplified representation with fallback - - } else if (typeof value === 'number') { - // Direct NodeID as value (e.g., "title": 18) - const nodeId = value; - - // Skip nodes with role 'none' or 'generic' - if (this.shouldSkipNode(nodeId, accessibilityNodes)) { - // For direct NodeID values, we'll skip this property entirely - continue; - } - - const content = this.getNodeContent(nodeId, accessibilityNodes); - const simplifiedRepresentation = Utils.getFormattedSubtreeByNodeId(String(nodeId), mappedNodes); - - // Replace the NodeID with an object containing content and simplified representation - processedObject[key] = { - originalNodeId: nodeId, - content: content ?? '[Content not found]', // Provide fallback - simplifiedRepresentation: simplifiedRepresentation ?? '[Simplified representation not found]', // Provide fallback - }; - } else { - // Recursively process other fields - // Pass mappedNodes down recursively - processedObject[key] = await this.processNodeStructureRecursive(value, accessibilityNodes, mappedNodes); - } - } - - // If the object is marked as skipped and has no other properties except nodeId and skipped, return null - if (processedObject.skipped && Object.keys(processedObject).length <= 3) { - return null; - } - - // Remove the skipped flag if it exists - if ('skipped' in processedObject) { - delete processedObject.skipped; - } - - return processedObject; - } - - // Return primitive values as is - return structure; - } - - /** - * Helper to check if a value is empty or undefined - * @param value The value to check - * @returns True if empty or undefined, false otherwise - */ - private isEmptyOrUndefined(value: unknown): boolean { - return value === undefined || value === null || value === ''; - } - - - async execute(args: { objective: string, schema: Record, offset?: number, chunkSize?: number, maxRetries?: number }, ctx?: LLMContext): Promise { - const { objective, schema, offset = 0, chunkSize = 60000, maxRetries = 1 } = args; // Default offset 0, chunkSize 60000, maxRetries 1 - let currentTry = 0; - let lastError: string | null = null; - - const agentService = AgentService.getInstance(); - const apiKey = agentService.getApiKey(); - const providerForExtraction = ctx?.provider; - const modelNameForExtraction = ctx?.miniModel || ctx?.model; - if (!providerForExtraction || !modelNameForExtraction) { - return { error: 'Missing LLM context (provider/model) for SchemaBasedDataExtractionTool' }; - } - - if (!apiKey) { - return { error: 'API key not configured.' }; - } - - if (!objective || typeof objective !== 'string' || objective.trim() === '') { - return { error: 'Objective must be a non-empty string' }; - } - - if (!schema || typeof schema !== 'object' || Object.keys(schema).length === 0) { - return { error: 'Schema must be a non-empty object' }; - } - - // --- Internal Agentic Loop --- - while (currentTry <= maxRetries) { - currentTry++; - logger.warn(`SchemaBasedDataExtractionTool: Attempt ${currentTry}/${maxRetries + 1}`); - let attemptError: Error | null = null; - - try { - // --- Step 1: Get Tree --- - logger.warn('SchemaBasedDataExtractionTool: Getting Accessibility Tree...'); - const getAccTreeTool = new GetAccessibilityTreeTool(); - const treeResult = await getAccTreeTool.execute({ reasoning: `Schema-based extraction attempt ${currentTry}` }); - if ('error' in treeResult) {throw new Error(`Tree Error: ${treeResult.error}`);} - const accessibilityTreeString = treeResult.simplified; - - if (!accessibilityTreeString || accessibilityTreeString.trim() === '') {throw new Error('Tree Error: Empty or blank tree content.');} - logger.warn('SchemaBasedDataExtractionTool: Got Accessibility Tree.'); - - // --- Step 2: LLM - Extract NodeIDs According to Schema --- - logger.warn('SchemaBasedDataExtractionTool: Extracting NodeIDs via LLM...'); - - const promptExtractData = ` -Objective: ${objective} - -Schema: ${JSON.stringify(schema, null, 2)} - -Full tree length: ${accessibilityTreeString.length} chars. Showing chars ${offset}-${offset + chunkSize}: -Simplified Accessibility Tree Chunk: -\`\`\` -${accessibilityTreeString.substring(offset, offset + chunkSize)} -\`\`\` -${accessibilityTreeString.length > offset + chunkSize ? `...(tree truncated at ${offset + chunkSize}/${accessibilityTreeString.length})...` : ''} -${lastError ? `Previous attempt failed with this error: "${lastError}". Consider a different approach.` : ''} -Extract NodeIDs according to the provided objective and schema, then return a structured JSON with NodeIDs instead of content.`; - - logger.info('SchemaBasedDataExtractionTool: Prompt:', promptExtractData); - // Use LLMClient to call the LLM - const llm = LLMClient.getInstance(); - const llmResponse = await llm.call({ - provider: providerForExtraction, - model: modelNameForExtraction, - messages: [ - { role: 'system', content: this.getSystemPrompt() }, - { role: 'user', content: promptExtractData } - ], - systemPrompt: this.getSystemPrompt(), - temperature: 0.7, - retryConfig: { maxRetries: 3, baseDelayMs: 1000 } - }); - const response = llmResponse.text; - logger.info('SchemaBasedDataExtractionTool: Response:', response); - - // Process the LLM response - this now contains NodeIDs instead of content - const nodeIdStructureJson = response?.trim() || ''; - - // Basic validation to ensure we got JSON - let nodeIdStructure; - try { - // Attempt to parse the JSON to validate it - nodeIdStructure = JSON.parse(nodeIdStructureJson); - } catch (error) { - throw new Error(`LLM did not return valid JSON data: ${(error as Error).message}`); - } - - // Step 3: Process the NodeID structure to replace IDs with content - logger.warn('SchemaBasedDataExtractionTool: Processing NodeIDs to get content...'); - const processedStructure = await this.processNodeStructure(nodeIdStructure, treeResult.nodes); - - logger.info('SchemaBasedDataExtractionTool: Processed structure:', processedStructure); - // Convert back to JSON string with proper formatting - const jsonData = JSON.stringify(processedStructure); - - // Fetch page metadata - let metadata: { url: string, title: string } | undefined; - const pageTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); - if (pageTarget) { - const metadataEval = await pageTarget.runtimeAgent().invoke_evaluate({ - expression: '({ url: window.location.href, title: document.title })', - returnByValue: true, - }); - metadata = metadataEval.result.value as { url: string, title: string }; - } - - // --- Success --- - return { - success: true, - message: 'Successfully extracted data according to the provided schema', - jsonData, - processedLength: offset + chunkSize, - totalLength: accessibilityTreeString.length, - truncated: accessibilityTreeString.length > offset + chunkSize, - metadata, - }; - - } catch (error) { - // Catch errors from any step within the try block - attemptError = error as Error; - logger.warn(`SchemaBasedDataExtractionTool: Attempt ${currentTry} failed:`, attemptError.message); - lastError = attemptError.message; // Store error message for the next attempt's prompt - } - } // End while loop - - // If loop finishes without success (i.e., all retries failed) - return { - error: `Failed data extraction after ${currentTry} attempts. Last error: ${lastError || 'Unknown error during final attempt.'}` - }; - } - - schema = { - type: 'object', - properties: { - objective: { - type: 'string', - description: 'The objective or goal of the extraction, explaining what information to find and why it is needed.', - }, - schema: { - type: 'object', - description: 'Schema defining the structure of data to extract. Can include nested objects and arrays.', - }, - offset: { - type: 'number', - description: 'Offset for the accessibility tree chunk (default: 0)', - default: 0 - }, - chunkSize: { - type: 'number', - description: 'Size of the accessibility tree chunk (default: 60000)', - default: 60000 - }, - maxRetries: { - type: 'number', - description: 'Maximum number of retries if an attempt fails (default: 1, meaning 2 total attempts).', - default: 1, - } - }, - required: ['objective', 'schema'], - }; -} - // Create interfaces for the visit history tool results export interface VisitHistoryDomainResult { visits: Array<{ @@ -4218,3 +3500,20 @@ export function getTools(): Array<( // Export the SequentialThinkingTool export { SequentialThinkingTool } from './SequentialThinkingTool.js'; +// Abortable sleep utility for tools that need delays/polling +function abortableSleep(ms: number, signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + if (!ms) return resolve(); + const timer = setTimeout(() => { cleanup(); resolve(); }, ms); + const onAbort = () => { clearTimeout(timer); cleanup(); reject(new DOMException('The operation was aborted', 'AbortError')); }; + const cleanup = () => { signal?.removeEventListener('abort', onAbort); }; + if (signal) { + if (signal.aborted) { + clearTimeout(timer); + cleanup(); + return reject(new DOMException('The operation was aborted', 'AbortError')); + } + signal.addEventListener('abort', onAbort, { once: true }); + } + }); +} diff --git a/front_end/panels/ai_chat/tracing/LangfuseProvider.ts b/front_end/panels/ai_chat/tracing/LangfuseProvider.ts index 6ad1eb31e4c..bdcf709be78 100644 --- a/front_end/panels/ai_chat/tracing/LangfuseProvider.ts +++ b/front_end/panels/ai_chat/tracing/LangfuseProvider.ts @@ -111,7 +111,7 @@ export class LangfuseProvider extends TracingProvider { traceId: string ): Promise { if (!this.enabled) { - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Tracing disabled, skipping observation creation:`, { + logger.debug(`Tracing disabled, skipping observation creation:`, { observationId: observation.id, name: observation.name, type: observation.type @@ -119,7 +119,7 @@ export class LangfuseProvider extends TracingProvider { return; } - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Creating observation:`, { + logger.debug(`Creating observation:`, { observationId: observation.id, name: observation.name, type: observation.type, @@ -199,7 +199,7 @@ export class LangfuseProvider extends TracingProvider { // Periodic cleanup of old observations this.cleanupObservationStore(); - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Successfully created observation:`, { + logger.debug(`Successfully created observation:`, { observationId: observation.id, eventType, stored: true, @@ -212,14 +212,14 @@ export class LangfuseProvider extends TracingProvider { updates: Partial ): Promise { if (!this.enabled) { - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Tracing disabled, skipping observation update:`, { + logger.debug(`Tracing disabled, skipping observation update:`, { observationId, updates }); return; } - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Updating observation:`, { + logger.debug(`Updating observation:`, { observationId, updates, hasEndTime: !!updates.endTime, @@ -230,7 +230,7 @@ export class LangfuseProvider extends TracingProvider { // Get the stored observation data const stored = this.observationStore.get(observationId); if (!stored) { - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Cannot update observation - not found:`, { + logger.debug(`Cannot update observation - not found:`, { observationId, availableObservations: Array.from(this.observationStore.keys()) }); @@ -316,7 +316,7 @@ export class LangfuseProvider extends TracingProvider { // Update stored observation data this.observationStore.set(observationId, { observation: updatedObservation, traceId: stored.traceId }); - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Successfully updated observation:`, { + logger.debug(`Successfully updated observation:`, { observationId, eventType, eventId: event.id, @@ -357,12 +357,12 @@ export class LangfuseProvider extends TracingProvider { async flush(): Promise { // Return existing flush promise if one is in progress if (this.isFlushInProgress && this.flushPromise) { - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Flush already in progress, waiting...`); + logger.debug(`Flush already in progress, waiting...`); return this.flushPromise; } if (!this.enabled || this.eventBuffer.length === 0) { - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Flush called but skipped:`, { + logger.debug(`Flush called but skipped:`, { enabled: this.enabled, bufferLength: this.eventBuffer.length }); @@ -381,13 +381,13 @@ export class LangfuseProvider extends TracingProvider { } private async performFlush(): Promise { - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Starting atomic flush of ${this.eventBuffer.length} events`); + logger.debug(`Starting atomic flush of ${this.eventBuffer.length} events`); // Atomic buffer clear using splice - prevents race conditions const events = this.eventBuffer.splice(0); if (events.length === 0) { - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: No events to flush after atomic clear`); + logger.debug(`No events to flush after atomic clear`); return; } @@ -396,19 +396,19 @@ export class LangfuseProvider extends TracingProvider { acc[event.type] = (acc[event.type] || 0) + 1; return acc; }, {} as Record); - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Event types being flushed:`, eventSummary); + logger.debug(`Event types being flushed:`, eventSummary); try { await this.sendBatch(events); logger.debug(`Flushed ${events.length} events to Langfuse`); - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Successfully flushed ${events.length} events`); + logger.debug(`Successfully flushed ${events.length} events`); } catch (error) { logger.error('Failed to flush events to Langfuse', error); console.error(`[HIERARCHICAL_TRACING] LangfuseProvider: Failed to flush events:`, error); // Re-add events to front maintaining chronological order this.eventBuffer.unshift(...events); - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Restored ${events.length} events to buffer`); + logger.debug(`Restored ${events.length} events to buffer`); throw error; } } @@ -548,11 +548,11 @@ export class LangfuseProvider extends TracingProvider { maxBufferSize: this.maxBufferSize, maxRetryEvents: this.maxRetryEvents }); - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Buffer overflow, discarded ${discarded} events`); + logger.debug(`Buffer overflow, discarded ${discarded} events`); } - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Adding event to buffer:`, { - eventType: event.type, + logger.debug(`Adding event to buffer:`, { + eventType: event.type, eventId: event.id, bufferSizeBefore: this.eventBuffer.length, bufferSizeAfter: this.eventBuffer.length + 1, @@ -568,13 +568,13 @@ export class LangfuseProvider extends TracingProvider { this.eventBuffer.push(event); if (this.eventBuffer.length >= this.batchSize) { - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Buffer full, triggering auto-flush`); + logger.debug(`Buffer full, triggering auto-flush`); logger.debug('Buffer full, triggering auto-flush'); this.flush().catch(error => { logger.error('Auto-flush failed', error); }); } else { - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Buffer not full yet:`, { + logger.debug(`Buffer not full yet:`, { currentSize: this.eventBuffer.length, batchSize: this.batchSize, remaining: this.batchSize - this.eventBuffer.length @@ -629,7 +629,7 @@ export class LangfuseProvider extends TracingProvider { this.lastCleanup = now; if (cleaned > 0) { logger.info(`Cleaned up ${cleaned} old observations from store`); - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Cleaned up ${cleaned} old observations`); + logger.debug(`Cleaned up ${cleaned} old observations`); } } @@ -650,6 +650,6 @@ export class LangfuseProvider extends TracingProvider { logger.error('Final flush failed during destroy', error); }); - console.log(`[HIERARCHICAL_TRACING] LangfuseProvider: Destroyed with ${this.eventBuffer.length} remaining events`); + logger.debug(`Destroyed with ${this.eventBuffer.length} remaining events`); } } \ No newline at end of file diff --git a/front_end/panels/ai_chat/ui/AIChatPanel.ts b/front_end/panels/ai_chat/ui/AIChatPanel.ts index c7efebad69b..b38228a4293 100644 --- a/front_end/panels/ai_chat/ui/AIChatPanel.ts +++ b/front_end/panels/ai_chat/ui/AIChatPanel.ts @@ -91,6 +91,7 @@ import * as Snackbars from '../../../ui/components/snackbars/snackbars.js'; import { MCPRegistry } from '../mcp/MCPRegistry.js'; import { getMCPConfig } from '../mcp/MCPConfig.js'; import { onMCPConfigChange } from '../mcp/MCPConfig.js'; +import { MCPConnectorsCatalogDialog } from './mcp/MCPConnectorsCatalogDialog.js'; const {html} = Lit; @@ -163,6 +164,10 @@ const UIStrings = { *@description AI chat UI tooltip text for the help button. */ help: 'Help', + /** + *@description AI chat UI tooltip text for the MCP connectors catalog button. + */ + mcpConnectors: 'MCP Connectors', /** *@description AI chat UI tooltip text for the settings button (gear icon). */ @@ -217,6 +222,7 @@ interface ToolbarViewInput { onHistoryClick: (event: MouseEvent) => void; onDeleteClick: () => void; onHelpClick: () => void; + onMCPConnectorsClick: () => void; onSettingsClick: () => void; onEvaluationTestClick: () => void; onBookmarkClick: () => void; @@ -227,17 +233,18 @@ interface ToolbarViewInput { function toolbarView(input: ToolbarViewInput): Lit.LitTemplate { // clang-format off + // Add history button when history feature is implemented + // + //
return html`