@sema-agent/core
Version:
Stateless, task-oriented AI agent core
99 lines (98 loc) • 4.31 kB
JavaScript
import { supportsConsolidation, supportsPeriodicConsolidation } from "./memory.js";
import { runMemoryConsolidation, DEFAULT_CONSOLIDATION_BAND, DEFAULT_CONSOLIDATION_SEARCH_LIMIT, DEFAULT_CONSOLIDATION_MAX_NOTES, } from "./runner/memory-consolidation.js";
function maxMarker(a, b) {
return a === undefined || b > a ? b : a;
}
function cappedHighWater(seen, blockers) {
let floor;
for (const id of blockers)
if (floor === undefined || id < floor)
floor = id;
let hw;
for (const id of seen) {
if (floor !== undefined && id >= floor)
continue;
hw = maxMarker(hw, id);
}
return hw;
}
export async function consolidateScope(scope, deps, opts = {}) {
const { store, onWarn } = deps;
if (!supportsPeriodicConsolidation(store)) {
onWarn?.(new Error("consolidateScope: store does not implement the consolidation cursor pair " +
"(getConsolidationCursor/setConsolidationCursor) — skipped (no-op), NOT degraded to a full re-consolidation"));
return undefined;
}
if (!supportsConsolidation(store) ||
typeof store.listStructuredNotes !== "function" ||
typeof store.getByIds !== "function") {
onWarn?.(new Error("consolidateScope: store is not id-addressable / has no manifest read pair — skipped (no-op)"));
return undefined;
}
const release = deps.acquire ? await deps.acquire(scope) : () => { };
if (release === undefined) {
onWarn?.(new Error(`consolidateScope: scope "${scope}" is locked by another consolidation — skipped (no-op)`));
return undefined;
}
try {
const cursor = await store.getConsolidationCursor(scope);
const headers = await store.listStructuredNotes(scope);
const pending = headers
.filter((h) => !h.consolidationGenerated && (cursor === undefined || h.id > cursor))
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
if (pending.length === 0) {
return undefined;
}
const maxNotes = opts.maxNotes ?? DEFAULT_CONSOLIDATION_MAX_NOTES;
const batch = pending.slice(0, maxNotes);
const batchIds = batch.map((h) => h.id);
const records = await store.getByIds(scope, batchIds);
const byId = new Map(records.map((r) => [r.id, r]));
const notes = batchIds
.map((id) => byId.get(id))
.filter((r) => r !== undefined)
.map((r) => ({ id: r.id, text: r.text }));
let stats;
if (notes.length > 0) {
stats = await runMemoryConsolidation({
store,
scope,
notes,
band: opts.band ?? { lo: DEFAULT_CONSOLIDATION_BAND.lo, hi: DEFAULT_CONSOLIDATION_BAND.hi },
searchLimit: opts.searchLimit ?? DEFAULT_CONSOLIDATION_SEARCH_LIMIT,
maxNotes,
llm: deps.llm,
onWarn,
});
}
const seen = [...batchIds, ...(stats?.addedIds ?? [])];
const unfedTail = pending.slice(maxNotes).map((h) => h.id);
const highWater = cappedHighWater(seen, [...(stats?.failedIds ?? []), ...unfedTail]);
if (highWater !== undefined && highWater !== cursor && (cursor === undefined || highWater > cursor)) {
await store.setConsolidationCursor(scope, highWater);
}
return stats ?? { tokens: 0, costMicroUsd: 0, applied: 0, addedIds: [], consolidatedIds: [], failedIds: [] };
}
finally {
release();
}
}
export async function advanceCursorAfterInline(store, scope, seenNoteIds, addedIds, failedIds = [], onWarn) {
if (!supportsPeriodicConsolidation(store))
return;
try {
const cursor = await store.getConsolidationCursor(scope);
const seen = [
...seenNoteIds.filter((id) => typeof id === "string" && Boolean(id)),
...addedIds,
];
const capped = cappedHighWater(seen, failedIds);
const highWater = cursor === undefined ? capped : maxMarker(cursor, capped ?? cursor);
if (highWater !== undefined && highWater !== cursor) {
await store.setConsolidationCursor(scope, highWater);
}
}
catch (err) {
onWarn?.(err);
}
}