summaryrefslogtreecommitdiff
path: root/src/null_store.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-07-07 11:26:32 -0600
committert <t@tjp.lol>2026-07-07 11:26:45 -0600
commitf83578fdc9264019a1a1cef8c5484a161167d3dd (patch)
tree888f11767f944d61e5ca8eb92fa1b2dba295a4b8 /src/null_store.zig
initial commit, moved libpanto over from the pantograph repo
Diffstat (limited to 'src/null_store.zig')
-rw-r--r--src/null_store.zig108
1 files changed, 108 insertions, 0 deletions
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);
+}