@hanfani/server
v0.4.0
Published
Runtime services for the Hanfani agent framework: the activity-summary grammar (emit + decode), an audit-log store and activity bus, a policy-monitor (objectives) engine, usage reporting, connection profiles, and an opt-in gate auto-approver.
Maintainers
Readme
@hanfani/server
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.
Determinism
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.
Install
pnpm add @hanfani/server @hanfani/corehono is an optional peer dependency, needed only if you use the HTTP route
registrars (registerAuditRoutes, registerUsageRoute, registerObjectivesRoutes,
registerConnectionProfileRoute).
What's inside
The activity grammar — owned here
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.
Audit-log store + activity bus
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.
Objectives — a policy-monitor engine
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 — the determinism mechanism
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 })Usage & connection profiles
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.
Dependencies
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.
Docs
An auto-generated API reference is available at jsdocs.io. Full guides are coming soon in the Hanfani framework docs.
