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 <noreply@anthropic.com>
This commit is contained in:
parent
b15e1eeb2a
commit
0bce493e10
4 changed files with 98 additions and 8 deletions
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: [] };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue