Documentation•Reference

HazelJS Decision Package

npm downloads

@hazeljs/​decision is the native Decision Runtime for HazelJS Agent OS — bounded, typed, auditable judgment as a first-class primitive.

It is not a thin wrapper around Jev, OpenAI, Gemini, or another external model. The default provider is hazel-agent: an evidence → candidates → judge → critic → confidence pipeline built from HazelJS primitives. External providers are optional adapters; every result still passes policy and Gatekeeper.

Models propose. Agents evaluate. Critics challenge. Confidence informs. Policies govern. Skillgate classifies. Gatekeeper authorizes. Durable Kernel executes. Agent VM isolates effects. Agent OS observes.

Reasoning ≠ Decision ≠ Policy ≠ Authorization ≠ Execution. Confidence is evidence for policy routing, never permission to mutate the world. High confidence never bypasses Skillgate / Agent Gatekeeper.

Quick Reference

  • Purpose: Bounded choice over a closed set (choices: [...] as const) with risk routing, confidence, deterministic policy, Gatekeeper auth, durable HITL, and optional capability execution.
  • When to use: Incident remediation, refund paths, ticket routing, fraud/risk triage, “needs human?” gates — anywhere an agent must pick from a known set instead of free-form LLM JSON.
  • Key APIs: createDecisionRuntime, decide, resumeFromHuman, replay, getResult / getResultAsync, registerCapability, evaluate, DecisionLab / createDecisionLab, createGovernedDecisionRuntime, DecisionModule / DecisionService, HazelDecisionAgent, MockDecisionProvider, createOpenAiDecisionProvider / createGeminiDecisionProvider / createJevDecisionProvider / createLocalDecisionProvider, createEnsembleDecisionProvider, registerShadowProvider / runWithShadow, DecisionCache, DecisionHistory (trends, similarByEvidence), fitCalibrationFromHistory, projectDecisionFlow, createCostAwareRouter, hybridRules, @Decision.
  • Dependencies: Peer @hazeljs/​agent. Optional peers: @hazeljs/​agent-gatekeeper, @hazeljs/​skillgate, @hazeljs/​audit, @hazeljs/​ai, @hazeljs/​core, @hazeljs/​guardrails, @hazeljs/​observability, @opentelemetry/​api.
  • Common mistakes: Treating confidence as authorization; letting models invent choices outside the closed set; skipping Gatekeeper when execute: true; auto-feeding history into prompts; assuming calibration authorizes; expecting Flow projection to replace DecisionRuntime.

Installation

npm install @hazeljs/​decision @hazeljs/​agent
# recommended for production execution:
npm install @hazeljs/​agent-gatekeeper @hazeljs/​skillgate
# optional:
npm install @hazeljs/​ai @hazeljs/​audit @hazeljs/​cli

Or:

hazel add decision

Minimal example

import { createDecisionRuntime } from '@hazeljs/​decision';

const decisions = createDecisionRuntime();

const result = await decisions.decide({
  objective: 'Choose the safest production remediation',
  state: {
    errorRate: 0.38,
    previousErrorRate: 0.01,
    deploymentAgeMinutes: 4,
    failedHealthChecks: 8,
  },
  choices: ['retry', 'rollback', 'escalate', 'ignore'] as const,
});

/​/​ result.decision: 'retry' | 'rollback' | 'escalate' | 'ignore'

Architecture

APPLICATION
    │
    ▼
decisions.decide({ objective, state, choices, risk, … })
    │
    ▼
DECISION RUNTIME
    │
    ▼
COMPLEXITY /​ RISK ROUTER
    ├── fast           → judge
    ├── standard       → evidence → judge → confidence
    ├── deliberate     → evidence → candidates → judge → critic → confidence
    ├── human-required → … → HITL
    └── rules          → DNA /​ hybrid rules path
    │
    ▼
DETERMINISTIC POLICY  (allow | critique | review | deny | …)
    │
    ▼
GATEKEEPER (authorization)   ← only if execute: true
    │
    ▼
DURABLE CHECKPOINTS + receipts (@hazeljs/​agent)
    │
    ▼
