// Generic agent that uses .claude/agents definitions with multi-provider SDK routing import { query } from "@anthropic-ai/claude-agent-sdk"; import { logger } from "../utils/logger.js"; import { withRetry } from "../utils/retry.js"; import { claudeFlowSdkServer } from "../mcp/claudeFlowSdkServer.js"; function getCurrentProvider() { // Determine provider from environment if (process.env.PROVIDER === 'gemini' || process.env.USE_GEMINI === 'true') { return 'gemini'; } if (process.env.PROVIDER === 'requesty' || process.env.USE_REQUESTY === 'true') { return 'requesty'; } if (process.env.PROVIDER === 'openrouter' || process.env.USE_OPENROUTER === 'true') { return 'openrouter'; } if (process.env.PROVIDER === 'onnx' || process.env.USE_ONNX === 'true') { return 'onnx'; } return 'anthropic'; // Default } function getModelForProvider(provider) { switch (provider) { case 'gemini': const geminiKey = process.env.GOOGLE_GEMINI_API_KEY; if (!geminiKey) { throw new Error('GOOGLE_GEMINI_API_KEY is required for Gemini provider.\n' + 'Set it via environment variable or use --provider anthropic for Claude models.\n' + 'Get your API key at: https://makersuite.google.com/app/apikey'); } return { model: process.env.COMPLETION_MODEL || 'gemini-2.0-flash-exp', apiKey: geminiKey, baseURL: process.env.PROXY_URL || undefined }; case 'requesty': const requestyKey = process.env.REQUESTY_API_KEY; if (!requestyKey) { throw new Error('REQUESTY_API_KEY is required for Requesty provider.\n' + 'Set it via environment variable or use --provider anthropic for Claude models.\n' + 'Get your API key at: https://requesty.ai'); } return { model: process.env.COMPLETION_MODEL || 'deepseek/deepseek-chat', apiKey: requestyKey, baseURL: process.env.PROXY_URL || undefined }; case 'openrouter': const openrouterKey = process.env.OPENROUTER_API_KEY; if (!openrouterKey) { throw new Error('OPENROUTER_API_KEY is required for OpenRouter provider.\n' + 'Set it via environment variable or use --provider anthropic for Claude models.\n' + 'Get your API key at: https://openrouter.ai/keys'); } return { model: process.env.COMPLETION_MODEL || 'deepseek/deepseek-chat', apiKey: openrouterKey, baseURL: process.env.PROXY_URL || undefined }; case 'onnx': return { model: 'onnx-local', apiKey: 'local', baseURL: process.env.PROXY_URL || undefined }; case 'anthropic': default: // For anthropic provider, require ANTHROPIC_API_KEY const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { throw new Error('ANTHROPIC_API_KEY is required but not set for Anthropic provider'); } return { model: process.env.COMPLETION_MODEL || 'claude-sonnet-4-5-20250929', apiKey, // No baseURL for direct Anthropic }; } } export async function claudeAgent(agent, input, onStream, modelOverride) { const startTime = Date.now(); const provider = getCurrentProvider(); logger.info('Starting Claude Agent SDK with multi-provider support', { agent: agent.name, provider, input: input.substring(0, 100), model: modelOverride || 'default' }); return withRetry(async () => { // Get model configuration for the selected provider const modelConfig = getModelForProvider(provider); const finalModel = modelOverride || modelConfig.model; // Configure environment for Claude Agent SDK with proxy routing // The SDK internally uses Anthropic client which reads ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY const envOverrides = {}; if (provider === 'gemini' && process.env.GOOGLE_GEMINI_API_KEY) { // Use ANTHROPIC_BASE_URL if already set by CLI (proxy mode) envOverrides.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || 'proxy-key'; envOverrides.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL || process.env.GEMINI_PROXY_URL || 'http://localhost:3000'; logger.info('Using Gemini proxy', { proxyUrl: envOverrides.ANTHROPIC_BASE_URL, model: finalModel }); } else if (provider === 'requesty' && process.env.REQUESTY_API_KEY) { // Use ANTHROPIC_BASE_URL if already set by CLI (proxy mode) envOverrides.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || 'proxy-key'; envOverrides.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL || process.env.REQUESTY_PROXY_URL || 'http://localhost:3000'; logger.info('Using Requesty proxy', { proxyUrl: envOverrides.ANTHROPIC_BASE_URL, model: finalModel }); } else if (provider === 'openrouter' && process.env.OPENROUTER_API_KEY) { // Use ANTHROPIC_BASE_URL if already set by CLI (proxy mode) envOverrides.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || 'proxy-key'; envOverrides.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL || process.env.OPENROUTER_PROXY_URL || 'http://localhost:3000'; logger.info('Using OpenRouter proxy', { proxyUrl: envOverrides.ANTHROPIC_BASE_URL, model: finalModel }); } else if (provider === 'onnx') { // For ONNX: Use ANTHROPIC_BASE_URL if already set by CLI (proxy mode) envOverrides.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || 'sk-ant-onnx-local-key'; envOverrides.ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL || process.env.ONNX_PROXY_URL || 'http://localhost:3001'; logger.info('Using ONNX local proxy', { proxyUrl: envOverrides.ANTHROPIC_BASE_URL, model: finalModel }); } // For Anthropic provider, use existing ANTHROPIC_API_KEY (no proxy needed) logger.info('Multi-provider configuration', { provider, model: finalModel, hasApiKey: !!envOverrides.ANTHROPIC_API_KEY || !!process.env.ANTHROPIC_API_KEY, hasBaseURL: !!envOverrides.ANTHROPIC_BASE_URL }); try { // MCP server setup - enable in-SDK server and optional external servers const mcpServers = {}; // CRITICAL FIX: Disable all MCP servers for Requesty provider // The Claude SDK hangs when trying to initialize MCP servers with Requesty // This is a fundamental incompatibility - SDK initialization fails before API call if (provider === 'requesty') { logger.info('⚠️ Requesty provider detected - disabling all MCP servers to prevent hang'); console.log('⚠️ Requesty: MCP tools disabled (SDK incompatibility)'); // Skip all MCP server initialization for Requesty // Continue with empty mcpServers object } else { // Enable in-SDK MCP server for custom tools (enabled by default) if (process.env.ENABLE_CLAUDE_FLOW_SDK !== 'false') { mcpServers['claude-flow-sdk'] = claudeFlowSdkServer; } // External MCP servers (enabled by default for full 213-tool access) // Disable by setting ENABLE_CLAUDE_FLOW_MCP=false if (process.env.ENABLE_CLAUDE_FLOW_MCP !== 'false') { mcpServers['claude-flow'] = { type: 'stdio', command: 'npx', args: ['claude-flow@alpha', 'mcp', 'start'], env: { ...process.env, MCP_AUTO_START: 'true', PROVIDER: provider } }; } if (process.env.ENABLE_FLOW_NEXUS_MCP !== 'false') { mcpServers['flow-nexus'] = { type: 'stdio', command: 'npx', args: ['flow-nexus@latest', 'mcp', 'start'], env: { ...process.env, FLOW_NEXUS_AUTO_START: 'true' } }; } if (process.env.ENABLE_AGENTIC_PAYMENTS_MCP !== 'false') { mcpServers['agentic-payments'] = { type: 'stdio', command: 'npx', args: ['-y', 'agentic-payments', 'mcp'], env: { ...process.env, AGENTIC_PAYMENTS_AUTO_START: 'true' } }; } // Load MCP servers from user config file (~/.agentic-flow/mcp-config.json) try { const fs = await import('fs'); const path = await import('path'); const os = await import('os'); const configPath = path.join(os.homedir(), '.agentic-flow', 'mcp-config.json'); if (fs.existsSync(configPath)) { const configContent = fs.readFileSync(configPath, 'utf-8'); const config = JSON.parse(configContent); // Add enabled user-configured servers for (const [name, server] of Object.entries(config.servers || {})) { const serverConfig = server; if (serverConfig.enabled) { mcpServers[name] = { type: 'stdio', command: serverConfig.command, args: serverConfig.args || [], env: { ...process.env, ...serverConfig.env } }; console.log(`[agentic-flow] Loaded MCP server: ${name}`); } } } } catch (error) { // Silently fail if config doesn't exist or can't be read console.log('[agentic-flow] No user MCP config found (this is normal)'); } } // End of provider !== 'requesty' check const queryOptions = { systemPrompt: agent.systemPrompt, model: finalModel, // Claude Agent SDK handles model selection permissionMode: 'bypassPermissions', // Auto-approve all tool usage for Docker automation // Enable all built-in tools by default (Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch) // Based on SDK types, allowedTools and disallowedTools control which tools are available // If not specified, all tools are enabled by default allowedTools: [ 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'WebFetch', 'WebSearch', 'NotebookEdit', 'TodoWrite' ], // Add MCP servers if configured mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined }; // Add environment overrides if present if (Object.keys(envOverrides).length > 0) { queryOptions.env = { ...process.env, ...envOverrides }; } const result = query({ prompt: input, options: queryOptions }); let output = ''; let toolCallCount = 0; for await (const msg of result) { const msgAny = msg; // Use any to handle different event types from SDK // Debug: Log message structure to understand SDK events if (process.env.DEBUG_STREAMING === 'true') { console.error(`[DEBUG] Message type: ${msg.type}, keys: ${Object.keys(msg).join(', ')}`); } // Handle assistant text messages if (msg.type === 'assistant') { const chunk = msg.message.content?.map((c) => c.type === 'text' ? c.text : '').join('') || ''; output += chunk; if (onStream && chunk) { onStream(chunk); } // Check for tool use in message content blocks const toolBlocks = msg.message.content?.filter((c) => c.type === 'tool_use') || []; for (const toolBlock of toolBlocks) { toolCallCount++; const toolName = toolBlock.name || 'unknown'; const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; const progressMsg = `\n[${timestamp}] 🔍 Tool call #${toolCallCount}: ${toolName}\n`; process.stderr.write(progressMsg); if (onStream) { onStream(progressMsg); } } } // Handle stream events that contain tool information if (msgAny.streamEvent) { const event = msgAny.streamEvent; // Tool use event (content_block_start with tool_use) if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use') { toolCallCount++; const toolName = event.content_block.name || 'unknown'; const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; const progressMsg = `\n[${timestamp}] 🔍 Tool call #${toolCallCount}: ${toolName}\n`; process.stderr.write(progressMsg); if (onStream) { onStream(progressMsg); } } // Tool result event (content_block_stop) if (event.type === 'content_block_stop') { const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; const resultMsg = `[${timestamp}] ✅ Tool completed\n`; process.stderr.write(resultMsg); if (onStream) { onStream(resultMsg); } } } // Flush output to ensure immediate visibility if (process.stderr.uncork) { process.stderr.uncork(); } if (process.stdout.uncork) { process.stdout.uncork(); } } const duration = Date.now() - startTime; logger.info('Claude Agent SDK completed', { agent: agent.name, provider, duration, outputLength: output.length }); return { output, agent: agent.name }; } catch (error) { logger.error('Claude Agent SDK execution failed', { provider, model: finalModel, error: error.message }); throw error; } }); } //# sourceMappingURL=claudeAgent.js.map