mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:38:28 +00:00
refactor(agents): dedupe config and truncation guards
This commit is contained in:
@@ -2,7 +2,9 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
truncateToolResultText,
|
||||
truncateToolResultMessage,
|
||||
calculateMaxToolResultChars,
|
||||
getToolResultTextLength,
|
||||
truncateOversizedToolResultsInMessages,
|
||||
isOversizedToolResult,
|
||||
sessionLikelyHasOversizedToolResults,
|
||||
@@ -82,6 +84,55 @@ describe("truncateToolResultText", () => {
|
||||
expect(lastNewline).toBeGreaterThan(keptContent.length - 100);
|
||||
}
|
||||
});
|
||||
|
||||
it("supports custom suffix and min keep chars", () => {
|
||||
const text = "x".repeat(5_000);
|
||||
const result = truncateToolResultText(text, 300, {
|
||||
suffix: "\n\n[custom-truncated]",
|
||||
minKeepChars: 250,
|
||||
});
|
||||
expect(result).toContain("[custom-truncated]");
|
||||
expect(result.length).toBeGreaterThan(250);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getToolResultTextLength", () => {
|
||||
it("sums all text blocks in tool results", () => {
|
||||
const msg = {
|
||||
role: "toolResult",
|
||||
content: [
|
||||
{ type: "text", text: "abc" },
|
||||
{ type: "image", source: { type: "base64", mediaType: "image/png", data: "x" } },
|
||||
{ type: "text", text: "12345" },
|
||||
],
|
||||
} as unknown as AgentMessage;
|
||||
|
||||
expect(getToolResultTextLength(msg)).toBe(8);
|
||||
});
|
||||
|
||||
it("returns zero for non-toolResult messages", () => {
|
||||
expect(getToolResultTextLength(makeAssistantMessage("hello"))).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncateToolResultMessage", () => {
|
||||
it("truncates with a custom suffix", () => {
|
||||
const msg = {
|
||||
role: "toolResult",
|
||||
toolCallId: "call_1",
|
||||
toolName: "read",
|
||||
content: [{ type: "text", text: "x".repeat(50_000) }],
|
||||
isError: false,
|
||||
timestamp: Date.now(),
|
||||
} as unknown as AgentMessage;
|
||||
|
||||
const result = truncateToolResultMessage(msg, 10_000, {
|
||||
suffix: "\n\n[persist-truncated]",
|
||||
minKeepChars: 2_000,
|
||||
}) as { content: Array<{ type: string; text: string }> };
|
||||
|
||||
expect(result.content[0]?.text).toContain("[persist-truncated]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateMaxToolResultChars", () => {
|
||||
|
||||
@@ -33,21 +33,32 @@ const TRUNCATION_SUFFIX =
|
||||
"The content above is a partial view. If you need more, request specific sections or use " +
|
||||
"offset/limit parameters to read smaller chunks.]";
|
||||
|
||||
type ToolResultTruncationOptions = {
|
||||
suffix?: string;
|
||||
minKeepChars?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncate a single text string to fit within maxChars, preserving the beginning.
|
||||
*/
|
||||
export function truncateToolResultText(text: string, maxChars: number): string {
|
||||
export function truncateToolResultText(
|
||||
text: string,
|
||||
maxChars: number,
|
||||
options: ToolResultTruncationOptions = {},
|
||||
): string {
|
||||
const suffix = options.suffix ?? TRUNCATION_SUFFIX;
|
||||
const minKeepChars = options.minKeepChars ?? MIN_KEEP_CHARS;
|
||||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
}
|
||||
const keepChars = Math.max(MIN_KEEP_CHARS, maxChars - TRUNCATION_SUFFIX.length);
|
||||
const keepChars = Math.max(minKeepChars, maxChars - suffix.length);
|
||||
// Try to break at a newline boundary to avoid cutting mid-line
|
||||
let cutPoint = keepChars;
|
||||
const lastNewline = text.lastIndexOf("\n", keepChars);
|
||||
if (lastNewline > keepChars * 0.8) {
|
||||
cutPoint = lastNewline;
|
||||
}
|
||||
return text.slice(0, cutPoint) + TRUNCATION_SUFFIX;
|
||||
return text.slice(0, cutPoint) + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +78,7 @@ export function calculateMaxToolResultChars(contextWindowTokens: number): number
|
||||
/**
|
||||
* Get the total character count of text content blocks in a tool result message.
|
||||
*/
|
||||
function getToolResultTextLength(msg: AgentMessage): number {
|
||||
export function getToolResultTextLength(msg: AgentMessage): number {
|
||||
if (!msg || (msg as { role?: string }).role !== "toolResult") {
|
||||
return 0;
|
||||
}
|
||||
@@ -91,7 +102,13 @@ function getToolResultTextLength(msg: AgentMessage): number {
|
||||
* Truncate a tool result message's text content blocks to fit within maxChars.
|
||||
* Returns a new message (does not mutate the original).
|
||||
*/
|
||||
function truncateToolResultMessage(msg: AgentMessage, maxChars: number): AgentMessage {
|
||||
export function truncateToolResultMessage(
|
||||
msg: AgentMessage,
|
||||
maxChars: number,
|
||||
options: ToolResultTruncationOptions = {},
|
||||
): AgentMessage {
|
||||
const suffix = options.suffix ?? TRUNCATION_SUFFIX;
|
||||
const minKeepChars = options.minKeepChars ?? MIN_KEEP_CHARS;
|
||||
const content = (msg as { content?: unknown }).content;
|
||||
if (!Array.isArray(content)) {
|
||||
return msg;
|
||||
@@ -114,10 +131,10 @@ function truncateToolResultMessage(msg: AgentMessage, maxChars: number): AgentMe
|
||||
}
|
||||
// Proportional budget for this block
|
||||
const blockShare = textBlock.text.length / totalTextChars;
|
||||
const blockBudget = Math.max(MIN_KEEP_CHARS, Math.floor(maxChars * blockShare));
|
||||
const blockBudget = Math.max(minKeepChars + suffix.length, Math.floor(maxChars * blockShare));
|
||||
return {
|
||||
...textBlock,
|
||||
text: truncateToolResultText(textBlock.text, blockBudget),
|
||||
text: truncateToolResultText(textBlock.text, blockBudget, { suffix, minKeepChars }),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user