diff options
Diffstat (limited to 'libpanto/src/null_store.zig')
| -rw-r--r-- | libpanto/src/null_store.zig | 97 |
1 files changed, 56 insertions, 41 deletions
diff --git a/libpanto/src/null_store.zig b/libpanto/src/null_store.zig index a9bd813..d2337ee 100644 --- a/libpanto/src/null_store.zig +++ b/libpanto/src/null_store.zig @@ -1,11 +1,16 @@ //! `NullStore`: a no-op `SessionStore` for embedders who opt out of //! persistence (and the default backing for an `Agent` constructed without -//! an explicit store). +//! a real store). //! -//! Every append is dropped. `loadConversation` returns an empty -//! conversation with no dangling prompt. `activeModel` is null and the -//! session id is the empty string. The store is stateless, so a single -//! process-global instance backs every `NullStore` handle. +//! 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. const std = @import("std"); const Allocator = std.mem.Allocator; @@ -18,54 +23,64 @@ const LoadedSession = session_store_mod.LoadedSession; const DiskMessage = session_store_mod.DiskMessage; const ActiveModel = session_store_mod.ActiveModel; -/// Stateless singleton context. The vtable ignores `ctx` entirely; we hand -/// out a pointer to this so the `*anyopaque` is always valid. -var singleton: u8 = 0; +pub const NullStore = struct { + allocator: Allocator, -fn appendMessagesVT( - _: *anyopaque, - _: []DiskMessage, - _: []const ?[]const u8, - _: []const ?[]const u8, -) anyerror!void {} + pub fn init(allocator: Allocator) NullStore { + return .{ .allocator = allocator }; + } -fn loadConversationVT(_: *anyopaque, alloc: Allocator) anyerror!LoadedSession { - return .{ - .conversation = conversation_mod.Conversation.init(alloc), - .dangling_user = null, - }; -} + fn appendMessagesVT( + ctx: *anyopaque, + messages: []DiskMessage, + _: []const ?[]const u8, + _: []const ?[]const u8, + ) anyerror!void { + const self: *NullStore = @ptrCast(@alignCast(ctx)); + // Appends consume their messages; we drop them, so free them. + for (messages) |m| m.deinit(self.allocator); + } -fn sessionIdVT(_: *anyopaque) []const u8 { - return ""; -} + fn loadConversationVT(ctx: *anyopaque, alloc: Allocator) anyerror!LoadedSession { + _ = ctx; + return .{ + .conversation = conversation_mod.Conversation.init(alloc), + .dangling_user = null, + }; + } -fn activeModelVT(_: *anyopaque) ?ActiveModel { - return null; -} + fn sessionIdVT(_: *anyopaque) []const u8 { + return ""; + } -const vtable: SessionStore.VTable = .{ - .appendMessages = appendMessagesVT, - .loadConversation = loadConversationVT, - .sessionId = sessionIdVT, - .activeModel = activeModelVT, -}; + fn activeModelVT(_: *anyopaque) ?ActiveModel { + return null; + } -/// A no-op `SessionStore`. Cheap to call repeatedly; all handles share one -/// stateless context. -pub fn store() SessionStore { - return .{ .ptr = &singleton, .vtable = &vtable }; -} + const vtable: SessionStore.VTable = .{ + .appendMessages = appendMessagesVT, + .loadConversation = loadConversationVT, + .sessionId = sessionIdVT, + .activeModel = activeModelVT, + }; + + /// Wrap this `NullStore` as a `SessionStore`. The handle borrows + /// `self`; `self` must outlive it. + pub fn store(self: *NullStore) SessionStore { + return .{ .ptr = self, .vtable = &vtable }; + } +}; const testing = std.testing; -test "NullStore: appends are dropped and load returns empty" { +test "NullStore: appends are dropped (and freed) and load returns empty" { + 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 }}; - defer messages[0].deinit(testing.allocator); - - const s = store(); + // No defer-free: the store consumes (frees) the message. try s.appendMessages(&messages, &.{null}, &.{null}); var loaded = try s.loadConversation(testing.allocator); |
