summaryrefslogtreecommitdiff
path: root/libawl/src/conversation.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libawl/src/conversation.zig')
-rw-r--r--libawl/src/conversation.zig185
1 files changed, 185 insertions, 0 deletions
diff --git a/libawl/src/conversation.zig b/libawl/src/conversation.zig
new file mode 100644
index 0000000..0e9a4e9
--- /dev/null
+++ b/libawl/src/conversation.zig
@@ -0,0 +1,185 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+/// A streaming text buffer used by content blocks.
+/// Thin alias over ArrayList(u8) — amortized O(1) appends,
+/// no O(n²) re-copying.
+pub const TextualBlock = std.ArrayList(u8);
+
+/// Create a TextualBlock with initial content (copies the slice).
+pub fn textualBlockFromSlice(alloc: Allocator, slice: []const u8) !TextualBlock {
+ var buf: TextualBlock = .empty;
+ try buf.appendSlice(alloc, slice);
+ return buf;
+}
+
+pub const ToolUseBlock = struct {
+ id: []const u8,
+ name: []const u8,
+ input: TextualBlock = .empty,
+
+ pub fn deinit(self: *ToolUseBlock, alloc: Allocator) void {
+ alloc.free(self.id);
+ alloc.free(self.name);
+ self.input.deinit(alloc);
+ }
+};
+
+pub const ToolResultBlock = struct {
+ tool_use_id: []const u8,
+ content: TextualBlock = .empty,
+
+ pub fn deinit(self: *ToolResultBlock, alloc: Allocator) void {
+ alloc.free(self.tool_use_id);
+ self.content.deinit(alloc);
+ }
+};
+
+pub const ContentBlock = union(enum) {
+ Text: TextualBlock,
+ Thinking: TextualBlock,
+ ToolUse: ToolUseBlock,
+ ToolResult: ToolResultBlock,
+
+ pub fn deinit(self: *ContentBlock, alloc: Allocator) void {
+ switch (self.*) {
+ inline else => |*b| b.deinit(alloc),
+ }
+ }
+};
+
+pub const MessageRole = enum {
+ system,
+ user,
+ assistant,
+};
+
+pub const Message = struct {
+ role: MessageRole,
+ content: std.ArrayList(ContentBlock) = .empty,
+
+ pub fn deinit(self: *Message, alloc: Allocator) void {
+ for (self.content.items) |*block| {
+ block.deinit(alloc);
+ }
+ self.content.deinit(alloc);
+ }
+};
+
+pub const Conversation = struct {
+ messages: std.ArrayList(Message) = .empty,
+ allocator: Allocator,
+
+ pub fn init(allocator: Allocator) Conversation {
+ return .{
+ .allocator = allocator,
+ };
+ }
+
+ pub fn addSystemMessage(self: *Conversation, text: []const u8) !void {
+ const tb = try textualBlockFromSlice(self.allocator, text);
+ var content: std.ArrayList(ContentBlock) = .empty;
+ try content.append(self.allocator, .{ .Text = tb });
+ try self.messages.append(self.allocator, .{
+ .role = .system,
+ .content = content,
+ });
+ }
+
+ pub fn addUserMessage(self: *Conversation, text: []const u8) !void {
+ const tb = try textualBlockFromSlice(self.allocator, text);
+ var content: std.ArrayList(ContentBlock) = .empty;
+ try content.append(self.allocator, .{ .Text = tb });
+ try self.messages.append(self.allocator, .{
+ .role = .user,
+ .content = content,
+ });
+ }
+
+ /// Append an assistant message. Ownership of the blocks is transferred
+ /// to the conversation; the caller must not deinit them after this call.
+ pub fn addAssistantMessage(self: *Conversation, blocks: []const ContentBlock) !void {
+ var content: std.ArrayList(ContentBlock) = .empty;
+ try content.ensureTotalCapacity(self.allocator, blocks.len);
+ for (blocks) |block| {
+ content.appendAssumeCapacity(block);
+ }
+ try self.messages.append(self.allocator, .{
+ .role = .assistant,
+ .content = content,
+ });
+ }
+
+ pub fn deinit(self: *Conversation) void {
+ for (self.messages.items) |*msg| {
+ msg.deinit(self.allocator);
+ }
+ self.messages.deinit(self.allocator);
+ }
+};
+
+test "Conversation - add messages and verify content" {
+ const allocator = std.testing.allocator;
+
+ var conv = Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addSystemMessage("You are a helpful assistant.");
+ try conv.addUserMessage("Hello!");
+ try conv.addAssistantMessage(&.{
+ .{ .Text = try textualBlockFromSlice(allocator, "Hi there!") },
+ });
+
+ try std.testing.expectEqual(@as(usize, 3), conv.messages.items.len);
+
+ 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,
+ );
+
+ try std.testing.expectEqual(MessageRole.user, conv.messages.items[1].role);
+ try std.testing.expectEqualStrings("Hello!", conv.messages.items[1].content.items[0].Text.items);
+
+ try std.testing.expectEqual(MessageRole.assistant, conv.messages.items[2].role);
+ try std.testing.expectEqualStrings("Hi there!", conv.messages.items[2].content.items[0].Text.items);
+}
+
+test "TextualBlock - incremental append" {
+ const allocator = std.testing.allocator;
+
+ var tb = TextualBlock.empty;
+ defer tb.deinit(allocator);
+
+ try tb.appendSlice(allocator, "Hello");
+ try tb.appendSlice(allocator, " world");
+ try std.testing.expectEqualStrings("Hello world", tb.items);
+}
+
+test "Conversation - deinit frees without leaks" {
+ const allocator = std.testing.allocator;
+
+ var conv = Conversation.init(allocator);
+ try conv.addSystemMessage("system");
+ try conv.addUserMessage("user message");
+ try conv.addAssistantMessage(&.{
+ .{ .Text = try textualBlockFromSlice(allocator, "response") },
+ });
+ conv.deinit();
+}
+
+test "ContentBlock - Thinking variant" {
+ const allocator = std.testing.allocator;
+
+ var conv = Conversation.init(allocator);
+ defer conv.deinit();
+
+ try conv.addAssistantMessage(&.{
+ .{ .Thinking = try textualBlockFromSlice(allocator, "hmm...") },
+ .{ .Text = try textualBlockFromSlice(allocator, "answer") },
+ });
+
+ try std.testing.expectEqual(@as(usize, 2), conv.messages.items[0].content.items.len);
+ try std.testing.expectEqualStrings("hmm...", conv.messages.items[0].content.items[0].Thinking.items);
+ try std.testing.expectEqualStrings("answer", conv.messages.items[0].content.items[1].Text.items);
+}