diff options
Diffstat (limited to 'libpanto-c/src')
| -rw-r--r-- | libpanto-c/src/lib.zig | 46 |
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) }; } |
