@sema-agent/core
Version:
Stateless, task-oriented AI agent core
106 lines (105 loc) • 6.63 kB
TypeScript
/** 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;