//! `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. `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; const session_store_mod = @import("session_store.zig"); const conversation_mod = @import("conversation.zig"); const SessionStore = session_store_mod.SessionStore; 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, pub fn init(allocator: Allocator) NullStore { return .{ .allocator = allocator }; } 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)); return .{ .info = self.emptyInfo(), .store = self.store() }; } fn listVT(ctx: *anyopaque) anyerror![]SessionInfo { const self: *NullStore = @ptrCast(@alignCast(ctx)); return self.allocator.alloc(SessionInfo, 0); } 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 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, }; /// 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 null" { var ns = NullStore.init(testing.allocator); const s = ns.store(); 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); const infos = try s.list(); defer s.freeSessionInfos(infos); try testing.expectEqual(@as(usize, 0), infos.len); }