diff options
| author | t <t@tjp.lol> | 2026-06-04 09:46:49 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-04 12:09:09 -0600 |
| commit | 3f1ace16afc7877b0bfad374cb286d4d84140960 (patch) | |
| tree | a114a0081e147ef02f0188e408d79199f5e7dcfc /libpanto/src/session.zig | |
| parent | ac5c4898dfa0a9e57424336774893dfc72b132e9 (diff) | |
failure retries scheme
Diffstat (limited to 'libpanto/src/session.zig')
| -rw-r--r-- | libpanto/src/session.zig | 72 |
1 files changed, 69 insertions, 3 deletions
diff --git a/libpanto/src/session.zig b/libpanto/src/session.zig index daa6cfa..0491157 100644 --- a/libpanto/src/session.zig +++ b/libpanto/src/session.zig @@ -208,6 +208,7 @@ pub const DiskResultPart = union(enum) { pub const DiskToolResultBlock = struct { tool_use_id: []const u8, // owned parts: []DiskResultPart, // owned + is_error: bool = false, pub fn deinit(self: DiskToolResultBlock, alloc: Allocator) void { alloc.free(self.tool_use_id); for (self.parts) |p| p.deinit(alloc); @@ -402,6 +403,12 @@ fn writeDiskBlock(s: *std.json.Stringify, block: DiskContentBlock) !void { try s.write("toolResult"); try s.objectField("toolUseId"); try s.write(b.tool_use_id); + // Persist the error marker only when set, so existing + // (success) tool-result logs serialize byte-identically. + if (b.is_error) { + try s.objectField("isError"); + try s.write(true); + } // `parts` is an array of {type:"text",text} and // {type:"image",mimeType,data} (data = inline base64). try s.objectField("parts"); @@ -613,7 +620,9 @@ fn parseDiskBlock(allocator: Allocator, obj: std.json.ObjectMap) ParseError!Disk const tuid = try dupeStringField(allocator, obj, "toolUseId"); errdefer allocator.free(tuid); const parts = try parseDiskResultParts(allocator, obj); - return .{ .tool_result = .{ .tool_use_id = tuid, .parts = parts } }; + // Missing `isError` in older logs defaults to false. + const is_err = readBool(obj, "isError"); + return .{ .tool_result = .{ .tool_use_id = tuid, .parts = parts, .is_error = is_err } }; } else if (std.mem.eql(u8, t, "compactionSummary")) { const text = try dupeStringField(allocator, obj, "text"); return .{ .compaction_summary = .{ .text = text } }; @@ -659,6 +668,12 @@ fn parseDiskResultParts(allocator: Allocator, obj: std.json.ObjectMap) ParseErro return list.toOwnedSlice(allocator); } +fn readBool(obj: std.json.ObjectMap, name: []const u8) bool { + const v = obj.get(name) orelse return false; + if (v != .bool) return false; + return v.bool; +} + fn readU64(obj: std.json.ObjectMap, name: []const u8) u64 { const v = obj.get(name) orelse return 0; if (v != .integer) return 0; @@ -729,7 +744,11 @@ pub fn contentBlockToDisk( }, } } - return .{ .tool_result = .{ .tool_use_id = tuid, .parts = try parts.toOwnedSlice(allocator) } }; + return .{ .tool_result = .{ + .tool_use_id = tuid, + .parts = try parts.toOwnedSlice(allocator), + .is_error = tr.is_error, + } }; }, // A `.System` block becomes a disk text block; its mode rides on // the enclosing `DiskMessage.mode` (set by the session manager), @@ -794,7 +813,7 @@ pub fn diskContentBlockToInternal( }, } } - return .{ .ToolResult = .{ .tool_use_id = tuid, .parts = parts } }; + return .{ .ToolResult = .{ .tool_use_id = tuid, .parts = parts, .is_error = b.is_error } }; }, .compaction_summary => |b| { const tb = try conversation.textualBlockFromSlice(allocator, b.text); @@ -959,6 +978,53 @@ test "serialize/parse tool result message entry" { try testing.expectEqual(@as(usize, 1), got.message.content[0].tool_result.parts.len); try testing.expectEqualStrings("file1.txt\nfile2.txt", got.message.content[0].tool_result.parts[0].text); try testing.expectEqualStrings("anthropic", got.provider.?); + // Unset is_error defaults to false and serializes without the field. + try testing.expect(!got.message.content[0].tool_result.is_error); + try testing.expect(std.mem.indexOf(u8, line, "isError") == null); +} + +test "serialize/parse tool result preserves is_error = true" { + const a = testing.allocator; + + var content = try a.alloc(DiskContentBlock, 1); + var trp = try a.alloc(DiskResultPart, 1); + trp[0] = .{ .text = try dupe(a, "file not found") }; + content[0] = .{ .tool_result = .{ + .tool_use_id = try dupe(a, "tool_err"), + .parts = trp, + .is_error = true, + } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "e1"), + .parent_id = try dupe(a, "e0"), + .timestamp = try dupe(a, "2026-04-25T17:40:18.000Z"), + }, + .provider = try dupe(a, "anthropic"), + .model = try dupe(a, "claude-sonnet-4-20250514"), + .message = .{ .role = .user, .content = content }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + try testing.expect(std.mem.indexOf(u8, line, "\"isError\":true") != null); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + try testing.expect(fe.entry.message.message.content[0].tool_result.is_error); +} + +test "parse tool result without isError defaults to false" { + const a = testing.allocator; + // A legacy line predating the is_error field. + const line = + \\{"type":"message","id":"x","parentId":"y","timestamp":"t","provider":"anthropic","model":"m","message":{"role":"user","content":[{"type":"toolResult","toolUseId":"t1","parts":[{"type":"text","text":"ok"}]}]}} + ; + var fe = try parseLine(a, line); + defer fe.deinit(a); + try testing.expect(!fe.entry.message.message.content[0].tool_result.is_error); } test "serialize/parse tool result with text + image part round-trips" { |
