UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

99 lines (98 loc) • 4.31 kB
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); } }