/** * Multi-Perspective Code Review * * Demonstrates: * - Dependency chain: generator produces code, three reviewers depend on it * - Parallel execution: security, performance, and style reviewers run concurrently * - Structured output: synthesizer returns a Zod-validated list of findings * - Shared memory: each agent's output is automatically stored and injected * into downstream agents' prompts by the framework * * Flow: * generator → [security-reviewer, performance-reviewer, style-reviewer] (parallel) → synthesizer * * Run: * npx tsx examples/patterns/multi-perspective-code-review.ts * * Prerequisites: * If LLM_PROVIDER is unset, this example auto-selects the first available key * in this fixed order: Gemini → Groq → OpenRouter → Anthropic. * This precedence is this example's implementation choice for satisfying * "default to whichever key is present". * Override with LLM_PROVIDER=gemini|groq|openrouter|anthropic. * * Supported env vars: * - Gemini: GEMINI_API_KEY, GOOGLE_API_KEY, or GOOGLE_AI_STUDIO_API_KEY * - Groq: GROQ_API_KEY * - OpenRouter: OPENROUTER_API_KEY * - Anthropic: ANTHROPIC_API_KEY * * Anthropic support is kept for backward compatibility with the original * example. It is not part of the free-provider path. */ import { z } from 'zod' import { OpenMultiAgent } from '../../src/index.js' import type { AgentConfig, OrchestratorEvent } from '../../src/types.js' // --------------------------------------------------------------------------- // API spec to implement // --------------------------------------------------------------------------- const API_SPEC = `POST /users endpoint that: - Accepts JSON body with name (string, required), email (string, required), age (number, optional) - Validates all fields - Inserts into a PostgreSQL database - Returns 201 with the created user or 400/500 on error` // --------------------------------------------------------------------------- // Structured output schema // --------------------------------------------------------------------------- const ReviewFinding = z.object({ priority: z.enum(['critical', 'high', 'medium', 'low']), category: z.enum(['security', 'performance', 'style']), issue: z.string().describe('A concise description of the code review finding'), fix_hint: z.string().describe('A short, actionable suggestion for fixing the issue'), }) const ReviewFindings = z.array(ReviewFinding) type ProviderId = 'anthropic' | 'gemini' | 'groq' | 'openrouter' type ProviderConfig = Pick // --------------------------------------------------------------------------- // Provider resolution // --------------------------------------------------------------------------- function getGeminiApiKey(): string | undefined { return ( process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY ?? process.env.GOOGLE_AI_STUDIO_API_KEY ) } function inferProvider(): ProviderId { if (getGeminiApiKey()) return 'gemini' if (process.env.GROQ_API_KEY) return 'groq' if (process.env.OPENROUTER_API_KEY) return 'openrouter' if (process.env.ANTHROPIC_API_KEY) return 'anthropic' throw new Error( 'No supported API key found. Set GEMINI_API_KEY / GOOGLE_API_KEY / GOOGLE_AI_STUDIO_API_KEY, ' + 'GROQ_API_KEY, OPENROUTER_API_KEY, or ANTHROPIC_API_KEY.', ) } function getSelectedProvider(): ProviderId { const requested = process.env.LLM_PROVIDER?.trim().toLowerCase() if (!requested) return inferProvider() if ( requested === 'anthropic' || requested === 'gemini' || requested === 'groq' || requested === 'openrouter' ) { return requested } throw new Error( `Unsupported LLM_PROVIDER="${process.env.LLM_PROVIDER}". ` + 'Use one of: gemini, groq, openrouter, anthropic.', ) } function getProviderConfigs(provider: ProviderId): { defaultModel: string defaultProvider: 'anthropic' | 'gemini' | 'openai' fast: ProviderConfig strong: ProviderConfig } { switch (provider) { case 'gemini': { const apiKey = getGeminiApiKey() if (!apiKey) { throw new Error( 'LLM_PROVIDER=gemini requires GEMINI_API_KEY, GOOGLE_API_KEY, or GOOGLE_AI_STUDIO_API_KEY.', ) } return { defaultModel: 'gemini-2.5-flash', defaultProvider: 'gemini', fast: { provider: 'gemini', model: 'gemini-2.5-flash', apiKey, }, strong: { provider: 'gemini', model: 'gemini-2.5-flash', apiKey, }, } } case 'groq': { const apiKey = process.env.GROQ_API_KEY if (!apiKey) { throw new Error('LLM_PROVIDER=groq requires GROQ_API_KEY.') } return { defaultModel: 'llama-3.3-70b-versatile', defaultProvider: 'openai', fast: { provider: 'openai', model: 'llama-3.3-70b-versatile', apiKey, baseURL: 'https://api.groq.com/openai/v1', }, strong: { provider: 'openai', model: 'llama-3.3-70b-versatile', apiKey, baseURL: 'https://api.groq.com/openai/v1', }, } } case 'openrouter': { const apiKey = process.env.OPENROUTER_API_KEY if (!apiKey) { throw new Error('LLM_PROVIDER=openrouter requires OPENROUTER_API_KEY.') } return { defaultModel: 'google/gemini-2.5-flash', defaultProvider: 'openai', fast: { provider: 'openai', model: 'google/gemini-2.5-flash', apiKey, baseURL: 'https://openrouter.ai/api/v1', }, strong: { provider: 'openai', model: 'google/gemini-2.5-flash', apiKey, baseURL: 'https://openrouter.ai/api/v1', }, } } case 'anthropic': default: if (!process.env.ANTHROPIC_API_KEY) { throw new Error('LLM_PROVIDER=anthropic requires ANTHROPIC_API_KEY.') } return { defaultModel: 'claude-sonnet-4-6', defaultProvider: 'anthropic', fast: { provider: 'anthropic', model: 'claude-sonnet-4-6', }, strong: { provider: 'anthropic', model: 'claude-sonnet-4-6', }, } } } const selectedProvider = getSelectedProvider() const providerConfigs = getProviderConfigs(selectedProvider) // --------------------------------------------------------------------------- // Agents // --------------------------------------------------------------------------- const generator: AgentConfig = { name: 'generator', ...providerConfigs.fast, systemPrompt: `You are a Node.js backend developer. Given an API spec, write a complete Express route handler. Include imports, validation, database query, and error handling. Output only the code, no explanation. Keep it under 80 lines.`, maxTurns: 2, } const securityReviewer: AgentConfig = { name: 'security-reviewer', ...providerConfigs.fast, systemPrompt: `You are a security reviewer. Review the code provided in context and check for OWASP top 10 vulnerabilities: SQL injection, XSS, broken authentication, sensitive data exposure, etc. Write your findings as a markdown checklist. Keep it to 150-200 words.`, maxTurns: 2, } const performanceReviewer: AgentConfig = { name: 'performance-reviewer', ...providerConfigs.fast, systemPrompt: `You are a performance reviewer. Review the code provided in context and check for N+1 queries, memory leaks, blocking calls, missing connection pooling, and inefficient patterns. Write your findings as a markdown checklist. Keep it to 150-200 words.`, maxTurns: 2, } const styleReviewer: AgentConfig = { name: 'style-reviewer', ...providerConfigs.fast, systemPrompt: `You are a code style reviewer. Review the code provided in context and check naming conventions, function structure, readability, error message clarity, and consistency. Write your findings as a markdown checklist. Keep it to 150-200 words.`, maxTurns: 2, } const synthesizer: AgentConfig = { name: 'synthesizer', ...providerConfigs.strong, systemPrompt: `You are a lead engineer synthesizing code review feedback. Review all the feedback and original code provided in context. Produce a deduplicated list of code review findings as JSON. Rules: - Output ONLY a JSON array matching the provided schema. - Merge overlapping reviewer comments into a single finding when they describe the same issue. - Use category "security", "performance", or "style" only. - Use priority "critical", "high", "medium", or "low" only. - issue should describe the problem, not the fix. - fix_hint should be specific and actionable. - If the code looks clean, return an empty JSON array.`, maxTurns: 2, outputSchema: ReviewFindings, } // --------------------------------------------------------------------------- // Orchestrator + team // --------------------------------------------------------------------------- function handleProgress(event: OrchestratorEvent): void { if (event.type === 'task_start') { console.log(` [START] ${event.task ?? '?'} → ${event.agent ?? '?'}`) } if (event.type === 'task_complete') { const success = (event.data as { success?: boolean })?.success ?? true console.log(` [DONE] ${event.task ?? '?'} (${success ? 'OK' : 'FAIL'})`) } } const orchestrator = new OpenMultiAgent({ defaultModel: providerConfigs.defaultModel, defaultProvider: providerConfigs.defaultProvider, onProgress: handleProgress, }) const team = orchestrator.createTeam('code-review-team', { name: 'code-review-team', agents: [generator, securityReviewer, performanceReviewer, styleReviewer, synthesizer], sharedMemory: true, }) // --------------------------------------------------------------------------- // Tasks // --------------------------------------------------------------------------- const tasks = [ { title: 'Generate code', description: `Write a Node.js Express route handler for this API spec:\n\n${API_SPEC}`, assignee: 'generator', }, { title: 'Security review', description: 'Review the generated code for security vulnerabilities.', assignee: 'security-reviewer', dependsOn: ['Generate code'], }, { title: 'Performance review', description: 'Review the generated code for performance issues.', assignee: 'performance-reviewer', dependsOn: ['Generate code'], }, { title: 'Style review', description: 'Review the generated code for style and readability.', assignee: 'style-reviewer', dependsOn: ['Generate code'], }, { title: 'Synthesize feedback', description: 'Synthesize all review feedback and the original code into a unified, prioritized structured findings array.', assignee: 'synthesizer', dependsOn: ['Security review', 'Performance review', 'Style review'], }, ] // --------------------------------------------------------------------------- // Run // --------------------------------------------------------------------------- console.log('Multi-Perspective Code Review') console.log('='.repeat(60)) console.log(`Provider: ${selectedProvider}`) console.log(`Spec: ${API_SPEC.split('\n')[0]}`) console.log('Pipeline: generator → 3 reviewers (parallel) → synthesizer') console.log('='.repeat(60)) console.log() const result = await orchestrator.runTasks(team, tasks) // --------------------------------------------------------------------------- // Output // --------------------------------------------------------------------------- console.log('\n' + '='.repeat(60)) console.log(`Overall success: ${result.success}`) console.log(`Tokens — input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`) console.log() for (const [name, r] of result.agentResults) { const icon = r.success ? 'OK ' : 'FAIL' const tokens = `in:${r.tokenUsage.input_tokens} out:${r.tokenUsage.output_tokens}` console.log(` [${icon}] ${name.padEnd(22)} ${tokens}`) } const synthResult = result.agentResults.get('synthesizer') if (synthResult?.structured) { console.log('\n' + '='.repeat(60)) console.log('STRUCTURED REVIEW FINDINGS') console.log('='.repeat(60)) console.log() console.log(JSON.stringify(synthResult.structured, null, 2)) } else if (synthResult) { console.log('\n' + '='.repeat(60)) console.log('SYNTHESIZER OUTPUT FAILED SCHEMA VALIDATION OR DID NOT PRODUCE VALID JSON') console.log('='.repeat(60)) console.log() console.log(synthResult.output.slice(0, 1200)) } console.log('\nDone.')