From bb85e867bc938138f29fb45230169af1ba60761c Mon Sep 17 00:00:00 2001 From: t Date: Wed, 10 Jun 2026 10:06:07 -0600 Subject: Add addUserText helper; update all call sites Conversation.addUserMessage now takes a []ContentBlock (symmetric with addAssistantMessage). Introduce a thin addUserText wrapper in agent.zig for the plain-text case and update every call site in agent.zig and anthropic_messages_json.zig accordingly. --- libpanto/src/conversation.zig | 45 ++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) (limited to 'libpanto/src/conversation.zig') diff --git a/libpanto/src/conversation.zig b/libpanto/src/conversation.zig index a436378..1942a0c 100644 --- a/libpanto/src/conversation.zig +++ b/libpanto/src/conversation.zig @@ -265,10 +265,24 @@ pub const Conversation = struct { }); } - pub fn addUserMessage(self: *Conversation, text: []const u8) !void { - const tb = try textualBlockFromSlice(self.allocator, text); + /// Append a `user`-role message from a slice of content blocks. + /// Symmetric with `addAssistantMessage`: ownership of the blocks (and + /// every byte they reference) transfers to the conversation; the caller + /// must not deinit them after this call. This is the general user-side + /// builder — a user turn may carry plain text, multiple text blocks + /// (e.g. messages queued while the agent was mid-turn), one or more + /// `.ToolResult` blocks, or any mix. For the common single-text case, + /// build a one-element `.{ .Text = ... }` slice (see the `addUserText` + /// test helpers across the codebase). + /// + /// Blocks must use this conversation's allocator for any owned bytes, + /// since the conversation will free them with that allocator. + pub fn addUserMessage(self: *Conversation, blocks: []const ContentBlock) !void { var content: std.ArrayList(ContentBlock) = .empty; - try content.append(self.allocator, .{ .Text = tb }); + try content.ensureTotalCapacity(self.allocator, blocks.len); + for (blocks) |block| { + content.appendAssumeCapacity(block); + } try self.messages.append(self.allocator, .{ .role = .user, .content = content, @@ -401,6 +415,15 @@ pub fn activeMessageWindow(messages: []const Message) []const Message { return messages; } +/// Test helper: append a single-text user message. `addUserMessage` takes +/// a block slice; this wraps the plain-text case the tests below use. +fn addUserText(conv: *Conversation, text: []const u8) !void { + const tb = try textualBlockFromSlice(conv.allocator, text); + var block: ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + test "Conversation - add messages and verify content" { const allocator = std.testing.allocator; @@ -408,7 +431,7 @@ test "Conversation - add messages and verify content" { defer conv.deinit(); try conv.addSystemMessage("You are a helpful assistant."); - try conv.addUserMessage("Hello!"); + try addUserText(&conv, "Hello!"); try conv.addAssistantMessage(&.{ .{ .Text = try textualBlockFromSlice(allocator, "Hi there!") }, }, null); @@ -448,7 +471,7 @@ test "Conversation - deinit frees without leaks" { var conv = Conversation.init(allocator); try conv.addSystemMessage("system"); - try conv.addUserMessage("user message"); + try addUserText(&conv, "user message"); try conv.addAssistantMessage(&.{ .{ .Text = try textualBlockFromSlice(allocator, "response") }, }, null); @@ -494,7 +517,7 @@ test "effectiveSystemBlocks - append accumulates in order" { try conv.addSystemMessage("a"); try conv.addSystemMessage("b"); - try conv.addUserMessage("hi"); + try addUserText(&conv, "hi"); try conv.addSystemMessage("c"); var blocks = try effectiveSystemBlocks(allocator, conv.messages.items); @@ -548,7 +571,7 @@ test "addCompactionSummary - sits alone in a user message" { var conv = Conversation.init(allocator); defer conv.deinit(); - try conv.addUserMessage("hi"); + try addUserText(&conv, "hi"); try conv.addCompactionSummary("summary of earlier history"); const m = conv.messages.items[1]; @@ -567,7 +590,7 @@ test "latestCompactionIndex - null when never compacted" { defer conv.deinit(); try conv.addSystemMessage("sys"); - try conv.addUserMessage("hi"); + try addUserText(&conv, "hi"); try conv.addAssistantMessage(&.{ .{ .Text = try textualBlockFromSlice(allocator, "hello") }, }, null); @@ -582,11 +605,11 @@ test "latestCompactionIndex - returns the latest summary anchor" { defer conv.deinit(); try conv.addSystemMessage("sys"); - try conv.addUserMessage("old"); + try addUserText(&conv, "old"); try conv.addCompactionSummary("S1"); - try conv.addUserMessage("mid"); + try addUserText(&conv, "mid"); try conv.addCompactionSummary("S2"); // index 4 - try conv.addUserMessage("recent"); + try addUserText(&conv, "recent"); try std.testing.expectEqual(@as(?usize, 4), latestCompactionIndex(conv.messages.items)); } -- cgit v1.3