Files
moeka-project/packages/core-agent/src/messages/compaction.ts
T
2026-08-26 19:49:58 +08:00

119 lines
3.6 KiB
TypeScript

import type { HistoryItem, Message, MessageHistoryBlockSegment, RawMessage } from './types'
/**
* Options for compacting projected conversation history.
*/
export interface CompactConversationEntriesOptions {
/** Ordered conversation entries to compact in place. */
entries: Array<Message | RawMessage>
/** Maximum number of explicit `turn` items to preserve inside each history block. */
recentTurnLimit: number
/** Optional domain-aware summary formatter used for removed history windows. */
summarizeCompactedHistory?: (input: {
keptItems: HistoryItem[]
originalItems: HistoryItem[]
removedTurnCount: number
}) => string
}
/**
* Compacts older structured history blocks while preserving recent turn/reaction pairs.
*
* Use when:
* - Long-running conversations need a smaller prompt footprint
* - Domain history must keep recent turn/reaction pairs intact without hardcoding one plugin format
*
* Expects:
* - History blocks to be ordered chronologically
* - `recentTurnLimit` to be a positive integer
* - `summarizeCompactedHistory`, when provided, returns domain-specific summary text for removed history
*
* Returns:
* - A new entry array with eligible history blocks compacted in place
*/
export function compactConversationEntries(input: CompactConversationEntriesOptions): Array<Message | RawMessage> {
if (input.recentTurnLimit <= 0)
return input.entries
return input.entries.map((entry) => {
if (!isStructuredMessage(entry))
return entry
return {
...entry,
segments: entry.segments.map((segment) => {
if (segment.type !== 'history-block')
return segment
return compactHistoryBlock(segment, input.recentTurnLimit, input.summarizeCompactedHistory)
}),
}
})
}
function compactHistoryBlock(
segment: MessageHistoryBlockSegment,
recentTurnLimit: number,
summarizeCompactedHistory?: (input: {
keptItems: HistoryItem[]
originalItems: HistoryItem[]
removedTurnCount: number
}) => string,
): MessageHistoryBlockSegment {
if (segment.compacted)
return segment
const historyTurnCount = countTurns(segment.items)
if (historyTurnCount <= recentTurnLimit)
return segment
const keptItems = keepRecentHistoryItems(segment.items, recentTurnLimit)
const removedTurnCount = historyTurnCount - recentTurnLimit
return {
compacted: true,
items: [
{
fromTurnIndex: segment.items.find(item => item.type === 'turn')?.type === 'turn'
? (segment.items.find(item => item.type === 'turn')?.turnIndex ?? undefined)
: undefined,
text: summarizeCompactedHistory?.({
keptItems,
originalItems: segment.items,
removedTurnCount,
}) ?? `Compacted ${removedTurnCount} older turns with paired reactions.`,
toTurnIndex: keptItems.findLast(item => item.type === 'turn')?.type === 'turn'
? keptItems.findLast(item => item.type === 'turn')?.turnIndex
: undefined,
type: 'summary',
},
...keptItems,
],
type: 'history-block',
}
}
function countTurns(items: HistoryItem[]) {
return items.reduce((count, item) => count + (item.type === 'turn' ? 1 : 0), 0)
}
function isStructuredMessage(entry: Message | RawMessage): entry is Message {
return 'segments' in entry
}
function keepRecentHistoryItems(items: HistoryItem[], recentTurnLimit: number) {
const keptItems: HistoryItem[] = []
let turnCount = 0
for (let index = items.length - 1; index >= 0; index -= 1) {
keptItems.unshift(items[index])
if (items[index].type === 'turn')
turnCount += 1
if (turnCount >= recentTurnLimit)
break
}
return keptItems
}