diff options
| author | t <t@tjp.lol> | 2026-06-07 11:35:49 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-07 11:35:49 -0600 |
| commit | b14859b9726185ab873356390068e887b7f486d3 (patch) | |
| tree | 6530d8ee62e7639b50c99a67d23f89e25ef572dc /libpanto/src/null_store.zig | |
| parent | d36a51358efbc48de3d5b732727455efc60acae1 (diff) | |
R2: redesign session store with wire-format identity
Replace the single-session SessionManager seam with a directory-backed
catalog. session_manager.zig -> file_system_jsonl_store.zig; the old
machinery becomes internal SessionFile, and a new FileSystemJSONLStore
implements the redesigned SessionStore vtable (create/list/resolve/latest/
load/appendMessages) minting Session/SessionInfo handles.
Session logs now record wire-format provider identity
({api_style, base_url, model, reasoning}) instead of a single provider
string or CLI aliases; no api_key material is ever stored. Disk* content
types renamed Stored*; the rich audit-oriented write record is
PersistentMessage (in-memory Message + WireIdentity + provenance).
Agent.init now takes a Session; persist_provider/persist_model display
strings deleted (banner stays alias-based, resume picks default model).
Message.metadata round-trips. Dangling-prompt recovery dropped. CLI
migrated to the catalog store for run/resume/list. Clean break, no version
bump (old logs wiped).
Diffstat (limited to 'libpanto/src/null_store.zig')
| -rw-r--r-- | libpanto/src/null_store.zig | 104 |
1 files changed, 60 insertions, 44 deletions
diff --git a/libpanto/src/null_store.zig b/libpanto/src/null_store.zig index d2337ee..1f3ab74 100644 --- a/libpanto/src/null_store.zig +++ b/libpanto/src/null_store.zig @@ -2,15 +2,10 @@ //! persistence (and the default backing for an `Agent` constructed without //! a real store). //! -//! Every append is dropped (but its messages are freed, honoring the store -//! ownership contract — appends consume their messages). `loadConversation` -//! returns an empty conversation with no dangling prompt. `activeModel` is -//! null and the session id is the empty string. -//! -//! Because freeing the dropped messages needs an allocator, `NullStore` -//! carries one. Construct it with `init(alloc)` and call `.store()` for the -//! interface handle. The struct is trivially copyable and holds no other -//! state, so the handle may borrow it for the agent's lifetime. +//! Every append is dropped. `load` returns null. `list` returns an empty +//! slice. `create`/`resolve`/`latest` mint/return empty handles. The struct +//! holds an allocator (needed to satisfy the `SessionInfo` ownership +//! contract for the empty handles it mints) and is trivially copyable. const std = @import("std"); const Allocator = std.mem.Allocator; @@ -19,9 +14,10 @@ const session_store_mod = @import("session_store.zig"); const conversation_mod = @import("conversation.zig"); const SessionStore = session_store_mod.SessionStore; -const LoadedSession = session_store_mod.LoadedSession; -const DiskMessage = session_store_mod.DiskMessage; -const ActiveModel = session_store_mod.ActiveModel; +const Session = session_store_mod.Session; +const SessionInfo = session_store_mod.SessionInfo; +const PersistentMessage = session_store_mod.PersistentMessage; +const Conversation = conversation_mod.Conversation; pub const NullStore = struct { allocator: Allocator, @@ -30,38 +26,62 @@ pub const NullStore = struct { return .{ .allocator = allocator }; } - fn appendMessagesVT( - ctx: *anyopaque, - messages: []DiskMessage, - _: []const ?[]const u8, - _: []const ?[]const u8, - ) anyerror!void { + fn emptyInfo(self: *NullStore) SessionInfo { + const a = self.allocator; + return .{ + .id = a.dupe(u8, "") catch "", + .created = a.dupe(u8, "") catch "", + .modified = a.dupe(u8, "") catch "", + .message_count = 0, + .last_user_message = a.dupe(u8, "") catch "", + .api_style = .openai_chat, + .base_url = a.dupe(u8, "") catch "", + .model = a.dupe(u8, "") catch "", + .reasoning = .default, + }; + } + + fn createVT(ctx: *anyopaque) Session { const self: *NullStore = @ptrCast(@alignCast(ctx)); - // Appends consume their messages; we drop them, so free them. - for (messages) |m| m.deinit(self.allocator); + return .{ .info = self.emptyInfo(), .store = self.store() }; } - fn loadConversationVT(ctx: *anyopaque, alloc: Allocator) anyerror!LoadedSession { - _ = ctx; - return .{ - .conversation = conversation_mod.Conversation.init(alloc), - .dangling_user = null, - }; + fn listVT(ctx: *anyopaque) anyerror![]SessionInfo { + const self: *NullStore = @ptrCast(@alignCast(ctx)); + return self.allocator.alloc(SessionInfo, 0); } - fn sessionIdVT(_: *anyopaque) []const u8 { - return ""; + fn freeSessionInfosVT(ctx: *anyopaque, infos: []SessionInfo) void { + const self: *NullStore = @ptrCast(@alignCast(ctx)); + for (infos) |i| i.deinit(self.allocator); + self.allocator.free(infos); } - fn activeModelVT(_: *anyopaque) ?ActiveModel { + fn resolveVT(_: *anyopaque, _: []const u8) anyerror!?Session { return null; } + fn latestVT(_: *anyopaque) anyerror!?Session { + return null; + } + + fn loadVT(_: *anyopaque, _: []const u8) anyerror!?Conversation { + return null; + } + + fn appendMessagesVT(_: *anyopaque, _: []const u8, _: []PersistentMessage) anyerror!void { + // Drop everything. The PersistentMessages borrow in-memory data + // owned by the caller (the conversation); nothing to free here. + } + const vtable: SessionStore.VTable = .{ + .create = createVT, + .list = listVT, + .freeSessionInfos = freeSessionInfosVT, + .resolve = resolveVT, + .latest = latestVT, + .load = loadVT, .appendMessages = appendMessagesVT, - .loadConversation = loadConversationVT, - .sessionId = sessionIdVT, - .activeModel = activeModelVT, }; /// Wrap this `NullStore` as a `SessionStore`. The handle borrows @@ -73,20 +93,16 @@ pub const NullStore = struct { const testing = std.testing; -test "NullStore: appends are dropped (and freed) and load returns empty" { +test "NullStore: appends are dropped and load returns null" { var ns = NullStore.init(testing.allocator); const s = ns.store(); - var msg_content = try testing.allocator.alloc(session_store_mod.DiskContentBlock, 1); - msg_content[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hi") } }; - var messages = [_]DiskMessage{.{ .role = .user, .content = msg_content }}; - // No defer-free: the store consumes (frees) the message. - try s.appendMessages(&messages, &.{null}, &.{null}); + try s.appendMessages("sid", &.{}); + try testing.expect((try s.load("sid")) == null); + try testing.expect((try s.resolve("sid")) == null); + try testing.expect((try s.latest()) == null); - var loaded = try s.loadConversation(testing.allocator); - defer loaded.deinit(testing.allocator); - try testing.expectEqual(@as(usize, 0), loaded.conversation.messages.items.len); - try testing.expect(loaded.dangling_user == null); - try testing.expect(s.activeModel() == null); - try testing.expectEqualStrings("", s.sessionId()); + const infos = try s.list(); + defer s.freeSessionInfos(infos); + try testing.expectEqual(@as(usize, 0), infos.len); } |
