From 1eddee33902757f7e06993825a3ad1011e7ec346 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 7 Jun 2026 14:49:20 -0600 Subject: Plug the deep-embedder holes; remove public.zig escape hatch Close the gap that forced the CLI to import internal libpanto namespaces, then delete the transitional re-exports. The CLI now uses only the curated public surface. Additions to the public Agent facade: - init/deinit (heap-pin the inner; the handle is a copyable value). - addSystemMessage (.append) and setSystemPrompt (.replace). - compact(override_system_prompt, extra) falling back to the config prompt. - sessionId() accessor. CompactionConfig gains compaction_prompt, which owns the compaction system prompt for both auto-compaction and the explicit compact() default; the Agent.compaction_system_prompt field is deleted. ConversationData is the public name for the owned conversation value type (Session.load returns it, Agent.init adopts it), distinct from the borrowed Conversation handle. Agent.addSystemMessage/setSystemPrompt are kept on Agent (they persist with the SystemMode) rather than dropped as the plan first proposed. --- libpanto/src/agent.zig | 23 ++++++------ libpanto/src/config.zig | 8 +++++ libpanto/src/public.zig | 93 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 84 insertions(+), 40 deletions(-) (limited to 'libpanto') diff --git a/libpanto/src/agent.zig b/libpanto/src/agent.zig index 5f9bee5..6d62b8a 100644 --- a/libpanto/src/agent.zig +++ b/libpanto/src/agent.zig @@ -206,11 +206,6 @@ pub const Agent = struct { /// Injectable streaming seam. Defaults to the real provider dispatch /// (`provider_mod.openStream`); tests override it with a stub. open_stream_fn: provider_mod.OpenStreamFn = provider_mod.openStream, - /// Compaction system prompt used for automatic compaction on context - /// overflow. Borrowed; set by the embedder (resolved from its - /// `COMPACTION.md` layers). When null, auto-compaction is disabled and - /// a context-overflow error propagates unchanged. - compaction_system_prompt: ?[]const u8 = null, /// Set by the embedder after `runStep` returns to learn whether an /// automatic compaction occurred this turn (so it can persist the /// rewritten conversation). Reset at the top of each `runStep`. @@ -454,7 +449,7 @@ pub const Agent = struct { err: anyerror, ) !provider_mod.ProviderStream { if (self.auto_compacted) return err; // already retried once this turn - const sys = self.compaction_system_prompt orelse return err; + const sys = self.config.compaction.compaction_prompt orelse return err; const res = try self.compact(sys, null); if (!res.compacted) return err; // nothing to shed; give up self.auto_compacted = true; @@ -595,11 +590,19 @@ pub const Agent = struct { /// if anything was compacted, appends the new compaction window /// (summary + restated kept suffix) to the store. Returns the same /// `CompactionResult` for the embedder to report. + /// + /// `override_system_prompt`, when non-null, is the compaction system + /// prompt for this run; when null it falls back to + /// `config.compaction.compaction_prompt`. With neither set, this errors + /// (`error.NoCompactionPrompt`). pub fn compactAndPersist( self: *Agent, - system_prompt: []const u8, + override_system_prompt: ?[]const u8, extra_instructions: ?[]const u8, ) !CompactionResult { + const system_prompt = override_system_prompt orelse + self.config.compaction.compaction_prompt orelse + return error.NoCompactionPrompt; const res = try self.compact(system_prompt, extra_instructions); if (res.compacted) { try turn_persist.persistCompaction( @@ -2852,13 +2855,12 @@ test "runStep: auto-compacts on context overflow and retries once" { var h = TestHarness.init(allocator); defer h.deinit(); h.activate(); - h.config.compaction = .{ .keep_verbatim = 10 }; + h.config.compaction = .{ .keep_verbatim = 10, .compaction_prompt = "Summarize the conversation." }; var ns = null_store_mod.NullStore.init(allocator); var agent = Agent.init(allocator, io, &h.config, ns.store().create(), null); defer agent.deinit(); h.seedInto(&agent); agent.open_stream_fn = stub.install(); - agent.compaction_system_prompt = "Summarize the conversation."; const conv = &agent.conversation; try conv.addSystemMessage("you are helpful"); @@ -3233,13 +3235,12 @@ test "runStep: context-overflow compaction fires a compaction retry notification var h = TestHarness.init(allocator); defer h.deinit(); h.activate(); - h.config.compaction = .{ .keep_verbatim = 10 }; + h.config.compaction = .{ .keep_verbatim = 10, .compaction_prompt = "Summarize the conversation." }; var ns = null_store_mod.NullStore.init(allocator); var agent = Agent.init(allocator, io, &h.config, ns.store().create(), null); defer agent.deinit(); h.seedInto(&agent); agent.open_stream_fn = stub.install(); - agent.compaction_system_prompt = "Summarize the conversation."; const conv = &agent.conversation; try conv.addSystemMessage("you are helpful"); diff --git a/libpanto/src/config.zig b/libpanto/src/config.zig index 203a091..5e01bb1 100644 --- a/libpanto/src/config.zig +++ b/libpanto/src/config.zig @@ -116,6 +116,14 @@ pub const WireIdentity = struct { pub const CompactionConfig = struct { keep_verbatim: u32 = 20_000, model: ?ProviderConfig = null, + /// The compaction system prompt. Used both for automatic compaction on + /// context overflow and as the default for an explicit `Agent.compact` + /// call (which may override it per-call). Borrowed; set by the embedder + /// (e.g. resolved from its `COMPACTION.md` layers, or a built-in + /// default). When null, auto-compaction is disabled and a + /// context-overflow error propagates unchanged, and an explicit + /// `compact` with no override is a no-op error. + compaction_prompt: ?[]const u8 = null, }; /// Policy for retrying transient provider/API failures. Conservative diff --git a/libpanto/src/public.zig b/libpanto/src/public.zig index 951bd3a..56a0676 100644 --- a/libpanto/src/public.zig +++ b/libpanto/src/public.zig @@ -64,7 +64,7 @@ pub const RetryConfig = config_mod.RetryConfig; pub const WireIdentity = config_mod.WireIdentity; // =========================================================================== -// Conversation construction (data, aliased — Job 2) +// Conversation construction (data, aliased) // =========================================================================== pub const ContentBlock = conversation_mod.ContentBlock; @@ -82,9 +82,17 @@ 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. +/// The owned conversation value type. This is what `Session.load` returns +/// and what `Agent.init` adopts. Build one standalone with `init` for +/// by-hand construction; once handed to an `Agent` it is owned by the agent. +/// For *operating on* an agent's live conversation, use the `Conversation` +/// handle (below) via `Agent.conversation()`. +pub const ConversationData = conversation_mod.Conversation; + +/// A borrowed handle onto a conversation owned by an `Agent`. Pointer- +/// wrapping makes the "don't move the conversation" invariant structural: +/// the handle is a cheap copyable view, minted per-call by +/// `Agent.conversation()`, borrowing state owned by its `Agent`. pub const Conversation = struct { inner: *conversation_mod.Conversation, @@ -113,7 +121,7 @@ pub const Conversation = struct { }; // =========================================================================== -// Tools (data + small façade — Job 1) +// Tools (data + small façade) // =========================================================================== pub const Tool = tool_mod.Tool; @@ -179,6 +187,31 @@ pub const CompactionResult = agent_mod.Agent.CompactionResult; pub const Agent = struct { inner: *agent_mod.Agent, + /// Construct an agent. The inner is heap-pinned at `init`, so the + /// returned `Agent` is a cheap copyable handle ("don't move the Agent" + /// stops being a rule anyone can violate). `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`. + pub fn init( + allocator: std.mem.Allocator, + io: std.Io, + config: *const Config, + session: Session, + maybe_conversation: ?ConversationData, + ) !Agent { + const inner = try allocator.create(agent_mod.Agent); + inner.* = agent_mod.Agent.init(allocator, io, config, session, maybe_conversation); + return .{ .inner = inner }; + } + + pub fn deinit(self: Agent) void { + const allocator = self.inner.allocator; + self.inner.deinit(); + allocator.destroy(self.inner); + } + pub fn registerTool(self: Agent, t: Tool) !void { return self.inner.registerTool(t); } @@ -191,18 +224,41 @@ pub const Agent = struct { pub fn run(self: Agent, message: UserMessage) !Stream { return .{ .inner = try self.inner.run(.{ .text = message.text }) }; } + + /// Append a system message to the conversation and persist it (the + /// `.append` `SystemMode`: it adds to the effective prompt). + pub fn addSystemMessage(self: Agent, text: []const u8) !void { + return self.inner.addSystemMessage(text, .append); + } + /// Replace the effective system prompt and persist it (the `.replace` + /// `SystemMode`: it discards all prior system text). On replay the log + /// reconstructs the same effective prompt. + pub fn setSystemPrompt(self: Agent, text: []const u8) !void { + return self.inner.addSystemMessage(text, .replace); + } + /// 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); + /// `override_system_prompt` falls back to + /// `config.compaction.compaction_prompt` when null. `extra` is appended + /// to the compaction prompt for this run (the `/compact $ARGUMENTS` + /// path). + pub fn compact(self: Agent, override_system_prompt: ?[]const u8, extra: ?[]const u8) !CompactionResult { + return self.inner.compactAndPersist(override_system_prompt, extra); } + /// A borrowed handle onto the agent's live conversation. pub fn conversation(self: Agent) Conversation { return .{ .inner = &self.inner.conversation }; } + + /// The id of the session this agent appends to. + pub fn sessionId(self: Agent) []const u8 { + return self.inner.session.info.id; + } }; // =========================================================================== -// Pricing (data, aliased — Job 1) +// Pricing (data, aliased) // =========================================================================== pub const Pricing = pricing_mod.Pricing; @@ -219,27 +275,6 @@ pub const PersistentMessage = session_store_mod.PersistentMessage; pub const NullStore = null_store_mod.NullStore; pub const FileSystemJSONLStore = fs_store_mod.FileSystemJSONLStore; -// =========================================================================== -// Deep-embedder escape hatches. -// -// 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 conversation = conversation_mod; - // =========================================================================== // Tests // =========================================================================== -- cgit v1.3