@sema-agent/core
Version:
Stateless, task-oriented AI agent core
134 lines (133 loc) • 8.05 kB
TypeScript
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>;
}