summaryrefslogtreecommitdiff
path: root/libpanto/src/conversation.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-13 23:45:59 -0600
committert <t@tjp.lol>2026-06-15 15:08:32 -0600
commit02b4c7a35ac0b714bc045d54fb4bb45d1ce4e490 (patch)
tree134196a82dd6fdd55b735be4c0cf38428c85038d /libpanto/src/conversation.zig
parent71643a5d69ffc40882c9fcde3cc8a3bcf02d7396 (diff)
Add Codex Responses support and session debugging
Teach provider config and auth resolution about the Codex Responses dialect, including user-facing `style = "openai_responses"` with `dialect = "codex"`. Serialize and parse Responses traffic with provider-specific reasoning replay, assistant phase metadata, and robust function-call assembly keyed by `output_index` so streamed tool inputs survive proxy quirks and empty terminal payloads. Also persist thinking origins and message metadata across sessions, add the Anthropic interleaved-thinking header switch, write per-session debug logs, and improve the TUI and scripts for inspecting tool output and session costs.
Diffstat (limited to 'libpanto/src/conversation.zig')
-rw-r--r--libpanto/src/conversation.zig60
1 files changed, 59 insertions, 1 deletions
diff --git a/libpanto/src/conversation.zig b/libpanto/src/conversation.zig
index 1942a0c..760f3ee 100644
--- a/libpanto/src/conversation.zig
+++ b/libpanto/src/conversation.zig
@@ -1,5 +1,6 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
+const config = @import("config.zig");
/// A streaming text buffer used by content blocks.
/// Thin alias over ArrayList(u8) — amortized O(1) appends,
@@ -13,19 +14,60 @@ pub fn textualBlockFromSlice(alloc: Allocator, slice: []const u8) !TextualBlock
return buf;
}
+/// Provenance for a replayable thinking signature. Scoped conservatively to
+/// the exact provider endpoint + wire model that produced the enclosing
+/// assistant message.
+pub const SignatureOrigin = struct {
+ api_style: config.APIStyle,
+ base_url: []const u8,
+ model: []const u8,
+
+ pub fn init(
+ alloc: Allocator,
+ api_style: config.APIStyle,
+ base_url: []const u8,
+ model: []const u8,
+ ) !SignatureOrigin {
+ const burl = try alloc.dupe(u8, base_url);
+ errdefer alloc.free(burl);
+ const mdl = try alloc.dupe(u8, model);
+ return .{ .api_style = api_style, .base_url = burl, .model = mdl };
+ }
+
+ pub fn deinit(self: *SignatureOrigin, alloc: Allocator) void {
+ alloc.free(self.base_url);
+ alloc.free(self.model);
+ }
+
+ pub fn dupe(self: SignatureOrigin, alloc: Allocator) !SignatureOrigin {
+ return init(alloc, self.api_style, self.base_url, self.model);
+ }
+
+ pub fn matches(self: SignatureOrigin, api_style: config.APIStyle, base_url: []const u8, model: []const u8) bool {
+ return self.api_style == api_style and
+ std.mem.eql(u8, self.base_url, base_url) and
+ std.mem.eql(u8, self.model, model);
+ }
+};
+
/// A reasoning/thinking block from the assistant.
///
/// `text` is the streamed reasoning content. `signature` is the opaque
/// integrity token Anthropic emits with extended-thinking responses and
/// requires echoed back verbatim on follow-up turns. It is optional because
-/// other providers (OpenAI-compatible APIs) do not produce it.
+/// other providers (OpenAI-compatible APIs) do not produce it. When present,
+/// `signature_origin` records the exact provider/style/model that emitted the
+/// enclosing assistant message so serializers can decide whether that opaque
+/// signature is safe to replay on a later turn.
pub const ThinkingBlock = struct {
text: TextualBlock = .empty,
signature: ?[]const u8 = null,
+ signature_origin: ?SignatureOrigin = null,
pub fn deinit(self: *ThinkingBlock, alloc: Allocator) void {
self.text.deinit(alloc);
if (self.signature) |sig| alloc.free(sig);
+ if (self.signature_origin) |*origin| origin.deinit(alloc);
}
};
@@ -222,6 +264,7 @@ pub const Message = struct {
block.deinit(alloc);
}
self.content.deinit(alloc);
+ if (self.metadata) |m| alloc.free(m);
}
};
@@ -335,6 +378,21 @@ pub const Conversation = struct {
}
};
+pub fn setThinkingOrigins(
+ allocator: Allocator,
+ blocks: []ContentBlock,
+ api_style: config.APIStyle,
+ base_url: []const u8,
+ model: []const u8,
+) !void {
+ for (blocks) |*block| {
+ if (block.* != .Thinking) continue;
+ const origin = try SignatureOrigin.init(allocator, api_style, base_url, model);
+ if (block.Thinking.signature_origin) |*old| old.deinit(allocator);
+ block.Thinking.signature_origin = origin;
+ }
+}
+
/// Derive the effective ordered list of system-text blocks from a slice of
/// messages. This is the single shared rule that governs both provider
/// serialization and session rebuild.