summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.zig102
-rw-r--r--src/subcommand.zig8
-rw-r--r--src/system_prompt.zig111
3 files changed, 107 insertions, 114 deletions
diff --git a/src/main.zig b/src/main.zig
index 46b6320..247817a 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -329,48 +329,51 @@ pub fn main(init: std.process.Init) !void {
};
const banner_provider_initial: []const u8 = model_ref.provider;
+ // The session store: a directory-backed JSONL catalog rooted at the
+ // 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);
+ defer session_store_impl.deinit();
+ const session_store = session_store_impl.store();
+
// Create or resume the session. Resume failures (missing/ambiguous id)
// are user errors — print a tidy message and exit 1 rather than
// printing a Zig stack trace.
- var session_mgr = openSession(
- alloc,
- io,
- session_dir,
- cwd,
+ var session = openSession(
+ session_store,
cli_flags,
+ session_dir,
stdout,
&stdout_file,
) catch |err| switch (err) {
error.SessionNotFound, error.AmbiguousSessionId => std.process.exit(1),
else => return err,
};
- defer session_mgr.deinit();
+ // `session.info` is adopted by the agent below (`Agent.init` can't fail)
+ // and freed in the agent's `deinit`; no separate cleanup here.
- const is_resume = session_mgr.getEntries().len > 0;
+ const is_resume = cli_flags.resume_kind != .none and session.info.message_count > 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.
+ // On resume, reconstruct the conversation from the store. (The
+ // dangling-prompt recovery feature was dropped in R2.) On a fresh
+ // session, the agent starts with an empty conversation.
var adopted_conversation: ?panto.conversation.Conversation = null;
if (is_resume) {
- 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;
+ adopted_conversation = try session.load();
+ const sid = session.info.id;
try stdout.print(
- "resumed session {s} ({d} entries)\n",
- .{ session_mgr.getSessionId()[0..@min(8, session_mgr.getSessionId().len)], session_mgr.getEntries().len },
+ "resumed session {s} ({d} messages)\n",
+ .{ sid[0..@min(8, sid.len)], session.info.message_count },
);
}
// 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). The agent owns its own tool registry (post-R1); extensions
- // register their tool source onto it *after* the agent is built but
- // before the first turn, so in-place registration is visible.
+ // session handle (for free persistence) and the resumed conversation
+ // (or starts fresh). The agent owns its own tool registry (post-R1);
+ // extensions register their tool source onto it *after* the agent is
+ // 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 = .{
@@ -381,12 +384,10 @@ pub fn main(init: std.process.Init) !void {
alloc,
io,
&active_config,
- session_mgr.store(),
+ session,
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
@@ -642,54 +643,31 @@ fn parseAgentFlags(alloc: std.mem.Allocator, args: std.process.Args) !AgentFlags
// Session bootstrap
// -----------------------------------------------------------------------------
+/// Mint or resolve the `Session` to drive, against the catalog `store`.
+/// Fresh sessions are created on demand; resume resolves by id or picks the
+/// most recent. The returned `Session` owns its `info` (freed via the
+/// agent's `deinit`, which adopts it).
fn openSession(
- alloc: std.mem.Allocator,
- io: std.Io,
- session_dir: []const u8,
- cwd: []const u8,
+ store: panto.session_store.SessionStore,
flags: AgentFlags,
+ session_dir: []const u8,
stdout: *std.Io.Writer,
stdout_file: *std.Io.File.Writer,
-) !panto.session_manager.SessionManager {
+) !panto.session_store.Session {
switch (flags.resume_kind) {
- .none => return try panto.session_manager.SessionManager.init(
- alloc,
- io,
- session_dir,
- cwd,
- ),
+ .none => return store.create(),
.most_recent => {
- const path_opt = panto.session_manager.findMostRecentSession(alloc, io, session_dir) catch null;
- if (path_opt) |path| {
- defer alloc.free(path);
- return try panto.session_manager.SessionManager.open(alloc, io, path);
- }
+ if (try store.latest()) |sess| return sess;
try stdout.print("no sessions to resume; starting fresh.\n", .{});
try stdout_file.flush();
- return try panto.session_manager.SessionManager.init(
- alloc,
- io,
- session_dir,
- cwd,
- );
+ return store.create();
},
.by_id => {
const id = flags.resume_id.?;
- const path = panto.session_manager.resolveSessionId(alloc, io, session_dir, id) catch |err| switch (err) {
- error.SessionNotFound => {
- try stdout.print("error: no session matching '{s}' in {s}\n", .{ id, session_dir });
- try stdout_file.flush();
- return err;
- },
- error.AmbiguousSessionId => {
- try stdout.print("error: session id '{s}' is ambiguous\n", .{id});
- try stdout_file.flush();
- return err;
- },
- else => return err,
- };
- defer alloc.free(path);
- return try panto.session_manager.SessionManager.open(alloc, io, path);
+ if (try store.resolve(id)) |sess| return sess;
+ try stdout.print("error: no session matching '{s}' in {s}\n", .{ id, session_dir });
+ try stdout_file.flush();
+ return error.SessionNotFound;
},
}
}
diff --git a/src/subcommand.zig b/src/subcommand.zig
index c50429e..3ccd2c6 100644
--- a/src/subcommand.zig
+++ b/src/subcommand.zig
@@ -272,8 +272,12 @@ fn runSessionsSubcommand(
const session_dir = try session_paths.sessionDirForCwd(allocator, environ_map, cwd);
defer allocator.free(session_dir);
- const infos = try panto.session_manager.listSessions(allocator, io, session_dir, null);
- defer panto.session_manager.freeSessionInfos(allocator, infos);
+ var store_impl = try panto.session_manager.FileSystemJSONLStore.init(allocator, io, session_dir, cwd);
+ defer store_impl.deinit();
+ const store = store_impl.store();
+
+ const infos = try store.list();
+ defer store.freeSessionInfos(infos);
var stdout_buffer: [4096]u8 = undefined;
var stdout_file = std.Io.File.stdout().writer(io, &stdout_buffer);
diff --git a/src/system_prompt.zig b/src/system_prompt.zig
index 6104034..d8c06bb 100644
--- a/src/system_prompt.zig
+++ b/src/system_prompt.zig
@@ -462,29 +462,28 @@ const TmpLayers = struct {
}
};
-fn openTmpSession(arena: Allocator, root: []const u8) !panto.session_manager.SessionManager {
+fn openTmpStore(arena: Allocator, root: []const u8) !panto.session_manager.FileSystemJSONLStore {
const sessions = try std.fs.path.join(arena, &.{ root, "sessions" });
- return panto.session_manager.SessionManager.init(testing.allocator, testing.io, sessions, "/cwd");
+ return panto.session_manager.FileSystemJSONLStore.init(testing.allocator, testing.io, sessions, "/cwd");
}
/// Minimal agent harness for system-prompt tests: a throwaway provider
-/// config wrapping a session store and (optionally) an adopted
-/// conversation. 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 for its lifetime.
+/// config plus a session (minted from / resolved against the catalog
+/// 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,
agent: panto.agent.Agent,
fn init(
self: *SPAgentHarness,
- store: panto.session_store.SessionStore,
+ session: panto.session_store.Session,
adopted: ?panto.conversation.Conversation,
) 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, store, adopted);
+ self.agent = panto.agent.Agent.init(testing.allocator, testing.io, &self.config, session, adopted);
}
fn deinit(self: *SPAgentHarness) void {
@@ -492,17 +491,19 @@ const SPAgentHarness = struct {
}
};
-/// 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.)
-fn forceFlush(mgr: *panto.session_manager.SessionManager) !void {
- const blocks = try mgr.allocator.alloc(panto.session_manager.DiskContentBlock, 1);
- blocks[0] = .{ .text = .{ .text = try mgr.allocator.dupe(u8, "ok") } };
- _ = try mgr.appendMessage(
- .{ .role = .assistant, .content = blocks },
- "prov",
- "model",
- );
+/// 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.session_store.WireIdentity = .{ .api_style = .openai_chat, .base_url = "u", .model = "m" };
+ var batch = [_]panto.session_store.PersistentMessage{
+ .{ .message = conv.messages.items[0], .identity = id },
+ };
+ try agent.session.append(&batch);
}
test "seedFresh then no-config-change resume is a no-op" {
@@ -518,29 +519,39 @@ test "seedFresh then no-config-change resume is a no-op" {
const project_dir = try layers.projectDir(arena);
// Fresh session: seed + persist (through the agent).
- var mgr = try openTmpSession(arena, layers.root);
+ var store_impl = try openTmpStore(arena, layers.root);
+ defer store_impl.deinit();
+ const store = store_impl.store();
+ var session_id_buf: [64]u8 = undefined;
+ var session_id_len: usize = 0;
{
var h: SPAgentHarness = undefined;
- h.init(mgr.store(), null);
+ 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 forceFlush(&mgr); // persist seed + append (+ assistant) to disk
- const entries_on_disk = mgr.getEntries().len;
- try testing.expectEqual(@as(usize, 3), entries_on_disk); // seed + append + assistant
- const session_file = try arena.dupe(u8, mgr.getSessionFile());
- mgr.deinit();
+ const session_id = session_id_buf[0..session_id_len];
- // 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();
- const conv2 = try mgr2.rebuildConversation();
+ // 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 seeded_count = eff_disk.items.len;
+ conv_disk.deinit();
+ try testing.expect(seeded_count > 0);
+
+ // Resume: adopt the rebuilt conversation + reconcile against unchanged
+ // config → the effective system prompt is unchanged.
+ var sess2 = (try store.resolve(session_id)).?;
+ const conv2 = try sess2.load();
var h2: SPAgentHarness = undefined;
- h2.init(mgr2.store(), conv2);
+ h2.init(sess2, conv2);
defer h2.deinit();
try reconcileResumeLayers(arena, testing.io, null, null, project_dir, &h2.agent);
- try testing.expectEqual(entries_on_disk, mgr2.getEntries().len);
+ const eff_after = try panto.conversation.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items);
+ try testing.expectEqual(seeded_count, eff_after.items.len);
}
test "resume after config change appends replace + append sequence" {
@@ -554,44 +565,44 @@ test "resume after config change appends replace + append sequence" {
try layers.writeProject("SYSTEM.md", "old seed");
const project_dir = try layers.projectDir(arena);
- var mgr = try openTmpSession(arena, layers.root);
+ var store_impl = try openTmpStore(arena, layers.root);
+ defer store_impl.deinit();
+ const store = store_impl.store();
+ var session_id_buf: [64]u8 = undefined;
+ var session_id_len: usize = 0;
{
var h: SPAgentHarness = undefined;
- h.init(mgr.store(), null);
+ 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 forceFlush(&mgr); // persist old seed (+ assistant) to disk
- try testing.expectEqual(@as(usize, 2), mgr.getEntries().len); // seed + assistant
- const session_file = try arena.dupe(u8, mgr.getSessionFile());
- mgr.deinit();
+ const session_id = session_id_buf[0..session_id_len];
// Change the config: new seed + a new append.
try layers.writeProject("SYSTEM.md", "new seed");
try layers.writeProject("APPEND_SYSTEM.md", "new append");
- var mgr2 = try panto.session_manager.SessionManager.open(alloc, testing.io, session_file);
- defer mgr2.deinit();
- const conv2 = try mgr2.rebuildConversation();
+ var sess2 = (try store.resolve(session_id)).?;
+ const conv2 = try sess2.load();
var h2: SPAgentHarness = undefined;
- h2.init(mgr2.store(), conv2);
+ h2.init(sess2, 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, 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]);
- // 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;
+ // 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);
- try testing.expectEqual(after_first, mgr2.getEntries().len);
+ const eff2 = try panto.conversation.effectiveSystemBlocks(arena, h2.agent.conversation.messages.items);
+ try testing.expectEqual(@as(usize, 2), eff2.items.len);
}
test "Resolved.blocks - seed leads appends" {