summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.zig179
1 files changed, 146 insertions, 33 deletions
diff --git a/src/main.zig b/src/main.zig
index 4f9c490..832f037 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,24 +1,123 @@
const std = @import("std");
-const libpanto = @import("panto");
+const panto = @import("panto");
-pub fn main() !void {
- const config = libpanto.config.Config{
- .api_style = .anthropic,
- .api_key = "test",
- .base_url = "https://api.anthropic.com",
- .model = "claude-sonnet-4-20250514",
+const Receiver = panto.provider.Receiver;
+const ReceiverVTable = panto.provider.ReceiverVTable;
+const ContentBlockType = panto.provider.ContentBlockType;
+const BlockMeta = panto.provider.BlockMeta;
+const MessageRole = panto.conversation.MessageRole;
+
+/// Receiver that prints streaming deltas to stdout. Thinking blocks are
+/// dimmed with ANSI escape codes; text blocks render plain.
+const CLIReceiver = struct {
+ stdout: *std.Io.Writer,
+ file: *std.Io.File.Writer,
+
+ pub fn receiver(self: *CLIReceiver) Receiver {
+ return .{ .ptr = self, .vtable = &vtable };
+ }
+
+ const vtable: ReceiverVTable = .{
+ .onMessageStart = onMessageStart,
+ .onBlockStart = onBlockStart,
+ .onContentDelta = onContentDelta,
+ .onBlockComplete = onBlockComplete,
+ .onMessageComplete = onMessageComplete,
+ .onError = onError,
};
- var debug_allocator = std.heap.DebugAllocator(.{}).init;
- defer _ = debug_allocator.deinit();
- const alloc = debug_allocator.allocator();
+ fn onMessageStart(ptr: *anyopaque, role: MessageRole) anyerror!void {
+ _ = ptr;
+ _ = role;
+ }
- var conv = libpanto.conversation.Conversation.init(alloc);
- defer conv.deinit();
+ fn onBlockStart(
+ ptr: *anyopaque,
+ block_type: ContentBlockType,
+ index: usize,
+ meta: ?BlockMeta,
+ ) anyerror!void {
+ _ = index;
+ _ = meta;
+ const self: *CLIReceiver = @ptrCast(@alignCast(ptr));
+ if (block_type == .Thinking) {
+ try self.stdout.writeAll("\x1b[2m[thinking] ");
+ }
+ try self.file.flush();
+ }
- try conv.addSystemMessage("You are a helpful assistant.");
+ fn onContentDelta(ptr: *anyopaque, index: usize, delta: []const u8) anyerror!void {
+ _ = index;
+ const self: *CLIReceiver = @ptrCast(@alignCast(ptr));
+ try self.stdout.writeAll(delta);
+ try self.file.flush();
+ }
- const io = std.Io.Threaded.global_single_threaded.io();
+ fn onBlockComplete(
+ ptr: *anyopaque,
+ index: usize,
+ block: panto.conversation.ContentBlock,
+ ) anyerror!void {
+ _ = index;
+ const self: *CLIReceiver = @ptrCast(@alignCast(ptr));
+ if (block == .Thinking) {
+ try self.stdout.writeAll("\x1b[0m\n");
+ }
+ try self.file.flush();
+ }
+
+ fn onMessageComplete(ptr: *anyopaque, message: panto.conversation.Message) anyerror!void {
+ _ = message;
+ const self: *CLIReceiver = @ptrCast(@alignCast(ptr));
+ try self.stdout.writeAll("\n");
+ try self.file.flush();
+ }
+
+ /// Reset any in-progress display state after a failed turn. Errors here
+ /// must be swallowed — we're already in the error path.
+ fn onError(ptr: *anyopaque, err: anyerror) void {
+ _ = &err;
+ const self: *CLIReceiver = @ptrCast(@alignCast(ptr));
+ // If we were rendering a Thinking block, clear the dim style so
+ // subsequent output (including the "[error: ...]" line) is readable.
+ self.stdout.writeAll("\x1b[0m") catch {};
+ self.file.flush() catch {};
+ }
+};
+
+pub fn main(init: std.process.Init) !void {
+ const alloc = init.gpa;
+ const io = init.io;
+
+ const api_key = init.environ_map.get("PANTO_API_KEY") orelse {
+ std.debug.print("error: PANTO_API_KEY is required\n", .{});
+ return error.MissingApiKey;
+ };
+ const base_url = init.environ_map.get("PANTO_BASE_URL") orelse "https://api.openai.com/v1";
+ const model = init.environ_map.get("PANTO_MODEL") orelse {
+ std.debug.print("error: PANTO_MODEL is required\n", .{});
+ return error.MissingModel;
+ };
+
+ const reasoning: panto.config.ReasoningEffort =
+ if (init.environ_map.get("PANTO_REASONING")) |val|
+ std.meta.stringToEnum(panto.config.ReasoningEffort, val) orelse {
+ std.debug.print(
+ "error: PANTO_REASONING must be one of: default, off, minimal, low, medium, high (got: {s})\n",
+ .{val},
+ );
+ return error.InvalidReasoning;
+ }
+ else
+ .default;
+
+ const config = panto.config.Config{
+ .api_style = .openai_chat,
+ .api_key = api_key,
+ .base_url = base_url,
+ .model = model,
+ .reasoning = reasoning,
+ };
var stdout_buffer: [4096]u8 = undefined;
var stdout_file = std.Io.File.stdout().writer(io, &stdout_buffer);
@@ -28,32 +127,46 @@ pub fn main() !void {
var stdin_file = std.Io.File.stdin().reader(io, &stdin_buffer);
const stdin = &stdin_file.interface;
- try stdout.print("panto — model: {s}\n> ", .{config.model});
+ var conv = panto.conversation.Conversation.init(alloc);
+ defer conv.deinit();
+ try conv.addSystemMessage("You are a helpful assistant.");
+
+ var provider_impl = panto.provider_openai_chat.OpenAIChatProvider.init(alloc, io, config);
+ defer provider_impl.deinit();
+
+ const agent = panto.agent.Agent.init(alloc, provider_impl.provider());
+
+ try stdout.print("panto — model: {s} @ {s}\n", .{ config.model, config.base_url });
+ try stdout.print("> ", .{});
try stdout_file.flush();
+ var cli_recv = CLIReceiver{ .stdout = stdout, .file = &stdout_file };
+ var recv = cli_recv.receiver();
+
while (true) {
- const line = stdin.takeDelimiter('\n') catch |err| {
- std.debug.print("Read error: {}\n", .{err});
+ const maybe_line = stdin.takeDelimiter('\n') catch |err| {
+ std.debug.print("read error: {}\n", .{err});
return;
};
- if (line) |text| {
- if (text.len == 0) {
- try stdout.print("> ", .{});
- try stdout_file.flush();
- continue;
- }
- // Copy the line since the reader buffer may be reused
- const owned = try alloc.dupe(u8, text);
- defer alloc.free(owned);
- try conv.addUserMessage(owned);
- // TODO: call agent.runStep() once provider is implemented
- try stdout.print("(streaming not yet implemented)\n> ", .{});
- try stdout_file.flush();
- } else {
- // EOF (Ctrl+D)
- try stdout.print("\n", .{});
+ const line = maybe_line orelse {
+ try stdout.writeAll("\n");
try stdout_file.flush();
break;
+ };
+
+ if (line.len == 0) {
+ try stdout.writeAll("\n> ");
+ try stdout_file.flush();
+ continue;
}
+
+ try conv.addUserMessage(line);
+
+ agent.runStep(&conv, &recv) catch |err| {
+ try stdout.print("\n[error: {s}]\n", .{@errorName(err)});
+ };
+
+ try stdout.writeAll("\n> ");
+ try stdout_file.flush();
}
}