CAPABILITY HANDLER (optional; never without auth)
flowchart TD
App[Application] --> Decide["decide()"]
Decide --> Router[ComplexityRouter]
Router --> Provider[DecisionProvider]
Provider --> Conf[Confidence]
Conf --> Policy[PolicyEvaluator]
Policy -->|allow| GK[Gatekeeper]
Policy -->|review| HITL[HumanTask]
Policy -->|deny| Stop[Denied]
GK -->|allow| Cap[CapabilityHandler]
GK -->|deny| Stop

What this package reuses (does not reimplement)

ConcernPrimitive
Agent Runtime / DNA / HITL / checkpoints@hazeljs/​agent
Call-time authorization@hazeljs/​agent-gatekeeper
Skill risk class (floor only)@hazeljs/​skillgate
Effect isolationHost effectGate / @hazeljs/​agent-vm (documented, not duplicated)
SpansOptional OTel tracer hazeljs

Skillgate curates and classifies skills. Gatekeeper authorizes each invocation. The decision runtime never collapses those layers.

decide() request shape

await decisions.decide({
  name: 'incident-remediation',           /​/​ optional DNA /​ registry key
  objective: 'Choose the safest remediation',
  state: incident,                        /​/​ data — prompt-injection resistant
  choices: ['retry', 'rollback', 'escalate', 'ignore'] as const,
  risk: 'high',                           /​/​ or { level, impact, reversible }
  strategy: 'auto',                       /​/​ fast | standard | deliberate | human-required | rules
  provider: 'hazel-agent',                /​/​ or mock /​ ensemble /​ openai /​ …
  policy: { confidence: { high: 0.95, medium: 0.7 } },
  execute: false,                         /​/​ true → Gatekeeper + capability after allow
  cache: false,                           /​/​ opt-in; requires runtime cache
  calibrate: false,                       /​/​ opt-in reporting remap
  context: { tenantId: 'acme', agentId: 'incident-agent' },
});

Result (selected fields)

FieldMeaning
decisionTyped choice from choices
confidence / confidenceDetailScore + type (heuristic | ensemble | calibrated …); never auth
evidence / candidates / criticPipeline artifacts
strategyResolved execution strategy
policy.outcomeallow | deny | critique | review | …
executionWhether capability was authorized / invoked
hitlHuman task id / status when review required
alternativesOther candidates, ranked by score (up to 5)
costmodelCalls, token counts, optional estimatedCost
latencyPer-stage milliseconds plus totalMs
provenance.stagesStage names for Lab / audit
statusRun machine status (COMPLETED, WAITING_FOR_HUMAN, DENIED, …)

Providers

ProviderRole
hazel-agentDefault. Native offline pipeline; optional LLM judge/critic via generateObject
mockTest double (MockDecisionProvider)
autoRouter hook (defaults toward hazel-agent / cost profiles)
ensembleMajority / weighted / unanimous / risk-sensitive fan-out
openai / gemini / jev / localThin adapters via create*DecisionProvider — still governed
import {
  createGovernedDecisionRuntime,
  createOpenAiDecisionProvider,
  createJevDecisionProvider,
} from '@hazeljs/​decision';

const decisions = createGovernedDecisionRuntime({
  generateObject: aiService, /​/​ @hazeljs/​ai or duck-typed
  agentRuntime,
  auditService,
  gatekeeper,
});

decisions.providers.register(createOpenAiDecisionProvider(aiService));
decisions.providers.register(createJevDecisionProvider({ decide: jevDecide }));

On model failure, hazel-agent falls back to the heuristic judge/critic (never to “allow”). Invalid choices outside the closed set are rejected.

Complexity / risk router

RiskDefault strategyStages
lowfast (small choice sets)judge
mediumstandardevidence → judge → confidence
highdeliberate+ candidates + critic
criticalhuman-required+ HITL

Critical risk, and any DNA capability with execution.<choice>.hitl: true, resolve to human-required even if the request asks for fast, standard, or deliberate. strategy: 'rules' is selected first and is not rewritten.

Risk is trusted configuration. Models cannot lower it. Skillgate skill class may raise the risk floor only.

Skill classRisk floor
readlow
writemedium
destructivehigh
admincritical

applySkillgateRiskFloor / skillClassToRiskFloor never lower the configured risk. Lookup is by capability name via lookupSkill.

Rules strategy and hybrid rules

Two different mechanisms. Both read numeric fields from state with dotted paths (amount, deployment.ageMinutes). Text in state cannot change them.

