const std = @import("std"); const panto = @import("panto"); 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, }; fn onMessageStart(ptr: *anyopaque, role: MessageRole) anyerror!void { _ = ptr; _ = role; } 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(); } 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(); } 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); const stdout = &stdout_file.interface; var stdin_buffer: [4096]u8 = undefined; var stdin_file = std.Io.File.stdin().reader(io, &stdin_buffer); const stdin = &stdin_file.interface; 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 maybe_line = stdin.takeDelimiter('\n') catch |err| { std.debug.print("read error: {}\n", .{err}); return; }; 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(); } }