@sema-agent/core
Version:
Stateless, task-oriented AI agent core
521 lines (520 loc) • 22.3 kB
JavaScript
import { uuidv7 } from "../internal/harness.js";
import { parseScopeKey } from "./memory-engine/scope-contract.js";
import { termSet } from "./memory-vector.js";
import { attrEscape } from "./task-notification.js";
import { sanitizeUntrustedText, ENGINE_AUTHORITY_ENVELOPE_TAGS } from "./untrusted-text.js";
import { thrownText } from "./thrown-value.js";
export function supportsConsolidation(store) {
return (typeof store.searchScored === "function" &&
typeof store.update === "function" &&
typeof store.delete === "function");
}
export function supportsPeriodicConsolidation(store) {
return (typeof store.getConsolidationCursor === "function" &&
typeof store.setConsolidationCursor === "function");
}
export const CALLER_AUTHORED_TYPES = new Set(["user", "feedback", "project", "reference"]);
export const DEFAULT_CALLER_AUTHORED_TYPE = "project";
export class MemoryGateError extends Error {
code;
constructor(code, detail) {
super(detail ? `${code}: ${detail}` : code);
this.name = "MemoryGateError";
this.code = code;
}
}
export class MemoryProjectionError extends Error {
code = "memory.projection_failed";
committed = true;
path;
constructor(path, cause) {
const detail = thrownText(cause);
super(`memory.projection_failed: the note IS durably committed — only the derived projection ${path} could not be rewritten: ${detail}. ` +
`Do not retry the write (it would store the note twice); the projection is rebuilt by the next mutation of this scope.`, cause !== undefined ? { cause } : undefined);
this.name = "MemoryProjectionError";
this.path = path;
}
}
function hasPromotableMarker(note) {
return note.outcomeStats !== undefined;
}
const SECRET_PATTERNS = [
{ label: "aws_access_key_id", re: /\bAKIA[0-9A-Z]{16}\b/ },
{ label: "github_token", re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/ },
{ label: "github_fine_grained_pat", re: /\bgithub_pat_[A-Za-z0-9_]{60,}\b/ },
{ label: "openai_api_key", re: /\bsk-(?:ant-(?:api03-)?|proj-)?[A-Za-z0-9_]{20,}\b/ },
{ label: "stripe_secret_key", re: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{24,}\b/ },
{ label: "slack_token", re: /\bxox[baprs]-(?:[A-Za-z0-9]+-){1,3}[A-Za-z0-9]{16,}\b/ },
{ label: "google_api_key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
{ label: "huggingface_token", re: /\bhf_(?=[A-Za-z0-9]*[0-9])[A-Za-z0-9]{20,}\b/ },
{ label: "private_key", re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/ },
{ label: "jwt", re: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{16,}\b/ },
{ label: "azure_storage_account_key", re: /AccountKey=[A-Za-z0-9+/]{40,}={0,2}/ },
{ label: "bearer_token", re: /\bAuthorization:\s*Bearer\s+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{20,}=*/ },
];
export function detectSecret(text) {
if (!text)
return null;
for (const p of SECRET_PATTERNS) {
if (p.re.test(text))
return p.label;
}
return null;
}
export function enforceSecretWriteGate(body) {
const label = detectSecret(body);
if (label !== null) {
throw new MemoryGateError("memory.secret_detected", `memory write blocked: detected ${label}`);
}
}
export function enforceStructuredNoteSecretGate(note) {
enforceSecretWriteGate(note.body);
if (note.name !== undefined)
enforceSecretWriteGate(note.name);
if (note.description !== undefined)
enforceSecretWriteGate(note.description);
if (note.type !== undefined)
enforceSecretWriteGate(note.type);
}
export function classifyPromotable(note) {
if (hasPromotableMarker(note))
return "promotable";
if (note.type !== undefined && !CALLER_AUTHORED_TYPES.has(note.type))
return "promotable";
return "caller_authored";
}
export function enforcePromotableWriteGate(note, utilityVerdict, utilityGate) {
if (classifyPromotable(note) === "caller_authored")
return;
if (utilityVerdict === undefined) {
throw new MemoryGateError("memory.promotable_ungated", "promotable memory write requires a utilityVerdict");
}
if (utilityGate === undefined) {
throw new MemoryGateError("memory.promotable_ungated", "promotable memory write but profile utilityGate is unwired");
}
if (!utilityGate(note.outcomeStats)) {
throw new MemoryGateError("memory.promotable_ungated", "promotable memory write failed the profile utilityGate");
}
}
function stripConsolidationGenerated(note) {
if (note.consolidationGenerated === undefined)
return note;
const { consolidationGenerated: _drop, ...rest } = note;
return rest;
}
export function guardedMemoryStore(inner, utilityGate) {
const guarded = {
get vectorMode() {
return inner.vectorMode;
},
read: (scope) => inner.read(scope),
clear: (scope) => inner.clear(scope),
append: (scope, note) => {
enforceSecretWriteGate(note);
return inner.append(scope, note);
},
appendStructured: inner.appendStructured
? (scope, note) => {
enforceStructuredNoteSecretGate(note);
enforcePromotableWriteGate(note, undefined, utilityGate);
return inner.appendStructured(scope, stripConsolidationGenerated(note));
}
: undefined,
appendConsolidationGenerated: inner.appendStructured
? (scope, note) => {
enforceStructuredNoteSecretGate(note);
enforcePromotableWriteGate(note, undefined, utilityGate);
const withMarker = { ...note, consolidationGenerated: true };
if (inner.appendConsolidationGenerated)
return inner.appendConsolidationGenerated(scope, withMarker);
return inner.appendStructured(scope, withMarker);
}
: undefined,
appendPromotable: (scope, note, utilityVerdict) => {
enforceStructuredNoteSecretGate(note);
enforcePromotableWriteGate(note, utilityVerdict, utilityGate);
if (inner.appendPromotable)
return inner.appendPromotable(scope, note, utilityVerdict);
if (inner.appendStructured)
return inner.appendStructured(scope, note);
return inner.append(scope, note.body);
},
};
if (inner.search)
guarded.search = (scope, query, limit) => inner.search(scope, query, limit);
if (inner.searchScored)
guarded.searchScored = (scope, query, limit) => inner.searchScored(scope, query, limit);
if (inner.update)
guarded.update = (scope, id, text) => {
enforceSecretWriteGate(text);
return inner.update(scope, id, text);
};
if (inner.delete)
guarded.delete = (scope, id) => inner.delete(scope, id);
if (inner.listStructuredNotes)
guarded.listStructuredNotes = (scope) => inner.listStructuredNotes(scope);
if (inner.getByIds)
guarded.getByIds = (scope, ids) => inner.getByIds(scope, ids);
if (inner.getConsolidationCursor)
guarded.getConsolidationCursor = (scope) => inner.getConsolidationCursor(scope);
if (inner.setConsolidationCursor)
guarded.setConsolidationCursor = (scope, cursor) => inner.setConsolidationCursor(scope, cursor);
return guarded;
}
function renderBullet(e) {
return `- (${e.ts} UTC) ${e.text}`;
}
export class InMemoryMemoryStore {
byScope = new Map();
cursorByScope = new Map();
vectorMode = "lexical";
read(scope) {
const entries = this.byScope.get(scope);
if (!entries || entries.length === 0) {
return null;
}
return entries.map(renderBullet).join("\n");
}
append(scope, note) {
const text = note.trim().replace(/^#+\s*/, "");
if (!text) {
return;
}
const ts = new Date().toISOString().replace("T", " ").slice(0, 16);
const entry = { id: uuidv7(), text, ts };
const arr = this.byScope.get(scope) ?? [];
arr.push(entry);
this.byScope.set(scope, arr);
return entry.id;
}
appendStructured(scope, note) {
const body = note.body.trim().replace(/^#+\s*/, "");
if (!body)
return "";
const ts = new Date().toISOString().replace("T", " ").slice(0, 16);
const entry = {
id: uuidv7(),
text: body,
ts,
name: note.name,
type: note.type ?? "project",
description: note.description?.trim() || firstSentence(body),
links: note.links,
...(note.consolidationGenerated ? { consolidationGenerated: true } : {}),
};
const arr = this.byScope.get(scope) ?? [];
arr.push(entry);
this.byScope.set(scope, arr);
return entry.id;
}
clear(scope) {
this.byScope.delete(scope);
this.cursorByScope.delete(scope);
}
search(scope, query, limit = 10) {
const entries = this.byScope.get(scope);
if (!entries || entries.length === 0) {
return [];
}
return entries
.map(renderBullet)
.filter((b) => lexicalSearchMatch(query, b))
.slice(-limit);
}
searchScored(scope, query, limit = 20) {
const entries = this.byScope.get(scope);
if (!entries || entries.length === 0) {
return [];
}
const q = termSet(query);
if (q.size === 0) {
return [];
}
const scored = [];
for (const e of entries) {
const t = termSet(e.text);
let inter = 0;
for (const term of q) {
if (t.has(term))
inter++;
}
const union = q.size + t.size - inter;
const jaccard = union === 0 ? 0 : inter / union;
if (jaccard === 0)
continue;
scored.push({ id: e.id, text: e.text, score: 1 - jaccard, ...(e.consolidationGenerated ? { consolidationGenerated: true } : {}) });
}
scored.sort((a, b) => a.score - b.score);
return scored.slice(0, limit);
}
update(scope, id, text) {
const clean = text.trim();
if (!clean) {
return;
}
const entry = this.byScope.get(scope)?.find((e) => e.id === id);
if (!entry) {
return;
}
const newBody = clean.replace(/^#+\s*/, "");
if (entry.description !== undefined && entry.description === firstSentence(entry.text)) {
entry.description = firstSentence(newBody);
}
entry.text = newBody;
entry.ts = new Date().toISOString().replace("T", " ").slice(0, 16);
}
delete(scope, id) {
const arr = this.byScope.get(scope);
if (!arr) {
return;
}
const i = arr.findIndex((e) => e.id === id);
if (i !== -1) {
arr.splice(i, 1);
}
}
listStructuredNotes(scope) {
const entries = this.byScope.get(scope) ?? [];
return entries.map((e) => ({
id: e.id,
description: e.description ?? firstSentence(e.text),
...parseNoteTimestamp(e.ts),
...(e.name ? { name: e.name } : {}),
...(e.type ? { type: e.type } : {}),
...(e.consolidationGenerated ? { consolidationGenerated: true } : {}),
}));
}
getConsolidationCursor(scope) {
return this.cursorByScope.get(scope);
}
setConsolidationCursor(scope, cursor) {
this.cursorByScope.set(scope, cursor);
}
getByIds(scope, ids) {
const entries = this.byScope.get(scope);
if (!entries)
return [];
const want = new Set(ids);
return entries
.filter((e) => want.has(e.id))
.map((e) => ({
id: e.id,
text: e.text,
description: e.description ?? firstSentence(e.text),
...parseNoteTimestamp(e.ts),
...(e.name ? { name: e.name } : {}),
...(e.type ? { type: e.type } : {}),
...(e.consolidationGenerated ? { consolidationGenerated: true } : {}),
}));
}
}
export function expandLexicalTerms(term) {
const t = term.toLowerCase();
const out = new Set([t]);
const stem = lightStem(t);
const stemOk = stem !== t && stem.length >= 3;
if (stemOk)
out.add(stem);
for (const group of LEXICAL_SYNONYM_GROUPS) {
if (group.has(t) || (stemOk && group.has(stem)))
for (const w of group)
out.add(w);
}
return [...out];
}
export function lexicalSearchMatch(query, text) {
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
if (terms.length === 0)
return false;
const lower = text.toLowerCase();
const tokens = termSet(text);
return terms.every((t) => {
const variants = expandLexicalTerms(t);
return variants.some((v) => v === t ? lower.includes(v) : tokens.has(v));
});
}
const LEXICAL_SYNONYM_GROUPS = [
new Set(["auth", "authentication", "authorization", "credentials", "credential", "login", "signin"]),
new Set(["token", "bearer", "apikey", "secret", "key"]),
new Set(["password", "passwd", "passphrase", "pwd"]),
new Set(["delete", "remove", "drop", "erase"]),
new Set(["config", "configuration", "settings", "setting"]),
new Set(["error", "failure", "exception", "fault"]),
new Set(["db", "database"]),
];
function lightStem(t) {
if (t.length > 4 && t.endsWith("ies"))
return `${t.slice(0, -3)}y`;
if (t.length > 3 && t.endsWith("es"))
return t.slice(0, -2);
if (t.length > 3 && t.endsWith("s") && !t.endsWith("ss"))
return t.slice(0, -1);
return t;
}
export function firstSentence(text) {
const t = text.trim().replace(/\s+/g, " ");
const dot = t.search(/[.!?](\s|$)/);
const cut = dot >= 0 && dot < 160 ? dot + 1 : Math.min(t.length, 120);
return t.slice(0, cut).trim();
}
export function parseNoteTimestamp(ts) {
if (typeof ts !== "string" || ts.trim() === "")
return { mtimeMs: 0, timestampMissing: true };
const ms = Date.parse(`${ts.replace(" ", "T")}:00Z`);
return Number.isFinite(ms) ? { mtimeMs: ms } : { mtimeMs: 0, timestampMissing: true };
}
export const MEMORY_PROJECT_KEY_ABSENT_REASONS = Object.freeze(["not_git", "bare", "unreadable"]);
export function normalizeMemorySpec(input) {
if (!input)
return undefined;
if (Object.prototype.hasOwnProperty.call(input, "scope")) {
const e = new Error("TaskSpec.memory: the singular `scope` field was removed — write `scopes: [\"<scope>\"]` instead " +
"(an ordered read layering; `writeScope` defaults to its last entry). Refusing to start with memory silently off.");
e.code = "config.memory_scope_spelling";
throw e;
}
if (input.capture !== undefined && input.capture !== "off") {
const rawCapture = input.capture;
const got = typeof rawCapture === "string"
? JSON.stringify(rawCapture.length > 64 ? `${rawCapture.slice(0, 64)}…` : rawCapture)
: rawCapture === null
? "null"
: typeof rawCapture;
const e = new Error(`TaskSpec.memory.capture must be exactly "off" when present (got ${got}) — the capture opt-out is a ` +
`single-member closed set (absence = capture as usual; there is no "on" spelling). Refusing to guess ` +
`which way a privacy switch was meant.`);
e.code = "config.memory_capture_spelling";
throw e;
}
const projectKey = readProjectKey(input);
const raw = input.scopes ?? [];
const scopes = [];
const seen = new Set();
for (const s of raw) {
const t = typeof s === "string" ? s.trim() : "";
if (!t || seen.has(t))
continue;
seen.add(t);
scopes.push(t);
}
if (scopes.length === 0)
return undefined;
let writeScope;
if (input.writeScope === null) {
writeScope = null;
}
else if (typeof input.writeScope === "string" && input.writeScope.trim()) {
writeScope = input.writeScope.trim();
}
else {
writeScope = scopes[scopes.length - 1];
}
const trustedTools = [];
const trustedSeen = new Set();
for (const t of input.trustedTools ?? []) {
const name = typeof t === "string" ? t.trim() : "";
if (!name || trustedSeen.has(name))
continue;
trustedSeen.add(name);
trustedTools.push(name);
}
const contentSafety = {
...(trustedTools.length > 0 ? { trustedTools } : {}),
...(input.execIsExternalContent === true ? { execIsExternalContent: true } : {}),
};
const origins = input.scopeOrigins !== undefined ? { scopeOrigins: input.scopeOrigins } : {};
if (projectKey?.scope !== undefined && writeScope !== null && writeScope !== projectKey.scope) {
const e = new Error(`TaskSpec.memory.projectKey declares project scope ${JSON.stringify(projectKey.scope)} while writeScope is ${JSON.stringify(writeScope)} — ` +
`every entry this session writes would land in ${JSON.stringify(writeScope)} instead of the project's own layer. ` +
`Pin writeScope to the project scope (or drop projectKey if the layering is deliberate). Refusing to start mis-addressed.`);
e.code = "config.memory_write_scope_mismatch";
throw e;
}
const capture = input.capture === "off" ? { capture: "off" } : {};
const project = projectKey !== undefined ? { projectKey } : {};
if (input.scopeContract === "v2") {
for (const key of writeScope !== null ? [...scopes, writeScope] : scopes)
parseScopeKey(key);
return { scopes, writeScope, enabled: input.enabled !== false, scopeContract: "v2", ...contentSafety, ...origins, ...capture, ...project };
}
return { scopes, writeScope, enabled: input.enabled !== false, ...contentSafety, ...origins, ...capture, ...project };
}
function readProjectKey(input) {
const raw = input.projectKey;
if (raw === undefined)
return undefined;
const refuse = (why) => {
const e = new Error(`TaskSpec.memory.projectKey must be exactly { scope: "<non-empty>" } or { absent: ${MEMORY_PROJECT_KEY_ABSENT_REASONS.map((r) => JSON.stringify(r)).join(" | ")} } — ${why}. ` +
`Refusing to guess whether this deployment has a project identity.`);
e.code = "config.memory_project_key_spelling";
throw e;
};
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
return refuse(`got ${raw === null ? "null" : Array.isArray(raw) ? "an array" : typeof raw}`);
const scope = raw.scope;
const absent = raw.absent;
if ((scope === undefined) === (absent === undefined))
return refuse(absent === undefined ? "neither arm was spelled" : "both arms were spelled");
if (absent !== undefined) {
if (typeof absent !== "string" || !MEMORY_PROJECT_KEY_ABSENT_REASONS.includes(absent)) {
return refuse(`the absence reason ${typeof absent === "string" ? JSON.stringify(absent.slice(0, 64)) : typeof absent} is not one of the stated words`);
}
return { absent: absent };
}
if (typeof scope !== "string" || scope.trim() === "")
return refuse(typeof scope === "string" ? "the project scope is blank" : `the project scope is ${typeof scope}`);
return { scope: scope.trim() };
}
export function memoryProjectScopeAbsentNotice(input) {
return {
code: "memory.project_scope_absent",
message: `No project identity was derived for this session's working root (${input.reason}), so its memory layering is used as declared: ` +
`${input.writeScope === null ? "the session writes nothing (read-only layering)" : `entries this session writes go to the shared layer ${JSON.stringify(input.writeScope)}`}. ` +
`If this root should have its own project memory, fix how the deployment resolves it — nothing here retries.`,
detail: { reason: input.reason, taskRoot: input.taskRoot, sessionId: input.sessionId, ...(input.writeScope !== null ? { writeScope: input.writeScope } : {}) },
};
}
export const MAX_MEMORY_BYTES = 100 * 1024;
export const MEMORY_WRAPPER_TAGS = ["user_memory"];
export const MEMORY_INJECTION_TAGS = [...ENGINE_AUTHORITY_ENVELOPE_TAGS];
export function composeMemoryBlock(content, scope) {
const trimmed = (content ?? "").trim();
if (!trimmed) {
return undefined;
}
let payload = trimmed;
const bytes = Buffer.byteLength(trimmed, "utf8");
if (bytes > MAX_MEMORY_BYTES) {
const buf = Buffer.from(trimmed, "utf8");
let cut = MAX_MEMORY_BYTES;
while (cut > 0 && (buf[cut] & 0xc0) === 0x80) {
cut--;
}
const head = buf.subarray(0, cut).toString("utf8");
payload = `${head}\n<truncated bytes=${bytes - cut}>`;
}
return `<user_memory scope="${attrEscape(scope)}">\n${sanitizeUntrustedText(payload, [...MEMORY_INJECTION_TAGS])}\n</user_memory>`;
}
export function composeLayeredMemoryBlock(layers) {
const sections = [];
for (const layer of layers) {
const trimmed = (layer.content ?? "").trim();
if (!trimmed)
continue;
let payload = trimmed;
const bytes = Buffer.byteLength(trimmed, "utf8");
if (bytes > MAX_MEMORY_BYTES) {
const buf = Buffer.from(trimmed, "utf8");
let cut = MAX_MEMORY_BYTES;
while (cut > 0 && (buf[cut] & 0xc0) === 0x80)
cut--;
payload = `${buf.subarray(0, cut).toString("utf8")}\n<truncated bytes=${bytes - cut}>`;
}
sections.push(`<scope name="${attrEscape(layer.scope)}">\n` +
`${sanitizeUntrustedText(payload, [...MEMORY_INJECTION_TAGS])}\n` +
`</scope>`);
}
if (sections.length === 0)
return undefined;
return `<user_memory>\n${sections.join("\n")}\n</user_memory>`;
}