diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index fcd62388a..888c4bb71 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -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")); +}