//! `NullStore`: a no-op `SessionStore` for embedders who opt out of //! 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. const std = @import("std"); const Allocator = std.mem.Allocator; 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; pub const NullStore = struct { allocator: Allocator, pub fn init(allocator: Allocator) NullStore { return .{ .allocator = allocator }; } 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 loadConversationVT(ctx: *anyopaque, alloc: Allocator) anyerror!LoadedSession { _ = ctx; return .{ .conversation = conversation_mod.Conversation.init(alloc), .dangling_user = null, }; } fn sessionIdVT(_: *anyopaque) []const u8 { return ""; } fn activeModelVT(_: *anyopaque) ?ActiveModel { return null; } 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 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 }}; // No defer-free: the store consumes (frees) the message. try s.appendMessages(&messages, &.{null}, &.{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()); }