UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

202 lines (201 loc) • 10.2 kB
import { uuidv7 } from "../../internal/harness.js"; import { firstSentence } from "../memory.js"; import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js"; import { scanMemoryFileName, scanMemoryWrite } from "./scan.js"; import { MEMORY_INDEX_FILENAME } from "./entry-scan.js"; import { scopeDirName } from "./layout.js"; export const REMOTE_HARVEST_PER_FILE_BYTES = 100 * 1024; export const REMOTE_MASS_DELETION_FUSE_RATIO = 0.5; export const REMOTE_HARVEST_MAX_FILES = 500; export const REMOTE_HARVEST_MAX_DEPTH = 3; export async function materializeEntriesToFiles(backend, scopes, writeScope) { const all = [...new Set(writeScope !== null ? [...scopes, writeScope] : [...scopes])]; const headers = await backend.listHeaders(all); const entries = await backend.getByIds(headers.map((h) => h.id)); const files = []; const readonlyDirs = [...new Set(all.filter((s) => s !== writeScope).map((s) => scopeDirName(s)))]; const baseline = { writeScope, files: [], readonlyDirs }; for (const e of entries) { const readonly = e.scope !== writeScope; const relPath = readonly ? `${scopeDirName(e.scope)}/${e.slug}.md` : `${e.slug}.md`; const content = serializeEntryFile(e); files.push({ relPath, content, readonly }); baseline.files.push({ relPath, id: e.id, rev: e.rev, scope: e.scope, readonly }); } return { files, baseline }; } export function harvestFilesToPatches(baseline, files, opts = {}) { const perFileBytes = opts.perFileBytes ?? REMOTE_HARVEST_PER_FILE_BYTES; const fuseRatio = opts.massDeletionFuseRatio ?? REMOTE_MASS_DELETION_FUSE_RATIO; const maxFiles = opts.maxFiles ?? REMOTE_HARVEST_MAX_FILES; const maxDepth = opts.maxDepth ?? REMOTE_HARVEST_MAX_DEPTH; const out = { patches: [], rejections: [], missing: [], projections: [] }; const writeScope = baseline.writeScope; if (writeScope === null) return out; const byRel = new Map(files.map((f) => [f.relPath, f])); const baseByRel = new Map(baseline.files.map((b) => [b.relPath, b])); const basePathById = new Map(baseline.files.filter((b) => !b.readonly).map((b) => [b.id, b.relPath])); const readonlyPrefixes = new Set(); for (const b of baseline.files) { if (!b.readonly) continue; const top = b.relPath.split("/")[0]; if (top !== undefined && b.relPath.includes("/")) readonlyPrefixes.add(`${top}/`); } for (const d of baseline.readonlyDirs ?? []) { if (d !== "") readonlyPrefixes.add(`${d}/`); } const inReadonlyDir = (relPath) => { for (const p of readonlyPrefixes) if (relPath.startsWith(p)) return true; return false; }; for (const b of baseline.files.filter((x) => x.readonly)) { const f = byRel.get(b.relPath); if (f === undefined) { out.missing.push(b.relPath); continue; } if (revOf(f.content, b.id) !== b.rev) { out.rejections.push({ path: b.relPath, code: "readonly_layer", reason: "a read-only inherited memory layer was modified; changes to inherited layers are not writable from this session", }); } } const presentIds = new Set(); const parsedByRel = new Map(); for (const f of files) { if (baseByRel.get(f.relPath)?.readonly) continue; const p = parseEntryFile(f.content); parsedByRel.set(f.relPath, p); if (p.id !== undefined) presentIds.add(p.id); } const writable = baseline.files.filter((b) => !b.readonly); const missingWritable = writable.filter((b) => !byRel.has(b.relPath) && !presentIds.has(b.id)); out.missing.push(...missingWritable.map((b) => b.relPath)); if (writable.length > 0 && missingWritable.length / writable.length > fuseRatio) { out.incident = { kind: "mass_deletion", detail: `${missingWritable.length}/${writable.length} materialized memory files are missing from the read-back set — judged an accident, harvest refused (no patches; explicit frontmatter tombstones are the only deletion channel)`, }; out.patches = []; return out; } let kept = 0; for (const f of files) { const base = baseByRel.get(f.relPath); if (base?.readonly) continue; if (f.relPath.startsWith("/") || f.relPath.split("/").some((seg) => seg === ".." || seg === "." || seg === "")) { out.rejections.push({ path: f.relPath, code: "outside_root", reason: "file path escapes the memory root (containment gate, fail-closed)" }); continue; } const baseName = f.relPath.split("/").pop() ?? f.relPath; if (baseName === MEMORY_INDEX_FILENAME || !baseName.endsWith(".md") || baseName.startsWith(".")) continue; if (inReadonlyDir(f.relPath)) { if (base === undefined) { out.rejections.push({ path: f.relPath, code: "readonly_layer", reason: "a file inside a read-only inherited layer's directory is not writable from this session" }); } continue; } if (f.relPath.split("/").length - 1 > maxDepth) { out.rejections.push({ path: f.relPath, code: "nested_too_deep", reason: `memory files nested deeper than ${maxDepth} directory levels are not harvested` }); continue; } if (kept >= maxFiles) { out.rejections.push({ path: f.relPath, code: "file_cap", reason: `memory file count exceeds the cap (${maxFiles}); consolidate before adding more` }); continue; } kept++; const baseUnchanged = base !== undefined && revOf(f.content, base.id) === base.rev; if (baseUnchanged) continue; const sizeBytes = Buffer.byteLength(f.content, "utf8"); if (sizeBytes > perFileBytes) { out.rejections.push({ path: f.relPath, code: "too_large", reason: `memory file is ${sizeBytes} bytes — over the ${perFileBytes}-byte cap; split or trim it (rejected, NOT truncated)` }); continue; } const findings = []; const nameFinding = scanMemoryFileName(f.relPath); if (nameFinding !== undefined) findings.push(nameFinding); findings.push(...scanMemoryWrite(f.content)); const finding = findings[0]; if (finding !== undefined) { out.rejections.push({ path: f.relPath, code: finding.code, reason: `memory write blocked: ${finding.reason} (remote lane: the deployment owns quarantine I/O)` }); continue; } const parsed = parsedByRel.get(f.relPath) ?? parseEntryFile(f.content); const baseId = base?.id; let id = baseId ?? parsed.id; if (id === undefined) id = uuidv7(); const fm = { ...parsed.frontmatter }; const slug = f.relPath.replace(/\.md$/i, ""); if (fm.name === undefined) fm.name = slug.split("/").pop(); if (fm.description === undefined && !fm.deleted) fm.description = firstSentence(parsed.body) || undefined; const entry = { id, slug, frontmatter: fm, body: parsed.body, rev: "", scope: writeScope }; entry.rev = computeEntryRev(entry); if (fm.deleted === true) { if (baseId !== undefined || (parsed.id !== undefined && basePathById.has(parsed.id))) { const basePath = baseId !== undefined ? f.relPath : basePathById.get(parsed.id); const baseRev = baseByRel.get(basePath)?.rev; out.patches.push({ op: "delete", id, ...(baseRev !== undefined ? { baseRev } : {}) }); } continue; } if (baseId !== undefined) { out.projections.push({ relPath: f.relPath, entry, content: serializeEntryFile(entry) }); out.patches.push({ op: "update", id, entry, ...(base.rev !== undefined ? { baseRev: base.rev } : {}) }); continue; } const renamedFrom = parsed.id !== undefined ? basePathById.get(parsed.id) : undefined; if (renamedFrom !== undefined && !byRel.has(renamedFrom)) { const moved = { ...entry, id: parsed.id, rev: computeEntryRev({ id: parsed.id, frontmatter: fm, body: parsed.body }) }; out.projections.push({ relPath: f.relPath, entry: moved, content: serializeEntryFile(moved) }); const baseRev = baseByRel.get(renamedFrom)?.rev; out.patches.push({ op: "update", id: parsed.id, entry: moved, ...(baseRev !== undefined ? { baseRev } : {}) }); continue; } out.projections.push({ relPath: f.relPath, entry, content: serializeEntryFile(entry) }); out.patches.push({ op: "add", id: entry.id, entry }); } return out; } export function projectionsToWriteBack(result, report) { const appliedIds = new Set(report.applied.filter((a) => a.op !== "delete").map((a) => a.id)); return result.projections.filter((p) => appliedIds.has(p.entry.id)).map((p) => ({ relPath: p.relPath, content: p.content })); } export function screenInboundEntries(entries, opts = {}) { const perFileBytes = opts.perFileBytes ?? REMOTE_HARVEST_PER_FILE_BYTES; const out = []; for (const e of entries) { const findings = []; const nameFinding = scanMemoryFileName(`${e.slug}.md`); if (nameFinding !== undefined) findings.push(nameFinding); const serialized = serializeEntryFile(e); const size = Buffer.byteLength(serialized, "utf8"); if (size > perFileBytes) findings.push({ code: "too_large", reason: `serialized entry is ${size} bytes — over the ${perFileBytes}-byte cap` }); findings.push(...scanMemoryWrite(serialized)); if (findings.length > 0) out.push({ id: e.id, findings }); } return out; } function revOf(text, id) { const parsed = parseEntryFile(text); return computeEntryRev({ id, frontmatter: parsed.frontmatter, body: parsed.body }); }