strategy: 'rules' asks hazel-agent to pick a closed-set choice from Decision DNA metadata.rules and skip the judge. A match sets confidence to 1 (type: 'heuristic') and records stage rules. The choice still passes policy and Gatekeeper.

await decisions.decide({
  objective: 'Escalate after repeated attempts',
  state: { attempts: 3 },
  choices: ['retry', 'escalate'] as const,
  strategy: 'rules',
  provider: 'hazel-agent',
  definition: {
    objective: 'Escalate after repeated attempts',
    choices: ['retry', 'escalate'],
    metadata: {
      rules: [{ whenPath: 'attempts', gte: 3, then: 'escalate' }],
    },
  },
});

then must be one of choices. No match continues with evidence extraction and the heuristic judge. The rules strategy does not call the LLM judge.

hybridRules on the runtime force a policy action after the provider has already chosen. They run before confidence bands. The first matching rule wins.

const decisions = createDecisionRuntime({
  hybridRules: [
    { whenPath: 'amount', gte: 5000, then: 'review', reason: 'amount >= 5000' },
  ],
});

then is a policy action (allow | deny | critique | review | escalate | fallback), not a new choice. gte matches value >= n; gt matches value > n.

Confidence & policy

interface DecisionConfidence {
  value: number; /​/​ [0, 1]
  type: 'provider' | 'heuristic' | 'ensemble' | 'calibrated' | 'self-reported';
  calibrated: boolean;
  components?: {
    candidateSeparation?: number;
    criticAgreement?: number;
    evidenceCoverage?: number;
    providerConfidence?: number;
  };
}

Default confidence bands (overridable via DNA / request policy):

BandPolicy action
≥ high (default 0.95)allow (eligible for Gatekeeper)
≥ medium (default 0.7)critique
< mediumreview (HITL)

Even with confidence 0.999 and policy allow, Gatekeeper can deny. See package example high-confidence-denied.ts.

Custom policy rules

Pass policy as a rule list or as { rules, confidence }. Request rules replace DNA policy rules for that call. confidence.high / confidence.medium still set the default bands when no rule matches.

await decisions.decide({
  ...,
  policy: {
    confidence: { high: 0.9, medium: 0.6 },
    rules: [
      { when: { risk: 'critical' }, action: 'review' },
      { when: { confidence: { lt: 0.4 } }, action: 'deny' },
    ],
  },
});

when.confidence accepts gte, gt, lt, and lte. when.risk matches the trusted risk level.

Policy actionWhat the runtime does
allowEligible for Gatekeeper when execute: true and a capability is mapped
critiqueDECISION_FINALIZED — choice is returned, capability is not invoked
reviewWAITING_FOR_HUMAN + HumanTask
escalateSame HITL path as review
denyDENIED — execution is not requested
fallbackCOMPLETED — choice is returned, capability is not invoked

strategy: 'human-required' forces review unless a hybrid rule already set an action. Hybrid rules override both bands and custom rules.

Decision DNA

Optional field on Agent DNA (format: 'hazeljs.agent.dna'):

exportAgentDna({
  name: 'incident-agent',
  tools: [],
  decisions: {
    'incident-remediation': {
      version: '3',
      objective: 'Choose the safest remediation',
      choices: ['retry', 'rollback', 'escalate', 'ignore'],
      risk: { level: 'high' },
      confidence: { high: 0.95, medium: 0.7 },
      evidence: { required: ['error-rate-change'], projections: [...] },
      scoring: { rollback: [{ evidenceKey: 'error-rate-change', weight: 0.4 }] },
      execution: {
        rollback: { capability: 'deployment.rollback' },
      },
    },
  },
});

Register with runtime.registry.register(...) or registry.registerFromDna(dna). DNA without decisions remains valid.

HITL & durable execution

Policy review / strategy human-required creates a HumanTask and checkpoints WAITING_FOR_HUMAN via @hazeljs/​agent stores.

await decisions.resumeFromHuman({
  decisionId,
  runId,
  action: 'override', /​/​ approve | reject | override
  decision: 'escalate',
  actor: 'ops-lead',
  reason: 'Production rollback currently prohibited',
  execute: true,
});

Overrides are audited. Resume loads the execution receipt and will not double-invoke the capability. Replay never re-executes side effects.

