diff options
| author | t <t@tjp.lol> | 2026-06-05 11:43:29 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-05 11:43:43 -0600 |
| commit | ccdaef8e682960ecadf73d3b2df70559a0baaf56 (patch) | |
| tree | 2a6e455ca3359b68bf5cb5b83015054bd6e2eb6f /src/system_prompt.zig | |
| parent | 03ac69658517a0054d2a573d1e1b60c1bfaaf977 (diff) | |
Move session persistence into the Agent; collapse CLI plumbing
The Agent now owns its Conversation and a SessionStore, and persists
everything it generates as turns progress. Embedders get persistence for
free; the CLI no longer drives the session log.
libpanto:
- Agent.init takes a SessionStore + optional Conversation to adopt (resume
path). Agent owns/tears down the conversation; turn-driving methods drop
the *Conversation param and operate on self.conversation.
- New Agent methods: submitUserMessage (adds+persists the user prompt
immediately), addSystemMessage(text, mode), runStep persists the turn on
every exit path (incl. partial turns on error), compactAndPersist for the
explicit /compact path. Auto-compaction persists its window via runStep's
tail.
- turn_persist.zig: DiskMessage mapping moved out of the CLI; reads usage
off Message.usage (no per-message-usage list). System mode rides on
DiskMessage.mode, derived from the System block.
- NullStore is now an allocator-bearing struct (frees consumed messages per
the store-consumes-messages contract).
- Tests: in-memory CapturingStore round-trip + NullStore turn test.
panto CLI:
- Delete src/session_persist.zig. REPL is submitUserMessage + runStep.
- Reorder construction: store -> registry -> agent -> bootstrap -> seed/
reconcile system prompt through the agent -> load extensions.
- command.Context drops conv/session_mgr; commands reach ctx.agent.
- system_prompt seed/reconcile call agent.addSystemMessage.
- CLIReceiver is display-only (per_message_usage removed).
Phases 3-5 of docs/pluggable-session-store.md. Behavior preserved; full
build + test suite green.
Diffstat (limited to 'src/system_prompt.zig')
| -rw-r--r-- | src/system_prompt.zig | 114 |
1 files changed, 64 insertions, 50 deletions
diff --git a/src/system_prompt.zig b/src/system_prompt.zig index 0b449dc..b5423f8 100644 --- a/src/system_prompt.zig +++ b/src/system_prompt.zig @@ -137,12 +137,11 @@ pub fn seedFresh( io: Io, environ_map: *const std.process.Environ.Map, base_dir: []const u8, - conv: *panto.conversation.Conversation, - mgr: *panto.session_manager.SessionManager, + agent: *panto.agent.Agent, ) !void { const user_dir = try userLayerDir(arena, environ_map); const project_dir = try projectLayerDir(arena, io); - return seedFreshLayers(arena, io, base_dir, user_dir, project_dir, conv, mgr); + return seedFreshLayers(arena, io, base_dir, user_dir, project_dir, agent); } /// Layer-explicit variant of `seedFresh` (see `resolveLayers`). @@ -152,14 +151,13 @@ pub fn seedFreshLayers( base_dir: ?[]const u8, user_dir: ?[]const u8, project_dir: ?[]const u8, - conv: *panto.conversation.Conversation, - mgr: *panto.session_manager.SessionManager, + agent: *panto.agent.Agent, ) !void { const resolved = try resolveLayers(arena, io, base_dir, user_dir, project_dir); const blocks = try resolved.blocks(arena); for (blocks) |text| { - try conv.addSystemMessage(text); - try appendSystemEntry(mgr.allocator, mgr, text, .append); + // The agent adds to its conversation and persists the entry. + try agent.addSystemMessage(text, .append); } } @@ -176,12 +174,11 @@ pub fn reconcileResume( io: Io, environ_map: *const std.process.Environ.Map, base_dir: []const u8, - conv: *panto.conversation.Conversation, - mgr: *panto.session_manager.SessionManager, + agent: *panto.agent.Agent, ) !void { const user_dir = try userLayerDir(arena, environ_map); const project_dir = try projectLayerDir(arena, io); - return reconcileResumeLayers(arena, io, base_dir, user_dir, project_dir, conv, mgr); + return reconcileResumeLayers(arena, io, base_dir, user_dir, project_dir, agent); } /// Layer-explicit variant of `reconcileResume` (see `resolveLayers`). @@ -191,22 +188,21 @@ pub fn reconcileResumeLayers( base_dir: ?[]const u8, user_dir: ?[]const u8, project_dir: ?[]const u8, - conv: *panto.conversation.Conversation, - mgr: *panto.session_manager.SessionManager, + agent: *panto.agent.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, conv.messages.items); + const window = try effectiveConfigWindow(arena, agent.conversation.messages.items); if (blocksEqual(config_blocks, window)) return; // Re-seed: one `replace` for the seed, then one `append` per append. - try conv.replaceSystemMessage(config_blocks[0]); - try appendSystemEntry(mgr.allocator, mgr, config_blocks[0], .replace); + // The agent adds to its conversation and persists each entry (with the + // correct system mode). + try agent.addSystemMessage(config_blocks[0], .replace); for (config_blocks[1..]) |text| { - try conv.addSystemMessage(text); - try appendSystemEntry(mgr.allocator, mgr, text, .append); + try agent.addSystemMessage(text, .append); } } @@ -292,22 +288,6 @@ fn blocksEqual(a: []const []const u8, b: []const []const u8) bool { return true; } -/// Append a system-role entry to the session log carrying `mode`. -fn appendSystemEntry( - alloc: Allocator, - mgr: *panto.session_manager.SessionManager, - text: []const u8, - mode: panto.session_manager.DiskSystemMode, -) !void { - const blocks = try alloc.alloc(panto.session_manager.DiskContentBlock, 1); - blocks[0] = .{ .text = .{ .text = try alloc.dupe(u8, text) } }; - _ = try mgr.appendMessage( - .{ .role = .system, .mode = mode, .content = blocks }, - null, - null, - ); -} - /// Read `dir/name`, returning its contents (owned by `arena`) or null if the /// file does not exist. Other I/O errors propagate. fn readLayerFile( @@ -487,6 +467,34 @@ fn openTmpSession(arena: Allocator, root: []const u8) !panto.session_manager.Ses return panto.session_manager.SessionManager.init(testing.allocator, testing.io, sessions, "/cwd"); } +/// Minimal agent harness for system-prompt tests: an empty registry and a +/// throwaway provider config, wrapping a session store and (optionally) an +/// adopted conversation. Holds the registry/config so the agent's borrowed +/// pointers stay valid for the agent's lifetime. +const SPAgentHarness = struct { + registry: panto.ToolRegistry, + config: panto.config.Config, + agent: panto.agent.Agent, + + fn init( + self: *SPAgentHarness, + store: panto.session_store.SessionStore, + adopted: ?panto.conversation.Conversation, + ) void { + self.registry = panto.ToolRegistry.init(testing.allocator); + self.config = .{ + .provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m" } }, + .registry = &self.registry, + }; + self.agent = panto.agent.Agent.init(testing.allocator, testing.io, &self.config, store, adopted); + } + + fn deinit(self: *SPAgentHarness) void { + self.agent.deinit(); + self.registry.deinit(); + } +}; + /// Force the session to flush to disk by appending an assistant entry. /// (System/user entries buffer in memory until the first assistant entry; /// a realistic resume only sees what a completed turn persisted.) @@ -512,12 +520,13 @@ test "seedFresh then no-config-change resume is a no-op" { try layers.writeProject("APPEND_SYSTEM.md", "project append"); const project_dir = try layers.projectDir(arena); - // Fresh session: seed + persist. + // Fresh session: seed + persist (through the agent). var mgr = try openTmpSession(arena, layers.root); { - var conv = panto.conversation.Conversation.init(alloc); - defer conv.deinit(); - try seedFreshLayers(arena, testing.io, null, null, project_dir, &conv, &mgr); + var h: SPAgentHarness = undefined; + h.init(mgr.store(), null); + defer h.deinit(); + try seedFreshLayers(arena, testing.io, null, null, project_dir, &h.agent); } try forceFlush(&mgr); // persist seed + append (+ assistant) to disk const entries_on_disk = mgr.getEntries().len; @@ -525,13 +534,15 @@ test "seedFresh then no-config-change resume is a no-op" { const session_file = try arena.dupe(u8, mgr.getSessionFile()); mgr.deinit(); - // Resume: reopen the file, rebuild + reconcile against unchanged config - // → no new entries. + // Resume: reopen the file, adopt the rebuilt conversation + reconcile + // against unchanged config → no new entries. var mgr2 = try panto.session_manager.SessionManager.open(alloc, testing.io, session_file); defer mgr2.deinit(); - var conv2 = try mgr2.rebuildConversation(); - defer conv2.deinit(); - try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &conv2, &mgr2); + const conv2 = try mgr2.rebuildConversation(); + var h2: SPAgentHarness = undefined; + h2.init(mgr2.store(), conv2); + defer h2.deinit(); + try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); try testing.expectEqual(entries_on_disk, mgr2.getEntries().len); } @@ -548,9 +559,10 @@ test "resume after config change appends replace + append sequence" { var mgr = try openTmpSession(arena, layers.root); { - var conv = panto.conversation.Conversation.init(alloc); - defer conv.deinit(); - try seedFreshLayers(arena, testing.io, null, null, project_dir, &conv, &mgr); + var h: SPAgentHarness = undefined; + h.init(mgr.store(), null); + defer h.deinit(); + try seedFreshLayers(arena, testing.io, null, null, project_dir, &h.agent); } try forceFlush(&mgr); // persist old seed (+ assistant) to disk try testing.expectEqual(@as(usize, 2), mgr.getEntries().len); // seed + assistant @@ -563,15 +575,17 @@ test "resume after config change appends replace + append sequence" { var mgr2 = try panto.session_manager.SessionManager.open(alloc, testing.io, session_file); defer mgr2.deinit(); - var conv2 = try mgr2.rebuildConversation(); - defer conv2.deinit(); - try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &conv2, &mgr2); + const conv2 = try mgr2.rebuildConversation(); + var h2: SPAgentHarness = undefined; + h2.init(mgr2.store(), conv2); + defer h2.deinit(); + try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); // old seed + assistant + replace(new seed) + append(new append) = 4. try testing.expectEqual(@as(usize, 4), mgr2.getEntries().len); // The effective prompt now reflects only the new config blocks. - const eff = try panto.conversation.effectiveSystemBlocks(arena, conv2.messages.items); + const eff = try panto.conversation.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]); @@ -579,7 +593,7 @@ test "resume after config change appends replace + append sequence" { // A second no-op resume must not append again (anchors to the new // `replace` window, not the stale original seed). const after_first = mgr2.getEntries().len; - try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &conv2, &mgr2); + try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent); try testing.expectEqual(after_first, mgr2.getEntries().len); } |
