summaryrefslogtreecommitdiff
path: root/libawl/src
diff options
context:
space:
mode:
Diffstat (limited to 'libawl/src')
-rw-r--r--libawl/src/agent.zig23
-rw-r--r--libawl/src/config.zig37
-rw-r--r--libawl/src/conversation.zig185
-rw-r--r--libawl/src/json.zig4
-rw-r--r--libawl/src/provider.zig66
-rw-r--r--libawl/src/provider_openai.zig6
-rw-r--r--libawl/src/root.zig9
-rw-r--r--libawl/src/sse.zig199
8 files changed, 0 insertions, 529 deletions
diff --git a/libawl/src/agent.zig b/libawl/src/agent.zig
deleted file mode 100644
index 7ebc058..0000000
--- a/libawl/src/agent.zig
+++ /dev/null
@@ -1,23 +0,0 @@
-const std = @import("std");
-const provider = @import("provider.zig");
-const conversation = @import("conversation.zig");
-
-pub const Agent = struct {
- provider: provider.Provider,
- allocator: std.mem.Allocator,
-
- pub fn init(allocator: std.mem.Allocator, prov: provider.Provider) Agent {
- return .{
- .provider = prov,
- .allocator = allocator,
- };
- }
-
- pub fn runStep(self: Agent, conv: *conversation.Conversation, receiver: *provider.Receiver) !void {
- try self.provider.streamStep(conv, receiver);
- }
-
- pub fn deinit(self: Agent) void {
- self.provider.deinit();
- }
-};
diff --git a/libawl/src/config.zig b/libawl/src/config.zig
deleted file mode 100644
index 8b6f147..0000000
--- a/libawl/src/config.zig
+++ /dev/null
@@ -1,37 +0,0 @@
-const std = @import("std");
-
-pub const Config = struct {
- api_key: []const u8,
- base_url: []const u8,
- model: []const u8,
-
- pub const Error = error{MissingApiKey};
-
- pub fn fromEnv() Error!Config {
- const api_key_z = std.c.getenv("PANTO_API_KEY") orelse return Error.MissingApiKey;
- const api_key = std.mem.sliceTo(api_key_z, 0);
-
- const base_url_z = std.c.getenv("PANTO_BASE_URL") orelse "https://api.openai.com/v1";
- const base_url = std.mem.sliceTo(base_url_z, 0);
-
- const model_z = std.c.getenv("PANTO_MODEL") orelse "gpt-4o";
- const model = std.mem.sliceTo(model_z, 0);
-
- return Config{
- .api_key = api_key,
- .base_url = base_url,
- .model = model,
- };
- }
-};
-
-test "Config - defaults" {
- const cfg = Config{
- .api_key = "test-key",
- .base_url = "https://api.openai.com/v1",
- .model = "gpt-4o",
- };
- try std.testing.expectEqualStrings("test-key", cfg.api_key);
- try std.testing.expectEqualStrings("https://api.openai.com/v1", cfg.base_url);
- try std.testing.expectEqualStrings("gpt-4o", cfg.model);
-}
diff --git a/libawl/src/conversation.zig b/libawl/src/conversation.zig
deleted file mode 100644
index 0e9a4e9..0000000
--- a/libawl/src/conversation.zig
+++ /dev/null
@@ -1,185 +0,0 @@
-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);
-}
diff --git a/libawl/src/json.zig b/libawl/src/json.zig
deleted file mode 100644
index 542391e..0000000
--- a/libawl/src/json.zig
+++ /dev/null
@@ -1,4 +0,0 @@
-// Serialization helpers — model → wire JSON, deltas → ContentBlocks.
-// Implementation pending; this module will handle:
-// 1. Serialize Conversation → OpenAI request body JSON
-// 2. Parse SSE chunk deltas → ContentBlock updates
diff --git a/libawl/src/provider.zig b/libawl/src/provider.zig
deleted file mode 100644
index 8d6eea5..0000000
--- a/libawl/src/provider.zig
+++ /dev/null
@@ -1,66 +0,0 @@
-const std = @import("std");
-const conversation = @import("conversation.zig");
-
-pub const ContentBlockType = enum {
- Text,
- Thinking,
- ToolUse,
- ToolResult,
-};
-
-pub const BlockMeta = struct {
- /// Only populated for ToolUse blocks. Null for Text/Thinking.
- tool_id: ?[]const u8 = null,
- tool_name: ?[]const u8 = null,
-};
-
-pub const ReceiverVTable = struct {
- onMessageStart: *const fn (*anyopaque, conversation.MessageRole) void,
- onBlockStart: *const fn (*anyopaque, ContentBlockType, usize, ?BlockMeta) void,
- onContentDelta: *const fn (*anyopaque, usize, []const u8) void,
- onBlockComplete: *const fn (*anyopaque, usize, conversation.ContentBlock) void,
- onMessageComplete: *const fn (*anyopaque, conversation.Message) void,
-};
-
-pub const Receiver = struct {
- ptr: *anyopaque,
- vtable: *const ReceiverVTable,
-
- pub fn onMessageStart(self: Receiver, role: conversation.MessageRole) void {
- self.vtable.onMessageStart(self.ptr, role);
- }
-
- pub fn onBlockStart(self: Receiver, block_type: ContentBlockType, index: usize, meta: ?BlockMeta) void {
- self.vtable.onBlockStart(self.ptr, block_type, index, meta);
- }
-
- pub fn onContentDelta(self: Receiver, block_index: usize, delta: []const u8) void {
- self.vtable.onContentDelta(self.ptr, block_index, delta);
- }
-
- pub fn onBlockComplete(self: Receiver, block_index: usize, block: conversation.ContentBlock) void {
- self.vtable.onBlockComplete(self.ptr, block_index, block);
- }
-
- pub fn onMessageComplete(self: Receiver, message: conversation.Message) void {
- self.vtable.onMessageComplete(self.ptr, message);
- }
-};
-
-pub const ProviderVTable = struct {
- streamStep: *const fn (*anyopaque, *conversation.Conversation, *Receiver) anyerror!void,
- deinit: *const fn (*anyopaque) void,
-};
-
-pub const Provider = struct {
- ptr: *anyopaque,
- vtable: *const ProviderVTable,
-
- pub fn streamStep(self: Provider, conv: *conversation.Conversation, receiver: *Receiver) anyerror!void {
- return self.vtable.streamStep(self.ptr, conv, receiver);
- }
-
- pub fn deinit(self: Provider) void {
- self.vtable.deinit(self.ptr);
- }
-};
diff --git a/libawl/src/provider_openai.zig b/libawl/src/provider_openai.zig
deleted file mode 100644
index c459a9e..0000000
--- a/libawl/src/provider_openai.zig
+++ /dev/null
@@ -1,6 +0,0 @@
-// OpenAI-compatible provider implementation.
-// Implementation pending; this module will:
-// - Convert Conversation → OpenAI wire JSON
-// - Make HTTP POST with stream: true
-// - Parse SSE events, drive block boundary state machine
-// - Call the full Receiver callback sequence
diff --git a/libawl/src/root.zig b/libawl/src/root.zig
deleted file mode 100644
index 72ceb97..0000000
--- a/libawl/src/root.zig
+++ /dev/null
@@ -1,9 +0,0 @@
-pub const conversation = @import("conversation.zig");
-pub const provider = @import("provider.zig");
-pub const agent = @import("agent.zig");
-pub const config = @import("config.zig");
-pub const sse = @import("sse.zig");
-
-// Internal modules — not re-exported as public API
-// pub const json = @import("json.zig");
-// pub const provider_openai = @import("provider_openai.zig");
diff --git a/libawl/src/sse.zig b/libawl/src/sse.zig
deleted file mode 100644
index 150c3ea..0000000
--- a/libawl/src/sse.zig
+++ /dev/null
@@ -1,199 +0,0 @@
-const std = @import("std");
-
-/// Parsed SSE event data — the content after "data: " (prefix removed).
-/// For multi-line data events, lines are joined with newlines per the SSE spec.
-///
-/// Borrowed from the parser's internal buffer. Call `SSEParser.freeEvents()`
-/// to free the events list when done (event data itself is borrowed and
-/// does not need individual freeing).
-const Event = []const u8;
-
-/// Incremental SSE line parser.
-///
-/// The HTTP client delivers arbitrary-sized read buffers; this module
-/// reassembles them into complete `data: ...\n\n` events.
-///
-/// Event data is borrowed from the internal buffer. Call `freeEvents()` to
-/// free the events list returned by `feed`; the event data itself is borrowed
-/// from the parser's buffer and does not need individual freeing.
-pub const SSEParser = struct {
- buf: std.ArrayList(u8) = .empty,
- allocator: std.mem.Allocator,
-
- pub fn init(allocator: std.mem.Allocator) SSEParser {
- return .{
- .allocator = allocator,
- };
- }
-
- /// Feed a chunk of raw bytes. Returns a list of complete SSE events
- /// found in the buffer. Returns an empty list if no complete events
- /// are available yet.
- ///
- /// Event data is borrowed from the internal buffer. Call `freeEvents()`
- /// on the returned list when done.
- pub fn feed(self: *SSEParser, chunk: []const u8) ![]Event {
- try self.buf.appendSlice(self.allocator, chunk);
-
- var events = std.ArrayList(Event).init(self.allocator);
- errdefer events.deinit();
-
- var write_pos: usize = 0;
- var scan_pos: usize = 0;
-
- while (std.mem.indexOf(u8, self.buf.items[scan_pos..], "\n\n")) |rel_delim| {
- const delim = scan_pos + rel_delim;
- const event_bytes = self.buf.items[scan_pos..delim];
-
- // Compact data lines in-place. Compacted data is always shorter
- // than the original event bytes (we strip at least "data: " per
- // line), so write_pos <= scan_pos and we never overwrite data
- // we haven't yet read.
- const data_start = write_pos;
- var line_start: usize = 0;
- var has_data = false;
-
- while (line_start < event_bytes.len) {
- const newline = std.mem.indexOfScalarPos(u8, event_bytes, line_start, '\n') orelse event_bytes.len;
- const line = event_bytes[line_start..newline];
-
- if (std.mem.startsWith(u8, line, "data: ")) {
- if (has_data) {
- self.buf.items[write_pos] = '\n';
- write_pos += 1;
- }
- const content = line["data: ".len..];
- std.mem.copyForwards(u8, self.buf.items[write_pos .. write_pos + content.len], content);
- write_pos += content.len;
- has_data = true;
- }
- // Ignore other SSE fields (event:, id:, retry:) and comments (starting with :)
-
- line_start = newline + 1;
- }
-
- if (has_data) {
- try events.append(self.buf.items[data_start..write_pos]);
- }
-
- scan_pos = delim + 2;
- }
-
- // Shift remaining (incomplete) bytes to the front, retaining
- // the backing allocation for reuse.
- const remaining_len = self.buf.items.len - scan_pos;
- if (remaining_len > 0 and scan_pos > 0) {
- std.mem.copyForwards(u8, self.buf.items[0..remaining_len], self.buf.items[scan_pos..]);
- }
- self.buf.shrinkRetainingCapacity(remaining_len);
-
- return events.toOwnedSlice();
- }
-
- /// Free a list of events returned by `feed`.
- /// Event data is borrowed from the parser's buffer and does not need
- /// to be freed individually; only the events array itself is freed.
- pub fn freeEvents(self: *SSEParser, events: []Event) void {
- self.allocator.free(events);
- }
-
- pub fn deinit(self: *SSEParser) void {
- self.buf.deinit(self.allocator);
- }
-};
-
-test "SSEParser - single complete event" {
- const allocator = std.testing.allocator;
-
- var parser = SSEParser.init(allocator);
- defer parser.deinit();
-
- const events = try parser.feed("data: hello\n\n");
- defer parser.freeEvents(events);
-
- try std.testing.expectEqual(@as(usize, 1), events.len);
- try std.testing.expectEqualStrings("hello", events[0]);
-}
-
-test "SSEParser - partial then complete" {
- const allocator = std.testing.allocator;
-
- var parser = SSEParser.init(allocator);
- defer parser.deinit();
-
- // Feed partial — no complete event yet
- const events1 = try parser.feed("data: hel");
- defer parser.freeEvents(events1);
- try std.testing.expectEqual(@as(usize, 0), events1.len);
-
- // Feed remainder — completes the event
- const events2 = try parser.feed("lo\n\n");
- defer parser.freeEvents(events2);
- try std.testing.expectEqual(@as(usize, 1), events2.len);
- try std.testing.expectEqualStrings("hello", events2[0]);
-}
-
-test "SSEParser - multiple events in single chunk" {
- const allocator = std.testing.allocator;
-
- var parser = SSEParser.init(allocator);
- defer parser.deinit();
-
- const events = try parser.feed("data: one\n\ndata: two\n\n");
- defer parser.freeEvents(events);
-
- try std.testing.expectEqual(@as(usize, 2), events.len);
- try std.testing.expectEqualStrings("one", events[0]);
- try std.testing.expectEqualStrings("two", events[1]);
-}
-
-test "SSEParser - data DONE signal" {
- const allocator = std.testing.allocator;
-
- var parser = SSEParser.init(allocator);
- defer parser.deinit();
-
- const events = try parser.feed("data: [DONE]\n\n");
- defer parser.freeEvents(events);
-
- try std.testing.expectEqual(@as(usize, 1), events.len);
- try std.testing.expectEqualStrings("[DONE]", events[0]);
-}
-
-test "SSEParser - empty event (keep-alive)" {
- const allocator = std.testing.allocator;
-
- var parser = SSEParser.init(allocator);
- defer parser.deinit();
-
- // A blank line (just \n\n) is a keep-alive comment with no data
- const events = try parser.feed("\n\n");
- defer parser.freeEvents(events);
- try std.testing.expectEqual(@as(usize, 0), events.len);
-}
-
-test "SSEParser - ignores non-data fields" {
- const allocator = std.testing.allocator;
-
- var parser = SSEParser.init(allocator);
- defer parser.deinit();
-
- const events = try parser.feed("event: message\ndata: payload\nid: 42\n\n");
- defer parser.freeEvents(events);
-
- try std.testing.expectEqual(@as(usize, 1), events.len);
- try std.testing.expectEqualStrings("payload", events[0]);
-}
-
-test "SSEParser - multi-line data joined with newline" {
- const allocator = std.testing.allocator;
-
- var parser = SSEParser.init(allocator);
- defer parser.deinit();
-
- const events = try parser.feed("data: line1\ndata: line2\n\n");
- defer parser.freeEvents(events);
-
- try std.testing.expectEqual(@as(usize, 1), events.len);
- try std.testing.expectEqualStrings("line1\nline2", events[0]);
-}