- Published on
Multi-Agent Systems — Orchestrating Specialized Agents for Complex Tasks
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Complex tasks often exceed what a single agent can do well. A multi-agent system divides work among specialized agents: one researches, one writes, one critiques, one executes. This post covers orchestrating multiple agents, managing shared state, parallel execution, and using LangGraph to build production-grade multi-agent workflows.
- Supervisor-Worker Pattern
- Agent-to-Agent Communication
- Shared State Management
- Agent Specialization
- Parallel Agent Execution
- Result Aggregation
- LangGraph for Multi-Agent Graphs
- Checklist
- Conclusion
Supervisor-Worker Pattern
The supervisor-worker pattern delegates tasks to specialized agents. The supervisor routes tasks and aggregates results.
interface TaskRequest {
id: string;
description: string;
type: 'research' | 'writing' | 'critique' | 'execution';
context: Record<string, unknown>;
deadline?: number;
}
interface TaskResult {
taskId: string;
status: 'success' | 'failed' | 'timeout';
output?: string;
error?: string;
executionTimeMs: number;
}
interface WorkerAgent {
name: string;
specialization: string;
execute: (task: TaskRequest) => Promise<TaskResult>;
}
class SupervisorAgent {
private workers: Map<string, WorkerAgent> = new Map();
private taskQueue: TaskRequest[] = [];
private results: Map<string, TaskResult> = new Map();
registerWorker(worker: WorkerAgent): void {
this.workers.set(worker.name, worker);
console.log(`Registered worker: ${worker.name} (${worker.specialization})`);
}
async orchestrate(mainTask: string): Promise<string> {
// Step 1: Decompose main task into subtasks
const subtasks = await this.decomposeTasks(mainTask);
console.log(`Decomposed into ${subtasks.length} subtasks`);
// Step 2: Route tasks to appropriate workers
const routedTasks = this.routeTasks(subtasks);
// Step 3: Execute tasks (can be parallel)
const results = await this.executeTasks(routedTasks);
// Step 4: Aggregate results
const finalOutput = await this.aggregateResults(results, mainTask);
return finalOutput;
}
private async decomposeTasks(mainTask: string): Promise<TaskRequest[]> {
const prompt = `Break this task into subtasks. Each subtask should be assignable to a specialist agent.
Return JSON: { "tasks": [{"description": "...", "type": "research|writing|critique|execution"}] }
Main task: ${mainTask}`;
const response = await this.llmCall(prompt);
try {
const parsed = JSON.parse(response);
return parsed.tasks.map(
(t: any, i: number) =>
({
id: `task-${i}`,
description: t.description,
type: t.type,
context: { mainTask },
} as TaskRequest),
);
} catch {
return [
{
id: 'task-0',
description: mainTask,
type: 'execution',
context: {},
},
];
}
}
private routeTasks(tasks: TaskRequest[]): Map<string, TaskRequest[]> {
const routed = new Map<string, TaskRequest[]>();
for (const task of tasks) {
// Route based on task type
let workerName = '';
switch (task.type) {
case 'research':
workerName = 'researcher';
break;
case 'writing':
workerName = 'writer';
break;
case 'critique':
workerName = 'critic';
break;
case 'execution':
workerName = 'executor';
break;
}
if (!routed.has(workerName)) {
routed.set(workerName, []);
}
routed.get(workerName)!.push(task);
}
return routed;
}
private async executeTasks(routedTasks: Map<string, TaskRequest[]>): Promise<TaskResult[]> {
const results: TaskResult[] = [];
// Execute in parallel where possible
const promises: Promise<void>[] = [];
for (const [workerName, tasks] of routedTasks.entries()) {
const worker = this.workers.get(workerName);
if (!worker) {
console.warn(`No worker found for: ${workerName}`);
continue;
}
for (const task of tasks) {
promises.push(
worker.execute(task).then((result) => {
results.push(result);
}),
);
}
}
await Promise.all(promises);
return results;
}
private async aggregateResults(results: TaskResult[], mainTask: string): Promise<string> {
const successResults = results.filter((r) => r.status === 'success');
const resultsSummary = successResults
.map((r) => `${r.taskId}: ${r.output}`)
.join('\n\n');
const prompt = `Original task: ${mainTask}
Results from specialist agents:
${resultsSummary}
Synthesize these results into a cohesive final answer.`;
return this.llmCall(prompt);
}
private async llmCall(prompt: string): Promise<string> {
return '';
}
}
// Example workers
const researcherAgent: WorkerAgent = {
name: 'researcher',
specialization: 'research',
async execute(task: TaskRequest): Promise<TaskResult> {
const startTime = Date.now();
try {
// Use web search tools to research
const output = `Research findings for: ${task.description}`;
return {
taskId: task.id,
status: 'success',
output,
executionTimeMs: Date.now() - startTime,
};
} catch (error) {
return {
taskId: task.id,
status: 'failed',
error: (error as Error).message,
executionTimeMs: Date.now() - startTime,
};
}
},
};
const writerAgent: WorkerAgent = {
name: 'writer',
specialization: 'writing',
async execute(task: TaskRequest): Promise<TaskResult> {
const startTime = Date.now();
try {
const output = `Written content for: ${task.description}`;
return {
taskId: task.id,
status: 'success',
output,
executionTimeMs: Date.now() - startTime,
};
} catch (error) {
return {
taskId: task.id,
status: 'failed',
error: (error as Error).message,
executionTimeMs: Date.now() - startTime,
};
}
},
};
The supervisor-worker pattern scales to dozens of agents. Each agent specializes in one type of task.
Agent-to-Agent Communication
Agents need to pass information to each other, not just through the supervisor.
interface Message {
from: string;
to: string;
subject: string;
content: string;
timestamp: number;
}
interface AgentInbox {
agentName: string;
messages: Message[];
}
class AgentCommunicationBus {
private inboxes: Map<string, AgentInbox> = new Map();
private messageLog: Message[] = [];
registerAgent(agentName: string): void {
this.inboxes.set(agentName, {
agentName,
messages: [],
});
}
async sendMessage(from: string, to: string, subject: string, content: string): Promise<void> {
const message: Message = {
from,
to,
subject,
content,
timestamp: Date.now(),
};
const inbox = this.inboxes.get(to);
if (inbox) {
inbox.messages.push(message);
}
this.messageLog.push(message);
console.log(`Message from ${from} to ${to}: ${subject}`);
}
async getMessages(agentName: string): Promise<Message[]> {
const inbox = this.inboxes.get(agentName);
return inbox?.messages || [];
}
async clearMessages(agentName: string): Promise<void> {
const inbox = this.inboxes.get(agentName);
if (inbox) {
inbox.messages = [];
}
}
}
// Agents communicate through the bus
class CommunicatingAgent {
constructor(
private name: string,
private communicationBus: AgentCommunicationBus,
) {}
async work(): Promise<void> {
// Get messages from other agents
const messages = await this.communicationBus.getMessages(this.name);
for (const msg of messages) {
console.log(`${this.name} received: ${msg.subject}`);
// Process message
}
// Send result to another agent
await this.communicationBus.sendMessage(
this.name,
'next-agent',
'Work completed',
'Here are my results...',
);
// Clear processed messages
await this.communicationBus.clearMessages(this.name);
}
}
Communication buses enable asynchronous agent interaction without tight coupling.
Shared State Management
Multiple agents need access to shared state: current progress, intermediate results, constraints.
interface SharedState {
sessionId: string;
mainTask: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
progress: {
completed: number;
total: number;
currentAgent?: string;
};
results: Map<string, unknown>;
constraints: {
maxTokens?: number;
maxTime?: number;
budget?: number;
};
errors: Array<{ agent: string; error: string; timestamp: number }>;
}
class StateManager {
private states: Map<string, SharedState> = new Map();
createState(sessionId: string, mainTask: string): SharedState {
const state: SharedState = {
sessionId,
mainTask,
status: 'pending',
progress: {
completed: 0,
total: 0,
currentAgent: undefined,
},
results: new Map(),
constraints: {
maxTokens: 100000,
maxTime: 300000, // 5 minutes
budget: 10, // $10
},
errors: [],
};
this.states.set(sessionId, state);
return state;
}
getState(sessionId: string): SharedState | undefined {
return this.states.get(sessionId);
}
updateProgress(sessionId: string, completed: number, currentAgent: string): void {
const state = this.states.get(sessionId);
if (state) {
state.progress.completed = completed;
state.progress.currentAgent = currentAgent;
}
}
storeResult(sessionId: string, key: string, value: unknown): void {
const state = this.states.get(sessionId);
if (state) {
state.results.set(key, value);
}
}
getResult(sessionId: string, key: string): unknown {
const state = this.states.get(sessionId);
return state?.results.get(key);
}
recordError(sessionId: string, agent: string, error: string): void {
const state = this.states.get(sessionId);
if (state) {
state.errors.push({
agent,
error,
timestamp: Date.now(),
});
}
}
canContinue(sessionId: string): boolean {
const state = this.states.get(sessionId);
if (!state) return false;
// Check constraints
if (state.progress.completed >= state.progress.total) {
return false; // Completed
}
return true;
}
}
Shared state prevents agents from duplicating work and enables early stopping when constraints are exceeded.
Agent Specialization
Each agent should focus on one type of task and be exceptionally good at it.
class SpecializedAgent {
constructor(
private name: string,
private tools: string[],
private systemPrompt: string,
) {}
async execute(task: string): Promise<string> {
const context = `You are ${this.name}. Your specialization: ${this.systemPrompt}
Your available tools: ${this.tools.join(', ')}
Task: ${task}`;
return this.llmCall(context);
}
private async llmCall(prompt: string): Promise<string> {
return '';
}
}
// Specialized agents
const agents = {
researcher: new SpecializedAgent(
'ResearchAgent',
['web_search', 'academic_database', 'news_api'],
'Research complex topics, find credible sources, evaluate information quality',
),
writer: new SpecializedAgent(
'WriterAgent',
['spell_check', 'grammar_check', 'plagiarism_check'],
'Write clear, engaging content. Ensure coherence, proper structure, compelling narrative',
),
critic: new SpecializedAgent(
'CriticAgent',
['fact_check', 'consistency_check', 'quality_score'],
'Critique work for factual accuracy, logical consistency, and quality. Provide constructive feedback',
),
executor: new SpecializedAgent(
'ExecutorAgent',
['code_runner', 'api_call', 'file_write'],
'Execute technical tasks. Write and test code, call APIs, perform system operations',
),
};
Specialization improves quality: a researcher-agent is better at research than a generalist agent.
Parallel Agent Execution
Some agents can work simultaneously. Coordinate parallel work with task dependencies.
interface TaskDependency {
taskId: string;
dependsOn: string[]; // Task IDs this depends on
worker: string;
description: string;
}
class ParallelExecutor {
async executeTasks(dependencies: TaskDependency[]): Promise<Map<string, unknown>> {
const completed = new Map<string, unknown>();
const inProgress = new Set<string>();
while (completed.size < dependencies.length) {
// Find tasks that can start (dependencies met)
const readyTasks = dependencies.filter(
(task) =>
!completed.has(task.taskId) &&
!inProgress.has(task.taskId) &&
task.dependsOn.every((dep) => completed.has(dep)),
);
if (readyTasks.length === 0) {
break; // Deadlock or all done
}
// Execute ready tasks in parallel
const promises = readyTasks.map((task) => {
inProgress.add(task.taskId);
return this.executeTask(task)
.then((result) => {
completed.set(task.taskId, result);
inProgress.delete(task.taskId);
})
.catch((error) => {
console.error(`Task ${task.taskId} failed: ${error}`);
inProgress.delete(task.taskId);
});
});
await Promise.all(promises);
}
return completed;
}
private async executeTask(task: TaskDependency): Promise<unknown> {
// Get dependency results
const deps = new Map<string, unknown>();
// ... load dependency results
// Execute task with dependency context
return `Result of ${task.taskId}`;
}
}
// Example task DAG (directed acyclic graph)
const taskDag: TaskDependency[] = [
{
taskId: 'research',
dependsOn: [],
worker: 'researcher',
description: 'Research the topic',
},
{
taskId: 'outline',
dependsOn: ['research'],
worker: 'writer',
description: 'Create outline based on research',
},
{
taskId: 'draft',
dependsOn: ['outline'],
worker: 'writer',
description: 'Write draft',
},
{
taskId: 'fact_check',
dependsOn: ['draft', 'research'],
worker: 'critic',
description: 'Fact-check draft against research',
},
{
taskId: 'revise',
dependsOn: ['fact_check'],
worker: 'writer',
description: 'Revise based on feedback',
},
];
Parallel execution reduces total runtime. Map task dependencies to find parallelizable work.
Result Aggregation
Combining results from multiple agents is non-trivial.
interface AggregationStrategy {
combine: (results: Map<string, unknown>) => Promise<unknown>;
priority?: Map<string, number>; // Higher = more important
}
class ResultAggregator {
async aggregateConcat(results: Map<string, unknown>): Promise<string> {
// Simple: concatenate all results
return Array.from(results.values()).join('\n\n');
}
async aggregateWeighted(
results: Map<string, unknown>,
weights: Map<string, number>,
): Promise<string> {
// Weighted combination: prioritize certain results
const weighted = Array.from(results.entries())
.sort(([keyA], [keyB]) => (weights.get(keyB) || 1) - (weights.get(keyA) || 1))
.map(([_, value]) => value);
return weighted.join('\n\n');
}
async aggregateConsensus(results: Map<string, unknown>): Promise<unknown> {
// For scoring/voting results, find consensus
const votes = Array.from(results.values()) as number[];
if (votes.length === 0) return 0;
const avg = votes.reduce((a, b) => a + b, 0) / votes.length;
return Math.round(avg * 10) / 10; // Round to 1 decimal
}
async aggregateSynthesis(
results: Map<string, unknown>,
mainTask: string,
): Promise<string> {
// Use LLM to synthesize disparate results
const resultsSummary = Array.from(results.entries())
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
.join('\n');
const prompt = `Original task: ${mainTask}
Results from specialist agents:
${resultsSummary}
Synthesize these results into a coherent final answer. Resolve conflicts, combine insights.`;
return this.llmCall(prompt);
}
private async llmCall(prompt: string): Promise<string> {
return '';
}
}
Choose aggregation strategy based on task type: concat for summaries, consensus for scores, synthesis for complex results.
LangGraph for Multi-Agent Graphs
LangGraph provides a graph-based framework for multi-agent workflows.
import { StateGraph, START, END } from '@langchain/langgraph';
interface WorkflowState {
task: string;
researchResults?: string;
outline?: string;
draft?: string;
critique?: string;
finalOutput?: string;
}
const workflow = new StateGraph<WorkflowState>({
channels: {
task: { value: null, reducer: (x, y) => y || x },
researchResults: { value: null, reducer: (x, y) => y || x },
outline: { value: null, reducer: (x, y) => y || x },
draft: { value: null, reducer: (x, y) => y || x },
critique: { value: null, reducer: (x, y) => y || x },
finalOutput: { value: null, reducer: (x, y) => y || x },
},
});
// Define nodes (agents)
workflow.addNode('research', async (state: WorkflowState) => {
const research = await researcherAgent.execute(state.task);
return { ...state, researchResults: research };
});
workflow.addNode('outline', async (state: WorkflowState) => {
const outline = await writerAgent.execute(
`Create an outline based on this research: ${state.researchResults}`,
);
return { ...state, outline };
});
workflow.addNode('draft', async (state: WorkflowState) => {
const draft = await writerAgent.execute(
`Write a draft based on this outline: ${state.outline}`,
);
return { ...state, draft };
});
workflow.addNode('critique', async (state: WorkflowState) => {
const critique = await criticAgent.execute(
`Review this draft: ${state.draft}. Is it accurate and well-written?`,
);
return { ...state, critique };
});
workflow.addNode('revise', async (state: WorkflowState) => {
const final = await writerAgent.execute(
`Revise this draft based on feedback: ${state.critique}\n\nOriginal: ${state.draft}`,
);
return { ...state, finalOutput: final };
});
// Define edges (control flow)
workflow.addEdge(START, 'research');
workflow.addEdge('research', 'outline');
workflow.addEdge('outline', 'draft');
workflow.addEdge('draft', 'critique');
// Conditional edge: only revise if critique suggests issues
workflow.addConditionalEdges(
'critique',
async (state: WorkflowState) => {
const hasIssues = (state.critique || '').toLowerCase().includes('issue');
return hasIssues ? 'revise' : 'finalize';
},
{
revise: 'revise',
finalize: END,
},
);
workflow.addEdge('revise', END);
const app = workflow.compile();
// Run workflow
async function runWorkflow(task: string) {
const result = await app.invoke({ task });
return result.finalOutput;
}
LangGraph handles state management, task ordering, and conditional logic automatically.
Checklist
- Supervisor: decompose tasks, route to specialists, aggregate results
- Communication: enable direct agent-to-agent messaging
- Shared state: central repository for progress and constraints
- Specialization: each agent excels at one type of task
- Parallelization: execute independent tasks simultaneously
- Aggregation: choose strategy based on result types
- LangGraph: use for graph-based coordination
Conclusion
Multi-agent systems beat single agents on complex tasks. The supervisor-worker pattern provides clear task distribution, agents specialize in narrow domains, and shared state prevents redundant work. LangGraph makes building and managing multi-agent workflows straightforward. Start with a supervisor and two specialized agents, add more as complexity demands.