diff options
| -rw-r--r-- | docs/libpanto-cleanup.md | 11 | ||||
| -rw-r--r-- | libpanto/build.zig | 4 | ||||
| -rw-r--r-- | libpanto/src/agent.zig | 2 | ||||
| -rw-r--r-- | libpanto/src/public.zig | 292 | ||||
| -rw-r--r-- | libpanto/src/root.zig | 67 |
5 files changed, 306 insertions, 70 deletions
diff --git a/docs/libpanto-cleanup.md b/docs/libpanto-cleanup.md index fbb4708..914285b 100644 --- a/docs/libpanto-cleanup.md +++ b/docs/libpanto-cleanup.md @@ -13,6 +13,17 @@ types, provider-loop internals). The smaller the surface, the better. This is a **prerequisite for Phase 1** of the bindings work: you cannot wrap a C ABI cleanly over an API whose boundary is undefined. +> **Updated during implementation (public.zig landed):** `public.zig` is now +> the module root (`build.zig` points `panto` at it) and `root.zig` is +> deleted; its `refAllDecls` test contract moved into `public.zig`. The +> curated façade (behavioral wrappers `Agent`/`Stream`/`Conversation`, +> `ResultParts`, the data aliases) is in place. `public.zig` *also* carries a +> clearly-marked **transitional** block re-exporting the internal module +> namespaces (`agent`, `conversation`, `session_store`, etc.) and the +> freestanding `textResult`/`ownedTextResult`/`freeResultParts` that the +> `panto` CLI still imports; Phase 4 trims these as the CLI migrates onto the +> curated surface. `Stream.Phase` was made `pub` so `State` can alias it. + ## Guiding principle: the public API is additive, expressed in one file We do **not** rewrite the internals. Almost everything we want already exists. diff --git a/libpanto/build.zig b/libpanto/build.zig index 973c75b..65f4093 100644 --- a/libpanto/build.zig +++ b/libpanto/build.zig @@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void { // libraries, so the module needs libc + the C translation unit + the // include path for `@cImport`. const panto_mod = b.addModule("panto", .{ - .root_source_file = b.path("src/root.zig"), + .root_source_file = b.path("src/public.zig"), .target = target, .optimize = optimize, .link_libc = true, @@ -18,7 +18,7 @@ pub fn build(b: *std.Build) void { // Unit tests const test_module = b.createModule(.{ - .root_source_file = b.path("src/root.zig"), + .root_source_file = b.path("src/public.zig"), .target = target, .optimize = optimize, .link_libc = true, diff --git a/libpanto/src/agent.zig b/libpanto/src/agent.zig index 604ee55..5f9bee5 100644 --- a/libpanto/src/agent.zig +++ b/libpanto/src/agent.zig @@ -1076,7 +1076,7 @@ pub const Stream = struct { /// drained. `next()` yields the queue first, then this error. pending_error: ?anyerror = null, - const Phase = enum { + pub const Phase = enum { /// Open the next provider response (with retries). turn_start, /// Pump the active provider response into events. diff --git a/libpanto/src/public.zig b/libpanto/src/public.zig new file mode 100644 index 0000000..44d13a9 --- /dev/null +++ b/libpanto/src/public.zig @@ -0,0 +1,292 @@ +//! `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. +//! +//! Three façade buckets: +//! - **behavioral** (`Agent`, `Stream`, `Conversation`): wrap a pointer to +//! a heap-pinned internal, exposing only the chosen methods. +//! - **data (read/output)** (`Event`, `Usage`, `Pricing`, `SessionInfo`): +//! aliased straight through; inspected field-by-field. +//! - **data (constructed)** (`ContentBlock`, `Message`, the block types, +//! the `Config` family): aliased straight through; Zig users build them +//! with `ArrayList` directly. + +const std = @import("std"); + +const config_mod = @import("config.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(); +} + +// =========================================================================== +// 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 APIStyle = config_mod.APIStyle; +pub const ReasoningEffort = config_mod.ReasoningEffort; +pub const CompactionConfig = config_mod.CompactionConfig; +pub const RetryConfig = config_mod.RetryConfig; +pub const WireIdentity = config_mod.WireIdentity; + +// =========================================================================== +// Conversation construction (data, aliased — Job 2) +// =========================================================================== + +pub const ContentBlock = conversation_mod.ContentBlock; +pub const Message = conversation_mod.Message; +pub const MessageRole = conversation_mod.MessageRole; +pub const TextualBlock = conversation_mod.TextualBlock; +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; + +/// A borrowed handle onto a conversation owned by an `Agent`, or a +/// standalone-owned conversation built by hand. Pointer-wrapping makes the +/// "don't move the conversation" invariant structural. +pub const Conversation = struct { + inner: *conversation_mod.Conversation, + + /// Read the in-memory messages directly, for context-management surgery. + pub fn messages(self: Conversation) []conversation_mod.Message { + return self.inner.messages.items; + } + pub fn addUserMessage(self: Conversation, text: []const u8) !void { + return self.inner.addUserMessage(text); + } + pub fn addAssistantMessage(self: Conversation, blocks: []const ContentBlock, usage: ?Usage) !void { + if (usage) |u| { + return self.inner.addAssistantMessageWithUsage(blocks, u); + } + return self.inner.addAssistantMessage(blocks); + } + pub fn addSystemMessage(self: Conversation, text: []const u8) !void { + return self.inner.addSystemMessage(text); + } + pub fn replaceSystemMessage(self: Conversation, text: []const u8) !void { + return self.inner.replaceSystemMessage(text); + } + pub fn addCompactionSummary(self: Conversation, text: []const u8) !void { + return self.inner.addCompactionSummary(text); + } +}; + +// =========================================================================== +// Tools (data + small façade — Job 1) +// =========================================================================== + +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 struct wrapper around `[]ResultPart` +/// (a bare alias can't carry methods). +pub const ResultParts = struct { + items: []ResultPart, + + pub fn fromText(alloc: std.mem.Allocator, text: []const u8) !ResultParts { + return .{ .items = try tool_mod.textResult(alloc, text) }; + } + pub fn fromTextOwned(alloc: std.mem.Allocator, text: []u8) !ResultParts { + return .{ .items = try tool_mod.ownedTextResult(alloc, text) }; + } + pub fn deinit(self: ResultParts, alloc: std.mem.Allocator) void { + tool_mod.freeResultParts(alloc, self.items); + } +}; + +// =========================================================================== +// Stream (behavioral, pointer-wrapped) +// =========================================================================== + +pub const Event = stream_mod.Event; +pub const ContentBlockType = provider_mod.ContentBlockType; +pub const ProviderRetryInfo = provider_mod.ProviderRetryInfo; + +/// The transparent turn state machine, renamed from the internal `Phase`. +pub const State = agent_mod.Stream.Phase; + +pub const Stream = struct { + inner: *agent_mod.Stream, + + /// `!?Event`: a value is progress (including the terminal event), `null` + /// is exhaustion, an error is a genuine failure. + pub fn next(self: Stream) !?Event { + return self.inner.next(); + } + pub fn deinit(self: Stream) void { + // The internal `Stream.deinit` persists the turn tail and frees its + // own heap allocation. + self.inner.deinit(); + } + pub fn state(self: Stream) State { + return self.inner.phase; + } +}; + +// =========================================================================== +// Agent (behavioral, pointer-wrapped) +// =========================================================================== + +pub const UserMessage = struct { text: []const u8 }; +pub const CompactionResult = agent_mod.Agent.CompactionResult; + +pub const Agent = struct { + inner: *agent_mod.Agent, + + pub fn registerTool(self: Agent, t: Tool) !void { + return self.inner.registerTool(t); + } + pub fn registerToolSource(self: Agent, src: ToolSource) !void { + return self.inner.registerToolSource(src); + } + pub fn setConfig(self: Agent, cfg: *const Config) void { + self.inner.setConfig(cfg); + } + pub fn run(self: Agent, message: UserMessage) !Stream { + return .{ .inner = try self.inner.run(.{ .text = message.text }) }; + } + /// Compact and persist (the explicit `/compact` entry point). + pub fn compact(self: Agent, system_prompt: []const u8, extra: ?[]const u8) !CompactionResult { + return self.inner.compactAndPersist(system_prompt, extra); + } + /// A borrowed handle onto the agent's live conversation. + pub fn conversation(self: Agent) Conversation { + return .{ .inner = &self.inner.conversation }; + } +}; + +// =========================================================================== +// Pricing (data, aliased — Job 1) +// =========================================================================== + +pub const Pricing = pricing_mod.Pricing; +pub const PricingRegistry = pricing_mod.Registry; + +// =========================================================================== +// 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; + +// =========================================================================== +// TRANSITIONAL internal escape hatches (Phase 4 removes these). +// +// The `panto` CLI still imports several internal module namespaces directly. +// Until the CLI is migrated onto the curated surface above, re-expose them +// here so the library still presents one root. Each of these is a candidate +// for deletion once the CLI no longer needs it. +// =========================================================================== + +pub const config = config_mod; +pub const conversation = conversation_mod; +pub const agent = agent_mod; +pub const stream = stream_mod; +pub const provider = provider_mod; +pub const tool = tool_mod; +pub const tool_source = tool_source_mod; +pub const pricing = pricing_mod; +pub const session_store = session_store_mod; +pub const session_manager = fs_store_mod; +pub const null_store = null_store_mod; +// Freestanding result-part helpers, superseded by `ResultParts`. The CLU's +// Lua bridge still calls these directly; Phase 4 migrates it to `ResultParts`. +pub const freeResultParts = tool_mod.freeResultParts; +pub const textResult = tool_mod.textResult; +pub const ownedTextResult = tool_mod.ownedTextResult; + +// =========================================================================== +// 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 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(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); +} diff --git a/libpanto/src/root.zig b/libpanto/src/root.zig deleted file mode 100644 index fb8c1df..0000000 --- a/libpanto/src/root.zig +++ /dev/null @@ -1,67 +0,0 @@ -const std = @import("std"); - -pub const conversation = @import("conversation.zig"); -pub const provider = @import("provider.zig"); -pub const stream = @import("stream.zig"); -pub const agent = @import("agent.zig"); -pub const config = @import("config.zig"); -pub const sse = @import("sse.zig"); -pub const tool = @import("tool.zig"); -pub const tool_source = @import("tool_source.zig"); -pub const tool_registry = @import("tool_registry.zig"); -pub const session = @import("session.zig"); -pub const session_manager = @import("file_system_jsonl_store.zig"); -pub const session_store = @import("session_store.zig"); -pub const null_store = @import("null_store.zig"); -pub const turn_persist = @import("turn_persist.zig"); -pub const pricing = @import("pricing.zig"); -pub const compaction = @import("compaction.zig"); -pub const image = @import("image.zig"); - -// Re-exports for ergonomic embedder use. -pub const Tool = tool.Tool; -pub const ResultPart = tool.ResultPart; -pub const MediaPart = tool.MediaPart; -pub const freeResultParts = tool.freeResultParts; -pub const textResult = tool.textResult; -pub const ownedTextResult = tool.ownedTextResult; -pub const ToolSource = tool_source.ToolSource; -pub const ToolDecl = tool_source.ToolDecl; -pub const ToolCall = tool_source.Call; -pub const ToolCallResult = tool_source.CallResult; -pub const ToolRegistry = tool_registry.ToolRegistry; -pub const Config = config.Config; -pub const ProviderConfig = config.ProviderConfig; - -// Re-export the pull-streaming surface for embedders. -pub const Event = stream.Event; -pub const Stream = agent.Stream; -pub const Agent = agent.Agent; - -// Internal modules. Not part of the public API — callers drive turns via -// the `Agent` (which holds a swappable `*const Config`): `agent.run()` -// returns a `*Stream` whose `next()` pulls one `Event` at a time. The -// process-global HTTP client is initialized with `config.initHttp` / torn -// down with `config.deinitHttp`. These impls are exposed here only so -// `refAllDecls` picks up their 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"); - -test { - // Test contract: deliberate error-path tests should not produce visible - // log output. Library code logs at `.err` for genuine production failures - // and `.warn` for expected-failure paths exercised by tests; the test - // runner's logger gates on `std.testing.log_level`, which defaults to - // `.warn`. Raising it to `.err` silences expected warnings without - // changing production behavior. Anything that *should* be visible in a - // passing test must use `std.debug.print` or assert via the testing API. - std.testing.log_level = .err; - - std.testing.refAllDecls(@This()); - std.testing.refAllDecls(openai_chat_json); - std.testing.refAllDecls(provider_openai_chat); - std.testing.refAllDecls(anthropic_messages_json); - std.testing.refAllDecls(provider_anthropic_messages); -} |
