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 | |
| 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')
| -rw-r--r-- | src/command.zig | 8 | ||||
| -rw-r--r-- | src/compaction.zig | 10 | ||||
| -rw-r--r-- | src/main.zig | 147 | ||||
| -rw-r--r-- | src/session_persist.zig | 153 | ||||
| -rw-r--r-- | src/system_prompt.zig | 114 |
5 files changed, 119 insertions, 313 deletions
diff --git a/src/command.zig b/src/command.zig index 26acc7a..49ad5b8 100644 --- a/src/command.zig +++ b/src/command.zig @@ -32,12 +32,10 @@ const panto = @import("panto"); pub const Context = struct { allocator: std.mem.Allocator, - /// In-memory conversation the agent is driving. - conv: *panto.conversation.Conversation, - /// The active agent (model/provider/tooling snapshot). + /// The active agent. Owns the conversation (`agent.conversation`) and + /// the session store (`agent.session_store`); commands reach both + /// through it rather than holding separate pointers. agent: *panto.agent.Agent, - /// Session log to persist any changes a command makes. - session_mgr: *panto.session_manager.SessionManager, /// REPL output writer and its backing file writer (for flushing). stdout: *std.Io.Writer, diff --git a/src/compaction.zig b/src/compaction.zig index a1f6e89..4ec5732 100644 --- a/src/compaction.zig +++ b/src/compaction.zig @@ -8,7 +8,6 @@ const std = @import("std"); const panto = @import("panto"); const command = @import("command.zig"); -const session_persist = @import("session_persist.zig"); /// Install the `/compact` command into `registry`. pub fn register(registry: *command.Registry) !void { @@ -23,20 +22,13 @@ pub fn register(registry: *command.Registry) !void { fn run(args: []const u8, ctx: *command.Context) anyerror!void { const extra: ?[]const u8 = if (args.len > 0) args else null; - const res = ctx.agent.compact(ctx.conv, ctx.compaction_prompt, extra) catch |err| { + const res = ctx.agent.compactAndPersist(ctx.compaction_prompt, extra) catch |err| { try ctx.stdout.print("\n[compaction failed: {s}]\n> ", .{@errorName(err)}); try ctx.stdout_file.flush(); return; }; if (res.compacted) { - try session_persist.persistCompaction( - ctx.allocator, - ctx.session_mgr, - ctx.conv, - ctx.provider_name, - ctx.model_name, - ); try ctx.stdout.print( "\n[compacted: summarized {d} message(s), kept {d} recent turn(s)]\n> ", .{ res.summarized_messages, res.kept_turns }, diff --git a/src/main.zig b/src/main.zig index 61e78d5..9059a6f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -14,7 +14,6 @@ const glob = @import("glob.zig"); const system_prompt = @import("system_prompt.zig"); const command = @import("command.zig"); const command_compaction = @import("compaction.zig"); -const session_persist = @import("session_persist.zig"); // Shorthand alias for the Lua C API. The bridge module owns the actual // `@cImport`; we re-use it here so the smoke check uses identical types. @@ -44,7 +43,6 @@ test { _ = system_prompt; _ = command; _ = command_compaction; - _ = session_persist; } const Receiver = panto.provider.Receiver; @@ -55,34 +53,24 @@ const MessageRole = panto.conversation.MessageRole; /// Receiver that prints streaming deltas to stdout. Thinking blocks are /// dimmed with ANSI escape codes; text blocks render plain. /// -/// Also captures one `?Usage` per assistant response during a turn. In -/// a tool-using turn the agent loop drives multiple streamStep calls; -/// each one ends with `onMessageComplete(msg, usage)`. We append the -/// usage — including `null` when the wire didn't deliver any — to -/// `per_message_usage` in order so `persistTurn` can pair each captured -/// usage with its corresponding assistant message. +/// Persistence is owned by the agent now; the receiver is display-only. const CLIReceiver = struct { stdout: *std.Io.Writer, file: *std.Io.File.Writer, allocator: std.mem.Allocator, - /// One slot per assistant message completed during the current - /// turn, in completion order. `null` means the provider did not - /// report usage on the wire for that message (typical of - /// OpenAI-compatible proxies that ignore `stream_options.include_usage`). - per_message_usage: std.ArrayList(?panto.session_manager.Usage) = .empty, - pub fn receiver(self: *CLIReceiver) Receiver { return .{ .ptr = self, .vtable = &vtable }; } - /// Reset usage state at the start of each turn. + /// Per-turn reset hook. Currently a no-op (no per-turn receiver state), + /// retained as the REPL's turn-boundary signal. pub fn beginTurn(self: *CLIReceiver) void { - self.per_message_usage.clearRetainingCapacity(); + _ = self; } pub fn deinit(self: *CLIReceiver) void { - self.per_message_usage.deinit(self.allocator); + _ = self; } const vtable: ReceiverVTable = .{ @@ -184,13 +172,8 @@ const CLIReceiver = struct { usage: ?panto.session_manager.Usage, ) anyerror!void { const self: *CLIReceiver = @ptrCast(@alignCast(ptr)); - // Only assistant messages come through streaming. The receiver - // contract says onMessageComplete fires exactly once per - // streamStep, with role=.assistant; record the usage slot in - // turn order regardless of whether the wire actually had usage. - if (message.role == .assistant) { - try self.per_message_usage.append(self.allocator, usage); - } + _ = message; + _ = usage; try self.stdout.writeAll("\n"); try self.file.flush(); } @@ -403,17 +386,19 @@ pub fn main(init: std.process.Init) !void { }; defer session_mgr.deinit(); - var conv = panto.conversation.Conversation.init(alloc); - defer conv.deinit(); - const is_resume = session_mgr.getEntries().len > 0; + + // On resume, reconstruct the conversation from the store. The dangling + // user prompt (a trailing user entry with no assistant reply) is split + // out and currently ignored (a future TUI will prefill it for editing). + // On a fresh session, the agent starts with an empty conversation. + var adopted_conversation: ?panto.conversation.Conversation = null; if (is_resume) { - // Resumed an existing session — rebuild the conversation from the - // log. The system prompt is part of the log. (Reconciliation - // against the on-disk SYSTEM.md happens after the agent tree is - // staged, below.) - conv.deinit(); - conv = try session_mgr.rebuildConversation(); + var loaded = try session_mgr.loadConversation(alloc); + // We don't use the dangling prompt yet; free it. + if (loaded.dangling_user) |d| alloc.free(d); + loaded.dangling_user = null; + adopted_conversation = loaded.conversation; try stdout.print( "resumed session {s} ({d} entries)\n", .{ session_mgr.getSessionId()[0..@min(8, session_mgr.getSessionId().len)], session_mgr.getEntries().len }, @@ -421,11 +406,33 @@ pub fn main(init: std.process.Init) !void { } // The tool registry is owned here and referenced by the active config - // snapshot. Extensions register their tool source into it *before* the - // snapshot is built, so the agent's first turn sees them. + // snapshot. Extensions register their tool source into it *after* the + // agent is built; the agent holds a `*const Config` pointing at this + // registry, so in-place registration before the first turn is visible. var registry = panto.ToolRegistry.init(alloc); defer registry.deinit(); + // Assemble the active configuration snapshot and build the agent now, + // before luarocks bootstrap and extension loading. The agent adopts the + // store (for free persistence) and the resumed conversation (or starts + // fresh). System-prompt seeding/reconciliation below runs *through* the + // agent so those entries persist. + const active_config: panto.config.Config = .{ + .provider = provider_config, + .registry = ®istry, + .compaction = compaction_cfg, + }; + var agent = panto.agent.Agent.init( + alloc, + io, + &active_config, + session_mgr.store(), + adopted_conversation, + ); + defer agent.deinit(); + agent.persist_provider = banner_provider_initial; + agent.persist_model = banner_model_initial; + // Spin up the long-lived Lua runtime. All Lua extensions load into // one `lua_State`; module-global state survives across calls. The // runtime registers with the agent as a single `ToolSource` named @@ -461,8 +468,7 @@ pub fn main(init: std.process.Init) !void { io, init.environ_map, luarocks_rt.layout.agent_dir, - &conv, - &session_mgr, + &agent, ); } else { // Fresh session — source and install the system prompt. @@ -471,8 +477,7 @@ pub fn main(init: std.process.Init) !void { io, init.environ_map, luarocks_rt.layout.agent_dir, - &conv, - &session_mgr, + &agent, ); } @@ -506,17 +511,6 @@ pub fn main(init: std.process.Init) !void { try registry.registerSource(rt.toolSource()); } - // Assemble the active configuration snapshot (provider + tool registry) - // and create the agent holding a pointer to it. Swapping this pointer - // later (model/provider/tool changes) takes effect at the next turn. - const active_config: panto.config.Config = .{ - .provider = provider_config, - .registry = ®istry, - .compaction = compaction_cfg, - }; - var agent = panto.agent.Agent.init(alloc, io, &active_config); - defer agent.deinit(); - // Resolve the compaction system prompt (COMPACTION.md across layers, // last wins; built-in default otherwise) and arm automatic compaction. // Owned by `sp_arena`, which lives for the whole REPL. @@ -572,9 +566,7 @@ pub fn main(init: std.process.Init) !void { var cmd_ctx: command.Context = .{ .allocator = alloc, - .conv = &conv, .agent = &agent, - .session_mgr = &session_mgr, .stdout = stdout, .stdout_file = &stdout_file, .compaction_prompt = compaction_prompt, @@ -617,54 +609,18 @@ pub fn main(init: std.process.Init) !void { continue; } - try conv.addUserMessage(line); - try session_persist.appendUserPromptToSession( - alloc, - &session_mgr, - line, - banner_provider_initial, - banner_model_initial, - ); - - const entries_before_step = conv.messages.items.len; + // Submit + persist the user prompt, then drive the turn. The agent + // owns the conversation and persists everything it generates + // (assistant/tool-result messages, and the compaction window on + // auto-compaction) through its session store — the CLI no longer + // touches persistence. + try agent.submitUserMessage(line); cli_recv.beginTurn(); - agent.runStep(&conv, &recv) catch |err| { + agent.runStep(&recv) catch |err| { try stdout.print("\n[error: {s}]\n", .{@errorName(err)}); }; - if (agent.auto_compacted) { - // Automatic compaction rewrote the in-memory conversation, so - // message indices no longer align with `entries_before_step`. - // Persist the whole post-compaction window (summary + duplicated - // kept suffix + the retried turn) as fresh entries; the - // superseded prefix stays on disk and is ignored on replay. - try session_persist.persistCompaction( - alloc, - &session_mgr, - &conv, - banner_provider_initial, - banner_model_initial, - ); - } else { - // Persist whatever new entries the agent produced this turn. The - // agent loop may have appended: - // - assistant message(s) (one per provider response) - // - user messages containing ToolResult blocks (one per tool round) - // Each assistant message gets paired with the Usage that the - // receiver captured at its onMessageComplete time (or null if - // the provider didn't emit usage that round). - try session_persist.persistTurn( - alloc, - &session_mgr, - &conv, - entries_before_step, - banner_provider_initial, - banner_model_initial, - cli_recv.per_message_usage.items, - ); - } - try stdout.writeAll("\n> "); try stdout_file.flush(); } @@ -784,4 +740,3 @@ fn openSession( }, } } - diff --git a/src/session_persist.zig b/src/session_persist.zig deleted file mode 100644 index d5349bc..0000000 --- a/src/session_persist.zig +++ /dev/null @@ -1,153 +0,0 @@ -//! Session-log persistence helpers shared between the REPL loop -//! (`main.zig`) and slash-command handlers (e.g. `compaction.zig`). -//! -//! These were previously private to `main.zig`. They were lifted into -//! their own module so that command handlers living outside the root -//! source file can reuse them without depending on `main.zig` (which, -//! being the executable root, can't be imported as a normal module). - -const std = @import("std"); -const panto = @import("panto"); - -/// Append a single user-prompt message to the session log. -pub fn appendUserPromptToSession( - alloc: std.mem.Allocator, - mgr: *panto.session_manager.SessionManager, - text: []const u8, - provider: []const u8, - model: []const u8, -) !void { - const blocks = try alloc.alloc(panto.session_manager.DiskContentBlock, 1); - blocks[0] = .{ .text = .{ .text = try alloc.dupe(u8, text) } }; - _ = try mgr.appendMessage( - .{ .role = .user, .content = blocks }, - provider, - model, - ); -} - -/// After the agent loop has driven a turn to completion, persist every -/// new in-memory message at index `>= start_index` to the session log. -/// -/// Each in-memory message is mapped to disk: -/// - assistant → assistant entry with provider/model/stop_reason metadata -/// and Usage (from `per_message_usage`, one slot per assistant message in -/// completion order). -/// - user (with ToolResult blocks) → user entry stamped with provider/model. -/// -/// `stop_reason` is recorded as `"stop"` for now; surfacing the real wire -/// value requires plumbing it through the Receiver vtable (future work, -/// separate from token plumbing). -pub fn persistTurn( - alloc: std.mem.Allocator, - mgr: *panto.session_manager.SessionManager, - conv: *const panto.conversation.Conversation, - start_index: usize, - provider: []const u8, - model: []const u8, - per_message_usage: []const ?panto.session_manager.Usage, -) !void { - var batch_messages: std.ArrayList(panto.session_manager.DiskMessage) = .empty; - defer batch_messages.deinit(alloc); - var batch_providers: std.ArrayList(?[]const u8) = .empty; - defer batch_providers.deinit(alloc); - var batch_models: std.ArrayList(?[]const u8) = .empty; - defer batch_models.deinit(alloc); - - var i = start_index; - var assistant_seen: usize = 0; - while (i < conv.messages.items.len) : (i += 1) { - const msg = conv.messages.items[i]; - const blocks = try alloc.alloc(panto.session_manager.DiskContentBlock, msg.content.items.len); - var allocated: usize = 0; - errdefer { - for (blocks[0..allocated]) |b| b.deinit(alloc); - alloc.free(blocks); - } - for (msg.content.items) |block| { - blocks[allocated] = try panto.session.contentBlockToDisk(alloc, block); - allocated += 1; - } - if (msg.role == .assistant and hasToolUseWithoutFollowingResults(conv, i)) { - for (blocks[0..allocated]) |b| b.deinit(alloc); - alloc.free(blocks); - continue; - } - switch (msg.role) { - .system => { - // Mid-turn system messages aren't a thing in pantograph today. - // Treat as harmless and persist verbatim, sans stamps. - try batch_messages.append(alloc, .{ .role = .system, .content = blocks }); - try batch_providers.append(alloc, null); - try batch_models.append(alloc, null); - }, - .user => { - try batch_messages.append(alloc, .{ .role = .user, .content = blocks }); - try batch_providers.append(alloc, provider); - try batch_models.append(alloc, model); - }, - .assistant => { - // Pair this assistant message with its usage, if the - // receiver captured one for this position. (A turn - // ending in a stream error can have fewer usage entries - // than assistant messages; treat as null and move on.) - const usage: ?panto.session_manager.Usage = if (assistant_seen < per_message_usage.len) - per_message_usage[assistant_seen] - else - null; - assistant_seen += 1; - try batch_messages.append(alloc, .{ - .role = .assistant, - .content = blocks, - .provider = try alloc.dupe(u8, provider), - .model = try alloc.dupe(u8, model), - .stop_reason = try alloc.dupe(u8, "stop"), - .usage = usage, - }); - try batch_providers.append(alloc, null); - try batch_models.append(alloc, null); - }, - } - } - try mgr.appendMessagesAtomic(batch_messages.items, batch_providers.items, batch_models.items); -} - -/// Persist a compaction to the session log. The agent rewrote the -/// in-memory conversation to `[system..., summary, kept-suffix...]`; the -/// superseded prefix stays on disk untouched. We append everything from -/// the compaction summary onward as fresh entries: the summary message -/// (carrying the `compactionSummary` block) followed by the duplicated -/// kept-verbatim suffix. On replay, the latest summary resets effective -/// context, so the duplicated suffix is what survives. -/// -/// Usage is not threaded through here: the duplicated entries are replays -/// of already-sized turns, and the summary itself isn't an assistant turn. -pub fn persistCompaction( - alloc: std.mem.Allocator, - mgr: *panto.session_manager.SessionManager, - conv: *const panto.conversation.Conversation, - provider: []const u8, - model: []const u8, -) !void { - const start = panto.conversation.latestCompactionIndex(conv.messages.items) orelse return; - try persistTurn(alloc, mgr, conv, start, provider, model, &.{}); -} - -pub fn hasToolUseWithoutFollowingResults(conv: *const panto.conversation.Conversation, index: usize) bool { - const msg = conv.messages.items[index]; - for (msg.content.items) |block| { - if (block != .ToolUse) continue; - if (index + 1 >= conv.messages.items.len) return true; - const next = conv.messages.items[index + 1]; - if (next.role != .user) return true; - var found = false; - for (next.content.items) |next_block| { - if (next_block == .ToolResult and std.mem.eql(u8, next_block.ToolResult.tool_use_id, block.ToolUse.id)) { - found = true; - break; - } - } - if (!found) return true; - } - return false; -} 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); } |
