@alma-harness/runtime

中文文档
0.41.0发布于 2026/11/04Apache-2.0TypeScriptES Module零依赖近 30 天下载 2157

Official store composition for governed Alma conversations.

原始使用文档
文档目录10 节
  1. Configure once: /deployment
  2. Start and resume a conversation: /agent
  3. Explicit runner composition
  4. PostgreSQL: /postgres
  5. Verifying the database
  6. Fencing a scope
  7. Erasing a scope
  8. Maintenance: /maintenance
  9. Receipt projections: /projections
  10. Conversation operations: /operations

@alma-harness/runtime

Official storage composition, a deployment that derives governed runners, and a simple text conversation facade. The root export has types only. Use /deployment to configure once, /agent for query and resume, /memory or /postgres for storage, and the lower-level factories for advanced routing.

Configure once: /deployment

import { createRuntimeDeployment } from '@alma-harness/runtime/deployment';
const deployment = createRuntimeDeployment({
  runtime, clients: { anthropic, openai }, prices, priceVersion,
  consumers: ['product'], caps, retentionMs: 60_000,
  limits: { deadlineMs: 8_000, runTimeoutMs: 120_000, stepTimeoutMs: 60_000, maxCalls: 24,
            maxInputChars: 64_000, maxRequestChars: 1_000_000, maxOutputChars: 100_000 },
  onBackgroundError: reportAccountingIssue,
  batch: { clients: { openai: openaiBatch }, retentionMs: 7 * 86_400_000 }, // optional
});
const titles = deployment.singleCall({ policy, policyVersion, outputContractVersion, system: [], maxTokens: 64 });
const pick = deployment.structuredCall({ policy, policyVersion, outputContractVersion, system, maxTokens, tools, toolChoice });
const chat = deployment.conversation({ policy, policyVersion, configRevision, resultContractVersion, system, tools,
  caps: perTurnCaps, limits: { maxCalls: 8 } });
const extract = deployment.batchCall({ policy, policyVersion, outputContractVersion, configRevision, system, tools, toolChoice,
  maxTokens, validate, retries, retryDelayMs, submitWindowMs });

Each factory returns the runner that createSingleCallRunner, createStructuredCallRunner, createConversationRunner or createBatchCallRunner builds from the same values, with their validation, bindings and replay (specs: runtime-deployment, deployment-batch-call).

  • Owned by the deployment: the runtime's stores and audit log, clients, prices, priceVersion, consumers, the result retention and a conversation's step. An operation that passes any of them, even as undefined, throws TypeError.

  • Overridable per operation: caps replaces whole, limits merge field by field, and onBackgroundError replaces.

  • Required limits, no defaults: single and structured calls need deadlineMs, maxInputChars and maxOutputChars. A conversation needs runTimeoutMs, stepTimeoutMs, maxCalls, maxInputChars, maxRequestChars and maxOutputChars.

  • Snapshot: prices, caps, limits, consumers, the clients and the store map are copied at construction. The deployment owns no pools.

  • Batch calls:

    • they need runtime.stores.batch and the deployment's batch.clients;
    • they take runTimeoutMs, maxRequestChars and maxOutputChars, within the batch runner's own maxima;
    • their results are kept for batch.retentionMs, which defaults to the deployment's retention;
    • the observer receives the attempt's batch key as operationKey;
    • /deployment needs @alma-harness/batch installed.

    A host moving pending batches from its own composition keeps them only with an unchanged schema, provider account and effective configuration (see the spec).

  • Resolved values: deployment.resolved and each runner's resolved hold the retention and the limits it was built with, frozen (spec: deployment-resolved-values).

  • Versioning stays yours: bump policyVersion, priceVersion, outputContractVersion, configRevision or inputRevision when what they name changes. A configuration revision covers all execution behavior, including models, policy, prompts, tools, hooks, context sources, generation controls, prices, caps, limits, retries, contracts and provider accounts. It leaves out only what changes neither behavior, resource identity nor authorization, such as a database login or a key rotated within one account. Retrying an existing execution under a changed revision conflicts, so binding those would block pending work.

Start and resume a conversation: /agent

