/** * 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 examples/patterns/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 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 // --------------------------------------------------------------------------- // Agent configs // --------------------------------------------------------------------------- const summaryConfig: AgentConfig = { name: 'summary', model: 'claude-sonnet-4-6', systemPrompt: `You are a meeting note-taker. Given a transcript, produce a three-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 action item is a concrete task with a clear owner. Skip vague intentions ("we should think 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. For every 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-computed analyses of the same meeting: a summary, an action-item list, and a sentiment report. 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 Steps Under "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 other sections. 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 concurrently pool.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() - parallelStart const byName = new Map() 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 | undefined const sentimentData = byName.get('sentiment')!.structured as SentimentReport | undefined if (!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 = 0 let totalOutput = 0 for (const { name, result } of timed) { totalInput += result.tokenUsage.input_tokens totalOutput += result.tokenUsage.output_tokens console.log( ` ${name.padEnd(14)} — input: ${result.tokenUsage.input_tokens}, output: ${result.tokenUsage.output_tokens}`, ) } totalInput += reportResult.tokenUsage.input_tokens totalOutput += reportResult.tokenUsage.output_tokens console.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.')