/** * Tool output pruning utilities for compaction. */ import type { AgentMessage } from "@f5-sales-demo/pi-agent-core"; import type { ToolResultMessage } from "@f5-sales-demo/pi-ai"; import type { SessionEntry, SessionMessageEntry } from "../session-manager"; import { estimateTokens } from "./compaction"; export interface PruneConfig { /** Keep the most recent tool output tokens intact. */ protectTokens: number; /** Only prune if total savings meets this threshold. */ minimumSavings: number; /** Tool names that should never be pruned. */ protectedTools: string[]; } export const DEFAULT_PRUNE_CONFIG: PruneConfig = { protectTokens: 40_000, minimumSavings: 20_000, protectedTools: ["skill", "read"], }; export interface PruneResult { prunedCount: number; tokensSaved: number; } function createPrunedNotice(tokens: number): string { return `[Output truncated - ${tokens} tokens]`; } function getToolResultMessage(entry: SessionEntry): ToolResultMessage | undefined { if (entry.type !== "message") return undefined; const message = entry.message as AgentMessage; if (message.role !== "toolResult") return undefined; return message as ToolResultMessage; } function estimatePrunedSavings(tokens: number): number { const noticeTokens = Math.ceil(createPrunedNotice(tokens).length / 4); return Math.max(0, tokens - noticeTokens); } export function pruneToolOutputs(entries: SessionEntry[], config: PruneConfig = DEFAULT_PRUNE_CONFIG): PruneResult { let accumulatedTokens = 0; let tokensSaved = 0; let prunedCount = 0; const candidates: Array<{ entry: SessionMessageEntry; tokens: number }> = []; for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; const message = getToolResultMessage(entry); if (!message) continue; const tokens = estimateTokens(message as AgentMessage); const isProtected = config.protectedTools.includes(message.toolName); if (message.prunedAt !== undefined) { accumulatedTokens += tokens; continue; } if (accumulatedTokens < config.protectTokens || isProtected) { accumulatedTokens += tokens; continue; } candidates.push({ entry: entry as SessionMessageEntry, tokens }); accumulatedTokens += tokens; } for (const candidate of candidates) { tokensSaved += estimatePrunedSavings(candidate.tokens); } if (tokensSaved < config.minimumSavings || candidates.length === 0) { return { prunedCount: 0, tokensSaved: 0 }; } const prunedAt = Date.now(); for (const candidate of candidates) { const message = candidate.entry.message as ToolResultMessage; message.content = [{ type: "text", text: createPrunedNotice(candidate.tokens) }]; message.prunedAt = prunedAt; prunedCount++; } return { prunedCount, tokensSaved }; }