SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
68d7816 verified
Raw History Blame Contribute Delete
8.22 kB
import type { Message } from '#/llm-adapter/contract/message';
import type { ProfileModelContext } from '#/agent/profile/profile';
import type { CompactionSource } from './types';
import { estimateTokensForMessage } from '#/llm-adapter/contract/tokens';
export interface CompactionConfig {
triggerRatio: number;
blockRatio: number;
reservedContextSize: number;
maxCompactionPerTurn: number;
maxOverflowCompactionAttempts: number;
maxRecentMessages: number;
maxRecentUserMessages: number;
maxRecentSizeRatio: number;
minOverflowReductionRatio: number;
}
export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
triggerRatio: 0.85,
blockRatio: 0.85,
reservedContextSize: 50_000,
maxCompactionPerTurn: Infinity,
maxOverflowCompactionAttempts: 3,
maxRecentMessages: 4,
maxRecentUserMessages: Infinity,
maxRecentSizeRatio: 0.2,
minOverflowReductionRatio: 0.05,
};
export interface CompactionStrategy {
shouldCompact(usedSize: number): boolean;
shouldBlock(usedSize: number): boolean;
computeCompactCount(messages: readonly Message[], source: CompactionSource): number;
reduceCompactOnOverflow(messages: readonly Message[]): number;
readonly checkAfterStep: boolean;
readonly maxCompactionPerTurn: number;
readonly maxOverflowCompactionAttempts: number;
}
export class RuntimeCompactionStrategy implements CompactionStrategy {
constructor(
private readonly context: () => ProfileModelContext,
private readonly estimateMessage: (message: Message) => number = estimateTokensForMessage,
) { }
shouldCompact(usedSize: number): boolean {
return this.delegate().shouldCompact(usedSize);
}
shouldBlock(usedSize: number): boolean {
return this.delegate().shouldBlock(usedSize);
}
computeCompactCount(messages: readonly Message[], source: CompactionSource): number {
return this.windowDelegate().computeCompactCount(messages, source);
}
reduceCompactOnOverflow(messages: readonly Message[]): number {
return this.windowDelegate().reduceCompactOnOverflow(messages);
}
get checkAfterStep(): boolean {
return this.config().triggerRatio !== this.config().blockRatio;
}
get maxCompactionPerTurn(): number {
return DEFAULT_COMPACTION_CONFIG.maxCompactionPerTurn;
}
get maxOverflowCompactionAttempts(): number {
return DEFAULT_COMPACTION_CONFIG.maxOverflowCompactionAttempts;
}
private delegate(): DefaultCompactionStrategy {
const model = this.context();
return new DefaultCompactionStrategy(
() => model.modelCapabilities.max_input_tokens ?? model.modelCapabilities.max_context_tokens,
this.config(model),
this.estimateMessage,
);
}
private windowDelegate(): DefaultCompactionStrategy {
return new DefaultCompactionStrategy(
() => this.context().modelCapabilities.max_input_tokens ?? this.context().modelCapabilities.max_context_tokens,
DEFAULT_COMPACTION_CONFIG,
this.estimateMessage,
);
}
private config(model: ProfileModelContext = this.context()): CompactionConfig {
const triggerRatio = model.compactionTriggerRatio ?? DEFAULT_COMPACTION_CONFIG.triggerRatio;
const blockRatio = Math.max(triggerRatio, DEFAULT_COMPACTION_CONFIG.blockRatio);
return {
...DEFAULT_COMPACTION_CONFIG,
triggerRatio,
blockRatio,
reservedContextSize:
model.reservedContextSize ?? DEFAULT_COMPACTION_CONFIG.reservedContextSize,
};
}
}
export class DefaultCompactionStrategy implements CompactionStrategy {
constructor(
protected readonly maxSizeProvider: () => number,
protected readonly config: CompactionConfig = DEFAULT_COMPACTION_CONFIG,
protected readonly estimateMessage: (message: Message) => number = estimateTokensForMessage,
) { }
protected get maxSize(): number {
return this.maxSizeProvider();
}
shouldCompact(usedSize: number): boolean {
if (this.maxSize <= 0) return false;
return (
usedSize >= this.maxSize * this.config.triggerRatio ||
this.shouldUseReservedContext(usedSize)
);
}
shouldBlock(usedSize: number): boolean {
if (this.maxSize <= 0) return false;
return (
usedSize >= this.maxSize * this.config.blockRatio ||
this.shouldUseReservedContext(usedSize)
);
}
private shouldUseReservedContext(usedSize: number): boolean {
const reservedSize = this.config.reservedContextSize;
return reservedSize > 0 && reservedSize < this.maxSize && usedSize + reservedSize >= this.maxSize;
}
computeCompactCount(messages: readonly Message[], source: CompactionSource): number {
if (source === 'manual') {
for (let i = messages.length - 1; i > 0; i--) {
if (canSplitAfter(messages, i)) {
return this.fitCompactCountToWindow(messages, i + 1);
}
}
return 0;
}
let recentMessages = 1;
let recentUserMessages = 0;
let recentSize = 0;
let bestN: number | undefined;
for (; recentMessages < messages.length; recentMessages++) {
const splitIndex = messages.length - recentMessages - 1;
const m2 = messages[messages.length - recentMessages]!;
if (m2.role === 'user') {
recentUserMessages++;
}
recentSize += this.estimateMessage(m2);
if (canSplitAfter(messages, splitIndex)) {
bestN = splitIndex + 1;
}
const reachesMax = recentMessages >= this.config.maxRecentMessages
|| recentUserMessages >= this.config.maxRecentUserMessages
|| recentSize >= this.maxSize * this.config.maxRecentSizeRatio;
if (reachesMax && bestN !== undefined) {
break;
}
}
return this.fitCompactCountToWindow(messages, bestN ?? 0);
}
reduceCompactOnOverflow(messages: readonly Message[]): number {
const minReducedSize = Math.max(
1,
Math.ceil(this.maxSize * this.config.minOverflowReductionRatio),
);
let reducedSize = 0;
let bestN: number | undefined;
for (let i = messages.length - 2; i > 0; i--) {
reducedSize += this.estimateMessage(messages[i + 1]!);
if (canSplitAfter(messages, i)) {
bestN = i + 1;
if (reducedSize >= minReducedSize) {
return i + 1;
}
}
}
return bestN ?? messages.length;
}
private fitCompactCountToWindow(
messages: readonly Message[],
compactedCount: number,
): number {
if (this.maxSize <= 0 || compactedCount <= 0) {
return compactedCount;
}
let compactedSize = 0;
for (let i = 0; i < compactedCount; i++) {
compactedSize += this.estimateMessage(messages[i]!);
}
if (compactedSize <= this.maxSize) {
return compactedCount;
}
let bestN: number | undefined;
for (let n = compactedCount - 1; n > 0; n--) {
compactedSize -= this.estimateMessage(messages[n]!);
if (!canSplitAfter(messages, n - 1)) {
continue;
}
bestN = n;
if (compactedSize <= this.maxSize) {
return n;
}
}
return bestN ?? compactedCount;
}
get checkAfterStep(): boolean {
return this.config.triggerRatio !== this.config.blockRatio;
}
get maxCompactionPerTurn(): number {
return this.config.maxCompactionPerTurn;
}
get maxOverflowCompactionAttempts(): number {
return this.config.maxOverflowCompactionAttempts;
}
}
function canSplitAfter(messages: readonly Message[], index: number): boolean {
const m = messages[index];
if (m === undefined) return false;
if (m.role === 'user') return false;
if (m.role === 'assistant' && m.toolCalls.length > 0) return false;
if (messages[index + 1]?.role === 'tool') return false;
if (prefixEndsWithOpenToolExchange(messages, index)) return false;
return true;
}
function prefixEndsWithOpenToolExchange(messages: readonly Message[], index: number): boolean {
if (messages[index]?.role !== 'tool') return false;
let toolResultCount = 0;
for (let i = index; i >= 0; i--) {
const message = messages[i];
if (message === undefined) return false;
if (message.role === 'tool') {
toolResultCount++;
continue;
}
return message.role === 'assistant' && message.toolCalls.length > toolResultCount;
}
return false;
}