summaryrefslogtreecommitdiff
path: root/libpanto/src/config.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/config.zig')
-rw-r--r--libpanto/src/config.zig403
1 files changed, 0 insertions, 403 deletions
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();
-}