fix(agents): bound search tool stderr

This commit is contained in:
Vincent Koc
2026-05-28 15:55:51 +02:00
parent 8ba71e4aff
commit f2f18f5958
4 changed files with 54 additions and 5 deletions

View File

@@ -8,7 +8,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
import type { AgentTool } from "../../runtime/index.js";
import { ensureTool } from "../../utils/tools-manager.js";
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
import { normalizePositiveLimit } from "./limits.js";
import { appendBoundedTextTail, normalizePositiveLimit } from "./limits.js";
import { resolveToCwd } from "./path-utils.js";
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
import type { FindToolDetails } from "./tool-contracts.js";
@@ -277,7 +277,7 @@ export function createFindToolDefinition(
};
child.stderr?.on("data", (chunk) => {
stderr += chunk.toString();
stderr = appendBoundedTextTail(stderr, chunk);
});
rl.on("line", (line) => {

View File

@@ -8,7 +8,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
import type { AgentTool } from "../../runtime/index.js";
import { ensureTool } from "../../utils/tools-manager.js";
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
import { normalizePositiveLimit } from "./limits.js";
import { appendBoundedTextTail, normalizePositiveLimit } from "./limits.js";
import { resolveToCwd } from "./path-utils.js";
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
import type { GrepToolDetails } from "./tool-contracts.js";
@@ -270,7 +270,7 @@ export function createGrepToolDefinition(
};
signal?.addEventListener("abort", onAbort, { once: true });
child.stderr?.on("data", (chunk) => {
stderr += chunk.toString();
stderr = appendBoundedTextTail(stderr, chunk);
});
const formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { normalizePositiveLimit } from "./limits.js";
import {
appendBoundedTextTail,
normalizePositiveLimit,
SESSION_TOOL_STDERR_TAIL_BYTES,
} from "./limits.js";
describe("session tool limits", () => {
it.each([
@@ -13,4 +17,25 @@ describe("session tool limits", () => {
])("normalizes %s to %s", (input, expected) => {
expect(normalizePositiveLimit(input, 500)).toBe(expected);
});
it("keeps a bounded tail of accumulated child output", () => {
let output = appendBoundedTextTail("old-", "middle-", 12);
output = appendBoundedTextTail(output, "recent", 12);
expect(output).toBe("iddle-recent");
expect(Buffer.byteLength(output, "utf8")).toBeLessThanOrEqual(12);
});
it("clips oversized chunks to the configured tail bytes", () => {
const output = appendBoundedTextTail("ignored", Buffer.from("x".repeat(128)), 16);
expect(output).toBe("x".repeat(16));
expect(Buffer.byteLength(output, "utf8")).toBe(16);
});
it("uses the session stderr tail limit by default", () => {
const output = appendBoundedTextTail("", "x".repeat(SESSION_TOOL_STDERR_TAIL_BYTES + 1));
expect(Buffer.byteLength(output, "utf8")).toBe(SESSION_TOOL_STDERR_TAIL_BYTES);
});
});

View File

@@ -4,3 +4,27 @@ export function normalizePositiveLimit(value: number | undefined, fallback: numb
}
return Math.max(1, Math.floor(value));
}
export const SESSION_TOOL_STDERR_TAIL_BYTES = 64 * 1024;
export function appendBoundedTextTail(
current: string,
chunk: Buffer | string,
maxBytes = SESSION_TOOL_STDERR_TAIL_BYTES,
): string {
const effectiveMaxBytes = normalizePositiveLimit(maxBytes, SESSION_TOOL_STDERR_TAIL_BYTES);
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
if (chunkBuffer.byteLength >= effectiveMaxBytes) {
return chunkBuffer.subarray(chunkBuffer.byteLength - effectiveMaxBytes).toString("utf8");
}
const currentBuffer = Buffer.from(current);
const nextBytes = currentBuffer.byteLength + chunkBuffer.byteLength;
if (nextBytes <= effectiveMaxBytes) {
return `${current}${chunkBuffer.toString("utf8")}`;
}
const currentTailBytes = Math.max(0, effectiveMaxBytes - chunkBuffer.byteLength);
const currentTail = currentBuffer.subarray(currentBuffer.byteLength - currentTailBytes);
return Buffer.concat([currentTail, chunkBuffer], effectiveMaxBytes).toString("utf8");
}