HazelJS Agent OS
Agent OS is the production lifecycle layer around HazelJS agents. The Agent Runtime is the kernel; Agent OS is how you loop, govern, recover, observe, package, and test agents that ship.
Status: Available on npm — @hazeljs/agent, @hazeljs/testing, @hazeljs/benchmark, and @hazeljs/inspector.
Dual narrative
| Audience | Story |
|---|---|
| App builders | AI-native TypeScript backend — HTTP, DI, agents, RAG, workflows |
| Agent platform teams | Agent OS — production lifecycle for agents that ship |
Capability map
| Pillar | API |
|---|---|
| Confidence loop | options.loop — plan → execute → critique → validate |
| State machine | AgentState + onState / onStateChange (RETRYING / BLOCKED) |
| Visual timeline | getTimeline · Inspector SSE · optional FileTimelineStore |
| Testing | @hazeljs/testing — describeAgent |
| Time travel | TimeTravelDebugger / runtime.getTimeTravel() |
| Benchmarks | @hazeljs/benchmark + hazel benchmark |
| Contracts | options.contract (+ fallback agent) |
| Policies | PolicyEngine — allow / deny / mask / require_approval |
| Recovery | options.recovery |
| Cost routing | CostOptimizer / options.costRoute |
| Skills | openApiToSkills / createSkillInvoker |
| Memory graph | AgentMemoryGraph + GraphRAG bridge |
| Evolution | evolveSystemPrompt / runEvolutionLoop |
| Simulator | runAgentSimulator |
| Knowledge freshness | assessKnowledgeFreshness |
| Agent DNA | exportAgentDna / hotReloadDna / installAgentPackage |
| Digital twin | runDigitalTwin / shouldRunCanary |
| Consensus | runConsensus |
| Governance | GovernanceGate / options.governance |
Execute with Agent OS options
await runtime.execute('support-desk', message, {
loop: {
maxIterations: 4,
successScore: 90,
stages: ['plan', 'execute', 'critique', 'validate'],
},
contract: {
name: 'support-desk-slo',
maxLatencyMs: 30_000,
fallbackAgent: 'safe-desk',
},
recovery: {
maxRetries: 2,
fallbackAgent: 'safe-desk',
steps: ['retry', 'fallback_agent', 'fail'],
},
costRoute: { qualityBias: 0.35, maxCostUsd: 0.05 },
governance: {
action: 'agent.execute',
roles: ['agent:run'],
residency: 'eu',
compliancePacks: ['soc2'],
},
});
Wire policies and governance on the runtime (or via AgentModule.forRoot):
import {
AgentModule,
PolicyEngine,
defaultPiiMaskPolicies,
GovernanceGate,
defaultAgentGovernance,
FileTimelineStore,
CostOptimizer,
} from '@hazeljs/agent';
AgentModule.forRoot({
runtime: {
llmProvider,
policyEngine: new PolicyEngine([
...defaultPiiMaskPolicies(),
{
id: 'refund-needs-approval',
tool: 'processRefund',
effect: 'require_approval',
priority: 20,
},
]),
governanceGate: new GovernanceGate(defaultAgentGovernance()),
costOptimizer: new CostOptimizer(),
timelineStore: new FileTimelineStore('./.hazel/timeline.jsonl'),
},
});
Register @Agent classes as @Service() providers so Inspector can list them and Run / Timeline work against AgentService.
DNA & marketplace
What is Agent DNA?
Agent DNA is a portable JSON snapshot of an agent definition — the parts that usually live as tribal knowledge across prompts, tool lists, and policy rules. Instead of “copy this class and hope,” you export a versioned artifact (format: hazeljs.agent.dna) you can review, store, share, and apply to a live runtime.
A DNA document typically includes:
| Field | Purpose |
|---|---|
name / version | Identity and semver of the packaged agent |
systemPrompt / model | Behavior and model preference |
tools[] | Tool names, descriptions, requiresApproval |
policies[] | Declarative policy rules (e.g. refund HITL) |
contracts[] | Optional SLO / I/O contracts bundled with the agent |
exportedAt | When the snapshot was produced |
DNA is configuration + contract, not a full binary of your TypeScript handlers. Tool implementations still live in your app (or get wired via dynamic skill handlers). DNA tells the runtime which tools exist, which need approval, and which policies/prompts to apply.
Why it matters
Without DNA, shipping an agent change often means a full app redeploy. With DNA you can:
- Export the current definition for review / audit / backup
- Hot-reload prompt, model, metadata, and policies on a running agent
- Install a package from disk (CLI or API) into a live
AgentRuntime - Wrap DNA in a marketplace package (
readme,keywords) for sharing
Export DNA
import { exportAgentDna, serializeDna, toMarketplacePackage, saveMarketplacePackage } from '@hazeljs/agent';
const dna = exportAgentDna({
name: 'support-desk',
description: 'Nordhelm Commerce support desk',
systemPrompt,
model: 'gpt-4o-mini',
tools: [
{ name: 'lookupOrder', description: 'Look up order' },
{ name: 'processRefund', description: 'Process refund', requiresApproval: true },
],
policies: [
{
id: 'refund-hitl',
tool: 'processRefund',
effect: 'require_approval',
priority: 20,
reason: 'Refunds require human approval',
},
],
contracts: [{ name: 'support-desk-slo', maxLatencyMs: 30_000, fallbackAgent: 'safe-desk' }],
version: '1.0.0',
});
// Persist raw DNA
fs.writeFileSync('./dna/support-desk.dna.json', serializeDna(dna));
// Or wrap as a marketplace package
const pkg = toMarketplacePackage(dna, {
readme: 'Nordhelm Commerce support desk agent',
keywords: ['support', 'ecommerce', 'agent-os'],
});
saveMarketplacePackage(pkg, './dna/support-desk.marketplace.json');
In the Nordhelm starter: GET /api/support/dna and npm run dna:export.
Hot-reload and install
hotReloadDna patches a registered agent in place (system prompt, description, model, metadata, policies, dynamic tools). The agent must already exist on the runtime — DNA does not create a blank agent from nothing.
// From JSON string or object
runtime.hotReloadDna(JSON.stringify(dna));
// From a marketplace / DNA file on disk
import { installAgentPackage } from '@hazeljs/agent';
installAgentPackage(runtime, './dna/support-desk.marketplace.json');
// CLI (when published):
// hazel agent install ./support-desk.dna.json
What hot-reload updates today
systemPrompt,description,model,metadata(incl.dnaVersion)- Policy engine rules when
policiesare present - Dynamic tool registrations when tools include handlers / DNA skill wiring
What still requires a deploy
- Changing TypeScript tool implementation bodies
- Adding brand-new agent classes that were never registered
Marketplace package shape
{
name: '@hazeljs/support-desk-agent',
version: '1.0.0',
description: '...',
dna: { format: 'hazeljs.agent.dna', /* ... */ },
readme: '...',
keywords: ['hazeljs', 'agent', 'dna']
}
Install path works from files today. A hosted catalog is optional product work — the format and installAgentPackage / CLI path are the Agent OS surface.
Digital twin (canary)
What is a digital twin?
A digital twin (canary) runs a shadow agent alongside your primary agent on the same input, then compares outputs. The caller always gets the primary result. The twin is for observation: drift detection, safer rollouts, and comparing a conservative fallback (e.g. safe-desk) against the full desk (support-desk).
This is not A/B routing of user traffic. It is shadow execution + compare.
Why it matters
| Without a twin | With runDigitalTwin |
|---|---|
| You change a prompt and hope | You compare old vs new (or full vs safe) on live inputs |
| Twin failures can break the request | swallowTwinErrors: true keeps primary healthy |
| “Looks similar” is vibes | Jaccard similarity + explicit divergences[] |
Typical uses:
- Canary a prompt / DNA change — primary = current, twin = candidate
- Conservative shadow — primary = full support desk, twin = lookup-only safe desk
- Sampled production traffic — use
shouldRunCanary(0.1)so only ~10% of requests pay the twin cost
API
import { runDigitalTwin, shouldRunCanary } from '@hazeljs/agent';
if (shouldRunCanary(0.1)) {
const { primary, twin, compare } = await runDigitalTwin({
runPrimary: () => runtime.execute('support-desk', message, { /* Agent OS options */ }),
runTwin: () => runtime.execute('safe-desk', message),
matchThreshold: 0.4, // default 0.85 — lower = looser match
swallowTwinErrors: true, // twin errors become divergences, not thrown
});
// Always return primary to the user
return { response: primary.response, twin: compare };
}
// Non-canary path: primary only
return runtime.execute('support-desk', message);
What compare contains
| Field | Meaning |
|---|---|
primaryOutput / twinOutput | Response strings compared |
similarity | Jaccard token similarity 0..1 |
match | similarity >= matchThreshold and no twin error |
primaryDurationMs / twinDurationMs | Latency of each run |
divergences | Reasons such as similarity_below_0.4, twin_error:…, text_diff_soft_match |
Primary runs first; twin runs after. Twin latency does not replace primary latency in the user-facing result, but total wall time is primary + twin unless you parallelize inside your own wrappers.
Nordhelm example
curl -s localhost:3040/api/support/chat \
-H 'content-type: application/json' \
-d '{"message":"Status of ORD-1003","canary":true}' | jq .twin
The starter’s canary: true flag calls runDigitalTwin with support-desk as primary and safe-desk as twin — useful to see when the conservative agent would answer differently (e.g. no refunds).
Tips
- Start with a lower
matchThresholdfor free-form support answers; raise it for deterministic tool-heavy flows. - Log
compare.divergencesandsimilarityto metrics — treat twin as a sensor, not a gate, until you trust it. - Pair with DNA: hot-reload a candidate prompt onto a twin agent name, canary against primary, then promote DNA to primary when similarity stays high.
Observe
| Endpoint | Description |
|---|---|
GET /__hazel/agents/:name/stream | SSE live timeline |
GET /__hazel/agents/:name/timeline | JSON replay |
POST /__hazel/agents/:name/run | Run from Inspector UI |
runtime.getTimeline({ agentName: 'support-desk' });
Test
import { describeAgent, runAgentSuite, expectTools } from '@hazeljs/testing';
const suite = describeAgent('Support Desk', ({ test }) => {
test('tracks ORD-1001', async ({ run }) => {
const r = await run('Where is my package for ORD-1001?');
expectTools(r, ['trackShipment'], 0.5);
});
});
See @hazeljs/testing.
Showcase: Nordhelm Support Desk
Concrete production example — e-commerce support with orders, tracking, refunds (HITL), loop, policies, contracts, recovery, cost, governance, DNA, twin, and Inspector.
Repo folder: hazeljs-agent-os-starter (sibling to hazeljs in the monorepo workspace).
cd hazeljs-agent-os-starter
npm install
npm run demo # three tickets, DemoLLM (no API key)
npm run dev # http://localhost:3040
curl -s localhost:3040/api/support/chat \
-H 'content-type: application/json' \
-d '{"message":"Where is my package for ORD-1001?"}'
curl -s localhost:3040/api/support/chat \
-H 'content-type: application/json' \
-d '{"message":"I want a refund for ORD-1002","canary":true}'
Open /__hazel for Agents → Run / Timeline. Sample orders: ORD-1001 (shipped), ORD-1002 (refund), ORD-1003 (processing).