Building production-grade AI systems with HazelJS Agent Runtime
Introduction
The future of AI isn't single-purpose chatbots—it's coordinated teams of specialized agents working together to solve complex problems. Just as modern software systems evolved from monolithic applications to microservices, AI systems are evolving from single-agent interactions to multi-agent orchestration.
But building multi-agent systems is hard. You need to:
- Route tasks to the right specialist
- Coordinate execution across multiple agents
- Handle failures gracefully
- Maintain state across agent boundaries
- Observe what's happening in your system
This is where HazelJS Agent Runtime shines. It provides three complementary patterns for multi-agent orchestration, each designed for different use cases:
-
@Delegate— Peer-to-peer agent delegation -
AgentGraph— DAG-based workflow pipelines -
SupervisorAgent— LLM-driven dynamic routing
In this article, we'll explore each pattern, understand when to use them, and build a complete content creation pipeline that combines all three.
Why Multi-Agent Systems?
Before diving into patterns, let's understand why you'd want multiple agents instead of one powerful agent.
The Monolith Problem
A single agent handling everything becomes:
- Bloated — Too many tools, overwhelming context
- Unfocused — Jack of all trades, master of none
- Hard to test — Can't isolate specific capabilities
- Expensive — Large context windows for every request
The Multi-Agent Solution
Specialized agents are:
- Focused — Each agent has a clear domain
- Composable — Mix and match for different workflows
- Testable — Test each agent independently
- Efficient — Smaller contexts, targeted prompts
Think of it like a software team: you don't hire one person to do design, frontend, backend, and DevOps. You hire specialists and coordinate their work.
Pattern 1: @Delegate — Peer-to-Peer Delegation
What is it?
@Delegate lets one agent call another agent as if it were a tool. The orchestrator agent doesn't know it's delegating—from the LLM's perspective, it's just calling a function. The runtime handles the delegation transparently.
Architecture
OrchestratorAgent
├── @Delegate → ResearchAgent
├── @Delegate → WriterAgent
└── @Delegate → EditorAgent
When to use it?
- Clear hierarchy — One orchestrator, multiple workers
- 2-5 agents — Simple delegation chains
- Predefined roles — You know which agent does what
Code Example
import { Agent, Delegate, Tool } from '@hazeljs/agent';
// Worker Agent 1: Research
@Agent({
name: 'ResearchAgent',
description: 'Expert researcher who finds and synthesizes information',
systemPrompt: 'You are an expert researcher. Find accurate, relevant information and cite sources.',
})
export class ResearchAgent {
@Tool({
description: 'Search the web for information',
parameters: [{ name: 'query', type: 'string', required: true }],
})
async searchWeb(input: { query: string }) {
// Simulate web search
return {
results: [
{ title: 'AI Agents Overview', snippet: 'AI agents are autonomous...' },
{ title: 'Multi-Agent Systems', snippet: 'Coordination between agents...' },
],
sources: ['https://example.com/ai-agents', 'https://example.com/multi-agent'],
};
}
@Tool({
description: 'Analyze research findings and extract key insights',
parameters: [{ name: 'data', type: 'string', required: true }],
})
async analyzeFindings(input: { data: string }) {
return {
insights: ['Key finding 1', 'Key finding 2', 'Key finding 3'],
confidence: 0.85,
};
}
}
// Worker Agent 2: Writing
@Agent({
name: 'WriterAgent',
description: 'Professional technical writer who creates polished content',
systemPrompt: 'You are a professional technical writer. Create clear, engaging content.',
})
export class WriterAgent {
@Tool({
description: 'Write a blog post section from research notes',
parameters: [
{ name: 'topic', type: 'string', required: true },
{ name: 'research', type: 'string', required: true },
],
})
async writeBlogSection(input: { topic: string; research: string }) {
return {
content: `## ${input.topic}\n\nBased on research: ${input.research}...`,
wordCount: 450,
};
}
@Tool({
description: 'Format content as markdown with proper structure',
parameters: [{ name: 'rawContent', type: 'string', required: true }],
})
async formatMarkdown(input: { rawContent: string }) {
return `# Article\n\n${input.rawContent}\n\n---\n*Generated by AI*`;
}
}
// Orchestrator Agent
@Agent({
name: 'ContentOrchestratorAgent',
description: 'Orchestrates research and writing tasks to create complete articles',
systemPrompt: `You orchestrate content creation. Use researchTopic to gather information,
then use writeArticle to create the final content. Always research before writing.`,
})
export class ContentOrchestratorAgent {
// This looks like a tool to the LLM, but it's actually a delegation to ResearchAgent
@Delegate({
agent: 'ResearchAgent',
description: 'Research a topic thoroughly and return key findings with sources',
inputField: 'query',
})
async researchTopic(query: string): Promise<string> {
return ''; // Body replaced at runtime by AgentRuntime
}
// This delegates to WriterAgent
@Delegate({
agent: 'WriterAgent',
description: 'Write a polished article from research notes',
inputField: 'content',
})
async writeArticle(content: string): Promise<string> {
return ''; // Body replaced at runtime by AgentRuntime
}
}
Running the Example
import { AgentRuntime } from '@hazeljs/agent';
import { AIService } from '@hazeljs/ai';
// Setup runtime
const runtime = new AgentRuntime({
llmProvider: new AIService({ provider: 'openai', apiKey: process.env.OPENAI_API_KEY }),
defaultMaxSteps: 15,
});
// Register all agents
const orchestrator = new ContentOrchestratorAgent();
const researcher = new ResearchAgent();
const writer = new WriterAgent();
[ResearchAgent, WriterAgent, ContentOrchestratorAgent].forEach(A => runtime.registerAgent(A));
runtime.registerAgentInstance('ContentOrchestratorAgent', orchestrator);
runtime.registerAgentInstance('ResearchAgent', researcher);
runtime.registerAgentInstance('WriterAgent', writer);
// Execute
const result = await runtime.execute(
'ContentOrchestratorAgent',
'Write a blog post about multi-agent AI systems',
{ sessionId: 'demo-session-1' }
);
console.log(result.response);
console.log(`Completed in ${result.steps.length} steps`);
Key Benefits
✅ Transparent — LLM doesn't know it's delegating
✅ Type-safe — Full TypeScript support
✅ Observable — All delegations emit events
✅ Simple — Easy to understand and debug
Pattern 2: AgentGraph — DAG Workflow Pipelines
What is it?
AgentGraph lets you build directed acyclic graphs (DAGs) of agents and functions. Think LangGraph but TypeScript-native and integrated with HazelJS.
You define:
- Nodes — Agents or functions
- Edges — Sequential or conditional connections
- Entry point — Where execution starts
Architecture
Entry → NodeA → NodeB → END (sequential)
Entry → Router → NodeA | NodeB → END (conditional)
Entry → Split → [NodeA ‖ NodeB] → Merge → END (parallel)
When to use it?
- Known workflow — You know the steps at design time
- Conditional routing — Different paths based on state
- Parallel execution — Multiple agents run simultaneously
- Complex pipelines — 5+ steps with branching logic
Example 1: Sequential Pipeline
import { END } from '@hazeljs/agent';
const pipeline = runtime
.createGraph('blog-pipeline')
.addNode('researcher', { type: 'agent', agentName: 'ResearchAgent' })
.addNode('writer', { type: 'agent', agentName: 'WriterAgent' })
.addNode('editor', { type: 'agent', agentName: 'EditorAgent' })
.addEdge('researcher', 'writer')
.addEdge('writer', 'editor')
.addEdge('editor', END)
.setEntryPoint('researcher')
.compile();
const result = await pipeline.execute('Write about TypeScript generics');
console.log(result.output);
Example 2: Conditional Routing
// Classifier agent determines the type of request
@Agent({
name: 'ClassifierAgent',
systemPrompt: 'Classify the request as "code", "article", or "analysis"',
})
export class ClassifierAgent {
@Tool({
description: 'Classify request type',
parameters: [{ name: 'request', type: 'string', required: true }],
})
async classify(input: { request: string }) {
// Simple classification logic
if (input.request.includes('code') || input.request.includes('implement')) {
return { type: 'code' };
} else if (input.request.includes('analyze')) {
return { type: 'analysis' };
}
return { type: 'article' };
}
}
const router = runtime
.createGraph('smart-router')
.addNode('classifier', { type: 'agent', agentName: 'ClassifierAgent' })
.addNode('coder', { type: 'agent', agentName: 'CoderAgent' })
.addNode('analyst', { type: 'agent', agentName: 'AnalystAgent' })
.addNode('writer', { type: 'agent', agentName: 'WriterAgent' })
.setEntryPoint('classifier')
.addConditionalEdge('classifier', (state) => {
const type = state.data?.type;
if (type === 'code') return 'coder';
if (type === 'analysis') return 'analyst';
return 'writer';
})
.addEdge('coder', END)
.addEdge('analyst', END)
.addEdge('writer', END)
.compile();
const result = await router.execute('Implement a binary search algorithm');
// Routes to CoderAgent
Example 3: Parallel Fan-Out / Fan-In
import { GraphState, ParallelBranchResult } from '@hazeljs/agent';
// Function to split task
async function splitTask(state: GraphState) {
return {
...state,
data: {
...state.data,
split: true,
timestamp: Date.now(),
},
};
}
// Function to merge results
async function mergeResults(state: GraphState) {
const results = state.data?.branchResults as ParallelBranchResult[];
const combined = results.map(r => r.output).join('\n\n---\n\n');
return {
...state,
output: `# Combined Analysis\n\n${combined}`,
};
}
const parallel = runtime
.createGraph('parallel-research')
.addNode('splitter', { type: 'function', fn: splitTask })
.addNode('parallel-1', {
type: 'parallel',
branches: ['tech-researcher', 'market-researcher', 'competitor-researcher'],
})
.addNode('tech-researcher', { type: 'agent', agentName: 'TechResearchAgent' })
.addNode('market-researcher', { type: 'agent', agentName: 'MarketResearchAgent' })
.addNode('competitor-researcher', { type: 'agent', agentName: 'CompetitorResearchAgent' })
.addNode('combiner', { type: 'function', fn: mergeResults })
.addEdge('splitter', 'parallel-1')
.addEdge('parallel-1', 'combiner')
.addEdge('combiner', END)
.setEntryPoint('splitter')
.compile();
const result = await parallel.execute('Analyze the AI framework market');
console.log(result.output);
Streaming Execution
for await (const chunk of pipeline.stream('Tell me about GraphRAG')) {
if (chunk.type === 'node_start') {
console.log(`▶ Starting ${chunk.nodeId}...`);
}
if (chunk.type === 'node_complete') {
console.log(`✓ ${chunk.nodeId}: ${chunk.output?.slice(0, 80)}...`);
}
if (chunk.type === 'error') {
console.error(`✗ Error in ${chunk.nodeId}:`, chunk.error);
}
}
Visualization
const diagram = pipeline.visualize();
console.log(diagram);
// Outputs Mermaid diagram:
// graph TD
// researcher --> writer
// writer --> editor
// editor --> END
Key Benefits
✅ Visual — Easy to understand workflow structure
✅ Flexible — Sequential, conditional, parallel execution
✅ Composable — Mix agents and functions
✅ Debuggable — Stream execution events
Pattern 3: SupervisorAgent — LLM-Driven Routing
What is it?
SupervisorAgent uses an LLM to make routing decisions. It decomposes tasks into subtasks, routes each to the best worker agent, and continues until the task is complete.
Architecture
User Task
│
Supervisor ←──────────────────────────┐
│ │
┌───▼────────────────┐ Worker result
│ Route to worker? │ │
└───────────┬────────┘ │
│ │
┌──────▼──────┐ │
│ WorkerAgent │───────────────────┘
└─────────────┘
When to use it?
- Dynamic tasks — Task structure unknown at design time
- Complex decomposition — LLM decides how to break down work
- Adaptive routing — Route based on worker results
- 6+ agents — Too many to hardcode routing logic
Code Example
import { SupervisorConfig } from '@hazeljs/agent';
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const supervisor = runtime.createSupervisor({
name: 'project-manager',
workers: [
'ResearchAgent',
'CoderAgent',
'WriterAgent',
'EditorAgent',
'DesignerAgent',
],
maxRounds: 8,
llm: async (prompt: string) => {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
});
return response.choices[0].message.content ?? '';
},
});
const result = await supervisor.run(
'Build a REST API for a todo app with documentation and tests',
{ sessionId: 'project-001' }
);
console.log(result.response);
console.log(`\nExecution rounds: ${result.rounds.length}`);
result.rounds.forEach((round, i) => {
console.log(`\nRound ${i + 1}:`);
console.log(` Worker: ${round.worker}`);
console.log(` Task: ${round.task.slice(0, 80)}...`);
console.log(` Result: ${round.workerResult.response.slice(0, 80)}...`);
});
How it works
- Supervisor receives task — "Build a REST API..."
- LLM decomposes — "First, we need to design the API schema"
- Routes to DesignerAgent — Designs API endpoints
- Receives result — API schema completed
- LLM decides next step — "Now implement the endpoints"
- Routes to CoderAgent — Implements the code
- Continues — Documentation, tests, etc.
- Completes — All subtasks done
Key Benefits
✅ Adaptive — LLM decides routing dynamically
✅ Scalable — Handles many workers easily
✅ Intelligent — Can adjust based on results
✅ Flexible — No hardcoded workflow
Comparison: When to Use Which Pattern?
| Pattern | Best For | Complexity | Control | Flexibility |
|---|---|---|---|---|
@Delegate |
2-5 agents, clear hierarchy | Low | High | Low |
AgentGraph |
Known workflows, conditional logic | Medium | High | Medium |
SupervisorAgent |
Dynamic tasks, 6+ agents | High | Low | High |
Decision Tree
Do you know the exact workflow at design time?
├─ YES → Use AgentGraph
└─ NO → Is the task highly dynamic?
├─ YES → Use SupervisorAgent
└─ NO → Do you have 2-5 agents with clear roles?
├─ YES → Use @Delegate
└─ NO → Use SupervisorAgent
Complete Example: Content Creation Pipeline
Let's combine all three patterns to build a production-grade content creation system.
System Architecture
User Request → SupervisorAgent
↓
┌───────────┴───────────┐
▼ ▼
ContentGraph SEOGraph
(AgentGraph) (AgentGraph)
│ │
└───────────┬───────────┘
▼
OrchestratorAgent
(@Delegate pattern)
│
┌───────────┼───────────┐
▼ ▼ ▼
Research Writer Editor
Implementation
See the complete code in the hazeljs-multi-agent-ai-workflows-example repository.
Production Best Practices
1. Error Handling
@Tool({ description: 'Call external API' })
async callAPI(input: { endpoint: string }) {
try {
return await this.api.call(input.endpoint);
} catch (error) {
return {
success: false,
error: (error as Error).message,
retryable: true,
};
}
}
2. Observability
runtime.on(AgentEventType.EXECUTION_STARTED, (e) => {
logger.info('Agent started', { agentName: e.data.agentName });
});
runtime.on(AgentEventType.TOOL_EXECUTION_COMPLETED, (e) => {
metrics.increment('tool.executions', { tool: e.data.toolName });
});
3. State Persistence
// Use Redis for production
import { RedisStateManager } from '@hazeljs/agent';
const runtime = new AgentRuntime({
stateManager: new RedisStateManager({
redis: redisClient,
ttl: 3600,
}),
});
4. Approval Workflows
@Tool({
requiresApproval: true,
description: 'Deploy to production',
})
async deploy(input: { environment: string }) {
// Requires human approval before execution
}
runtime.on(AgentEventType.TOOL_APPROVAL_REQUESTED, async (event) => {
// Send to approval UI
await notifyApprovalSystem(event.data);
});
Performance Considerations
Parallel Execution
// Sequential: 30s total
researcher → writer → editor // 10s + 10s + 10s
// Parallel: 10s total
[tech-research ‖ market-research ‖ competitor-research] → combiner
Context Management
- Keep agents focused — Smaller contexts = faster inference
- Use delegation — Don't pass full context to every agent
- Cache results — Avoid re-running expensive operations
Cost Optimization
// Use cheaper models for routing
const supervisor = runtime.createSupervisor({
llm: async (prompt) => {
return await openai.chat.completions.create({
model: 'gpt-4o-mini', // Cheaper for routing decisions
messages: [{ role: 'user', content: prompt }],
});
},
});
// Use powerful models for specialized work
@Agent({
name: 'WriterAgent',
llmConfig: { model: 'gpt-4o' }, // Better for creative writing
})
Conclusion
Multi-agent orchestration is the key to building production-grade AI systems. HazelJS provides three powerful patterns:
-
@Delegatefor simple peer-to-peer delegation -
AgentGraphfor complex DAG workflows -
SupervisorAgentfor dynamic LLM-driven routing
Choose the right pattern for your use case, or combine them for maximum flexibility.
Next Steps
- Try the example — Clone hazeljs-multi-agent-ai-workflows-example
- Read the docs — hazeljs.ai/docs/agent
- Join the community — Discord
Resources
Built with HazelJS — Production-grade AI infrastructure for TypeScript.
Questions? Reach out on Discord.
Top comments (0)