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/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/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/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
|
## 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