ACAgentic Craft

Foundation~35 minHazelJS

Tool-Using Agent Loop

Build a read-only Ops Desk: @Agent + @Tool, register on AgentRuntime, execute a goal, and follow one full think→act→observe cycle.

Authors
editorial-team
Published
Last reviewed
Progress is stored locally in this browser.

Direct answer

A tool-using agent loop in HazelJS is AgentRuntime.execute: assemble context → model proposes a tool or a final answer → runtime validates args → executes your @Tool → observation returns → repeat until success or a hard stop.

Today you build only the happy path: one read tool, one goal, one answer. No refunds, no MCP, no Skillgate yet.

Pattern: /patterns/tool-using-agent. Guide: /guides/build-your-first-tool-using-agent.

The story: Ops Desk

Goal: What is the status of order ORD-1001?
Done when: the agent reports status from a tool, not from imagination.
Non-goals: refunds, email, shell, SQL writes.

Write that on a sticky note. If the goal is fuzzy, the loop will thrash.

Step 1Define the agent + tool

typescript
import { Agent, Tool } from '@hazeljs/agent';

@Agent({
  name: 'ops-desk',
  description: 'Read-only order status desk',
  systemPrompt: [
    'You are Ops Desk for Nordhelm support.',
    'Use lookupOrder for facts. Never invent order ids or statuses.',
    'When you have enough tool evidence, give a short final answer.',
  ].join(' '),
  maxSteps: 6,
})
export class OpsDeskAgent {
  @Tool({
    name: 'lookupOrder',
    description: 'Fetch order status by id (example: ORD-1001)',
    parameters: [{ name: 'orderId', type: 'string', required: true }],
  })
  async lookupOrder({ orderId }: { orderId: string }) {
    if (orderId !== 'ORD-1001') {
      return { ok: false, error: `No order ${orderId}` };
    }
    return {
      ok: true,
      orderId,
      status: 'shipped',
      carrier: 'NordPost',
      eta: '2026-08-16',
    };
  }
}

Why structured returns? The model reads tool output as text. { ok, … } / { ok: false, error } is easier to reason about than thrown exceptions or HTML blobs.

Step 2Register and execute

typescript
import {
  AgentRuntime,
  createMockLlmProvider,
} from '@hazeljs/agent';
import { OpsDeskAgent } from './ops-desk.agent';

async function main() {
  const runtime = new AgentRuntime({
    llmProvider: createMockLlmProvider(
      'Replace with OpenAIProvider when you want a real model.'
    ),
  });

  runtime.registerAgent(OpsDeskAgent);
  runtime.registerAgentInstance('ops-desk', new OpsDeskAgent());

  const result = await runtime.execute(
    'ops-desk',
    'What is the status of ORD-1001?',
    {
      loop: {
        maxIterations: 4,
        stages: ['plan', 'execute', 'critique', 'validate'],
      },
    }
  );

  console.log('response:', result.response);
  console.log('executionId:', result.executionId);
}

main();

With a mock LLM you prove registration and wiring. With a real provider you should see lookupOrder called, then a final answer that mentions shipped / NordPost.

DI app variant (same idea)

typescript
import { AgentModule } from '@hazeljs/agent';
import { OpenAIProvider } from '@hazeljs/ai';

AgentModule.forRoot({
  runtime: {
    llmProvider: new OpenAIProvider(process.env.OPENAI_API_KEY!),
  },
});
// Register OpsDeskAgent as a service/provider in your HazelJS app

Step 3Walk one loop in plain language

PhaseWhoWhat happens
1. AssembleRuntimeGoal + system prompt + prior observations
2. ProposeModel“Call lookupOrder with orderId: ORD-1001or final text
3. ValidateRuntimeArgs must match the tool parameters
4. ExecuteYour codelookupOrder runs in-process
5. ObserveRuntimeResult is appended for the next model turn
6. StopRuntimeFinal answer, or maxSteps / other budget

If validation fails, the tool never runs. That is intentional.

Step 4Read the result like an engineer

After execute, inspect at least:

  • result.response — what the user would see
  • result.executionId — correlate later with timelines
  • Stop / status fields your runtime version exposes — why the run ended

If the model answers without calling lookupOrder, tighten the system prompt and add a golden test in lesson 6. Do not “hope” the next prompt tweak sticks.

What not to add yet

Skip for now (each has its own lesson or track):

  • Skillgate / MCP tool discovery
  • PolicyEngine / governance packs
  • Durable stores and HITL
  • Multi-agent graphs

Finish the read-only loop first. Anthropic’s effective agents advice applies: simplest control structure that works.

Checkpoint

  • @Agent({ name: 'ops-desk' }) matches execute('ops-desk', …)
  • lookupOrder returns structured JSON for ORD-1001
  • You can explain the six phases above without looking

What to do next

Add hard stops and budgets: /learn/agentic-development/bounded-runs-budgets-and-stops

Artifact: Ops Desk agent that answers “status of ORD-1001?” via lookupOrder

Sources

Related