/** * Multi-Source Research Aggregation * * Demonstrates runTasks() with explicit dependency chains: * - Parallel execution: three analyst agents research the same topic independently * - Dependency chain via dependsOn: synthesizer waits for all analysts to finish * - Automatic shared memory: agent output flows to downstream agents via the framework * * Compare with example 07 (fan-out-aggregate) which uses AgentPool.runParallel() * for the same 3-analysts + synthesizer pattern. This example shows the runTasks() * API with explicit dependsOn declarations instead. * * Flow: * [technical-analyst, market-analyst, community-analyst] (parallel) → synthesizer * * Run: * npx tsx examples/patterns/research-aggregation.ts "" * * Provider selection (env): * - LLM_PROVIDER=anthropic (default) → requires ANTHROPIC_API_KEY * - LLM_PROVIDER=gemini → requires GEMINI_API_KEY (+ optional peer dep @google/genai) * - LLM_PROVIDER=groq → requires GROQ_API_KEY * - LLM_PROVIDER=openrouter → requires OPENROUTER_API_KEY * * Optional: * - LLM_MODEL=... overrides the default model for the selected provider. */ import { z } from 'zod' import { OpenMultiAgent } from '../../src/index.js' import type { AgentConfig, OrchestratorEvent } from '../../src/types.js' // --------------------------------------------------------------------------- // Topic + provider selection // --------------------------------------------------------------------------- const TOPIC = process.argv[2] ?? 'WebAssembly adoption in 2026' type ProviderChoice = 'anthropic' | 'gemini' | 'groq' | 'openrouter' function resolveProvider(): { label: ProviderChoice model: string provider: NonNullable baseURL?: string apiKey?: string } { const raw = (process.env.LLM_PROVIDER ?? 'anthropic').toLowerCase() as ProviderChoice const modelOverride = process.env.LLM_MODEL switch (raw) { case 'gemini': return { label: 'gemini', provider: 'gemini', model: modelOverride ?? 'gemini-2.5-flash' } case 'groq': return { label: 'groq', provider: 'openai', baseURL: 'https://api.groq.com/openai/v1', apiKey: process.env.GROQ_API_KEY, model: modelOverride ?? 'llama-3.3-70b-versatile', } case 'openrouter': return { label: 'openrouter', provider: 'openai', baseURL: 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY, model: modelOverride ?? 'openai/gpt-4o-mini', } case 'anthropic': default: return { label: 'anthropic', provider: 'anthropic', model: modelOverride ?? 'claude-sonnet-4-6' } } } const PROVIDER = resolveProvider() if (PROVIDER.label === 'groq' && !PROVIDER.apiKey) { throw new Error('LLM_PROVIDER=groq requires GROQ_API_KEY') } if (PROVIDER.label === 'openrouter' && !PROVIDER.apiKey) { throw new Error('LLM_PROVIDER=openrouter requires OPENROUTER_API_KEY') } // --------------------------------------------------------------------------- // Output schema (synthesizer) // --------------------------------------------------------------------------- const FindingSchema = z.object({ title: z.string().describe('One-sentence finding'), detail: z.string().describe('2-4 sentence explanation'), analysts: z.array(z.enum(['technical-analyst', 'market-analyst', 'community-analyst'])) .min(1) .describe('Analyst agent names that support this finding'), confidence: z.number().min(0).max(1).describe('0..1 confidence score'), }) const ContradictionSchema = z.object({ claim_a: z.string().describe('Claim from analyst A (quote or tight paraphrase)'), claim_b: z.string().describe('Contradicting claim from analyst B (quote or tight paraphrase)'), analysts: z.tuple([ z.enum(['technical-analyst', 'market-analyst', 'community-analyst']), z.enum(['technical-analyst', 'market-analyst', 'community-analyst']), ]) .describe('Exactly two analyst agent names (must be different)'), }).refine((x) => x.analysts[0] !== x.analysts[1], { message: 'contradictions.analysts must reference two different analysts', path: ['analysts'], }) const ResearchAggregationSchema = z.object({ summary: z.string().describe('High-level executive summary'), findings: z.array(FindingSchema).describe('Key findings extracted from the analyst reports'), contradictions: z.array(ContradictionSchema).describe('Explicit contradictions (may be empty)'), }) // --------------------------------------------------------------------------- // Agents — three analysts + one synthesizer // --------------------------------------------------------------------------- const technicalAnalyst: AgentConfig = { name: 'technical-analyst', model: PROVIDER.model, systemPrompt: `You are a technical analyst. Task: Given a topic, produce a compact report that is easy to cross-reference. Output markdown with EXACT sections: ## Claims (max 6 bullets) Each bullet is one falsifiable technical claim. ## Evidence (max 4 bullets) Concrete examples, benchmarks, or implementation details. Constraints: <= 160 words total. No filler.`, maxTurns: 1, } const marketAnalyst: AgentConfig = { name: 'market-analyst', model: PROVIDER.model, systemPrompt: `You are a market analyst. Output markdown with EXACT sections: ## Claims (max 6 bullets) Adoption, players, market dynamics. ## Evidence (max 4 bullets) Metrics, segments, named companies, or directional estimates. Constraints: <= 160 words total. No filler.`, maxTurns: 1, } const communityAnalyst: AgentConfig = { name: 'community-analyst', model: PROVIDER.model, systemPrompt: `You are a developer community analyst. Output markdown with EXACT sections: ## Claims (max 6 bullets) Sentiment, ecosystem maturity, learning curve, community signals. ## Evidence (max 4 bullets) Tooling, docs, conferences, repos, surveys. Constraints: <= 160 words total. No filler.`, maxTurns: 1, } const synthesizer: AgentConfig = { name: 'synthesizer', model: PROVIDER.model, outputSchema: ResearchAggregationSchema, systemPrompt: `You are a research director. You will receive three analyst reports. Your job: produce ONLY a JSON object matching the required schema. Rules: 1. Extract 3-6 findings. Each finding MUST list the analyst names that support it. 2. Extract contradictions as explicit pairs of claims. Each contradiction MUST: - include claim_a and claim_b copied VERBATIM from the analysts' "## Claims" bullets - include analysts as a 2-item array with the two analyst names 3. contradictions MUST be an array (may be empty). 4. No markdown, no code fences, no extra text. JSON only.`, maxTurns: 2, } // --------------------------------------------------------------------------- // Orchestrator + team // --------------------------------------------------------------------------- function handleProgress(event: OrchestratorEvent): void { if (event.type === 'task_start') { console.log(` [START] ${event.task ?? ''} → ${event.agent ?? ''}`) } if (event.type === 'task_complete') { console.log(` [DONE] ${event.task ?? ''}`) } } const orchestrator = new OpenMultiAgent({ defaultModel: PROVIDER.model, defaultProvider: PROVIDER.provider, ...(PROVIDER.baseURL ? { defaultBaseURL: PROVIDER.baseURL } : {}), ...(PROVIDER.apiKey ? { defaultApiKey: PROVIDER.apiKey } : {}), onProgress: handleProgress, }) const team = orchestrator.createTeam('research-team', { name: 'research-team', agents: [technicalAnalyst, marketAnalyst, communityAnalyst, synthesizer], sharedMemory: true, }) // --------------------------------------------------------------------------- // Tasks — three analysts run in parallel, synthesizer depends on all three // --------------------------------------------------------------------------- const tasks = [ { title: 'Technical analysis', description: `Research the technical aspects of ${TOPIC}. Focus on capabilities, limitations, performance, and architecture.`, assignee: 'technical-analyst', }, { title: 'Market analysis', description: `Research the market landscape for ${TOPIC}. Focus on adoption rates, key players, market size, and competition.`, assignee: 'market-analyst', }, { title: 'Community analysis', description: `Research the developer community around ${TOPIC}. Focus on sentiment, ecosystem maturity, learning resources, and community activity.`, assignee: 'community-analyst', }, { title: 'Synthesize report', description: `Cross-reference all analyst findings, identify key insights, flag contradictions, and produce a unified research report.`, assignee: 'synthesizer', dependsOn: ['Technical analysis', 'Market analysis', 'Community analysis'], }, ] // --------------------------------------------------------------------------- // Run // --------------------------------------------------------------------------- console.log('Multi-Source Research Aggregation') console.log('='.repeat(60)) console.log(`Topic: ${TOPIC}`) console.log(`Provider: ${PROVIDER.label} (model=${PROVIDER.model})`) console.log('Pipeline: 3 analysts (parallel) → synthesizer') console.log('='.repeat(60)) console.log() const result = await orchestrator.runTasks(team, tasks) // --------------------------------------------------------------------------- // Parallelism assertion (analysts should benefit from concurrency) // --------------------------------------------------------------------------- const analystTitles = new Set(['Technical analysis', 'Market analysis', 'Community analysis']) const analystTasks = (result.tasks ?? []).filter((t) => analystTitles.has(t.title)) if ( analystTasks.length === 3 && analystTasks.every((t) => t.metrics?.startMs !== undefined && t.metrics?.endMs !== undefined) ) { const durations = analystTasks.map((t) => Math.max(0, (t.metrics!.endMs - t.metrics!.startMs))) const serialSum = durations.reduce((a, b) => a + b, 0) const minStart = Math.min(...analystTasks.map((t) => t.metrics!.startMs)) const maxEnd = Math.max(...analystTasks.map((t) => t.metrics!.endMs)) const parallelWall = Math.max(0, maxEnd - minStart) // Require parallel wall time < 70% of the serial sum. if (serialSum > 0 && parallelWall >= 0.7 * serialSum) { throw new Error( `Parallelism assertion failed: parallelWall=${parallelWall}ms, serialSum=${serialSum}ms (need < 0.7x). ` + `Tighten analyst prompts or increase concurrency.`, ) } } // --------------------------------------------------------------------------- // 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(20)} ${tokens}`) } const synthResult = result.agentResults.get('synthesizer') if (synthResult?.success) { console.log('\n' + '='.repeat(60)) console.log('SYNTHESIZED OUTPUT (JSON)') console.log('='.repeat(60)) console.log() if (synthResult.structured) { console.log(JSON.stringify(synthResult.structured, null, 2)) } else { // Should not happen when outputSchema succeeds, but keep a fallback. console.log(synthResult.output) } } console.log('\nDone.')