summaryrefslogtreecommitdiff
path: root/libpanto/src/session.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/session.zig')
-rw-r--r--libpanto/src/session.zig302
1 files changed, 181 insertions, 121 deletions
diff --git a/libpanto/src/session.zig b/libpanto/src/session.zig
index 0491157..ca7046c 100644
--- a/libpanto/src/session.zig
+++ b/libpanto/src/session.zig
@@ -23,6 +23,38 @@ const Allocator = std.mem.Allocator;
const Writer = std.Io.Writer;
const conversation = @import("conversation.zig");
+const config = @import("config.zig");
+
+pub const APIStyle = config.APIStyle;
+pub const ReasoningEffort = config.ReasoningEffort;
+
+/// Wire-format provider identity stamped on a message entry. This is the
+/// ground truth of which endpoint a turn was sent to — never a CLI config
+/// alias, and never any `api_key` material. Recorded on user/assistant
+/// entries; null on system entries.
+pub const WireStamp = struct {
+ api_style: APIStyle,
+ base_url: []const u8, // owned
+ model: []const u8, // owned
+ reasoning: ReasoningEffort = .default,
+
+ pub fn deinit(self: WireStamp, alloc: Allocator) void {
+ alloc.free(self.base_url);
+ alloc.free(self.model);
+ }
+
+ pub fn dupe(self: WireStamp, alloc: Allocator) !WireStamp {
+ const burl = try alloc.dupe(u8, self.base_url);
+ errdefer alloc.free(burl);
+ const mdl = try alloc.dupe(u8, self.model);
+ return .{
+ .api_style = self.api_style,
+ .base_url = burl,
+ .model = mdl,
+ .reasoning = self.reasoning,
+ };
+ }
+};
/// Bumped whenever the on-disk format changes in a way that older readers
/// cannot tolerate. Older files are upgraded by `migrate()` on load and
@@ -83,47 +115,45 @@ pub const SessionEntry = union(enum) {
pub const MessageEntry = struct {
base: EntryBase,
- /// Recorded on user message entries (both human prompts and tool-result
- /// messages). Both are submissions to a provider API; the stamp says
- /// which one. Null on system and assistant entries.
- provider: ?[]const u8 = null, // owned
- model: ?[]const u8 = null, // owned
- message: DiskMessage,
+ /// Wire-format provider identity for this entry. Recorded on user and
+ /// assistant message entries (both are tied to a provider API call);
+ /// null on system entries.
+ stamp: ?WireStamp = null,
+ message: StoredMessage,
pub fn deinit(self: MessageEntry, alloc: Allocator) void {
self.base.deinit(alloc);
- if (self.provider) |p| alloc.free(p);
- if (self.model) |m| alloc.free(m);
+ if (self.stamp) |s| s.deinit(alloc);
self.message.deinit(alloc);
}
};
-pub const DiskMessageRole = enum { system, user, assistant };
+pub const StoredMessageRole = enum { system, user, assistant };
/// Mode for a system-role message. Mirrors `conversation.SystemMode`.
/// `append` adds to the effective prompt; `replace` discards all prior
/// system text. Only meaningful on system messages; absent on disk means
/// `append` (back-compatible with pre-mode logs).
-pub const DiskSystemMode = enum { append, replace };
+pub const StoredSystemMode = enum { append, replace };
-pub const DiskMessage = struct {
- role: DiskMessageRole,
- content: []DiskContentBlock, // owned
+pub const StoredMessage = struct {
+ role: StoredMessageRole,
+ content: []StoredContentBlock, // owned
/// System-message mode. Recorded only for system-role messages; an
/// absent `mode` on disk parses back as `.append`.
- mode: DiskSystemMode = .append,
- // Assistant-only metadata. Null for system/user messages.
- provider: ?[]const u8 = null, // owned
- model: ?[]const u8 = null, // owned
+ mode: StoredSystemMode = .append,
+ /// Assistant-only stop reason. Null for system/user messages.
stop_reason: ?[]const u8 = null, // owned
usage: ?Usage = null,
+ /// Opaque per-message metadata bag (see `conversation.Message.metadata`).
+ /// Round-trips verbatim; `libpanto` never interprets it.
+ metadata: ?[]const u8 = null, // owned
- pub fn deinit(self: DiskMessage, alloc: Allocator) void {
+ pub fn deinit(self: StoredMessage, alloc: Allocator) void {
for (self.content) |block| block.deinit(alloc);
alloc.free(self.content);
- if (self.provider) |p| alloc.free(p);
- if (self.model) |m| alloc.free(m);
if (self.stop_reason) |s| alloc.free(s);
+ if (self.metadata) |m| alloc.free(m);
}
};
@@ -138,14 +168,14 @@ pub const Usage = conversation.Usage;
// Content blocks
// =============================================================================
-pub const DiskContentBlock = union(enum) {
- text: DiskTextBlock,
- thinking: DiskThinkingBlock,
- tool_use: DiskToolUseBlock,
- tool_result: DiskToolResultBlock,
- compaction_summary: DiskCompactionSummaryBlock,
+pub const StoredContentBlock = union(enum) {
+ text: StoredTextBlock,
+ thinking: StoredThinkingBlock,
+ tool_use: StoredToolUseBlock,
+ tool_result: StoredToolResultBlock,
+ compaction_summary: StoredCompactionSummaryBlock,
- pub fn deinit(self: DiskContentBlock, alloc: Allocator) void {
+ pub fn deinit(self: StoredContentBlock, alloc: Allocator) void {
switch (self) {
.text => |b| b.deinit(alloc),
.thinking => |b| b.deinit(alloc),
@@ -156,30 +186,30 @@ pub const DiskContentBlock = union(enum) {
}
};
-pub const DiskTextBlock = struct {
+pub const StoredTextBlock = struct {
text: []const u8, // owned
- pub fn deinit(self: DiskTextBlock, alloc: Allocator) void {
+ pub fn deinit(self: StoredTextBlock, alloc: Allocator) void {
alloc.free(self.text);
}
};
-pub const DiskThinkingBlock = struct {
+pub const StoredThinkingBlock = struct {
thinking: []const u8, // owned
/// Anthropic's opaque integrity token. Other providers do not produce
/// one. Preserved here so resumed sessions can be sent back to
/// Anthropic with the original thinking block intact.
signature: ?[]const u8 = null, // owned
- pub fn deinit(self: DiskThinkingBlock, alloc: Allocator) void {
+ pub fn deinit(self: StoredThinkingBlock, alloc: Allocator) void {
alloc.free(self.thinking);
if (self.signature) |s| alloc.free(s);
}
};
-pub const DiskToolUseBlock = struct {
+pub const StoredToolUseBlock = struct {
id: []const u8, // owned
name: []const u8, // owned
input: []const u8, // raw JSON bytes, owned
- pub fn deinit(self: DiskToolUseBlock, alloc: Allocator) void {
+ pub fn deinit(self: StoredToolUseBlock, alloc: Allocator) void {
alloc.free(self.id);
alloc.free(self.name);
alloc.free(self.input);
@@ -188,13 +218,13 @@ pub const DiskToolUseBlock = struct {
/// One on-disk tool-result part: either text or an inline base64 media
/// attachment (no sidecar files).
-pub const DiskResultPart = union(enum) {
+pub const StoredResultPart = union(enum) {
text: []const u8, // owned
media: struct {
media_type: []const u8, // owned
data: []const u8, // owned (base64)
},
- pub fn deinit(self: DiskResultPart, alloc: Allocator) void {
+ pub fn deinit(self: StoredResultPart, alloc: Allocator) void {
switch (self) {
.text => |t| alloc.free(t),
.media => |m| {
@@ -205,11 +235,11 @@ pub const DiskResultPart = union(enum) {
}
};
-pub const DiskToolResultBlock = struct {
+pub const StoredToolResultBlock = struct {
tool_use_id: []const u8, // owned
- parts: []DiskResultPart, // owned
+ parts: []StoredResultPart, // owned
is_error: bool = false,
- pub fn deinit(self: DiskToolResultBlock, alloc: Allocator) void {
+ pub fn deinit(self: StoredToolResultBlock, alloc: Allocator) void {
alloc.free(self.tool_use_id);
for (self.parts) |p| p.deinit(alloc);
alloc.free(self.parts);
@@ -219,9 +249,9 @@ pub const DiskToolResultBlock = struct {
/// A compaction summary block: the synthetic seed text standing in for a
/// compacted conversation prefix. Sits alone in a `user`-role message. See
/// `conversation.CompactionSummaryBlock`.
-pub const DiskCompactionSummaryBlock = struct {
+pub const StoredCompactionSummaryBlock = struct {
text: []const u8, // owned
- pub fn deinit(self: DiskCompactionSummaryBlock, alloc: Allocator) void {
+ pub fn deinit(self: StoredCompactionSummaryBlock, alloc: Allocator) void {
alloc.free(self.text);
}
};
@@ -294,21 +324,25 @@ fn writeMessageEntry(s: *std.json.Stringify, m: MessageEntry) !void {
if (m.base.parent_id) |p| try s.write(p) else try s.write(null);
try s.objectField("timestamp");
try s.write(m.base.timestamp);
- // Top-level provider/model on user message entries.
- if (m.provider) |p| {
- try s.objectField("provider");
- try s.write(p);
- }
- if (m.model) |mm| {
- try s.objectField("model");
- try s.write(mm);
- }
+ // Wire-format provider identity on user/assistant entries.
+ if (m.stamp) |st| try writeWireStamp(s, st);
try s.objectField("message");
try writeDiskMessage(s, m.message);
try s.endObject();
}
-fn writeDiskMessage(s: *std.json.Stringify, msg: DiskMessage) !void {
+fn writeWireStamp(s: *std.json.Stringify, st: WireStamp) !void {
+ try s.objectField("apiStyle");
+ try s.write(@tagName(st.api_style));
+ try s.objectField("baseUrl");
+ try s.write(st.base_url);
+ try s.objectField("model");
+ try s.write(st.model);
+ try s.objectField("reasoning");
+ try s.write(@tagName(st.reasoning));
+}
+
+fn writeDiskMessage(s: *std.json.Stringify, msg: StoredMessage) !void {
try s.beginObject();
try s.objectField("role");
try s.write(@tagName(msg.role));
@@ -324,18 +358,14 @@ fn writeDiskMessage(s: *std.json.Stringify, msg: DiskMessage) !void {
try writeDiskBlock(s, block);
}
try s.endArray();
- if (msg.provider) |p| {
- try s.objectField("provider");
- try s.write(p);
- }
- if (msg.model) |mm| {
- try s.objectField("model");
- try s.write(mm);
- }
if (msg.stop_reason) |sr| {
try s.objectField("stopReason");
try s.write(sr);
}
+ if (msg.metadata) |md| {
+ try s.objectField("metadata");
+ try s.write(md);
+ }
if (msg.usage) |u| {
try s.objectField("usage");
try s.beginObject();
@@ -363,7 +393,7 @@ fn writeDiskMessage(s: *std.json.Stringify, msg: DiskMessage) !void {
try s.endObject();
}
-fn writeDiskBlock(s: *std.json.Stringify, block: DiskContentBlock) !void {
+fn writeDiskBlock(s: *std.json.Stringify, block: StoredContentBlock) !void {
switch (block) {
.text => |b| {
try s.beginObject();
@@ -518,10 +548,8 @@ fn parseMessageEntry(allocator: Allocator, obj: std.json.ObjectMap) ParseError!M
};
errdefer if (parent_id) |p| allocator.free(p);
- const provider: ?[]const u8 = try dupeOptionalStringField(allocator, obj, "provider");
- errdefer if (provider) |p| allocator.free(p);
- const model: ?[]const u8 = try dupeOptionalStringField(allocator, obj, "model");
- errdefer if (model) |m| allocator.free(m);
+ const stamp = try parseWireStamp(allocator, obj);
+ errdefer if (stamp) |st| st.deinit(allocator);
const msg_v = obj.get("message") orelse return error.MissingField;
if (msg_v != .object) return error.MissingField;
@@ -529,28 +557,45 @@ fn parseMessageEntry(allocator: Allocator, obj: std.json.ObjectMap) ParseError!M
return .{
.base = .{ .id = id, .parent_id = parent_id, .timestamp = timestamp },
- .provider = provider,
- .model = model,
+ .stamp = stamp,
.message = msg,
};
}
-fn parseDiskMessage(allocator: Allocator, obj: std.json.ObjectMap) ParseError!DiskMessage {
+/// Parse the wire-format provider stamp from a message entry object.
+/// Returns null when no `apiStyle` field is present (system entries).
+fn parseWireStamp(allocator: Allocator, obj: std.json.ObjectMap) ParseError!?WireStamp {
+ const style_v = obj.get("apiStyle") orelse return null;
+ if (style_v != .string) return null;
+ const api_style = std.meta.stringToEnum(APIStyle, style_v.string) orelse return error.MissingField;
+ const base_url = try dupeStringField(allocator, obj, "baseUrl");
+ errdefer allocator.free(base_url);
+ const model = try dupeStringField(allocator, obj, "model");
+ errdefer allocator.free(model);
+ const reasoning: ReasoningEffort = blk: {
+ const rv = obj.get("reasoning") orelse break :blk .default;
+ if (rv != .string) break :blk .default;
+ break :blk std.meta.stringToEnum(ReasoningEffort, rv.string) orelse .default;
+ };
+ return .{ .api_style = api_style, .base_url = base_url, .model = model, .reasoning = reasoning };
+}
+
+fn parseDiskMessage(allocator: Allocator, obj: std.json.ObjectMap) ParseError!StoredMessage {
const role_v = obj.get("role") orelse return error.MissingField;
if (role_v != .string) return error.MissingField;
- const role = std.meta.stringToEnum(DiskMessageRole, role_v.string) orelse return error.UnknownRole;
+ const role = std.meta.stringToEnum(StoredMessageRole, role_v.string) orelse return error.UnknownRole;
// `mode` is optional; absent defaults to `.append`. Unknown values are
// tolerated as `.append` rather than rejecting an otherwise-valid log.
- const mode: DiskSystemMode = blk: {
+ const mode: StoredSystemMode = blk: {
const mv = obj.get("mode") orelse break :blk .append;
if (mv != .string) break :blk .append;
- break :blk std.meta.stringToEnum(DiskSystemMode, mv.string) orelse .append;
+ break :blk std.meta.stringToEnum(StoredSystemMode, mv.string) orelse .append;
};
const content_v = obj.get("content") orelse return error.MissingField;
if (content_v != .array) return error.MissingField;
- var content_list = try std.ArrayList(DiskContentBlock).initCapacity(allocator, content_v.array.items.len);
+ var content_list = try std.ArrayList(StoredContentBlock).initCapacity(allocator, content_v.array.items.len);
errdefer {
for (content_list.items) |b| b.deinit(allocator);
content_list.deinit(allocator);
@@ -566,12 +611,10 @@ fn parseDiskMessage(allocator: Allocator, obj: std.json.ObjectMap) ParseError!Di
allocator.free(content);
}
- const provider: ?[]const u8 = try dupeOptionalStringField(allocator, obj, "provider");
- errdefer if (provider) |p| allocator.free(p);
- const model: ?[]const u8 = try dupeOptionalStringField(allocator, obj, "model");
- errdefer if (model) |m| allocator.free(m);
const stop_reason: ?[]const u8 = try dupeOptionalStringField(allocator, obj, "stopReason");
errdefer if (stop_reason) |s| allocator.free(s);
+ const metadata: ?[]const u8 = try dupeOptionalStringField(allocator, obj, "metadata");
+ errdefer if (metadata) |m| allocator.free(m);
var usage: ?Usage = null;
if (obj.get("usage")) |uv| {
@@ -590,14 +633,13 @@ fn parseDiskMessage(allocator: Allocator, obj: std.json.ObjectMap) ParseError!Di
.role = role,
.content = content,
.mode = mode,
- .provider = provider,
- .model = model,
.stop_reason = stop_reason,
.usage = usage,
+ .metadata = metadata,
};
}
-fn parseDiskBlock(allocator: Allocator, obj: std.json.ObjectMap) ParseError!DiskContentBlock {
+fn parseDiskBlock(allocator: Allocator, obj: std.json.ObjectMap) ParseError!StoredContentBlock {
const type_v = obj.get("type") orelse return error.MissingField;
if (type_v != .string) return error.MissingField;
const t = type_v.string;
@@ -634,8 +676,8 @@ fn parseDiskBlock(allocator: Allocator, obj: std.json.ObjectMap) ParseError!Disk
/// Parse the `parts` array of a `toolResult` disk block. Falls back to a
/// legacy single `content` string field (older session logs) -> one text
/// part. Each element is {type:"text",text} or {type:"image",mimeType,data}.
-fn parseDiskResultParts(allocator: Allocator, obj: std.json.ObjectMap) ParseError![]DiskResultPart {
- var list: std.ArrayList(DiskResultPart) = .empty;
+fn parseDiskResultParts(allocator: Allocator, obj: std.json.ObjectMap) ParseError![]StoredResultPart {
+ var list: std.ArrayList(StoredResultPart) = .empty;
errdefer {
for (list.items) |p| p.deinit(allocator);
list.deinit(allocator);
@@ -698,13 +740,13 @@ fn dupeOptionalStringField(allocator: Allocator, obj: std.json.ObjectMap, name:
// Bridge between in-memory and on-disk content blocks
// =============================================================================
-/// Convert an in-memory `ContentBlock` to a `DiskContentBlock`. All strings
+/// Convert an in-memory `ContentBlock` to a `StoredContentBlock`. All strings
/// are duplicated; the source block remains untouched and the resulting
/// disk block is independently owned.
pub fn contentBlockToDisk(
allocator: Allocator,
block: conversation.ContentBlock,
-) !DiskContentBlock {
+) !StoredContentBlock {
switch (block) {
.Text => |tb| {
const text = try allocator.dupe(u8, tb.items);
@@ -727,7 +769,7 @@ pub fn contentBlockToDisk(
.ToolResult => |tr| {
const tuid = try allocator.dupe(u8, tr.tool_use_id);
errdefer allocator.free(tuid);
- var parts: std.ArrayList(DiskResultPart) = .empty;
+ var parts: std.ArrayList(StoredResultPart) = .empty;
errdefer {
for (parts.items) |p| p.deinit(allocator);
parts.deinit(allocator);
@@ -751,7 +793,7 @@ pub fn contentBlockToDisk(
} };
},
// A `.System` block becomes a disk text block; its mode rides on
- // the enclosing `DiskMessage.mode` (set by the session manager),
+ // the enclosing `StoredMessage.mode` (set by the session manager),
// not on the block itself.
.System => |sb| {
const text = try allocator.dupe(u8, sb.text.items);
@@ -764,12 +806,12 @@ pub fn contentBlockToDisk(
}
}
-/// Convert a `DiskContentBlock` to an in-memory `ContentBlock`. Allocates
+/// Convert a `StoredContentBlock` to an in-memory `ContentBlock`. Allocates
/// fresh owned buffers for every string field. The returned block is
/// independently owned.
pub fn diskContentBlockToInternal(
allocator: Allocator,
- block: DiskContentBlock,
+ block: StoredContentBlock,
) !conversation.ContentBlock {
switch (block) {
.text => |b| {
@@ -856,7 +898,7 @@ test "serialize/parse header round-trip" {
test "serialize/parse user message entry round-trip (with provider/model stamp)" {
const a = testing.allocator;
- var content = try a.alloc(DiskContentBlock, 1);
+ var content = try a.alloc(StoredContentBlock, 1);
content[0] = .{ .text = .{ .text = try dupe(a, "hello world") } };
const entry: SessionEntry = .{ .message = .{
@@ -865,8 +907,12 @@ test "serialize/parse user message entry round-trip (with provider/model stamp)"
.parent_id = try dupe(a, "00000000"),
.timestamp = try dupe(a, "2026-04-25T17:40:16.000Z"),
},
- .provider = try dupe(a, "openai"),
- .model = try dupe(a, "gpt-4o"),
+ .stamp = .{
+ .api_style = .openai_chat,
+ .base_url = try dupe(a, "https://api.openai.com/v1"),
+ .model = try dupe(a, "gpt-4o"),
+ .reasoning = .high,
+ },
.message = .{
.role = .user,
.content = content,
@@ -883,9 +929,11 @@ test "serialize/parse user message entry round-trip (with provider/model stamp)"
const got = fe.entry.message;
try testing.expectEqualStrings("a1b2c3d4", got.base.id);
try testing.expectEqualStrings("00000000", got.base.parent_id.?);
- try testing.expectEqualStrings("openai", got.provider.?);
- try testing.expectEqualStrings("gpt-4o", got.model.?);
- try testing.expectEqual(DiskMessageRole.user, got.message.role);
+ try testing.expectEqual(APIStyle.openai_chat, got.stamp.?.api_style);
+ try testing.expectEqualStrings("https://api.openai.com/v1", got.stamp.?.base_url);
+ try testing.expectEqualStrings("gpt-4o", got.stamp.?.model);
+ try testing.expectEqual(ReasoningEffort.high, got.stamp.?.reasoning);
+ try testing.expectEqual(StoredMessageRole.user, got.message.role);
try testing.expectEqual(@as(usize, 1), got.message.content.len);
try testing.expectEqualStrings("hello world", got.message.content[0].text.text);
}
@@ -893,7 +941,7 @@ test "serialize/parse user message entry round-trip (with provider/model stamp)"
test "serialize/parse assistant message entry with metadata" {
const a = testing.allocator;
- var content = try a.alloc(DiskContentBlock, 3);
+ var content = try a.alloc(StoredContentBlock, 3);
content[0] = .{ .thinking = .{
.thinking = try dupe(a, "let me think"),
.signature = try dupe(a, "sig-xyz"),
@@ -911,13 +959,17 @@ test "serialize/parse assistant message entry with metadata" {
.parent_id = try dupe(a, "a1b2c3d4"),
.timestamp = try dupe(a, "2026-04-25T17:40:17.000Z"),
},
+ .stamp = .{
+ .api_style = .anthropic_messages,
+ .base_url = try dupe(a, "https://api.anthropic.com"),
+ .model = try dupe(a, "claude-sonnet-4-20250514"),
+ },
.message = .{
.role = .assistant,
.content = content,
- .provider = try dupe(a, "anthropic"),
- .model = try dupe(a, "claude-sonnet-4-20250514"),
.stop_reason = try dupe(a, "toolUse"),
.usage = .{ .input = 1500, .output = 85 },
+ .metadata = try dupe(a, "{\"k\":1}"),
},
} };
defer entry.deinit(a);
@@ -928,14 +980,15 @@ test "serialize/parse assistant message entry with metadata" {
var fe = try parseLine(a, line);
defer fe.deinit(a);
const got = fe.entry.message;
- try testing.expectEqual(DiskMessageRole.assistant, got.message.role);
+ try testing.expectEqual(StoredMessageRole.assistant, got.message.role);
try testing.expectEqual(@as(usize, 3), got.message.content.len);
try testing.expectEqualStrings("let me think", got.message.content[0].thinking.thinking);
try testing.expectEqualStrings("sig-xyz", got.message.content[0].thinking.signature.?);
try testing.expectEqualStrings("bash", got.message.content[2].tool_use.name);
try testing.expectEqualStrings("{\"command\":\"ls\"}", got.message.content[2].tool_use.input);
- try testing.expectEqualStrings("anthropic", got.message.provider.?);
+ try testing.expectEqualStrings("anthropic", @tagName(got.stamp.?.api_style)[0..9]);
try testing.expectEqualStrings("toolUse", got.message.stop_reason.?);
+ try testing.expectEqualStrings("{\"k\":1}", got.message.metadata.?);
try testing.expect(got.message.usage != null);
try testing.expectEqual(@as(u64, 1500), got.message.usage.?.input);
try testing.expectEqual(@as(u64, 85), got.message.usage.?.output);
@@ -944,8 +997,8 @@ test "serialize/parse assistant message entry with metadata" {
test "serialize/parse tool result message entry" {
const a = testing.allocator;
- var content = try a.alloc(DiskContentBlock, 1);
- var trp = try a.alloc(DiskResultPart, 1);
+ var content = try a.alloc(StoredContentBlock, 1);
+ var trp = try a.alloc(StoredResultPart, 1);
trp[0] = .{ .text = try dupe(a, "file1.txt\nfile2.txt") };
content[0] = .{ .tool_result = .{
.tool_use_id = try dupe(a, "tool_abc"),
@@ -958,8 +1011,11 @@ test "serialize/parse tool result message entry" {
.parent_id = try dupe(a, "b2c3d4e5"),
.timestamp = try dupe(a, "2026-04-25T17:40:18.000Z"),
},
- .provider = try dupe(a, "anthropic"),
- .model = try dupe(a, "claude-sonnet-4-20250514"),
+ .stamp = .{
+ .api_style = .anthropic_messages,
+ .base_url = try dupe(a, "https://api.anthropic.com"),
+ .model = try dupe(a, "claude-sonnet-4-20250514"),
+ },
.message = .{
.role = .user,
.content = content,
@@ -973,11 +1029,11 @@ test "serialize/parse tool result message entry" {
var fe = try parseLine(a, line);
defer fe.deinit(a);
const got = fe.entry.message;
- try testing.expectEqual(DiskMessageRole.user, got.message.role);
+ try testing.expectEqual(StoredMessageRole.user, got.message.role);
try testing.expectEqualStrings("tool_abc", got.message.content[0].tool_result.tool_use_id);
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.?);
+ try testing.expectEqual(APIStyle.anthropic_messages, got.stamp.?.api_style);
// 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);
@@ -986,8 +1042,8 @@ test "serialize/parse tool result message entry" {
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);
+ var content = try a.alloc(StoredContentBlock, 1);
+ var trp = try a.alloc(StoredResultPart, 1);
trp[0] = .{ .text = try dupe(a, "file not found") };
content[0] = .{ .tool_result = .{
.tool_use_id = try dupe(a, "tool_err"),
@@ -1001,8 +1057,11 @@ test "serialize/parse tool result preserves is_error = true" {
.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"),
+ .stamp = .{
+ .api_style = .anthropic_messages,
+ .base_url = try dupe(a, "https://api.anthropic.com"),
+ .model = try dupe(a, "claude-sonnet-4-20250514"),
+ },
.message = .{ .role = .user, .content = content },
} };
defer entry.deinit(a);
@@ -1030,8 +1089,8 @@ test "parse tool result without isError defaults to false" {
test "serialize/parse tool result with text + image part round-trips" {
const a = testing.allocator;
- var content = try a.alloc(DiskContentBlock, 1);
- var trp = try a.alloc(DiskResultPart, 2);
+ var content = try a.alloc(StoredContentBlock, 1);
+ var trp = try a.alloc(StoredResultPart, 2);
trp[0] = .{ .text = try dupe(a, "here is the image") };
trp[1] = .{ .media = .{
.media_type = try dupe(a, "image/png"),
@@ -1048,8 +1107,11 @@ test "serialize/parse tool result with text + image part round-trips" {
.parent_id = try dupe(a, "img00000"),
.timestamp = try dupe(a, "2026-04-25T17:40:18.000Z"),
},
- .provider = try dupe(a, "anthropic"),
- .model = try dupe(a, "claude-sonnet-4-20250514"),
+ .stamp = .{
+ .api_style = .anthropic_messages,
+ .base_url = try dupe(a, "https://api.anthropic.com"),
+ .model = try dupe(a, "claude-sonnet-4-20250514"),
+ },
.message = .{ .role = .user, .content = content },
} };
defer entry.deinit(a);
@@ -1072,7 +1134,7 @@ test "system message mode round-trips; absent mode defaults to append" {
// replace-mode system entry round-trips.
{
- var content = try a.alloc(DiskContentBlock, 1);
+ var content = try a.alloc(StoredContentBlock, 1);
content[0] = .{ .text = .{ .text = try dupe(a, "fresh seed") } };
const entry: SessionEntry = .{ .message = .{
.base = .{
@@ -1094,7 +1156,7 @@ test "system message mode round-trips; absent mode defaults to append" {
var fe = try parseLine(a, line);
defer fe.deinit(a);
- try testing.expectEqual(DiskSystemMode.replace, fe.entry.message.message.mode);
+ try testing.expectEqual(StoredSystemMode.replace, fe.entry.message.message.mode);
}
// A legacy system entry with no `mode` parses back as append.
@@ -1104,7 +1166,7 @@ test "system message mode round-trips; absent mode defaults to append" {
;
var fe = try parseLine(a, line);
defer fe.deinit(a);
- try testing.expectEqual(DiskSystemMode.append, fe.entry.message.message.mode);
+ try testing.expectEqual(StoredSystemMode.append, fe.entry.message.message.mode);
}
}
@@ -1147,7 +1209,7 @@ test "contentBlockToDisk: Text round-trips via in-memory" {
test "diskContentBlockToInternal: ToolUse preserves id/name/input" {
const a = testing.allocator;
- const disk: DiskContentBlock = .{ .tool_use = .{
+ const disk: StoredContentBlock = .{ .tool_use = .{
.id = try a.dupe(u8, "tu_1"),
.name = try a.dupe(u8, "bash"),
.input = try a.dupe(u8, "{\"command\":\"ls\"}"),
@@ -1164,7 +1226,7 @@ test "diskContentBlockToInternal: ToolUse preserves id/name/input" {
test "Usage: all five fields round-trip; zero-valued fields omitted from JSON" {
const a = testing.allocator;
- var content = try a.alloc(DiskContentBlock, 1);
+ var content = try a.alloc(StoredContentBlock, 1);
content[0] = .{ .text = .{ .text = try dupe(a, "hi") } };
const entry: SessionEntry = .{ .message = .{
@@ -1176,8 +1238,6 @@ test "Usage: all five fields round-trip; zero-valued fields omitted from JSON" {
.message = .{
.role = .assistant,
.content = content,
- .provider = try dupe(a, "anthropic"),
- .model = try dupe(a, "claude-sonnet-4-20250514"),
.stop_reason = try dupe(a, "stop"),
.usage = .{
.input = 100,
@@ -1213,7 +1273,7 @@ test "Usage: all five fields round-trip; zero-valued fields omitted from JSON" {
test "Usage: zero-valued auxiliary fields are omitted but parse back as 0" {
const a = testing.allocator;
- var content = try a.alloc(DiskContentBlock, 1);
+ var content = try a.alloc(StoredContentBlock, 1);
content[0] = .{ .text = .{ .text = try dupe(a, "hi") } };
const entry: SessionEntry = .{ .message = .{
@@ -1248,7 +1308,7 @@ test "Usage: zero-valued auxiliary fields are omitted but parse back as 0" {
test "diskContentBlockToInternal: Thinking preserves signature" {
const a = testing.allocator;
- const disk: DiskContentBlock = .{ .thinking = .{
+ const disk: StoredContentBlock = .{ .thinking = .{
.thinking = try a.dupe(u8, "reasoning..."),
.signature = try a.dupe(u8, "sig123"),
} };
@@ -1263,7 +1323,7 @@ test "diskContentBlockToInternal: Thinking preserves signature" {
test "compactionSummary block round-trips through serialize/parse" {
const a = testing.allocator;
- var content = try a.alloc(DiskContentBlock, 1);
+ var content = try a.alloc(StoredContentBlock, 1);
content[0] = .{ .compaction_summary = .{ .text = try dupe(a, "earlier history summary") } };
const entry: SessionEntry = .{ .message = .{
@@ -1283,7 +1343,7 @@ test "compactionSummary block round-trips through serialize/parse" {
var fe = try parseLine(a, line);
defer fe.deinit(a);
const got = fe.entry.message;
- try testing.expectEqual(DiskMessageRole.user, got.message.role);
+ try testing.expectEqual(StoredMessageRole.user, got.message.role);
try testing.expectEqualStrings("earlier history summary", got.message.content[0].compaction_summary.text);
}