diff options
| author | t <t@tjp.lol> | 2026-06-02 09:02:49 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-02 09:49:18 -0600 |
| commit | 9c64a7d4462a11674e2dea481b037b5f5d9c62fc (patch) | |
| tree | a4f0a493a9b7558d40f5eb79a0eeae46ecc23515 /libpanto/src/conversation.zig | |
| parent | 456d986be1357c247753d9dc21734bb898d5e78b (diff) | |
system prompt building and logging
Diffstat (limited to 'libpanto/src/conversation.zig')
| -rw-r--r-- | libpanto/src/conversation.zig | 162 |
1 files changed, 160 insertions, 2 deletions
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]); +} |
