@hazeljs/agent
v1.0.6
Published
AI-native Agent Runtime for HazelJS - stateful, long-running agents with tools, memory, and human-in-the-loop
Maintainers
Readme
@hazeljs/agent
Build AI agents that actually do things.
Part of the HazelJS AI-native backend and Agent OS. Stateful, tool-using, memory-enabled — plus confidence loops, a rich state machine, Inspector timelines, and CI tests via @hazeljs/testing. Define tools with @Tool, delegate between agents with @Delegate, orchestrate multi-agent pipelines with AgentGraph, and route tasks automatically with SupervisorAgent.
🚀 Trusted by 200K+ monthly downloads • 37+ GitHub stars • 15+ daily active developers
Why @hazeljs/agent?
Built for AI-native applications - not just another agent framework. When you combine @hazeljs/agent with @hazeljs/core, @hazeljs/ai, and @hazeljs/rag, you get a complete stack for intelligent backends.
Perfect for:
- AI startups building production agent systems
- Teams creating customer support or automation agents
- Developers who want stateful, long-running workflows
- Projects needing multi-agent orchestration
Overview
Unlike stateless request handlers, agents are:
- Stateful — Maintain context across multiple steps and sessions
- Long-running — Execute complex, multi-hop workflows over time
- Tool-using — Call functions safely with timeout, retry, and approval workflows
- Multi-agent — Orchestrate teams of specialised agents with
AgentGraph,SupervisorAgent, and@Delegate - Memory-enabled — Integrate with persistent memory systems
- Observable — Full event system for monitoring and debugging
- Resumable — Support pause/resume and human-in-the-loop
Agent OS (Phase 1)
// Confidence loop — plan → execute → critique → validate
await runtime.execute('support-agent', goal, {
loop: { maxIterations: 8, successScore: 95 },
});
// Rich state subscriptions
runtime.onState('planning', (e) => console.log('planning', e));
runtime.onStateChange((e) => console.log(e.from, '→', e.to));
// Timeline for Inspector / debugging
const steps = runtime.getTimeline({ executionId });Install @hazeljs/testing for describeAgent CI suites, and @hazeljs/inspector for SSE timelines at /__hazel/agents/:name/stream.
Agent OS Phases 2–4 (highlights)
import {
PolicyEngine,
TimeTravelDebugger,
CostOptimizer,
exportAgentDna,
runConsensus,
GovernanceGate,
} from '@hazeljs/agent';
runtime.setPolicyEngine(
new PolicyEngine([{ id: 'deny-x', tool: 'x', effect: 'deny', priority: 10 }])
);
await runtime.execute('support-agent', goal, {
contract: {
name: 'refund',
outputIncludes: 'refund',
maxLatencyMs: 8000,
fallbackAgent: 'safe-agent',
},
recovery: { maxRetries: 3, fallbackAgent: 'safe-agent' },
costRoute: { qualityBias: 0.3, maxCostUsd: 0.05 },
});
const fork = runtime.getTimeTravel().fork(executionId);Also: openApiToSkills, AgentMemoryGraph, runEvolutionLoop, runAgentSimulator, runDigitalTwin, hazel benchmark (@hazeljs/benchmark).
Installation
npm install @hazeljs/agent @hazeljs/core @hazeljs/ragQuick Start — Single Agent
1. Define an agent
import { Agent, Tool } from '@hazeljs/agent';
@Agent({
name: 'support-agent',
description: 'Customer support agent',
systemPrompt: 'You are a helpful customer support agent.',
enableMemory: true,
enableRAG: true,
})
export class SupportAgent {
@Tool({
description: 'Look up order information by order ID',
parameters: [{ name: 'orderId', type: 'string', description: 'The order ID', required: true }],
})
async lookupOrder(input: { orderId: string }) {
return { orderId: input.orderId, status: 'shipped', trackingNumber: 'TRACK123' };
}
@Tool({
description: 'Process a refund for an order',
requiresApproval: true, // requires human approval before execution
parameters: [
{ name: 'orderId', type: 'string', required: true },
{ name: 'amount', type: 'number', required: true },
],
})
async processRefund(input: { orderId: string; amount: number }) {
return { success: true, refundId: 'REF123', amount: input.amount };
}
}2. Set up the runtime
import { AgentRuntime, AgentService } from '@hazeljs/agent';
import { MemoryManager } from '@hazeljs/rag';
import { AIEnhancedService } from '@hazeljs/ai';
const runtime = new AgentRuntime({
memoryManager: new MemoryManager(/* ... */),
llmProvider: AgentService.createLLMProviderFromAI(new AIEnhancedService()),
defaultMaxSteps: 10,
// Optional: pass observabilityProvider from @hazeljs/observability for OTel spans
});
const agent = new SupportAgent();
runtime.registerAgent(SupportAgent);
runtime.registerAgentInstance('support-agent', agent);3. Execute
const result = await runtime.execute('support-agent', 'I need to check my order #12345', {
sessionId: 'user-session-123',
userId: 'user-456',
enableMemory: true,
});
console.log(result.response);
console.log(`Completed in ${result.steps.length} steps`);4. Handle human-in-the-loop
runtime.on('tool.approval.requested', async (event) => {
console.log('Approval needed:', event.data);
runtime.approveToolExecution(event.data.requestId, 'admin-user');
});
const resumed = await runtime.resume(result.executionId);Multi-Agent Orchestration
@hazeljs/agent ships three complementary patterns for coordinating multiple agents. Use them individually or combine them.
Pattern 1 — @Delegate: peer-to-peer agent calls
@Delegate marks a method on an agent as a delegation point to another agent. The method body is replaced at runtime with an actual runtime.execute(targetAgent, input) call — making agent-to-agent communication completely transparent to the LLM (it sees delegation targets as ordinary tools).
OrchestratorAgent
└── @Delegate → ResearchAgent
└── @Delegate → WriterAgentimport { Agent, Delegate } from '@hazeljs/agent';
@Agent({
name: 'OrchestratorAgent',
description: 'Plans and delegates research and writing tasks',
systemPrompt: 'You orchestrate research and writing. Use the available tools to complete tasks.',
})
export class OrchestratorAgent {
// The LLM sees this as a tool. At runtime it calls ResearchAgent.
@Delegate({
agent: 'ResearchAgent',
description: 'Research a topic thoroughly and return key findings',
inputField: 'query',
})
async researchTopic(query: string): Promise<string> {
return ''; // body replaced at runtime by AgentRuntime
}
// The LLM sees this as a tool. At runtime it calls WriterAgent.
@Delegate({
agent: 'WriterAgent',
description: 'Write a polished article from the provided research notes',
inputField: 'content',
})
async writeArticle(content: string): Promise<string> {
return ''; // body replaced at runtime by AgentRuntime
}
}
@Agent({ name: 'ResearchAgent', systemPrompt: 'You are an expert researcher.' })
export class ResearchAgent {
@Tool({
description: 'Search the web',
parameters: [{ name: 'query', type: 'string', required: true }],
})
async searchWeb(input: { query: string }) {
return `Research findings for: ${input.query}`;
}
}
@Agent({ name: 'WriterAgent', systemPrompt: 'You are a professional technical writer.' })
export class WriterAgent {
@Tool({
description: 'Format content as Markdown',
parameters: [{ name: 'raw', type: 'string', required: true }],
})
async formatMarkdown(input: { raw: string }) {
return `## Article\n\n${input.raw}`;
}
}Registration:
const orchestrator = new OrchestratorAgent();
const researcher = new ResearchAgent();
const writer = new WriterAgent();
[ResearchAgent, WriterAgent, OrchestratorAgent].forEach((A) => runtime.registerAgent(A));
[
['OrchestratorAgent', orchestrator],
['ResearchAgent', researcher],
['WriterAgent', writer],
].forEach(([name, inst]) => runtime.registerAgentInstance(name as string, inst));
const result = await runtime.execute('OrchestratorAgent', 'Write a blog post about LLMs');
console.log(result.response);Note:
@Delegateimplicitly registers the method as@Tool. Do not add@Toolseparately.
Pattern 2 — AgentGraph: DAG pipelines
AgentGraph lets you wire agents and functions into a directed acyclic graph with sequential edges, conditional routing, and parallel fan-out/fan-in. Think LangGraph but TypeScript-native and integrated with AgentRuntime.
Entry → NodeA → NodeB → END (sequential)
Entry → RouterNode → NodeA | NodeB → END (conditional)
Entry → Splitter → [NodeA ‖ NodeB] → Combiner → END (parallel)import { END } from '@hazeljs/agent';
// Create graph via the runtime
const graph = runtime.createGraph('research-pipeline');Sequential pipeline
const pipeline = runtime
.createGraph('blog-pipeline')
.addNode('researcher', { type: 'agent', agentName: 'ResearchAgent' })
.addNode('writer', { type: 'agent', agentName: 'WriterAgent' })
.addEdge('researcher', 'writer')
.addEdge('writer', END)
.setEntryPoint('researcher')
.compile();
const result = await pipeline.execute('Write a blog post about TypeScript generics');
console.log(result.output);Conditional routing
const router = runtime
.createGraph('router')
.addNode('classifier', { type: 'agent', agentName: 'ClassifierAgent' })
.addNode('coder', { type: 'agent', agentName: 'CoderAgent' })
.addNode('writer', { type: 'agent', agentName: 'WriterAgent' })
.setEntryPoint('classifier')
.addConditionalEdge('classifier', (state) => (state.data?.type === 'code' ? 'coder' : 'writer'))
.addEdge('coder', END)
.addEdge('writer', END)
.compile();
const result = await router.execute('Write a sorting algorithm in TypeScript');Parallel fan-out / fan-in
async function splitTask(state: GraphState) {
return { ...state, data: { ...state.data, split: true } };
}
async function mergeResults(state: GraphState) {
const results = state.data?.branchResults as ParallelBranchResult[];
return { ...state, output: results.map((r) => r.output).join('\n---\n') };
}
const parallel = runtime
.createGraph('parallel-research')
.addNode('splitter', { type: 'function', fn: splitTask })
.addNode('parallel-1', { type: 'parallel', branches: ['tech-researcher', 'market-researcher'] })
.addNode('tech-researcher', { type: 'agent', agentName: 'TechResearchAgent' })
.addNode('market-researcher', { type: 'agent', agentName: 'MarketResearchAgent' })
.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('Analyse the AI framework market');Streaming execution
for await (const chunk of pipeline.stream('Tell me about GraphRAG')) {
if (chunk.type === 'node_complete') {
console.log(`✓ ${chunk.nodeId}: ${chunk.output?.slice(0, 80)}...`);
}
}AgentGraph API
interface AgentGraph {
addNode(id: string, config: GraphNodeConfig): this;
addEdge(from: string, to: string): this;
addConditionalEdge(from: string, router: RouterFunction): this;
setEntryPoint(nodeId: string): this;
compile(): CompiledGraph;
}
interface CompiledGraph {
execute(input: string, options?: GraphExecutionOptions): Promise<GraphExecutionResult>;
stream(input: string, options?: GraphExecutionOptions): AsyncIterable<GraphStreamChunk>;
visualize(): string; // returns a Mermaid diagram string
}Pattern 3 — SupervisorAgent: LLM-driven routing
SupervisorAgent uses an LLM to decompose tasks into subtasks, route each subtask to the best worker agent, and accumulate results — continuing until the task is complete or maxRounds is reached.
User Task
│
Supervisor ←──────────────────────────┐
│ │
┌───▼────────────────┐ Worker result
│ Route to worker? │ │
└───────────┬────────┘ │
│ │
┌──────▼──────┐ │
│ WorkerAgent │───────────────────┘
└─────────────┘const supervisor = runtime.createSupervisor({
name: 'project-manager',
workers: ['ResearchAgent', 'CoderAgent', 'WriterAgent'],
maxRounds: 6,
llm: async (prompt) => {
const res = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
});
return res.choices[0].message.content ?? '';
},
});
const result = await supervisor.run('Build and document a REST API for a todo app', {
sessionId: 'proj-001',
});
console.log(result.response);
result.rounds.forEach((round, i) => {
console.log(
`Round ${i + 1}: routed to ${round.worker} — ${round.workerResult.response.slice(0, 80)}`
);
});SupervisorConfig:
| Field | Type | Default | Description |
| ----------- | ------------------------------------- | ------- | -------------------------------------------------- |
| name | string | — | Supervisor instance name |
| workers | string[] | — | Registered agent names available to the supervisor |
| maxRounds | number | 5 | Maximum routing iterations |
| llm | (prompt: string) => Promise<string> | — | LLM function for routing decisions |
| sessionId | string | auto | Session for memory continuity across rounds |
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ AgentRuntime │
├──────────────┬───────────────┬───────────────┬───────────────────┤
│ Registry │ State Mgr │ Executor │ Tool Executor │
│ (agents, │ (in-mem / │ (step loop, │ (timeout, │
│ tools) │ Redis / DB) │ approval) │ retry, audit) │
├──────────────┴───────────────┴───────────────┴───────────────────┤
│ Multi-Agent Layer │
│ ┌──────────────┐ ┌───────────────────┐ ┌────────────────────┐ │
│ │ AgentGraph │ │ SupervisorAgent │ │ @Delegate │ │
│ │ (DAG pipes) │ │ (LLM routing) │ │ (peer-to-peer) │ │
│ └──────────────┘ └───────────────────┘ └────────────────────┘ │
├────────────────────────────────────────────────────────────────── ┤
│ Memory Module (@hazeljs/rag) │ RAG Module │
└──────────────────────────────────────────────────────────────────┘State Machine
Every agent execution follows a deterministic state machine:
idle → thinking → using_tool → thinking → ... → completed
↓
waiting_for_input
↓
waiting_for_approval
↓
failedEvent System
import { AgentEventType } from '@hazeljs/agent';
runtime.on(AgentEventType.EXECUTION_STARTED, (e) => console.log('started:', e.data));
runtime.on(AgentEventType.EXECUTION_COMPLETED, (e) => console.log('completed:', e.data));
runtime.on(AgentEventType.STEP_STARTED, (e) => console.log('step:', e.data));
runtime.on(AgentEventType.TOOL_EXECUTION_STARTED, (e) => console.log('tool:', e.data));
runtime.on(AgentEventType.TOOL_APPROVAL_REQUESTED, (e) => {
console.log('approval needed:', e.data);
runtime.approveToolExecution(e.data.requestId, 'admin');
});
// Catch-all
runtime.onAny((e) => console.log(e.type, e.data));Production deployment (1.0.1+)
For multi-instance deployments, use Redis-backed state and durable approvals:
import { AgentModule } from '@hazeljs/agent';
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
await AgentModule.forRootAsync({
redis: { client: redis },
useRedisApprovals: true,
runtime: {
strictEventHandlers: process.env.NODE_ENV === 'production',
observabilityProvider: myObservabilityProvider, // optional, from @hazeljs/observability
enableMetrics: true,
enableCircuitBreaker: true,
},
});Environment variables: REDIS_URL, AGENT_STATE_BACKEND (memory | redis | database)
Factories: createStateManager, createStateManagerFromEnv, createApprovalStore
Observability: Pass observabilityProvider for OpenTelemetry spans (agent.execute, agent.tool.execute, agent.llm). The enableObservability flag is reserved; spans require an injected provider.
Resilience: Circuit breaker and retry use @hazeljs/resilience internally.
See PERSISTENCE.md, PRODUCTION_READINESS.md, and QUICKSTART.md.
HazelJS Module Integration
import { HazelModule } from '@hazeljs/core';
import { AgentModule } from '@hazeljs/agent';
import { RagModule } from '@hazeljs/rag';
@HazelModule({
imports: [
RagModule.forRoot({
/* ... */
}),
AgentModule.forRoot({
runtime: {
defaultMaxSteps: 10,
enableMetrics: true,
strictEventHandlers: process.env.NODE_ENV === 'production',
},
agents: [SupportAgent, ResearchAgent, WriterAgent, OrchestratorAgent],
}),
],
})
export class AppModule {}Best Practices
Keep tools idempotent
@Tool({ description: 'Create an order' })
async createOrder(input: { orderId: string; items: Item[] }) {
const existing = await this.findOrder(input.orderId);
if (existing) return existing; // safe to retry
return this.createNewOrder(input);
}Use @Delegate for domain specialisation
Keep each agent focused on one domain. @Delegate lets the orchestrator combine specialists without any agent becoming a monolith.
Choose the right multi-agent pattern
| Pattern | Use when |
| ----------------- | ------------------------------------------------------------- |
| @Delegate | Two or three agents with a clear orchestrator / worker split |
| AgentGraph | Workflow is known at design time; conditional routing matters |
| SupervisorAgent | Task decomposition is dynamic; you want LLM-driven routing |
Require approval for destructive actions
@Tool({ requiresApproval: true, description: 'Delete user account' })
async deleteAccount(input: { userId: string }) { /* ... */ }Handle errors in tools
@Tool({ description: 'Call external API' })
async callExternalAPI(input: { endpoint: string }) {
try {
return await this.api.call(input.endpoint);
} catch (error) {
return { success: false, error: (error as Error).message };
}
}API Reference
AgentRuntime
class AgentRuntime {
execute(
agentName: string,
input: string,
options?: ExecuteOptions
): Promise<AgentExecutionResult>;
resume(executionId: string, input?: string): Promise<AgentExecutionResult>;
registerAgent(agentClass: new (...args: unknown[]) => unknown): void;
registerAgentInstance(name: string, instance: unknown): void;
createGraph(name: string): AgentGraph;
createSupervisor(config: SupervisorConfig): SupervisorAgent;
approveToolExecution(requestId: string, approvedBy: string): void;
rejectToolExecution(requestId: string, reason?: string): void;
on(event: string, handler: (e: AgentEvent) => void): void;
onAny(handler: (e: AgentEvent) => void): void;
}Decorators
| Decorator | Target | Description |
| ------------------- | ------ | ------------------------------------------------------------------------ |
| @Agent(config) | Class | Declares a class as an agent |
| @Tool(config) | Method | Exposes a method as an LLM-callable tool |
| @Delegate(config) | Method | Delegates a method to another agent (registers as @Tool automatically) |
GraphNodeConfig types
// Agent node — runs a registered agent
{ type: 'agent', agentName: string }
// Function node — runs a custom function
{ type: 'function', fn: (state: GraphState) => Promise<GraphState> }
// Parallel node — fans out to multiple branches simultaneously
{ type: 'parallel', branches: string[] }Examples
- hazeljs-ai-multiagent-starter — Full multi-agent REST API with
AgentGraph,SupervisorAgent, and@Delegate - hazeljs-rag-documents-starter — RAG + GraphRAG knowledge base API
License
Apache 2.0
Contributing
Contributions are welcome! See CONTRIBUTING.md for details.
