diff options
| author | t <t@tjp.lol> | 2026-06-07 11:42:41 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-07 11:42:41 -0600 |
| commit | 11818552dad254649af012df3b4277633943d2b6 (patch) | |
| tree | 88c60f54b0a71d92adb4b09c7ff984080b5d86ac | |
| parent | 457ee6a0d56c5a0470e77fca79d3e85f65f51fec (diff) | |
Migrate panto CLI onto the public.zig surface
Move the CLI off the internal libpanto module namespaces onto the curated
public API: data-type aliases (Config family, Message/MessageRole/
effectiveSystemBlocks, Event, Pricing/PricingRegistry, the session seam,
FileSystemJSONLStore, ContentBlockType), process lifecycle (panto.init/
deinit), and ResultParts.fromText/fromTextOwned/deinit in place of the
freestanding textResult/ownedTextResult/freeResultParts.
The CLI remains on two flagged internal namespaces, panto.agent and
panto.conversation: it is a deep embedder that drives the loop below the
curated Agent/Conversation facades (system-prompt seeding through the
agent, compaction_system_prompt, the open_stream_fn test seam, standalone
Conversation values, compactAndPersist). These stay re-exported from
public.zig as a documented deep-embedder escape hatch; trimming the
transitional block removed every other internal re-export.
| -rw-r--r-- | docs/libpanto-cleanup.md | 23 | ||||
| -rw-r--r-- | libpanto/src/public.zig | 39 | ||||
| -rw-r--r-- | src/config_file.zig | 12 | ||||
| -rw-r--r-- | src/extension_loader.zig | 2 | ||||
| -rw-r--r-- | src/lua_bridge.zig | 10 | ||||
| -rw-r--r-- | src/lua_runtime.zig | 10 | ||||
| -rw-r--r-- | src/main.zig | 22 | ||||
| -rw-r--r-- | src/models_toml.zig | 6 | ||||
| -rw-r--r-- | src/subcommand.zig | 2 | ||||
| -rw-r--r-- | src/system_prompt.zig | 22 |
10 files changed, 83 insertions, 65 deletions
diff --git a/docs/libpanto-cleanup.md b/docs/libpanto-cleanup.md index 914285b..5fb8db3 100644 --- a/docs/libpanto-cleanup.md +++ b/docs/libpanto-cleanup.md @@ -106,6 +106,29 @@ natural there as well.) --- +> **Updated during implementation (CLI migration landed):** the `panto` CLI +> now consumes the curated `public.zig` surface for all data types +> (`Config`/`ProviderConfig`/`ReasoningEffort`/..., `Message`/`MessageRole`/ +> `effectiveSystemBlocks`, `Event`, `Pricing`/`PricingRegistry`, +> `Session`/`SessionStore`/`WireIdentity`/`PersistentMessage`, +> `FileSystemJSONLStore`, `ContentBlockType`), for process lifecycle +> (`panto.init`/`panto.deinit`), and for result-part ergonomics +> (`ResultParts.fromText`/`fromTextOwned`/`deinit`, replacing the freestanding +> `textResult`/`ownedTextResult`/`freeResultParts`). +> +> **FLAGGED:** the CLI still reaches two internal namespaces — `panto.agent` +> and `panto.conversation` — because it is a *deep* embedder that drives the +> loop below the curated `Agent`/`Conversation` façades: it needs +> system-prompt seeding *through* the agent (`agent.addSystemMessage`), +> `compaction_system_prompt`, the injectable `open_stream_fn` test seam, +> direct `agent.conversation` field access, raw standalone `Conversation` +> values (the façade `Conversation` only wraps a pointer to an agent-owned +> one), and `compactAndPersist`. These two namespaces remain re-exported from +> `public.zig` as a documented deep-embedder escape hatch (not part of the +> stable surface a C-ABI/binding should use). Closing this gap — growing the +> façade with standalone `Conversation` construction and agent +> system-prompt/compaction plumbing — is the natural follow-up. + ## The two jobs the API must serve Everything below is derived from two user jobs, not from "the CLI happens to diff --git a/libpanto/src/public.zig b/libpanto/src/public.zig index 44d13a9..951bd3a 100644 --- a/libpanto/src/public.zig +++ b/libpanto/src/public.zig @@ -220,30 +220,25 @@ pub const NullStore = null_store_mod.NullStore; pub const FileSystemJSONLStore = fs_store_mod.FileSystemJSONLStore; // =========================================================================== -// TRANSITIONAL internal escape hatches (Phase 4 removes these). +// Deep-embedder escape hatches. // -// 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; +// The `panto` CLI is a *deep* embedder: it drives the agent loop at a level +// the curated `Agent`/`Conversation` façades deliberately do not expose +// (system-prompt seeding through the agent, `compaction_system_prompt`, the +// injectable `open_stream_fn` test seam, direct `conversation` field access, +// raw standalone `Conversation` values, `compactAndPersist`, etc.). Rather +// than inflate the curated surface to cover one in-tree embedder, these two +// internal namespaces stay reachable for embedders who opt into the +// unstable internal API. A C-ABI or language binding should use the curated +// surface above, not these. +// +// FLAGGED (see docs/libpanto-cleanup.md): exposing `agent`/`conversation` +// internals is a known gap between the curated façade and what a deep +// embedder needs. Revisit if/when the façade grows the missing operations +// (e.g. `Conversation` standalone construction, agent system-prompt and +// compaction-prompt plumbing). 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; +pub const conversation = conversation_mod; // =========================================================================== // Tests diff --git a/src/config_file.zig b/src/config_file.zig index f1ae65f..229a5b2 100644 --- a/src/config_file.zig +++ b/src/config_file.zig @@ -51,7 +51,7 @@ const models_toml = @import("models_toml.zig"); // constructors/mutators. const tvalue = toml.value_mod; -pub const APIStyle = panto.config.APIStyle; +pub const APIStyle = panto.APIStyle; // =========================================================================== // Resolved config model @@ -109,7 +109,7 @@ pub const Policy = struct { } }; -/// Build a `panto.config.ProviderConfig` for a chosen `<provider>:<alias>` model +/// Build a `panto.ProviderConfig` for a chosen `<provider>:<alias>` model /// reference, combining the resolved provider (transport/auth) with the /// model definition from `models.toml` (wire name + knobs). /// @@ -117,19 +117,19 @@ pub const Policy = struct { /// a missing alias falls back to using the alias verbatim as the wire /// model name with default knobs. Returns the assembled `Config` plus the /// wire model id (borrowed from `defs`/`ref` — valid as long as both -/// outlive the returned config's use). The `panto.config.ProviderConfig` itself +/// outlive the returned config's use). The `panto.ProviderConfig` itself /// borrows the provider/model strings; the caller must keep `cfg` (the /// `Config`) and `defs` alive for its lifetime. pub fn buildProviderConfig( cfg: *const Config, defs: *const models_toml.ModelRegistry, ref: ModelRef, -) ResolveError!panto.config.ProviderConfig { +) ResolveError!panto.ProviderConfig { const prov = cfg.provider(ref.provider) orelse return error.UnknownProvider; const def_opt = defs.get(ref.provider, ref.model); const wire_model: []const u8 = if (def_opt) |d| d.model else ref.model; - const reasoning: panto.config.ReasoningEffort = if (def_opt) |d| d.reasoning else .default; + const reasoning: panto.ReasoningEffort = if (def_opt) |d| d.reasoning else .default; const max_tokens: u32 = if (def_opt) |d| (d.max_tokens orelse 64_000) else 64_000; switch (prov.style) { @@ -944,7 +944,7 @@ test "buildProviderConfig: missing alias uses the alias verbatim as wire model" const pc = try buildProviderConfig(&cfg, &models, ref); try testing.expectEqual(APIStyle.openai_chat, pc.style()); try testing.expectEqualStrings("gpt-4o", pc.openai_chat.model); - try testing.expectEqual(panto.config.ReasoningEffort.default, pc.openai_chat.reasoning); + try testing.expectEqual(panto.ReasoningEffort.default, pc.openai_chat.reasoning); } test "buildProviderConfig: unknown provider errors" { diff --git a/src/extension_loader.zig b/src/extension_loader.zig index 6d470cc..ff49022 100644 --- a/src/extension_loader.zig +++ b/src/extension_loader.zig @@ -507,7 +507,7 @@ fn xokText(result: panto.ToolCallResult) []const u8 { /// Test helper: free a results slice (parts on `.ok`). fn xfreeResults(results: []panto.ToolCallResult) void { for (results) |r| switch (r) { - .ok => |b| panto.freeResultParts(testing.allocator, b), + .ok => |b| (panto.ResultParts{ .items = b }).deinit(testing.allocator), .err => {}, }; } diff --git a/src/lua_bridge.zig b/src/lua_bridge.zig index 245b0ec..444cedc 100644 --- a/src/lua_bridge.zig +++ b/src/lua_bridge.zig @@ -270,7 +270,7 @@ pub fn pushJsonAsLua( /// one `.media` part per attachment (`data` is base64-encoded bytes). /// /// Every returned slice/part owns its bytes (allocated with `allocator`); -/// the caller frees via `panto.freeResultParts`. +/// the caller frees via `panto.ResultParts.deinit`. pub fn readHandlerResult( L: *c.lua_State, idx: c_int, @@ -281,8 +281,8 @@ pub fn readHandlerResult( var len: usize = 0; const ptr = c.lua_tolstring(L, idx, &len); if (ptr == null) return BridgeError.BadHandlerReturn; - return (panto.textResult(allocator, ptr[0..len]) catch - BridgeError.OutOfMemory); + return ((panto.ResultParts.fromText(allocator, ptr[0..len]) catch + return BridgeError.OutOfMemory).items); } if (ty != T_TABLE) return BridgeError.BadHandlerReturn; return readHandlerResultTable(L, idx, allocator); @@ -796,7 +796,7 @@ test "handler invocation: input parsed, result captured" { } const result = try readHandlerResult(L, -1, std.testing.allocator); - defer panto.freeResultParts(std.testing.allocator, result); + defer (panto.ResultParts{ .items = result }).deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), result.len); try std.testing.expectEqualStrings("got: hello", result[0].text); } @@ -821,7 +821,7 @@ test "readHandlerResult: table with text and attachments" { } const result = try readHandlerResult(L, -1, std.testing.allocator); - defer panto.freeResultParts(std.testing.allocator, result); + defer (panto.ResultParts{ .items = result }).deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 3), result.len); try std.testing.expectEqualStrings("see image", result[0].text); try std.testing.expectEqualStrings("image/png", result[1].media.media_type.?); diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig index 73078c0..c15ee63 100644 --- a/src/lua_runtime.zig +++ b/src/lua_runtime.zig @@ -549,7 +549,7 @@ const Slot = struct { /// via the `pcall` wrapping. ok: bool = false, /// Result payload as owned parts. Allocated from `allocator`. - /// Caller frees via `panto.freeResultParts`. + /// Caller frees via `panto.ResultParts.deinit`. value: ?[]panto.ResultPart = null, /// On `ok = false`, an owned copy of the error message. err_msg: ?[]u8 = null, @@ -703,11 +703,11 @@ fn runBatch( continue; } if (slot.ok) { - results[i] = .{ .ok = slot.value orelse try panto.textResult(allocator, "") }; + results[i] = .{ .ok = slot.value orelse (try panto.ResultParts.fromText(allocator, "")).items }; // Free the err_msg if both ended up set somehow. if (slot.err_msg) |m| allocator.free(m); } else { - if (slot.value) |v| panto.freeResultParts(allocator, v); + if (slot.value) |v| (panto.ResultParts{ .items = v }).deinit(allocator); std.log.warn( "panto-lua: tool '{s}' failed: {s}", .{ @@ -740,7 +740,7 @@ fn formatToolError( "panto-lua: tool '{s}' failed: {s}", .{ tool_name, message }, ); - return panto.ownedTextResult(allocator, text); + return (try panto.ResultParts.fromTextOwned(allocator, text)).items; } /// Start one coroutine: create a thread under the runtime's lua_State, @@ -1077,7 +1077,7 @@ fn okText(result: panto.ToolCallResult) []const u8 { /// Test helper: free a results slice (parts on `.ok`). fn freeResults(results: []panto.ToolCallResult) void { for (results) |r| switch (r) { - .ok => |b| panto.freeResultParts(testing.allocator, b), + .ok => |b| (panto.ResultParts{ .items = b }).deinit(testing.allocator), .err => {}, }; } diff --git a/src/main.zig b/src/main.zig index 247817a..995735f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -45,9 +45,9 @@ test { _ = command_compaction; } -const ContentBlockType = panto.provider.ContentBlockType; -const MessageRole = panto.conversation.MessageRole; -const Event = panto.stream.Event; +const ContentBlockType = panto.ContentBlockType; +const MessageRole = panto.MessageRole; +const Event = panto.Event; /// Display state for the pull-stream CLI renderer. Prints streaming deltas /// to stdout; thinking blocks are dimmed with ANSI escape codes, text blocks @@ -307,22 +307,22 @@ pub fn main(init: std.process.Init) !void { // Resolve the optional compaction-model override into a ProviderConfig. // On any resolution failure we log and fall back to no override (the // active chat model is used for compaction). - const compaction_provider_config: ?panto.config.ProviderConfig = blk: { + const compaction_provider_config: ?panto.ProviderConfig = blk: { const ref = app_config.compaction_model_ref orelse break :blk null; break :blk config_file.buildProviderConfig(&app_config, &models.defs, ref) catch |err| { std.log.warn("compaction_model resolution failed ({t}); using active model", .{err}); break :blk null; }; }; - const compaction_cfg: panto.config.CompactionConfig = .{ + const compaction_cfg: panto.CompactionConfig = .{ .keep_verbatim = app_config.compaction_keep_verbatim orelse 20_000, .model = compaction_provider_config, }; // Process-global HTTP client: one connection pool for every provider / // base_url the agent may switch to. Torn down at shutdown. - panto.config.initHttp(alloc, io); - defer panto.config.deinitHttp(); + panto.init(alloc, io); + defer panto.deinit(); const banner_model_initial: []const u8 = switch (provider_config) { inline else => |c| c.model, @@ -333,7 +333,7 @@ pub fn main(init: std.process.Init) !void { // per-cwd `session_dir` (the CLI owns the cwd→dir grouping; the store // is cwd-agnostic). Must outlive the agent, which appends through a // `Session` handle minted/resolved below. - var session_store_impl = try panto.session_manager.FileSystemJSONLStore.init(alloc, io, session_dir, cwd); + var session_store_impl = try panto.FileSystemJSONLStore.init(alloc, io, session_dir, cwd); defer session_store_impl.deinit(); const session_store = session_store_impl.store(); @@ -376,7 +376,7 @@ pub fn main(init: std.process.Init) !void { // built but before the first turn, so in-place registration is visible. // System-prompt seeding/reconciliation below runs *through* the agent // so those entries persist. - const active_config: panto.config.Config = .{ + const active_config: panto.Config = .{ .provider = provider_config, .compaction = compaction_cfg, }; @@ -648,12 +648,12 @@ fn parseAgentFlags(alloc: std.mem.Allocator, args: std.process.Args) !AgentFlags /// most recent. The returned `Session` owns its `info` (freed via the /// agent's `deinit`, which adopts it). fn openSession( - store: panto.session_store.SessionStore, + store: panto.SessionStore, flags: AgentFlags, session_dir: []const u8, stdout: *std.Io.Writer, stdout_file: *std.Io.File.Writer, -) !panto.session_store.Session { +) !panto.Session { switch (flags.resume_kind) { .none => return store.create(), .most_recent => { diff --git a/src/models_toml.zig b/src/models_toml.zig index 790b61f..2e2a920 100644 --- a/src/models_toml.zig +++ b/src/models_toml.zig @@ -49,9 +49,9 @@ const Io = std.Io; const toml = @import("toml"); const panto = @import("panto"); -pub const Pricing = panto.pricing.Pricing; -pub const Registry = panto.pricing.Registry; -pub const ReasoningEffort = panto.config.ReasoningEffort; +pub const Pricing = panto.Pricing; +pub const Registry = panto.PricingRegistry; +pub const ReasoningEffort = panto.ReasoningEffort; /// A resolved model definition: the wire model id plus per-model knobs. /// Strings are owned by the `ModelRegistry`. diff --git a/src/subcommand.zig b/src/subcommand.zig index 3ccd2c6..f0fdfc9 100644 --- a/src/subcommand.zig +++ b/src/subcommand.zig @@ -272,7 +272,7 @@ fn runSessionsSubcommand( const session_dir = try session_paths.sessionDirForCwd(allocator, environ_map, cwd); defer allocator.free(session_dir); - var store_impl = try panto.session_manager.FileSystemJSONLStore.init(allocator, io, session_dir, cwd); + var store_impl = try panto.FileSystemJSONLStore.init(allocator, io, session_dir, cwd); defer store_impl.deinit(); const store = store_impl.store(); diff --git a/src/system_prompt.zig b/src/system_prompt.zig index d8c06bb..500b135 100644 --- a/src/system_prompt.zig +++ b/src/system_prompt.zig @@ -249,7 +249,7 @@ pub fn resolveCompactionLayers( /// seed sequence). Returned slices borrow from `messages`. fn effectiveConfigWindow( arena: Allocator, - messages: []const panto.conversation.Message, + messages: []const panto.Message, ) ![]const []const u8 { // Find the index of the last message carrying a `replace`-mode block. var anchor: ?usize = null; @@ -462,9 +462,9 @@ const TmpLayers = struct { } }; -fn openTmpStore(arena: Allocator, root: []const u8) !panto.session_manager.FileSystemJSONLStore { +fn openTmpStore(arena: Allocator, root: []const u8) !panto.FileSystemJSONLStore { const sessions = try std.fs.path.join(arena, &.{ root, "sessions" }); - return panto.session_manager.FileSystemJSONLStore.init(testing.allocator, testing.io, sessions, "/cwd"); + return panto.FileSystemJSONLStore.init(testing.allocator, testing.io, sessions, "/cwd"); } /// Minimal agent harness for system-prompt tests: a throwaway provider @@ -472,12 +472,12 @@ fn openTmpStore(arena: Allocator, root: []const u8) !panto.session_manager.FileS /// store). Post-R1 the agent owns its own (empty) tool registry, so the /// harness only holds the config to keep the agent's borrowed pointer valid. const SPAgentHarness = struct { - config: panto.config.Config, + config: panto.Config, agent: panto.agent.Agent, fn init( self: *SPAgentHarness, - session: panto.session_store.Session, + session: panto.Session, adopted: ?panto.conversation.Conversation, ) void { self.config = .{ @@ -499,8 +499,8 @@ fn forceFlush(agent: *panto.agent.Agent) !void { var conv = panto.conversation.Conversation.init(testing.allocator); defer conv.deinit(); try conv.addAssistantMessage(&.{}); - const id: panto.session_store.WireIdentity = .{ .api_style = .openai_chat, .base_url = "u", .model = "m" }; - var batch = [_]panto.session_store.PersistentMessage{ + const id: panto.WireIdentity = .{ .api_style = .openai_chat, .base_url = "u", .model = "m" }; + var batch = [_]panto.PersistentMessage{ .{ .message = conv.messages.items[0], .identity = id }, }; try agent.session.append(&batch); @@ -537,7 +537,7 @@ test "seedFresh then no-config-change resume is a no-op" { // The persisted conversation reflects the seeded system prompt. var conv_disk = (try store.load(session_id)).?; - const eff_disk = try panto.conversation.effectiveSystemBlocks(arena, conv_disk.messages.items); + const eff_disk = try panto.effectiveSystemBlocks(arena, conv_disk.messages.items); const seeded_count = eff_disk.items.len; conv_disk.deinit(); try testing.expect(seeded_count > 0); @@ -550,7 +550,7 @@ test "seedFresh then no-config-change resume is a no-op" { h2.init(sess2, conv2); defer h2.deinit(); try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); - const eff_after = try panto.conversation.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); + const eff_after = try panto.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); try testing.expectEqual(seeded_count, eff_after.items.len); } @@ -593,7 +593,7 @@ test "resume after config change appends replace + append sequence" { try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); // The effective prompt now reflects only the new config blocks. - const eff = try panto.conversation.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); + const eff = try panto.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); try testing.expectEqual(@as(usize, 2), eff.items.len); try testing.expectEqualStrings("new seed", eff.items[0]); try testing.expectEqualStrings("new append", eff.items[1]); @@ -601,7 +601,7 @@ test "resume after config change appends replace + append sequence" { // A second no-op resume must not change the effective prompt (anchors // to the new `replace` window, not the stale original seed). try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); - const eff2 = try panto.conversation.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); + const eff2 = try panto.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); try testing.expectEqual(@as(usize, 2), eff2.items.len); } |
