openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
239 lines (238 loc) • 10.9 kB
JavaScript
import { f as stripRuntimeContextCustomMessages } from "./internal-runtime-context-BH_40W4f.js";
import { n as extractToolResultId, t as extractToolCallsFromAssistant } from "./tool-call-id-DOLZ5Kkz.js";
import { a as stripToolResultDetails, n as repairToolUseResultPairing } from "./session-transcript-repair-B17NYGhL.js";
import "./sessions-BTpdzjUa.js";
import { B as estimateTokens } from "./proxy-C1ZgbVua.js";
//#region src/agents/compaction-planning.ts
/**
* Planning helpers for transcript compaction. The module estimates sanitized
* token usage, chooses chunking strategy, and preserves active tool-use pairs
* while splitting history for summaries.
*/
/** Default share of context window targeted for compaction chunks. */
const BASE_CHUNK_RATIO = .4;
/** Lower bound for adaptive compaction chunk sizing. */
const MIN_CHUNK_RATIO = .15;
/** Buffer for estimateTokens() inaccuracy. */
const SAFETY_MARGIN = 1.2;
const DEFAULT_PARTS = 2;
/**
* Overhead reserved for summary prompt, system prompt, prior summary, wrapper
* tags, and high-reasoning summary generation.
*/
const SUMMARIZATION_OVERHEAD_TOKENS = 4096;
/** Estimates compaction tokens after removing fields that must not reach summarization. */
function estimateMessagesTokens(messages) {
return sanitizeCompactionMessages(messages).reduce((sum, message) => sum + estimateTokens(message), 0);
}
/**
* Per-original-message token estimates, aligned 1:1 to the input array. Sanitizes
* the full array once instead of wrapping and re-cloning each message in its own
* 1-element array. Runtime-context entries are not model-visible, so they estimate
* to 0 here just as sanitizeCompactionMessages([msg]) would drop them.
*/
function estimatePerMessageTokens(messages) {
const detailStripped = stripToolResultDetails(messages);
const modelVisible = new Set(stripRuntimeContextCustomMessages(detailStripped));
return detailStripped.map((message) => modelVisible.has(message) ? estimateTokens(message) : 0);
}
/** Removes runtime-only context and tool-result details before token estimates or summaries. */
function sanitizeCompactionMessages(messages) {
return stripToolResultDetails(stripRuntimeContextCustomMessages(messages));
}
/** Clamps requested split parts to a usable count for the available messages. */
function normalizeCompactionParts(parts, messageCount) {
if (!Number.isFinite(parts) || parts <= 1) return 1;
return Math.min(Math.max(1, Math.floor(parts)), Math.max(1, messageCount));
}
/** Splits messages into roughly equal token-share chunks without separating active tool pairs. */
function splitMessagesByTokenShare(messages, parts = DEFAULT_PARTS) {
if (messages.length === 0) return [];
const normalizedParts = normalizeCompactionParts(parts, messages.length);
if (normalizedParts <= 1) return [messages];
const perMessageTokens = estimatePerMessageTokens(messages);
const targetTokens = perMessageTokens.reduce((sum, tokens) => sum + tokens, 0) / normalizedParts;
const chunks = [];
let current = [];
let currentTokens = 0;
let pendingToolCallIds = /* @__PURE__ */ new Set();
let pendingChunkStartIndex = null;
let currentTokenCounts = [];
const splitCurrentAtPendingBoundary = () => {
if (pendingChunkStartIndex === null || pendingChunkStartIndex <= 0 || chunks.length >= normalizedParts - 1) return false;
chunks.push(current.slice(0, pendingChunkStartIndex));
current = current.slice(pendingChunkStartIndex);
currentTokenCounts = currentTokenCounts.slice(pendingChunkStartIndex);
currentTokens = currentTokenCounts.reduce((sum, tokens) => sum + tokens, 0);
pendingChunkStartIndex = 0;
return true;
};
for (let index = 0; index < messages.length; index += 1) {
const message = messages[index];
const messageTokens = perMessageTokens[index];
if (pendingToolCallIds.size === 0 && chunks.length < normalizedParts - 1 && current.length > 0 && currentTokens + messageTokens > targetTokens) {
chunks.push(current);
current = [];
currentTokenCounts = [];
currentTokens = 0;
pendingChunkStartIndex = null;
}
current.push(message);
currentTokenCounts.push(messageTokens);
currentTokens += messageTokens;
if (message.role === "assistant") {
const toolCalls = extractToolCallsFromAssistant(message);
const stopReason = message.stopReason;
const keepsPending = stopReason !== "aborted" && stopReason !== "error" && toolCalls.length > 0;
pendingToolCallIds = /* @__PURE__ */ new Set();
if (keepsPending) for (const toolCall of toolCalls) pendingToolCallIds.add(toolCall.id);
pendingChunkStartIndex = keepsPending ? current.length - 1 : null;
} else if (message.role === "toolResult" && pendingToolCallIds.size > 0) {
const resultId = extractToolResultId(message);
if (!resultId) {
pendingToolCallIds = /* @__PURE__ */ new Set();
pendingChunkStartIndex = null;
} else pendingToolCallIds.delete(resultId);
if (pendingToolCallIds.size === 0 && chunks.length < normalizedParts - 1 && currentTokens > targetTokens) {
splitCurrentAtPendingBoundary();
pendingChunkStartIndex = null;
}
}
}
if (pendingToolCallIds.size > 0 && currentTokens > targetTokens) splitCurrentAtPendingBoundary();
if (current.length > 0) chunks.push(current);
return chunks;
}
/** Chunks messages by a max-token budget while applying the shared estimator safety margin. */
function chunkMessagesByMaxTokens(messages, maxTokens) {
if (messages.length === 0) return [];
const effectiveMax = Math.max(1, Math.floor(maxTokens / SAFETY_MARGIN));
const perMessageTokens = estimatePerMessageTokens(messages);
const chunks = [];
let currentChunk = [];
let currentTokens = 0;
for (let index = 0; index < messages.length; index += 1) {
const message = messages[index];
const messageTokens = perMessageTokens[index];
if (currentChunk.length > 0 && currentTokens + messageTokens > effectiveMax) {
chunks.push(currentChunk);
currentChunk = [];
currentTokens = 0;
}
currentChunk.push(message);
currentTokens += messageTokens;
if (messageTokens > effectiveMax) {
chunks.push(currentChunk);
currentChunk = [];
currentTokens = 0;
}
}
if (currentChunk.length > 0) chunks.push(currentChunk);
return chunks;
}
/**
* Compute adaptive chunk ratio based on average message size.
* When messages are large, we use smaller chunks to avoid exceeding model limits.
*/
function computeAdaptiveChunkRatio(messages, contextWindow) {
if (messages.length === 0) return BASE_CHUNK_RATIO;
const avgRatio = estimateMessagesTokens(messages) / messages.length * SAFETY_MARGIN / contextWindow;
if (avgRatio > .1) {
const reduction = Math.min(avgRatio * 2, BASE_CHUNK_RATIO - MIN_CHUNK_RATIO);
return Math.max(MIN_CHUNK_RATIO, BASE_CHUNK_RATIO - reduction);
}
return BASE_CHUNK_RATIO;
}
/** Builds sanitized chunks for summarization prompts. */
function buildSummaryChunks(params) {
return chunkMessagesByMaxTokens(sanitizeCompactionMessages(params.messages), params.maxChunkTokens);
}
/** Separates messages too large to summarize and emits compact placeholder notes for them. */
function buildOversizedFallbackPlan(params) {
const smallMessages = [];
const oversizedNotes = [];
const perMessageTokens = estimatePerMessageTokens(params.messages);
const oversizedThreshold = params.contextWindow * .5;
for (let index = 0; index < params.messages.length; index += 1) {
const msg = params.messages[index];
const tokens = perMessageTokens[index];
if (tokens * 1.2 > oversizedThreshold) {
const role = msg.role ?? "message";
oversizedNotes.push(`[Large ${role} (~${Math.round(tokens / 1e3)}K tokens) omitted from summary]`);
} else smallMessages.push(msg);
}
return {
smallMessages,
oversizedNotes
};
}
/** Plans whether to split a summarization stage based on message count and token budget. */
function buildStageSplitPlan(params) {
const minMessagesForSplit = Math.max(2, params.minMessagesForSplit ?? 4);
const parts = normalizeCompactionParts(params.parts ?? DEFAULT_PARTS, params.messages.length);
const totalTokens = estimateMessagesTokens(params.messages);
if (parts <= 1 || params.messages.length < minMessagesForSplit || totalTokens <= params.maxChunkTokens) return { mode: "single" };
const chunks = splitMessagesByTokenShare(params.messages, parts).filter((chunk) => chunk.length > 0);
return chunks.length > 1 ? {
mode: "split",
chunks
} : { mode: "single" };
}
/** Drops oldest token-share chunks until history fits the requested context share. */
function pruneHistoryForContextShare(params) {
const defaultShare = params.mode === "handoff" ? .2 : .5;
const maxHistoryShare = params.maxHistoryShare ?? defaultShare;
const budgetTokens = Math.max(1, Math.floor(params.maxContextTokens * maxHistoryShare));
let keptMessages = params.messages;
const allDroppedMessages = [];
let droppedChunks = 0;
let droppedMessages = 0;
let droppedTokens = 0;
const parts = normalizeCompactionParts(params.parts ?? DEFAULT_PARTS, keptMessages.length);
while (keptMessages.length > 0 && estimateMessagesTokens(keptMessages) > budgetTokens) {
const chunks = splitMessagesByTokenShare(keptMessages, parts);
if (chunks.length <= 1) break;
const [dropped, ...rest] = chunks;
const repairReport = repairToolUseResultPairing(rest.flat());
const repairedKept = repairReport.messages;
const orphanedCount = repairReport.droppedOrphanCount;
droppedChunks += 1;
droppedMessages += dropped.length + orphanedCount;
droppedTokens += estimateMessagesTokens(dropped);
allDroppedMessages.push(...dropped);
keptMessages = repairedKept;
}
return {
messages: keptMessages,
droppedMessagesList: allDroppedMessages,
droppedChunks,
droppedMessages,
droppedTokens,
keptTokens: estimateMessagesTokens(keptMessages),
budgetTokens
};
}
/** Computes whether new content exceeds the history budget and plans pruning when needed. */
function buildHistoryPrunePlan(params) {
const summarizableTokens = estimateMessagesTokens(params.messagesToSummarize) + estimateMessagesTokens(params.turnPrefixMessages);
const newContentTokens = Math.max(0, Math.floor(params.tokensBefore - summarizableTokens));
const maxHistoryTokens = Math.floor(params.contextWindowTokens * params.maxHistoryShare * SAFETY_MARGIN);
if (newContentTokens <= maxHistoryTokens) return {
summarizableTokens,
newContentTokens,
maxHistoryTokens
};
return {
summarizableTokens,
newContentTokens,
maxHistoryTokens,
pruned: pruneHistoryForContextShare({
messages: params.messagesToSummarize,
maxContextTokens: params.contextWindowTokens,
maxHistoryShare: params.maxHistoryShare,
parts: params.parts
})
};
}
//#endregion
export { buildStageSplitPlan as a, estimateMessagesTokens as c, buildOversizedFallbackPlan as i, sanitizeCompactionMessages as l, SUMMARIZATION_OVERHEAD_TOKENS as n, buildSummaryChunks as o, buildHistoryPrunePlan as r, computeAdaptiveChunkRatio as s, SAFETY_MARGIN as t };