UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

189 lines (188 loc) • 7.55 kB
import { mkdirSync, promises as fs } from "node:fs"; import path from "node:path"; import { createSafeNotifier, observeThenableRejection } from "../core/safe-notify.js"; import { verifyPromptArtifact } from "./artifact.js"; export function refusalClause(refusal) { return refusal.code === "unreadable" ? `unreadable (${refusal.reason})` : `refused by this engine (${refusal.code}: ${refusal.reason})`; } export function storedArtifactLine(artifactDigest, refusal, consequence) { return `center prompt artifact ${artifactDigest.slice(0, 23)}… ${refusal !== undefined ? `is ${refusalClause(refusal)}` : "has no stored copy"} — ${consequence}`; } function tellRefused(onRefused, refusal) { const notifier = createSafeNotifier(); notifier.notify(() => observeThenableRejection(onRefused?.(refusal), notifier, "artifact-store.onRefused"), "artifact-store.onRefused"); return undefined; } function verifiedRead(bytes, artifactDigest, onRefused) { const refuse = (refusal) => tellRefused(onRefused, refusal); let parsed; try { parsed = JSON.parse(bytes); } catch { return refuse({ code: "shape", reason: "the stored bytes are not JSON" }); } const v = verifyPromptArtifact(parsed); if (!v.ok) return refuse({ code: v.code, reason: v.reason }); return v.artifact.artifactDigest === artifactDigest ? v.artifact : refuse({ code: "digest", reason: "the stored artifact is not the one its digest names" }); } export class MemoryPromptArtifactStore { byDigest = new Map(); async get(artifactDigest, onRefused) { const bytes = this.byDigest.get(artifactDigest); return bytes === undefined ? undefined : verifiedRead(bytes, artifactDigest, onRefused); } async put(artifact) { this.byDigest.set(artifact.artifactDigest, JSON.stringify(artifact)); } } export class FilePromptArtifactStore { dir; constructor(dir) { this.dir = dir; mkdirSync(path.join(dir, "artifacts"), { recursive: true }); } fileFor(artifactDigest) { const m = /^sha256:([0-9a-f]{64})$/.exec(artifactDigest); return m ? path.join(this.dir, "artifacts", `${m[1]}.json`) : undefined; } async get(artifactDigest, onRefused) { const file = this.fileFor(artifactDigest); if (file === undefined) return undefined; let bytes; try { bytes = await fs.readFile(file, "utf8"); } catch (err) { const code = typeof err === "object" && err !== null ? err.code : undefined; if (code === "ENOENT") return undefined; return tellRefused(onRefused, { code: "unreadable", reason: `the store could not read its file: ${typeof code === "string" ? code : "an error with no code"}` }); } return verifiedRead(bytes, artifactDigest, onRefused); } async put(artifact) { const file = this.fileFor(artifact.artifactDigest); if (file === undefined) throw new Error(`prompt-artifact store: malformed digest ${JSON.stringify(artifact.artifactDigest).slice(0, 120)}`); const tmp = `${file}.tmp-${process.pid}-${Date.now()}`; await fs.writeFile(tmp, JSON.stringify(artifact), "utf8"); await fs.rename(tmp, file); } } export class MemoryPromptSourceStateStore { state; async load() { return this.state; } async save(state) { this.state = state; } } export class FilePromptSourceStateStore { file; constructor(dir) { mkdirSync(dir, { recursive: true }); this.file = path.join(dir, "source-state.json"); } async load() { let bytes; try { bytes = await fs.readFile(this.file, "utf8"); } catch { return undefined; } try { const raw = JSON.parse(bytes); if (raw?.state === "active" && typeof raw.artifactDigest === "string" && typeof raw.sourceRevision === "string") { return { state: "active", artifactDigest: raw.artifactDigest, sourceRevision: raw.sourceRevision }; } if (raw?.state === "disabled" && typeof raw.sourceRevision === "string") { return { state: "disabled", sourceRevision: raw.sourceRevision }; } } catch { } return undefined; } async save(state) { const tmp = `${this.file}.tmp-${process.pid}-${Date.now()}`; await fs.writeFile(tmp, JSON.stringify(state), "utf8"); await fs.rename(tmp, this.file); } } export class CenterPromptSource { store; stateStore; onError; notifier = createSafeNotifier(); candidate; state = "unavailable"; tail = Promise.resolve(); exclusive(fn) { const run = this.tail.then(fn, fn); this.tail = run.then(() => undefined, () => undefined); return run; } constructor(opts) { this.store = opts.store; this.stateStore = opts.stateStore ?? new MemoryPromptSourceStateStore(); this.onError = opts.onError; } current() { return this.candidate; } async get(artifactDigest, onRefused) { if (this.candidate?.artifactDigest === artifactDigest) return this.candidate; return this.store.get(artifactDigest, onRefused); } async restore() { return this.exclusive(async () => { const state = await this.stateStore.load(); if (state?.state === "disabled") { this.candidate = undefined; this.state = "disabled"; return; } if (state?.state !== "active") { this.candidate = undefined; this.state = "unavailable"; return; } const refused = {}; const stored = await this.store.get(state.artifactDigest, (r) => void (refused.current = r)); this.candidate = stored !== undefined ? { ...stored, sourceRevision: state.sourceRevision } : undefined; this.state = stored !== undefined ? "active" : "unavailable"; if (stored === undefined) { const line = new Error(storedArtifactLine(state.artifactDigest, refused.current, "the source boots unavailable: a new session has no center layer until a verified artifact is applied")); this.notifier.notify(() => observeThenableRejection(this.onError?.(line, { phase: "prompt-constitution", sessionId: "", classification: "center-source-restore" }), this.notifier, "center-source.onError"), "center-source.onError"); } }); } sourceState() { return this.state; } async applySnapshot(raw) { const v = verifyPromptArtifact(raw); if (!v.ok) return v; return this.exclusive(async () => { await this.store.put(v.artifact); await this.stateStore.save({ state: "active", artifactDigest: v.artifact.artifactDigest, sourceRevision: v.artifact.sourceRevision }); this.candidate = v.artifact; this.state = "active"; return { ok: true, artifactDigest: v.artifact.artifactDigest }; }); } async applyDisabled(sourceRevision) { return this.exclusive(async () => { await this.stateStore.save({ state: "disabled", sourceRevision }); this.candidate = undefined; this.state = "disabled"; }); } }