summaryrefslogtreecommitdiff
path: root/libpanto/src/null_store.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/null_store.zig')
-rw-r--r--libpanto/src/null_store.zig77
1 files changed, 77 insertions, 0 deletions
diff --git a/libpanto/src/null_store.zig b/libpanto/src/null_store.zig
new file mode 100644
index 0000000..a9bd813
--- /dev/null
+++ b/libpanto/src/null_store.zig
@@ -0,0 +1,77 @@
+//! `NullStore`: a no-op `SessionStore` for embedders who opt out of
+//! persistence (and the default backing for an `Agent` constructed without
+//! an explicit 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.
+
+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;
+
+/// 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;
+
+fn appendMessagesVT(
+ _: *anyopaque,
+ _: []DiskMessage,
+ _: []const ?[]const u8,
+ _: []const ?[]const u8,
+) anyerror!void {}
+
+fn loadConversationVT(_: *anyopaque, alloc: Allocator) anyerror!LoadedSession {
+ 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,
+};
+
+/// A no-op `SessionStore`. Cheap to call repeatedly; all handles share one
+/// stateless context.
+pub fn store() SessionStore {
+ return .{ .ptr = &singleton, .vtable = &vtable };
+}
+
+const testing = std.testing;
+
+test "NullStore: appends are dropped and load returns empty" {
+ 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();
+ 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());
+}