//! Active configuration the agent consults on every turn. //! //! `ProviderConfig` is a tagged union keyed by `APIStyle`; each variant //! carries the settings specific to one wire dialect. New providers add a //! new tag and a new payload struct here; nothing else in libpanto needs a //! central enum refresh. //! //! `Config` bundles the active `ProviderConfig` together with the //! `ToolRegistry` the agent should expose this turn. It is an **immutable //! snapshot**: the agent holds a `*const Config` and re-reads it at the top //! of every turn, so swapping that pointer (e.g. from a `panto.configure` //! hook) changes provider, model, base_url, and/or the visible tool set //! atomically at the next turn boundary. Because the snapshot is read-only //! while a turn is in flight, concurrent tool workers reading the old //! snapshot stay consistent. const std = @import("std"); const Io = std.Io; const tool_registry_mod = @import("tool_registry.zig"); pub const ToolRegistry = tool_registry_mod.ToolRegistry; /// The wire dialect a provider speaks. pub const APIStyle = enum { openai_chat, anthropic_messages, }; /// Reasoning intensity hint sent to providers that support it. /// /// `.default` omits the field entirely so the provider's own default applies. /// `.off` sends `"none"` (supported by OpenRouter/NanoGPT; ignored or rejected /// elsewhere). The remaining values mirror the `reasoning_effort` parameter /// accepted by OpenAI reasoning models and OpenAI-compatible proxies. pub const ReasoningEffort = enum { default, off, minimal, low, medium, high, }; pub const OpenAIChatConfig = struct { api_key: []const u8, base_url: []const u8, model: []const u8, reasoning: ReasoningEffort = .default, max_tokens: u32 = 64_000, }; pub const AnthropicMessagesConfig = struct { api_key: []const u8, base_url: []const u8, model: []const u8, /// Value sent in the `anthropic-version` header. api_version: []const u8 = "2023-06-01", /// Required by Anthropic's Messages API. max_tokens: u32 = 64_000, }; /// Per-provider transport/auth/model configuration. Tagged by `APIStyle`. pub const ProviderConfig = union(APIStyle) { openai_chat: OpenAIChatConfig, anthropic_messages: AnthropicMessagesConfig, pub fn style(self: ProviderConfig) APIStyle { return @as(APIStyle, self); } }; /// An immutable snapshot of everything the agent consults per turn: which /// provider/model to talk to, and which tools to expose. The agent holds a /// `*const Config` and re-reads it each turn; replacing the pointer swaps /// the active configuration wholesale at the next turn boundary. /// /// `registry` is borrowed, not owned — its lifetime is managed by whoever /// built the snapshot (typically the embedder). A `Config` may be copied /// freely; copies share the same borrowed registry. pub const Config = struct { provider: ProviderConfig, registry: *const ToolRegistry, pub fn style(self: Config) APIStyle { return self.provider.style(); } }; // =========================================================================== // Process-global HTTP client // =========================================================================== // // `std.http.Client`'s connection pool is mutex-guarded and keyed by host, // so a single client safely multiplexes every provider/base_url the agent // ever switches to, across concurrent turns. We keep exactly one for the // whole process: switching `base_url` simply leaves the old host's idle // connections to time out (and reuses them if the user switches back). // // Embedders must call `initHttp` once before any turn and `deinitHttp` // once at shutdown. var global_http: ?std.http.Client = null; /// Initialize the process-global HTTP client. Call once from the embedder's /// `main()` before driving any agent turns. Idempotent: a second call with /// an already-initialized client is a no-op. pub fn initHttp(allocator: std.mem.Allocator, io: Io) void { if (global_http != null) return; global_http = .{ .allocator = allocator, .io = io }; } /// Tear down the process-global HTTP client. Call once at shutdown, after /// all turns have completed. pub fn deinitHttp() void { if (global_http) |*c| { c.deinit(); global_http = null; } } /// Borrow the process-global HTTP client. Asserts `initHttp` has run. pub fn httpClient() *std.http.Client { return &(global_http orelse @panic("libpanto: httpClient() called before initHttp()")); } const t = std.testing; test "ProviderConfig - openai_chat variant" { const cfg: ProviderConfig = .{ .openai_chat = .{ .api_key = "sk-test", .base_url = "https://api.openai.com/v1", .model = "gpt-4o", .reasoning = .high, } }; try t.expectEqual(APIStyle.openai_chat, cfg.style()); try t.expectEqualStrings("sk-test", cfg.openai_chat.api_key); try t.expectEqual(ReasoningEffort.high, cfg.openai_chat.reasoning); } test "ProviderConfig - anthropic_messages variant" { const cfg: ProviderConfig = .{ .anthropic_messages = .{ .api_key = "sk-ant-test", .base_url = "https://api.anthropic.com", .model = "claude-sonnet-4-20250514", } }; try t.expectEqual(APIStyle.anthropic_messages, cfg.style()); try t.expectEqualStrings("2023-06-01", cfg.anthropic_messages.api_version); try t.expectEqual(@as(u32, 64_000), cfg.anthropic_messages.max_tokens); } test "ProviderConfig - openai_chat reasoning defaults to .default" { const cfg: ProviderConfig = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m", } }; try t.expectEqual(ReasoningEffort.default, cfg.openai_chat.reasoning); } test "Config bundles provider + registry and forwards style" { var reg = ToolRegistry.init(t.allocator); defer reg.deinit(); const cfg: Config = .{ .provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m" } }, .registry = ®, }; try t.expectEqual(APIStyle.openai_chat, cfg.style()); } test "global http client: init/borrow/deinit" { var threaded: std.Io.Threaded = .init(t.allocator, .{}); defer threaded.deinit(); const io = threaded.io(); initHttp(t.allocator, io); defer deinitHttp(); initHttp(t.allocator, io); // idempotent _ = httpClient(); }