diff options
| author | t <t@tjp.lol> | 2026-06-05 11:27:33 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-05 11:27:43 -0600 |
| commit | 03ac69658517a0054d2a573d1e1b60c1bfaaf977 (patch) | |
| tree | c38c4b009c98e96497db7482d04f803bf2ee78f9 /libpanto/src/null_store.zig | |
| parent | 003908344336cc13e74618291aa9f3af137f030f (diff) | |
Add pluggable SessionStore interface with FSJSONL + Null backends
Introduce a neutral persistence seam (session_store.zig) following the
{ptr, vtable} shape of the other libpanto seams. The interface traffics in
DiskMessage so non-JSONL backends (e.g. Postgres) can implement it.
- session_store.zig: SessionStore vtable (appendMessages, loadConversation,
sessionId, activeModel), LoadedSession{conversation, dangling_user},
re-exported disk types, and an FSJSONLStore alias.
- session_manager.zig: add store() wrapper + loadConversation() with
dangling trailing-user detection (excluded from the rebuilt conversation,
returned as dangling_user).
- null_store.zig: no-op backend, stateless singleton.
- root.zig: export session_store + null_store.
Phase 1+2 of docs/pluggable-session-store.md. Behavior-preserving; CLI not
yet rewired.
Diffstat (limited to 'libpanto/src/null_store.zig')
| -rw-r--r-- | libpanto/src/null_store.zig | 77 |
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()); +} |
