From 0bce493e10d85fe3c39cc05df0155004305e0b18 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 23:39:52 +0800 Subject: [PATCH] fix(compaction): handle pi-agent-core toolResult format in truncation and pruning The pre-emptive truncation, tool result pruning, and summary fallback only checked for Anthropic-style `role: "user"` messages with `type: "tool_result"` blocks. The actual runtime uses pi-agent-core format with `role: "toolResult"`, `toolCallId`, and `toolName` on the message itself. This caused truncation and pruning to silently skip all tool results in real agent runs. Add handlers for the pi-agent-core format in all four affected modules: - session-manager.ts: check both "user" and "toolResult" roles - tool-result-truncation.ts: new handler for toolResult format - tool-result-pruning.ts: new processToolResultMessage() + updated loops - summary-fallback.ts: include "toolResult" in artifact ref extraction Verified via agent-driven E2E tests (5 test sessions, 6 artifacts). Co-Authored-By: Claude Opus 4.6 --- .../agent/context-window/summary-fallback.ts | 3 +- .../context-window/tool-result-pruning.ts | 72 +++++++++++++++++-- .../context-window/tool-result-truncation.ts | 29 +++++++- .../core/src/agent/session/session-manager.ts | 2 +- 4 files changed, 98 insertions(+), 8 deletions(-) diff --git a/packages/core/src/agent/context-window/summary-fallback.ts b/packages/core/src/agent/context-window/summary-fallback.ts index dd15aabe..7893319f 100644 --- a/packages/core/src/agent/context-window/summary-fallback.ts +++ b/packages/core/src/agent/context-window/summary-fallback.ts @@ -105,7 +105,8 @@ function extractArtifactRefs(messages: AgentMessage[]): string[] { const pattern = /Full result (?:saved to|available at) (artifacts\/[^\s.]+\.txt)/g; for (const msg of messages) { - if (msg.role !== "user") continue; + const role = msg.role as string; + if (role !== "user" && role !== "toolResult") continue; const content = (msg as any).content; if (typeof content === "string") { for (const match of content.matchAll(pattern)) { diff --git a/packages/core/src/agent/context-window/tool-result-pruning.ts b/packages/core/src/agent/context-window/tool-result-pruning.ts index be1cdaa6..2dc67f71 100644 --- a/packages/core/src/agent/context-window/tool-result-pruning.ts +++ b/packages/core/src/agent/context-window/tool-result-pruning.ts @@ -318,7 +318,51 @@ function softTrimText( } /** - * Process a user message containing tool results. + * Process a pi-agent-core "toolResult" message. + * These have role="toolResult" with content as text blocks and toolCallId/toolName on the message. + */ +function processToolResultMessage( + message: AgentMessage, + settings: ToolResultPruningSettings, + mode: "soft" | "hard", +): { message: AgentMessage; changed: boolean; charsSaved: number } { + const msgAny = message as any; + const toolName = msgAny.toolName ?? "unknown"; + + if (!isToolPrunable(toolName, settings)) { + return { message, changed: false, charsSaved: 0 }; + } + + if (hasImageContent(msgAny.content)) { + return { message, changed: false, charsSaved: 0 }; + } + + const originalText = extractToolResultText(msgAny.content); + + if (mode === "soft") { + const result = softTrimText(originalText, settings); + if (!result) return { message, changed: false, charsSaved: 0 }; + return { + message: { ...message, content: [{ type: "text", text: result.trimmed }] } as AgentMessage, + changed: true, + charsSaved: result.saved, + }; + } + + // Hard clear + const artifactRef = extractArtifactRef(originalText); + const placeholder = artifactRef + ? `${settings.hardClear.placeholder} Full result available at ${artifactRef}.` + : settings.hardClear.placeholder; + return { + message: { ...message, content: [{ type: "text", text: placeholder }] } as AgentMessage, + changed: true, + charsSaved: originalText.length - placeholder.length, + }; +} + +/** + * Process a user message containing tool results (Anthropic format). * Returns modified message if any tool results were trimmed/cleared. */ function processUserMessageToolResults( @@ -467,7 +511,26 @@ export function pruneToolResults(params: { // Phase 1: Soft Trim for (let i = pruneStartIndex; i < cutoffIndex; i++) { const msg = result[i]; - if (!msg || msg.role !== "user") continue; + if (!msg) continue; + + const msgRole = msg.role as string; + + // pi-agent-core "toolResult" format + if (msgRole === "toolResult") { + prunableIndexes.push(i); + const processed = processToolResultMessage(msg, settings, "soft"); + if (processed.changed) { + result[i] = processed.message; + changed = true; + softTrimmed++; + charsSaved += processed.charsSaved; + totalChars -= processed.charsSaved; + } + continue; + } + + // Anthropic-style "user" format with tool_result blocks + if (msgRole !== "user") continue; const msgAny = msg as any; if (!Array.isArray(msgAny.content)) continue; @@ -506,9 +569,10 @@ export function pruneToolResults(params: { if (ratio < settings.hardClearRatio) break; const msg = result[i]!; - const beforeChars = estimateMessageChars(msg); - const processed = processUserMessageToolResults(msg, settings, "hard"); + const processed = (msg.role as string) === "toolResult" + ? processToolResultMessage(msg, settings, "hard") + : processUserMessageToolResults(msg, settings, "hard"); if (processed.changed) { result[i] = processed.message; changed = true; diff --git a/packages/core/src/agent/context-window/tool-result-truncation.ts b/packages/core/src/agent/context-window/tool-result-truncation.ts index 0cf40470..1a4c9005 100644 --- a/packages/core/src/agent/context-window/tool-result-truncation.ts +++ b/packages/core/src/agent/context-window/tool-result-truncation.ts @@ -150,9 +150,34 @@ export function truncateOversizedToolResults(params: { }; const msgAny = params.message as any; + const role = params.message.role as string; - // Only process user messages with array content (tool results come as user messages) - if (params.message.role !== "user" || !Array.isArray(msgAny.content)) { + // Handle pi-agent-core "toolResult" format: + // { role: "toolResult", content: [{ type: "text", text: "..." }], toolCallId, toolName } + if (role === "toolResult" && Array.isArray(msgAny.content)) { + const maxChars = computeMaxChars(params.contextWindowTokens, settings); + if (hasImages(msgAny.content)) { + return { message: params.message, truncated: false, artifacts: [] }; + } + const text = extractText(msgAny.content); + const effectiveMax = Math.max(maxChars, settings.minKeepChars); + if (text.length <= effectiveMax) { + return { message: params.message, truncated: false, artifacts: [] }; + } + const toolCallId = msgAny.toolCallId ?? "unknown"; + const toolName = msgAny.toolName ?? "unknown"; + const artifactRelPath = params.saveArtifact(toolCallId, text); + const truncatedText = truncateText(text, maxChars, artifactRelPath, settings); + return { + message: { ...params.message, content: [{ type: "text", text: truncatedText }] } as AgentMessage, + truncated: true, + artifacts: [{ toolCallId, toolName, originalChars: text.length, artifactRelPath }], + }; + } + + // Handle Anthropic-style "user" format with tool_result blocks: + // { role: "user", content: [{ type: "tool_result", tool_use_id, content: "..." }] } + if (role !== "user" || !Array.isArray(msgAny.content)) { return { message: params.message, truncated: false, artifacts: [] }; } diff --git a/packages/core/src/agent/session/session-manager.ts b/packages/core/src/agent/session/session-manager.ts index 5bcd31b0..c51ea1bd 100644 --- a/packages/core/src/agent/session/session-manager.ts +++ b/packages/core/src/agent/session/session-manager.ts @@ -252,7 +252,7 @@ export class SessionManager { // Pre-emptive truncation: save oversized tool results as artifacts // and persist a truncated version in the JSONL session file. let persistMessage = message; - if (this.enableToolResultTruncation && message.role === "user") { + if (this.enableToolResultTruncation && (message.role === "user" || message.role === "toolResult")) { const result = truncateOversizedToolResults({ message, contextWindowTokens: this.contextWindowTokens,