diff options
| author | t <t@tjp.lol> | 2026-06-03 09:11:36 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-03 12:34:13 -0600 |
| commit | ac5c4898dfa0a9e57424336774893dfc72b132e9 (patch) | |
| tree | 03eb8e32a001845dbaec163dfd4905ffc23d9fa6 /libpanto/src/compaction.zig | |
| parent | 2416a308c0e467f8dbdbe09942aa903bca751c0e (diff) | |
image uploads
Diffstat (limited to 'libpanto/src/compaction.zig')
| -rw-r--r-- | libpanto/src/compaction.zig | 64 |
1 files changed, 46 insertions, 18 deletions
diff --git a/libpanto/src/compaction.zig b/libpanto/src/compaction.zig index 254b876..3a1c012 100644 --- a/libpanto/src/compaction.zig +++ b/libpanto/src/compaction.zig @@ -146,19 +146,33 @@ fn priorAssistantUsage( fn countWords(msg: Message) u64 { var total: u64 = 0; for (msg.content.items) |block| { - const text: []const u8 = switch (block) { - .Text => |b| b.items, - .Thinking => |b| b.text.items, - .ToolUse => |b| b.input.items, - .ToolResult => |b| b.content.items, - .System => |b| b.text.items, - .CompactionSummary => |b| b.text.items, - }; - total += countWordsIn(text); + switch (block) { + .Text => |b| total += countWordsIn(b.items), + .Thinking => |b| total += countWordsIn(b.text.items), + .ToolUse => |b| total += countWordsIn(b.input.items), + .ToolResult => |b| { + // Text parts count by words. Media parts (base64 image / + // PDF blobs) would wildly distort the retention window if + // counted by length, so each is charged a fixed estimate + // (matching the pi reference impl's ~4800-char image cost, + // divided back out to words via `word_to_token_factor`). + for (b.parts.items) |p| switch (p) { + .text => |t| total += countWordsIn(t.items), + .media => total += media_part_word_estimate, + }; + }, + .System => |b| total += countWordsIn(b.text.items), + .CompactionSummary => |b| total += countWordsIn(b.text.items), + } } return total; } +/// Fixed per-media-part word estimate. The pi reference impl sizes each +/// inline image at ~4800 characters for compaction token math; expressed +/// as words (≈4800/6 chars-per-word) this lands near 800. +const media_part_word_estimate: u64 = 800; + fn countWordsIn(text: []const u8) u64 { var count: u64 = 0; var in_word = false; @@ -351,8 +365,18 @@ pub fn serializeTranscript( try w.appendSlice(allocator, line); }, .ToolResult => |b| { + var body: conversation.TextualBlock = .empty; + defer body.deinit(allocator); + for (b.parts.items) |p| switch (p) { + .text => |t| try body.appendSlice(allocator, t.items), + .media => |m| { + const note = try std.fmt.allocPrint(allocator, "[{s} attachment]", .{m.media_type}); + defer allocator.free(note); + try body.appendSlice(allocator, note); + }, + }; const line = try std.fmt.allocPrint(allocator, "[Tool result ({s})]: {s}", .{ - b.tool_use_id, b.content.items, + b.tool_use_id, body.items, }); defer allocator.free(line); try w.appendSlice(allocator, line); @@ -436,6 +460,16 @@ fn freeMsgs(alloc: Allocator, msgs: []Message) void { for (msgs) |*m| m.deinit(alloc); } +/// Build a `.ToolResult` content block with a single text part. +fn textToolResult(alloc: Allocator, id: []const u8, text: []const u8) !conversation.ContentBlock { + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(alloc, .{ .text = try conversation.textualBlockFromSlice(alloc, text) }); + return .{ .ToolResult = .{ + .tool_use_id = try alloc.dupe(u8, id), + .parts = parts, + } }; +} + test "countWordsIn - basic word counting" { try testing.expectEqual(@as(u64, 0), countWordsIn("")); try testing.expectEqual(@as(u64, 0), countWordsIn(" \n\t ")); @@ -530,10 +564,7 @@ test "computeSplit - tool-result user messages attach to the current turn" { // Turn 1: user q1 -> assistant(tool_use) -> user(tool_result) -> assistant a1 var tr_content: std.ArrayList(conversation.ContentBlock) = .empty; - try tr_content.append(a, .{ .ToolResult = .{ - .tool_use_id = try a.dupe(u8, "t1"), - .content = try conversation.textualBlockFromSlice(a, "result"), - } }); + try tr_content.append(a, try textToolResult(a, "t1", "result")); var msgs = [_]Message{ try userMsg(a, "q1"), @@ -689,10 +720,7 @@ test "serializeTranscript - tool call and result framing" { .input = try conversation.textualBlockFromSlice(a, "{\"cmd\":\"ls\"}"), } }); var tr_content: std.ArrayList(conversation.ContentBlock) = .empty; - try tr_content.append(a, .{ .ToolResult = .{ - .tool_use_id = try a.dupe(u8, "tc1"), - .content = try conversation.textualBlockFromSlice(a, "file.txt"), - } }); + try tr_content.append(a, try textToolResult(a, "tc1", "file.txt")); var msgs = [_]Message{ .{ .role = .assistant, .content = tu_content }, .{ .role = .user, .content = tr_content }, |
