summaryrefslogtreecommitdiff
path: root/libpanto/src/compaction.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/compaction.zig')
-rw-r--r--libpanto/src/compaction.zig64
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 },