blob: d28f479b3473e7389012440c4333b2b8a5c60cf3 [file]
import type { RuntimeEvent } from '@maka/core';
import type { ContextBudgetDiagnostic } from '@maka/core';
import { isArchivedToolResultPlaceholder } from './tool-result-archive.js';
import {
estimateRuntimeEventsTokens,
finitePositive,
stableStringify,
tokenizeSearchQuery,
turnKey,
} from './context-budget-helpers.js';
export interface RuntimeEventHistorySearchPolicy {
enabled: boolean;
query?: string;
maxResults?: number;
around?: number;
maxEstimatedTokens?: number;
}
export interface RuntimeEventHistorySearchHit {
eventId: string;
turnId: string;
ts: number;
score: number;
matchedTerms: string[];
}
export interface RuntimeEventHistoryAroundResult {
events: RuntimeEvent[];
hits: RuntimeEventHistorySearchHit[];
diagnosticPatch: Partial<ContextBudgetDiagnostic>;
}
export function searchRuntimeEventHistory(
events: readonly RuntimeEvent[],
query: string,
policy: RuntimeEventHistorySearchPolicy | undefined,
): RuntimeEventHistorySearchHit[] {
if (policy?.enabled !== true) return [];
const terms = tokenizeSearchQuery(query);
if (terms.length === 0) return [];
const maxResults = finitePositive(policy.maxResults) ?? 5;
return events
.map((event) => scoreRuntimeEventSearchHit(event, terms))
.filter((hit): hit is RuntimeEventHistorySearchHit => hit !== undefined)
.sort((a, b) => b.score - a.score || b.ts - a.ts || b.eventId.localeCompare(a.eventId))
.slice(0, maxResults);
}
export function retrieveRuntimeEventHistoryAround(
events: 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 = finitePositive(policy.maxEstimatedTokens) ?? 4_096;
const hits = searchRuntimeEventHistory(events, policy.query ?? query, policy);
const selectedIndexes = new Set<number>();
const indexesByEventId = new Map(events.map((event, index) => [event.id, index]));
for (const hit of hits) {
const index = indexesByEventId.get(hit.eventId);
if (index === undefined) continue;
for (
let cursor = Math.max(0, index - around);
cursor <= Math.min(events.length - 1, index + around);
cursor += 1
) {
selectedIndexes.add(cursor);
}
}
const selectedEvents: RuntimeEvent[] = [];
let selectedTokens = 0;
let skipped = 0;
for (const index of [...selectedIndexes].sort((a, b) => a - b)) {
const event = events[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 } : {}),
},
};
}
function scoreRuntimeEventSearchHit(
event: RuntimeEvent,
terms: readonly string[],
): RuntimeEventHistorySearchHit | undefined {
const haystack = runtimeEventSearchText(event).toLowerCase();
if (!haystack) return undefined;
let score = 0;
const matchedTerms: string[] = [];
for (const term of terms) {
if (!haystack.includes(term)) continue;
matchedTerms.push(term);
score += term.length;
}
if (score <= 0) return undefined;
return {
eventId: event.id,
turnId: turnKey(event),
ts: event.ts,
score,
matchedTerms,
};
}
export function runtimeEventSearchText(event: RuntimeEvent): string {
const content = event.content;
if (!content) return '';
switch (content.kind) {
case 'text':
case 'thinking':
return content.text;
case 'function_call':
return `${content.name} ${stableStringify(content.args)}`;
case 'function_response':
if (isArchivedToolResultPlaceholder(content.result)) {
return [
content.name,
content.result.toolName,
content.result.toolCallId,
content.result.artifactId,
content.result.bodySha256,
content.result.reason,
].join(' ');
}
return `${content.name} ${stableStringify(content.result)}`;
case 'error':
return `${content.message} ${content.reason ?? ''} ${content.code ?? ''}`;
}
}