@sema-agent/core
Version:
Stateless, task-oriented AI agent core
137 lines (136 loc) • 6.19 kB
JavaScript
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}`;
}