blob: c83d123c8986fdab91ee634f3b81d43935ee59ef [file]
import {
estimateTokens,
estimateRuntimeEventsTokens,
stableJsonLength,
turnKey,
groupEventsByTurn,
finitePositive,
} from './context-budget-helpers.js';
// Public re-export surface for @maka/runtime consumers. Explicit list keeps
// the ./context-budget subpath from leaking leaf-internal collaboration symbols.
export { estimateRuntimeEventsTokens, estimateTokens } from './context-budget-helpers.js';
export {
ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
isArchivedToolResultPlaceholder,
deserializeToolResultArchive,
retrieveArchivedToolResultsForReplay,
serializeToolResultForArchive,
} from './tool-result-archive.js';
export type {
ArchiveRetrievalMode,
ArchiveRetrievalPolicy,
ArchiveRetrievalResult,
StaleToolResultPrunePolicy,
StaleToolResultArchiveCandidate,
ToolResultArchiveReader,
ToolResultArchiveReaderInput,
ToolResultArchiveReadFailureReason,
ToolResultArchiveReadResult,
ToolResultArchiveRef,
ArchivedToolResultPlaceholder,
} from './tool-result-archive.js';
export type { ArchivedToolResultReason, SynthesisSourceRef } from './context-source-ref.js';
export {
retrieveRuntimeEventHistoryAround,
searchRuntimeEventHistory,
} from './runtime-event-history-search.js';
export type {
RuntimeEventHistoryAroundResult,
RuntimeEventHistorySearchHit,
RuntimeEventHistorySearchPolicy,
} from './runtime-event-history-search.js';
export {
buildSynthesisCacheBlocksFromHydratedArchives,
deriveSynthesisCoverageFromSourceRefs,
rawEvidenceRequestReason,
stableSynthesisBlockId,
validateSynthesisCacheBlockShape,
} from './synthesis-cache.js';
export type {
SynthesisCacheBlock,
SynthesisCacheCoverage,
SynthesisCachePolicy,
} from './synthesis-cache.js';
export {
buildHistoryCompactBlockFromSummary,
historyCompactBlockToRuntimeEvent,
renderHistoryCompactBlock,
validateHistoryCompactBlockShape,
} from './history-compact.js';
export type {
HistoryCompactBlock,
HistoryCompactCoverage,
HistoryCompactMidTurnPolicy,
HistoryCompactPolicy,
HistoryCompactReplayResult,
HistoryCompactSourceArchiveRef,
HistoryRewriteGatePolicy,
} from './history-compact.js';
export { ACTIVE_ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND } from './active-tool-result-prune.js';
export type { ActiveArchivedToolResultPlaceholder } from './active-tool-result-prune.js';
import { SynthesisCachePolicy } from './synthesis-cache.js';
import {
searchRuntimeEventHistory,
type RuntimeEventHistoryAroundResult,
type RuntimeEventHistorySearchPolicy,
} from './runtime-event-history-search.js';
import {
ArchiveRetrievalPolicy,
collectStaleToolResultArchiveCandidates as collectStaleToolResultArchiveCandidatesNarrow,
pruneStaleToolResultsBeforeCompact,
type StaleToolResultPrunePolicy,
type StaleToolResultArchiveCandidate,
} from './tool-result-archive.js';
import { isValidSynthesisSourceRef, type SynthesisSourceRef } from './context-source-ref.js';
import { type ActiveToolResultPrunePolicy } from './active-tool-result-prune.js';
import {
applyRuntimeEventHistoryCompact as applyRuntimeEventHistoryCompactNarrow,
evaluateHistoryCompactCheckpointReplay as evaluateHistoryCompactCheckpointReplayNarrow,
isHistoryCompactContentEvent,
type HistoryCompactBlock,
type HistoryCompactPolicy,
type HistoryCompactReplayOptions,
type HistoryCompactReplayResult,
type HistoryCompactCheckpointReplayFit,
type HistoryRewriteGatePolicy,
} from './history-compact.js';
import type { ModelMessage } from './model-protocol.js';
import type { RuntimeEvent } from '@maka/core';
import type {
CompactionDecisionDiagnostic,
ContextBudgetDiagnostic,
PromptSegmentEstimate,
} from '@maka/core';
import {
compactionDecisionDiagnosticPatch,
historyCompactBlockToCompactionBoundary,
} from './compaction-boundary.js';
import type { ActiveFullCompactPolicy } from './active-full-compact.js';
import type { SemanticCompactPolicy } from './semantic-compact.js';
import {
historyCompactCheckpointToRuntimeEvent,
matchHistoryCompactCheckpointPrefix,
midTurnHeadAnchorEvent,
renderHistoryCompactCheckpoint,
type HistoryCompactCheckpoint,
} from './history-compact-checkpoint.js';
export interface ContextBudgetPolicy {
name?: string;
/**
* Approximate max model-visible prior-history tokens. This is an estimate
* used for shaping, not provider billing.
*/
maxHistoryEstimatedTokens?: number;
/** Hard cap on prior turns retained for model replay. */
maxHistoryTurns?: number;
/** Keep at least this many recent turns even if the token estimate exceeds the cap. */
minRecentTurns?: number;
/** Estimate conversion. Defaults to 4 chars/token, intentionally conservative for mixed text. */
charsPerToken?: number;
/** Optional replay-only pruning for stale oversized tool results before whole-turn compaction. */
staleToolResultPrune?: StaleToolResultPrunePolicy;
/**
* Optional current-turn, provider-visible tool-result pruning before the next
* AI SDK step. Defaults off and does not mutate persisted session messages.
*/
activeToolResultPrune?: ActiveToolResultPrunePolicy;
/**
* Optional active-loop full compact replacement. When enabled, request projection can
* replace a validated older provider-message span with a source-bearing block.
*/
activeFullCompact?: ActiveFullCompactPolicy;
/**
* Optional current-turn LLM semantic compact replacement. Runs after active
* tool-result pruning and before the next provider step.
*/
semanticCompact?: SemanticCompactPolicy;
/** Optional replay-only archive hydration after pruning. Defaults off. */
archiveRetrieval?: ArchiveRetrievalPolicy;
/** Optional deterministic prior-history search used to re-add bounded around-context. Defaults off. */
historySearch?: RuntimeEventHistorySearchPolicy;
/** Optional replay-only source-bearing synthesis cache over older RuntimeEvent history. Defaults off. */
synthesisCache?: SynthesisCachePolicy;
/** Optional replay-only high-water compaction of older RuntimeEvent history into a source-bearing block. */
historyCompact?: HistoryCompactPolicy;
/** Named rewrite/compaction gate for diagnostics and explicit cache-shape resets. */
historyRewrite?: HistoryRewriteGatePolicy;
}
export interface BudgetedRuntimeContext {
events: RuntimeEvent[];
diagnostic: ContextBudgetDiagnostic;
historyCompactBlocks?: HistoryCompactBlock[];
}
export interface PromptSegmentInput {
systemPrompt?: string;
toolSchemaChars: number;
toolCount: number;
priorMessages: readonly ModelMessage[];
priorRuntimeEventCount?: number;
currentUserContent: string;
turnTailPrompt?: string;
charsPerToken?: number;
}
export function applyRuntimeEventContextBudget(
events: readonly RuntimeEvent[],
policy: ContextBudgetPolicy | undefined,
options: Pick<HistoryCompactReplayOptions, 'historyCompactProtocol'> = {},
): BudgetedRuntimeContext | undefined {
const prunePolicy = policy?.staleToolResultPrune;
const pruneEnabled = prunePolicy?.enabled === true;
const archiveRetrievalEnabled = policy?.archiveRetrieval?.enabled === true;
const historySearchEnabled = policy?.historySearch?.enabled === true;
const synthesisCacheEnabled = policy?.synthesisCache?.enabled === true;
const historyCompactEnabled = policy?.historyCompact?.enabled === true;
const historyRewriteEnabled = policy?.historyRewrite?.enabled === true;
const enabled = Boolean(
policy?.maxHistoryEstimatedTokens ||
policy?.maxHistoryTurns ||
pruneEnabled ||
archiveRetrievalEnabled ||
historySearchEnabled ||
synthesisCacheEnabled ||
historyCompactEnabled ||
historyRewriteEnabled,
);
if (!enabled) return undefined;
if (!policy) return undefined;
const charsPerToken = policy?.charsPerToken ?? 4;
const maxTokens = finitePositive(policy?.maxHistoryEstimatedTokens);
const maxTurns = finitePositive(policy?.maxHistoryTurns);
const minRecentTurns = Math.max(0, Math.floor(policy?.minRecentTurns ?? 1));
const estimatedTokensBefore = estimateRuntimeEventsTokens(events, charsPerToken);
const pruned = pruneStaleToolResultsBeforeCompact(
events,
policy?.staleToolResultPrune,
charsPerToken,
policy?.minRecentTurns,
);
const compacted = applyRuntimeEventHistoryCompactNarrow(
pruned.events,
policy?.historyCompact,
policy?.charsPerToken,
policy?.maxHistoryEstimatedTokens,
{
charsPerToken,
maxHistoryEstimatedTokens: maxTokens,
...(options.historyCompactProtocol
? { historyCompactProtocol: options.historyCompactProtocol }
: {}),
},
);
const hasCompactedReplay = compacted.blocks.length > 0 || compacted.checkpoint !== undefined;
const budgetEvents = hasCompactedReplay ? compacted.events : pruned.events;
const turnGroups = groupEventsByTurn(
budgetEvents.filter(isHistoryCompactContentEvent),
charsPerToken,
);
const keptTurnIds = new Set<string>();
let keptEvents: RuntimeEvent[];
if (hasCompactedReplay) {
keptEvents = budgetEvents;
for (const event of keptEvents) keptTurnIds.add(turnKey(event));
} else {
let keptTokens = 0;
for (let index = turnGroups.length - 1; index >= 0; index -= 1) {
const group = turnGroups[index]!;
const nextTurnCount = keptTurnIds.size + 1;
const mustKeep = nextTurnCount <= minRecentTurns;
const wouldExceedTurns = maxTurns !== undefined && nextTurnCount > maxTurns;
const wouldExceedTokens =
maxTokens !== undefined && keptTokens > 0 && keptTokens + group.estimatedTokens > maxTokens;
if (!mustKeep && (wouldExceedTurns || wouldExceedTokens)) break;
keptTurnIds.add(group.turnId);
keptTokens += group.estimatedTokens;
}
keptEvents = budgetEvents.filter((event) => keptTurnIds.has(turnKey(event)));
}
const diagnostic: ContextBudgetDiagnostic = {
enabled: true,
...(policy?.name ? { policyName: policy.name } : {}),
...(maxTokens !== undefined ? { maxHistoryEstimatedTokens: maxTokens } : {}),
...(maxTurns !== undefined ? { maxHistoryTurns: maxTurns } : {}),
estimatedTokensBefore,
estimatedTokensAfter: estimateRuntimeEventsTokens(keptEvents, charsPerToken),
keptTurns: keptTurnIds.size,
droppedTurns: hasCompactedReplay
? compacted.blocks.reduce((total, block) => total + block.coverage.turnIds.length, 0) +
(compacted.checkpoint?.coverage.turnCount ?? 0)
: Math.max(0, turnGroups.length - keptTurnIds.size),
keptEvents: keptEvents.length,
droppedEvents: Math.max(
0,
(hasCompactedReplay ? pruned.events.length : budgetEvents.length) - keptEvents.length,
),
...(policy.historyRewrite?.enabled === true
? {
historyRewriteVersion: policy.historyRewrite.historyRewriteVersion,
historyRewriteResetReason: policy.historyRewrite.resetReason,
historyRewriteGate: policy.historyRewrite.name ?? 'history-rewrite',
}
: {}),
...compacted.diagnosticPatch,
...(pruned.prunedToolResults > 0
? {
prunedToolResults: pruned.prunedToolResults,
prunedToolResultEstimatedTokensBefore: pruned.estimatedTokensBefore,
prunedToolResultEstimatedTokensAfter: pruned.estimatedTokensAfter,
archivePlaceholders: pruned.prunedToolResults,
archivePlaceholderReasonCounts: {
stale_tool_result_pruned_before_compact: pruned.prunedToolResults,
},
}
: {}),
...(pruned.archiveWriteFailures > 0
? {
archiveWriteFailures: pruned.archiveWriteFailures,
unarchivedToolResults: pruned.archiveWriteFailures,
}
: {}),
};
return {
events: keptEvents,
diagnostic,
...(compacted.blocks.length > 0 ? { historyCompactBlocks: compacted.blocks } : {}),
};
}
export function hasOversizedRetainedHistoryTurn(
events: readonly RuntimeEvent[],
policy: ContextBudgetPolicy | undefined,
): boolean {
const maxTokens = finitePositive(policy?.maxHistoryEstimatedTokens);
if (maxTokens === undefined) return false;
const charsPerToken = policy?.charsPerToken ?? 4;
return groupEventsByTurn(events.filter(isHistoryCompactContentEvent), charsPerToken).some(
(turn) => turn.estimatedTokens > maxTokens,
);
}
export function buildPromptSegmentEstimates(input: PromptSegmentInput): PromptSegmentEstimate[] {
const charsPerToken = input.charsPerToken ?? 4;
return [
segment('system_prompt', input.systemPrompt?.length ?? 0, charsPerToken),
{
...segment('tool_schema', input.toolSchemaChars, charsPerToken),
toolCount: input.toolCount,
},
{
...segment('prior_history', estimateModelMessagesChars(input.priorMessages), charsPerToken),
messageCount: input.priorMessages.length,
...(input.priorRuntimeEventCount !== undefined
? { eventCount: input.priorRuntimeEventCount }
: {}),
},
segment('current_user', input.currentUserContent.length, charsPerToken),
segment('turn_tail', input.turnTailPrompt?.length ?? 0, charsPerToken),
];
}
export function estimateModelMessagesChars(messages: readonly ModelMessage[]): number {
return messages.reduce((total, message) => total + estimateModelMessageChars(message), 0);
}
function estimateModelMessageChars(message: ModelMessage): number {
const raw = message as unknown as { content?: unknown };
return estimateContentChars(raw.content);
}
function estimateContentChars(content: unknown): number {
if (typeof content === 'string') return content.length;
if (Array.isArray(content)) {
return content.reduce((total, part) => total + estimatePartChars(part), 0);
}
return stableJsonLength(content);
}
function estimatePartChars(part: unknown): number {
if (!part || typeof part !== 'object') return stableJsonLength(part);
const value = part as Record<string, unknown>;
let total = 0;
for (const key of ['text', 'toolName', 'toolCallId'] as const) {
if (typeof value[key] === 'string') total += value[key].length;
}
for (const key of ['input', 'output'] as const) {
if (value[key] !== undefined) total += stableJsonLength(value[key]);
}
return total;
}
function segment(
kind: PromptSegmentEstimate['kind'],
chars: number,
charsPerToken: number,
): PromptSegmentEstimate {
return {
kind,
chars,
estimatedTokens: estimateTokens(chars, charsPerToken),
};
}
// finiteRatio / normalizeWhitespace / boundText relocated to context-budget-helpers.ts
// ============================================================================
// Replay ordering + context-budget diagnostic merge helpers.
// Relocated from ai-sdk-backend.ts: these are pure functions over
// RuntimeEvent / ContextBudgetDiagnostic and belong to this budgeting domain.
// ============================================================================
export function mergeRuntimeEventsInOriginalOrder(
original: readonly RuntimeEvent[],
current: readonly RuntimeEvent[],
extra: readonly RuntimeEvent[],
): RuntimeEvent[] {
const wantedIds = new Set<string>();
const byId = new Map<string, RuntimeEvent>();
for (const event of current) {
wantedIds.add(event.id);
byId.set(event.id, event);
}
for (const event of extra) {
wantedIds.add(event.id);
if (!byId.has(event.id)) byId.set(event.id, event);
}
const out: RuntimeEvent[] = [];
for (const event of original) {
if (!wantedIds.has(event.id)) continue;
out.push(byId.get(event.id) ?? event);
}
return out;
}
export function buildContextBudgetDiagnosticShell(
before: readonly RuntimeEvent[],
after: readonly RuntimeEvent[],
policy: ContextBudgetPolicy | undefined,
): ContextBudgetDiagnostic {
const charsPerToken = policy?.charsPerToken ?? 4;
const turnCountBefore = new Set(before.map((event) => runtimeEventTurnKey(event))).size;
const turnCountAfter = new Set(after.map((event) => runtimeEventTurnKey(event))).size;
return {
enabled: true,
...(policy?.name ? { policyName: policy.name } : {}),
...(policy?.maxHistoryEstimatedTokens !== undefined
? { maxHistoryEstimatedTokens: policy.maxHistoryEstimatedTokens }
: {}),
...(policy?.maxHistoryTurns !== undefined ? { maxHistoryTurns: policy.maxHistoryTurns } : {}),
estimatedTokensBefore: estimateRuntimeEventsTokens(before, charsPerToken),
estimatedTokensAfter: estimateRuntimeEventsTokens(after, charsPerToken),
keptTurns: turnCountAfter,
droppedTurns: Math.max(0, turnCountBefore - turnCountAfter),
keptEvents: after.length,
droppedEvents: Math.max(0, before.length - after.length),
...(policy?.historyRewrite?.enabled === true
? {
historyRewriteVersion: policy.historyRewrite.historyRewriteVersion,
historyRewriteResetReason: policy.historyRewrite.resetReason,
historyRewriteGate: policy.historyRewrite.name ?? 'history-rewrite',
}
: {}),
};
}
export function runtimeEventTurnKey(event: RuntimeEvent): string {
return event.turnId || '<unknown-turn>';
}
export function retrieveReplayHistoryAroundSearchSource(
replayEvents: readonly RuntimeEvent[],
searchEvents: readonly RuntimeEvent[],
query: string,
policy: RuntimeEventHistorySearchPolicy | undefined,
options: { charsPerToken?: number } = {},
): RuntimeEventHistoryAroundResult {
if (policy?.enabled !== true) {
return { events: [], hits: [], diagnosticPatch: {} };
}
const charsPerToken = options.charsPerToken ?? 4;
const around = Math.max(0, Math.floor(policy.around ?? 1));
const maxEstimatedTokens =
typeof policy.maxEstimatedTokens === 'number' &&
Number.isFinite(policy.maxEstimatedTokens) &&
policy.maxEstimatedTokens > 0
? Math.floor(policy.maxEstimatedTokens)
: 4_096;
const hits = searchRuntimeEventHistory(searchEvents, policy.query ?? query, policy);
const selectedIndexes = new Set<number>();
const indexesByEventId = new Map(replayEvents.map((event, index) => [event.id, index]));
let skipped = 0;
for (const hit of hits) {
const index = indexesByEventId.get(hit.eventId);
if (index === undefined) {
skipped += 1;
continue;
}
for (
let cursor = Math.max(0, index - around);
cursor <= Math.min(replayEvents.length - 1, index + around);
cursor += 1
) {
selectedIndexes.add(cursor);
}
}
const selectedEvents: RuntimeEvent[] = [];
let selectedTokens = 0;
for (const index of [...selectedIndexes].sort((a, b) => a - b)) {
const event = replayEvents[index]!;
const estimate = estimateRuntimeEventsTokens([event], charsPerToken);
if (selectedTokens + estimate > maxEstimatedTokens) {
skipped += 1;
continue;
}
selectedEvents.push(event);
selectedTokens += estimate;
}
return {
events: selectedEvents,
hits,
diagnosticPatch: {
historySearchMatches: hits.length,
historyAroundRetrievedEvents: selectedEvents.length,
historyAroundEstimatedTokens: selectedTokens,
...(skipped > 0 ? { historyAroundSkippedEvents: skipped } : {}),
},
};
}
export function buildHistorySearchSource(
events: readonly RuntimeEvent[],
policy: ContextBudgetPolicy | undefined,
): readonly RuntimeEvent[] {
if (policy?.staleToolResultPrune?.enabled !== true) return events;
return (
applyRuntimeEventContextBudget(events, {
...policy,
maxHistoryEstimatedTokens: undefined,
maxHistoryTurns: undefined,
archiveRetrieval: undefined,
historySearch: undefined,
historyRewrite: undefined,
})?.events ?? events
);
}
export function mergeContextBudgetDiagnostic(
base: ContextBudgetDiagnostic,
patch: Partial<ContextBudgetDiagnostic>,
): ContextBudgetDiagnostic {
return {
...base,
...patch,
...mergeCountRecordField(
'archiveRetrievalFailureReasonCounts',
base.archiveRetrievalFailureReasonCounts,
patch.archiveRetrievalFailureReasonCounts,
),
...mergeCountRecordField(
'archiveRetrievalSkippedReasonCounts',
base.archiveRetrievalSkippedReasonCounts,
patch.archiveRetrievalSkippedReasonCounts,
),
...mergeCountRecordField(
'synthesisCacheSkippedReasonCounts',
base.synthesisCacheSkippedReasonCounts,
patch.synthesisCacheSkippedReasonCounts,
),
...mergeCountRecordField(
'synthesisCacheInvalidationReasonCounts',
base.synthesisCacheInvalidationReasonCounts,
patch.synthesisCacheInvalidationReasonCounts,
),
...mergeCountRecordField(
'synthesisCacheLoadSkippedReasonCounts',
base.synthesisCacheLoadSkippedReasonCounts,
patch.synthesisCacheLoadSkippedReasonCounts,
),
...mergeCountRecordField(
'synthesisCacheWriteSkippedReasonCounts',
base.synthesisCacheWriteSkippedReasonCounts,
patch.synthesisCacheWriteSkippedReasonCounts,
),
...mergeCountRecordField(
'synthesisCacheEvictionReasonCounts',
base.synthesisCacheEvictionReasonCounts,
patch.synthesisCacheEvictionReasonCounts,
),
...mergeCountRecordField(
'historyCompactSkippedReasonCounts',
base.historyCompactSkippedReasonCounts,
patch.historyCompactSkippedReasonCounts,
),
...mergeCountRecordField(
'historyCompactLoadSkippedReasonCounts',
base.historyCompactLoadSkippedReasonCounts,
patch.historyCompactLoadSkippedReasonCounts,
),
...mergeCountRecordField(
'historyCompactWriteSkippedReasonCounts',
base.historyCompactWriteSkippedReasonCounts,
patch.historyCompactWriteSkippedReasonCounts,
),
...mergeCompactionDecisionDiagnostics(base.compactionDecisions, patch.compactionDecisions),
};
}
export function mergeContextBudgetDiagnosticPatches(
left: Partial<ContextBudgetDiagnostic> | undefined,
right: Partial<ContextBudgetDiagnostic> | undefined,
): Partial<ContextBudgetDiagnostic> | undefined {
if (!left && !right) return undefined;
if (!left) return right;
if (!right) return left;
return mergeContextBudgetDiagnostic(left as ContextBudgetDiagnostic, right);
}
export function shouldAppendContextCompactedNote(
contextBudget: ContextBudgetDiagnostic | undefined,
): boolean {
if ((contextBudget?.historyCompactBlocksWritten ?? 0) <= 0) return false;
return (
contextBudget?.compactionDecisions?.some(
(decision) =>
decision.stage === 'priorReplay' &&
decision.boundaryKind === 'historyCompact' &&
decision.decision === 'replaced',
) === true
);
}
export function shouldAppendContextCompactionFailedOpenNote(
contextBudget: ContextBudgetDiagnostic | undefined,
): boolean {
return (
(contextBudget?.historyCompactWriteFailures ?? 0) > 0 &&
contextBudget?.compactionDecisions?.some(
(decision) =>
decision.stage === 'priorReplay' &&
decision.boundaryKind === 'historyCompact' &&
decision.decision === 'failedOpen',
) === true
);
}
export function minimalContextBudgetDiagnostic(): ContextBudgetDiagnostic {
return {
enabled: true,
estimatedTokensBefore: 0,
estimatedTokensAfter: 0,
keptTurns: 0,
droppedTurns: 0,
keptEvents: 0,
droppedEvents: 0,
};
}
function mergeCountRecords(
left: Record<string, number> | undefined,
right: Record<string, number> | undefined,
): Record<string, number> | undefined {
if (!left && !right) return undefined;
const out: Record<string, number> = { ...(left ?? {}) };
for (const [key, value] of Object.entries(right ?? {})) {
out[key] = (out[key] ?? 0) + value;
}
return out;
}
function mergeCountRecordField<K extends keyof ContextBudgetDiagnostic>(
key: K,
left: Record<string, number> | undefined,
right: Record<string, number> | undefined,
): Pick<ContextBudgetDiagnostic, K> | Record<string, never> {
const merged = mergeCountRecords(left, right);
return merged === undefined ? {} : ({ [key]: merged } as Pick<ContextBudgetDiagnostic, K>);
}
function mergeCompactionDecisionDiagnostics(
left: readonly CompactionDecisionDiagnostic[] | undefined,
right: readonly CompactionDecisionDiagnostic[] | undefined,
): { compactionDecisions: CompactionDecisionDiagnostic[] } | Record<string, never> {
if (!left && !right) return {};
if (!right || right.length === 0) return { compactionDecisions: [...(left ?? [])] };
const replacesHistoryCompact = right.some(
(decision) => decision.stage === 'priorReplay' && decision.boundaryKind === 'historyCompact',
);
const retainedLeft = replacesHistoryCompact
? (left ?? []).filter(
(decision) =>
!(decision.stage === 'priorReplay' && decision.boundaryKind === 'historyCompact'),
)
: (left ?? []);
return { compactionDecisions: [...retainedLeft, ...right] };
}
// Public compat wrappers: preserve the pre-split `(events, policy, options)`
// signature for @maka/runtime consumers. Internal callers (this module and
// ai-sdk-backend) import the narrow leaf API directly from the leaf modules.
export function collectStaleToolResultArchiveCandidates(
events: readonly RuntimeEvent[],
policy: ContextBudgetPolicy | undefined,
): StaleToolResultArchiveCandidate[] {
return collectStaleToolResultArchiveCandidatesNarrow(
events,
policy?.staleToolResultPrune,
policy?.charsPerToken ?? 4,
policy?.minRecentTurns,
);
}
export function applyRuntimeEventHistoryCompact(
events: readonly RuntimeEvent[],
policy: ContextBudgetPolicy | undefined,
options: HistoryCompactReplayOptions = {},
): HistoryCompactReplayResult {
return applyRuntimeEventHistoryCompactNarrow(
events,
policy?.historyCompact,
policy?.charsPerToken,
policy?.maxHistoryEstimatedTokens,
options,
);
}
export function evaluateHistoryCompactCheckpointReplay(
checkpoint: HistoryCompactCheckpoint,
replayTail: readonly RuntimeEvent[],
policy: ContextBudgetPolicy | undefined,
options: HistoryCompactReplayOptions = {},
): HistoryCompactCheckpointReplayFit {
return evaluateHistoryCompactCheckpointReplayNarrow(
checkpoint,
replayTail,
policy?.charsPerToken,
policy?.maxHistoryEstimatedTokens,
options,
);
}