summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-07 11:39:14 -0600
committert <t@tjp.lol>2026-06-07 11:39:14 -0600
commit457ee6a0d56c5a0470e77fca79d3e85f65f51fec (patch)
tree4f513379a10fb9db2c792d99933a85f4f7991459
parentb14859b9726185ab873356390068e887b7f486d3 (diff)
Add public.zig as the libpanto root; delete root.zig
public.zig is now the sole exported root (build.zig points the panto module at it). It defines the curated public API allowlist: behavioral facades (Agent/Stream/Conversation wrapping a pointer to the heap-pinned internal), the ResultParts struct, Pricing, the session seam, and data-type aliases for the Config/Conversation/Event families. A clearly-marked transitional block re-exports the internal module namespaces and the freestanding result-part helpers the panto CLI still imports directly; Phase 4 trims these as the CLI migrates onto the curated surface. Stream.Phase is now pub so State can alias it. The refAllDecls test contract moved from root.zig into public.zig.
-rw-r--r--docs/libpanto-cleanup.md11
-rw-r--r--libpanto/build.zig4
-rw-r--r--libpanto/src/agent.zig2
-rw-r--r--libpanto/src/public.zig292
-rw-r--r--libpanto/src/root.zig67
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);
-}