@alma-harness/core
v0.12.0
Published
Alma core contracts: agentic loop, neutral messages, scoped tools, budget, routing policy, tenancy/audit, lifecycle events, routines.
Readme
@alma-harness/core
The contracts and the security core of Alma
— a TypeScript runtime for multi-tenant agents operating on sensitive data.
The engine that runs against them ships as
@alma-harness/loop.
Status: pre-1.0. The API is still moving; see the roadmap for where it stands.
What it owns
The vocabulary every other package speaks, and the rules the engine may not
bend. What a turn is allowed to do is defined here; the engine that enforces
it lives in @alma-harness/loop.
- Messages (
Msg,Block) — the neutral format every provider is translated to and from. A text block withorigin: "harness"was appended by the harness (the per-turn volatile suffix, spec: volatile-per-turn), not typed by the person. Amediablock travels by reference: with aMediaSourceconfigured the loop asks the product for the bytes and they go to the provider as base64, the session keeping the ref alone (spec: media-by-bytes); without one the reference is handed to the provider to fetch — egress either way, logged by the loop by kind and size, never by URI (spec: what-the-wire-cuts). The harness itself never fetches it. - Tenancy (
Scope,scopePath) —{org, uid}bound by closure. The model never sees or chooses it; deriving it from a verified identity is the host application's job. - Lifecycle events (
LifecycleHooks,Interceptor) — the closed set of typed seams an extension may attach to, capability-narrowing only. - Tools (
defineTool) — capability by registration. What the model may call is derived from the registry per step, never a list it can extend. Outputs are bounded at registration (maxOutputChars, a default applies): a result enters the transcript once and is re-sent forever, and removing it later costs more than it saves. A tool declareddeferredstays out of the prefix until the engine's built-in search loads it for a turn (spec: deferred-tools). A provider-executed tool (ProviderToolSpec, todayweb_search) is declared on the request, runs on the provider's side, and comes back as two neutral blocks —provider_tool_callandprovider_tool_result, provider-tagged, the provider's payloadopaquefor replay — under amaxSensitivityceiling,internalby default (spec: provider-tools). - Routing (
ModelPolicy,Sensitivity,Tier) — complexity × sensitivity, enforced at dispatch and never escalating through a delegate. The policy also says how hard the model may think (ModelChoice.reasoning, spec: reasoning-blocks); the reasoning it produces is aBlockthat stays backstage — in the session, never in the reply. - Budget (
BudgetCaps,priceUsage,SpendStore) — dollar caps, fail-closed pricing: an unpriced model never spends, and neither does an unpriced service tier or an unpriced web search (webSearchUsdPerRequest); the table carries tiers and context bands, and pricing follows the tier the wire says served (spec: pricing-tiers).perTurnUsdnumbers block, while{ usd, onExceeded: "warn" }continues and reports the crossing; session and tenant-day caps are accounted through theSpendStoreseam with per-cap policy —warnby default (the turn continues, the crossing is reported),blockopt-in. - Audit (
AuditLog) — access, routing, cost, recall and context trails. Where they are written is swappable; that they are written is not. - The rotation marker (
MsgMeta.rotation) — a summary message that stands for earlier ones (spec: long-context). The log is never rewritten; the engine's view starts at the marker. - Why a turn failed (
TurnFailure,FailureKind,ProviderError) — one closed vocabulary across every provider and seam, with the one verdict a product acts on:retryable, whether the same request unchanged may succeed later (spec: error-taxonomy). Adapters throwProviderError; the engine classifies failures internally. A retryable provider diagnosis does not authorize replay of an admitted canonical operation. - Conversation ownership lives in execution's durable operation admission. TurnStore and its expiring lease/claim types are removed. Canonical conversation replay binds input and configuration; uncertainty never grants automatic retry.
- Request controls (
ModelRequest, spec: provider-request-controls) — native JSON schema output and explicit Anthropic cache controls. These are adapter inputs, not validated results or a governed single-call runner.cache: falsedisables breakpoints;conversationTail: falsekeeps only the stable system prefix. Other adapters reject explicit cache controls. Output schema changes fromstep:preare refused and logged, including attempts to add one. - Model jobs (
ModelJobClient) — one-call work answered by a provider's batch API (spec: model-jobs). Where a job runs is swappable; that it is scoped, routed, accounted and on the trail is the runner's, in@alma-harness/loop. - Routines (
Routine,TriggerSource,RoutineStore,OutputSink,RoutineRunStore) — a routine is data: a goal, an intent, a schedule, a profile or none, its own per-run cap, a named destination (§8, spec: routine-runner). Where the schedule fires from, where a result goes and where a run is recorded are seams; claiming a run before executing it, capping it and recording it is the runner's, in@alma-harness/schedule.RoutineRunStore.claimis mandatory and atomic: insertrunningonly if absent, or transitionsubmittedtorunningfor collection. Interrupted claims need reconciliation; see the recovery notes. - Memory contracts (
EpisodeStore,ProfileStore,MemoryErasure, …) — the interfaces live here because memory integrates with tenancy, audit and routing; the implementations live in@alma-harness/memory.
What it must never do
- Reach the filesystem, spawn a subprocess, or ship a shell. Ever.
- Accept a scope from the model, or let a hook rewrite routing, budget or the advertised tool set.
- Depend on a provider SDK. The neutral message format is the boundary.
Testing helpers
@alma-harness/core/testing ships reference implementations of the contracts
this package publishes — a recording AuditLog, and the in-memory
SessionStore, SpendStore, CostSettlementStore and RoutineRunStore. Nothing in it imports vitest. The shared
storage contract suites every adapter must pass live in
@alma-harness/testing; the in-memory memory stores in
@alma-harness/memory/testing; the scripted ModelClient that drives the
engine in @alma-harness/loop/testing.
import { InMemorySessionStore, RecordingAuditLog } from "@alma-harness/core/testing";Financial warnings use BudgetCapName (perTurnUsd plus the unchanged
PersistentCapName). normalizePerTurnCap validates and copies explicit policy.
Routine run/delivery metadata carries capsCrossed; empty run arrays normalize
to absent in both reference stores. See spec: financial-warning-policy.
Cost settlement foundation
CostSettlementStore records known priced usage with a host-issued scoped ID.
settle returns an applied/replayed receipt; changing its normalized input is a
SettlementConflictError. The executor must persist identity before dispatch
and retain usage for reconciliation. This store never dispatches a provider.
Replayed totals are historical; current budget checks use SpendStore.peek.
normalizeCostSettlement rejects unknown fields and snapshots the closed record
before I/O. IDs are 1–200 non-space printable ASCII characters; scope also obeys
scopePath. Timestamps require a zone and at most millisecond precision. Optional
usage zeros remain distinct from absence. serviceTier is the resolved priced
tier; capsCrossed preserves precomputed evidence, without evaluating policy.
Consumers (up to 16) are an explicit list, which may be empty.
Scoped get/list read receipts; list defaults to 50 (maximum 100) and filters
by operation, model, priced tier and inclusive occurrence interval. Its ascending
(at, id) cursor is observational, not a reliable projection offset.
pending returns full unacknowledged receipts for a consumer; acknowledge after
applying idempotently. Replays never recreate acknowledged obligations.
InMemoryCostSettlementStore is volatile and owns separate counters. The
Postgres implementation shares existing durable spend counters.
Do not account for one call through both this seam and legacy audit/spend writes. Governed runners use transactional settlement; legacy runners retain their existing writers. Select one accounting path per call. See idempotent-cost-settlement.
Single-dispatch evidence
SingleDispatchModelClient is an opt-in trusted host capability (spec:
provider-dispatch-evidence). validate checks its request support without I/O;
streamEvidence is single-consumption and reports a bounded request reference
and final UsageEvidence separately from content/stop events. A fresh method
call is a fresh dispatch: the executor must claim it durably first.
Evidence distinguishes known, unpriced (counts known, attribution unresolved),
unknown and pre-aborted not_dispatched. Only known may be priced. The new
OpenAI evidence normalizes ordinary input excluding both cache reads and writes,
which have separate price terms. Existing legacy usage mappings are unchanged.
Drain the stream: early return closes transport but may leave billing unresolved.
A provider error's retryable flag is not authorization to redispatch the same
operation. No durable runner adopts this capability yet.
Durable execution journal
ExecutionStore (spec: durable-execution-records) keeps a content-free claim,
usage evidence and the original pricing snapshot. InMemoryExecutionStore from
@alma-harness/core/testing implements the same contract as PostgreSQL. Only the
first claim/recovery winner receives a fence; reads and exact retries never do.
dispatch must commit before provider egress and cannot replay. receive retains
known/unpriced/unknown distinctions. list exposes unresolved scoped metadata;
reconcileExpired fences expired prepared/dispatching work, never resends it.
Claim retries must reuse the full original input, including prices and timestamps.
Recovery leases fence journal writes only; idempotent settlement still requires
CostSettlementStore with the original settlementId. This journal neither calls
providers nor settles bills, stores responses or marks results complete. Before
runner adoption, scoped result storage/erasure and a durable inbox for authoritative
usage arriving after fencing are mandatory. Set dispatch deadlines beyond the
provider timeout plus a drain grace period; preserve rejected late evidence there.
ExecutionResultStore (spec 081) reserves scoped call/session identities before
context reads and stores a closed response envelope under explicit sensitivity,
retention and character caps. Erased/expired identities cannot be revived; host
cleanup scheduling and audited erasure composition are required. Contracts and
content-free errors live here; implementations live in memory and PostgreSQL.
Documentation
Docs index · Architecture · Invariants · Quickstart
UsageInbox preserves scoped usage observations; helpers/reference live in
@alma-harness/execution, SQL in postgres. It grants no dispatch or billing authority.
Apache-2.0
Governed journal inputs optionally snapshot governance (caps, consumers, result
retention). executionSettlementRequest(record) reconstructs the original request.
After governed accounting, settle(fence, {request, receipt}) and complete persist
fenced lifecycle transitions; never pass the full governed object with decisions.
Recovery may finish settled records, never redispatch; completion is not delivery.
Result content/erasure remain separate. New readers precede governed writers.
normalizeBudgetCaps, normalizeSpendTotals, normalizeCostReceipt and
costReceiptWarnings share closed financial rules across journal and accounting.
See governed execution lifecycle.
RoutingEvent and AccessEvent optionally carry operationId/attemptId/callId for governed single calls; turnId retains its original conversation meaning.
ModelRequest.temperature is optional; adapters validate support before egress
(spec: explicit-temperature-controls). Execution controls persist finite 0..2
values and normalize negative zero. Absence preserves provider defaults.
tryDelegate returns a DelegateAttempt; forwarding refusals is explicit (spec: governed-delegation-refusal).
Known evidence may attest zero-use Anthropic context rejection (spec: safe-context-rejection-rotation). Ordinary zero usage cannot authorize retry.
ModelRequest.toolChoice is optional host-owned generation policy:
{ type: "auto" | "none" | "required" } or { type: "tool", name: "capture" }.
normalizeToolChoice validates and copies its closed shape; validateToolChoice
checks the actual advertised ToolSpec[], rejecting missing or ambiguous names.
Adapters additionally enforce their documented model/control support. Absence
preserves existing behavior; explicit auto stays distinct from absence.
Execution controls persist the choice, so changing it under an existing identity
conflicts. Selection never authorizes tool execution or personal-memory writes.
See governed tool choice.
Session labels
SessionLabelStore (spec: session-labels) keeps a host's attribution for a
session — opaque identifiers and flags that map a pseudonymous scope/session back
to the host's own records — apart from immutable receipts, so erasure can sever
it. normalizeSessionLabels accepts 1–8 entries, keys ^[a-z][a-z0-9_]{0,31}$,
values ^[A-Za-z0-9_.:-]{1,128}$: syntactic only, never personal data values.
put stores once (equal replay returns the record, different labels throw
SessionLabelConflictError); erase(scope, sessionId) tombstones, erase(scope)
seals the scope, and either makes later put throw SessionLabelErasedError.
Writes and erasure serialize per scope. Labels never drive routing, access,
pricing, audit or scope. InMemorySessionLabelStore is in ./testing.
