summaryrefslogtreecommitdiff
path: root/libpanto/src/session.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-02 09:02:49 -0600
committert <t@tjp.lol>2026-06-02 09:49:18 -0600
commit9c64a7d4462a11674e2dea481b037b5f5d9c62fc (patch)
treea4f0a493a9b7558d40f5eb79a0eeae46ecc23515 /libpanto/src/session.zig
parent456d986be1357c247753d9dc21734bb898d5e78b (diff)
system prompt building and logging
Diffstat (limited to 'libpanto/src/session.zig')
-rw-r--r--libpanto/src/session.zig72
1 files changed, 72 insertions, 0 deletions
diff --git a/libpanto/src/session.zig b/libpanto/src/session.zig
index e77d121..c1fd1c4 100644
--- a/libpanto/src/session.zig
+++ b/libpanto/src/session.zig
@@ -100,9 +100,18 @@ pub const MessageEntry = struct {
pub const DiskMessageRole = enum { system, user, assistant };
+/// Mode for a system-role message. Mirrors `conversation.SystemMode`.
+/// `append` adds to the effective prompt; `replace` discards all prior
+/// system text. Only meaningful on system messages; absent on disk means
+/// `append` (back-compatible with pre-mode logs).
+pub const DiskSystemMode = enum { append, replace };
+
pub const DiskMessage = struct {
role: DiskMessageRole,
content: []DiskContentBlock, // owned
+ /// System-message mode. Recorded only for system-role messages; an
+ /// absent `mode` on disk parses back as `.append`.
+ mode: DiskSystemMode = .append,
// Assistant-only metadata. Null for system/user messages.
provider: ?[]const u8 = null, // owned
model: ?[]const u8 = null, // owned
@@ -296,6 +305,12 @@ fn writeDiskMessage(s: *std.json.Stringify, msg: DiskMessage) !void {
try s.beginObject();
try s.objectField("role");
try s.write(@tagName(msg.role));
+ // `mode` is meaningful only for system messages. Emit it there so the
+ // append/replace semantics round-trip; omit it everywhere else.
+ if (msg.role == .system) {
+ try s.objectField("mode");
+ try s.write(@tagName(msg.mode));
+ }
try s.objectField("content");
try s.beginArray();
for (msg.content) |block| {
@@ -479,6 +494,14 @@ fn parseDiskMessage(allocator: Allocator, obj: std.json.ObjectMap) ParseError!Di
if (role_v != .string) return error.MissingField;
const role = std.meta.stringToEnum(DiskMessageRole, role_v.string) orelse return error.UnknownRole;
+ // `mode` is optional; absent defaults to `.append`. Unknown values are
+ // tolerated as `.append` rather than rejecting an otherwise-valid log.
+ const mode: DiskSystemMode = blk: {
+ const mv = obj.get("mode") orelse break :blk .append;
+ if (mv != .string) break :blk .append;
+ break :blk std.meta.stringToEnum(DiskSystemMode, mv.string) orelse .append;
+ };
+
const content_v = obj.get("content") orelse return error.MissingField;
if (content_v != .array) return error.MissingField;
var content_list = try std.ArrayList(DiskContentBlock).initCapacity(allocator, content_v.array.items.len);
@@ -520,6 +543,7 @@ fn parseDiskMessage(allocator: Allocator, obj: std.json.ObjectMap) ParseError!Di
return .{
.role = role,
.content = content,
+ .mode = mode,
.provider = provider,
.model = model,
.stop_reason = stop_reason,
@@ -612,6 +636,13 @@ pub fn contentBlockToDisk(
const content = try allocator.dupe(u8, tr.content.items);
return .{ .tool_result = .{ .tool_use_id = tuid, .content = content } };
},
+ // A `.System` block becomes a disk text block; its mode rides on
+ // the enclosing `DiskMessage.mode` (set by the session manager),
+ // not on the block itself.
+ .System => |sb| {
+ const text = try allocator.dupe(u8, sb.text.items);
+ return .{ .text = .{ .text = text } };
+ },
}
}
@@ -808,6 +839,47 @@ test "serialize/parse tool result message entry" {
try testing.expectEqualStrings("anthropic", got.provider.?);
}
+test "system message mode round-trips; absent mode defaults to append" {
+ const a = testing.allocator;
+
+ // replace-mode system entry round-trips.
+ {
+ var content = try a.alloc(DiskContentBlock, 1);
+ content[0] = .{ .text = .{ .text = try dupe(a, "fresh seed") } };
+ const entry: SessionEntry = .{ .message = .{
+ .base = .{
+ .id = try dupe(a, "aabbccdd"),
+ .parent_id = null,
+ .timestamp = try dupe(a, "2026-04-25T17:40:00Z"),
+ },
+ .message = .{
+ .role = .system,
+ .content = content,
+ .mode = .replace,
+ },
+ } };
+ defer entry.deinit(a);
+
+ const line = try serializeEntry(a, entry);
+ defer a.free(line);
+ try testing.expect(std.mem.indexOf(u8, line, "\"mode\":\"replace\"") != null);
+
+ var fe = try parseLine(a, line);
+ defer fe.deinit(a);
+ try testing.expectEqual(DiskSystemMode.replace, fe.entry.message.message.mode);
+ }
+
+ // A legacy system entry with no `mode` parses back as append.
+ {
+ const line =
+ \\{"type":"message","id":"abcdefab","parentId":null,"timestamp":"2026-04-25T17:40:00Z","message":{"role":"system","content":[{"type":"text","text":"hi"}]}}
+ ;
+ var fe = try parseLine(a, line);
+ defer fe.deinit(a);
+ try testing.expectEqual(DiskSystemMode.append, fe.entry.message.message.mode);
+ }
+}
+
test "parse: null parentId is handled" {
const a = testing.allocator;
const line =