diff --git a/examples/11-code-review.ts b/examples/11-code-review.ts new file mode 100644 index 0000000..df85d98 --- /dev/null +++ b/examples/11-code-review.ts @@ -0,0 +1,238 @@ +/** + * Example 11 — Multi-Perspective Code Review + * + * Demonstrates: + * - Dependency chain: reviewer agents depend on generator output + * - Fan-out: three review agents run in parallel, each with a focused lens + * - Aggregation: a synthesizer merges feedback into prioritized action items + * + * Flow: + * generator -> [security-reviewer, performance-reviewer, style-reviewer] -> synthesizer + * + * Run: + * npx tsx examples/11-code-review.ts + * + * Prerequisites: + * ANTHROPIC_API_KEY env var must be set. + */ + +import { Agent, AgentPool, ToolRegistry, ToolExecutor, registerBuiltInTools } from '../src/index.js' +import type { AgentConfig, AgentRunResult } from '../src/types.js' + +// --------------------------------------------------------------------------- +// Spec for the code generator +// --------------------------------------------------------------------------- + +const SPEC = `Write a Node.js HTTP handler (using the built-in http module, no frameworks) +that accepts POST /users, reads a JSON body with { name, email }, stores it in +a global in-memory array, and returns 201 with the created user object including +a generated id. Include basic error handling.` + +// --------------------------------------------------------------------------- +// Agent configs +// --------------------------------------------------------------------------- + +const generatorConfig: AgentConfig = { + name: 'generator', + model: 'claude-sonnet-4-6', + systemPrompt: `You are a backend developer. Given a spec, produce working +Node.js code. Output ONLY the code — no explanations, no markdown fences.`, + maxTurns: 1, + temperature: 0.2, +} + +const securityReviewerConfig: AgentConfig = { + name: 'security-reviewer', + model: 'claude-sonnet-4-6', + systemPrompt: `You are a security engineer reviewing code for OWASP top 10 +vulnerabilities: injection, broken auth, sensitive data exposure, XXE, broken +access control, misconfig, XSS, insecure deserialization, known-vuln components, +and insufficient logging. For each finding, state the category, severity +(critical/high/medium/low), the offending line or pattern, and a fix. +Keep your review to 200-300 words.`, + maxTurns: 1, + temperature: 0.3, +} + +const performanceReviewerConfig: AgentConfig = { + name: 'performance-reviewer', + model: 'claude-sonnet-4-6', + systemPrompt: `You are a performance engineer reviewing code for N+1 queries, +memory leaks, blocking calls on the event loop, unbounded data structures, +missing timeouts, and inefficient algorithms. For each finding, state the +issue, impact (high/medium/low), the offending pattern, and a fix. +Keep your review to 200-300 words.`, + maxTurns: 1, + temperature: 0.3, +} + +const styleReviewerConfig: AgentConfig = { + name: 'style-reviewer', + model: 'claude-sonnet-4-6', + systemPrompt: `You are a senior developer reviewing code for naming clarity, +function length, single-responsibility violations, error message quality, +missing JSDoc, inconsistent formatting, and readability. For each finding, +state the issue, severity (nit/suggestion/important), and a fix. +Keep your review to 200-300 words.`, + maxTurns: 1, + temperature: 0.3, +} + +const synthesizerConfig: AgentConfig = { + name: 'synthesizer', + model: 'claude-sonnet-4-6', + systemPrompt: `You are a tech lead merging code review feedback from three +reviewers (security, performance, style) into one actionable report. Structure: + +1. Critical issues (must fix before merge) +2. Important issues (fix soon) +3. Suggestions (nice to have) + +For each item, include the reviewer source, a one-line summary, and the fix. +Deduplicate overlapping findings. Keep the report to 300-400 words.`, + maxTurns: 1, + temperature: 0.3, +} + +// --------------------------------------------------------------------------- +// Build agents +// --------------------------------------------------------------------------- + +function buildAgent(config: AgentConfig): Agent { + const registry = new ToolRegistry() + registerBuiltInTools(registry) + const executor = new ToolExecutor(registry) + return new Agent(config, registry, executor) +} + +const generator = buildAgent(generatorConfig) +const securityReviewer = buildAgent(securityReviewerConfig) +const performanceReviewer = buildAgent(performanceReviewerConfig) +const styleReviewer = buildAgent(styleReviewerConfig) +const synthesizer = buildAgent(synthesizerConfig) + +// --------------------------------------------------------------------------- +// Pool setup +// --------------------------------------------------------------------------- + +const pool = new AgentPool(4) +pool.add(generator) +pool.add(securityReviewer) +pool.add(performanceReviewer) +pool.add(styleReviewer) +pool.add(synthesizer) + +console.log('Multi-Perspective Code Review') +console.log('='.repeat(60)) +console.log(`\nSpec: ${SPEC.replace(/\n/g, ' ').trim()}\n`) + +// --------------------------------------------------------------------------- +// Step 1: Generate code +// --------------------------------------------------------------------------- + +console.log('[Step 1] Generating code...\n') + +const genResult = await pool.run('generator', SPEC) + +if (!genResult.success) { + console.error('Generator failed:', genResult.output) + process.exit(1) +} + +console.log('Generated code:') +console.log(genResult.output.slice(0, 300) + '...\n') + +// --------------------------------------------------------------------------- +// Step 2: Fan-out — three reviewers in parallel +// --------------------------------------------------------------------------- + +console.log('[Step 2] Fan-out: 3 reviewers running in parallel...\n') + +const reviewPrompt = `Review the following Node.js code:\n\n${genResult.output}` + +const reviewResults: Map = await pool.runParallel([ + { agent: 'security-reviewer', prompt: reviewPrompt }, + { agent: 'performance-reviewer', prompt: reviewPrompt }, + { agent: 'style-reviewer', prompt: reviewPrompt }, +]) + +const reviewers = ['security-reviewer', 'performance-reviewer', 'style-reviewer'] as const +for (const name of reviewers) { + const result = reviewResults.get(name)! + const status = result.success ? 'OK' : 'FAILED' + console.log(` ${name} [${status}] — ${result.tokenUsage.output_tokens} output tokens`) + console.log(` ${result.output.slice(0, 120).replace(/\n/g, ' ')}...`) + console.log() +} + +for (const name of reviewers) { + if (!reviewResults.get(name)!.success) { + console.error(`Reviewer '${name}' failed: ${reviewResults.get(name)!.output}`) + process.exit(1) + } +} + +// --------------------------------------------------------------------------- +// Step 3: Synthesize reviews +// --------------------------------------------------------------------------- + +console.log('[Step 3] Synthesizer merging feedback...\n') + +const synthPrompt = `Three reviewers have independently reviewed the same code. +Merge their feedback into a single prioritized report. + +--- SECURITY REVIEW --- +${reviewResults.get('security-reviewer')!.output} + +--- PERFORMANCE REVIEW --- +${reviewResults.get('performance-reviewer')!.output} + +--- STYLE REVIEW --- +${reviewResults.get('style-reviewer')!.output} + +Now produce the unified review report.` + +const synthResult = await pool.run('synthesizer', synthPrompt) + +if (!synthResult.success) { + console.error('Synthesizer failed:', synthResult.output) + process.exit(1) +} + +// --------------------------------------------------------------------------- +// Final output +// --------------------------------------------------------------------------- + +console.log('='.repeat(60)) +console.log('UNIFIED CODE REVIEW REPORT') +console.log('='.repeat(60)) +console.log() +console.log(synthResult.output) +console.log() + +// --------------------------------------------------------------------------- +// Token usage +// --------------------------------------------------------------------------- + +console.log('-'.repeat(60)) +console.log('Token Usage Summary:') +console.log('-'.repeat(60)) + +let totalInput = genResult.tokenUsage.input_tokens +let totalOutput = genResult.tokenUsage.output_tokens +console.log(` ${'generator'.padEnd(22)} — input: ${genResult.tokenUsage.input_tokens}, output: ${genResult.tokenUsage.output_tokens}`) + +for (const name of reviewers) { + const r = reviewResults.get(name)! + totalInput += r.tokenUsage.input_tokens + totalOutput += r.tokenUsage.output_tokens + console.log(` ${name.padEnd(22)} — input: ${r.tokenUsage.input_tokens}, output: ${r.tokenUsage.output_tokens}`) +} + +totalInput += synthResult.tokenUsage.input_tokens +totalOutput += synthResult.tokenUsage.output_tokens +console.log(` ${'synthesizer'.padEnd(22)} — input: ${synthResult.tokenUsage.input_tokens}, output: ${synthResult.tokenUsage.output_tokens}`) +console.log('-'.repeat(60)) +console.log(` ${'TOTAL'.padEnd(22)} — input: ${totalInput}, output: ${totalOutput}`) + +console.log('\nDone.')