Durable reads

Checkpoints live on the Agent CheckpointService you pass to createDecisionRuntime({ checkpoints, humanTasks }). The runtime wraps them in DecisionDurableStore (kind: 'hazeljs.decision').

decisions.registerCapability('deployment.rollback', async ({ decisionId, state }) => {
  return { rolledBack: true, decisionId };
});

const inMemory = decisions.getResult(result.id);
const hydrated = await decisions.getResultAsync(result.id, result.trace.runId);
const labRun = await decisions.replay(result.trace.runId!);
MethodBehavior
registerCapability(name, handler)Same map as capabilityHandlers at construction
getResult(decisionId)In-memory result for this process
getResultAsync(decisionId, runId)Memory, then the newest checkpoint that carries that result
replay(runId)Latest checkpoint as a Lab run. Handlers are not called

clearMemory() drops the in-memory index so tests can simulate a restart. The next getResultAsync / resumeFromHuman reads the checkpoint.

Decision Lab

import { createDecisionRuntime, createDecisionLab } from '@hazeljs/​decision';

const lab = createDecisionLab(createDecisionRuntime());
const run = await lab.run({ objective, state, choices: [...] as const, risk: 'high' });
/​/​ run.graph — pipeline nodes for UI

const cmp = await lab.compare(request, ['hazel-agent', 'mock']);
/​/​ cmp.executionForbidden === true

Agent Office hosts the control plane at /​office/​decisions in the sibling app hazeljs-agent-office (Run, Compare, Flow graph, History, Calibration) — not in Inspector. Meridian exposes the teaching HTTP path: POST /​api/​decision/​refund → resume with Gatekeeper still authorizing payments.refund.

Ensemble, cache, history, routing

import {
  createEnsembleDecisionProvider,
  DecisionCache,
  DecisionHistory,
  createCostAwareRouter,
} from '@hazeljs/​decision';

runtime.providers.register(
  createEnsembleDecisionProvider({
    providers: [hazel, openai],
    strategy: 'majority', /​/​ weighted | unanimous | risk-sensitive
  })
);

const runtime = createDecisionRuntime({
  cache: new DecisionCache({ ttlMs: 30_000 }),
  history: new DecisionHistory(),
});
await runtime.decide({ ..., cache: true }); /​/​ opt-in only; never caches side effects

runtime.providers.setRouterHook(
  createCostAwareRouter([
    { name: 'local', costTier: 'economy' },
    { name: 'hazel-agent', costTier: 'balanced' },
    { name: 'openai', costTier: 'premium', minRisk: 'high' },
  ])
);

history.trends(); /​/​ aggregates for Lab — never auto-injected into prompts
history.similarByEvidence(['error-rate-change', 'recent-deployment']);

similarByEvidence(keys, limit = 10) ranks stored rows by shared evidence keys (not raw state). Use it for Lab and incident lookup. It does not enter prompts.

Shadow compare

DecisionLab.compare already refuses execution. Two helpers wrap the same rule:

import { registerShadowProvider, runWithShadow, createDecisionLab } from '@hazeljs/​decision';

registerShadowProvider(runtime.providers, createLocalDecisionProvider(generator));

const lab = createDecisionLab(runtime);
const { primary, shadows } = await runWithShadow(lab, request, ['local', 'mock']);
/​/​ both calls set execute: false

registerShadowProvider only registers the provider. Callers keep production decide({ execute: true }) off that name unless they opt in explicitly.

Flow graph projection

import { projectDecisionFlow, buildDecisionFlowDefinition } from '@hazeljs/​decision';

const graph = projectDecisionFlow({ name: 'incident-remediation', risk: 'high' });
const stub = buildDecisionFlowDefinition(graph); /​/​ optional @hazeljs/​flow registration

DecisionRuntime remains the authority. Flow projection is scaffolding / UI, not a second decision engine.

Calibration (opt-in)

import { fitCalibrationFromHistory, DecisionHistory } from '@hazeljs/​decision';

const history = new DecisionHistory();
const runtime = createDecisionRuntime({ history });
/​/​ decide + evaluate({ expectedDecision }) …
runtime.setCalibration(fitCalibrationFromHistory(history));
await runtime.decide({ ..., calibrate: true }); /​/​ reporting only

