ACAgentic Craft

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

Before you start

  • 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

You will leave with

  • 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/ai or createMockLlmProvider for 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

bash
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:

  1. Goal — e.g. “Given an order id, report status and open questions.”
  2. Done condition — structured fields the agent should fill from tools.
  3. Tools — names, parameters, side-effect class (read only for v1).
  4. BudgetsmaxSteps: 6, loop.maxIterations: 4, optional cost/latency contract later.
  5. 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

typescript
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:

typescript
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):

typescript
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:

  • maxSteps on the agent
  • loop.maxIterations (and optional stages) on execute
  • 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

typescript
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:

Do not start there. Host-mediated validation and budgets still apply.

Step 8Harden before writes

Only after reads work:

Common mistakes

  • Starting with a shell tool
  • Using createMockLlmProvider in prod and wondering why answers are stubby
  • Unlimited conversation history as “memory”
  • Skipping describeAgent because 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.maxIterations trip correctly in a forced long-loop test
  • Traces or timeline explain a failure without reading the model’s mind

Next

Sources

Continue learning