summaryrefslogtreecommitdiff
path: root/libpanto
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto')
-rw-r--r--libpanto/src/anthropic_messages_json.zig81
-rw-r--r--libpanto/src/conversation.zig162
-rw-r--r--libpanto/src/openai_chat_json.zig79
-rw-r--r--libpanto/src/session.zig72
-rw-r--r--libpanto/src/session_manager.zig16
5 files changed, 396 insertions, 14 deletions
diff --git a/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig
index 5b65c2b..f8bfc2e 100644
--- a/libpanto/src/anthropic_messages_json.zig
+++ b/libpanto/src/anthropic_messages_json.zig
@@ -111,20 +111,26 @@ fn writeRawJson(s: *std.json.Stringify, raw: []const u8) !void {
try s.write(parsed.value);
}
+/// Build the top-level Anthropic `system` string. Applies the shared
+/// append/replace derivation (see `conversation.effectiveSystemBlocks`),
+/// strips trailing newlines from each surviving block, and joins them with
+/// a horizontal rule (`\n\n---\n\n`). Anthropic's wire format requires a
+/// single string, so this rule is its concession to that constraint.
fn collectSystemPrompt(
conv: *const conversation.Conversation,
out: *std.ArrayList(u8),
allocator: Allocator,
) !void {
+ var blocks = try conversation.effectiveSystemBlocks(allocator, conv.messages.items);
+ defer blocks.deinit(allocator);
+
+ const sep = "\n\n---\n\n";
var first = true;
- for (conv.messages.items) |msg| {
- if (msg.role != .system) continue;
- for (msg.content.items) |block| {
- if (block != .Text) continue;
- if (!first) try out.append(allocator, '\n');
- try out.appendSlice(allocator, block.Text.items);
- first = false;
- }
+ for (blocks.items) |text| {
+ const trimmed = std.mem.trimEnd(u8, text, "\n");
+ if (!first) try out.appendSlice(allocator, sep);
+ try out.appendSlice(allocator, trimmed);
+ first = false;
}
}
@@ -208,6 +214,11 @@ fn writeBlock(s: *std.json.Stringify, block: conversation.ContentBlock) !void {
try s.write(tr.content.items);
try s.endObject();
},
+ // System blocks never reach here: `serializeRequest` filters out
+ // `.system` messages before emitting the `messages` array, and the
+ // system text is hoisted into the top-level `system` string by
+ // `collectSystemPrompt`. Present only to keep the switch exhaustive.
+ .System => {},
}
}
@@ -526,7 +537,7 @@ test "serializeRequest - system extracted into top-level field" {
try testing.expectEqualStrings("Hello!", content[0].object.get("text").?.string);
}
-test "serializeRequest - multiple system messages concatenated with newlines" {
+test "serializeRequest - multiple system messages joined with horizontal rule" {
const allocator = testing.allocator;
var conv = conversation.Conversation.init(allocator);
@@ -545,7 +556,57 @@ test "serializeRequest - multiple system messages concatenated with newlines" {
var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{});
defer parsed.deinit();
try testing.expectEqualStrings(
- "Be terse.\nBe accurate.",
+ "Be terse.\n\n---\n\nBe accurate.",
+ parsed.value.object.get("system").?.string,
+ );
+}
+
+test "serializeRequest - replace-mode system block wipes prior system text" {
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addSystemMessage("original seed");
+ try conv.addSystemMessage("original append");
+ try conv.replaceSystemMessage("fresh seed");
+ try conv.addSystemMessage("fresh append");
+ try conv.addUserMessage("Hi");
+
+ const cfg = testConfig("claude-x");
+ var empty_tools = tool_registry_mod.ToolRegistry.init(allocator);
+ defer empty_tools.deinit();
+ const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools);
+ defer allocator.free(body);
+
+ var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{});
+ defer parsed.deinit();
+ try testing.expectEqualStrings(
+ "fresh seed\n\n---\n\nfresh append",
+ parsed.value.object.get("system").?.string,
+ );
+}
+
+test "serializeRequest - trailing newlines stripped before the rule join" {
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addSystemMessage("line one\n\n");
+ try conv.addSystemMessage("line two\n");
+ try conv.addUserMessage("Hi");
+
+ const cfg = testConfig("claude-x");
+ var empty_tools = tool_registry_mod.ToolRegistry.init(allocator);
+ defer empty_tools.deinit();
+ const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools);
+ defer allocator.free(body);
+
+ var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{});
+ defer parsed.deinit();
+ try testing.expectEqualStrings(
+ "line one\n\n---\n\nline two",
parsed.value.object.get("system").?.string,
);
}
diff --git a/libpanto/src/conversation.zig b/libpanto/src/conversation.zig
index 6776412..f145d7f 100644
--- a/libpanto/src/conversation.zig
+++ b/libpanto/src/conversation.zig
@@ -51,11 +51,29 @@ pub const ToolResultBlock = struct {
}
};
+/// How a `.system` content block combines with the system text collected
+/// before it. `append` adds to the running effective prompt; `replace`
+/// discards everything collected so far and starts fresh from this block.
+pub const SystemMode = enum { append, replace };
+
+/// A system-prompt content block. System prompts remain `.system`-role
+/// messages; this block records the mode that governs how its text folds
+/// into the effective system prompt (see `effectiveSystemBlocks`).
+pub const SystemBlock = struct {
+ text: TextualBlock = .empty,
+ mode: SystemMode = .append,
+
+ pub fn deinit(self: *SystemBlock, alloc: Allocator) void {
+ self.text.deinit(alloc);
+ }
+};
+
pub const ContentBlock = union(enum) {
Text: TextualBlock,
Thinking: ThinkingBlock,
ToolUse: ToolUseBlock,
ToolResult: ToolResultBlock,
+ System: SystemBlock,
pub fn deinit(self: *ContentBlock, alloc: Allocator) void {
switch (self.*) {
@@ -63,6 +81,7 @@ pub const ContentBlock = union(enum) {
.Thinking => |*b| b.deinit(alloc),
.ToolUse => |*b| b.deinit(alloc),
.ToolResult => |*b| b.deinit(alloc),
+ .System => |*b| b.deinit(alloc),
}
}
};
@@ -95,10 +114,30 @@ pub const Conversation = struct {
};
}
+ /// Append a system message in `append` mode. Adds to the effective
+ /// system prompt. (Back-compatible: same external behavior as before
+ /// the `.System` block existed.)
pub fn addSystemMessage(self: *Conversation, text: []const u8) !void {
+ return self.appendSystemBlock(text, .append);
+ }
+
+ /// Append a system message in `replace` mode. When the effective
+ /// prompt is rebuilt (see `effectiveSystemBlocks`), this discards all
+ /// prior system text and starts fresh.
+ pub fn replaceSystemMessage(self: *Conversation, text: []const u8) !void {
+ return self.appendSystemBlock(text, .replace);
+ }
+
+ /// Append a `.system`-role message whose single content block is a
+ /// `.System` block carrying `mode`.
+ fn appendSystemBlock(self: *Conversation, text: []const u8, mode: SystemMode) !void {
const tb = try textualBlockFromSlice(self.allocator, text);
var content: std.ArrayList(ContentBlock) = .empty;
- try content.append(self.allocator, .{ .Text = tb });
+ errdefer {
+ for (content.items) |*b| b.deinit(self.allocator);
+ content.deinit(self.allocator);
+ }
+ try content.append(self.allocator, .{ .System = .{ .text = tb, .mode = mode } });
try self.messages.append(self.allocator, .{
.role = .system,
.content = content,
@@ -137,6 +176,50 @@ pub const Conversation = struct {
}
};
+/// Derive the effective ordered list of system-text blocks from a slice of
+/// messages. This is the single shared rule that governs both provider
+/// serialization and session rebuild.
+///
+/// Walk the messages in order; for each `.system` message's `.System`
+/// block:
+/// - `append`: add the block's text to the running list.
+/// - `replace`: clear the running list, then add this block's text.
+///
+/// The returned slices are **borrowed** from `messages` — valid only as
+/// long as the underlying conversation is unmodified. The caller owns the
+/// returned `ArrayList` itself and must `deinit` it (this frees the slice
+/// storage, not the borrowed text).
+///
+/// Running this walk over a *prefix* of the messages reconstructs the
+/// effective prompt as of that point — the `/tree` faithfulness property.
+pub fn effectiveSystemBlocks(
+ alloc: Allocator,
+ messages: []const Message,
+) !std.ArrayList([]const u8) {
+ var out: std.ArrayList([]const u8) = .empty;
+ errdefer out.deinit(alloc);
+ for (messages) |msg| {
+ if (msg.role != .system) continue;
+ for (msg.content.items) |block| {
+ switch (block) {
+ .System => |sb| {
+ switch (sb.mode) {
+ .append => {},
+ .replace => out.clearRetainingCapacity(),
+ }
+ try out.append(alloc, sb.text.items);
+ },
+ // Be tolerant of plain `.Text` blocks on a system message
+ // (e.g. hand-built test conversations): treat them as
+ // append-mode text.
+ .Text => |tb| try out.append(alloc, tb.items),
+ else => {},
+ }
+ }
+ }
+ return out;
+}
+
test "Conversation - add messages and verify content" {
const allocator = std.testing.allocator;
@@ -154,7 +237,11 @@ test "Conversation - add messages and verify content" {
try std.testing.expectEqual(MessageRole.system, conv.messages.items[0].role);
try std.testing.expectEqualStrings(
"You are a helpful assistant.",
- conv.messages.items[0].content.items[0].Text.items,
+ conv.messages.items[0].content.items[0].System.text.items,
+ );
+ try std.testing.expectEqual(
+ SystemMode.append,
+ conv.messages.items[0].content.items[0].System.mode,
);
try std.testing.expectEqual(MessageRole.user, conv.messages.items[1].role);
@@ -202,3 +289,74 @@ test "ContentBlock - Thinking variant" {
try std.testing.expectEqualStrings("hmm...", conv.messages.items[0].content.items[0].Thinking.text.items);
try std.testing.expectEqualStrings("answer", conv.messages.items[0].content.items[1].Text.items);
}
+
+test "System block - addSystemMessage records append mode, replaceSystemMessage records replace mode" {
+ const allocator = std.testing.allocator;
+
+ var conv = Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addSystemMessage("base");
+ try conv.replaceSystemMessage("fresh");
+
+ try std.testing.expectEqual(SystemMode.append, conv.messages.items[0].content.items[0].System.mode);
+ try std.testing.expectEqualStrings("base", conv.messages.items[0].content.items[0].System.text.items);
+ try std.testing.expectEqual(SystemMode.replace, conv.messages.items[1].content.items[0].System.mode);
+ try std.testing.expectEqualStrings("fresh", conv.messages.items[1].content.items[0].System.text.items);
+}
+
+test "effectiveSystemBlocks - append accumulates in order" {
+ const allocator = std.testing.allocator;
+
+ var conv = Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addSystemMessage("a");
+ try conv.addSystemMessage("b");
+ try conv.addUserMessage("hi");
+ try conv.addSystemMessage("c");
+
+ var blocks = try effectiveSystemBlocks(allocator, conv.messages.items);
+ defer blocks.deinit(allocator);
+
+ try std.testing.expectEqual(@as(usize, 3), blocks.items.len);
+ try std.testing.expectEqualStrings("a", blocks.items[0]);
+ try std.testing.expectEqualStrings("b", blocks.items[1]);
+ try std.testing.expectEqualStrings("c", blocks.items[2]);
+}
+
+test "effectiveSystemBlocks - replace wipes everything collected so far" {
+ const allocator = std.testing.allocator;
+
+ var conv = Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addSystemMessage("a");
+ try conv.addSystemMessage("b");
+ try conv.replaceSystemMessage("fresh");
+ try conv.addSystemMessage("after");
+
+ var blocks = try effectiveSystemBlocks(allocator, conv.messages.items);
+ defer blocks.deinit(allocator);
+
+ try std.testing.expectEqual(@as(usize, 2), blocks.items.len);
+ try std.testing.expectEqualStrings("fresh", blocks.items[0]);
+ try std.testing.expectEqualStrings("after", blocks.items[1]);
+}
+
+test "effectiveSystemBlocks - prefix reconstructs prompt as of that point" {
+ const allocator = std.testing.allocator;
+
+ var conv = Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addSystemMessage("a");
+ try conv.replaceSystemMessage("fresh");
+ try conv.addSystemMessage("after");
+
+ // Truncate at position 1 (only the first `addSystemMessage`).
+ var blocks = try effectiveSystemBlocks(allocator, conv.messages.items[0..1]);
+ defer blocks.deinit(allocator);
+ try std.testing.expectEqual(@as(usize, 1), blocks.items.len);
+ try std.testing.expectEqualStrings("a", blocks.items[0]);
+}
diff --git a/libpanto/src/openai_chat_json.zig b/libpanto/src/openai_chat_json.zig
index 6991559..3b81c41 100644
--- a/libpanto/src/openai_chat_json.zig
+++ b/libpanto/src/openai_chat_json.zig
@@ -140,7 +140,24 @@ pub fn serializeRequest(
try s.objectField("messages");
try s.beginArray();
+ // Hoist the effective system prompt to the front as separate,
+ // individually-positioned `system` messages (one per surviving block,
+ // in derivation order). Keeping them distinct preserves block-level
+ // addressability for `/tree`-style truncation — we deliberately do NOT
+ // concatenate them the way Anthropic's single-string format forces.
+ var sys_blocks = try conversation.effectiveSystemBlocks(allocator, conv.messages.items);
+ defer sys_blocks.deinit(allocator);
+ for (sys_blocks.items) |text| {
+ try s.beginObject();
+ try s.objectField("role");
+ try s.write("system");
+ try s.objectField("content");
+ try s.write(text);
+ try s.endObject();
+ }
+ // Then every non-system message, in its original order.
for (conv.messages.items) |msg| {
+ if (msg.role == .system) continue;
try writeMessage(&s, msg, allocator);
}
try s.endArray();
@@ -497,6 +514,68 @@ test "serializeRequest - system + user" {
try testing.expectEqualStrings("Hello!", msgs[1].object.get("content").?.string);
}
+test "serializeRequest - multiple system blocks hoisted as separate leading messages" {
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addSystemMessage("seed");
+ try conv.addUserMessage("Hello!");
+ try conv.addSystemMessage("mid-conversation append");
+ try conv.addUserMessage("again");
+
+ const cfg = testConfig("gpt-4o");
+ var tools = emptyTools();
+ defer tools.deinit();
+ const body = try serializeRequest(allocator, &cfg, &conv, &tools);
+ defer allocator.free(body);
+
+ var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{});
+ defer parsed.deinit();
+
+ const msgs = parsed.value.object.get("messages").?.array.items;
+ // Two system messages first (in derivation order), then the two users.
+ try testing.expectEqual(@as(usize, 4), msgs.len);
+ try testing.expectEqualStrings("system", msgs[0].object.get("role").?.string);
+ try testing.expectEqualStrings("seed", msgs[0].object.get("content").?.string);
+ try testing.expectEqualStrings("system", msgs[1].object.get("role").?.string);
+ try testing.expectEqualStrings("mid-conversation append", msgs[1].object.get("content").?.string);
+ try testing.expectEqualStrings("user", msgs[2].object.get("role").?.string);
+ try testing.expectEqualStrings("Hello!", msgs[2].object.get("content").?.string);
+ try testing.expectEqualStrings("user", msgs[3].object.get("role").?.string);
+ try testing.expectEqualStrings("again", msgs[3].object.get("content").?.string);
+}
+
+test "serializeRequest - replace-mode system block wipes prior system messages" {
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addSystemMessage("original");
+ try conv.replaceSystemMessage("fresh seed");
+ try conv.addSystemMessage("fresh append");
+ try conv.addUserMessage("Hi");
+
+ const cfg = testConfig("gpt-4o");
+ var tools = emptyTools();
+ defer tools.deinit();
+ const body = try serializeRequest(allocator, &cfg, &conv, &tools);
+ defer allocator.free(body);
+
+ var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{});
+ defer parsed.deinit();
+
+ const msgs = parsed.value.object.get("messages").?.array.items;
+ try testing.expectEqual(@as(usize, 3), msgs.len);
+ try testing.expectEqualStrings("system", msgs[0].object.get("role").?.string);
+ try testing.expectEqualStrings("fresh seed", msgs[0].object.get("content").?.string);
+ try testing.expectEqualStrings("system", msgs[1].object.get("role").?.string);
+ try testing.expectEqualStrings("fresh append", msgs[1].object.get("content").?.string);
+ try testing.expectEqualStrings("user", msgs[2].object.get("role").?.string);
+}
+
test "serializeRequest - assistant Thinking blocks are stripped from outbound history" {
const allocator = testing.allocator;
diff --git a/libpanto/src/session.zig b/libpanto/src/session.zig
index e77d121..c1fd1c4 100644
--- a/libpanto/src/session.zig
+++ b/libpanto/src/session.zig
@@ -100,9 +100,18 @@ pub const MessageEntry = struct {
pub const DiskMessageRole = 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 DiskMessage = struct {
role: DiskMessageRole,
content: []DiskContentBlock, // 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
@@ -296,6 +305,12 @@ fn writeDiskMessage(s: *std.json.Stringify, msg: DiskMessage) !void {
try s.beginObject();
try s.objectField("role");
try s.write(@tagName(msg.role));
+ // `mode` is meaningful only for system messages. Emit it there so the
+ // append/replace semantics round-trip; omit it everywhere else.
+ if (msg.role == .system) {
+ try s.objectField("mode");
+ try s.write(@tagName(msg.mode));
+ }
try s.objectField("content");
try s.beginArray();
for (msg.content) |block| {
@@ -479,6 +494,14 @@ fn parseDiskMessage(allocator: Allocator, obj: std.json.ObjectMap) ParseError!Di
if (role_v != .string) return error.MissingField;
const role = std.meta.stringToEnum(DiskMessageRole, 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 mv = obj.get("mode") orelse break :blk .append;
+ if (mv != .string) break :blk .append;
+ break :blk std.meta.stringToEnum(DiskSystemMode, 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);
@@ -520,6 +543,7 @@ fn parseDiskMessage(allocator: Allocator, obj: std.json.ObjectMap) ParseError!Di
return .{
.role = role,
.content = content,
+ .mode = mode,
.provider = provider,
.model = model,
.stop_reason = stop_reason,
@@ -612,6 +636,13 @@ pub fn contentBlockToDisk(
const content = try allocator.dupe(u8, tr.content.items);
return .{ .tool_result = .{ .tool_use_id = tuid, .content = content } };
},
+ // A `.System` block becomes a disk text block; its mode rides on
+ // the enclosing `DiskMessage.mode` (set by the session manager),
+ // not on the block itself.
+ .System => |sb| {
+ const text = try allocator.dupe(u8, sb.text.items);
+ return .{ .text = .{ .text = text } };
+ },
}
}
@@ -808,6 +839,47 @@ test "serialize/parse tool result message entry" {
try testing.expectEqualStrings("anthropic", got.provider.?);
}
+test "system message mode round-trips; absent mode defaults to append" {
+ const a = testing.allocator;
+
+ // replace-mode system entry round-trips.
+ {
+ var content = try a.alloc(DiskContentBlock, 1);
+ content[0] = .{ .text = .{ .text = try dupe(a, "fresh seed") } };
+ const entry: SessionEntry = .{ .message = .{
+ .base = .{
+ .id = try dupe(a, "aabbccdd"),
+ .parent_id = null,
+ .timestamp = try dupe(a, "2026-04-25T17:40:00Z"),
+ },
+ .message = .{
+ .role = .system,
+ .content = content,
+ .mode = .replace,
+ },
+ } };
+ defer entry.deinit(a);
+
+ const line = try serializeEntry(a, entry);
+ defer a.free(line);
+ try testing.expect(std.mem.indexOf(u8, line, "\"mode\":\"replace\"") != null);
+
+ var fe = try parseLine(a, line);
+ defer fe.deinit(a);
+ try testing.expectEqual(DiskSystemMode.replace, fe.entry.message.message.mode);
+ }
+
+ // A legacy system entry with no `mode` parses back as append.
+ {
+ const line =
+ \\{"type":"message","id":"abcdefab","parentId":null,"timestamp":"2026-04-25T17:40:00Z","message":{"role":"system","content":[{"type":"text","text":"hi"}]}}
+ ;
+ var fe = try parseLine(a, line);
+ defer fe.deinit(a);
+ try testing.expectEqual(DiskSystemMode.append, fe.entry.message.message.mode);
+ }
+}
+
test "parse: null parentId is handled" {
const a = testing.allocator;
const line =
diff --git a/libpanto/src/session_manager.zig b/libpanto/src/session_manager.zig
index bf9585d..8b503f2 100644
--- a/libpanto/src/session_manager.zig
+++ b/libpanto/src/session_manager.zig
@@ -37,6 +37,7 @@ pub const SessionEntry = session_mod.SessionEntry;
pub const MessageEntry = session_mod.MessageEntry;
pub const DiskMessage = session_mod.DiskMessage;
pub const DiskMessageRole = session_mod.DiskMessageRole;
+pub const DiskSystemMode = session_mod.DiskSystemMode;
pub const DiskContentBlock = session_mod.DiskContentBlock;
pub const Usage = session_mod.Usage;
pub const CURRENT_VERSION = session_mod.CURRENT_VERSION;
@@ -668,8 +669,19 @@ fn appendMessageToConv(
content.deinit(allocator);
}
try content.ensureTotalCapacity(allocator, disk_msg.content.len);
+ const sys_mode: conversation_mod.SystemMode = switch (disk_msg.mode) {
+ .append => .append,
+ .replace => .replace,
+ };
for (disk_msg.content) |db| {
- const block = try session_mod.diskContentBlockToInternal(allocator, db);
+ var block = try session_mod.diskContentBlockToInternal(allocator, db);
+ // System-role text blocks become `.System` blocks carrying the
+ // message's recorded mode, so the append/replace derivation works
+ // on the rebuilt conversation exactly as it did when written.
+ if (disk_msg.role == .system and block == .Text) {
+ const tb = block.Text;
+ block = .{ .System = .{ .text = tb, .mode = sys_mode } };
+ }
content.appendAssumeCapacity(block);
}
const role: conversation_mod.MessageRole = switch (disk_msg.role) {
@@ -1248,7 +1260,7 @@ test "SessionManager: rebuildConversation reconstructs system/user/assistant tur
defer conv.deinit();
try testing.expectEqual(@as(usize, 3), conv.messages.items.len);
try testing.expectEqual(conversation_mod.MessageRole.system, conv.messages.items[0].role);
- try testing.expectEqualStrings("you are helpful", conv.messages.items[0].content.items[0].Text.items);
+ try testing.expectEqualStrings("you are helpful", conv.messages.items[0].content.items[0].System.text.items);
try testing.expectEqual(conversation_mod.MessageRole.user, conv.messages.items[1].role);
try testing.expectEqualStrings("hello", conv.messages.items[1].content.items[0].Text.items);
try testing.expectEqual(conversation_mod.MessageRole.assistant, conv.messages.items[2].role);