Calibrated scores never authorize execution and are never auto-fed into prompts.

Module / DI

import { DecisionModule, DecisionService, createGovernedDecisionRuntime } from '@hazeljs/​decision';

DecisionModule.forRoot({ generateObject, agentRuntime, gatekeeper, auditService });

/​/​ or without DI:
const decisions = createGovernedDecisionRuntime({ generateObject, gatekeeper });

DecisionService.decide(...) delegates to the wired DecisionRuntime.

Observability

In-process metrics include:

  • hazeljs_decisions_total
  • hazeljs_decision_duration
  • hazeljs_decision_confidence
  • hazeljs_decision_critic_total
  • hazeljs_decision_review_total
  • hazeljs_decision_override_total
  • hazeljs_decision_denied_total
  • hazeljs_decision_execution_total
  • hazeljs_decision_failures_total

Optional OTel span: decision.run (tracer hazeljs). Raw state is not recorded by default.

Security

  • State is data (prompt-injection resistant).
  • Choices are a closed set — invalid model output is rejected.
  • Risk / policy / capabilities are not model-writable.
  • Cross-tenant evidence is keyed by decision id + tenantId on provenance.
  • Replay never re-executes side effects.
  • Missing Gatekeeper ⇒ execution denied when execute: true.
  • History / calibration never auto-inject into prompts.

Errors

Thrown by the runtime today:

ClassCodeWhen
DecisionValidationErrorDECISION_VALIDATIONEmpty choices, missing objective, unknown DNA name, override without a decision
DecisionInvalidOutputErrorDECISION_INVALID_OUTPUTModel or ensemble verb outside the closed set
DecisionProviderErrorDECISION_PROVIDERUnknown provider, or a provider failure that is not a heuristic fallback
DecisionReviewRequiredErrorDECISION_REVIEW_REQUIREDOnly when metadata.throwOnReview is set and status is WAITING_FOR_HUMAN

Default HITL returns the result (status: 'WAITING_FOR_HUMAN'). It does not throw. Gatekeeper deny is status: 'DENIED' on that result, not an authorization exception. A capability handler exception propagates as thrown.

DecisionTimeoutError, DecisionPolicyError, DecisionAuthorizationError, and DecisionExecutionError are exported (DECISION_TIMEOUT, DECISION_POLICY, DECISION_AUTHORIZATION, DECISION_EXECUTION) for callers. The current runtime does not raise them. All of these extend DecisionError (code, optional details).

CLI

run does not invoke a capability unless you pass --execute, and --execute still stops at Gatekeeper.

hazel decision run \
  --name incident-remediation \
  --objective "Choose the safest remediation" \
  --state .​/incident.json \
  --choices retry,rollback,escalate,ignore \
  --risk high \
  --provider hazel-agent \
  --strategy auto \
  --json

hazel decision compare \
  --providers hazel-agent,mock \
  --state .​/incident.json \
  --risk high \
  --json

hazel decision flow --name incident-remediation --risk critical --strategy auto --json
hazel decision lab --state .​/incident.json --risk high --provider hazel-agent --json
CommandFlags
run--name, --objective, --state, --choices, --risk, --provider, --strategy, --execute, --json
compare--name, --objective, --state, --choices, --risk, --providers, --json
flow--name, --risk, --strategy, --json
lab--state, --risk, --provider, --json

Defaults: name incident-remediation, choices retry,rollback,escalate,ignore, risk high, provider hazel-agent, strategy auto. compare defaults providers to hazel-agent,mock and never executes.

Examples (package)

npx tsx examples/​incident-remediation.ts
npx tsx examples/​refund-hitl.ts
npx tsx examples/​high-confidence-denied.ts
npx tsx examples/​prompt-injection.ts
npx tsx examples/​decision-lab.ts
npx tsx examples/​flow-and-calibration.ts
npx tsx examples/​ensemble.ts

Progressive complexity

  1. decide()
    • provider
    • risk + confidence
    • critic + policy
    • Gatekeeper + durable HITL
    • Decision DNA + Lab + evaluation / calibration

Level 1 users are not forced into Level 6 concepts.

Intentionally never

  • Automatic prompt injection of historical outcomes
  • Confidence as authorization
  • Collapsing Skillgate + Gatekeeper into the decision model