import { createConversationAgent } from '@alma-harness/runtime/agent';
const agent = createConversationAgent({
  id: 'assistant', revision: 'v1', runtime,
  model: { ref: model, client, prices },
  instructions: 'You are a helpful assistant.',
  intent: { tier: 'standard', sensitivity: 'personal' },
  limits: { caps: { perTurnUsd: 0.25 }, retentionMs: 60_000 },
  onBackgroundError: reportAccountingIssue, // audit defaults to runtime.stores.audit
});
const user = agent.forUser({ org, uid }); // verified by the host
const first = await user.query({ prompt: 'Hello', messageId: 'delivery-1' });
if (first.view.status === 'completed')
  await user.query({ resume: first.sessionId, prompt: 'Continue', messageId: 'delivery-2' });
const observed = await user.read({ sessionId: first.sessionId, messageId: 'delivery-1' });

See the quickstart.

  • One immutable revision covers the whole configuration, trusted capabilities included.
  • Versioned defaults: 120s timeout, 24 calls, 64,000 input characters, 1,000,000 request characters and 100,000 output characters.
  • resume selects existing history in the bound scope. An unknown or foreign session throws ConversationResumeError. The host authorizes the session: an id is not a credential.
  • The first session id derives from agent, scope and messageId (conversationAgentSessionId). To retry a lost response, repeat the same request.
  • Every query returns { sessionId, messageId, rootKey, view }. Inspect view.status before using its result. read never dispatches.
  • What the facade does not do: it adds no input queue, retry, scheduler or erasure barrier.

Explicit runner composition

import { createMemoryRuntime } from '@alma-harness/runtime/memory';
import { createConversationRunner } from '@alma-harness/conversation';
const runtime = createMemoryRuntime({ stepResultPolicy: policy, rootResultPolicy: policy });
const runner = createConversationRunner({ ...hostConfig, ...runtime.stores, step: { ...hostConfig.step, ...runtime.stores.step } });

Memory is ephemeral: a restart loses history, admission and replay protection. close() never erases stores or reconciles uncertain work.

PostgreSQL: /postgres

import { createPostgresRuntime, migratePostgresRuntime } from '@alma-harness/runtime/postgres';
const namespace = { schema: 'agent_execution', rootSchema: 'agent_results', onPoolError: report };
await migratePostgresRuntime({ ...namespace, connectionString: migrationURL }); // migration principal, outside requests
const runtime = createPostgresRuntime({ ...namespace, connectionString: runtimeURL,
  poolMax: { execution: 2, roots: 1 }, stepResultPolicy: policy, rootResultPolicy: policy });
  • Two dedicated schemas with different lowercase names. Connection URLs with startup options are rejected.
  • The migration installs the adapters, grants schema USAGE to alma_app and alma_retention, and revokes CREATE. It never grants login membership: the operator gives the runtime login alma_app, without superuser, BYPASSRLS or ownership. It is repeatable, not atomic across schemas: after a failure, fix the cause and rerun.
  • Upgrade window: a database last migrated by one of the two previous minor releases migrates in place. An older one upgrades in steps (spec: upgrade-window).
  • Pools: construction opens no connection. Two bounded pools, five connections each by default, set by poolMax: { execution, roots }. Budget instances × (execution + roots + batch) plus migrations, maintenance and a reserve against the database's capacity.
  • Batch stores: batch: { schema, resultPolicy, poolMax? } opens a third pool, five connections by default, and builds runtime.stores.batch on a store schema that installs batch and results. Pass that schema in stores to migration and verification.
  • Connections: connection: { applicationName, idleTimeoutMs } (spec: pool-connection-options).
    • applicationName is a label of 1 to 40 characters of [A-Za-z0-9_.:-]. Each pool connects as <label>:execution, :roots and :batch, so pg_stat_activity tells them apart. Without a label Alma sets no name, and the URL's or PGAPPNAME applies. A label beside a URL that carries application_name is refused.
    • idleTimeoutMs sets how long an idle connection stays open: 1,000 to 600,000, 10,000 by default.
    • Keep-alive: every pool enables TCP keep-alive. When the first probe goes out is the operating system's setting.
  • onPoolError(error, schema) is required and synchronous. It reports idle-client failures and must not throw.
  • runtime.stores.audit writes to the execution namespace, on the execution pool.
  • Retention and erasure: register sessions, both result stores and stores.batch.results in the host's audited retention and erasure. A reconciliation_required turn's session is freed before it returns; the host decides whether to send again (spec: failed-turn-release).

