要解决的问题
合同审查有明确的先后关系,但并非每一步都必须排队执行。全部串行会浪费时间;某一步暂时失败后重跑整条流程,则会重复已经完成的工作。
只提取一次合同条款,并行完成合规检查与摘要,最后生成 Markdown 通知。
适合评估可重复审查流程的法务运营与工作流团队
合同审查有明确的先后关系,但并非每一步都必须排队执行。全部串行会浪费时间;某一步暂时失败后重跑整条流程,则会重复已经完成的工作。
示例内置合同内容,以及一个四步骤的审查计划。
每个专职 Agent 只处理它需要的证据;互不依赖的工作同时进行,需要上游结果的工作则按顺序等待。
上游任务先准备两位审查者都需要的共同材料。
识别并整理相关合同条款。
两项任务都在条款提取后开始,彼此不需要等待。
检查提取出的条款;失败时只重试这一步。
生成简明的合同摘要。
最后一步等两个审查分支都完成后再开始。
把全部结果合并成最终 Markdown 通知。
只有合规检查和合同摘要都成功后,系统才生成最终产物。
上面是面向业务的解释;下面的命令、运行前提、API 和源码继续与真实仓库同步。
OpenMultiAgent 在仓库的克隆里运行这个文件:
npx tsx packages/core/examples/cookbook/contract-review-dag.tsFORCE_FAIL=task2 npx tsx packages/core/examples/cookbook/contract-review-dag.ts
OMA 与 provider 无关——这个示例按上面的 key 编写,但你也可以用 OpenAI、Gemini、Groq 等任意 provider 运行。 全部 provider →
完整示例,从固定的 Framework commit 同步。
/*** Contract Review DAG with Step-Level Retry** Demonstrates DAG task orchestration with step-level retry using runTasks().* Scenario: contract review pipeline with 4 tasks forming a DAG:** [Task 1: extract-clauses]* ├──→ [Task 2: compliance-check] ────┐* │ ├──→ [Task 4: notify] → Markdown* └──→ [Task 3: summary] ──────────────┘** Key features:* - DAG dependencies with parallel execution (Task 2 and 3 run concurrently)* - Step-level retry on Task 2/3/4 with exponential backoff* - FORCE_FAIL=task2 env var triggers Task 2 failure on first attempt** Run:* npx tsx packages/core/examples/cookbook/contract-review-dag.ts* FORCE_FAIL=task2 npx tsx packages/core/examples/cookbook/contract-review-dag.ts** Prerequisites:* ANTHROPIC_API_KEY env var must be set.*/import { OpenMultiAgent } from '../../src/index.js'import type { AgentConfig, OrchestratorEvent } from '../../src/types.js'import { readFileSync } from 'fs'import { fileURLToPath } from 'url'import { dirname, join } from 'path'const __filename = fileURLToPath(import.meta.url)const __dirname = dirname(__filename)// ---------------------------------------------------------------------------// Attempt counter for FORCE_FAIL mechanism (closure-based per agent)// ---------------------------------------------------------------------------const attemptCounter = new Map<string, number>()function getAndIncrementAttempt(agentName: string): number {const count = (attemptCounter.get(agentName) ?? 0) + 1attemptCounter.set(agentName, count)return count}// ---------------------------------------------------------------------------// Step 1: Agent configurations (4 agents)// ---------------------------------------------------------------------------const extractorConfig: AgentConfig = {name: 'extractor',model: 'claude-sonnet-4-6',provider: 'anthropic',systemPrompt: `You are a contract clause extraction specialist. Extract all clauses from the provided contract text and output as a JSON array.Each object must have:- id: clause number (string)- title: clause title- content: full clause text- riskLevel: "low" | "medium" | "high"Output ONLY valid JSON, no other text.`,maxTurns: 1,temperature: 0.1,}const complianceCheckerConfig: AgentConfig = {name: 'compliance-checker',model: 'claude-sonnet-4-6',provider: 'anthropic',systemPrompt: `You are a contract compliance auditor. Review each clause for regulatory and operational compliance.For each clause, output:- clauseId: clause number- isCompliant: true/false- issues: array of issues found (empty array if none)- riskCategory: "none" | "regulatory" | "operational" | "legal"Output ONLY valid JSON array, no other text.`,maxTurns: 1,temperature: 0.1,beforeRun: (context) => {const attempt = getAndIncrementAttempt('compliance-checker')// Only trigger FORCE_FAIL on first attempt (attempt=1)if (attempt === 1 && process.env.FORCE_FAIL === 'task2') {throw new Error('[FORCE_FAIL_TRIGGERED]')}return context},}const summarizerConfig: AgentConfig = {name: 'summarizer',model: 'claude-sonnet-4-6',provider: 'anthropic',systemPrompt: `You are a contract summary specialist. Generate an executive summary from the extracted clause list.Include:- Contract overview (main areas covered)- Key clause summary (3-5 most important points)- Risk callouts (highlight high-risk clauses)- Recommended next stepsOutput in Markdown format.`,maxTurns: 1,temperature: 0.2,}const notifierConfig: AgentConfig = {name: 'notifier',model: 'claude-sonnet-4-6',provider: 'anthropic',systemPrompt: `You are a report writer. Combine the compliance check results and summary into a final contract review report.Include:- Executive Summary- Compliance Results- Risk Details- Recommended ActionsOutput in Markdown format.`,maxTurns: 1,temperature: 0.2,}// ---------------------------------------------------------------------------// Step 2: Task configurations (4 tasks)// ---------------------------------------------------------------------------interface TaskConfig {title: stringdescription: stringassignee?: stringdependsOn?: readonly string[]maxRetries?: numberretryDelayMs?: numberretryBackoff?: number}// Task configs will be created after reading contract text// ---------------------------------------------------------------------------// Step 3: Progress tracking// ---------------------------------------------------------------------------interface TaskTiming {startTime: numberendTime?: number}const taskTimings = new Map<string, TaskTiming>()const taskStartTimes = new Map<string, number>()function handleProgress(event: OrchestratorEvent): void {const ts = new Date().toISOString()switch (event.type) {case 'task_start': {const task = event.data as { id: string; title: string } | undefinedif (task) {const now = Date.now()taskTimings.set(task.id, { startTime: now })taskStartTimes.set(task.title, now) // Use title as key for easier verificationconsole.log(`[${ts}] task_start: ${task.title}`)}break}case 'task_complete': {const task = event.data as { id: string; title: string } | undefinedif (task) {const timing = taskTimings.get(task.id)if (timing) {timing.endTime = Date.now()const duration = timing.endTime - timing.startTimeconsole.log(`[${ts}] task_complete: ${task.title} (${duration}ms)`)}}break}case 'task_retry':console.log(`[${ts}] task_retry:`, JSON.stringify(event.data))breakcase 'task_skipped':console.log(`[${ts}] task_skipped: ${event.task}`)breakcase 'agent_start':if (event.agent) {console.log(`[${ts}] agent_start: ${event.agent}`)}breakcase 'agent_complete':if (event.agent) {console.log(`[${ts}] agent_complete: ${event.agent}`)}breakcase 'message':if (typeof event.data === 'string') {console.log(`[${ts}] message: ${event.data}`)}breakcase 'error':console.log(`[${ts}] error:`, JSON.stringify(event.data))breakdefault:break}}// ---------------------------------------------------------------------------// Step 4: Parallelism verification// ---------------------------------------------------------------------------function verifyParallelism(): void {const t2Start = taskStartTimes.get('compliance-check')const t3Start = taskStartTimes.get('summary')if (t2Start !== undefined && t3Start !== undefined) {const diff = Math.abs(t2Start - t3Start)console.log('\n=== Parallelism Check ===')console.log(`Task 2 (compliance-check) start: ${t2Start}`)console.log(`Task 3 (summary) start: ${t3Start}`)console.log(`Time difference: ${diff}ms`)if (diff >= 500) {console.error(`ASSERTION FAILED: Task 2 and Task 3 start times differ by ${diff}ms (>= 500ms). ` +`Expected parallel execution after Task 1 completes.`,)process.exit(1)}console.log(`Parallel execution (< 500ms): YES`)console.log('========================\n')} else {console.log('\n=== Parallelism Check: Unable to verify (missing timing data) ===\n')}}// ---------------------------------------------------------------------------// Step 5: Orchestrator and Team configuration// ---------------------------------------------------------------------------const orchestrator = new OpenMultiAgent({defaultModel: 'claude-sonnet-4-6',defaultProvider: 'anthropic',onProgress: handleProgress,})const team = orchestrator.createTeam('contract-review-team', {name: 'contract-review-team',agents: [extractorConfig, complianceCheckerConfig, summarizerConfig, notifierConfig],sharedMemory: true,})// ---------------------------------------------------------------------------// Step 6: Read contract text// ---------------------------------------------------------------------------const contractText = readFileSync(join(__dirname, '..', 'fixtures', 'sample-contract.txt'),'utf-8',)// ---------------------------------------------------------------------------// Step 6b: Create task configs with contract text// ---------------------------------------------------------------------------const taskConfigs: TaskConfig[] = [{title: 'extract-clauses',description: `Extract all clauses from the following contract text into structured JSON.\n\n=== CONTRACT TEXT ===\n${contractText}\n=== END CONTRACT ===\n\nOutput only valid JSON array.`,assignee: 'extractor',},{title: 'compliance-check',description: 'Check each clause for regulatory and operational compliance. Using the clause list from Task 1 above.',assignee: 'compliance-checker',dependsOn: ['extract-clauses'],maxRetries: 2,retryDelayMs: 500,retryBackoff: 2,},{title: 'summary',description: 'Generate executive summary of the contract. Using the clause list from Task 1 above.',assignee: 'summarizer',dependsOn: ['extract-clauses'],maxRetries: 2,retryDelayMs: 500,retryBackoff: 2,},{title: 'notify',description: 'Generate final markdown report with all analysis results',assignee: 'notifier',dependsOn: ['compliance-check', 'summary'],maxRetries: 2,retryDelayMs: 500,retryBackoff: 2,},]console.log('Contract Review DAG with Step-Level Retry')console.log('='.repeat(60))console.log(`\nContract: ${contractText.split('\n')[0]}`)console.log(`Length: ${contractText.split(/\s+/).length} words\n`)console.log(`FORCE_FAIL mode: ${process.env.FORCE_FAIL === 'task2' ? 'ENABLED (task2)' : 'disabled'}\n`)// ---------------------------------------------------------------------------// Step 7: Run the DAG// ---------------------------------------------------------------------------async function main(): Promise<void> {try {console.log('[Orchestration] Starting DAG execution...\n')const result = await orchestrator.runTasks(team, taskConfigs)// Verify parallelismverifyParallelism()console.log('\n' + '='.repeat(60))console.log('FINAL RESULT')console.log('='.repeat(60))console.log(`Success: ${result.success}`)console.log(`Total tasks: ${result.tasks?.length ?? taskConfigs.length}`)console.log(`Total token usage: ${result.totalTokenUsage.input_tokens} input, ${result.totalTokenUsage.output_tokens} output`)// Output each agent's resultconsole.log('\n' + '='.repeat(60))console.log('AGENT RESULTS')console.log('='.repeat(60))for (const [agentName, agentResult] of result.agentResults) {console.log(`\n--- ${agentName} ---`)console.log(agentResult.success ? agentResult.output : `[FAILED] ${agentResult.output}`)}if (result.success) {console.log('\n--- Final Report ---')const notifierResult = result.agentResults.get('notifier')if (notifierResult?.output) {console.log(notifierResult.output)}} else {console.log('\nWorkflow failed.')for (const task of result.tasks ?? []) {if (task.status === 'failed') {console.log(` - ${task.title}: failed`)}}}console.log('\n' + '='.repeat(60))console.log('Done.')} catch (error) {console.error('Fatal error:', error)process.exit(1)}}main()
open-multi-agent 采用 MIT 许可、可自行免费运行。当你需要在期限内交付、集成,或获得支持时,元定义科技(YuanASI)提供商业交付与支持。