Foundation~45 minHazelJS
Build Your First Tool-Using Agent
Hands-on HazelJS: install packages, define @Agent/@Tool, wire AgentModule or AgentRuntime, execute with budgets, and add describeAgent tests.
- Authors
- editorial-team
- Published
- Last reviewed
- Node.js + TypeScript project (or clone hazeljs-agent-os-starter from the hazel-js/hazeljs monorepo)
- Familiarity with npm packages and basic DI/module setup
- An OpenAI-compatible API key — or willingness to use createMockLlmProvider for a dry run
- A bounded @Agent / @Tool class registered on AgentRuntime
- execute() with loop.maxIterations (and optional stages) plus maxSteps
- A describeAgent golden test and a clear path to real LLM providers
On this page
Goal
Ship a read-mostly tool-using agent on HazelJS that answers an operational question by calling 1–2 tools in a bounded AgentRuntime loop, with a tiny describeAgent suite. Do not start with irreversible writes.
Prefer the thin starter path: hazeljs-agent-os-starter in the hazel-js/hazeljs monorepo, or a minimal @Agent / @Tool module as below. Docs: Agent package, Agent OS.
Related lesson: /learn/agentic-development/scaffold-your-first-agent then /learn/agentic-development/tool-using-agent-loop. Pattern: /patterns/tool-using-agent.
Prerequisites
- TypeScript project with decorator support (
experimentalDecorators) - Ability to install
@hazeljs/agent,@hazeljs/core, and an LLM path (@hazeljs/aiorcreateMockLlmProviderfor demos) - One or two read APIs you can safely call (orders, tickets, docs search)
- Somewhere to store run logs / timelines (file is fine for v1)
Step 1Install packages
npm install @hazeljs/agent @hazeljs/core @hazeljs/ai
# optional later: @hazeljs/testing @hazeljs/skillgate @hazeljs/mcp
For production LLM calls, also install your provider SDK (commonly openai). For a demo without network LLM cost, use createMockLlmProvider from @hazeljs/agent (the Agent OS starter’s DemoLLM-style path).
Step 2Write the one-pager
Document before coding:
- Goal — e.g. “Given an order id, report status and open questions.”
- Done condition — structured fields the agent should fill from tools.
- Tools — names, parameters, side-effect class (
readonly for v1). - Budgets —
maxSteps: 6,loop.maxIterations: 4, optional cost/latency contract later. - Non-goals — refunds, shell, SQL writes, paging on-call.
If you cannot write this page, you are not ready to wire the runtime. Anthropic’s Building effective agents advice applies: simplest control structure first.
Step 3Define agent + tools
import { Agent, Tool } from '@hazeljs/agent';
@Agent({
name: 'ops-lookup',
description: 'Read-only order lookup agent',
systemPrompt:
'Use lookupOrder for facts. Never invent ids. Stop when you can answer.',
maxSteps: 6,
})
export class OpsLookupAgent {
@Tool({
name: 'lookupOrder',
description: 'Fetch order metadata by id (e.g. ORD-1001)',
parameters: [{ name: 'orderId', type: 'string', required: true }],
})
async lookupOrder({ orderId }: { orderId: string }) {
// Replace with your adapter
if (orderId !== 'ORD-1001') {
return { ok: false, error: `No order ${orderId}` };
}
return { ok: true, orderId, status: 'shipped', carrier: 'NordPost' };
}
}
Return structured { ok, … } / { ok: false, error }. Never throw raw exceptions into model-visible observations.
Step 4Wire AgentModule or AgentRuntime
Script / starter style:
import {
AgentRuntime,
createMockLlmProvider,
} from '@hazeljs/agent';
import { OpsLookupAgent } from './ops-lookup.agent';
async function main() {
// Demo path: mock LLM. Swap for OpenAIProvider (@hazeljs/ai) in real apps.
const llm = createMockLlmProvider(
'Demo: replace createMockLlmProvider with your LLM provider.'
);
const runtime = new AgentRuntime({
llmProvider: llm,
enableRetry: false,
enableCircuitBreaker: false,
});
runtime.registerAgent(OpsLookupAgent);
runtime.registerAgentInstance('ops-lookup', new OpsLookupAgent());
const result = await runtime.execute(
'ops-lookup',
'What is the status of ORD-1001?',
{
loop: {
maxIterations: 4,
stages: ['plan', 'execute', 'critique', 'validate'],
},
}
);
console.log(result.response);
}
main();
DI app style (services / Inspector):
import { AgentModule } from '@hazeljs/agent';
import { OpenAIProvider } from '@hazeljs/ai';
AgentModule.forRoot({
runtime: {
llmProvider: new OpenAIProvider(process.env.OPENAI_API_KEY!),
},
});
// Register @Agent classes as @Service() providers so Inspector can list them
Step 5Budgets and policy in code
Put controls in runtime options and @Agent config—not only in the system prompt:
maxStepson the agentloop.maxIterations(and optional stages) onexecute- Allow-list of tools (only what you register)
- Later:
PolicyEngine,costRoute,contract,governance
Prompts reinforce policy; they cannot enforce it.
Step 6Add a describeAgent test
import { describeAgent, expectTools, runAgentSuite } from '@hazeljs/testing';
const suite = describeAgent('Ops Lookup', ({ test }) => {
test('looks up ORD-1001', async ({ run }) => {
const r = await run('Status of ORD-1001?');
expectTools(r, ['lookupOrder'], 0.5);
});
test('unknown id does not call write tools', async ({ run }) => {
const r = await run('Status of ORD-UNKNOWN?');
// Assert response reports not found; no processRefund / shell tools exist yet
void r;
});
});
await runAgentSuite(suite);
Run these in CI on every prompt/tool change. Glossary: /glossary/evaluation (when published mapping applies).
Step 7Optional: Skillgate / MCP later
After local @Tool methods work:
- Curate REST → skills with Skillgate into
ToolRegistry - Expose or consume tools via
@hazeljs/mcp/ MCP
Do not start there. Host-mediated validation and budgets still apply.
Step 8Harden before writes
Only after reads work:
- Add
requiresApproval: trueon mutations - Prefer
durableSuspend+approveAndResume(/patterns/human-approval-gate) - Persist with
createDurableRunStore(/patterns/durable-agent-run)
Common mistakes
- Starting with a shell tool
- Using
createMockLlmProviderin prod and wondering why answers are stubby - Unlimited conversation history as “memory”
- Skipping
describeAgentbecause the demo looked good - Framework hopping before the one-pager exists
Done when
- A teammate can run the agent on two fixtures without your laptop magic
maxSteps/loop.maxIterationstrip correctly in a forced long-loop test- Traces or timeline explain a failure without reading the model’s mind