Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ function generateSystemPrompt(projectInfo: ProjectInfo | null, useTools: boolean
- When you need a tool, emit a tool call (do not describe tool usage in plain text).
- Do not put tool-call syntax or commands in your normal response.
- Do not present shell commands in fenced code blocks like \`\`\`bash\`\`\`; use the bash tool instead.
- Only call tools when you need file contents, codebase search results, or command output to answer the user's request. If the user asks for general guidance or meta/instructional help, respond directly without calling tools.
- Wait for tool results before continuing; if a tool fails, explain and try a different tool.

## Available Tools
Expand Down
69 changes: 65 additions & 4 deletions src/utils/tool-result-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import type { Message } from '../types.js';
import { AGENT_CONFIG } from '../constants.js';

const TOOL_RESULT_HEADER_REGEX = /(ERROR from|Result from) ([^:\n]+):\s*/g;
const TOOL_RESULT_HEADER_TEST = /(ERROR from|Result from) ([^:\n]+):/;

/**
* Create a short summary of a tool result for truncation.
*/
Expand Down Expand Up @@ -46,6 +49,53 @@ export function summarizeToolResult(toolName: string, content: string, isError:
}
}

function looksLikeToolResultText(text: string): boolean {
return TOOL_RESULT_HEADER_TEST.test(text);
}

function truncateToolResultText(text: string): string | null {
const matches = Array.from(text.matchAll(new RegExp(TOOL_RESULT_HEADER_REGEX.source, 'g')));
if (matches.length === 0) return null;

let rebuilt = '';
let lastIndex = 0;
let changed = false;

for (let i = 0; i < matches.length; i++) {
const match = matches[i];
const headerStart = match.index ?? 0;
const headerEnd = headerStart + match[0].length;
const nextStart = i + 1 < matches.length ? (matches[i + 1].index ?? text.length) : text.length;

rebuilt += text.slice(lastIndex, headerStart);

const toolName = match[2].trim();
const isError = match[1] === 'ERROR from';
const rawContent = text.slice(headerEnd, nextStart);
const trimmedContent = rawContent.trim();

if (trimmedContent.length > AGENT_CONFIG.TOOL_RESULT_TRUNCATE_THRESHOLD) {
const summary = summarizeToolResult(toolName, trimmedContent, isError);
if (isError) {
rebuilt += `ERROR from ${toolName}: ${summary}\n\n`;
} else {
rebuilt += `Result from ${toolName}:\n${summary}\n\n`;
}
changed = true;
} else {
rebuilt += text.slice(headerStart, nextStart);
}

lastIndex = nextStart;
}

if (lastIndex < text.length) {
rebuilt += text.slice(lastIndex);
}

return changed ? rebuilt : text;
}

/**
* Truncate old tool results in message history to save context.
* Keeps recent tool results intact, truncates older ones to summaries.
Expand All @@ -55,11 +105,16 @@ export function truncateOldToolResults(messages: Message[]): void {
const toolResultIndices: number[] = [];
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (typeof msg.content !== 'string') {
const hasToolResult = msg.content.some(block => block.type === 'tool_result');
if (hasToolResult) {
if (typeof msg.content === 'string') {
if (looksLikeToolResultText(msg.content)) {
toolResultIndices.push(i);
}
continue;
}

const hasToolResult = msg.content.some(block => block.type === 'tool_result');
if (hasToolResult) {
toolResultIndices.push(i);
}
}

Expand All @@ -68,7 +123,13 @@ export function truncateOldToolResults(messages: Message[]): void {

for (const idx of indicesToTruncate) {
const msg = messages[idx];
if (typeof msg.content === 'string') continue;
if (typeof msg.content === 'string') {
const truncated = truncateToolResultText(msg.content);
if (truncated && truncated !== msg.content) {
msg.content = truncated;
}
continue;
}

msg.content = msg.content.map(block => {
if (block.type !== 'tool_result') return block;
Expand Down
35 changes: 35 additions & 0 deletions tests/tool-result-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ describe('tool-result-utils', () => {
};
}

function createTextToolResultMessage(text: string): Message {
return {
role: 'user',
content: text,
};
}

it('truncates old tool results', () => {
const longContent = 'A'.repeat(5000);
const messages: Message[] = [
Expand Down Expand Up @@ -265,5 +272,33 @@ describe('tool-result-utils', () => {
expect(firstContent.tool_use_id).toBeDefined();
expect(firstContent.content).toContain('ERROR');
});

it('truncates old text-based tool results', () => {
const longOutput = 'A'.repeat(600);
const messages: Message[] = [
createTextToolResultMessage(`Result from read_file:\n${longOutput}\n\n`),
createTextMessage('Response 1'),
createTextToolResultMessage(`ERROR from bash: ${longOutput}\n\n`),
createTextMessage('Response 2'),
createTextToolResultMessage(`Result from read_file:\n${longOutput}\n\n`), // Recent
createTextMessage('Response 3'),
createTextToolResultMessage(`Result from read_file:\n${longOutput}\n\n`), // Recent
];

truncateOldToolResults(messages);

const first = messages[0].content as string;
expect(first).toContain('[read_file:');
expect(first.length).toBeLessThan(200);

const second = messages[2].content as string;
expect(second).toContain('[bash ERROR:');

const recent = messages[4].content as string;
expect(recent).toContain(longOutput);

const mostRecent = messages[6].content as string;
expect(mostRecent).toContain(longOutput);
});
});
});