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.zig97
1 files changed, 56 insertions, 41 deletions
diff --git a/libpanto/src/null_store.zig b/libpanto/src/null_store.zig
index a9bd813..d2337ee 100644
--- a/libpanto/src/null_store.zig
+++ b/libpanto/src/null_store.zig
@@ -1,11 +1,16 @@
//! `NullStore`: a no-op `SessionStore` for embedders who opt out of
//! persistence (and the default backing for an `Agent` constructed without
-//! an explicit store).
+//! a real 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.
+//! 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;
@@ -18,54 +23,64 @@ 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;
+pub const NullStore = struct {
+ allocator: Allocator,
-fn appendMessagesVT(
- _: *anyopaque,
- _: []DiskMessage,
- _: []const ?[]const u8,
- _: []const ?[]const u8,
-) anyerror!void {}
+ pub fn init(allocator: Allocator) NullStore {
+ return .{ .allocator = allocator };
+ }
-fn loadConversationVT(_: *anyopaque, alloc: Allocator) anyerror!LoadedSession {
- return .{
- .conversation = conversation_mod.Conversation.init(alloc),
- .dangling_user = null,
- };
-}
+ 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 sessionIdVT(_: *anyopaque) []const u8 {
- return "";
-}
+ fn loadConversationVT(ctx: *anyopaque, alloc: Allocator) anyerror!LoadedSession {
+ _ = ctx;
+ return .{
+ .conversation = conversation_mod.Conversation.init(alloc),
+ .dangling_user = null,
+ };
+ }
-fn activeModelVT(_: *anyopaque) ?ActiveModel {
- return null;
-}
+ fn sessionIdVT(_: *anyopaque) []const u8 {
+ return "";
+ }
-const vtable: SessionStore.VTable = .{
- .appendMessages = appendMessagesVT,
- .loadConversation = loadConversationVT,
- .sessionId = sessionIdVT,
- .activeModel = activeModelVT,
-};
+ fn activeModelVT(_: *anyopaque) ?ActiveModel {
+ return null;
+ }
-/// A no-op `SessionStore`. Cheap to call repeatedly; all handles share one
-/// stateless context.
-pub fn store() SessionStore {
- return .{ .ptr = &singleton, .vtable = &vtable };
-}
+ 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 load returns empty" {
+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 }};
- defer messages[0].deinit(testing.allocator);
-
- const s = store();
+ // No defer-free: the store consumes (frees) the message.
try s.appendMessages(&messages, &.{null}, &.{null});
var loaded = try s.loadConversation(testing.allocator);