UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

137 lines (136 loc) • 6.19 kB
import { MEMORY_INJECTION_TAGS } from "./memory.js"; import { sanitizeUntrustedText } from "./untrusted-text.js"; export const DEFAULT_MAX_SELECTED = 5; export const DEFAULT_MAX_LINKED = 3; export const DEFAULT_SELECT_TIMEOUT_MS = 5_000; const MANIFEST_MAX_LINES = 200; const MANIFEST_MAX_BYTES = 25_000; const SELECTED_BODY_MAX_BYTES = 50_000; const ONE_DAY_MS = 24 * 60 * 60 * 1_000; const ONE_WEEK_MS = 7 * ONE_DAY_MS; const ONE_MONTH_MS = 30.44 * ONE_DAY_MS; export function formatMemoryAge(ageMs) { if (ageMs < ONE_DAY_MS) return "today"; if (ageMs < 2 * ONE_DAY_MS) return "yesterday"; if (ageMs < ONE_WEEK_MS) return `${Math.floor(ageMs / ONE_DAY_MS)} days ago`; if (ageMs < ONE_MONTH_MS) { const weeks = Math.floor(ageMs / ONE_WEEK_MS); return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`; } const months = Math.floor(ageMs / ONE_MONTH_MS); return `${months} ${months === 1 ? "month" : "months"} ago`; } export const RECALL_CAVEAT = "These notes were recalled as relevant, but they are point-in-time observations from when they were " + "written — they may now be stale. Trust current tool output over memory, and update or remove a note you " + "find is wrong.\n\n" + "## Before recommending from memory\n" + '- "The memory says X exists" is not the same as "X exists now" — a note is not proof the thing still exists.\n' + "- If the user is about to act on your recommendation (not just asking about history), verify first.\n" + "- If a note names a file, check the file exists; if it names a function or symbol, grep for it; if it names a flag or value, read the current source.\n" + "- Memory is a snapshot from when it was written, not a live view — prefer `git log` or reading the code over recalling the snapshot."; function isNewerNote(candidate, incumbent) { if (candidate.timestampMissing || incumbent.timestampMissing) return false; return candidate.mtimeMs > incumbent.mtimeMs; } export function resolveLinkedIds(headers, selected, max) { if (max <= 0 || selected.length === 0) return []; const byName = new Map(); for (const h of headers) { if (!h.name) continue; const prev = byName.get(h.name); if (!prev || isNewerNote(h, prev)) byName.set(h.name, h); } const selectedIds = new Set(selected.map((r) => r.id)); const out = []; const seen = new Set(); for (const r of selected) { for (const m of r.text.matchAll(/\[\[([^[\]\n]{1,128})\]\]/g)) { const target = byName.get(m[1].trim()); if (!target || selectedIds.has(target.id) || seen.has(target.id)) continue; seen.add(target.id); out.push(target.id); if (out.length >= max) return out; } } return out; } function manifestField(s, cap) { const t = s.replace(/\s+/g, " ").trim(); return t.length > cap ? `${t.slice(0, cap)}…` : t; } export function buildManifestText(headers) { const lines = []; let bytes = 0; let truncated; for (const h of headers) { if (lines.length >= MANIFEST_MAX_LINES) { truncated = "lines"; break; } const tag = h.type ? `[${manifestField(h.type, 32)}] ` : ""; const line = `- ${h.id}: ${tag}${manifestField(h.description, 300)}`; const lb = Buffer.byteLength(line, "utf8") + 1; if (bytes + lb > MANIFEST_MAX_BYTES) { truncated = "bytes"; break; } lines.push(line); bytes += lb; } const note = truncated ? `\n<manifest truncated: ${truncated} cap reached; ${headers.length - lines.length} notes omitted — consolidation should shrink memory>` : ""; return lines.join("\n") + note; } export function validateSelectedIds(headers, ids, max) { const valid = new Set(headers.map((h) => h.id)); const out = []; const seen = new Set(); for (const id of ids) { if (out.length >= max) break; if (valid.has(id) && !seen.has(id)) { seen.add(id); out.push(id); } } return out; } export function composeSelectiveBody(manifestText, selected, nowMs, linked = [], recallable = true, recallToolName) { const renderNote = (r, label) => { const ageMs = nowMs - r.mtimeMs; const verify = r.timestampMissing || ageMs > ONE_DAY_MS ? " — verify it's still current" : ""; const stale = r.timestampMissing ? ` (write time unknown${verify})` : ` (written ${formatMemoryAge(ageMs)}${verify})`; const prefix = label ? `${sanitizeUntrustedText(label, [...MEMORY_INJECTION_TAGS])} ` : ""; return `- ${prefix}${sanitizeUntrustedText(r.text, [...MEMORY_INJECTION_TAGS])}${stale}`; }; let body = selected.map((r) => renderNote(r)).join("\n"); if (linked.length > 0) { body += `\n\nLinked notes (followed one hop from [[name]] references above — same staleness caveats apply):\n` + linked.map((r) => renderNote(r, r.name ? `[[${r.name}]]` : "")).join("\n"); } if (Buffer.byteLength(body, "utf8") > SELECTED_BODY_MAX_BYTES) { const buf = Buffer.from(body, "utf8"); let cut = SELECTED_BODY_MAX_BYTES; while (cut > 0 && (buf[cut] & 0xc0) === 0x80) cut--; body = `${buf.subarray(0, cut).toString("utf8")}\n<selected notes truncated to ${SELECTED_BODY_MAX_BYTES} bytes>`; } const selectedSection = selected.length > 0 ? `\n\nRelevant notes:\n${body}` : "\n\n(no notes selected as relevant to this task)"; const recallCall = recallToolName !== undefined ? `call ${recallToolName}` : `search the memory-recall tool bound to this scope`; const manifestSection = recallable ? `Memory index — to load a note in full, ${recallCall} with keywords from its description:\n${sanitizeUntrustedText(manifestText, [...MEMORY_INJECTION_TAGS])}` : ""; return `${manifestSection}${selectedSection}`; } export function encodeSurfacedKey(scope, id) { return `${scope.length}:${scope}:${id}`; }