From 1beefefc69beee214430eb5bd2528a4f5692d2a8 Mon Sep 17 00:00:00 2001 From: t Date: Mon, 1 Jun 2026 08:21:00 -0600 Subject: Rename system extension layer to base for clarity Replace all references to the "system" layer with "base" to better reflect its role as the foundational extension/tool layer in panto's hierarchy. The layer hierarchy now consistently uses: project > user > base. Includes: - Update agent README and build.zig documentation - Refactor tool registry and config module to support layered lookups - Add TestHarness abstraction for cleaner test setup - Improve JSON serialization with wire-encoded tool names - Add glob pattern matching for tool/extension discovery --- libpanto/src/config.zig | 118 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 14 deletions(-) (limited to 'libpanto/src/config.zig') diff --git a/libpanto/src/config.zig b/libpanto/src/config.zig index e395230..6744b56 100644 --- a/libpanto/src/config.zig +++ b/libpanto/src/config.zig @@ -1,9 +1,24 @@ -//! Per-provider configuration. +//! Active configuration the agent consults on every turn. //! -//! `Config` 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. +//! `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 { @@ -43,19 +58,74 @@ pub const AnthropicMessagesConfig = struct { max_tokens: u32 = 4096, }; -pub const Config = union(APIStyle) { +/// Per-provider transport/auth/model configuration. Tagged by `APIStyle`. +pub const ProviderConfig = union(APIStyle) { openai_chat: OpenAIChatConfig, anthropic_messages: AnthropicMessagesConfig, - pub fn style(self: Config) APIStyle { + pub fn style(self: ProviderConfig) APIStyle { return @as(APIStyle, self); } }; -const t = @import("std").testing; +/// 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()")); +} -test "Config - openai_chat variant" { - const cfg: Config = .{ .openai_chat = .{ +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", @@ -66,8 +136,8 @@ test "Config - openai_chat variant" { try t.expectEqual(ReasoningEffort.high, cfg.openai_chat.reasoning); } -test "Config - anthropic_messages variant" { - const cfg: Config = .{ .anthropic_messages = .{ +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", @@ -77,11 +147,31 @@ test "Config - anthropic_messages variant" { try t.expectEqual(@as(u32, 4096), cfg.anthropic_messages.max_tokens); } -test "Config - openai_chat reasoning defaults to .default" { - const cfg: Config = .{ .openai_chat = .{ +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(); +} -- cgit v1.3