Verifying the database

import { checkPostgresRuntime, verifyPostgresRuntime } from '@alma-harness/runtime/postgres';
const logins = [{ name: 'api', role: 'alma_app' }, { name: 'maintenance_discovery', role: 'alma_maintenance' }];
const report = await verifyPostgresRuntime({ connectionString: migrationURL, schema, rootSchema, logins }); // deploy job
if (!report.ok) throw new Error(JSON.stringify(report.differences));
await checkPostgresRuntime({ connectionString: runtimeURL, schema, rootSchema, logins }); // startup, read-only

verifyPostgresRuntime compares the deployed pair with a shadow that this version migrates inside a transaction that always rolls back. It needs no manifest or snapshot (specs: runtime-verification, verification-digest-order, store-verification).

  • What it enforces:
    • Alma's roles are NOLOGIN and members of no role;
    • each listed login reaches exactly its role;
    • no default grants, global or in a covered schema, to the actors or PUBLIC; none to other roles in a covered schema, or global from the migration principal;
    • no untrusted event trigger, publication or external SECURITY DEFINER routine;
    • the discovery roles' column ceilings;
    • no grant the migration does not make, to anyone;
    • no rewrite rules or inheritance edges;
    • no SET on session_replication_role.
  • Trust is given by fingerprint (trustedRoutines, trustedPublications, trustedEventTriggers).
  • The report carries hashes (diagnostics: true shows bounded text), and is capped at 500 differences.
  • A clean run certifies a digest from one snapshot into alma_runtime_verification.

checkPostgresRuntime re-enforces every invariant as the runtime login. It then compares the live catalog with that digest, and throws RuntimeVerificationError (invariant, unverified, version or drift). Roles are cluster-wide: another product adding a member to an Alma role is drift until you verify again. Requires PostgreSQL 16 or later.

Store schemas. Pass the same stores list, for example [{ schema: 'agent_memory', install: ['memory', 'audit'] }], to migration, verification and the check.

  • Kinds: memory, audit, routines, routineRuns, batch and results. Up to eight dedicated schemas.
  • Verified whole: a store schema holds only what its kinds' migrations create.
  • Routines: routines adds alma_scheduler to the checked roles.

Fencing a scope

import { createPostgresScopeFence } from '@alma-harness/runtime/postgres';
const fences = createPostgresScopeFence({ ...namespace, connectionString: runtimeURL, stores });
const { status, record } = await fences.fence(scope, { actionId: 'erase-42', actor: 'privacy-desk' });
await fences.close();

A fence is permanent: no API lifts it (spec: scope-fence). Once fence resolves, Alma grants the scope no new authority to dispatch a model call or to reserve a place for content, and no writer inserts the scope's content anywhere (spec: scope-erasure).

  • Refused with ScopeFencedError (@alma-harness/execution), which carries no content:
    • a new turn, and a new single, structured or batch call key;
    • every new result reservation. An enrichment run reports reconciliation_required and writes no plan;
    • a session append, a memory episode or fact, a routine registration, and a routine run record.
  • Still served: reads, replays of stored keys, maintenance recovery and usage acknowledgement.
  • In flight: work that already held its claim and reservations may dispatch and store its result. A turn admitted before the fence returns reconciliation_required at its next step or append. On PostgreSQL, a new key in a session that turn still occupies answers busy. A fence erases nothing: erase the scope next.
  • Where: the pair's execution namespace holds the canonical record, copied into the roots namespace and every store schema installing batch, results, memory, routines or routineRuns. The first record wins; a later fence returns it as existing. Only a resolved fence proves every schema is fenced, so retry a failed one, under any actionId. A schema fenced on its own with another record makes fence reject and name it, for an operator to reconcile. get reads the pair only.
  • Not a guard against the host: the gates refuse inserts. A writer that edits Alma's rows by hand as the app login acts on the host's authority.
  • Connections: connect as the application login. At most one connection, none at construction.
  • Memory: createMemoryRuntime exposes runtime.stores.fences, which gates its own stores.
  • Upgrading: the fence tables and gates change the verified catalog, so the startup check reports drift until you verify again. Fence only after migrating, verifying with the complete stores list, starting every process on this version, and retiring every older writer.

