mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-21 16:41:56 +00:00
feat(tool-truncation): use head+tail strategy to preserve errors during truncation
When tool results are truncated to fit the context window, important content at the end (error messages, stack traces, result summaries) was previously lost since only the beginning was kept. Now detects when the tail contains error/diagnostic content and splits the truncation budget: 70% head, 30% tail, with a middle omission marker. This preserves both the initial context and the error output that's typically most actionable. Falls back to head-only truncation when the tail doesn't contain important patterns (errors, JSON closing, summary lines). # Conflicts: # src/agents/pi-embedded-runner/tool-result-truncation.ts
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Docs/Web search: remove outdated Brave free-tier wording and replace prescriptive AI ToS guidance with neutral compliance language in Brave setup docs. (#26860) Thanks @HenryLoenwind.
|
||||
- Tools/Diffs guidance loading: move diffs usage guidance from unconditional prompt-hook injection to the plugin companion skill path, reducing unrelated-turn prompt noise while keeping diffs tool behavior unchanged. (#32630) thanks @sircrumpet.
|
||||
- Agents/tool-result truncation: preserve important tail diagnostics by using head+tail truncation for oversized tool results while keeping configurable truncation options. (#20076) thanks @jlwestsr.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -289,3 +289,25 @@ describe("sessionLikelyHasOversizedToolResults", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncateToolResultText head+tail strategy", () => {
|
||||
it("preserves error content at the tail when present", () => {
|
||||
const head = "Line 1\n".repeat(500);
|
||||
const middle = "data data data\n".repeat(500);
|
||||
const tail = "\nError: something failed\nStack trace: at foo.ts:42\n";
|
||||
const text = head + middle + tail;
|
||||
const result = truncateToolResultText(text, 5000);
|
||||
// Should contain both the beginning and the error at the end
|
||||
expect(result).toContain("Line 1");
|
||||
expect(result).toContain("Error: something failed");
|
||||
expect(result).toContain("middle content omitted");
|
||||
});
|
||||
|
||||
it("uses simple head truncation when tail has no important content", () => {
|
||||
const text = "normal line\n".repeat(1000);
|
||||
const result = truncateToolResultText(text, 5000);
|
||||
expect(result).toContain("normal line");
|
||||
expect(result).not.toContain("middle content omitted");
|
||||
expect(result).toContain("truncated");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,34 @@ type ToolResultTruncationOptions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncate a single text string to fit within maxChars, preserving the beginning.
|
||||
* Marker inserted between head and tail when using head+tail truncation.
|
||||
*/
|
||||
const MIDDLE_OMISSION_MARKER =
|
||||
"\n\n⚠️ [... middle content omitted — showing head and tail ...]\n\n";
|
||||
|
||||
/**
|
||||
* Detect whether text likely contains error/diagnostic content near the end,
|
||||
* which should be preserved during truncation.
|
||||
*/
|
||||
function hasImportantTail(text: string): boolean {
|
||||
// Check last ~2000 chars for error-like patterns
|
||||
const tail = text.slice(-2000).toLowerCase();
|
||||
return (
|
||||
/\b(error|exception|failed|fatal|traceback|panic|stack trace|errno|exit code)\b/.test(tail) ||
|
||||
// JSON closing — if the output is JSON, the tail has closing structure
|
||||
/\}\s*$/.test(tail.trim()) ||
|
||||
// Summary/result lines often appear at the end
|
||||
/\b(total|summary|result|complete|finished|done)\b/.test(tail)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a single text string to fit within maxChars.
|
||||
*
|
||||
* Uses a head+tail strategy when the tail contains important content
|
||||
* (errors, results, JSON structure), otherwise preserves the beginning.
|
||||
* This ensures error messages and summaries at the end of tool output
|
||||
* aren't lost during truncation.
|
||||
*/
|
||||
export function truncateToolResultText(
|
||||
text: string,
|
||||
@@ -51,11 +78,35 @@ export function truncateToolResultText(
|
||||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
}
|
||||
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) {
|
||||
const budget = Math.max(minKeepChars, maxChars - suffix.length);
|
||||
|
||||
// If tail looks important, split budget between head and tail
|
||||
if (hasImportantTail(text) && budget > minKeepChars * 2) {
|
||||
const tailBudget = Math.min(Math.floor(budget * 0.3), 4_000);
|
||||
const headBudget = budget - tailBudget - MIDDLE_OMISSION_MARKER.length;
|
||||
|
||||
if (headBudget > minKeepChars) {
|
||||
// Find clean cut points at newline boundaries
|
||||
let headCut = headBudget;
|
||||
const headNewline = text.lastIndexOf("\n", headBudget);
|
||||
if (headNewline > headBudget * 0.8) {
|
||||
headCut = headNewline;
|
||||
}
|
||||
|
||||
let tailStart = text.length - tailBudget;
|
||||
const tailNewline = text.indexOf("\n", tailStart);
|
||||
if (tailNewline !== -1 && tailNewline < tailStart + tailBudget * 0.2) {
|
||||
tailStart = tailNewline + 1;
|
||||
}
|
||||
|
||||
return text.slice(0, headCut) + MIDDLE_OMISSION_MARKER + text.slice(tailStart) + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: keep the beginning
|
||||
let cutPoint = budget;
|
||||
const lastNewline = text.lastIndexOf("\n", budget);
|
||||
if (lastNewline > budget * 0.8) {
|
||||
cutPoint = lastNewline;
|
||||
}
|
||||
return text.slice(0, cutPoint) + suffix;
|
||||
|
||||
Reference in New Issue
Block a user