summaryrefslogtreecommitdiff
path: root/libpanto-c
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto-c')
-rw-r--r--libpanto-c/include/panto.h16
-rw-r--r--libpanto-c/src/lib.zig46
2 files changed, 58 insertions, 4 deletions
diff --git a/libpanto-c/include/panto.h b/libpanto-c/include/panto.h
index c43b39f..3b4a22a 100644
--- a/libpanto-c/include/panto.h
+++ b/libpanto-c/include/panto.h
@@ -139,13 +139,23 @@ PANTO_EXPORT void panto_message_builder_destroy(PantoMessageBuilder *builder);
PANTO_EXPORT PantoStatus panto_message_builder_add_text(PantoMessageBuilder *builder, const char *text);
/* `signature` may be NULL/empty when the provider emitted no signature, in
* which case the `signature_*` origin args are ignored. When a signature is
- * supplied, pass the origin (style/base_url/model) it was produced under so
- * the block can be replayed; an Anthropic request drops thinking blocks whose
- * signature origin does not match the request's base_url/model. */
+ * supplied you may pass the origin (style/base_url/model) it was produced
+ * under so the block can be replayed; an Anthropic request drops thinking
+ * blocks whose signature origin does not match the request's base_url/model.
+ * The origin args are OPTIONAL: pass PANTO_API_STYLE 0 and NULL base_url/model
+ * to omit them and instead rely on the enclosing message's identity (see
+ * `panto_message_builder_set_identity`), which is the preferred, per-message
+ * source of truth — no per-block identity copy required. */
PANTO_EXPORT PantoStatus panto_message_builder_add_thinking(PantoMessageBuilder *builder, const char *text, const char *signature, PantoAPIStyle signature_api_style, const char *signature_base_url, const char *signature_model);
PANTO_EXPORT PantoStatus panto_message_builder_add_tool_use(PantoMessageBuilder *builder, const char *id, const char *name, const char *input);
PANTO_EXPORT PantoStatus panto_message_builder_add_tool_result(PantoMessageBuilder *builder, const char *tool_use_id, const char *text, bool is_error);
PANTO_EXPORT PantoStatus panto_message_builder_add_system(PantoMessageBuilder *builder, const char *text, PantoSystemMode mode);
+/* Attach the wire identity (provider/style/model + reasoning) that produced
+ * this message. This is the per-message source of truth for replaying thinking
+ * signatures and is preserved through compaction, so a kept-verbatim turn is
+ * not re-stamped with the compaction model. Strings are copied. Optional; when
+ * omitted, thinking replay relies on each block's own signature origin. */
+PANTO_EXPORT PantoStatus panto_message_builder_set_identity(PantoMessageBuilder *builder, PantoAPIStyle api_style, const char *base_url, const char *model, PantoReasoningEffort reasoning);
/* Commit the builder's blocks as one message of the builder's role. Consumes
* (frees) the builder. `usage` applies to assistant turns; pass has_usage=false
* otherwise. */
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) };
}