Erasing a scope

import { createPostgresScopeErasure } from '@alma-harness/runtime/postgres';
const erasure = createPostgresScopeErasure({ ...namespace, connectionString: runtimeURL, stores,
  batchClients: (configRevision, provider) => batchClientFor(configRevision, provider) });
const report = await erasure.erase(scope, { actionId: 'erase-42', actor: 'privacy-desk' });
if (report.status === 'pending') retryAfter(report.waitingUntil); // then call erase again
await erasure.close();

One call erases an owner from the pair and every declared store schema (spec: scope-erasure). Pass the same stores list as migration and verification. Each call:

  1. fences the scope, and keeps an existing fence and its record;
  2. erases content: sessions, labels (sealed), step, root, batch and store results (tombstones), memory episodes and facts, and routines (deleted);
  3. closes pending work:
    • running and submitted routine runs become failed with the reason scope erased, and every run loses its handle and any other reason;
    • uncertain admissions are released with the fence's actionId as their resolution, at most 100 a call. No operator login is needed: the fence authorizes it;
  4. discards the scope's batches at the provider (spec: provider-batch-retention). batchClients returns the client of the account a configuration revision used, or undefined. A running batch is cancelled and stays open until a later call deletes it. A submission still in flight without a handle stays open until its submit deadline, which sets waitingUntil;
  5. proves it: report.tables lists every scope table with its rows, the open rows that still hold content or unclosed work, and why the rest is kept (tombstone, closed, journal, accounting, audit, fence). report.provider gives the scope's batches and those not confirmed deleted. status is erased only when every open is 0.
  • Pending: read tables[].open for the cause. A turn admitted before the fence and still inside its deadline sets waitingUntil; call again after it. More than 100 uncertain admissions take another call. A content row still open after a call is an inconsistency to investigate, not a wait.

  • Retries: each step commits on its own and is idempotent. A failed call throws after writing scope:failed to the pair's audit trail; the next call completes it. Every call writes scope:erased or scope:pending.

  • In flight: neither kind of call stores a result.

    • One that reads its reservation after the erasure stops without dispatching: it rejects, and its journal records it not_dispatched.
    • One already past that read may still dispatch under the claim it held before the fence. It completes with an unavailable result, and its cost is recorded.
  • What stays: identifiers, amounts, usage, times and hashes, never message text Alma wrote. Two host-authored strings stay on the append-only audit trail: a routing policy's rationale and an access event's resource. Choose pseudonymous identifiers, and keep user text out of both.

  • Not counted: the maintenance and projection cursors, which hold only a scope's key, and the tenant-day spend, which has no uid.

  • Connections: connect as the application login. During a call, at most one connection per schema it touches; none between calls.

  • Provider batches: each call asks the provider again, one retrieval and up to three deletions per OpenAI batch, so a retry confirms nothing remains. A throttled or failed request leaves its batch open. What the provider keeps regardless:

    • an OpenAI batch object, with identifiers, counts and error messages;
    • a batch whose acceptance was lost, until its expiry (OpenAI) or 29 days (Anthropic);
    • prompt caches and logs.

    A batch cancelled before collection may be billed and never recorded.

  • Out of scope: the host's own data, and retained turn claims of older deployments, which a host erases with createMemoryErasure({ turns }).

Maintenance: /maintenance

import { createPostgresMaintenance } from '@alma-harness/runtime/maintenance';
const maintenance = createPostgresMaintenance({ schema, rootSchema, stepResultPolicy, rootResultPolicy, onPoolError,
  discovery: { connectionString: discoveryURL }, worker: { connectionString: workerURL } });
const report = await maintenance.maintain({ limit: 50, signal }); // schedule it

maintain finds scopes with due work and sweeps them, least recently visited first (spec: runtime-maintenance):

  • it expires admissions, roots and executions;
  • it completes executions from retained results;
  • it acknowledges usage.

