diff options
| author | t <t@tjp.lol> | 2026-06-01 08:21:00 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-01 11:20:56 -0600 |
| commit | 1beefefc69beee214430eb5bd2528a4f5692d2a8 (patch) | |
| tree | a459dbde5da17babe7d872999341d2006ebfe02e /src/main.zig | |
| parent | 5c016cfdbcb122e2b4f0496ef946642d68953c4b (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 'src/main.zig')
| -rw-r--r-- | src/main.zig | 196 |
1 files changed, 113 insertions, 83 deletions
diff --git a/src/main.zig b/src/main.zig index bdb5390..aaf86b5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,6 +9,8 @@ const self_exe = @import("self_exe.zig"); const subcommand = @import("subcommand.zig"); const session_paths = @import("session_paths.zig"); const models_toml = @import("models_toml.zig"); +const config_file = @import("config_file.zig"); +const glob = @import("glob.zig"); // Shorthand alias for the Lua C API. The bridge module owns the actual // `@cImport`; we re-use it here so the smoke check uses identical types. @@ -33,6 +35,8 @@ test { _ = self_exe; _ = subcommand; _ = models_toml; + _ = config_file; + _ = glob; } const Receiver = panto.provider.Receiver; @@ -195,74 +199,58 @@ fn luaSmokeCheck() void { lua.lua_settop(L, 0); } -fn loadConfig(environ_map: *const std.process.Environ.Map) !panto.config.Config { - const style_str = environ_map.get("PANTO_API_STYLE") orelse "openai_chat"; - const style = std.meta.stringToEnum(panto.config.APIStyle, style_str) orelse { - std.debug.print( - "error: PANTO_API_STYLE must be one of: openai_chat, anthropic_messages (got: {s})\n", - .{style_str}, - ); - return error.InvalidApiStyle; - }; +/// Print a friendly message for a config.toml load failure before the +/// process exits non-zero. +fn reportConfigError(err: anyerror) void { + switch (err) { + error.InvalidConfigToml => std.debug.print( + "error: a config.toml failed to parse (see log above).\n", + .{}, + ), + error.InvalidProvider => std.debug.print( + "error: a [providers.*] entry is malformed (need style + base_url).\n", + .{}, + ), + error.InvalidModelRef => std.debug.print( + "error: defaults.model must look like \"provider:model\".\n", + .{}, + ), + error.UnknownDefaultProvider => std.debug.print( + "error: defaults.model names a provider that isn't configured (or whose API key env var isn't set).\n", + .{}, + ), + error.PolicyConflict => std.debug.print( + "error: a tool/extension pattern appears in both allow and deny.\n", + .{}, + ), + error.NoHomeDirectory => std.debug.print( + "error: cannot resolve config paths — set HOME or XDG_CONFIG_HOME/XDG_DATA_HOME.\n", + .{}, + ), + else => std.debug.print("error: failed to load config: {s}\n", .{@errorName(err)}), + } +} - switch (style) { - .openai_chat => { - const api_key = environ_map.get("OPENAI_API_KEY") orelse { - std.debug.print("error: OPENAI_API_KEY is required\n", .{}); - return error.MissingApiKey; - }; - const base_url = environ_map.get("OPENAI_BASE_URL") orelse "https://api.openai.com/v1"; - const model = environ_map.get("OPENAI_MODEL") orelse { - std.debug.print("error: OPENAI_MODEL is required\n", .{}); - return error.MissingModel; - }; - const reasoning: panto.config.ReasoningEffort = - if (environ_map.get("OPENAI_REASONING")) |val| - std.meta.stringToEnum(panto.config.ReasoningEffort, val) orelse { - std.debug.print( - "error: OPENAI_REASONING must be one of: default, off, minimal, low, medium, high (got: {s})\n", - .{val}, - ); - return error.InvalidReasoning; - } - else - .default; - return .{ .openai_chat = .{ - .api_key = api_key, - .base_url = base_url, - .model = model, - .reasoning = reasoning, - } }; - }, - .anthropic_messages => { - const api_key = environ_map.get("ANTHROPIC_API_KEY") orelse { - std.debug.print("error: ANTHROPIC_API_KEY is required\n", .{}); - return error.MissingApiKey; - }; - const base_url = environ_map.get("ANTHROPIC_BASE_URL") orelse "https://api.anthropic.com"; - const model = environ_map.get("ANTHROPIC_MODEL") orelse { - std.debug.print("error: ANTHROPIC_MODEL is required\n", .{}); - return error.MissingModel; - }; - const api_version = environ_map.get("ANTHROPIC_API_VERSION") orelse "2023-06-01"; - const max_tokens: u32 = if (environ_map.get("ANTHROPIC_MAX_TOKENS")) |val| - std.fmt.parseInt(u32, val, 10) catch { - std.debug.print( - "error: ANTHROPIC_MAX_TOKENS must be a positive integer (got: {s})\n", - .{val}, - ); - return error.InvalidMaxTokens; - } - else - 4096; - return .{ .anthropic_messages = .{ - .api_key = api_key, - .base_url = base_url, - .model = model, - .api_version = api_version, - .max_tokens = max_tokens, - } }; +/// Print a friendly message for a model-selection failure. +fn reportModelError(err: anyerror, cfg: *const config_file.Config) void { + switch (err) { + error.NoModelSelected => { + std.debug.print( + "error: no model selected. Set `defaults.model = \"provider:alias\"` in config.toml.\n", + .{}, + ); + if (cfg.providers.len == 0) { + std.debug.print( + " (no providers resolved — check that an API key or its env var is present.)\n", + .{}, + ); + } }, + error.UnknownProvider => std.debug.print( + "error: the selected model names an unconfigured provider.\n", + .{}, + ), + else => std.debug.print("error: model selection failed: {s}\n", .{@errorName(err)}), } } @@ -294,8 +282,6 @@ pub fn main(init: std.process.Init) !void { .agent => {}, } - const config = try loadConfig(init.environ_map); - // Parse the agent-mode flags. Currently only `--resume [<id>]`. const cli_flags = try parseAgentFlags(alloc, init.minimal.args); defer cli_flags.deinit(alloc); @@ -315,19 +301,47 @@ pub fn main(init: std.process.Init) !void { const session_dir = try session_paths.sessionDirForCwd(alloc, init.environ_map, cwd); defer alloc.free(session_dir); - // Load the user's models.toml — a missing file is fine (empty - // registry). Cost lookups against an empty registry return null, - // and the display layer will format that as "unknown." + // Load the merged config.toml (base → user → project). Providers, + // default model, and tool/extension policy all come from here. + var app_config = config_file.load(alloc, io, init.environ_map, cwd) catch |err| { + reportConfigError(err); + std.process.exit(1); + }; + defer app_config.deinit(); + + // Load models.toml — model aliases (wire name + knobs) and pricing. + // A missing file is fine (empty registries); cost lookups return + // null, formatted as "unknown" by the display layer. const models_toml_path = try models_toml.configPath(alloc, init.environ_map); defer alloc.free(models_toml_path); - var pricing_registry = try models_toml.loadFromPath(alloc, io, models_toml_path); - defer pricing_registry.deinit(); - std.log.debug("models.toml: {d} entries from {s}", .{ pricing_registry.count(), models_toml_path }); + var models = try models_toml.loadFromPath(alloc, io, models_toml_path); + defer models.deinit(); + std.log.debug( + "models.toml: {d} model def(s), {d} price entr(ies) from {s}", + .{ models.defs.count(), models.pricing.count(), models_toml_path }, + ); + // `models.pricing` is parsed and ready; the print-based CLI doesn't + // surface per-turn cost yet (that lands with the TUI frontend). - const banner_model_initial: []const u8 = switch (config) { + // Choose the active model and assemble the provider config. + const model_ref = app_config.selectModel(&models.defs, null) catch |err| { + reportModelError(err, &app_config); + std.process.exit(1); + }; + const provider_config = config_file.buildProviderConfig(&app_config, &models.defs, model_ref) catch |err| { + reportModelError(err, &app_config); + std.process.exit(1); + }; + + // Process-global HTTP client: one connection pool for every provider / + // base_url the agent may switch to. Torn down at shutdown. + panto.config.initHttp(alloc, io); + defer panto.config.deinitHttp(); + + const banner_model_initial: []const u8 = switch (provider_config) { inline else => |c| c.model, }; - const banner_provider_initial: []const u8 = @tagName(config); + const banner_provider_initial: []const u8 = model_ref.provider; // Create or resume the session. Resume failures (missing/ambiguous id) // are user errors — print a tidy message and exit 1 rather than @@ -365,9 +379,11 @@ pub fn main(init: std.process.Init) !void { try appendSystemToSession(alloc, &session_mgr, system_text); } - const prov = try panto.provider.Provider.init(alloc, io, config); - var agent = panto.agent.Agent.init(alloc, io, prov); - defer agent.deinit(); + // The tool registry is owned here and referenced by the active config + // snapshot. Extensions register their tool source into it *before* the + // snapshot is built, so the agent's first turn sees them. + var registry = panto.ToolRegistry.init(alloc); + defer registry.deinit(); // Spin up the long-lived Lua runtime. All Lua extensions load into // one `lua_State`; module-global state survives across calls. The @@ -394,10 +410,10 @@ pub fn main(init: std.process.Init) !void { // chance to register tools that might want to yield. try rt.installScheduler(); - // Discover Lua extensions across three layers — system + // Discover Lua extensions across three layers — base // ($PANTO_HOME/agent), user ($XDG_CONFIG_HOME/panto or // $HOME/.config/panto), and project (./.panto). Project shadows - // user shadows system; tool-name collisions across surviving + // user shadows base; tool-name collisions across surviving // entries abort startup. const n_ext_tools = extension_loader.discoverAndLoad( alloc, @@ -405,6 +421,10 @@ pub fn main(init: std.process.Init) !void { init.environ_map, luarocks_rt.layout.agent_dir, rt, + .{ + .extensions = &app_config.extensions, + .tools = &app_config.tools, + }, ) catch |err| { std.log.err("extension discovery failed: {t}", .{err}); return err; @@ -412,10 +432,20 @@ pub fn main(init: std.process.Init) !void { std.log.debug("extensions: {d} tool(s) registered", .{n_ext_tools}); if (n_ext_tools > 0) { - try agent.registerToolSource(rt.toolSource()); + try registry.registerSource(rt.toolSource()); } - const banner_base: []const u8 = switch (config) { + // Assemble the active configuration snapshot (provider + tool registry) + // and create the agent holding a pointer to it. Swapping this pointer + // later (model/provider/tool changes) takes effect at the next turn. + const active_config: panto.config.Config = .{ + .provider = provider_config, + .registry = ®istry, + }; + var agent = panto.agent.Agent.init(alloc, io, &active_config); + defer agent.deinit(); + + const banner_base: []const u8 = switch (provider_config) { inline else => |c| c.base_url, }; try stdout.print( |
