summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/libpanto-cleanup.md23
-rw-r--r--libpanto/src/public.zig39
-rw-r--r--src/config_file.zig12
-rw-r--r--src/extension_loader.zig2
-rw-r--r--src/lua_bridge.zig10
-rw-r--r--src/lua_runtime.zig10
-rw-r--r--src/main.zig22
-rw-r--r--src/models_toml.zig6
-rw-r--r--src/subcommand.zig2
-rw-r--r--src/system_prompt.zig22
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);
}