summaryrefslogtreecommitdiff
path: root/libpanto/src/config.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-01 08:21:00 -0600
committert <t@tjp.lol>2026-06-01 11:20:56 -0600
commit1beefefc69beee214430eb5bd2528a4f5692d2a8 (patch)
treea459dbde5da17babe7d872999341d2006ebfe02e /libpanto/src/config.zig
parent5c016cfdbcb122e2b4f0496ef946642d68953c4b (diff)
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
Diffstat (limited to 'libpanto/src/config.zig')
-rw-r--r--libpanto/src/config.zig118
1 files changed, 104 insertions, 14 deletions
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 = &reg,
+ };
+ 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();
+}