325 lines
11 KiB
JavaScript
325 lines
11 KiB
JavaScript
/**
|
|
* Smart Agent Flow - Multi-Agent Routing System
|
|
*
|
|
* Enables Qwen to:
|
|
* 1. Read available agents (names, roles, capabilities)
|
|
* 2. Use multiple agents in a single task by delegating sub-tasks
|
|
* 3. Merge results back into the main response
|
|
*
|
|
* Components:
|
|
* - Agent Registry: Available agents with metadata
|
|
* - Orchestrator: Decides which agents to use
|
|
* - Router: Routes sub-tasks to agents
|
|
* - Merger: Combines agent outputs
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// AGENT REGISTRY
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Built-in agents with their capabilities
|
|
*/
|
|
const BUILTIN_AGENTS = {
|
|
build: {
|
|
id: 'build',
|
|
name: 'Build Agent',
|
|
role: 'Full-stack development',
|
|
capabilities: ['coding', 'debugging', 'implementation', 'refactoring'],
|
|
whenToUse: 'General development tasks, implementing features, fixing bugs',
|
|
priority: 1
|
|
},
|
|
plan: {
|
|
id: 'plan',
|
|
name: 'Planning Agent',
|
|
role: 'Architecture and planning',
|
|
capabilities: ['architecture', 'design', 'task-breakdown', 'estimation'],
|
|
whenToUse: 'Complex features requiring upfront design, multi-step tasks',
|
|
priority: 2
|
|
},
|
|
test: {
|
|
id: 'test',
|
|
name: 'Testing Agent',
|
|
role: 'Quality assurance',
|
|
capabilities: ['unit-tests', 'integration-tests', 'test-strategy', 'coverage'],
|
|
whenToUse: 'Writing tests, improving coverage, test-driven development',
|
|
priority: 3
|
|
},
|
|
docs: {
|
|
id: 'docs',
|
|
name: 'Documentation Agent',
|
|
role: 'Technical writing',
|
|
capabilities: ['documentation', 'comments', 'readme', 'api-docs'],
|
|
whenToUse: 'Writing docs, improving comments, creating READMEs',
|
|
priority: 4
|
|
},
|
|
security: {
|
|
id: 'security',
|
|
name: 'Security Reviewer',
|
|
role: 'Security analysis',
|
|
capabilities: ['vulnerability-scan', 'auth-review', 'input-validation', 'secrets'],
|
|
whenToUse: 'Auth changes, handling sensitive data, security-critical code',
|
|
priority: 5
|
|
},
|
|
refactor: {
|
|
id: 'refactor',
|
|
name: 'Refactoring Agent',
|
|
role: 'Code improvement',
|
|
capabilities: ['cleanup', 'optimization', 'patterns', 'technical-debt'],
|
|
whenToUse: 'Improving code quality, reducing tech debt, applying patterns',
|
|
priority: 6
|
|
}
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// ORCHESTRATOR CONFIGURATION
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
const DEFAULT_CONFIG = {
|
|
enabled: true,
|
|
maxAgentsPerRequest: 3,
|
|
maxTokensPerAgent: 2000,
|
|
mergeStrategy: 'advisory', // 'advisory' = main model merges, 'sequential' = chain outputs
|
|
autoDetect: true, // Automatically detect when to use multiple agents
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// SMART AGENT FLOW CLASS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
export class SmartAgentFlow {
|
|
constructor(config = {}) {
|
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
this.agents = { ...BUILTIN_AGENTS };
|
|
this.activeAgents = [];
|
|
this.agentOutputs = [];
|
|
}
|
|
|
|
/**
|
|
* Load custom agents from .opencode/agent directory
|
|
*/
|
|
loadCustomAgents(projectPath) {
|
|
const agentDir = path.join(projectPath, '.opencode', 'agent');
|
|
if (!fs.existsSync(agentDir)) return;
|
|
|
|
const files = fs.readdirSync(agentDir).filter(f => f.endsWith('.md'));
|
|
for (const file of files) {
|
|
const content = fs.readFileSync(path.join(agentDir, file), 'utf8');
|
|
const name = path.basename(file, '.md');
|
|
|
|
// Parse agent metadata from markdown frontmatter or content
|
|
this.agents[name] = {
|
|
id: name,
|
|
name: this.extractTitle(content) || name,
|
|
role: 'Custom agent',
|
|
capabilities: this.extractCapabilities(content),
|
|
whenToUse: this.extractWhenToUse(content),
|
|
priority: 10,
|
|
custom: true
|
|
};
|
|
}
|
|
}
|
|
|
|
extractTitle(content) {
|
|
const match = content.match(/^#\s+(.+)$/m);
|
|
return match ? match[1].trim() : null;
|
|
}
|
|
|
|
extractCapabilities(content) {
|
|
const match = content.match(/capabilities?:?\s*(.+)/i);
|
|
if (match) {
|
|
return match[1].split(/[,;]/).map(c => c.trim().toLowerCase());
|
|
}
|
|
return ['general'];
|
|
}
|
|
|
|
extractWhenToUse(content) {
|
|
const match = content.match(/when\s*to\s*use:?\s*(.+)/i);
|
|
return match ? match[1].trim() : 'Custom agent for specialized tasks';
|
|
}
|
|
|
|
/**
|
|
* Get all available agents
|
|
*/
|
|
getAgents() {
|
|
return Object.values(this.agents);
|
|
}
|
|
|
|
/**
|
|
* Get agent by ID
|
|
*/
|
|
getAgent(id) {
|
|
return this.agents[id] || null;
|
|
}
|
|
|
|
/**
|
|
* Analyze request to determine if multi-agent mode is beneficial
|
|
*/
|
|
analyzeRequest(request) {
|
|
if (!this.config.autoDetect || !this.config.enabled) {
|
|
return { useMultiAgent: false, reason: 'Multi-agent mode disabled' };
|
|
}
|
|
|
|
const requestLower = request.toLowerCase();
|
|
|
|
// Patterns that suggest multi-agent mode
|
|
const patterns = {
|
|
multiDiscipline: /\b(frontend|backend|database|api|ui|server|client)\b.*\b(and|with|plus)\b.*\b(frontend|backend|database|api|ui|server|client)\b/i,
|
|
highRisk: /\b(auth|authentication|authorization|permission|password|secret|token|security)\b/i,
|
|
largeRefactor: /\b(refactor|rewrite|restructure|reorganize)\b.*\b(entire|whole|all|complete)\b/i,
|
|
needsReview: /\b(review|check|verify|validate|audit)\b.*\b(security|code|implementation)\b/i,
|
|
needsPlanning: /\b(plan|design|architect|strategy)\b.*\b(before|first|then)\b/i,
|
|
needsTests: /\b(test|coverage|tdd|unit test|integration)\b/i,
|
|
needsDocs: /\b(document|readme|api docs|comments)\b/i
|
|
};
|
|
|
|
const detectedPatterns = [];
|
|
const suggestedAgents = new Set(['build']); // Always include build
|
|
|
|
if (patterns.multiDiscipline.test(requestLower)) {
|
|
detectedPatterns.push('multi-discipline');
|
|
suggestedAgents.add('plan');
|
|
}
|
|
if (patterns.highRisk.test(requestLower)) {
|
|
detectedPatterns.push('security-sensitive');
|
|
suggestedAgents.add('security');
|
|
}
|
|
if (patterns.largeRefactor.test(requestLower)) {
|
|
detectedPatterns.push('large-refactor');
|
|
suggestedAgents.add('refactor');
|
|
suggestedAgents.add('plan');
|
|
}
|
|
if (patterns.needsReview.test(requestLower)) {
|
|
detectedPatterns.push('needs-review');
|
|
suggestedAgents.add('security');
|
|
}
|
|
if (patterns.needsPlanning.test(requestLower)) {
|
|
detectedPatterns.push('needs-planning');
|
|
suggestedAgents.add('plan');
|
|
}
|
|
if (patterns.needsTests.test(requestLower)) {
|
|
detectedPatterns.push('needs-tests');
|
|
suggestedAgents.add('test');
|
|
}
|
|
if (patterns.needsDocs.test(requestLower)) {
|
|
detectedPatterns.push('needs-docs');
|
|
suggestedAgents.add('docs');
|
|
}
|
|
|
|
// Use multi-agent if more than one pattern detected
|
|
const useMultiAgent = suggestedAgents.size > 1 && detectedPatterns.length > 0;
|
|
|
|
return {
|
|
useMultiAgent,
|
|
reason: useMultiAgent
|
|
? `Detected: ${detectedPatterns.join(', ')}`
|
|
: 'Single-agent sufficient for this request',
|
|
suggestedAgents: Array.from(suggestedAgents).slice(0, this.config.maxAgentsPerRequest),
|
|
patterns: detectedPatterns
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Start multi-agent flow for a request
|
|
*/
|
|
startFlow(agentIds) {
|
|
this.activeAgents = agentIds.map(id => this.agents[id]).filter(Boolean);
|
|
this.agentOutputs = [];
|
|
return this.activeAgents;
|
|
}
|
|
|
|
/**
|
|
* Record agent output
|
|
*/
|
|
recordOutput(agentId, output) {
|
|
this.agentOutputs.push({
|
|
agentId,
|
|
agent: this.agents[agentId],
|
|
output,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get summary for UI display
|
|
*/
|
|
getFlowStatus() {
|
|
return {
|
|
active: this.activeAgents.length > 0,
|
|
agents: this.activeAgents.map(a => ({
|
|
id: a.id,
|
|
name: a.name,
|
|
role: a.role
|
|
})),
|
|
outputs: this.agentOutputs.length,
|
|
enabled: this.config.enabled
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build context for model about available agents
|
|
*/
|
|
buildAgentContext() {
|
|
const agents = this.getAgents();
|
|
let context = `\n## Available Agents\n\nYou have access to the following specialized agents:\n\n`;
|
|
|
|
for (const agent of agents) {
|
|
context += `### ${agent.name} (${agent.id})\n`;
|
|
context += `- **Role**: ${agent.role}\n`;
|
|
context += `- **Capabilities**: ${agent.capabilities.join(', ')}\n`;
|
|
context += `- **When to use**: ${agent.whenToUse}\n\n`;
|
|
}
|
|
|
|
context += `## Multi-Agent Guidelines\n\n`;
|
|
context += `Use multiple agents when:\n`;
|
|
context += `- The request spans multiple disciplines (UI + backend + DB + deployment)\n`;
|
|
context += `- Risk is high (auth, permissions, data loss)\n`;
|
|
context += `- Large refactor needed and you want a review pass\n\n`;
|
|
context += `Do NOT use multiple agents when:\n`;
|
|
context += `- Small changes or trivial questions\n`;
|
|
context += `- User asked for speed or minimal output\n`;
|
|
context += `- No clear benefit from additional perspectives\n`;
|
|
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* Toggle multi-agent mode
|
|
*/
|
|
toggle(enabled = null) {
|
|
if (enabled === null) {
|
|
this.config.enabled = !this.config.enabled;
|
|
} else {
|
|
this.config.enabled = enabled;
|
|
}
|
|
return this.config.enabled;
|
|
}
|
|
|
|
/**
|
|
* Reset flow state
|
|
*/
|
|
reset() {
|
|
this.activeAgents = [];
|
|
this.agentOutputs = [];
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let smartAgentFlowInstance = null;
|
|
|
|
export function getSmartAgentFlow(config) {
|
|
if (!smartAgentFlowInstance) {
|
|
smartAgentFlowInstance = new SmartAgentFlow(config);
|
|
}
|
|
return smartAgentFlowInstance;
|
|
}
|
|
|
|
export default SmartAgentFlow;
|