UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

666 lines (665 loc) • 32.8 kB
import { CROSS_SESSION_MESSAGE_TAG } from "../agents/cross-session-envelope.js"; import { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, CompactionError, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, formatFileOperations, formatPersistedOutputRefs, prepareCompaction, shouldCompact, uuidv7, } from "../internal/harness.js"; import { fileArgPath } from "./tool-registry.js"; import { contextEditFrontier, resolveTriggerWindow } from "./context-edit.js"; import { selectCompactionEpoch } from "../prompt-assembly/epoch.js"; import { brainToRuntime } from "./runtime.js"; import { defuseFenceMarkers, sanitizeUntrustedText } from "./untrusted-text.js"; import { createSafeNotifier, observeThenableRejection } from "./safe-notify.js"; export const DEFAULT_COMPACTION_INSTRUCTIONS = "Preserve verbatim every code, identifier, name, number, file path, error message, credential " + "reference, and explicit user instruction or decision mentioned anywhere in the conversation. " + "When in doubt, keep the specific detail rather than generalizing it."; const FALLBACK_RESERVE_FRACTION = 0.15; export const COMPACTION_INSTRUCTIONS_MAX_CHARS = 2048; export function sanitizeCompactionInstructions(text) { const defused = defuseFenceMarkers(sanitizeUntrustedText(text)); const cps = [...defused]; return cps.length > COMPACTION_INSTRUCTIONS_MAX_CHARS ? cps.slice(0, COMPACTION_INSTRUCTIONS_MAX_CHARS).join("") : defused; } const WORKING_FILE_BREAKOUT_TAGS = ["working-file", "summary"]; function escapeWorkingFilePathAttr(path) { return path.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/[\r\n]+/g, " "); } const COMPACTION_MANUAL_CANCEL_FLAG = "semaCompactionManualCancel"; export function isCompactionManualCancel(err) { return (typeof err === "object" && err !== null && err[COMPACTION_MANUAL_CANCEL_FLAG] === true); } function summarizationCharsCapacity(model) { return model.contextWindow > 0 ? model.contextWindow * (model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN) : Number.POSITIVE_INFINITY; } export const STALE_ANCHOR_STRUCTURAL_MARGIN = 2; export function sanitizeCompactionSettings(settings, contextWindow) { const wide = settings; const reserveInvalid = !Number.isFinite(wide.reserveTokens); const keepRecentInvalid = !Number.isFinite(wide.keepRecentTokens); const clampToleranceInvalid = wide.clampTolerance !== undefined && !Number.isFinite(wide.clampTolerance); const summaryBudgetInvalid = wide.summaryOutputBudgetTokens !== undefined && !Number.isFinite(wide.summaryOutputBudgetTokens); const base = reserveInvalid || keepRecentInvalid || clampToleranceInvalid || summaryBudgetInvalid ? { ...settings, ...(reserveInvalid ? { reserveTokens: DEFAULT_COMPACTION_SETTINGS.reserveTokens } : {}), ...(keepRecentInvalid ? { keepRecentTokens: DEFAULT_COMPACTION_SETTINGS.keepRecentTokens } : {}), ...(clampToleranceInvalid ? { clampTolerance: DEFAULT_CLAMP_TOLERANCE } : {}), ...(summaryBudgetInvalid ? { summaryOutputBudgetTokens: undefined } : {}), } : settings; if (!contextWindow || !Number.isFinite(contextWindow) || contextWindow <= 0) { return base; } let reserveTokens = base.reserveTokens; if (contextWindow - reserveTokens <= 0) { reserveTokens = Math.ceil(contextWindow * FALLBACK_RESERVE_FRACTION); } const editAt = contextEditFrontier(contextWindow); if (contextWindow - reserveTokens > editAt) { reserveTokens = contextWindow - editAt; } const threshold = contextWindow - reserveTokens; let keepRecentTokens = base.keepRecentTokens; if (keepRecentTokens >= threshold) { keepRecentTokens = Math.max(1, Math.floor(threshold / 2)); } if (reserveTokens === base.reserveTokens && keepRecentTokens === base.keepRecentTokens) { return base; } return { ...base, reserveTokens, keepRecentTokens }; } export async function maybeCompact(opts) { const compactStartAt = Date.now(); const rawSettings = opts.settings ?? DEFAULT_COMPACTION_SETTINGS; if (!rawSettings.enabled) { return { compacted: false, disabled: true }; } const ctx = await opts.session.buildContext(); const cpt = opts.model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN; const est = estimateContextTokens(ctx.messages, cpt); const overhead = opts.overheadTokens ?? 0; const structuralTokens = ctx.messages.reduce((sum, m) => sum + estimateTokens(m, cpt), 0); const anchorStale = est.usageTokens > 0 && est.tokens > structuralTokens * STALE_ANCHOR_STRUCTURAL_MARGIN + overhead; const tokens = anchorStale || est.usageTokens === 0 ? structuralTokens + overhead : est.tokens; const window = resolveTriggerWindow(opts.model, opts.contextOverlay).window; const windowKnown = Number.isFinite(window) && window > 0; const windowField = (opts.contextOverlay?.autoCompactTokens ?? opts.model.autoCompactTokens) != null ? "autoCompactTokens" : (opts.contextOverlay?.contextTokens ?? opts.model.contextTokens) != null ? "contextTokens" : "contextWindow"; const settings = sanitizeCompactionSettings(rawSettings, window); const contextUsage = windowKnown ? { usedTokens: tokens, windowTokens: window, compactAtTokens: window - settings.reserveTokens } : undefined; const wantsCompact = shouldCompact(tokens, window, settings); const naturalTrigger = wantsCompact && tokens >= (opts.minTokens ?? 0); const force = opts.force || (opts.forceUnderThreshold === true && !naturalTrigger); if (!force && !naturalTrigger) { if (wantsCompact) { return { contextUsage, compacted: false, suppressedByFloor: { estTokens: tokens, floor: opts.minTokens ?? 0 } }; } return { contextUsage, compacted: false, ...(windowKnown ? {} : { unevaluableWindow: { estTokens: tokens, windowField, ...(typeof window === "number" ? { windowValue: window } : {}), ...(opts.model.id !== undefined ? { modelId: opts.model.id } : {}), }, }), }; } const branch = await opts.session.getBranch(); const prep = prepareCompaction(branch, settings, cpt, window, opts.contextInstructionFiles); if (!prep.ok) { throw prep.error; } if (!prep.value) { return { contextUsage, compacted: false, noop: true }; } const trigger = opts.trigger ?? "auto"; const notifier = createSafeNotifier(opts.onNotifyError !== undefined ? { onError: opts.onNotifyError } : undefined); let hookInstructions; if (opts.preCompact) { let pre; try { pre = (await opts.preCompact({ trigger, ...(opts.customInstructions !== undefined ? { customInstructions: opts.customInstructions } : {}), })) ?? undefined; } catch { } if (pre?.block !== undefined && pre.block !== "") { if (trigger !== "forced") { return { contextUsage, compacted: false, blocked: true }; } } if (pre?.additionalInstructions) { hookInstructions = pre.additionalInstructions; } } const specInstructions = opts.customInstructions !== undefined ? sanitizeCompactionInstructions(opts.customInstructions) : undefined; const hookSanitized = hookInstructions !== undefined ? sanitizeCompactionInstructions(hookInstructions) : undefined; const effectiveInstructions = hookSanitized ? specInstructions ? `${specInstructions}\n\n${hookSanitized}` : hookSanitized : specInstructions; const signalAtCommit = opts.signal; if (signalAtCommit?.aborted === true) { throw Object.assign(new CompactionError("aborted", "compaction cancelled before the pass committed — nothing was summarized or persisted"), opts.manualCancelSignal?.aborted === true ? { [COMPACTION_MANUAL_CANCEL_FLAG]: true } : {}); } const pass = uuidv7(); notifier.notify(() => observeThenableRejection(opts.onStarted?.({ pass, trigger, startedAt: Date.now() }), notifier, "compaction.onStarted"), "compaction.onStarted"); const maxReuse = opts.maxConsecutiveProviderReuse ?? 3; const forceRealSummary = maxReuse > 0 && (opts.consecutiveProviderReuse ?? 0) >= maxReuse; let providerSummary; if (opts.summaryProvider && !forceRealSummary && !prep.value.isSplitTurn) { const providerModifiedFiles = computeFileLists(prep.value.fileOps).modifiedFilesByRecency; const signal = opts.signal; const cut = Symbol("cut"); let release = () => undefined; const cutByAbort = new Promise((resolve) => { if (signal === undefined || signal.aborted) return; const onAbort = () => resolve(cut); signal.addEventListener("abort", onAbort, { once: true }); release = () => signal.removeEventListener("abort", onAbort); }); let consulted; try { consulted = signal?.aborted === true ? cut : await Promise.race([ Promise.resolve(opts.summaryProvider({ messagesToSummarize: prep.value.messagesToSummarize, previousSummary: prep.value.previousSummary, modifiedFiles: providerModifiedFiles, })), cutByAbort, ]); const trimmed = typeof consulted === "string" ? consulted.trim() : ""; if (trimmed) { providerSummary = trimmed; } } catch { } finally { release(); } if (consulted === cut) { throw Object.assign(new CompactionError("aborted", "compaction cancelled while the summary provider was answering — nothing was persisted"), opts.manualCancelSignal?.aborted === true ? { [COMPACTION_MANUAL_CANCEL_FLAG]: true } : {}); } } let summary; let firstKeptEntryId; let summaryTokensBefore; let details; let modelFallback = false; let clampedRatio; let clampReason; let summaryStartAt; let summaryEndAt; let ptlRetries = 0; const reused = providerSummary !== undefined; if (providerSummary !== undefined) { summary = providerSummary; firstKeptEntryId = prep.value.firstKeptEntryId; summaryTokensBefore = prep.value.tokensBefore; const providerFileLists = computeFileLists(prep.value.fileOps); details = providerFileLists; summary += formatFileOperations(providerFileLists.readFiles, providerFileLists.modifiedFiles); if (prep.value.invokedSkills.length > 0) { details.invokedSkills = prep.value.invokedSkills; } const reusedRefs = prep.value.persistedOutputRefs ?? []; if (reusedRefs.length > 0) { details.persistedOutputRefs = reusedRefs; summary += formatPersistedOutputRefs(reusedRefs); } if (prep.value.elidedMessages !== undefined && prep.value.elidedMessages > 0) { details.elidedMessages = prep.value.elidedMessages; } if (prep.value.carriedUnsummarizedMessages !== undefined && prep.value.carriedUnsummarizedMessages > 0) { details.unsummarizedMessages = prep.value.carriedUnsummarizedMessages; } } else { let summaryModel = opts.compactionModel ?? opts.model; const wantsWindowSafety = opts.compactionModel !== undefined && summarizationCharsCapacity(opts.compactionModel) < summarizationCharsCapacity(opts.model); let dry; if (wantsWindowSafety) { dry = dryRunSummarizationClamp(prep.value, summaryModel, effectiveInstructions); } const mainSection = dry !== undefined && prep.value.contextInstructionFiles !== undefined ? dryRunSummarizationClamp(prep.value, opts.model, effectiveInstructions).instructionFilesChars : 0; const contentTokens = dry !== undefined ? Math.ceil((dry.conversationChars + mainSection + (prep.value.previousSummary?.length ?? 0)) / cpt) : 0; const observeWindowSafety = (info) => { notifier.notify(() => opts.onWindowSafety?.(info), "compaction.onWindowSafety"); }; if (wantsWindowSafety && dry !== undefined) { const tolerance = settings.clampTolerance ?? DEFAULT_CLAMP_TOLERANCE; const smallCpt = summaryModel.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN; const headroomTokens = Math.ceil(dry.keptChars / smallCpt); if (dry.truncationRatio > tolerance) { let gate; let estCostMicroUsd; const bg = opts.fallbackBudget; if (bg?.mainInputPer1M !== undefined) { estCostMicroUsd = Math.round(contentTokens * bg.mainInputPer1M); if (bg.spentMicroUsd() + estCostMicroUsd > bg.capMicroUsd) { gate = "budget"; } } if (gate === undefined) { summaryModel = opts.model; modelFallback = true; observeWindowSafety({ kind: "fallback", truncationRatio: dry.truncationRatio, contentTokens, headroomTokens, ...(estCostMicroUsd !== undefined ? { estCostMicroUsd } : {}), }); } else { clampedRatio = dry.truncationRatio; clampReason = gate; observeWindowSafety({ kind: "clamp_disclosure", reason: gate, truncationRatio: dry.truncationRatio, contentTokens, headroomTokens, ...(estCostMicroUsd !== undefined ? { estCostMicroUsd } : {}), }); } } else if (dry.truncationRatio > 0) { clampedRatio = dry.truncationRatio; clampReason = "tolerance"; observeWindowSafety({ kind: "clamp_disclosure", reason: "tolerance", truncationRatio: dry.truncationRatio, contentTokens, headroomTokens, }); } } const auth = await opts.getApiKeyAndHeaders?.(summaryModel); const summarySignal = opts.signal; const runtime = brainToRuntime(opts.brain); const forkCandidate = opts.compactionModel === undefined ? opts.forkContext?.() : undefined; const forkContext = forkCandidate !== undefined && (forkCandidate.modelId === undefined || forkCandidate.modelId === summaryModel.id) ? forkCandidate : undefined; let res; summaryStartAt = Date.now(); try { res = await compact(prep.value, summaryModel, auth?.apiKey, auth?.headers, effectiveInstructions, summarySignal, opts.thinking, undefined, runtime, undefined, opts.onInputTruncated, () => { ptlRetries += 1; }, forkContext); } finally { summaryEndAt = Date.now(); } if (!res.ok) { if (opts.manualCancelSignal?.aborted === true && res.error instanceof CompactionError && res.error.code === "aborted") { res.error[COMPACTION_MANUAL_CANCEL_FLAG] = true; } throw res.error; } summary = (res.value.summary ?? "").replace(/<analysis>[\s\S]*?<\/analysis>\s*/gi, "").trim(); if (!summary) { throw new Error("compaction produced an empty summary (reasoning may have consumed the output budget; history left untouched)"); } if (Math.ceil(summary.length / cpt) > res.value.tokensBefore * 1.5) { throw new Error(`compaction produced an oversized summary (≈${Math.ceil(summary.length / cpt)} tokens vs ` + `${res.value.tokensBefore} before compaction; history left untouched)`); } firstKeptEntryId = res.value.firstKeptEntryId; summaryTokensBefore = res.value.tokensBefore; details = res.value.details; } const persistStartAt = summaryEndAt ?? Date.now(); let summaryWithAttachments = summary; const attachedFiles = []; const attachedComplete = []; const att = opts.workingFileAttachments; const recentlyRead = att?.recentlyReadFiles?.() ?? []; const rawCandidateFiles = recentlyRead.length > 0 ? recentlyRead : (details?.modifiedFilesByRecency ?? details?.modifiedFiles); const maxFilesForSelection = Math.max(1, att?.maxFiles ?? 3); const excludedReadStatePreserveKeys = []; const candidateFiles = att !== undefined && Array.isArray(rawCandidateFiles) && rawCandidateFiles.length > 0 ? await (async () => { const keptTailReadPathsRaw = new Set(); const firstKeptIdx = branch.findIndex((e) => e.id === firstKeptEntryId); if (firstKeptIdx >= 0) { const dedupStub = att?.isDedupStubResult; for (let i = firstKeptIdx; i < branch.length; i++) { const entry = branch[i]; if (entry.type !== "message" || entry.message.role !== "assistant") continue; const msg = entry.message; if (!("content" in msg) || !Array.isArray(msg.content)) continue; const pendingReads = new Map(); for (const block of msg.content) { if (typeof block !== "object" || block === null) continue; if (!("type" in block) || block.type !== "toolCall") continue; const tc = block; if (typeof tc.name !== "string" || typeof tc.id !== "string" || !tc.arguments) continue; if (tc.name !== "read" && tc.name !== "Read" && tc.name !== "read_file") continue; const path = fileArgPath(tc.arguments); if (path) pendingReads.set(tc.id, path); } if (pendingReads.size === 0) continue; for (let j = i + 1; j < branch.length && pendingReads.size > 0; j++) { const next = branch[j]; if (next.type !== "message") continue; if (next.message.role === "assistant") break; if (next.message.role !== "toolResult") continue; const tr = next.message; const path = pendingReads.get(tr.toolCallId); if (path === undefined) continue; pendingReads.delete(tr.toolCallId); if (tr.isError) continue; if (dedupStub) { const text = tr.content.map((c) => ("text" in c ? c.text : "")).join(""); if (dedupStub(text)) continue; } keptTailReadPathsRaw.add(path); } } } const instructionPathsRaw = new Set(att?.instructionSourcePaths ?? []); if (keptTailReadPathsRaw.size === 0 && instructionPathsRaw.size === 0) return rawCandidateFiles; const normalize = att?.normalizePath; if (!normalize) { excludedReadStatePreserveKeys.push(...keptTailReadPathsRaw, ...instructionPathsRaw); return rawCandidateFiles.filter((p) => !keptTailReadPathsRaw.has(p) && !instructionPathsRaw.has(p)); } const normalizeSafe = async (p) => { try { return await normalize(p); } catch { return p; } }; const normalizeCap = Math.max(maxFilesForSelection * 4, 12); const boundedCandidates = rawCandidateFiles.slice(0, normalizeCap); const overflowCandidates = rawCandidateFiles.slice(normalizeCap); const [normBounded, normKeptTail, normInstruction] = await Promise.all([ Promise.all(boundedCandidates.map(normalizeSafe)), Promise.all([...keptTailReadPathsRaw].map(normalizeSafe)), Promise.all([...instructionPathsRaw].map(normalizeSafe)), ]); excludedReadStatePreserveKeys.push(...normKeptTail, ...normInstruction); const excluded = new Set([...normKeptTail, ...normInstruction]); const filteredBounded = boundedCandidates.filter((_, i) => !excluded.has(normBounded[i])); return [...filteredBounded, ...overflowCandidates]; })() : rawCandidateFiles; if (att && Array.isArray(candidateFiles) && candidateFiles.length > 0) { const maxFiles = maxFilesForSelection; const perFileCap = Math.max(200, att.maxCharsPerFile ?? 16_000); const windowShareChars = windowKnown ? Math.floor(window * 0.15) * cpt : Number.POSITIVE_INFINITY; let remaining = Math.min(maxFiles * perFileCap, windowShareChars); const blocks = []; for (const path of candidateFiles.slice(0, maxFiles)) { if (remaining <= 0) break; let content = null; let withheldPattern; try { const r = await att.readFile(path); if (typeof r === "string") content = r; else if (r !== null && typeof r === "object") withheldPattern = r.withheld.pattern; } catch { content = null; } if (withheldPattern !== undefined) { blocks.push(`<working-file path="${escapeWorkingFilePathAttr(path)}" withheld="content withheld: matches the sensitive-path read deny list (pattern ${escapeWorkingFilePathAttr(withheldPattern)})"/>`); continue; } if (content === null) continue; const cap = Math.min(perFileCap, remaining); const wasClipped = content.length > cap; const clipped = wasClipped ? `${content.slice(0, cap)}\n[... ${content.length - cap} more characters truncated — read the file for the rest]` : content; remaining -= clipped.length; blocks.push(`<working-file path="${escapeWorkingFilePathAttr(path)}">\n${sanitizeUntrustedText(clipped, WORKING_FILE_BREAKOUT_TAGS)}\n</working-file>`); attachedFiles.push({ path, chars: Math.min(content.length, cap), truncated: wasClipped }); if (!wasClipped) attachedComplete.push({ path, content }); } if (blocks.length > 0) { summaryWithAttachments += "\n\nCurrent on-disk contents of files touched in the summarized work (re-read at compaction time — trust these over any excerpts in the summary above):\n" + blocks.join("\n"); } } let preChars = 0; for (const m of ctx.messages) preChars += estimateTokens(m, cpt); const ca = opts.centerAdoption; const stored = await opts.session.getPromptEpoch?.(); const current = stored?.kind === "pin" ? stored.descriptor : undefined; const identityNow = ca?.promptIdentity ?? opts.promptIdentity ?? (current !== undefined ? { layers: current.layers, ...(current.sourceRevision !== undefined ? { sourceRevision: current.sourceRevision } : {}) } : undefined); const restatedEpoch = identityNow !== undefined ? selectCompactionEpoch(current, identityNow.layers, identityNow.sourceRevision) : undefined; const restatedListings = await opts.session.getAnnouncedListing().catch(() => undefined); if (opts.signal?.aborted === true) { throw Object.assign(new Error("compaction cancelled before commit — the abort signal fired during the summary call; nothing was persisted"), opts.manualCancelSignal?.aborted === true ? { [COMPACTION_MANUAL_CANCEL_FLAG]: true } : {}); } await opts.session.appendCompaction(summaryWithAttachments, firstKeptEntryId, summaryTokensBefore, { ...(details ?? {}), ...(restatedEpoch !== undefined ? { promptEpoch: restatedEpoch } : {}), ...(restatedListings !== undefined ? { announcedListings: restatedListings } : {}), ...(opts.activeTools !== undefined && opts.activeTools.length > 0 ? { activeTools: [...opts.activeTools] } : {}), ...(opts.gitRestate !== undefined ? { gitAnnouncement: { kind: opts.gitRestate.pending.kind, hash: opts.gitRestate.pending.hash, pending: true } } : {}), }, false); if (opts.onCommitted !== undefined) { const firstKeptIdx = branch.findIndex((e) => e.id === firstKeptEntryId); const effect = firstKeptIdx >= 0 ? { summarizedEntryIds: new Set(branch.slice(0, firstKeptIdx).map((e) => e.id)) } : { summarizedEntryIds: "unknown" }; try { opts.onCommitted(effect); } catch { } } if (opts.gitRestate !== undefined) { try { await opts.gitRestate.land(); } catch { } } if (opts.postureRestate !== undefined) { try { await opts.postureRestate(); } catch { } } try { opts.onApplied?.(attachedComplete, excludedReadStatePreserveKeys); } catch { } if (ca !== undefined && restatedEpoch !== undefined) { try { ca.apply(restatedEpoch.identity); } catch { } } let freedTokens; let postTriggerTokens; try { const post = (await opts.session.buildContext()).messages; let postChars = 0; for (const m of post) postChars += estimateTokens(m, cpt); freedTokens = Math.max(0, preChars - postChars); postTriggerTokens = postChars + (ca !== undefined ? ca.overheadTokensAfter : (opts.overheadTokens ?? 0)); } catch { } const messagesCompacted = prep.value.messagesToSummarize.length; notifier.notify(() => opts.onCompaction?.({ messagesCompacted, freedTokens, tokensBefore: summaryTokensBefore, reused, }), "compaction.onCompaction"); if (opts.postCompact) { await notifier.notifyAsync(() => opts.postCompact?.({ trigger, summary, tokensBefore: summaryTokensBefore, ...(freedTokens !== undefined ? { freedTokens } : {}), ...(postTriggerTokens !== undefined ? { tokensAfter: postTriggerTokens } : {}), }), "compaction.postCompact"); } const endAt = Date.now(); const summarizedCrossSessionMessages = countSummarizedCrossSessionMessages(branch, firstKeptEntryId); return { contextUsage, compacted: true, pass, tokensBefore: summaryTokensBefore, freedTokens, triggerTokens: tokens, postTriggerTokens, firstKeptEntryId, ...(summarizedCrossSessionMessages > 0 ? { summarizedCrossSessionMessages } : {}), reused, naturalTrigger, durationMs: endAt - compactStartAt, phaseDurations: { prepareMs: (summaryStartAt ?? persistStartAt) - compactStartAt, ...(summaryStartAt !== undefined && summaryEndAt !== undefined ? { summaryMs: summaryEndAt - summaryStartAt } : {}), persistMs: endAt - persistStartAt, ptlRetries, }, ...(postTriggerTokens !== undefined ? { postOverThreshold: shouldCompact(postTriggerTokens, window, settings) } : {}), ...(attachedFiles.length > 0 ? { attachedFiles } : {}), ...(modelFallback ? { modelFallback: true, fallbackReason: "window" } : {}), ...(clampedRatio !== undefined ? { clampedRatio } : {}), ...(clampReason !== undefined ? { clampReason } : {}), }; } export function mintCompactedEvent(comp, trigger, identity) { return { type: "compacted", trigger, ...(comp.pass !== undefined ? { pass: comp.pass } : {}), tokensBefore: comp.tokensBefore ?? 0, ...(comp.freedTokens !== undefined ? { freedTokens: comp.freedTokens } : {}), ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}), ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}), ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}), ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}), ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}), ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}), ...(comp.modelFallback ? { modelFallback: true } : {}), ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}), ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}), ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}), ...(comp.summarizedCrossSessionMessages !== undefined ? { summarizedCrossSessionMessages: comp.summarizedCrossSessionMessages } : {}), ...identity, }; } export function mintCompactionStartedEvent(start, identity) { return { type: "compaction_started", trigger: start.trigger, pass: start.pass, startedAt: start.startedAt, ...identity }; } export function compactionPassReport(sink, ident) { let pass; return { onStarted: (start) => { sink.push(mintCompactionStartedEvent(start, ident())); pass = start.pass; }, get pass() { return pass; }, get end() { return pass !== undefined ? { pass } : {}; }, }; } export function countSummarizedCrossSessionMessages(branch, firstKeptEntryId) { const firstKeptIdx = branch.findIndex((e) => e.id === firstKeptEntryId); const summarized = firstKeptIdx >= 0 ? branch.slice(0, firstKeptIdx) : branch; let n = 0; for (const e of summarized) { if (e.type !== "message" || e.message === undefined || e.message.role !== "user" || e.message.provenance !== "engine-note") continue; const c = e.message.content; const text = typeof c === "string" ? c : Array.isArray(c) ? c.map((b) => (b !== null && typeof b === "object" && b.type === "text" ? String(b.text ?? "") : "")).join("") : ""; if (text.startsWith(`<${CROSS_SESSION_MESSAGE_TAG} `) || text.startsWith(`<${CROSS_SESSION_MESSAGE_TAG}>`)) n += 1; } return n; } export function nextTrimForceBackoff(prev, comp, trimForced = false) { if (trimForced && comp.naturalTrigger !== true && comp.compacted && comp.freedTokens !== undefined && comp.freedTokens <= 0) { return true; } if (!comp.compacted || comp.postOverThreshold === undefined) { return prev; } return comp.postOverThreshold; } export function createRapidRefillState() { return { lastCompactionTurn: Number.NEGATIVE_INFINITY, count: 0 }; } export function recordCompactionAndCheckRapidRefill(state, turn, minGapTurns = 3, tripAt = 3) { if (turn - state.lastCompactionTurn < minGapTurns) { state.count += 1; } else { state.count = 0; } state.lastCompactionTurn = turn; return state.count >= tripAt; }