Merge pull request #3073 from lightpanda-io/agent-tool-cap

agent: reduce tool output cap to 64KB except for extract
This commit is contained in:
Adrià Arrufat
2026-07-28 09:44:39 +02:00
committed by GitHub

View File

@@ -1535,7 +1535,7 @@ fn recordSlashToolCall(
// capToolOutput returns its input unchanged under the cap; dupe so content
// doesn't alias the caller's per-iteration arena.
const capped = capToolOutput(ma, result.text);
const capped = capToolOutput(ma, tool_name, result.text);
const content = if (capped.ptr == result.text.ptr) try ma.dupe(u8, capped) else capped;
const tool_results = try ma.alloc(zenai.provider.ToolResult, 1);
@@ -1807,15 +1807,23 @@ fn buildUserMessageParts(
return parts.toOwnedSlice(ma);
}
// Cap per-call tool output so heavy pages don't balloon the message arena (and
// the next request body) without bound.
const tool_output_max_bytes: usize = 1 * 1024 * 1024;
// Tool results are re-sent with every subsequent turn, so an unscoped read of
// a huge page is paid once per remaining turn — the cap bounds that growth.
// `extract` is exempt up to the loose backstop: its output is schema-shaped
// JSON the model explicitly asked for, and truncation would corrupt it.
const tool_output_max_bytes: usize = 64 * 1024;
const extract_output_max_bytes: usize = 1024 * 1024;
fn capToolOutput(allocator: std.mem.Allocator, output: []const u8) []const u8 {
if (output.len <= tool_output_max_bytes) return output;
const prefix = string.truncateUtf8(output, tool_output_max_bytes);
var suffix_buf: [64]u8 = undefined;
const suffix = std.fmt.bufPrint(&suffix_buf, "\n...[truncated, original {d} bytes]", .{output.len}) catch return prefix;
fn toolOutputCap(tool_name: []const u8) usize {
return if (std.mem.eql(u8, tool_name, "extract")) extract_output_max_bytes else tool_output_max_bytes;
}
fn capToolOutput(allocator: std.mem.Allocator, tool_name: []const u8, output: []const u8) []const u8 {
const cap = toolOutputCap(tool_name);
if (output.len <= cap) return output;
const prefix = string.truncateUtf8(output, cap);
var suffix_buf: [128]u8 = undefined;
const suffix = std.fmt.bufPrint(&suffix_buf, "\n...[truncated, original {d} bytes — re-read scoped (selector/backendNodeId)]", .{output.len}) catch return prefix;
return std.mem.concat(allocator, u8, &.{ prefix, suffix }) catch prefix;
}
@@ -1836,7 +1844,7 @@ fn handleToolCall(ctx: *anyopaque, allocator: std.mem.Allocator, tool_name: []co
defer self.terminal.spinner.setThinking();
const outcome: zenai.provider.Client.ToolHandler.Result = if (browser_tools.call(allocator, self.session, &self.node_registry, tool_name, arguments)) |result|
.{ .content = capToolOutput(allocator, result.text), .is_error = result.is_error }
.{ .content = capToolOutput(allocator, tool_name, result.text), .is_error = result.is_error }
else |err|
.{ .content = std.fmt.allocPrint(allocator, "Error: {s}", .{@errorName(err)}) catch "Error: tool execution failed", .is_error = true };
@@ -1952,7 +1960,7 @@ test "savePrompt: save instructions followed by the rendered script skill" {
test "capToolOutput: passes through when under cap" {
const ta = std.testing.allocator;
const out = capToolOutput(ta, "short");
const out = capToolOutput(ta, "markdown", "short");
try std.testing.expectEqualStrings("short", out);
}
@@ -1972,9 +1980,23 @@ test "capToolOutput: appends a marker when truncating" {
buf[cap + 1] = 0x9C;
@memset(buf[cap + 2 ..], 'b');
const out = capToolOutput(ta, buf);
const out = capToolOutput(ta, "markdown", buf);
defer if (out.ptr != buf.ptr) ta.free(out);
try std.testing.expect(std.unicode.utf8ValidateSlice(out));
try std.testing.expect(std.mem.indexOf(u8, out, "truncated") != null);
}
test "capToolOutput: extract is exempt from the default cap" {
const ta = std.testing.allocator;
const buf = try ta.alloc(u8, tool_output_max_bytes + 8);
defer ta.free(buf);
@memset(buf, 'a');
const out = capToolOutput(ta, "extract", buf);
try std.testing.expect(out.ptr == buf.ptr);
try std.testing.expectEqual(extract_output_max_bytes, toolOutputCap("extract"));
try std.testing.expectEqual(tool_output_max_bytes, toolOutputCap("html"));
}