summaryrefslogtreecommitdiff
path: root/libpanto-c/src
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-18 14:15:55 -0600
committert <t@tjp.lol>2026-06-18 14:16:24 -0600
commit270cd00129551647e7c764a13836e03e0b2dc4e2 (patch)
treef939bbf999dd67d996b4025b03dbfe4abd30b9f4 /libpanto-c/src
parent7c2d8825660f73198149c0b2ce26166b16ba3532 (diff)
preserve signature origins across compactions
Diffstat (limited to 'libpanto-c/src')
-rw-r--r--libpanto-c/src/lib.zig46
1 files changed, 45 insertions, 1 deletions
diff --git a/libpanto-c/src/lib.zig b/libpanto-c/src/lib.zig
index 7aa735f..9d584a0 100644
--- a/libpanto-c/src/lib.zig
+++ b/libpanto-c/src/lib.zig
@@ -21,7 +21,7 @@ const NullStoreHandle = struct { store: panto.NullStore };
const FileSystemJSONLStoreHandle = struct { store: panto.FileSystemJSONLStore };
const ToolHandle = struct { ctx: *anyopaque, invoke: PantoToolInvokeFn, destroy: ?PantoToolDestroyFn, decl: panto.ToolDecl };
const ToolSourceHandle = struct { ctx: *anyopaque, invoke_batch: PantoToolSourceInvokeBatchFn, destroy: ?PantoToolDestroyFn, name: []u8, decls: []panto.ToolDecl };
-const MessageBuilder = struct { role: panto.MessageRole, blocks: std.ArrayList(panto.ContentBlock) = .empty };
+const MessageBuilder = struct { role: panto.MessageRole, blocks: std.ArrayList(panto.ContentBlock) = .empty, identity: ?panto.WireIdentity = null };
pub const PantoSlice = extern struct { ptr: ?[*]const u8, len: usize };
pub const PantoStatus = enum(c_int) { ok = 0, err = 1 };
@@ -293,9 +293,26 @@ export fn panto_message_builder_destroy(builder: ?*PantoMessageBuilder) void {
const b = unwrapBuilder(ptr);
for (b.blocks.items) |*blk| blk.deinit(allocator);
b.blocks.deinit(allocator);
+ if (b.identity) |id| panto.freeWireIdentity(allocator, id);
allocator.destroy(b);
}
}
+/// Attach the per-message wire identity that produced this message. Used when
+/// rebuilding a conversation for resumption so the identity survives a later
+/// compaction (and so a thinking signature replays to the right model without
+/// a per-block identity copy). The strings are copied.
+export fn panto_message_builder_set_identity(builder: *PantoMessageBuilder, api_style: PantoAPIStyle, base_url: ?[*:0]const u8, model: ?[*:0]const u8, reasoning: PantoReasoningEffort) PantoStatus {
+ const b = unwrapBuilder(builder);
+ const id = panto.dupeWireIdentity(allocator, .{
+ .api_style = @enumFromInt(@intFromEnum(api_style)),
+ .base_url = cstr(base_url),
+ .model = cstr(model),
+ .reasoning = @enumFromInt(@intFromEnum(reasoning)),
+ }) catch |e| return setErr("{t}", .{e});
+ if (b.identity) |old| panto.freeWireIdentity(allocator, old);
+ b.identity = id;
+ return .ok;
+}
export fn panto_message_builder_add_text(builder: *PantoMessageBuilder, text: ?[*:0]const u8) PantoStatus {
const b = unwrapBuilder(builder);
const tb = panto.textualBlockFromSlice(allocator, cstr(text)) catch |e| return setErr("{t}", .{e});
@@ -361,9 +378,16 @@ export fn panto_conversation_add_message(c: *PantoConversation, builder: *PantoM
// double-free what the conversation now owns).
defer {
b.blocks.deinit(allocator);
+ if (b.identity) |id| panto.freeWireIdentity(allocator, id);
allocator.destroy(b);
}
cc.addMessage(b.role, b.blocks.items, if (has_usage) zigUsage(u) else null) catch |e| return setErr("{t}", .{e});
+ // Transfer the builder's identity onto the freshly appended message. The
+ // `defer` frees the builder's copy; null it out so ownership moves cleanly.
+ if (b.identity) |id| {
+ cc.messages.items[cc.messages.items.len - 1].identity = id;
+ b.identity = null;
+ }
return .ok;
}
export fn panto_session_create_null(out: **PantoSession) PantoStatus {
@@ -920,6 +944,26 @@ test "zigBlock round-trips an owned thinking block" {
try std.testing.expectEqualStrings("claude", origin.model);
}
+test "message builder set_identity attaches per-message identity" {
+ var conv: *PantoConversation = undefined;
+ try std.testing.expectEqual(PantoStatus.ok, panto_conversation_create(&conv));
+ defer panto_conversation_destroy(conv);
+
+ const b = panto_message_builder_create(.assistant).?;
+ try std.testing.expectEqual(PantoStatus.ok, panto_message_builder_add_thinking(b, "reasoned", "sig", .openai_chat, null, null));
+ try std.testing.expectEqual(PantoStatus.ok, panto_message_builder_set_identity(b, .anthropic_messages, "https://api.anthropic.com", "claude", .default));
+ try std.testing.expectEqual(PantoStatus.ok, panto_conversation_add_message(conv, b, false, undefined));
+
+ const cc = unwrapConversation(conv);
+ const id = cc.messages.items[0].identity.?;
+ try std.testing.expect(id.api_style == .anthropic_messages);
+ try std.testing.expectEqualStrings("https://api.anthropic.com", id.base_url);
+ try std.testing.expectEqualStrings("claude", id.model);
+ // No per-block origin was supplied; the message identity carries replay
+ // provenance on its own.
+ try std.testing.expect(cc.messages.items[0].content.items[0].Thinking.signature_origin == null);
+}
+
fn cloneToolDecl(d: PantoToolDecl) !panto.ToolDecl {
return .{ .name = try dupSlice(d.name), .description = try dupSlice(d.description), .schema_json = try dupSlice(d.schema_json) };
}