UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

106 lines (105 loc) • 6.63 kB
/** A store's retention capability declaration. Absent ⇒ read fail-closed as `"none"`. */ export type RetentionDeclaration = "managed" | "none"; /** The declaration face a storage implementation carries (structural — every store interface's * optional `retention` field conforms). `"managed"` is a PROMISE to honor * {@link ManagedRetentionCapability}; `"none"` is the honest default for stores that cannot delete * on schedule (all bundled in-memory/TTL stores declare it). */ export interface RetentionDeclaring { readonly retention?: RetentionDeclaration; } /** * The deployment's managed-retention policy ({@link import("./types.js").RunnerDeps.retentionPolicy}). * Core consumes it ONLY for the startup capability validation (and exposes it to the deployment's * scheduler); the engine never deletes data on the task path. Administrative locking rides the * locked-config plane (key `"retentionPolicy"`), not a field here — one lock mechanism, not two. */ export interface RetentionPolicy { /** Retention horizon in days. Fail-loud validated: non-finite or negative refuses the deployment * (`config.retention_policy`); `0` is absurd-but-evaluable (delete-eligible immediately) and legal. */ maxAgeDays: number; } /** * What a `retention: "managed"` store PROMISES (contract TYPE — core ships no implementation; the * server half consumes this. Reference implementation of the cascade: the server's session-purge * order contract). Every method is expected to be: * - **tenant-domain scoped**: work is enumerated and executed per retention domain (tenant/scope), * never as one global sweep; * - **idempotent**: a retried invocation after a crash re-converges (already-deleted rows are * counted, not errors); * - **tombstoned**: deletions leave tombstones so replicas/backups converge instead of resurrecting; * - **audited**: every destructive call is recorded append-only. The IDENTITY of that record — * which occurrence, which of the three operations, at what wall clock — belongs to the side that * WRITES the row, not to the receipt: an implementation records it in the same transaction as the * deletion (recording after the delete would leave an unwitnessed delete on a crash), and it is * the only side that knows its own op, sweep round, and policy generation. The receipt below is * therefore the CALL's three counts, deliberately not an audit row. */ export interface ManagedRetentionCapability { /** Enumerate the retention domains (tenant/scope keys) this store holds data for. */ listRetentionDomains(): Promise<readonly string[]>; /** CAS-fenced checkpoint expiry for one domain: `pending → expired` strictly by deadline, exactly * one winner per row across replicas, AND release the associated session pins (the reap/unpin * split is the documented gap this contract closes). */ expireCheckpoints(input: { domain: string; cutoffMs: number; }): Promise<RetentionReceipt>; /** Lineage-aware transcript/session deletion: a session tree is deleted only when no live * reference (fork child, background child, workflow run, pending checkpoint) still points into * it — references are re-checked inside the deletion transaction, never assumed from a prior * scan; {@link RETENTION_CASCADE_EXEMPT} is never deleted with one. */ deleteExpiredSessions(input: { domain: string; cutoffMs: number; }): Promise<RetentionReceipt>; /** Delete offloaded tool results whose owning sessions are gone (or past the horizon), skipping * refs still reachable from live transcripts. */ deleteOrphanToolResults(input: { domain: string; cutoffMs: number; }): Promise<RetentionReceipt>; } /** * The three counts one destructive retention call produced. NOT an audit row and deliberately * identity-free (see the capability's `audited` clause): a scheduler that needs to tell two * same-shaped calls apart reads its store's audit records, which the implementation writes with its * own occurrence id, operation, and timestamp inside the deleting transaction. Adding those here * would ask a return value to know its own call context. */ /** @contract retention.cascade_domains — a managed-retention cascade deletes checkpoint rows, * session trees (transcripts and lineage) and orphan tool results; nothing else is session data. */ export declare const RETENTION_CASCADE_DOMAINS: readonly ["checkpoints", "session-trees", "orphan-tool-results"]; /** @contract retention.governance_records_outlive_the_purge — the pollution marker dir, the capture * opt-out record dir and the lineage ledger are governance FACTS about content that exists, not * session data: deleting them makes a one-way privacy record revocable. */ export declare const RETENTION_CASCADE_EXEMPT: readonly ["session-pollution", "session-capture-optout", "lineage.json"]; export interface RetentionReceipt { domain: string; /** Rows destroyed by THIS call (idempotent retries count already-gone rows as 0, not errors). */ deleted: number; /** Rows intentionally skipped (live lineage references, legal-hold marks — hold semantics are the * scheduler's: when a hold mark exists for a domain, the scheduler skips the domain entirely and * records the skip; this field carries the per-row remainder). */ skipped: number; /** Tombstones written for the deleted rows. */ tombstones: number; } /** * Startup validation (and per-prepare backstop): with the retention policy LOCKED and configured, * every wired store must declare `retention: "managed"` — a locked policy over a store that cannot * delete is refused (`config.retention_capability`), never silently accepted. Also fail-loud * validates the policy value itself (`config.retention_policy`). Exported for deployments to call at * their own startup; the prepare preflight re-runs it on every leg regardless. */ export declare function assertRetentionCapability(input: { policy: RetentionPolicy | undefined; /** Whether locked-config locks the `retentionPolicy` key (locked-config.ts `LockedKey`). */ locked: boolean; /** The wired stores to check, each with its deployment-facing name for the refusal message. * Accepts any store object (interfaces that predate the declaration conform structurally with * no declaration ⇒ read fail-closed as `"none"`); the declaration read is the one narrow. */ stores: ReadonlyArray<{ name: string; store: object | undefined; }>; }): void;