The problem
A useful meeting record requires several kinds of reading. Asking one pass to summarize, extract commitments, and judge tone can blur those objectives and slow the result.
Transform one transcript into a summary, structured action items, sentiment, and a single Markdown report.
Useful forTeams turning meeting transcripts into useful follow-up
A useful meeting record requires several kinds of reading. Asking one pass to summarize, extract commitments, and judge tone can blur those objectives and slow the result.
The same transcript is shared with three specialists, each with a different responsibility.
Each specialist sees the evidence it needs. Independent work runs together; dependent work waits for the right inputs.
Each specialist works on the same transcript at the same time.
Captures the main discussion and decisions.
Returns schema-validated owners and follow-up work.
Returns a structured view of meeting sentiment.
The aggregator waits for every specialist result.
Merges the three perspectives into one Markdown report.
The example also compares parallel wall-clock time with the sum of the individual agent durations.
The business view above is editorial. The commands, prerequisites, APIs, and source below stay synchronized with the real repository.
AgentAgentPoolToolRegistryToolExecutorregisterBuiltInTools From a clone of the repo — this exact file:
npx tsx packages/core/examples/cookbook/meeting-summarizer.ts
OMA is provider-agnostic — this example is written for the key above, but you can run it on OpenAI, Gemini, Groq and others. All providers →
The complete example, synchronized from the pinned Framework commit.
/*** Meeting Summarizer (Parallel Post-Processing)** Demonstrates:* - Fan-out of three specialized agents on the same meeting transcript* - Structured output (Zod schemas) for action items and sentiment* - Parallel timing check: wall time vs sum of per-agent durations* - Aggregator merges into a single Markdown report** Run:* npx tsx packages/core/examples/cookbook/meeting-summarizer.ts** Prerequisites:* ANTHROPIC_API_KEY env var must be set.*/import { readFileSync } from 'node:fs'import { fileURLToPath } from 'node:url'import path from 'node:path'import { z } from 'zod'import { Agent, AgentPool, ToolRegistry, ToolExecutor, registerBuiltInTools } from '../../src/index.js'import type { AgentConfig, AgentRunResult } from '../../src/types.js'// ---------------------------------------------------------------------------// Load the transcript fixture// ---------------------------------------------------------------------------const __dirname = path.dirname(fileURLToPath(import.meta.url))const TRANSCRIPT = readFileSync(path.join(__dirname, '../fixtures/meeting-transcript.txt'),'utf-8',)// ---------------------------------------------------------------------------// Zod schemas for structured agents// ---------------------------------------------------------------------------const ActionItemList = z.object({items: z.array(z.object({task: z.string().describe('The action to be taken'),owner: z.string().describe('Name of the person responsible'),due_date: z.string().optional().describe('ISO date or human-readable due date if mentioned'),}),),})type ActionItemList = z.infer<typeof ActionItemList>const SentimentReport = z.object({participants: z.array(z.object({participant: z.string().describe('Name as it appears in the transcript'),tone: z.enum(['positive', 'neutral', 'negative', 'mixed']),evidence: z.string().describe('Direct quote or brief paraphrase supporting the tone'),}),),})type SentimentReport = z.infer<typeof SentimentReport>// ---------------------------------------------------------------------------// Agent configs// ---------------------------------------------------------------------------const summaryConfig: AgentConfig = {name: 'summary',model: 'claude-sonnet-4-6',systemPrompt: `You are a meeting note-taker. Given a transcript, produce athree-paragraph summary:1. What was discussed (the agenda).2. Decisions made.3. Notable context or risk the team should remember.Plain prose. No bullet points. 200-300 words total.`,maxTurns: 1,temperature: 0.3,}const actionItemsConfig: AgentConfig = {name: 'action-items',model: 'claude-sonnet-4-6',systemPrompt: `You extract action items from meeting transcripts. An actionitem is a concrete task with a clear owner. Skip vague intentions ("we shouldthink about X"). Include due dates only when the speaker named one explicitly.Return JSON matching the schema.`,maxTurns: 1,temperature: 0.1,outputSchema: ActionItemList,}const sentimentConfig: AgentConfig = {name: 'sentiment',model: 'claude-sonnet-4-6',systemPrompt: `You analyze the tone of each participant in a meeting. Forevery named speaker, classify their overall tone as positive, neutral,negative, or mixed, and include one short quote or paraphrase as evidence.Return JSON matching the schema.`,maxTurns: 1,temperature: 0.2,outputSchema: SentimentReport,}const aggregatorConfig: AgentConfig = {name: 'aggregator',model: 'claude-sonnet-4-6',systemPrompt: `You are a report writer. You receive three pre-computedanalyses of the same meeting: a summary, an action-item list, and a sentimentreport. Your job is to merge them into a single Markdown report.Output structure — use exactly these four H2 headings, in order:## Summary## Action Items## Sentiment## Next StepsUnder "Action Items" render a Markdown table with columns: Task, Owner, Due.Under "Sentiment" render one bullet per participant.Under "Next Steps" synthesize 3-5 concrete follow-ups based on the othersections. Do not invent action items that are not grounded in the other data.`,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 summary = buildAgent(summaryConfig)const actionItems = buildAgent(actionItemsConfig)const sentiment = buildAgent(sentimentConfig)const aggregator = buildAgent(aggregatorConfig)const pool = new AgentPool(3) // three specialists can run concurrentlypool.add(summary)pool.add(actionItems)pool.add(sentiment)pool.add(aggregator)console.log('Meeting Summarizer (Parallel Post-Processing)')console.log('='.repeat(60))console.log(`\nTranscript: ${TRANSCRIPT.split('\n')[0]}`)console.log(`Length: ${TRANSCRIPT.split(/\s+/).length} words\n`)// ---------------------------------------------------------------------------// Step 1: Parallel fan-out with per-agent timing// ---------------------------------------------------------------------------console.log('[Step 1] Running 3 agents in parallel...\n')const specialists = ['summary', 'action-items', 'sentiment'] as const// Kick off all three concurrently and record each one's own wall duration.// Sum-of-per-agent beats a separate serial pass: half the LLM cost, and the// sum is the work parallelism saved.const parallelStart = performance.now()const timed = await Promise.all(specialists.map(async (name) => {const t = performance.now()const result = await pool.run(name, TRANSCRIPT)return { name, result, durationMs: performance.now() - t }}),)const parallelElapsed = performance.now() - parallelStartconst byName = new Map<string, AgentRunResult>()const serialSum = timed.reduce((acc, r) => {byName.set(r.name, r.result)return acc + r.durationMs}, 0)for (const { name, result, durationMs } of timed) {const status = result.success ? 'OK' : 'FAILED'console.log(` ${name.padEnd(14)} [${status}] — ${Math.round(durationMs)}ms, ${result.tokenUsage.output_tokens} out tokens`,)}console.log()for (const { name, result } of timed) {if (!result.success) {console.error(`Specialist '${name}' failed: ${result.output}`)process.exit(1)}}const actionData = byName.get('action-items')!.structured as ActionItemList | undefinedconst sentimentData = byName.get('sentiment')!.structured as SentimentReport | undefinedif (!actionData || !sentimentData) {console.error('Structured output missing: action-items or sentiment failed schema validation')process.exit(1)}// ---------------------------------------------------------------------------// Step 2: Parallelism assertion// ---------------------------------------------------------------------------console.log('[Step 2] Parallelism check')console.log(` Parallel wall time: ${Math.round(parallelElapsed)}ms`)console.log(` Serial sum (per-agent): ${Math.round(serialSum)}ms`)console.log(` Speedup: ${(serialSum / parallelElapsed).toFixed(2)}x\n`)if (parallelElapsed >= serialSum * 0.7) {console.error(`ASSERTION FAILED: parallel wall time (${Math.round(parallelElapsed)}ms) is not ` +`less than 70% of serial sum (${Math.round(serialSum)}ms). Expected substantial ` +`speedup from fan-out.`,)process.exit(1)}// ---------------------------------------------------------------------------// Step 3: Aggregate into Markdown report// ---------------------------------------------------------------------------console.log('[Step 3] Aggregating into Markdown report...\n')const aggregatorPrompt = `Merge the three analyses below into a single Markdown report.--- SUMMARY (prose) ---${byName.get('summary')!.output}--- ACTION ITEMS (JSON) ---${JSON.stringify(actionData, null, 2)}--- SENTIMENT (JSON) ---${JSON.stringify(sentimentData, null, 2)}Produce the Markdown report per the system instructions.`const reportResult = await pool.run('aggregator', aggregatorPrompt)if (!reportResult.success) {console.error('Aggregator failed:', reportResult.output)process.exit(1)}// ---------------------------------------------------------------------------// Final output// ---------------------------------------------------------------------------console.log('='.repeat(60))console.log('MEETING REPORT')console.log('='.repeat(60))console.log()console.log(reportResult.output)console.log()console.log('-'.repeat(60))// ---------------------------------------------------------------------------// Token usage summary// ---------------------------------------------------------------------------console.log('\nToken Usage Summary:')console.log('-'.repeat(60))let totalInput = 0let totalOutput = 0for (const { name, result } of timed) {totalInput += result.tokenUsage.input_tokenstotalOutput += result.tokenUsage.output_tokensconsole.log(` ${name.padEnd(14)} — input: ${result.tokenUsage.input_tokens}, output: ${result.tokenUsage.output_tokens}`,)}totalInput += reportResult.tokenUsage.input_tokenstotalOutput += reportResult.tokenUsage.output_tokensconsole.log(` ${'aggregator'.padEnd(14)} — input: ${reportResult.tokenUsage.input_tokens}, output: ${reportResult.tokenUsage.output_tokens}`,)console.log('-'.repeat(60))console.log(` ${'TOTAL'.padEnd(14)} — input: ${totalInput}, output: ${totalOutput}`)console.log('\nDone.')
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.