UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

212 lines (211 loc) • 15.8 kB
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 { StrategyStore } from "../../core/strategy-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 { FileStrategyStore, type FileStrategyStoreOptions } from "./strategy-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; /** File-backed teacher-mode strategy repository (`root/strategies`) — pass as * `TeacherConfig.strategyStore` (with a `scope`) so escalation strategies survive a restart; * the in-memory default forgets everything each process, which zeroes the cross-session reuse * the repository exists for. Aggregated here so the TOC full-persistence assembly stays one * constructor (the same half-wiring hazard as the delegation trio). */ readonly strategyStore: StrategyStore; /** 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>; }