summaryrefslogtreecommitdiff
path: root/libpanto
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-07-04 09:50:12 -0600
committert <t@tjp.lol>2026-07-05 16:23:49 -0600
commitf759f149377942c4e04802c45162cda1c9bfb2b3 (patch)
tree397dd2fc35839d8fcbf6a5c237bee363c6ed3c07 /libpanto
parent1ed07e2e4473b91c669c062bbfef6bb499f7d2b7 (diff)
big cli/tui gaps project
Diffstat (limited to 'libpanto')
-rw-r--r--libpanto/src/file_system_jsonl_store.zig98
-rw-r--r--libpanto/src/provider_openai_chat.zig52
2 files changed, 93 insertions, 57 deletions
diff --git a/libpanto/src/file_system_jsonl_store.zig b/libpanto/src/file_system_jsonl_store.zig
index 5b7f450..9bc9436 100644
--- a/libpanto/src/file_system_jsonl_store.zig
+++ b/libpanto/src/file_system_jsonl_store.zig
@@ -554,7 +554,11 @@ pub const SessionFile = struct {
if (self.flushed) {
try self.persistEntries(entries);
} else {
- try self.flushBufferedMany(entries);
+ // File is created on the first assistant message (see module docs).
+ for (messages) |m| if (m.role == .assistant) {
+ try self.flushBufferedMany(entries);
+ break;
+ };
}
for (entries, 0..) |entry, i| {
@@ -976,39 +980,6 @@ fn buildFileInfo(
// Recent / resume helpers
// =============================================================================
-/// Find the most recent session file in `session_dir`. Returns null if
-/// none exist. Caller owns the returned path.
-pub fn findMostRecentSession(allocator: Allocator, io: Io, session_dir: []const u8) !?[]u8 {
- var dir = Io.Dir.cwd().openDir(io, session_dir, .{}) catch |err| switch (err) {
- error.FileNotFound => return null,
- else => return err,
- };
- defer dir.close(io);
-
- var best_name: ?[]u8 = null;
- errdefer if (best_name) |b| allocator.free(b);
-
- var it = dir.iterate();
- while (try it.next(io)) |entry| {
- if (entry.kind != .file) continue;
- if (!std.mem.endsWith(u8, entry.name, ".jsonl")) continue;
- if (best_name) |b| {
- // Lexicographic compare. UUIDv7 filenames sort chronologically.
- if (std.mem.order(u8, entry.name, b) == .gt) {
- allocator.free(b);
- best_name = try allocator.dupe(u8, entry.name);
- }
- } else {
- best_name = try allocator.dupe(u8, entry.name);
- }
- }
-
- const name = best_name orelse return null;
- defer allocator.free(name);
- best_name = null;
- return try std.fs.path.join(allocator, &.{ session_dir, name });
-}
-
/// Resolve a (possibly abbreviated) session id to a session file path
/// within `session_dir`. Errors if no match or ambiguous prefix.
pub fn resolveSessionId(
@@ -1209,9 +1180,16 @@ pub const FileSystemJSONLStore = struct {
fn latestVT(ctx: *anyopaque) anyerror!?session_store_mod.Session {
const self: *FileSystemJSONLStore = @ptrCast(@alignCast(ctx));
- const path = (try findMostRecentSession(self.allocator, self.io, self.dir)) orelse return null;
- defer self.allocator.free(path);
- return try self.sessionFromPath(path);
+ // Most-recently-*modified* wins, matching the `list()` sort order —
+ // not newest-created (lexicographic UUIDv7 filename).
+ const fis = try listSessions(self.allocator, self.io, self.dir, null);
+ defer {
+ for (fis) |fi| fi.deinit(self.allocator);
+ self.allocator.free(fis);
+ }
+ if (fis.len == 0) return null;
+ const info = try self.infoFromFileInfo(fis[0]);
+ return .{ .info = info, .store = self.store() };
}
fn sessionFromPath(self: *FileSystemJSONLStore, path: []const u8) !?session_store_mod.Session {
@@ -1436,25 +1414,25 @@ test "SessionFile: full flow — buffer, flush on assistant, append, resume" {
);
defer mgr.deinit();
- // First append flushes the header + entry to disk immediately.
+ // System message buffers in memory — nothing on disk yet.
const sys_blocks = try testing.allocator.alloc(StoredContentBlock, 1);
sys_blocks[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "You are helpful.") } };
_ = try mgr.appendMessage(
.{ .role = .system, .content = sys_blocks },
null,
);
- try testing.expect(mgr.isFlushed());
+ try testing.expect(!mgr.isFlushed());
- // User message — appended to the already-flushed file.
+ // User message — still buffered.
const usr_blocks = try testing.allocator.alloc(StoredContentBlock, 1);
usr_blocks[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hi there") } };
_ = try mgr.appendMessage(
.{ .role = .user, .content = usr_blocks },
oaStamp(),
);
- try testing.expect(mgr.isFlushed());
+ try testing.expect(!mgr.isFlushed());
- // Assistant message.
+ // Assistant message — first flush: header + all buffered entries.
const a_blocks = try testing.allocator.alloc(StoredContentBlock, 1);
a_blocks[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hello") } };
_ = try mgr.appendMessage(
@@ -1698,7 +1676,7 @@ test "listSessions: returns most recent first, with counts" {
try testing.expect(std.mem.order(u8, infos[0].modified, infos[1].modified) != .lt);
}
-test "findMostRecentSession: picks lexicographically greatest" {
+test "latest: picks most-recently-modified, not newest-created" {
const io = testing.io;
var td = try TmpSessionDir.init(testing.allocator);
@@ -1706,12 +1684,17 @@ test "findMostRecentSession: picks lexicographically greatest" {
const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" });
defer testing.allocator.free(sessions);
- // Pre-resolution before any sessions exist → null.
- try testing.expect((try findMostRecentSession(testing.allocator, io, sessions)) == null);
+ var catalog = try FileSystemJSONLStore.init(testing.allocator, io, sessions);
+ defer catalog.deinit();
+ const st = catalog.store();
- // Create two.
- var second_file: ?[]u8 = null;
- defer if (second_file) |s| testing.allocator.free(s);
+ // Before any sessions exist → null.
+ try testing.expect((try st.latest()) == null);
+
+ // Create session A, then session B (newer id), then append to A again
+ // so A is the most recently *modified*.
+ var first_id: ?[]u8 = null;
+ defer if (first_id) |s| testing.allocator.free(s);
for (0..2) |i| {
var mgr = try SessionFile.init(testing.allocator, io, sessions, null);
@@ -1721,14 +1704,23 @@ test "findMostRecentSession: picks lexicographically greatest" {
_ = try mgr.appendMessage(.{ .role = .user, .content = u }, oaStamp());
const a = try testing.allocator.alloc(StoredContentBlock, 1);
a[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "a") } };
- _ = try mgr.appendMessage(.{ .role = .assistant, .content = a }, null);
- if (i == 1) second_file = try testing.allocator.dupe(u8, mgr.getSessionFile());
+ _ = try mgr.appendMessage(.{ .role = .assistant, .content = a }, oaStamp());
+ if (i == 0) first_id = try testing.allocator.dupe(u8, mgr.header.id);
io.sleep(.fromMilliseconds(2), .real) catch {};
}
+ {
+ const path = try resolveSessionId(testing.allocator, io, sessions, first_id.?);
+ defer testing.allocator.free(path);
+ var mgr = try SessionFile.open(testing.allocator, io, path);
+ defer mgr.deinit();
+ const a = try testing.allocator.alloc(StoredContentBlock, 1);
+ a[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "a") } };
+ _ = try mgr.appendMessage(.{ .role = .assistant, .content = a }, oaStamp());
+ }
- const found = (try findMostRecentSession(testing.allocator, io, sessions)).?;
- defer testing.allocator.free(found);
- try testing.expectEqualStrings(second_file.?, found);
+ var latest = (try st.latest()).?;
+ defer latest.info.deinit(testing.allocator);
+ try testing.expectEqualStrings(first_id.?, latest.info.id);
}
test "SessionFile: tool-use round-trip — assistant w/ ToolUse, user w/ ToolResult, assistant" {
diff --git a/libpanto/src/provider_openai_chat.zig b/libpanto/src/provider_openai_chat.zig
index f653ff6..a05c7ba 100644
--- a/libpanto/src/provider_openai_chat.zig
+++ b/libpanto/src/provider_openai_chat.zig
@@ -674,23 +674,23 @@ fn handleEvent(
try out.push(.{ .message_start = .assistant });
}
- if (d.reasoning_content) |rc| {
+ if (d.reasoning_content) |rc| if (rc.len > 0) {
if (!state.started) {
state.started = true;
try out.push(.{ .message_start = .assistant });
}
try state.openBlock(.thinking, out);
try state.appendDelta(out, rc);
- }
+ };
- if (d.content) |c| {
+ if (d.content) |c| if (c.len > 0) {
if (!state.started) {
state.started = true;
try out.push(.{ .message_start = .assistant });
}
try state.openBlock(.text, out);
try state.appendDelta(out, c);
- }
+ };
if (d.tool_calls.len > 0) {
if (!state.started) {
@@ -909,6 +909,50 @@ test "openai_chat: omitted stream usage yields null on message_complete" {
try testing.expectEqualStrings("msg_complete[usage:null]", found.?);
}
+test "openai_chat: empty content alongside reasoning does not split thinking block" {
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+ try addUserText(&conv, "think");
+
+ var rec = EventRecorder{ .allocator = allocator };
+ defer rec.deinit();
+
+ const events = [_][]const u8{
+ \\{"choices":[{"delta":{"role":"assistant"}}]}
+ ,
+ \\{"choices":[{"delta":{"reasoning_content":"rea","content":""}}]}
+ ,
+ \\{"choices":[{"delta":{"reasoning_content":"son","content":""}}]}
+ ,
+ \\{"choices":[{"delta":{"reasoning_content":"ing","content":""}}]}
+ ,
+ \\{"choices":[{"delta":{},"finish_reason":"stop"}]}
+ ,
+ "[DONE]",
+ };
+ try runStreamedTurn(allocator, &conv, &rec, &events);
+
+ const expected = [_][]const u8{
+ "msg_start",
+ "block_start[0]:Thinking",
+ "delta[0]:rea",
+ "delta[0]:son",
+ "delta[0]:ing",
+ "block_complete[0]",
+ "msg_complete[usage:null]",
+ };
+ try testing.expectEqual(expected.len, rec.events.items.len);
+ for (expected, rec.events.items) |want, got| {
+ try testing.expectEqualStrings(want, got);
+ }
+
+ const asst = conv.messages.items[1];
+ try testing.expectEqual(@as(usize, 1), asst.content.items.len);
+ try testing.expectEqualStrings("reasoning", asst.content.items[0].Thinking.text.items);
+}
+
test "fragmented tool_call id and name are reassembled" {
// Lenient OpenAI-compatible providers occasionally split `id` and
// `function.name` across multiple deltas instead of sending them whole