tasq/node_modules/agentic-flow/dist/agents/directApiAgent.js

383 lines
18 KiB
JavaScript

// Direct API agent with multi-provider support (Anthropic, OpenRouter, Gemini)
import Anthropic from '@anthropic-ai/sdk';
import { logger } from '../utils/logger.js';
import { withRetry } from '../utils/retry.js';
import { execSync } from 'child_process';
import { ModelRouter } from '../router/router.js';
// Lazy initialize clients
let anthropic = null;
let router = null;
function getAnthropicClient() {
if (!anthropic) {
const apiKey = process.env.ANTHROPIC_API_KEY;
// Validate API key format
if (!apiKey) {
throw new Error('ANTHROPIC_API_KEY is required but not set');
}
if (!apiKey.startsWith('sk-ant-')) {
throw new Error(`Invalid ANTHROPIC_API_KEY format. Expected format: sk-ant-...\n` +
`Got: ${apiKey.substring(0, 10)}...\n\n` +
`Please check your API key at: https://console.anthropic.com/settings/keys`);
}
anthropic = new Anthropic({ apiKey });
}
return anthropic;
}
function getRouter() {
if (!router) {
// Router will now auto-create config from environment variables if no file exists
router = new ModelRouter();
}
return router;
}
function getCurrentProvider() {
// Determine provider from environment
if (process.env.PROVIDER === 'gemini' || process.env.USE_GEMINI === 'true') {
return 'gemini';
}
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';
}
// Define claude-flow tools as native Anthropic tool definitions
const claudeFlowTools = [
{
name: 'memory_store',
description: 'Store a value in persistent memory with optional namespace and TTL',
input_schema: {
type: 'object',
properties: {
key: { type: 'string', description: 'Memory key' },
value: { type: 'string', description: 'Value to store' },
namespace: { type: 'string', description: 'Memory namespace', default: 'default' },
ttl: { type: 'number', description: 'Time-to-live in seconds' }
},
required: ['key', 'value']
}
},
{
name: 'memory_retrieve',
description: 'Retrieve a value from persistent memory',
input_schema: {
type: 'object',
properties: {
key: { type: 'string', description: 'Memory key' },
namespace: { type: 'string', description: 'Memory namespace', default: 'default' }
},
required: ['key']
}
},
{
name: 'memory_search',
description: 'Search for keys matching a pattern in memory',
input_schema: {
type: 'object',
properties: {
pattern: { type: 'string', description: 'Search pattern (supports wildcards)' },
namespace: { type: 'string', description: 'Memory namespace to search in' },
limit: { type: 'number', description: 'Maximum results to return', default: 10 }
},
required: ['pattern']
}
},
{
name: 'swarm_init',
description: 'Initialize a multi-agent swarm with specified topology',
input_schema: {
type: 'object',
properties: {
topology: { type: 'string', enum: ['mesh', 'hierarchical', 'ring', 'star'], description: 'Swarm topology' },
maxAgents: { type: 'number', description: 'Maximum number of agents', default: 8 },
strategy: { type: 'string', enum: ['balanced', 'specialized', 'adaptive'], description: 'Agent distribution strategy', default: 'balanced' }
},
required: ['topology']
}
},
{
name: 'agent_spawn',
description: 'Spawn a new agent in the swarm',
input_schema: {
type: 'object',
properties: {
type: { type: 'string', enum: ['researcher', 'coder', 'analyst', 'optimizer', 'coordinator'], description: 'Agent type' },
capabilities: { type: 'array', items: { type: 'string' }, description: 'Agent capabilities' },
name: { type: 'string', description: 'Custom agent name' }
},
required: ['type']
}
},
{
name: 'task_orchestrate',
description: 'Orchestrate a complex task across the swarm',
input_schema: {
type: 'object',
properties: {
task: { type: 'string', description: 'Task description or instructions' },
strategy: { type: 'string', enum: ['parallel', 'sequential', 'adaptive'], description: 'Execution strategy', default: 'adaptive' },
priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'Task priority', default: 'medium' },
maxAgents: { type: 'number', description: 'Maximum agents to use for this task' }
},
required: ['task']
}
},
{
name: 'swarm_status',
description: 'Get current swarm status and metrics',
input_schema: {
type: 'object',
properties: {
verbose: { type: 'boolean', description: 'Include detailed metrics', default: false }
}
}
}
];
// Execute tool calls using claude-flow CLI
async function executeToolCall(toolName, toolInput) {
try {
logger.info('Executing tool', { toolName, input: toolInput });
switch (toolName) {
case 'memory_store': {
const { key, value, namespace = 'default', ttl } = toolInput;
const cmd = `npx claude-flow@alpha memory store "${key}" "${value}" --namespace "${namespace}"${ttl ? ` --ttl ${ttl}` : ''}`;
const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
logger.info('Memory stored', { key, namespace });
return `✅ Stored successfully\n📝 Key: ${key}\n📦 Namespace: ${namespace}\n💾 Size: ${value.length} bytes`;
}
case 'memory_retrieve': {
const { key, namespace = 'default' } = toolInput;
const cmd = `npx claude-flow@alpha memory retrieve "${key}" --namespace "${namespace}"`;
const result = execSync(cmd, { encoding: 'utf-8' });
logger.info('Memory retrieved', { key });
return `✅ Retrieved:\n${result}`;
}
case 'memory_search': {
const { pattern, namespace, limit = 10 } = toolInput;
const cmd = `npx claude-flow@alpha memory search "${pattern}"${namespace ? ` --namespace "${namespace}"` : ''} --limit ${limit}`;
const result = execSync(cmd, { encoding: 'utf-8' });
return `🔍 Search results:\n${result}`;
}
case 'swarm_init': {
const { topology, maxAgents = 8, strategy = 'balanced' } = toolInput;
const cmd = `npx claude-flow@alpha swarm init --topology ${topology} --max-agents ${maxAgents} --strategy ${strategy}`;
const result = execSync(cmd, { encoding: 'utf-8' });
return `🚀 Swarm initialized:\n${result}`;
}
case 'agent_spawn': {
const { type, capabilities, name } = toolInput;
const capStr = capabilities ? ` --capabilities "${capabilities.join(',')}"` : '';
const nameStr = name ? ` --name "${name}"` : '';
const cmd = `npx claude-flow@alpha agent spawn --type ${type}${capStr}${nameStr}`;
const result = execSync(cmd, { encoding: 'utf-8' });
return `🤖 Agent spawned:\n${result}`;
}
case 'task_orchestrate': {
const { task, strategy = 'adaptive', priority = 'medium', maxAgents } = toolInput;
const maxStr = maxAgents ? ` --max-agents ${maxAgents}` : '';
const cmd = `npx claude-flow@alpha task orchestrate "${task}" --strategy ${strategy} --priority ${priority}${maxStr}`;
const result = execSync(cmd, { encoding: 'utf-8' });
return `⚡ Task orchestrated:\n${result}`;
}
case 'swarm_status': {
const { verbose = false } = toolInput;
const cmd = `npx claude-flow@alpha swarm status${verbose ? ' --verbose' : ''}`;
const result = execSync(cmd, { encoding: 'utf-8' });
return `📊 Swarm status:\n${result}`;
}
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
catch (error) {
logger.error('Tool execution failed', { toolName, error: error.message });
return `❌ Tool execution failed: ${error.message}`;
}
}
/**
* Direct API agent using Anthropic SDK with native tool calling
* Bypasses Claude Agent SDK subprocess issues entirely
*/
export async function directApiAgent(agent, input, onStream) {
const startTime = Date.now();
logger.info('Starting direct API agent', {
agent: agent.name,
input: input.substring(0, 100)
});
return withRetry(async () => {
const messages = [
{ role: 'user', content: input }
];
let finalOutput = '';
let toolUseCount = 0;
const maxToolUses = 10; // Prevent infinite loops
// Agentic loop: keep calling API until no more tool uses
while (toolUseCount < maxToolUses) {
logger.debug('API call iteration', { toolUseCount, messagesLength: messages.length });
const provider = getCurrentProvider();
let response;
try {
// Use router for non-Anthropic providers
if (provider === 'gemini' || provider === 'openrouter') {
const routerInstance = getRouter();
// Convert Anthropic messages format to router format
const routerMessages = messages.map(msg => ({
role: msg.role,
content: typeof msg.content === 'string' ? msg.content : msg.content.map((block) => {
if ('text' in block)
return { type: 'text', text: block.text };
if ('tool_use_id' in block)
return {
type: 'tool_result',
content: block.content
};
if ('name' in block && 'input' in block)
return {
type: 'tool_use',
id: block.id,
name: block.name,
input: block.input
};
return { type: 'text', text: '' };
}).filter((b) => b.type === 'text' || b.type === 'tool_use' || b.type === 'tool_result')
}));
// Add system prompt as first message if needed
const messagesWithSystem = agent.systemPrompt
? [{ role: 'system', content: agent.systemPrompt }, ...routerMessages]
: routerMessages;
const params = {
model: provider === 'gemini'
? (process.env.COMPLETION_MODEL || 'gemini-2.0-flash-exp')
: (process.env.COMPLETION_MODEL || 'deepseek/deepseek-chat'),
messages: messagesWithSystem,
maxTokens: 8192,
temperature: 0.7
};
const routerResponse = await routerInstance.chat(params);
// Convert router response to Anthropic format
response = {
id: routerResponse.id,
model: routerResponse.model,
stop_reason: routerResponse.stopReason,
content: routerResponse.content.map(block => {
if (block.type === 'text')
return { type: 'text', text: block.text || '' };
if (block.type === 'tool_use')
return {
type: 'tool_use',
id: block.id || '',
name: block.name || '',
input: block.input || {}
};
return { type: 'text', text: '' };
})
};
}
else {
// Use Anthropic client for Anthropic provider
const client = getAnthropicClient();
response = await client.messages.create({
model: process.env.COMPLETION_MODEL || 'claude-sonnet-4-5-20250929',
max_tokens: 8192,
system: agent.systemPrompt || 'You are a helpful AI assistant.',
messages,
tools: claudeFlowTools
});
}
}
catch (error) {
// Enhance authentication errors with helpful guidance
if (error?.status === 401 || error?.statusCode === 401) {
const providerName = provider === 'gemini' ? 'Google Gemini' : provider === 'openrouter' ? 'OpenRouter' : 'Anthropic';
const apiKey = provider === 'gemini'
? process.env.GOOGLE_GEMINI_API_KEY
: provider === 'openrouter'
? process.env.OPENROUTER_API_KEY
: process.env.ANTHROPIC_API_KEY;
throw new Error(`${providerName} API authentication failed (401)\n\n` +
`Your API key is invalid, expired, or lacks permissions.\n` +
`Current key: ${apiKey?.substring(0, 15)}...\n\n` +
`Please check your ${providerName} API key and update your .env file.\n\n` +
`Alternative providers:\n` +
` --provider anthropic (Claude models)\n` +
` --provider openrouter (100+ models, 99% cost savings)\n` +
` --provider gemini (Google models)\n` +
` --provider onnx (free local inference)`);
}
throw error;
}
logger.debug('API response', {
stopReason: response.stop_reason,
contentBlocks: response.content.length
});
// Process response content
const toolResults = [];
for (const block of response.content) {
if (block.type === 'text') {
finalOutput += block.text;
if (onStream) {
onStream(block.text);
}
}
else if (block.type === 'tool_use') {
toolUseCount++;
logger.info('Tool use requested', {
toolName: block.name,
toolUseId: block.id
});
// Execute the tool
const toolResult = await executeToolCall(block.name, block.input);
// Collect tool result
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: toolResult
});
logger.debug('Tool result collected', {
toolUseId: block.id,
resultLength: toolResult.length
});
}
}
// If there were tool uses, add assistant message and all tool results
if (toolResults.length > 0) {
// Add assistant message with tool uses
messages.push({
role: 'assistant',
content: response.content
});
// Add all tool results in one user message
messages.push({
role: 'user',
content: toolResults
});
logger.debug('Tool results added to conversation', {
count: toolResults.length
});
}
// Stop if no tool use or end_turn
if (response.stop_reason === 'end_turn' || response.content.every((b) => b.type === 'text')) {
// Add final assistant message if it has text
const textContent = response.content.filter((b) => b.type === 'text');
if (textContent.length > 0 && messages[messages.length - 1].role !== 'assistant') {
messages.push({
role: 'assistant',
content: response.content
});
}
break;
}
}
const duration = Date.now() - startTime;
logger.info('Direct API agent completed', {
agent: agent.name,
duration,
toolUseCount,
outputLength: finalOutput.length
});
return { output: finalOutput, agent: agent.name };
});
}
//# sourceMappingURL=directApiAgent.js.map