Compare commits
9 Commits
dc8cbe0262
...
03dc897929
| Author | SHA1 | Date |
|---|---|---|
|
|
03dc897929 | |
|
|
cb11020c65 | |
|
|
91494bcca9 | |
|
|
faf24aaffa | |
|
|
d8c3808851 | |
|
|
54bfe2ed2d | |
|
|
40f13a09a6 | |
|
|
0fd18d8a19 | |
|
|
34ca8602d0 |
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* Multi-Perspective Code Review
|
||||
*
|
||||
* Demonstrates:
|
||||
* - Dependency chain: generator produces code, three reviewers depend on it
|
||||
* - Parallel execution: security, performance, and style reviewers run concurrently
|
||||
* - Shared memory: each agent's output is automatically stored and injected
|
||||
* into downstream agents' prompts by the framework
|
||||
*
|
||||
* Flow:
|
||||
* generator → [security-reviewer, performance-reviewer, style-reviewer] (parallel) → synthesizer
|
||||
*
|
||||
* Run:
|
||||
* npx tsx examples/14-multi-perspective-code-review.ts
|
||||
*
|
||||
* Prerequisites:
|
||||
* ANTHROPIC_API_KEY env var must be set.
|
||||
*/
|
||||
|
||||
import { OpenMultiAgent } from '../src/index.js'
|
||||
import type { AgentConfig, OrchestratorEvent } from '../src/types.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API spec to implement
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const API_SPEC = `POST /users endpoint that:
|
||||
- Accepts JSON body with name (string, required), email (string, required), age (number, optional)
|
||||
- Validates all fields
|
||||
- Inserts into a PostgreSQL database
|
||||
- Returns 201 with the created user or 400/500 on error`
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const generator: AgentConfig = {
|
||||
name: 'generator',
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: `You are a Node.js backend developer. Given an API spec, write a complete
|
||||
Express route handler. Include imports, validation, database query, and error handling.
|
||||
Output only the code, no explanation. Keep it under 80 lines.`,
|
||||
maxTurns: 2,
|
||||
}
|
||||
|
||||
const securityReviewer: AgentConfig = {
|
||||
name: 'security-reviewer',
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: `You are a security reviewer. Review the code provided in context and check
|
||||
for OWASP top 10 vulnerabilities: SQL injection, XSS, broken authentication,
|
||||
sensitive data exposure, etc. Write your findings as a markdown checklist.
|
||||
Keep it to 150-200 words.`,
|
||||
maxTurns: 2,
|
||||
}
|
||||
|
||||
const performanceReviewer: AgentConfig = {
|
||||
name: 'performance-reviewer',
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: `You are a performance reviewer. Review the code provided in context and check
|
||||
for N+1 queries, memory leaks, blocking calls, missing connection pooling, and
|
||||
inefficient patterns. Write your findings as a markdown checklist.
|
||||
Keep it to 150-200 words.`,
|
||||
maxTurns: 2,
|
||||
}
|
||||
|
||||
const styleReviewer: AgentConfig = {
|
||||
name: 'style-reviewer',
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: `You are a code style reviewer. Review the code provided in context and check
|
||||
naming conventions, function structure, readability, error message clarity, and
|
||||
consistency. Write your findings as a markdown checklist.
|
||||
Keep it to 150-200 words.`,
|
||||
maxTurns: 2,
|
||||
}
|
||||
|
||||
const synthesizer: AgentConfig = {
|
||||
name: 'synthesizer',
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: `You are a lead engineer synthesizing code review feedback. Review all
|
||||
the feedback and original code provided in context. Produce a unified report with:
|
||||
|
||||
1. Critical issues (must fix before merge)
|
||||
2. Recommended improvements (should fix)
|
||||
3. Minor suggestions (nice to have)
|
||||
|
||||
Deduplicate overlapping feedback. Keep the report to 200-300 words.`,
|
||||
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') {
|
||||
const success = (event.data as { success?: boolean })?.success ?? true
|
||||
console.log(` [DONE] ${event.task ?? '?'} (${success ? 'OK' : 'FAIL'})`)
|
||||
}
|
||||
}
|
||||
|
||||
const orchestrator = new OpenMultiAgent({
|
||||
defaultModel: 'claude-sonnet-4-6',
|
||||
onProgress: handleProgress,
|
||||
})
|
||||
|
||||
const team = orchestrator.createTeam('code-review-team', {
|
||||
name: 'code-review-team',
|
||||
agents: [generator, securityReviewer, performanceReviewer, styleReviewer, synthesizer],
|
||||
sharedMemory: true,
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tasks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
title: 'Generate code',
|
||||
description: `Write a Node.js Express route handler for this API spec:\n\n${API_SPEC}`,
|
||||
assignee: 'generator',
|
||||
},
|
||||
{
|
||||
title: 'Security review',
|
||||
description: 'Review the generated code for security vulnerabilities.',
|
||||
assignee: 'security-reviewer',
|
||||
dependsOn: ['Generate code'],
|
||||
},
|
||||
{
|
||||
title: 'Performance review',
|
||||
description: 'Review the generated code for performance issues.',
|
||||
assignee: 'performance-reviewer',
|
||||
dependsOn: ['Generate code'],
|
||||
},
|
||||
{
|
||||
title: 'Style review',
|
||||
description: 'Review the generated code for style and readability.',
|
||||
assignee: 'style-reviewer',
|
||||
dependsOn: ['Generate code'],
|
||||
},
|
||||
{
|
||||
title: 'Synthesize feedback',
|
||||
description: 'Synthesize all review feedback and the original code into a unified, prioritized action item report.',
|
||||
assignee: 'synthesizer',
|
||||
dependsOn: ['Security review', 'Performance review', 'Style review'],
|
||||
},
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Run
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
console.log('Multi-Perspective Code Review')
|
||||
console.log('='.repeat(60))
|
||||
console.log(`Spec: ${API_SPEC.split('\n')[0]}`)
|
||||
console.log('Pipeline: generator → 3 reviewers (parallel) → synthesizer')
|
||||
console.log('='.repeat(60))
|
||||
console.log()
|
||||
|
||||
const result = await orchestrator.runTasks(team, tasks)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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(22)} ${tokens}`)
|
||||
}
|
||||
|
||||
const synthResult = result.agentResults.get('synthesizer')
|
||||
if (synthResult?.success) {
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('UNIFIED REVIEW REPORT')
|
||||
console.log('='.repeat(60))
|
||||
console.log()
|
||||
console.log(synthResult.output)
|
||||
}
|
||||
|
||||
console.log('\nDone.')
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/**
|
||||
* Example 15 — 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 examples/15-research-aggregation.ts
|
||||
*
|
||||
* Prerequisites:
|
||||
* ANTHROPIC_API_KEY env var must be set.
|
||||
*/
|
||||
|
||||
import { OpenMultiAgent } from '../src/index.js'
|
||||
import type { AgentConfig, OrchestratorEvent } from '../src/types.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Topic
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TOPIC = 'WebAssembly adoption in 2026'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agents — three analysts + one synthesizer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const technicalAnalyst: AgentConfig = {
|
||||
name: 'technical-analyst',
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: `You are a technical analyst. Given a topic, research its technical
|
||||
capabilities, limitations, performance characteristics, and architectural patterns.
|
||||
Write your findings as structured markdown. Keep it to 200-300 words.`,
|
||||
maxTurns: 2,
|
||||
}
|
||||
|
||||
const marketAnalyst: AgentConfig = {
|
||||
name: 'market-analyst',
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: `You are a market analyst. Given a topic, research industry adoption
|
||||
rates, key companies using the technology, market size estimates, and competitive
|
||||
landscape. Write your findings as structured markdown. Keep it to 200-300 words.`,
|
||||
maxTurns: 2,
|
||||
}
|
||||
|
||||
const communityAnalyst: AgentConfig = {
|
||||
name: 'community-analyst',
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: `You are a developer community analyst. Given a topic, research
|
||||
developer sentiment, ecosystem maturity, learning resources, community size,
|
||||
and conference/meetup activity. Write your findings as structured markdown.
|
||||
Keep it to 200-300 words.`,
|
||||
maxTurns: 2,
|
||||
}
|
||||
|
||||
const synthesizer: AgentConfig = {
|
||||
name: 'synthesizer',
|
||||
model: 'claude-sonnet-4-6',
|
||||
systemPrompt: `You are a research director who synthesizes multiple analyst reports
|
||||
into a single cohesive document. You will receive all prior analyst outputs
|
||||
automatically. Then:
|
||||
|
||||
1. Cross-reference claims across reports - flag agreements and contradictions
|
||||
2. Identify the 3 most important insights
|
||||
3. Produce a structured report with: Executive Summary, Key Findings,
|
||||
Areas of Agreement, Open Questions, and Recommendation
|
||||
|
||||
Keep the final report to 300-400 words.`,
|
||||
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: 'claude-sonnet-4-6',
|
||||
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('Pipeline: 3 analysts (parallel) → synthesizer')
|
||||
console.log('='.repeat(60))
|
||||
console.log()
|
||||
|
||||
const result = await orchestrator.runTasks(team, tasks)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 REPORT')
|
||||
console.log('='.repeat(60))
|
||||
console.log()
|
||||
console.log(synthResult.output)
|
||||
}
|
||||
|
||||
console.log('\nDone.')
|
||||
|
|
@ -841,21 +841,47 @@ export class OpenMultiAgent {
|
|||
if (agentConfigs.length > 0 && isSimpleGoal(goal)) {
|
||||
const bestAgent = selectBestAgent(goal, agentConfigs)
|
||||
|
||||
// Use buildAgent() + agent.run() directly instead of this.runAgent()
|
||||
// to avoid duplicate progress events and double completedTaskCount.
|
||||
// Events are emitted here; counting is handled by buildTeamRunResult().
|
||||
const effectiveBudget = resolveTokenBudget(bestAgent.maxTokenBudget, this.config.maxTokenBudget)
|
||||
const effective: AgentConfig = {
|
||||
...bestAgent,
|
||||
provider: bestAgent.provider ?? this.config.defaultProvider,
|
||||
baseURL: bestAgent.baseURL ?? this.config.defaultBaseURL,
|
||||
apiKey: bestAgent.apiKey ?? this.config.defaultApiKey,
|
||||
maxTokenBudget: effectiveBudget,
|
||||
}
|
||||
const agent = buildAgent(effective)
|
||||
|
||||
this.config.onProgress?.({
|
||||
type: 'agent_start',
|
||||
agent: bestAgent.name,
|
||||
data: { phase: 'short-circuit', goal },
|
||||
})
|
||||
|
||||
// Forward the caller's abort signal so short-circuit honours the same
|
||||
// cancellation contract as the full coordinator path.
|
||||
const result = await this.runAgent(
|
||||
bestAgent,
|
||||
goal,
|
||||
options?.abortSignal ? { abortSignal: options.abortSignal } : undefined,
|
||||
)
|
||||
const agentResults = new Map<string, AgentRunResult>()
|
||||
agentResults.set(bestAgent.name, result)
|
||||
const traceFields = this.config.onTrace
|
||||
? { onTrace: this.config.onTrace, runId: generateRunId(), traceAgent: bestAgent.name }
|
||||
: null
|
||||
const abortFields = options?.abortSignal ? { abortSignal: options.abortSignal } : null
|
||||
const runOptions: Partial<RunOptions> | undefined =
|
||||
traceFields || abortFields
|
||||
? { ...(traceFields ?? {}), ...(abortFields ?? {}) }
|
||||
: undefined
|
||||
|
||||
const result = await agent.run(goal, runOptions)
|
||||
|
||||
if (result.budgetExceeded) {
|
||||
this.config.onProgress?.({
|
||||
type: 'budget_exceeded',
|
||||
agent: bestAgent.name,
|
||||
data: new TokenBudgetExceededError(
|
||||
bestAgent.name,
|
||||
result.tokenUsage.input_tokens + result.tokenUsage.output_tokens,
|
||||
effectiveBudget ?? 0,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
this.config.onProgress?.({
|
||||
type: 'agent_complete',
|
||||
|
|
@ -863,6 +889,8 @@ export class OpenMultiAgent {
|
|||
data: { phase: 'short-circuit', result },
|
||||
})
|
||||
|
||||
const agentResults = new Map<string, AgentRunResult>()
|
||||
agentResults.set(bestAgent.name, result)
|
||||
return this.buildTeamRunResult(agentResults)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -348,46 +348,39 @@ describe('runTeam short-circuit', () => {
|
|||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Regression: abortSignal forwarding (PR #70 review point 4)
|
||||
// Regression: no duplicate progress events (#82)
|
||||
//
|
||||
// The short-circuit path must forward `options.abortSignal` from runTeam
|
||||
// through to runAgent, otherwise simple-goal cancellations are silently
|
||||
// ignored — a regression vs the full coordinator path which already
|
||||
// honours the signal via PR #69.
|
||||
// The short-circuit path must emit exactly one agent_start and one
|
||||
// agent_complete event. Before the fix, calling this.runAgent() added
|
||||
// a second pair of events on top of the ones emitted by the short-circuit
|
||||
// block itself, and buildTeamRunResult() double-counted completedTasks.
|
||||
// -------------------------------------------------------------------------
|
||||
it('forwards abortSignal from runTeam to runAgent in short-circuit path', async () => {
|
||||
it('emits exactly one agent_start and one agent_complete (no duplicates)', async () => {
|
||||
mockAdapterResponses = ['done']
|
||||
|
||||
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
|
||||
const events: OrchestratorEvent[] = []
|
||||
const oma = new OpenMultiAgent({
|
||||
defaultModel: 'mock-model',
|
||||
onProgress: (e) => events.push(e),
|
||||
})
|
||||
const team = oma.createTeam('t', teamCfg())
|
||||
|
||||
// Spy on runAgent to capture the options argument
|
||||
const runAgentSpy = vi.spyOn(oma, 'runAgent')
|
||||
|
||||
const controller = new AbortController()
|
||||
await oma.runTeam(team, 'Say hello', { abortSignal: controller.signal })
|
||||
|
||||
expect(runAgentSpy).toHaveBeenCalledTimes(1)
|
||||
const callArgs = runAgentSpy.mock.calls[0]!
|
||||
// Third positional arg must contain the same signal we passed in
|
||||
expect(callArgs[2]).toBeDefined()
|
||||
expect(callArgs[2]?.abortSignal).toBe(controller.signal)
|
||||
})
|
||||
|
||||
it('runAgent invoked without abortSignal when caller omits it', async () => {
|
||||
mockAdapterResponses = ['done']
|
||||
|
||||
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
|
||||
const team = oma.createTeam('t', teamCfg())
|
||||
|
||||
const runAgentSpy = vi.spyOn(oma, 'runAgent')
|
||||
|
||||
await oma.runTeam(team, 'Say hello')
|
||||
|
||||
expect(runAgentSpy).toHaveBeenCalledTimes(1)
|
||||
const callArgs = runAgentSpy.mock.calls[0]!
|
||||
// Third positional arg should be undefined when caller doesn't pass one
|
||||
expect(callArgs[2]).toBeUndefined()
|
||||
const starts = events.filter(e => e.type === 'agent_start')
|
||||
const completes = events.filter(e => e.type === 'agent_complete')
|
||||
expect(starts).toHaveLength(1)
|
||||
expect(completes).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('completedTaskCount is exactly 1 after a successful short-circuit run', async () => {
|
||||
mockAdapterResponses = ['done']
|
||||
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
|
||||
const team = oma.createTeam('t', teamCfg())
|
||||
|
||||
await oma.runTeam(team, 'Say hello')
|
||||
|
||||
expect(oma.getStatus().completedTasks).toBe(1)
|
||||
})
|
||||
|
||||
it('aborted signal causes the underlying agent loop to skip the LLM call', async () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue