const std = @import("std"); const debug_trace = @import("../shared/debug_trace.zig"); const text_utils = @import("../shared/text_utils.zig"); const types = @import("../shared/types.zig"); const Allocator = std.mem.Allocator; pub const default_max_tool_result_bytes: usize = 64 * 1024; pub const min_configured_tool_result_bytes: usize = 1024; pub fn resolveMaxToolResultBytes(setting: ?usize, default_value: usize) usize { return setting orelse default_value; } pub fn prepareModelOutput( alloc: Allocator, tool_name: []const u8, raw: []const u8, max_bytes: usize, ) error{OutOfMemory}![]const u8 { var scratch_impl = std.heap.ArenaAllocator.init(alloc); defer scratch_impl.deinit(); const scratch = scratch_impl.allocator(); const sanitized = try text_utils.sanitizeModelText(scratch, raw); const masked = text_utils.maskSecrets(scratch, sanitized) catch |err| switch (err) { error.OutOfMemory, error.WriteFailed => return error.OutOfMemory, }; const capped = try truncateText(scratch, .{ .text = masked, .max_bytes = max_bytes, .marker = try std.fmt.allocPrint( scratch, "\n... [tool result truncated for {s}: original {d} bytes; cap is {d} bytes]\n", .{ tool_name, masked.len, max_bytes }, ), .trace_scope = "tool", .trace_label = tool_name, }); return try alloc.dupe(u8, capped); } pub const PreparedInlineResult = struct { model_output: []u8, memory: types.ToolResultMemory, }; pub fn prepareInlineResult( alloc: Allocator, tool_name: []const u8, raw_output: []const u8, max_bytes: usize, ) error{OutOfMemory}!PreparedInlineResult { const model_output = @constCast(try prepareModelOutput( alloc, tool_name, raw_output, max_bytes, )); return .{ .model_output = model_output, .memory = .{ .output_handle = null, .preview = null, .output_bytes = raw_output.len, .stored_output_bytes = model_output.len, .truncated = model_output.len < raw_output.len, }, }; } pub const TruncateOptions = struct { text: []const u8, max_bytes: usize, marker: []const u8, trace_scope: []const u8 = "tool", trace_label: []const u8 = "result", }; pub fn truncateText(arena: std.mem.Allocator, opts: TruncateOptions) ![]const u8 { if (opts.text.len <= opts.max_bytes) return opts.text; const prefix_cap = if (opts.max_bytes > opts.marker.len) opts.max_bytes - opts.marker.len else 0; const prefix_len = text_utils.utf8BackwardBoundary(opts.text, prefix_cap); debug_trace.logf( opts.trace_scope, "model-facing tool result truncated label={s} original_bytes={d} cap_bytes={d}", .{ opts.trace_label, opts.text.len, opts.max_bytes }, ); if (prefix_len == 0) return try arena.dupe(u8, opts.marker); return try std.mem.concat(arena, u8, &.{ opts.text[0..prefix_len], opts.marker }); } test "prepareModelOutput masks secrets before applying cap" { const alloc = std.testing.allocator; const output = try prepareModelOutput(alloc, "mcp__server__tool", "token=secret-value", default_max_tool_result_bytes); defer alloc.free(@constCast(output)); try std.testing.expectEqualStrings("token=[redacted]", output); } test "prepareModelOutput masks quoted sensitive assignments" { const alloc = std.testing.allocator; const output = try prepareModelOutput(alloc, "run_command", "API_KEY=\"secret-value\"", default_max_tool_result_bytes); defer alloc.free(@constCast(output)); try std.testing.expectEqualStrings("API_KEY=\"[redacted]\"", output); } test "prepareModelOutput caps chatty output with explicit marker" { const alloc = std.testing.allocator; var bytes = [_]u8{'x'} ** 256; const output = try prepareModelOutput(alloc, "grep_files", bytes[0..], 128); defer alloc.free(@constCast(output)); try std.testing.expect(output.len <= 128); try std.testing.expect(std.mem.find(u8, output, "... [tool result truncated for grep_files: original 256 bytes; cap is 128 bytes]") != null); } test "prepareModelOutput keeps complete codepoints at the cap" { const alloc = std.testing.allocator; const text = "x" ++ ("\xc3\xa9" ** 300); for ([_]usize{ 128, 129 }) |cap| { const output = try prepareModelOutput(alloc, "grep_files", text, cap); defer alloc.free(@constCast(output)); try std.testing.expect(output.len <= cap); const marker_start = std.mem.find(u8, output, "\n... [tool result truncated").?; const prefix = output[0..marker_start]; try std.testing.expect(std.unicode.utf8ValidateSlice(prefix)); try std.testing.expect(std.mem.endsWith(u8, prefix, "\xc3\xa9")); } }