From 621d7fee0ace729f8d44126032d2c6e13f72ee7f Mon Sep 17 00:00:00 2001 From: t Date: Tue, 7 Jul 2026 11:29:23 -0600 Subject: Move libpanto projects to libpantograph dependency Remove the in-repo libpanto sources and binding projects from pantograph. Consume libpantograph through the Zig package URL at code.tjp.lol/libpantograph.git, including the Lua module artifact used by CLI extensions. --- libpanto/src/config.zig | 403 ------------------------------------------------ 1 file changed, 403 deletions(-) delete mode 100644 libpanto/src/config.zig (limited to 'libpanto/src/config.zig') diff --git a/libpanto/src/config.zig b/libpanto/src/config.zig deleted file mode 100644 index f2f04e7..0000000 --- a/libpanto/src/config.zig +++ /dev/null @@ -1,403 +0,0 @@ -//! 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` carries the active `ProviderConfig` (plus retry/compaction -//! policy). 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, -//! and base_url 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. -//! -//! The tool set is **not** part of `Config` — it lives on the `Agent` -//! (`Agent.registerTool`/`registerToolSource`), so swapping provider/model -//! between turns no longer means rebuilding the tool list. - -const std = @import("std"); -const Io = std.Io; - -/// The wire dialect a provider speaks. -pub const APIStyle = enum { - openai_chat, - anthropic_messages, - /// OpenAI Responses API (`/responses`). Used by the ChatGPT-subscription - /// Codex backend, whose request/stream shape differs from Chat Completions. - openai_responses, - /// ChatGPT-subscription Codex Responses dialect. User config selects this - /// with `style = "openai_responses"` plus `dialect = "codex"`. - openai_codex_responses, -}; - -/// A single HTTP request header (name/value). Used for provider -/// `extra_headers` — caller-supplied headers merged onto a provider's -/// built-in request headers (e.g. GitHub Copilot's editor-identity headers, -/// or auth-derived headers from an OAuth exchange). Borrowed slices; valid -/// as long as the owning config is. -pub const Header = struct { - name: []const u8, - value: []const u8, -}; - -/// 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, -}; - -/// Anthropic extended-thinking mode. -pub const Thinking = enum { - /// Do not request extended thinking. - disabled, - /// Manual extended thinking with an explicit token budget (`thinking_budget_tokens`). - /// Supported on Haiku 4.5 and older models, but NOT on Opus 4.8+ (which - /// only accepts adaptive). Not safe as a cross-model default. - enabled, - /// Adaptive thinking: Claude decides when and how much to think. - /// Requires Opus 4.6 / Sonnet 4.6 or newer; automatic on Opus 4.7+. - adaptive, -}; - -/// Effort level sent with Anthropic adaptive thinking (`thinking = .adaptive`). -/// Ignored when `thinking` is `.enabled` or `.disabled`. -pub const Effort = enum { - low, - medium, - high, - xhigh, - max, -}; - -pub const OpenAIChatConfig = struct { - api_key: []const u8, - base_url: []const u8, - model: []const u8, - reasoning: ReasoningEffort = .default, - max_tokens: u32 = 64_000, - /// Caller-supplied request headers merged onto the built-in ones - /// (content-type/accept/authorization). Used for provider identity - /// headers and auth-exchange-derived headers. Empty by default. - /// Borrowed; valid as long as this config is. - extra_headers: []const Header = &.{}, -}; - -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, - /// Extended-thinking mode. `.enabled` sends a manual thinking block with - /// `thinking_budget_tokens`; `.adaptive` lets Claude decide and uses - /// `effort` instead. `.disabled` omits thinking entirely. - /// - /// Defaults to `.disabled`: it is the only mode accepted by every current - /// model. `.enabled` fails on Opus 4.8+ (adaptive-only) and `.adaptive` - /// fails on Haiku 4.5 (no adaptive support), so neither is a safe default - /// without parsing model strings. - thinking: Thinking = .disabled, - /// Effort level for adaptive thinking. Only emitted on the wire when - /// `thinking == .adaptive`; ignored otherwise. - effort: Effort = .medium, - /// Maximum tokens Claude may spend on internal reasoning when - /// `thinking == .enabled`. `null` falls back to `max_tokens - 1`. - /// Ignored when `thinking == .adaptive` or `.disabled`. - thinking_budget_tokens: ?u32 = 32_000, - /// When true and `thinking == .enabled`, sends the - /// `interleaved-thinking-2025-05-14` beta header so Claude can think - /// between tool calls. Ignored when `thinking == .adaptive` (interleaving - /// is automatic there) or `.disabled`. - thinking_interleaved: bool = false, - /// Use `Authorization: Bearer ...` instead of `x-api-key`. This is set - /// by the embedder from the configured auth family (not guessed from the - /// base URL): standard Anthropic uses `false`, while OAuth-backed - /// Anthropic-compatible providers such as Copilot set `true`. - use_bearer_auth: bool = false, - /// Place one `cache_control` breakpoint on the last cacheable block of - /// each request, replicating Anthropic's "automatic caching" (a single - /// advancing breakpoint) via the broadly-supported per-block marker - /// rather than the top-level field. - /// - /// Defaults on: for a normal append-only multi-turn session it's a - /// near-pure win (reads 0.1x base input vs. a one-time 1.25x write). - /// Set false when the workload makes that write premium unrecoverable - /// — e.g. one-shot requests, or an embedder that aggressively rewrites - /// history so prefixes are never reused. There the 1.25x write is pure - /// overhead with no read to amortize it. - prompt_cache: bool = true, - /// Caller-supplied request headers merged onto the built-in ones. See - /// `OpenAIChatConfig.extra_headers`. Empty by default. - extra_headers: []const Header = &.{}, -}; - -/// OpenAI Responses API config. Same transport knobs as `openai_chat`, but a -/// distinct request body (`input` items, `reasoning`, `store`, `include`) and -/// streaming event shape. Auth-derived headers (`chatgpt-account-id`) and the -/// static Codex identity headers (`OpenAI-Beta`, `originator`) ride on -/// `extra_headers`. -pub const OpenAIResponsesConfig = struct { - api_key: []const u8, - base_url: []const u8, - model: []const u8, - reasoning: ReasoningEffort = .default, - max_tokens: u32 = 64_000, - extra_headers: []const Header = &.{}, -}; - -/// Per-provider transport/auth/model configuration. Tagged by `APIStyle`. -pub const ProviderConfig = union(APIStyle) { - openai_chat: OpenAIChatConfig, - anthropic_messages: AnthropicMessagesConfig, - openai_responses: OpenAIResponsesConfig, - openai_codex_responses: OpenAIResponsesConfig, - - pub fn style(self: ProviderConfig) APIStyle { - return @as(APIStyle, self); - } - - /// The wire-format provider identity for this config: the ground-truth - /// `{api_style, base_url, model, reasoning}` that a turn is sent with. - /// Anthropic carries thinking/effort/budget/interleaved instead of - /// reasoning. Borrowed slices; valid as long as the config is. - pub fn wireIdentity(self: ProviderConfig) WireIdentity { - return switch (self) { - .openai_chat => |c| .{ - .api_style = .openai_chat, - .base_url = c.base_url, - .model = c.model, - .reasoning = c.reasoning, - }, - .anthropic_messages => |c| .{ - .api_style = .anthropic_messages, - .base_url = c.base_url, - .model = c.model, - .thinking = c.thinking, - .effort = c.effort, - .thinking_budget_tokens = c.thinking_budget_tokens, - .thinking_interleaved = c.thinking_interleaved, - }, - .openai_responses => |c| .{ - .api_style = .openai_responses, - .base_url = c.base_url, - .model = c.model, - .reasoning = c.reasoning, - }, - .openai_codex_responses => |c| .{ - .api_style = .openai_codex_responses, - .base_url = c.base_url, - .model = c.model, - .reasoning = c.reasoning, - }, - }; - } -}; - -/// Wire-format provider identity (see `ProviderConfig.wireIdentity`). This -/// is the same shape as `session_store.WireIdentity`; defined here to avoid -/// a module cycle (config must not import session_store). -/// -/// OpenAI uses `reasoning`; Anthropic uses `thinking`/`effort`/ -/// `thinking_budget_tokens`/`thinking_interleaved`. Fields unused by the -/// active provider carry their zero/default values. -pub const WireIdentity = struct { - api_style: APIStyle, - base_url: []const u8, - model: []const u8, - /// OpenAI only. - reasoning: ReasoningEffort = .default, - /// Anthropic only. - thinking: Thinking = .enabled, - /// Anthropic only; only meaningful when `thinking == .adaptive`. - effort: Effort = .medium, - /// Anthropic only; only meaningful when `thinking == .enabled`. - thinking_budget_tokens: ?u32 = 32_000, - /// Anthropic only; only meaningful when `thinking == .enabled`. - thinking_interleaved: bool = false, -}; - -/// Compaction settings the agent consults when summarizing old history. -/// -/// `keep_verbatim` is the budget (in tokens) governing how much recent -/// conversation is kept verbatim after compaction. The retention walk -/// accumulates whole turns backward and stops once the running total -/// exceeds this value, so `keep_verbatim` is an *upper bound* on the size -/// of the kept suffix (the turn that crosses the threshold is summarized, -/// not kept). -/// -/// `model`, when set, is the provider/model used to run the compaction -/// request itself. On failure (e.g. the compaction model rejects the -/// transcript for context length) the agent falls back to the active chat -/// model. When null, compaction uses the active chat model directly. -pub const CompactionConfig = struct { - keep_verbatim: u32 = 20_000, - model: ?ProviderConfig = null, - /// The compaction system prompt. Used both for automatic compaction on - /// context overflow and as the default for an explicit `Agent.compact` - /// call (which may override it per-call). Borrowed; set by the embedder - /// (e.g. resolved from its `COMPACTION.md` layers, or a built-in - /// default). When null, auto-compaction is disabled and a - /// context-overflow error propagates unchanged, and an explicit - /// `compact` with no override is a no-op error. - compaction_prompt: ?[]const u8 = null, -}; - -/// Policy for retrying transient provider/API failures. Conservative -/// defaults: four attempts (one initial + three retries) with exponential -/// backoff and jitter, capped at 10s per delay. -pub const RetryConfig = struct { - /// Total attempts including the first. `4` => initial try + up to 3 - /// retries. Must be >= 1. - max_attempts: usize = 4, - /// Base delay before the first retry, in milliseconds. - initial_delay_ms: u64 = 500, - /// Upper bound on any single backoff delay, in milliseconds. Also caps - /// a provider-supplied `Retry-After`. - max_delay_ms: u64 = 10_000, - /// Exponential growth factor applied per retry. - multiplier: f64 = 2.0, - /// When true, apply random jitter in `[0, computed_delay)` (full - /// jitter) to avoid thundering-herd retries. - jitter: bool = true, -}; - -/// An immutable snapshot of the provider/model the agent talks to, plus -/// retry and compaction policy. The agent holds a `*const Config` and -/// re-reads it each turn; replacing the pointer swaps the active -/// configuration at the next turn boundary. The tool set is owned by the -/// `Agent`, not the snapshot, so a swap never touches tools. -pub const Config = struct { - provider: ProviderConfig, - compaction: CompactionConfig = .{}, - retry: RetryConfig = .{}, - - 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); - try t.expectEqual(false, cfg.anthropic_messages.use_bearer_auth); - try t.expectEqual(Thinking.disabled, cfg.anthropic_messages.thinking); - try t.expectEqual(Effort.medium, cfg.anthropic_messages.effort); - try t.expectEqual(@as(?u32, 32_000), cfg.anthropic_messages.thinking_budget_tokens); - try t.expectEqual(false, cfg.anthropic_messages.thinking_interleaved); -} - -test "ProviderConfig - anthropic_messages wireIdentity carries thinking fields" { - const cfg: ProviderConfig = .{ .anthropic_messages = .{ - .api_key = "k", - .base_url = "https://api.anthropic.com", - .model = "claude-opus-4-8", - .thinking = .adaptive, - .effort = .high, - .thinking_budget_tokens = null, - .thinking_interleaved = false, - } }; - const id = cfg.wireIdentity(); - try t.expectEqual(APIStyle.anthropic_messages, id.api_style); - try t.expectEqual(Thinking.adaptive, id.thinking); - try t.expectEqual(Effort.high, id.effort); - try t.expectEqual(@as(?u32, null), id.thinking_budget_tokens); - try t.expectEqual(false, id.thinking_interleaved); - // reasoning field carries its zero default; not meaningful for Anthropic - try t.expectEqual(ReasoningEffort.default, id.reasoning); -} - -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 carries provider and forwards style" { - const cfg: Config = .{ - .provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m" } }, - }; - 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