← All examplesEnterprise support
// Orchestration
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
01 Run it
OMA APIs
OpenMultiAgentFrom a clone of the repo — this exact file:
npx tsx packages/core/examples/patterns/research-aggregation.ts "<topic>"
Open the complete, synchronized source · 315 lines
The complete example, synchronized from the pinned Framework commit.
/*** 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 packages/core/examples/patterns/research-aggregation.ts "<topic>"** 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: ProviderChoicemodel: stringprovider: NonNullable<AgentConfig['provider']>baseURL?: stringapiKey?: string} {const raw = (process.env.LLM_PROVIDER ?? 'anthropic').toLowerCase() as ProviderChoiceconst modelOverride = process.env.LLM_MODELswitch (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 names3. 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.')
// Learn the concepts
// Enterprise
Taking this to production?
open-multi-agent is MIT-licensed and free to run yourself. When you need it delivered, integrated, or supported on a deadline, 元定义科技 (YuanASI) offers commercial delivery and support.