UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

134 lines (133 loc) • 8.05 kB
import type { RunnerDeps } from "../core/runner-deps.js"; import { type ArtifactVerifyCode, type PromptArtifact, type PublishedPromptArtifact } from "./artifact.js"; /** Why a copy a store holds could not be used: the verifier refused it (its code and reason — a retired schema's reason * names the republish rewrite), or the store could not read it back (`unreadable`, the reason naming the read error). */ export interface StoredArtifactRefusal { readonly code: ArtifactVerifyCode | "unreadable"; readonly reason: string; } /** What a read tells its caller when a copy was there and could not be used. */ export type OnArtifactRefused = (refusal: StoredArtifactRefusal) => void; /** The clause every reader says of a copy that could not be used, by its cause — one wording, wherever it is said. */ export declare function refusalClause(refusal: StoredArtifactRefusal): string; /** * The operator line for a stored center artifact that could not be read back: which copy, why — refused or unreadable * (the clause above), or no copy at all — and what follows from it. The one sentence a boot restore and a pinned resume * say. */ export declare function storedArtifactLine(artifactDigest: string, refusal: StoredArtifactRefusal | undefined, consequence: string): string; export interface PromptArtifactStore { /** * Verified read by digest — undefined on a miss OR on a copy that cannot be used. When a copy was there and could not * be used — the verifier refused its bytes, or the store could not read them — `onRefused` is told once before the * read answers undefined, so a caller can tell a refused or unreadable copy (a retired schema, a corrupt file, a file * the process may not read) from a missing one. A store that holds a copy it cannot use calls it. */ get(artifactDigest: string, onRefused?: OnArtifactRefused): Promise<PublishedPromptArtifact | undefined>; /** Write by digest (the caller verified before putting). */ put(artifact: PublishedPromptArtifact): Promise<void>; } export declare class MemoryPromptArtifactStore implements PromptArtifactStore { private readonly byDigest; get(artifactDigest: string, onRefused?: OnArtifactRefused): Promise<PublishedPromptArtifact | undefined>; put(artifact: PublishedPromptArtifact): Promise<void>; } /** `artifacts/<hex>.json` under a caller-owned directory; atomic tmp+rename writes. */ export declare class FilePromptArtifactStore implements PromptArtifactStore { private readonly dir; constructor(dir: string); private fileFor; /** A file that is not there is a miss; one that is there and cannot be read (a directory in its place, a file the * process may not open, too many open files) is told to `onRefused` as `unreadable`, by its error code. */ get(artifactDigest: string, onRefused?: OnArtifactRefused): Promise<PublishedPromptArtifact | undefined>; put(artifact: PublishedPromptArtifact): Promise<void>; } export type PromptSourceState = { state: "active"; artifactDigest: string; sourceRevision: string; } | { state: "disabled"; sourceRevision: string; }; export interface PromptSourceStateStore { load(): Promise<PromptSourceState | undefined>; save(state: PromptSourceState): Promise<void>; } export declare class MemoryPromptSourceStateStore implements PromptSourceStateStore { private state; load(): Promise<PromptSourceState | undefined>; save(state: PromptSourceState): Promise<void>; } /** `source-state.json` sibling of the artifact dir; atomic tmp+rename; malformed file = undefined * (stateless boot — conservative: no candidate until the next verified response). */ export declare class FilePromptSourceStateStore implements PromptSourceStateStore { private readonly file; constructor(dir: string); load(): Promise<PromptSourceState | undefined>; save(state: PromptSourceState): Promise<void>; } /** The narrow face the Runner consumes (`RunnerDeps.promptSource`) — resolution only, no publish. */ export interface PromptArtifactSource { /** The current verified candidate new sessions (and every top-level session at a compaction) adopt; undefined = none. */ current(): PublishedPromptArtifact | undefined; /** Resolve a PINNED digest (the resume path). Undefined = unavailable — the caller fails loud. `onRefused` is the * store's (see {@link PromptArtifactStore.get}): told when a copy was there and could not be used, so the caller says * why. */ get(artifactDigest: string, onRefused?: OnArtifactRefused): Promise<PromptArtifact | undefined>; /** The tri-state every adoption decision keys on: `"active"` = a verified candidate is live; `"disabled"` = an * EXPLICIT verified revocation (`applyDisabled`) — the one state that empties a pinned center layer; * `"unavailable"` = no verified state (never configured, or a store miss at boot) — an outage never strips policy, * only an explicit revocation does. */ sourceState(): "active" | "disabled" | "unavailable"; } /** * The candidate state machine. Publish-side entry points: * - `applySnapshot(raw)` — a fetched published artifact: verify → store.put → persist active state → atomic * in-memory swap. Any verification failure leaves EVERYTHING unchanged. * - `applyDisabled(sourceRevision)` — an explicit, successful disable/delete response: persist disabled state + * drop the candidate. (Network failure/304 = simply don't call anything.) * - `restore()` — boot: re-resolve the persisted active state through the verified store; a miss, an unreadable copy * or a verify failure boots stateless (no candidate) without touching the persisted record, and says which on * `onError` (a refused copy — a retired schema's reason names the republish rewrite — and an unreadable one are told * apart from no copy at all). */ export declare class CenterPromptSource implements PromptArtifactSource { private readonly store; private readonly stateStore; private readonly onError; private readonly notifier; private candidate; private state; /** Serialization tail for the three STATE-MUTATING entry points. Each is a multi-await sequence ending in a * synchronous swap of (candidate, state); with two in flight, an interleaving where B's `stateStore.save` * resolves and swaps before A's would leave the persisted record naming B's digest while the in-memory candidate * names A's. `CenterPromptSource` is driven by a caller's poll loop, so overlapping applies are an ordinary * schedule. Serializing costs nothing (publish-rate operations) and makes the durable record and the in-memory * candidate agree by construction. */ private tail; /** Run `fn` after every previously-queued mutation settles (success OR failure — a failed apply must not wedge the queue). */ private exclusive; /** `onError` is the operator channel of the source's own acts (a boot restore that finds its persisted artifact * refused, unreadable or gone) — the Runner's sink, in its shape (`RunnerDeps.onError`): pass the Runner's. A boot * restore belongs to no session: its context is `{ phase: "prompt-constitution", sessionId: "", classification: * "center-source-restore" }`. Omitted = those acts are silent. */ constructor(opts: { store: PromptArtifactStore; stateStore?: PromptSourceStateStore; onError?: RunnerDeps["onError"]; }); current(): PublishedPromptArtifact | undefined; get(artifactDigest: string, onRefused?: OnArtifactRefused): Promise<PromptArtifact | undefined>; restore(): Promise<void>; sourceState(): "active" | "disabled" | "unavailable"; applySnapshot(raw: unknown): Promise<{ ok: true; artifactDigest: string; } | { ok: false; code: ArtifactVerifyCode; reason: string; }>; applyDisabled(sourceRevision: string): Promise<void>; }