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. --- src/system_prompt.zig | 107 +++++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 50 deletions(-) (limited to 'src/system_prompt.zig') diff --git a/src/system_prompt.zig b/src/system_prompt.zig index 500b135..39c3ea8 100644 --- a/src/system_prompt.zig +++ b/src/system_prompt.zig @@ -137,7 +137,7 @@ pub fn seedFresh( io: Io, environ_map: *const std.process.Environ.Map, base_dir: []const u8, - agent: *panto.agent.Agent, + agent: panto.Agent, ) !void { const user_dir = try userLayerDir(arena, environ_map); const project_dir = try projectLayerDir(arena, io); @@ -151,13 +151,13 @@ pub fn seedFreshLayers( base_dir: ?[]const u8, user_dir: ?[]const u8, project_dir: ?[]const u8, - agent: *panto.agent.Agent, + agent: panto.Agent, ) !void { const resolved = try resolveLayers(arena, io, base_dir, user_dir, project_dir); const blocks = try resolved.blocks(arena); for (blocks) |text| { // The agent adds to its conversation and persists the entry. - try agent.addSystemMessage(text, .append); + try agent.addSystemMessage(text); } } @@ -174,7 +174,7 @@ pub fn reconcileResume( io: Io, environ_map: *const std.process.Environ.Map, base_dir: []const u8, - agent: *panto.agent.Agent, + agent: panto.Agent, ) !void { const user_dir = try userLayerDir(arena, environ_map); const project_dir = try projectLayerDir(arena, io); @@ -188,21 +188,21 @@ pub fn reconcileResumeLayers( base_dir: ?[]const u8, user_dir: ?[]const u8, project_dir: ?[]const u8, - agent: *panto.agent.Agent, + agent: panto.Agent, ) !void { const resolved = try resolveLayers(arena, io, base_dir, user_dir, project_dir); const config_blocks = try resolved.blocks(arena); - const window = try effectiveConfigWindow(arena, agent.conversation.messages.items); + const window = try effectiveConfigWindow(arena, agent.conversation().messages()); if (blocksEqual(config_blocks, window)) return; - // Re-seed: one `replace` for the seed, then one `append` per append. - // The agent adds to its conversation and persists each entry (with the - // correct system mode). - try agent.addSystemMessage(config_blocks[0], .replace); + // Re-seed: one `setSystemPrompt` (replace) for the seed, then one + // `addSystemMessage` (append) per append. The agent adds to its + // conversation and persists each entry (with the correct system mode). + try agent.setSystemPrompt(config_blocks[0]); for (config_blocks[1..]) |text| { - try agent.addSystemMessage(text, .append); + try agent.addSystemMessage(text); } } @@ -339,7 +339,7 @@ test "blocksEqual - length and content" { test "effectiveConfigWindow - no replace anchors to leading system blocks" { const alloc = testing.allocator; - var conv = panto.conversation.Conversation.init(alloc); + var conv = panto.ConversationData.init(alloc); defer conv.deinit(); try conv.addSystemMessage("seed"); @@ -357,7 +357,7 @@ test "effectiveConfigWindow - no replace anchors to leading system blocks" { test "effectiveConfigWindow - anchors to last replace block" { const alloc = testing.allocator; - var conv = panto.conversation.Conversation.init(alloc); + var conv = panto.ConversationData.init(alloc); defer conv.deinit(); try conv.addSystemMessage("old seed"); @@ -473,38 +473,45 @@ fn openTmpStore(arena: Allocator, root: []const u8) !panto.FileSystemJSONLStore /// harness only holds the config to keep the agent's borrowed pointer valid. const SPAgentHarness = struct { config: panto.Config, - agent: panto.agent.Agent, + agent: panto.Agent, + /// A second handle onto the same session (a copyable value proxying to + /// the same store + id) so the test can `forceFlush` without reaching + /// agent internals. `session_id` mirrors the agent's session id. + session: panto.Session, + session_id: []const u8, fn init( self: *SPAgentHarness, session: panto.Session, - adopted: ?panto.conversation.Conversation, - ) void { + adopted: ?panto.ConversationData, + ) !void { self.config = .{ .provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m" } }, }; - self.agent = panto.agent.Agent.init(testing.allocator, testing.io, &self.config, session, adopted); + self.session = session; + self.session_id = session.info.id; + self.agent = try panto.Agent.init(testing.allocator, testing.io, &self.config, session, adopted); } fn deinit(self: *SPAgentHarness) void { self.agent.deinit(); } -}; -/// Force the session to flush to disk by appending an assistant message -/// through the agent's session. (System/user messages buffer in memory -/// until the first assistant message; a realistic resume only sees what a -/// completed turn persisted.) -fn forceFlush(agent: *panto.agent.Agent) !void { - var conv = panto.conversation.Conversation.init(testing.allocator); - defer conv.deinit(); - try conv.addAssistantMessage(&.{}); - 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); -} + /// Force the session to flush to disk by appending an assistant message + /// through the (shared) session handle. (System/user messages buffer in + /// memory until the first assistant message; a realistic resume only + /// sees what a completed turn persisted.) + fn forceFlush(self: *SPAgentHarness) !void { + var conv = panto.ConversationData.init(testing.allocator); + defer conv.deinit(); + try conv.addAssistantMessage(&.{}); + 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 self.session.append(&batch); + } +}; test "seedFresh then no-config-change resume is a no-op" { const alloc = testing.allocator; @@ -526,12 +533,12 @@ test "seedFresh then no-config-change resume is a no-op" { var session_id_len: usize = 0; { var h: SPAgentHarness = undefined; - h.init(store.create(), null); + try h.init(store.create(), null); defer h.deinit(); - try seedFreshLayers(arena, testing.io, null, null, project_dir, &h.agent); - try forceFlush(&h.agent); // persist seed (+ assistant) to disk - @memcpy(session_id_buf[0..h.agent.session.info.id.len], h.agent.session.info.id); - session_id_len = h.agent.session.info.id.len; + try seedFreshLayers(arena, testing.io, null, null, project_dir, h.agent); + try h.forceFlush(); // persist seed (+ assistant) to disk + @memcpy(session_id_buf[0..h.session_id.len], h.session_id); + session_id_len = h.session_id.len; } const session_id = session_id_buf[0..session_id_len]; @@ -547,10 +554,10 @@ test "seedFresh then no-config-change resume is a no-op" { var sess2 = (try store.resolve(session_id)).?; const conv2 = try sess2.load(); var h2: SPAgentHarness = undefined; - h2.init(sess2, conv2); + try h2.init(sess2, conv2); defer h2.deinit(); - try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); - const eff_after = try panto.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); + try reconcileResumeLayers(arena, testing.io, null, null, project_dir, h2.agent); + const eff_after = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages()); try testing.expectEqual(seeded_count, eff_after.items.len); } @@ -572,12 +579,12 @@ test "resume after config change appends replace + append sequence" { var session_id_len: usize = 0; { var h: SPAgentHarness = undefined; - h.init(store.create(), null); + try h.init(store.create(), null); defer h.deinit(); - try seedFreshLayers(arena, testing.io, null, null, project_dir, &h.agent); - try forceFlush(&h.agent); // persist old seed (+ assistant) to disk - @memcpy(session_id_buf[0..h.agent.session.info.id.len], h.agent.session.info.id); - session_id_len = h.agent.session.info.id.len; + try seedFreshLayers(arena, testing.io, null, null, project_dir, h.agent); + try h.forceFlush(); // persist old seed (+ assistant) to disk + @memcpy(session_id_buf[0..h.session_id.len], h.session_id); + session_id_len = h.session_id.len; } const session_id = session_id_buf[0..session_id_len]; @@ -588,20 +595,20 @@ test "resume after config change appends replace + append sequence" { var sess2 = (try store.resolve(session_id)).?; const conv2 = try sess2.load(); var h2: SPAgentHarness = undefined; - h2.init(sess2, conv2); + try h2.init(sess2, conv2); defer h2.deinit(); - try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); + 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.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); + const eff = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages()); 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]); // 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.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items); + try reconcileResumeLayers(arena, testing.io, null, null, project_dir, h2.agent); + const eff2 = try panto.effectiveSystemBlocks(arena, h2.agent.conversation().messages()); try testing.expectEqual(@as(usize, 2), eff2.items.len); } -- cgit v1.3