* Tool output pruning utilities for compaction.
*/
import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
import type { AgentMessage } from "../types";
import { estimateTokens } from "./compaction";
import type { SessionEntry, SessionMessageEntry } from "./entries";
import {
collectToolCallsById,
isProtectedToolResult,
isSkillReadToolResult,
type ProtectedToolMatcher,
} from "./tool-protection";
export interface PruneConfig {
protectTokens: number;
minimumSavings: number;
protectedTools: ProtectedToolMatcher[];
}
export const DEFAULT_PRUNE_CONFIG: PruneConfig = {
protectTokens: 40_000,
minimumSavings: 20_000,
protectedTools: ["skill", isSkillReadToolResult],
};
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 }> = [];
const toolCallsById = collectToolCallsById(entries);
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 = isProtectedToolResult(message, toolCallsById.get(message.toolCallId), config.protectedTools);
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 };
}