It never dispatches, and its report holds counts only. A settled execution is due once recovery could take it: past its recovery lease, or else its deadline. A completed call missing its root receipt is found through its pending usage row (spec: maintenance-scan). The scan's index and the discovery role's recovery_until grant change the verified catalog: verify again after migrating.

Logins:

  • a discovery login in alma_maintenance only, which sees identifiers, states and times across scopes;
  • a worker login in alma_app only, which is not the request path's.

Neither may be in alma_retention. Every call checks this.

Connections: at most three per instance: two on the discovery login and one worker connection, which also serves each call's login check (specs: upkeep-connection-budget, login-check-connection). connection takes the same options as the runtime's. The worker's pools and the discovery pool connect as <label>:maintenance:execution, :roots and :discovery.

Receipt projections: /projections

import { createPostgresProjections } from '@alma-harness/runtime/projections';
const projections = createPostgresProjections({ schema, rootSchema, stepResultPolicy, rootResultPolicy, onPoolError,
  discovery: { connectionString: projectionURL }, worker: { connectionString: workerURL } });
await projections.drain('cost-dashboard', async delivery => { /* dedupe on (scope, consumer, settlementId) */ }, { limit: 50 });
await projections.quiesce(scope, () => erasure.erase(scope, request), { timeoutMs: 30_000 });

drain delivers each pending governed receipt of one consumer, with its session attribution: available, erased or none (spec: receipt-projections).

  • Acknowledgement: a receipt is acknowledged only after the handler resolves, so commit the deduplication and the effect together.
  • Delivery: fair, and never loses a receipt.
  • Logins: a discovery login in alma_projection only, and a worker login in alma_app only, as for maintenance.
  • Connections: connection as for maintenance, with names under <label>:projections:.
  • Erasure: quiesce waits for a drain working the scope, then runs your erasure. A destination that stores labels must serialize with erasure through gate rows, as the spec shows.

Projections drain only the pair's execution namespace.

Conversation operations: /operations

createConversationOperations inspects, repairs and resolves one admission, with capabilities the host supplies. The alma-operations CLI runs them from bounded JSON on stdin with a local --host module. See the operator runbook.

A call whose journal is not_dispatched never left, and counts as accounted at no cost, so it no longer blocks resolution (spec: never-sent-calls).

A turn its runner released needs no resolution: inspect shows turn-failed-v1:, and resolve answers binding_conflict (spec: failed-turn-release).

An operation_failed caused by a database error carries its SQLSTATE in error.sqlstate, and the CLI prints it: {"error":"operation_failed","sqlstate":"42501"}. No message, detail or query of that error leaves (spec: uncertain-turn-causes).

runtime.stores.labels is the session label store (spec: session-labels). Wire it into erasure as createMemoryErasure({ sessionLabels }).

版本与发布记录

@alma-harness/runtime 共发布 45 个版本,首次发布于 2026/09/11;过去 12 个月发布了 45 个版本。

版本发布日期距今
0.41.0latest2026/11/043 天前
0.40.02026/10/308 天前
0.39.22026/10/309 天前
0.39.12026/10/299 天前
0.39.02026/10/299 天前
0.38.02026/10/2910 天前

关于 @alma-harness/runtime 的常见问题

@alma-harness/runtime 是什么?

Official store composition for governed Alma conversations.

如何安装 @alma-harness/runtime?

在项目目录运行 npm install @alma-harness/runtime,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@alma-harness/core(^0.41.0)、@alma-harness/batch(^0.41.0)、@alma-harness/memory(^0.41.0)、@alma-harness/execution(^0.41.0)、@alma-harness/single-call(^0.41.0)、@alma-harness/conversation(^0.41.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 alma-operations,可以用 npx alma-operations 直接运行。

@alma-harness/runtime 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@alma-harness/runtime 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@alma-harness/runtime 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@alma-harness/runtime 可以商用吗?

@alma-harness/runtime 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@alma-harness/runtime 的下载量有多少?

根据 npm 官方下载统计,@alma-harness/runtime 在 2026/10/06 – 2026/11/04 这 30 天内被下载 2,157 次,日均约 72 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@alma-harness/runtime 还在维护吗?

最新版本 0.41.0 发布于 2026/11/04(3 天前);至今共发布 45 个版本,其中过去 12 个月发布了 45 个;首次发布于 2026/09/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。