@sema-agent/core
Version:
Stateless, task-oriented AI agent core
204 lines (203 loc) • 15.1 kB
TypeScript
import { type MemoryStore, type UtilityGate } from "../../core/memory.js";
import type { CheckpointStore } from "../../core/checkpoint-store.js";
import type { ToolResultStore } from "../../core/tool-result-store.js";
import { type EvictPolicy } from "../../core/session-store.js";
import type { SessionStore } from "../../core/session.js";
import { type FileCheckpointStoreOptions } from "./checkpoint-store.js";
import { type FileSessionPolicyStoreOptions } from "./session-policy-store.js";
import { type FileFileHistoryStoreOptions } from "./file-history-store.js";
import type { CorruptReadInfo } from "./corrupt-read-sink.js";
import type { FileHistoryStore } from "../../core/file-history-store.js";
import type { BackgroundAgentStore } from "../../core/background-agent-store.js";
import type { MailboxStore } from "../../core/mailbox-store.js";
import type { RosterStore } from "../../agents/roster-store.js";
import type { SessionPolicyStore } from "../../core/session-policy-store.js";
import type { WorkflowJournalStore } from "../../core/workflow-journal-store.js";
import type { UsageWindowStore } from "../../core/usage-window-store.js";
export { FileCheckpointStore, type FileCheckpointStoreOptions } from "./checkpoint-store.js";
export { FileMemoryStore } from "./memory-store.js";
export { FileSessionRepo, type FileSessionRepoOptions } from "./session-store.js";
export { FileToolResultStore } from "./tool-result-store.js";
export { FileSessionPolicyStore, type FileSessionPolicyStoreOptions, type SessionPolicyCorruptReadInfo } from "./session-policy-store.js";
export { FileFileHistoryStore, type FileFileHistoryStoreOptions } from "./file-history-store.js";
export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
export { FileUsageWindowStore, type FileUsageWindowStoreOptions } from "./usage-window-store.js";
export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
export { FileStoreLockError, type FileStoreLockErrorCode } from "./fs-atomic.js";
export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
export { processFingerprint, isProcessLive, type ProcessFingerprint } from "./fs-atomic.js";
export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootAdoptionFile, ROOT_ADOPTION_FILE, type AdoptionErrorCode, type AdoptionSource, type AdoptionReport, type AdoptionReceipt, type AdoptionLegReport, type AffectedDeploymentConfig, type RootAdoptionFile, } from "./adoption/marker.js";
export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, type AdoptionStatus, type AdoptLocalDataRootOptions, type AdoptLocalDataRootResult, type AdoptionCarriageLeg, type AdoptionCarriageLegContext, type AdoptionConfigWitnessReceipt, } from "./adoption/adopt.js";
/**
* design/80 — the file-backed `StorageBackend` for the local (TOC) single-user binary: the 3rd store impl
* alongside `InMemory*` and `Pg*`, selected purely by what the embedder injects into `RunnerDeps` (no engine
* change). Construct one over a single data root and pass its four members straight into `RunnerDeps`:
*
* ```ts
* const backend = new FileStorageBackend(); // ~/.ai-agent (or $AGENT_DATA_DIR)
* const runner = new Runner({
* brain,
* sessionStore: backend.sessionStore,
* checkpointStore: backend.checkpointStore,
* memoryStore: backend.memoryStore,
* toolResultStore: backend.toolResultStore,
* });
* // …on shutdown:
* await backend.dispose();
* ```
*
* **Single-instance-per-data-dir (§2.4 / §7 decision 4):** the constructor takes a coarse boot `flock` on
* `root/LOCK`. A second `FileStorageBackend` over the same dir FAILS FAST ("another instance owns this data
* dir"); a stale lock from a dead PID is pruned. The once-only checkpoint CAS is in-process (one event
* loop), so there is still no per-operation lock. What this backend's lock does NOT cover — and never did,
* despite what the store docs used to say — is a store constructed DIRECTLY off the root export (or one
* this backend does not construct at all, like `FileWorkflowRunStore`); those now take their own
* per-directory writer fence at construction ({@link import("./fs-atomic.js").acquireStoreDirLock}), so
* the single-writer premise holds however the stores were assembled. Serving many writers CONCURRENTLY
* (rather than refusing the second) is the Pg/TiDB backend's job, by design.
*/
export interface FileStorageBackendOptions {
/** Data root. Default `$AGENT_DATA_DIR ?? ~/.ai-agent`, NFC-normalized + realpath-canonicalized. */
root?: string;
/** Profile-injected promotable-write gate (design/77 §2). Threaded into `guardedMemoryStore`. */
utilityGate?: UtilityGate;
/** `TtlSessionStore` idle-eviction policy. Default `"forget"` (§7 decision 5 — durable history is never
* deleted by an idle timer). */
evict?: EvictPolicy;
/** Checkpoint store tuning (fsync cadence for `put`/`setPendingSteer`, ledger compaction threshold). */
checkpoint?: FileCheckpointStoreOptions;
/**
* design/381 §片2 — file-history tuning, today just the boundary RETENTION bound (omitted ⇒ the
* engine default; `{ retention: { keep: "unbounded" } }` hands the GC schedule to the deployment).
* The corrupt-read sink is deliberately NOT accepted here: it is wired from {@link
* FileStorageBackendOptions.onCorruptRead} above, because one failure grammar takes one sink.
*/
fileHistory?: Omit<FileFileHistoryStoreOptions, "onCorruptRead">;
/** The session-rules store's options — today the names this deployment mounts its own tools under
* (`mountedToolNames`, the roster its write and loosening checks judge rule names against). The corrupt-read sink is
* wired from {@link FileStorageBackendOptions.onCorruptRead}, as for {@link fileHistory}. */
sessionPolicy?: Omit<FileSessionPolicyStoreOptions, "onCorruptRead">;
/** design/81 Slice 5 — an optional {@link import("../../core/memory.js").Embedder}. Injected ⇒ the memory
* store does portable (in-process) vector recall over a rebuildable sidecar index (`vectorMode:"portable"`);
* absent ⇒ the lexical floor, byte-identical to before. Config-driven (the deployment injects it); OFF by
* default. 🔴 the consolidation band `{0.05,0.3}` is tuned for the lexical distance — a real embedding model
* needs it re-tuned + a no-mis-merge live check BEFORE production-enable (design/81 Slice 5 release gate). */
embedder?: import("../../core/memory.js").Embedder;
/**
* Disclosure sink for a durable read this backend's stores treat as ABSENT because the bytes were
* corrupt or unreadable (never fires on plain ENOENT — real absence).
*
* Why it lives on the BACKEND options and not only on the individual store: the fail-open it discloses
* exists for the DEFAULT deployment (a corrupt policy file yields an apparently normal, UNCONSTRAINED
* task), and the default deployment constructs its stores through this one-argument constructor — the
* per-store constructor parameter was unreachable from here, so the disclosure was wired exactly
* nowhere for the deployments that need it most.
*
* Reach TODAY: {@link FileStorageBackend.sessionPolicyStore} (an absent file ⇒ no rules; a present but
* unreadable / unparseable / malformed file ⇒ refused, the disclosure delivered alongside the refusal)
* and {@link FileStorageBackend.sessionStore}'s repo (a session listing that silently omits what it
* could not read). The aggregated roster, mailbox and usage-window stores receive the same sink (the
* usage-window ledger refuses a damaged record and discloses its location here). A store that
* grows the seat later is wired here in the same constructor.
*
* The payload is the UNION shape ({@link FileStorageCorruptReadInfo}): `path` + `reason` always,
* `sessionId` when the disclosure is about one session (the session store and the session-policy face),
* `principal` when the read was keyed by one. One sink for one failure grammar — a host does not register three.
*/
onCorruptRead?: (info: FileStorageCorruptReadInfo) => void;
}
/**
* The payload of {@link FileStorageBackendOptions.onCorruptRead} — the widest shape any store this
* backend constructs can deliver, so ONE sink reaches all of them (a per-store payload type would make
* the backend's forwarding contravariance-illegal, which is what kept the seat single-store).
* `SessionPolicyCorruptReadInfo` is assignable to it; the two-field store seats are satisfied by it.
* `path` (absolute) and `reason` ride every level; `kind` tells a skipped JSONL record (`"record"`) and
* that replay's count (`"replay"`) from a disclosure about a file or directory as a whole (`"file"`).
*/
export type FileStorageCorruptReadInfo = CorruptReadInfo & {
/** Present when the disclosure is about ONE session: the session-policy face's rows, and the session store's
* log of that session (its skipped records and tally, or the log skipped from a listing). Absent on a
* disclosure about a directory as a whole or about a store not keyed by session. */
sessionId?: string;
/** Present only when the read was keyed by a principal. */
principal?: string;
};
export declare class FileStorageBackend {
/** The resolved, canonical data root all four stores live under. */
readonly root: string;
readonly sessionStore: SessionStore;
readonly checkpointStore: CheckpointStore;
/** Wrapped in `guardedMemoryStore` (Gate-2 chokepoint). design/81: implements the id-addressable trio on
* a lexical cosine-distance → `supportsConsolidation` is true → end-of-task consolidation lights up. */
readonly memoryStore: MemoryStore;
readonly toolResultStore: ToolResultStore;
/** design/99 §E6 — file-backed per-session permission rules (E6). Wire into `RunnerDeps.sessionPolicyStore`
* for a local deployment that needs session rules to survive a restart. */
readonly sessionPolicyStore: SessionPolicyStore;
/** SVC-2 (onboarded per) — file-backed durable workflow resume journal. Wire into
* `RunnerDeps.workflowJournalStore` so `resumeFromRunId` survives a process restart on a TOC box. */
readonly workflowJournalStore: WorkflowJournalStore;
/** design/164 件五 — file-backed cross-task usage-governance ledger. Wire into
* `RunnerDeps.usageWindowStore` so a deployment's `usageWindows` survive a restart (an in-memory
* ledger silently re-grants every allowance on every boot). */
readonly usageWindowStore: UsageWindowStore;
/** design/151 S1a / subagent transcript persistence — the durable background-agent execution
* ledger over the same data root. Wire into BOTH `RunnerDeps.backgroundAgentStore` AND (for a
* deployment-composed Agent tool) `SubagentToolOptions.background.agentStore` — SAME instance
* (the RB-37 pairing discipline). Aggregated here so the TOC full-persistence assembly is one
* constructor, the same reason the session/checkpoint/tool-result trio are. */
readonly backgroundAgentStore: BackgroundAgentStore;
/** design/151 §7 S3c — the durable SendMessage mailbox (tier-3 parking lane), same data root.
* Wire into `RunnerDeps.mailboxStore` (single-instance pairing, same as above). */
readonly mailboxStore: MailboxStore;
/** design/147 S1c — the durable name→agent roster (layer 0.5 resolution), `root/roster.json`.
* Wire into `RunnerDeps.rosterStore` so name-addressing survives a restart alongside the rows. */
readonly rosterStore: RosterStore;
/**
* design/381 slice 2 — the durable per-edited-file rewind history over the same data root
* (`root/file-history/`). Wire into `RunnerDeps.fileHistoryStore` so `/rewind` survives a restart:
* WIRING IS THE OPT-IN (DV-9), so a deployment that omits it runs with rewind off, exactly as
* before this member existed. It replaces the retired whole-tree `fileSnapshotStore` member —
* per-edited-file means no size gate, no exclusion table, no tree traversal, and files the agent
* never edited are structurally out of reach of a restore.
*/
readonly fileHistoryStore: FileHistoryStore;
/**
* design/84 Seam B (TOC) — the per-scope consolidation lock for `consolidateScope`'s `acquire` injection
* point: `consolidateScope(scope, { store: backend.memoryStore, llm, acquire: backend.consolidationLock })`.
* Stops two OS processes sharing this data dir from consolidating the SAME scope at once (a busy scope ⇒
* `undefined` ⇒ the pass is a no-op). The locks live under `root/consolidation-locks/`.
*/
readonly consolidationLock: (scope: string) => (() => void) | undefined;
private readonly lock;
private readonly fileAgentRows;
private readonly fileMailbox;
private readonly fileCheckpoints;
private readonly fileMemory;
private readonly ttl;
/** RB-437 — kept so {@link dispose} can release the shared per-session authorities (and their append
* descriptors) this backend joined; `TtlSessionStore.dispose()` only clears the sweep timer. */
private readonly fileSessions;
private readonly fileWorkflowJournal;
constructor(opts?: FileStorageBackendOptions);
/**
* The data-dir boot lock could not be handed back — a root that turned read-only under a running
* process, an unreadable lock file. Teardown here is best-effort by contract, but best-effort must not
* mean SILENT: a lock file naming a LIVE pid refuses this data root to every other process, and only
* somebody saying so out loud makes that operator-actionable. (The release's own answer is the seat for
* this — a discarded boolean was the backend never learning that the fence it took at construction was
* never given back.)
*
* The wording states what a FAILED release actually establishes and no more: the removal could not be
* proven. A repeat teardown replays that same verdict without re-reading the directory (the release is
* one-shot), so this must not assert the file's CURRENT contents — only the consequence while it is
* there, and the ways out. In particular it must NOT promise that exiting clears it: the two conditions
* that produce this verdict are an UNREADABLE lock (never pruned by anyone — a stale-lock reclaim has to
* read the pid it is reclaiming) and a directory the removal cannot write; neither is undone by the
* owner exiting, and "it will be cleaned up automatically" is the one sentence that turns a five-second
* `chmod` into an outage.
*/
private discloseRetainedBootLock;
/** Release file handles + the boot lock (best-effort). Call on shutdown so a successor can acquire. */
dispose(): Promise<void>;
}