From f83578fdc9264019a1a1cef8c5484a161167d3dd Mon Sep 17 00:00:00 2001 From: t Date: Tue, 7 Jul 2026 11:26:32 -0600 Subject: initial commit, moved libpanto over from the pantograph repo --- src/null_store.zig | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/null_store.zig (limited to 'src/null_store.zig') diff --git a/src/null_store.zig b/src/null_store.zig new file mode 100644 index 0000000..1f3ab74 --- /dev/null +++ b/src/null_store.zig @@ -0,0 +1,108 @@ +//! `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); +} -- cgit v1.3