UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

222 lines (221 loc) • 11.5 kB
import { createHash } from "node:crypto"; import { isBandId } from "./bands.js"; import { SEMA_DEFAULT_CATALOG } from "./catalog/index.js"; import { OVERLAY_OPS, PROMPT_TRAITS, isWordOf } from "./catalog/schema.js"; import { canonical } from "./epoch.js"; import { codePointsOver, sectionOfMessage, validateOverlay } from "./overlay.js"; export const ARTIFACT_LIMITS = Object.freeze({ maxOverlays: 64, maxOverlayTextBytes: 32_000, maxTotalTextBytes: 131_072, maxIntentChars: 2_000, maxRefs: 32, maxRefChars: 500, maxSourceRevisionChars: 500, }); const DIGEST_RE = /^sha256:[0-9a-f]{64}$/; const MESSAGE_IDS = new Set(SEMA_DEFAULT_CATALOG.messages.map((m) => m.id)); const SECTION_IDS = new Set(SEMA_DEFAULT_CATALOG.sections.map((s) => s.id)); function sha256Hex(text) { return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`; } export function computeArtifactDigest(payload) { return sha256Hex(canonical(payload)); } export function requiresOf(overlays) { const targets = new Map(); const need = (id, kind) => { targets.set(`${kind}:${id}`, { id, kind }); }; const ops = new Set(); const traits = new Set(); const bindings = new Set(); for (const o of overlays) { ops.add(o.op); need(o.target, o.op === "add" ? "band" : o.op === "drop" ? "section" : "message"); if (o.message !== undefined) need(o.message, "message"); for (const t of [...(o.when?.traits?.allOf ?? []), ...(o.when?.traits?.noneOf ?? [])]) traits.add(t); for (const b of [...(o.when?.binding?.anyOf ?? []), ...(o.when?.binding?.noneOf ?? [])]) bindings.add(b); } const byText = (a, b) => (a < b ? -1 : a > b ? 1 : 0); return { targets: [...targets.values()].sort((a, b) => byText(`${a.kind}:${a.id}`, `${b.kind}:${b.id}`)), ops: OVERLAY_OPS.filter((op) => ops.has(op)), traits: PROMPT_TRAITS.filter((t) => traits.has(t)), bindings: [...bindings].sort(byText), }; } export function unmetRequirements(requires) { const unmet = []; for (const t of requires.targets) { const has = t.kind === "message" ? MESSAGE_IDS.has(t.id) : t.kind === "section" ? SECTION_IDS.has(t.id) : isBandId(t.id); if (!has) unmet.push(`${t.kind} "${t.id}"`); } for (const op of requires.ops) if (!isWordOf(OVERLAY_OPS, op)) unmet.push(`op "${String(op)}"`); for (const tr of requires.traits) if (!isWordOf(PROMPT_TRAITS, tr)) unmet.push(`trait "${String(tr)}"`); return unmet; } function isRecord(v) { return v !== null && typeof v === "object" && !Array.isArray(v); } function requiresShaped(v) { if (!isRecord(v)) return false; const strings = (a) => Array.isArray(a) && a.every((s) => typeof s === "string"); return (Object.keys(v).every((k) => k === "targets" || k === "ops" || k === "traits" || k === "bindings") && Array.isArray(v.targets) && v.targets.every((t) => isRecord(t) && typeof t.id === "string" && (t.kind === "message" || t.kind === "section" || t.kind === "band") && Object.keys(t).every((k) => k === "id" || k === "kind")) && strings(v.ops) && strings(v.traits) && strings(v.bindings)); } const bindingListOf = (b) => { if (b === undefined) return undefined; const names = b.anyOf !== undefined ? b.anyOf : b.noneOf; return { anyOf: b.anyOf !== undefined, names, set: new Set(names) }; }; function bindingsMeet(a, b) { if (a === undefined || b === undefined) return true; if (a.anyOf && b.anyOf) return a.names.some((x) => b.set.has(x)); if (a.anyOf) return a.names.some((x) => !b.set.has(x)); if (b.anyOf) return b.names.some((x) => !a.set.has(x)); return true; } function replaceConflict(overlays) { const groups = new Map(); for (const o of overlays) { if (o.op !== "replace") continue; const { binding, ...rest } = o.when ?? {}; const key = canonical({ target: o.target, when: rest }); groups.set(key, [...(groups.get(key) ?? []), { o, list: bindingListOf(binding) }]); } for (const group of groups.values()) { for (let i = 0; i < group.length; i++) { for (let j = i + 1; j < group.length; j++) { const [{ o: p, list: pl }, { o: q, list: ql }] = [group[i], group[j]]; if (bindingsMeet(pl, ql)) { return `overlays "${p.id}" and "${q.id}" both replace "${p.target}" under one condition that can hold in the same run (write exclusive binding conditions: "noneOf": [x] beside "anyOf": [x])`; } } } } return undefined; } export function verifyPromptArtifact(raw) { if (!isRecord(raw)) return { ok: false, code: "shape", reason: "the published artifact is not an object" }; if (typeof raw.sourceRevision !== "string" || raw.sourceRevision.length === 0 || codePointsOver(raw.sourceRevision, ARTIFACT_LIMITS.maxSourceRevisionChars)) { return { ok: false, code: "shape", reason: `sourceRevision missing or over ${ARTIFACT_LIMITS.maxSourceRevisionChars} characters (code points)` }; } const v = verifyPromptArtifactBody(raw); return v.ok ? { ok: true, artifact: { ...v.artifact, sourceRevision: raw.sourceRevision } } : v; } export function verifyPromptArtifactBody(raw) { const fail = (code, reason) => ({ ok: false, code, reason }); if (!isRecord(raw)) return fail("shape", "the artifact is not an object"); for (const k of Object.keys(raw)) if (k !== "payload" && k !== "artifactDigest" && k !== "sourceRevision") return fail("shape", `the artifact carries ${JSON.stringify(k).slice(0, 60)}`); if (typeof raw.artifactDigest !== "string" || !DIGEST_RE.test(raw.artifactDigest)) return fail("shape", "artifactDigest malformed"); const payload = raw.payload; if (!isRecord(payload)) return fail("shape", "payload is not an object"); if (payload.schemaVersion !== 3) { const republish = payload.schemaVersion === 2 ? `; republish under schemaVersion 3 — an overlay's "scope": { "binding": b } becomes "when": { "binding": { "anyOf": [b] } }` : ""; return fail("schema", `unsupported schemaVersion ${JSON.stringify(payload.schemaVersion)?.slice(0, 40)} (this engine reads schemaVersion 3 only${republish})`); } for (const k of Object.keys(payload)) if (k !== "schemaVersion" && k !== "requires" && k !== "overlays" && k !== "evidence") return fail("shape", `payload carries ${JSON.stringify(k).slice(0, 60)}`); if (!Array.isArray(payload.overlays)) return fail("shape", "overlays is not an array"); if (payload.overlays.length === 0) return fail("shape", "overlays is empty — publish at least one overlay, or disable the source instead"); if (payload.overlays.length > ARTIFACT_LIMITS.maxOverlays) return fail("limits", `${payload.overlays.length} overlays > max ${ARTIFACT_LIMITS.maxOverlays}`); const requires = payload.requires; if (!requiresShaped(requires)) return fail("shape", "requires is not { targets: { id, kind }[], ops: string[], traits: string[], bindings: string[] }"); const unmet = unmetRequirements(requires); if (unmet.length > 0) return fail("requires", `this engine does not meet the artifact's requirements: ${unmet.slice(0, 6).join(", ")}${unmet.length > 6 ? ` and ${unmet.length - 6} more` : ""}`); const overlays = []; let totalBytes = 0; for (const rawOverlay of payload.overlays) { const v = validateOverlay(rawOverlay, { source: "center" }); if ("violation" in v) return fail("overlay", v.violation); const text = v.overlay.text; if (text !== undefined) { const bytes = Buffer.byteLength(text, "utf8"); if (bytes > ARTIFACT_LIMITS.maxOverlayTextBytes) return fail("limits", `overlay "${v.overlay.id}" text ${bytes} bytes > max ${ARTIFACT_LIMITS.maxOverlayTextBytes}`); totalBytes += bytes; } overlays.push(v.overlay); } if (totalBytes > ARTIFACT_LIMITS.maxTotalTextBytes) return fail("limits", `overlay text ${totalBytes} bytes > max ${ARTIFACT_LIMITS.maxTotalTextBytes}`); if (canonical(requiresOf(overlays)) !== canonical(requires)) return fail("requires", "requires differs from what the overlays require (build the artifact with buildPromptArtifact)"); const conflict = replaceConflict(overlays); if (conflict !== undefined) return fail("overlay", conflict); const evidence = payload.evidence; if (!isRecord(evidence) || typeof evidence.intent !== "string" || evidence.intent.trim() === "" || codePointsOver(evidence.intent, ARTIFACT_LIMITS.maxIntentChars)) { return fail("evidence", `evidence.intent is a non-blank string of at most ${ARTIFACT_LIMITS.maxIntentChars} characters (code points)`); } if (!Array.isArray(evidence.refs) || evidence.refs.length > ARTIFACT_LIMITS.maxRefs || evidence.refs.some((r) => typeof r !== "string" || r.trim() === "" || codePointsOver(r, ARTIFACT_LIMITS.maxRefChars))) { return fail("evidence", `evidence.refs is a list of at most ${ARTIFACT_LIMITS.maxRefs} non-blank references of at most ${ARTIFACT_LIMITS.maxRefChars} characters (code points) each`); } for (const k of Object.keys(evidence)) if (k !== "intent" && k !== "refs") return fail("shape", `evidence carries ${JSON.stringify(k).slice(0, 60)}`); const behaviorReplace = overlays.find((o) => o.op === "replace" && sectionOfMessage(o.target)?.band === "core-behavior"); if (behaviorReplace !== undefined && evidence.refs.length === 0) { return fail("evidence", `overlay "${behaviorReplace.id}" replaces core-behavior text; evidence.refs names at least one evaluation or golden`); } const normalized = { schemaVersion: 3, requires: requiresOf(overlays), overlays, evidence: { intent: evidence.intent, refs: [...evidence.refs] }, }; if (computeArtifactDigest(normalized) !== raw.artifactDigest) return fail("digest", "artifactDigest does not match canonical(payload)"); return { ok: true, artifact: { payload: normalized, artifactDigest: raw.artifactDigest } }; } const CENTER_ORIGIN = Object.freeze({ source: "center", by: "center" }); export function centerOverlaysOf(adoption) { if (adoption === undefined) return []; return adoption.artifact.payload.overlays.map((overlay) => ({ overlay, origin: CENTER_ORIGIN })); } export function adoptCenter(artifact, sourceRevision) { const v = verifyPromptArtifactBody(artifact); return v.ok ? { artifact: v.artifact, ...(sourceRevision !== undefined ? { sourceRevision } : {}) } : undefined; } export function buildPromptArtifact(opts) { const overlays = opts.overlays.map((o) => { const v = validateOverlay(o, { source: "center" }); return "overlay" in v ? v.overlay : o; }); const payload = JSON.parse(JSON.stringify({ schemaVersion: 3, requires: requiresOf(overlays), overlays, evidence: opts.evidence })); return verifyPromptArtifact({ payload, artifactDigest: sha256Hex(canonical(payload)), sourceRevision: opts.sourceRevision }); }