diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.zig | 179 |
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(); } } |
