Adds examples/patterns/meeting-summarizer.ts — fan-out pattern
tailored for meeting transcripts. Three specialists run in parallel
on the same transcript:
- summary: three-paragraph prose summary
- action-items: Zod-validated { task, owner, due_date? }[] schema
- sentiment: Zod-validated per-participant tone + evidence
Each specialist's wall duration is recorded, and the script asserts
parallel wall time < 70% of the sum of per-agent durations to prove
fan-out gives a real speedup. An aggregator then merges the three
outputs into a single Markdown report with four H2 sections
(Summary, Action Items, Sentiment, Next Steps).
Ships with examples/fixtures/meeting-transcript.txt — a 350-word
engineering standup transcript designed so the three schemas exercise
cleanly (concrete owners + due dates for action items, a mixed-tone
on-call engineer for sentiment).
Lint and test pass on upstream/main.
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
parent
912e0eae03
commit
9d5345f15d
|
|
@ -44,6 +44,7 @@ Reusable shapes for common multi-agent problems.
|
|||
| [`patterns/multi-perspective-code-review`](patterns/multi-perspective-code-review.ts) | Multiple reviewer agents in parallel, then synthesis. |
|
||||
| [`patterns/research-aggregation`](patterns/research-aggregation.ts) | Multi-source research collated by a synthesis agent. |
|
||||
| [`patterns/agent-handoff`](patterns/agent-handoff.ts) | Synchronous sub-agent delegation via `delegate_to_agent`. |
|
||||
| [`patterns/meeting-summarizer`](patterns/meeting-summarizer.ts) | Fan-out post-processing of a transcript into summary, structured action items, and sentiment. |
|
||||
|
||||
## integrations — external systems
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
Weekly Engineering Standup — 2026-04-18
|
||||
Attendees: Maya (Eng Manager), Raj (Senior Backend), Priya (Frontend Lead), Dan (SRE)
|
||||
|
||||
Maya: Quick round-table. Raj, where are we on the billing-v2 migration?
|
||||
Raj: Cutover is scheduled for Tuesday the 28th. I want to get the shadow-write harness deployed by Friday so we have a full weekend of production traffic comparisons before the cutover. I'll own that. Concerned about the reconciliation query taking 40 seconds on the biggest accounts; I'll look into adding a covering index before cutover.
|
||||
|
||||
Maya: Good. Priya, the checkout redesign?
|
||||
Priya: Ship-ready. I finished the accessibility audit yesterday, all high-priority items landed. Two medium items on the backlog I'll tackle next sprint. Planning to flip the feature flag for 5% of traffic on Thursday the 23rd and ramp from there. I've been heads-down on this for three weeks and honestly feeling pretty good about where it landed.
|
||||
|
||||
Maya: Great. Dan, Sunday's incident — what's the status on the retro?
|
||||
Dan: Retro doc is up. Root cause was the failover script assuming a single-region topology after we moved to multi-region in Q1. The script hasn't been exercised in production since February. I'm frustrated that nobody caught it in review — the change was obvious if you read the diff, but it's twenty pages of YAML. I'm going to propose a rule that multi-region changes need a second reviewer on the SRE team. That's an action for me before the next postmortem, I'll have it drafted by Monday the 27th.
|
||||
|
||||
Maya: Reasonable. Anything else? Dan, how are you holding up? You've been on call a lot.
|
||||
Dan: Honestly? Tired. The back-to-back incidents took the wind out of me. I'd like to hand off primary next rotation. I'll work with Raj on the handoff doc.
|
||||
|
||||
Maya: Noted. Let's make that happen. Priya, anything blocking you?
|
||||
Priya: Nope, feeling good.
|
||||
|
||||
Raj: Just flagging — I saw the Slack thread about the authz refactor. If we're doing that this quarter, it conflicts with billing-v2 timelines. Can we park it until May?
|
||||
|
||||
Maya: Yes, I'll follow up with Len and reply in the thread. Thanks everyone.
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
/**
|
||||
* 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<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 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<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 | 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.')
|
||||
Loading…
Reference in New Issue