HazelJS Agent OS
Agent OS is durable AI agents inside your TypeScript backend. The Agent Runtime is the kernel; Agent OS is how you package (DNA), govern writes (Skillgate), survive crashes (HITL), and declare desired state (local apply) — in the same DI app as your APIs.
Status: Agent OS kernel on npm — @hazeljs/agent (+ testing, benchmark, inspector, skillgate). Local Store + apply/reconcile shipped. Hosted registry is an optional Team SKU. Cloud marketplace/fleet remain product layers.
Primary path: Clone Meridian — store:sync → platform:sync → npm run dev.
Dual narrative
| Audience | Story |
|---|---|
| App builders | Durable agents in your TypeScript backend — DNA, HITL, Skillgate, local apply (same DI as HTTP) |
| Agent platform teams | Agent OS control plane — Store, Definitions/Deployments, reconcile; K8s optional |
Capability map
| Pillar | API |
|---|---|
| Confidence loop | options.loop — plan → execute → critique → validate |
| Durable process | AgentRun + createDurableRunStore / createSqlDurableRunStore |
| Durable HITL | durableSuspend · approveAndResume · Flow bridge |
| Worker leases | workerId · RepositoryAgentRunLeaseService · reclaimExpired |
| Identity / budget | AgentIdentity · capabilities · RunBudget |
| Scheduler | AgentScheduler · scheduleRun · queue adapter |
| Skillgate | @hazeljs/skillgate — curated REST → tools |
| State machine | AgentState + onState / onStateChange |
| Visual timeline | getTimeline · Inspector SSE · FileTimelineStore |
| Testing | @hazeljs/testing — describeAgent |
| Time travel | TimeTravelDebugger / runtime.getTimeTravel() |
| Benchmarks | @hazeljs/benchmark + hazel benchmark |
| Contracts | options.contract (+ fallback agent) |
| Policies | PolicyEngine / PolicyService — allow / deny / mask / capabilities |
| Recovery | options.recovery |
| Cost routing | CostOptimizer / options.costRoute |
| Skills | Skillgate · openApiToSkills / createSkillInvoker |
| Memory graph | AgentMemoryGraph + GraphRAG bridge |
| Evolution | evolveSystemPrompt / runEvolutionLoop |
| Simulator | runAgentSimulator |
| Knowledge freshness | assessKnowledgeFreshness |
| Agent DNA | exportAgentDna / hotReloadDna / bootstrapRuntimeFromDna / CLI hazel agent run |
| 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: Meridian Ops (flagship)
Concrete production teaching app — router, support, fraud HITL, Skillgate, DNA Store, and local apply.
Repo: hazeljs-meridian-ops.
cd hazeljs-meridian-ops
npm install
npm run store:sync # Publish DNA packages + lockfile
npm run platform:sync # Apply Definitions / Deployments (does not restart Node)
npm run dev
Also available (thinner starter): hazeljs-agent-os-starter (Nordhelm support desk) for a single support-agent slice — prefer Meridian for the full Agent OS path.
Durable runs, SQL store & worker leases (Gamma)
Agent OS Gamma makes agents process-like: crash-surviving HITL, SQL-backed run records (any Prisma SQL DB), and multi-worker fencing.
Durable HITL
const store = createDurableRunStore('.hazel/runs');
const runtime = new AgentRuntime({
llmProvider,
durableSuspend: true, // approve-required tools suspend instead of holding the worker
runRepository: store.runRepository,
checkpointService: store.checkpointService,
humanTaskService: store.humanTaskService,
});
const waiting = await runtime.execute('order-desk', 'Refund ORD-100');
// …restart process…
await runtime.approveAndResume(waiting.executionId, {
approved: true,
approvedBy: 'ops@example.com',
});
SQL process store (provider-agnostic)
import { createSqlDurableRunStore } from '@hazeljs/agent';
import { PrismaClient } from '@prisma/client';
// datasource.provider = postgresql | mysql | sqlite | sqlserver | …
const store = createSqlDurableRunStore(new PrismaClient());
Copy Agent OS models from @hazeljs/agent prisma-schema.example.prisma into your schema, migrate, and generate.
Worker leases
const runtime = new AgentRuntime({
workerId: process.env.WORKER_ID ?? `worker-${process.pid}`,
runLeaseTtlMs: 30_000,
runRepository: store.runRepository,
// …
});
import { RepositoryAgentRunLeaseService } from '@hazeljs/agent';
const leases = new RepositoryAgentRunLeaseService(store.runRepository);
await leases.reclaimExpired(); // RUNNING + expired lease → SUSPENDED
Production queue + heartbeat patterns: see the monorepo guide docs/agent-os-audit/19-queue-lease-worker-guide.md.
CLI
hazel agent doctor
hazel agent run ./agent.dna.json "hello" --mock
hazel agent runs list --dir .hazel/runs
hazel agent logs --timeline .hazel/runs/timeline.jsonl
hazel agent run uses bootstrapRuntimeFromDna — DNA + durable store + mock or OpenAI-compatible LLM.
Order-resolution reference
Multi-agent desk + fraud child (callAgent) + refund HITL + leases:
- Package example:
hazeljs/packages/agent/examples/order-resolution-gamma.ts - Alpha lite HITL:
examples/durable-hitl-alpha.ts
Skillgate
Curate REST → governed skills, then let Agent OS run the loop: Skillgate guide.