From f83578fdc9264019a1a1cef8c5484a161167d3dd Mon Sep 17 00:00:00 2001 From: t Date: Tue, 7 Jul 2026 11:26:32 -0600 Subject: initial commit, moved libpanto over from the pantograph repo --- src/public.zig | 299 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 src/public.zig (limited to 'src/public.zig') diff --git a/src/public.zig b/src/public.zig new file mode 100644 index 0000000..d8b9f7b --- /dev/null +++ b/src/public.zig @@ -0,0 +1,299 @@ +//! `public.zig` — the sole exported root of `libpanto`. +//! +//! This file is the **explicit public API allowlist**. Internal modules keep +//! their `pub` declarations (needed for cross-file linking) but are never +//! re-exported here; the public surface is what this file names, not +//! "whatever happens to be `pub`." +//! +//! The surface is organized around two user jobs (see +//! `docs/libpanto-cleanup.md`): +//! 1. Run an agent loop — configure a provider, register tools, submit a +//! turn, consume the pull event stream, persist. +//! 2. Construct & operate on conversations — build a `Conversation` by +//! hand, do context-management surgery. +//! +//! Every name here is a straight alias of an internal decl: the internal +//! types have been shaped to *be* the public API (public methods + a +//! transparent public field or two, every other field underscore-prefixed +//! internal state), so this file selects the surface rather than wrapping +//! it. Three buckets: +//! - **behavioral** (`Agent`, `Stream`, `Conversation`): heap-pinned +//! drivers whose `init` hands back a cheap, movable pointer; the chosen +//! methods plus a transparent field (`Agent.conversation`, +//! `Stream.state`) are the surface. +//! - **data (read/output)** (`Event`, `Usage`, `Pricing`, `SessionInfo`): +//! inspected field-by-field. +//! - **data (constructed)** (`ContentBlock`, `Message`, the block types, +//! the `Config` family): Zig users build them with `ArrayList` directly. + +const std = @import("std"); + +const config_mod = @import("config.zig"); +const auth_mod = @import("auth.zig"); +const conversation_mod = @import("conversation.zig"); +const agent_mod = @import("agent.zig"); +const stream_mod = @import("stream.zig"); +const provider_mod = @import("provider.zig"); +const tool_mod = @import("tool.zig"); +const tool_source_mod = @import("tool_source.zig"); +const pricing_mod = @import("pricing.zig"); +const session_store_mod = @import("session_store.zig"); +const fs_store_mod = @import("file_system_jsonl_store.zig"); +const null_store_mod = @import("null_store.zig"); + +// =========================================================================== +// Process lifecycle +// =========================================================================== + +/// Initialize the process-global HTTP client. Call once before any turn. +pub fn init(allocator: std.mem.Allocator, io: std.Io) void { + config_mod.initHttp(allocator, io); +} + +/// Tear down the process-global HTTP client. Call once at shutdown. +pub fn deinit() void { + config_mod.deinitHttp(); +} + +/// Borrow the process-global HTTP client (asserts `init` has run). Embedders +/// driving the OAuth auth flows need this to pass to `oauthLogin` etc. +pub const httpClient = config_mod.httpClient; + +// =========================================================================== +// Config (data, aliased) +// =========================================================================== + +pub const Config = config_mod.Config; +pub const ProviderConfig = config_mod.ProviderConfig; +pub const OpenAIChatConfig = config_mod.OpenAIChatConfig; +pub const AnthropicMessagesConfig = config_mod.AnthropicMessagesConfig; +pub const OpenAIResponsesConfig = config_mod.OpenAIResponsesConfig; +pub const APIStyle = config_mod.APIStyle; +pub const ReasoningEffort = config_mod.ReasoningEffort; +pub const Thinking = config_mod.Thinking; +pub const Effort = config_mod.Effort; +pub const CompactionConfig = config_mod.CompactionConfig; +pub const RetryConfig = config_mod.RetryConfig; +pub const WireIdentity = config_mod.WireIdentity; +pub const Header = config_mod.Header; + +// =========================================================================== +// Auth (data + flows, aliased) +// =========================================================================== + +pub const AuthConfig = auth_mod.AuthConfig; +pub const AuthType = auth_mod.AuthType; +pub const ApiKeyAuth = auth_mod.ApiKeyAuth; +pub const OAuthDeviceAuth = auth_mod.OAuthDeviceAuth; +pub const ExchangeConfig = auth_mod.ExchangeConfig; +pub const DeviceDialect = auth_mod.DeviceDialect; +pub const TokenRequestFormat = auth_mod.TokenRequestFormat; +pub const TokenSet = auth_mod.TokenSet; +pub const ParsedTokenSet = auth_mod.ParsedTokenSet; +pub const ResolvedCredential = auth_mod.ResolvedCredential; + +/// Token persistence under an embedder-chosen auth directory. Files are +/// written owner-only. +pub const loadTokenSet = auth_mod.loadTokenSet; +pub const saveTokenSet = auth_mod.saveTokenSet; +pub const deleteTokenSet = auth_mod.deleteTokenSet; + +// OAuth device-flow mechanism: interactive login, refresh, exchange, and the +// pure credential/JWT helpers the embedder's auth manager composes. +pub const AuthError = auth_mod.AuthError; +pub const Presenter = auth_mod.Presenter; +pub const DeviceCodePrompt = auth_mod.DeviceCodePrompt; +pub const OAuthTokens = auth_mod.OAuthTokens; +pub const oauthLogin = auth_mod.login; +pub const refreshTokens = auth_mod.refreshTokens; +pub const runExchange = auth_mod.runExchange; +pub const buildCredential = auth_mod.buildCredential; +pub const tokensToTokenSet = auth_mod.tokensToTokenSet; +pub const needsRefresh = auth_mod.needsRefresh; +pub const needsExchange = auth_mod.needsExchange; +pub const parseJwtExp = auth_mod.parseJwtExp; +pub const extractAccountId = auth_mod.extractAccountId; + +/// Non-streaming HTTP helper over the process-global client, plus JSON +/// path readers — the building blocks of the OAuth flows. +pub const http = @import("http_helper.zig"); + +// =========================================================================== +// Conversation construction (data, aliased) +// =========================================================================== + +pub const ContentBlock = conversation_mod.ContentBlock; +pub const Message = conversation_mod.Message; +pub const MessageRole = conversation_mod.MessageRole; +pub const TextualBlock = conversation_mod.TextualBlock; +/// Build a `TextualBlock` (the streaming text buffer used inside content +/// blocks) from a slice, copying the bytes with `alloc`. The canonical way +/// for binding code to construct `.Text`/`.Thinking`/tool-result text when +/// assembling `ContentBlock`s by hand (e.g. for conversation resumption). +pub const textualBlockFromSlice = conversation_mod.textualBlockFromSlice; +pub const SignatureOrigin = conversation_mod.SignatureOrigin; +/// Duplicate / free a `Message.identity`'s owned slices. Binding code uses +/// these to attach a per-message wire identity when rebuilding a conversation +/// for resumption (so it survives a later compaction). +pub const dupeWireIdentity = conversation_mod.dupeWireIdentity; +pub const freeWireIdentity = conversation_mod.freeWireIdentity; +pub const ThinkingBlock = conversation_mod.ThinkingBlock; +pub const ToolUseBlock = conversation_mod.ToolUseBlock; +pub const ToolResultBlock = conversation_mod.ToolResultBlock; +pub const ResultPartStored = conversation_mod.ResultPartStored; +pub const StoredMediaPart = conversation_mod.StoredMediaPart; +pub const SystemBlock = conversation_mod.SystemBlock; +pub const SystemMode = conversation_mod.SystemMode; +pub const CompactionSummaryBlock = conversation_mod.CompactionSummaryBlock; +pub const Usage = conversation_mod.Usage; +pub const effectiveSystemBlocks = conversation_mod.effectiveSystemBlocks; + +/// The conversation type. Aliased straight through: its interface has been +/// pared down to exactly the public surface (constructors `init`/`deinit`, +/// the `add*`/`replace*` builders, and the `messages`/`allocator` data +/// fields), so no façade is needed. This is what `Session.load` returns, +/// what `Agent.init` adopts, and what `Agent.conversation()` borrows a +/// pointer to (for in-place surgery). Build one standalone with `init` for +/// by-hand construction; once handed to an `Agent` it is owned by the agent. +pub const Conversation = conversation_mod.Conversation; + +// =========================================================================== +// Tools (data, aliased) +// =========================================================================== + +pub const Tool = tool_mod.Tool; +pub const ToolSource = tool_source_mod.ToolSource; +pub const ToolDecl = tool_mod.ToolDecl; +pub const ToolCall = tool_source_mod.Call; +pub const ToolCallResult = tool_source_mod.CallResult; +pub const MediaPart = tool_mod.MediaPart; +pub const ResultPart = tool_mod.ResultPart; + +/// Result-part ergonomics: a thin wrapper around `[]ResultPart` carrying +/// the `fromText`/`fromTextOwned` constructors and `deinit`. Aliased +/// straight through. +pub const ResultParts = tool_mod.ResultParts; + +// =========================================================================== +// Stream (behavioral, heap-pinned) +// =========================================================================== + +pub const Event = stream_mod.Event; +pub const ContentBlockType = provider_mod.ContentBlockType; +pub const ProviderRetryInfo = provider_mod.ProviderRetryInfo; + +/// The pull event stream for one turn. Aliased straight through: its public +/// surface is exactly `next`/`deinit` plus the transparent `state` field +/// (`State`); every other field is underscore-prefixed internal state. +/// `Agent.run` returns a `*Stream` (heap-pinned); the caller owns it and +/// must `deinit` it (which persists the turn tail and frees the allocation). +/// +/// `next()` returns `!?Event`: a value is progress (including the terminal +/// event), `null` is exhaustion, an error is a genuine failure. +pub const Stream = agent_mod.Stream; + +// =========================================================================== +// Agent (behavioral, heap-pinned) +// =========================================================================== + +pub const UserMessage = agent_mod.UserMessage; +pub const CompactionResult = agent_mod.CompactionResult; + +/// The agent loop driver. Aliased straight through: its public surface is +/// exactly the chosen methods (`init`/`deinit`, `registerTool`, +/// `registerToolSource`, `setConfig`, `run`, `addSystemMessage`, +/// `setSystemPrompt`, `compact`, `sessionId`) plus the transparent +/// `conversation` field; every other field is underscore-prefixed internal +/// state. +/// +/// `init` heap-pins the agent and returns a `*Agent` — a cheap, movable +/// handle out of the gate ("don't move the Agent" stops being a rule anyone +/// can violate). The caller owns it and must `deinit` it. `session` is +/// minted via `store.create()` (fresh) or `resolve`/`latest` (resume); when +/// `maybe_conversation` is non-null it is adopted (ownership transferred) — +/// the resume path hands the value returned by `Session.load`. +/// +/// Operate on the live conversation in place via the `conversation` field +/// (valid for the agent's lifetime; do not retain past it). +pub const Agent = agent_mod.Agent; + +// =========================================================================== +// Pricing (data, aliased) +// =========================================================================== + +pub const Pricing = pricing_mod.Pricing; +pub const PricingRegistry = pricing_mod.Registry; +/// Compute the cost of a single turn's `Usage` under the given `Pricing`, +/// in micro-cents (1/1,000,000 of a cent per token). Returns null when any +/// priced component of any nonzero category is null (the poison rule — +/// don't pretend a turn with unknown cache pricing is free). Aliased +/// through `libpanto` because the embedder accumulates session totals +/// across potentially-switched models. +pub const costMicroCents = pricing_mod.costMicroCents; +/// Add a turn's micro-cents cost to a running session total. Either +/// side null => result null. The single accumulation point for the +/// per-turn session cost (see `pricing.zig` for why). +pub const addCost = pricing_mod.addCost; + +// =========================================================================== +// Sessions +// =========================================================================== + +pub const SessionStore = session_store_mod.SessionStore; +pub const Session = session_store_mod.Session; +pub const SessionInfo = session_store_mod.SessionInfo; +pub const PersistentMessage = session_store_mod.PersistentMessage; +pub const NullStore = null_store_mod.NullStore; +pub const FileSystemJSONLStore = fs_store_mod.FileSystemJSONLStore; + +// =========================================================================== +// Tests +// =========================================================================== + +const openai_chat_json = @import("openai_chat_json.zig"); +const provider_openai_chat = @import("provider_openai_chat.zig"); +const anthropic_messages_json = @import("anthropic_messages_json.zig"); +const provider_anthropic_messages = @import("provider_anthropic_messages.zig"); +const openai_responses_json = @import("openai_responses_json.zig"); +const provider_openai_responses = @import("provider_openai_responses.zig"); +const compaction_mod = @import("compaction.zig"); +const sse_mod = @import("sse.zig"); +const tool_registry_mod = @import("tool_registry.zig"); +const session_mod = @import("session.zig"); +const turn_persist_mod = @import("turn_persist.zig"); +const image_mod = @import("image.zig"); + +test { + // See the note in the old root.zig: gate test logs at `.err` so expected + // warning-path tests stay quiet. + std.testing.log_level = .err; + + std.testing.refAllDecls(@This()); + // Internal modules' tests (not part of the public surface, but their + // tests must still run). + std.testing.refAllDecls(config_mod); + std.testing.refAllDecls(auth_mod); + std.testing.refAllDecls(@import("http_helper.zig")); + std.testing.refAllDecls(conversation_mod); + std.testing.refAllDecls(agent_mod); + std.testing.refAllDecls(stream_mod); + std.testing.refAllDecls(provider_mod); + std.testing.refAllDecls(tool_mod); + std.testing.refAllDecls(tool_source_mod); + std.testing.refAllDecls(tool_registry_mod); + std.testing.refAllDecls(pricing_mod); + std.testing.refAllDecls(session_store_mod); + std.testing.refAllDecls(fs_store_mod); + std.testing.refAllDecls(null_store_mod); + std.testing.refAllDecls(session_mod); + std.testing.refAllDecls(turn_persist_mod); + std.testing.refAllDecls(compaction_mod); + std.testing.refAllDecls(sse_mod); + std.testing.refAllDecls(image_mod); + std.testing.refAllDecls(openai_chat_json); + std.testing.refAllDecls(provider_openai_chat); + std.testing.refAllDecls(anthropic_messages_json); + std.testing.refAllDecls(provider_anthropic_messages); + std.testing.refAllDecls(openai_responses_json); + std.testing.refAllDecls(provider_openai_responses); +} -- cgit v1.3