AI Engineering — Agent Orchestration
Agent orchestration is the discipline of coordinating multiple AI agents or agent steps to achieve complex goals. As agent systems grow beyond single-turn tool use, you need patterns for sequencing work, managing state, handling failures, and incorporating human oversight.
Orchestration determines how agents communicate, share context, and hand off work. The right pattern depends on task dependencies, error tolerance, latency requirements, and cost constraints.
The simplest pattern: agents execute one after another, passing output as input to the next. This works when tasks have clear linear dependencies (A must complete before B can start).
| 1 | // Sequential agent pipeline |
| 2 | interface PipelineStep { |
| 3 | name: string; |
| 4 | agent: Agent; |
| 5 | transform: (input: any) => string; |
| 6 | } |
| 7 | |
| 8 | class SequentialPipeline { |
| 9 | constructor(private steps: PipelineStep[]) {} |
| 10 | |
| 11 | async run(initialInput: string) { |
| 12 | let currentInput = initialInput; |
| 13 | |
| 14 | for (const step of this.steps) { |
| 15 | console.log(`Running step: ${step.name}`); |
| 16 | |
| 17 | const agentInput = step.transform(currentInput); |
| 18 | const result = await step.agent.run(agentInput); |
| 19 | |
| 20 | currentInput = result; |
| 21 | } |
| 22 | |
| 23 | return currentInput; |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | // Example: Content generation pipeline |
| 28 | const pipeline = new SequentialPipeline([ |
| 29 | { |
| 30 | name: "outline", |
| 31 | agent: outlineAgent, |
| 32 | transform: (topic) => |
| 33 | `Create an outline for: "${topic}"` |
| 34 | }, |
| 35 | { |
| 36 | name: "draft", |
| 37 | agent: draftAgent, |
| 38 | transform: (outline) => |
| 39 | `Write a draft based on this outline: ${outline}` |
| 40 | }, |
| 41 | { |
| 42 | name: "review", |
| 43 | agent: reviewAgent, |
| 44 | transform: (draft) => |
| 45 | `Review and improve this draft: ${draft}` |
| 46 | } |
| 47 | ]); |
| 48 | |
| 49 | const article = await pipeline.run("AI Orchestration"); |
info
Directed Acyclic Graphs (DAGs) model workflows where some steps can run in parallel and others have dependencies. This maximizes throughput while preserving ordering constraints.
| 1 | // DAG-based orchestration |
| 2 | interface DAGNode { |
| 3 | id: string; |
| 4 | execute: (inputs: Map<string, any>) => Promise<any>; |
| 5 | dependencies: string[]; |
| 6 | } |
| 7 | |
| 8 | class DAGExecutor { |
| 9 | constructor(private nodes: Map<string, DAGNode>) {} |
| 10 | |
| 11 | async execute(): Promise<Map<string, any>> { |
| 12 | const results = new Map<string, any>(); |
| 13 | const completed = new Set<string>(); |
| 14 | const inProgress = new Set<string>(); |
| 15 | |
| 16 | while (completed.size < this.nodes.size) { |
| 17 | const ready = this.getReadyNodes(completed, inProgress); |
| 18 | |
| 19 | if (ready.length === 0 && inProgress.size > 0) { |
| 20 | // Wait for in-progress tasks |
| 21 | await new Promise(r => setTimeout(r, 100)); |
| 22 | continue; |
| 23 | } |
| 24 | |
| 25 | const batch = ready.map(async (node) => { |
| 26 | inProgress.add(node.id); |
| 27 | const deps = new Map( |
| 28 | node.dependencies.map(d => [d, results.get(d)]) |
| 29 | ); |
| 30 | const result = await node.execute(deps); |
| 31 | results.set(node.id, result); |
| 32 | completed.add(node.id); |
| 33 | inProgress.delete(node.id); |
| 34 | }); |
| 35 | |
| 36 | await Promise.all(batch); |
| 37 | } |
| 38 | |
| 39 | return results; |
| 40 | } |
| 41 | |
| 42 | private getReadyNodes( |
| 43 | completed: Set<string>, |
| 44 | inProgress: Set<string> |
| 45 | ): DAGNode[] { |
| 46 | return Array.from(this.nodes.values()).filter(node => |
| 47 | !completed.has(node.id) && |
| 48 | !inProgress.has(node.id) && |
| 49 | node.dependencies.every(d => completed.has(d)) |
| 50 | ); |
| 51 | } |
| 52 | } |
best practice
A manager agent decomposes tasks and delegates to specialized sub-agents. The manager tracks progress, handles exceptions, and synthesizes results. This mirrors human organizational structures.
| 1 | // Hierarchical manager-worker pattern |
| 2 | class ManagerAgent { |
| 3 | private workers = new Map<string, Agent>(); |
| 4 | private taskQueue: Task[] = []; |
| 5 | |
| 6 | constructor( |
| 7 | private orchestrator: Agent, |
| 8 | workers: Record<string, Agent> |
| 9 | ) { |
| 10 | Object.entries(workers).forEach( |
| 11 | ([name, agent]) => this.workers.set(name, agent) |
| 12 | ); |
| 13 | } |
| 14 | |
| 15 | async run(goal: string): Promise<string> { |
| 16 | // Step 1: Decompose goal into subtasks |
| 17 | const plan = await this.orchestrator.run( |
| 18 | `Decompose this goal into subtasks and assign workers. |
| 19 | Goal: ${goal} |
| 20 | Available workers: ${[...this.workers.keys()].join(", ")} |
| 21 | Return a JSON array of {worker, task}` |
| 22 | ); |
| 23 | |
| 24 | const subtasks = JSON.parse(plan); |
| 25 | |
| 26 | // Step 2: Execute subtasks in parallel |
| 27 | const results = await Promise.all( |
| 28 | subtasks.map(async (subtask: any) => { |
| 29 | const worker = this.workers.get(subtask.worker); |
| 30 | if (!worker) { |
| 31 | return { error: `Unknown worker: ${subtask.worker}` }; |
| 32 | } |
| 33 | return { |
| 34 | worker: subtask.worker, |
| 35 | result: await worker.run(subtask.task) |
| 36 | }; |
| 37 | }) |
| 38 | ); |
| 39 | |
| 40 | // Step 3: Synthesize results |
| 41 | return await this.orchestrator.run( |
| 42 | `Synthesize these results into a final answer: |
| 43 | ${JSON.stringify(results)}` |
| 44 | ); |
| 45 | } |
| 46 | } |
A supervisor agent monitors worker agents, approves or rejects their outputs, and handles failures. This is critical for production systems where quality control and error recovery are essential.
| 1 | // Supervisor with approval gates |
| 2 | class Supervisor { |
| 3 | private maxRetries = 3; |
| 4 | |
| 5 | constructor( |
| 6 | private supervisor: Agent, |
| 7 | private worker: Agent |
| 8 | ) {} |
| 9 | |
| 10 | async run(task: string): Promise<string> { |
| 11 | let attempt = 0; |
| 12 | |
| 13 | while (attempt < this.maxRetries) { |
| 14 | attempt++; |
| 15 | const result = await this.worker.run(task); |
| 16 | |
| 17 | const review = await this.supervisor.run( |
| 18 | `Review this result for quality and correctness. |
| 19 | Task: ${task} |
| 20 | Result: ${result} |
| 21 | Respond with APPROVED or REJECTED plus reason.` |
| 22 | ); |
| 23 | |
| 24 | if (review.startsWith("APPROVED")) { |
| 25 | return result; |
| 26 | } |
| 27 | |
| 28 | console.log(`Attempt ${attempt} rejected: ${review}`); |
| 29 | } |
| 30 | |
| 31 | throw new Error(`Task failed after ${this.maxRetries} attempts`); |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | // Human-in-the-loop supervisor |
| 36 | class HumanSupervisor { |
| 37 | async run(result: string, task: string): Promise<boolean> { |
| 38 | console.log(`\n=== HUMAN APPROVAL REQUIRED ===`); |
| 39 | console.log(`Task: ${task}`); |
| 40 | console.log(`Result: ${result}\n`); |
| 41 | |
| 42 | // In production, this would notify via Slack/email/UI |
| 43 | const approved = await promptUser("Approve? (y/n): "); |
| 44 | return approved; |
| 45 | } |
| 46 | } |
warning
Orchestrated agents share state. Common approaches include a shared context object passed through the chain, a global state store, or event-driven message passing. The choice affects latency, consistency, and fault tolerance.
| 1 | // Shared context state |
| 2 | interface WorkflowState { |
| 3 | workflowId: string; |
| 4 | status: "running" | "completed" | "failed"; |
| 5 | currentStep: string; |
| 6 | data: Record<string, any>; |
| 7 | errors: Error[]; |
| 8 | startTime: number; |
| 9 | } |
| 10 | |
| 11 | class WorkflowContext { |
| 12 | private state: WorkflowState; |
| 13 | |
| 14 | constructor(workflowId: string) { |
| 15 | this.state = { |
| 16 | workflowId, |
| 17 | status: "running", |
| 18 | currentStep: "init", |
| 19 | data: {}, |
| 20 | errors: [], |
| 21 | startTime: Date.now() |
| 22 | }; |
| 23 | } |
| 24 | |
| 25 | get(key: string): any { |
| 26 | return this.state.data[key]; |
| 27 | } |
| 28 | |
| 29 | set(key: string, value: any): void { |
| 30 | this.state.data[key] = value; |
| 31 | } |
| 32 | |
| 33 | addError(error: Error): void { |
| 34 | this.state.errors.push(error); |
| 35 | } |
| 36 | |
| 37 | snapshot(): WorkflowState { |
| 38 | return { ...this.state }; |
| 39 | } |
| 40 | |
| 41 | // Persist state for resumability |
| 42 | async persist(): Promise<void> { |
| 43 | await db.collection("workflows").updateOne( |
| 44 | { workflowId: this.state.workflowId }, |
| 45 | { $set: this.state }, |
| 46 | { upsert: true } |
| 47 | ); |
| 48 | } |
| 49 | |
| 50 | static async resume(workflowId: string): Promise<WorkflowContext> { |
| 51 | const doc = await db.collection("workflows").findOne({ workflowId }); |
| 52 | const ctx = new WorkflowContext(workflowId); |
| 53 | ctx.state = doc!; |
| 54 | return ctx; |
| 55 | } |
| 56 | } |
best practice
Different orchestration patterns suit different workflow shapes. Choose based on task dependencies, error tolerance, and operational requirements.
| Pattern | Structure | Parallelism | Fault Tolerance | Use Case |
|---|---|---|---|---|
| Sequential | Chain | None | Low | Linear pipelines |
| DAG | Graph | Partial | Medium | ETL, data processing |
| Hierarchical | Tree | At leaves | Medium | Manager-worker teams |
| Supervisor | Star | None | High | Quality-critical tasks |
| Swarm | Peer-to-peer | Full | High | Exploratory tasks |
| Routing | Classifier | N/A | Medium | Intent-based dispatch |
| 1 | // Routing pattern — classify then route |
| 2 | class Router { |
| 3 | constructor( |
| 4 | private classifier: Agent, |
| 5 | private routes: Map<string, Agent> |
| 6 | ) {} |
| 7 | |
| 8 | async route(input: string): Promise<string> { |
| 9 | const intent = await this.classifier.run( |
| 10 | `Classify this request into one of: |
| 11 | ${[...this.routes.keys()].join(", ")} |
| 12 | Request: "${input}" |
| 13 | Respond with only the category name.` |
| 14 | ); |
| 15 | |
| 16 | const handler = this.routes.get(intent.trim()); |
| 17 | if (!handler) { |
| 18 | return `No handler for: ${intent}`; |
| 19 | } |
| 20 | |
| 21 | return await handler.run(input); |
| 22 | } |
| 23 | } |