336 lines
16 KiB
JavaScript
336 lines
16 KiB
JavaScript
// 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
|