Runtime services for the Hanfani agent framework —
the layer above the headless @hanfani/core engine.
The agent proposes, an approver decides, the server acts. The approver is a human — or, when the user enables it in settings, a deterministic rule the user configured. The model never approves unless you authorize to do so.
Where @hanfani/core defines what a workflow and an agent are (pure, no I/O),
@hanfani/server provides the runtime that observes and serves them: the audit
grammar the server emits, an audit-log store and activity bus, a policy-monitor
(objectives) engine, usage reporting, connection profiles, and a settings-driven
auto-approval mechanism. Every module is generic — domain specifics are injected
— so any workflow can use them.
The framework's aim is that the same inputs always produce the same outcome:
same objective config + same ordered activity ⇒ same result, every time.
Completion is decided by server facts (post-approval effect events), never
by model prose. The one naturally non-deterministic step is human approval — so
auto-approval (below) exists to make that branch deterministic too, by the
user's choice, without ever letting the model approve its own actions.
pnpm add @hanfani/server @hanfani/corehono is an optional peer dependency, needed only if you use the HTTP route
registrars (registerAuditRoutes, registerUsageRoute, registerObjectivesRoutes,
registerConnectionProfileRoute).
The audit stream carries only a kind and a free-text summary. The exact shape
of that summary is a contract between whoever writes an audit entry (the
server's action ledger) and whoever reads it (objective monitors, the UI, an
auto-approver). Because the server emits it, the server owns it — and the emitter
and decoder live in the same module so they cannot silently drift:
import { formatActivitySummary, parseActivityEvent } from '@hanfani/server'
const summary = formatActivitySummary({ kind: 'resolved', decision: 'approved', tool: 'saveDraft' })
// → "approved saveDraft"
parseActivityEvent({ kind: 'resolved', summary }, { tools: ['saveDraft'] })
// → { kind: 'resolved', tool: 'saveDraft', decision: 'approved', structured: true }The grammar:
kind |
Summary format | Example |
|---|---|---|
gate |
the bare tool name | saveDraft |
resolved |
approved <tool> | rejected |
approved saveDraft |
effect |
executed <tool> |
executed saveDraft |
finished |
finished |
finished |
parseActivityEvent is generic — tool is a plain string; pass { tools } to
narrow/validate it. A parse(format(x)) round-trip test locks the pair together.
import { createAuditStore, registerAuditRoutes, startAuditBridge } from '@hanfani/server'
const audit = createAuditStore(myStorePort, { actorOf }) // dedup + subscriber bus
registerAuditRoutes(app, audit) // GET /api/audit-log (+ /stream)
startAuditBridge({ baseUrl, onEntry: audit.persist }) // consume a pipeline SSE streamThe store logic (dedup window, the in-process bus, the human-actor hook) is
framework-owned; you bind the three-method AuditStorePort to your database.
A deterministic monitor over a workflow's activity: given a trigger and a
success rule, did the server observe the right facts? Completion is decided by
server facts (post-approval effect events), never by model prose. Every
domain decision is delegated to an ObjectivePolicy you implement; the engine
drives the run lifecycle (create → progress → gate → resolved → success/fail →
deadline).
import { createObjectivesEngine, createInMemoryObjectivesStore } from '@hanfani/server/objectives'
const engine = createObjectivesEngine(myEmailPolicy, {
store: createInMemoryObjectivesStore(), // or your DB-backed ObjectivesStore
loadPayload,
emitActivity: audit.persist,
})
audit.subscribe((entry) => void engine.evaluateActivityEntry(entry))Auto-approval turns the human-approval branch into a deterministic one by the user's intent. The user enables it in settings and configures declarative rules; a matching gate is then resolved by a pure rule instead of a human click. The decision is a pure function — same settings + same context ⇒ same decision — so approvals replay reproducibly. The model is never in this path; approval authority is the human or the human's pre-declared settings.
import { matchAutoApproval, startGateAutoApprover } from '@hanfani/server'
const settings = {
enabled: true, // the master switch, toggled from user settings
rules: [{ id: 'trusted-sender', agent: 'reply', tool: 'saveDraft', when: (c) => trusted.has(c.extra.senderEmail) }],
}
// Pure decision (testable, replayable):
matchAutoApproval(settings, ctx) // → { approved: true, ruleId: 'trusted-sender' }
// Runtime bridge — reads settings live, resolves matching gates, records the deciding rule:
startGateAutoApprover({ subscribe: audit.subscribe, baseUrl, settings: () => settings, resolveContext, onAutoApproved })getUsageSummary/registerUsageRoute— month-to-date Anthropic spend vs. a budget.getConnectionProfile— the never-connected vs. session-expired vs. store-unavailable state machine, with an injected identityenrichhook.
Depends on zod (schemas) at runtime.
@hanfani/core and
hono (the HTTP route registrars) are optional
peers — pull them in only for the features that use them.
An auto-generated API reference is available at jsdocs.io. Full guides are coming soon in the Hanfani framework docs.