summaryrefslogtreecommitdiff
path: root/libpanto-c/src/lib.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto-c/src/lib.zig')
-rw-r--r--libpanto-c/src/lib.zig295
1 files changed, 283 insertions, 12 deletions
diff --git a/libpanto-c/src/lib.zig b/libpanto-c/src/lib.zig
index 716e16b..7aa735f 100644
--- a/libpanto-c/src/lib.zig
+++ b/libpanto-c/src/lib.zig
@@ -11,6 +11,7 @@ const PantoStream = opaque {};
const PantoConversation = opaque {};
const PantoSession = opaque {};
const PantoSessionStore = opaque {};
+const PantoMessageBuilder = opaque {};
const AgentHandle = struct { agent: *panto.Agent, config: *panto.Config };
const ConversationHandle = struct { conversation: panto.Conversation };
@@ -20,6 +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 };
pub const PantoSlice = extern struct { ptr: ?[*]const u8, len: usize };
pub const PantoStatus = enum(c_int) { ok = 0, err = 1 };
@@ -54,7 +56,8 @@ pub const PantoProviderRetry = extern struct { attempt: usize, max_attempts: usi
pub const PantoToolDispatchStart = extern struct { count: usize };
pub const PantoToolDispatchComplete = extern struct { message_index: usize };
pub const PantoEvent = extern struct { tag: PantoEventTag, data: extern union { message_start: PantoMessageRole, block_start: PantoBlockStart, tool_details: PantoToolDetails, content_delta: PantoContentDelta, block_complete: PantoBlockComplete, message_complete: PantoMessageComplete, provider_retry: PantoProviderRetry, tool_dispatch_start: PantoToolDispatchStart, tool_dispatch_result: PantoToolDispatchComplete, tool_dispatch_complete: PantoToolDispatchComplete } };
-pub const PantoPersistentMessage = extern struct { role: PantoMessageRole, has_usage: bool, usage: PantoUsage, has_metadata: bool, metadata: PantoSlice };
+pub const PantoOwnedContentBlock = extern struct { tag: PantoContentBlockTag, text: PantoSlice, tool_id: PantoSlice, tool_name: PantoSlice, is_error: bool, system_mode: PantoSystemMode, thinking_signature: PantoSlice, thinking_signature_api_style: PantoAPIStyle, thinking_signature_base_url: PantoSlice, thinking_signature_model: PantoSlice };
+pub const PantoPersistentMessage = extern struct { role: PantoMessageRole, has_usage: bool, usage: PantoUsage, has_metadata: bool, metadata: PantoSlice, api_style: PantoAPIStyle, base_url: PantoSlice, model: PantoSlice, reasoning: PantoReasoningEffort, content: ?[*]const PantoOwnedContentBlock, content_len: usize };
pub const PantoSessionStoreVTable = extern struct {
create: *const fn (*anyopaque, **PantoSession) callconv(.c) PantoStatus,
list: *const fn (*anyopaque, *PantoSessionInfoList) callconv(.c) PantoStatus,
@@ -212,19 +215,32 @@ export fn panto_conversation_block_text(c: *PantoConversation, mi: usize, bi: us
}
export fn panto_conversation_block_tool_id(c: *PantoConversation, mi: usize, bi: usize) PantoSlice {
const b = blockAt(c, mi, bi) orelse return .{ .ptr = null, .len = 0 };
- return switch (b.*) { .ToolUse => |t| outSlice(t.id), .ToolResult => |t| outSlice(t.tool_use_id), else => .{ .ptr = null, .len = 0 } };
+ return switch (b.*) {
+ .ToolUse => |t| outSlice(t.id),
+ .ToolResult => |t| outSlice(t.tool_use_id),
+ else => .{ .ptr = null, .len = 0 },
+ };
}
export fn panto_conversation_block_tool_name(c: *PantoConversation, mi: usize, bi: usize) PantoSlice {
const b = blockAt(c, mi, bi) orelse return .{ .ptr = null, .len = 0 };
- return switch (b.*) { .ToolUse => |t| outSlice(t.name), else => .{ .ptr = null, .len = 0 } };
+ return switch (b.*) {
+ .ToolUse => |t| outSlice(t.name),
+ else => .{ .ptr = null, .len = 0 },
+ };
}
export fn panto_conversation_block_tool_result_is_error(c: *PantoConversation, mi: usize, bi: usize) bool {
const b = blockAt(c, mi, bi) orelse return false;
- return switch (b.*) { .ToolResult => |t| t.is_error, else => false };
+ return switch (b.*) {
+ .ToolResult => |t| t.is_error,
+ else => false,
+ };
}
export fn panto_conversation_block_system_mode(c: *PantoConversation, mi: usize, bi: usize) PantoSystemMode {
const b = blockAt(c, mi, bi) orelse return .append;
- return switch (b.*) { .System => |s| @enumFromInt(@intFromEnum(s.mode)), else => .append };
+ return switch (b.*) {
+ .System => |s| @enumFromInt(@intFromEnum(s.mode)),
+ else => .append,
+ };
}
export fn panto_conversation_add_system_message(c: *PantoConversation, text: ?[*:0]const u8) PantoStatus {
unwrapConversation(c).addSystemMessage(cstr(text)) catch |e| return setErr("{t}", .{e});
@@ -252,6 +268,104 @@ export fn panto_conversation_add_compaction_summary(c: *PantoConversation, text:
unwrapConversation(c).addCompactionSummary(cstr(text)) catch |e| return setErr("{t}", .{e});
return .ok;
}
+fn unwrapBuilder(b: *PantoMessageBuilder) *MessageBuilder {
+ return @ptrCast(@alignCast(b));
+}
+/// Take ownership of `block` into the builder, freeing it on OOM.
+fn pushBlock(b: *MessageBuilder, block: panto.ContentBlock) PantoStatus {
+ b.blocks.append(allocator, block) catch |e| {
+ var blk = block;
+ blk.deinit(allocator);
+ return setErr("{t}", .{e});
+ };
+ return .ok;
+}
+export fn panto_message_builder_create(role: PantoMessageRole) ?*PantoMessageBuilder {
+ const b = allocator.create(MessageBuilder) catch {
+ _ = setErr("OutOfMemory", .{});
+ return null;
+ };
+ b.* = .{ .role = @enumFromInt(@intFromEnum(role)) };
+ return @ptrCast(b);
+}
+export fn panto_message_builder_destroy(builder: ?*PantoMessageBuilder) void {
+ if (builder) |ptr| {
+ const b = unwrapBuilder(ptr);
+ for (b.blocks.items) |*blk| blk.deinit(allocator);
+ b.blocks.deinit(allocator);
+ allocator.destroy(b);
+ }
+}
+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});
+ return pushBlock(b, .{ .Text = tb });
+}
+export fn panto_message_builder_add_thinking(builder: *PantoMessageBuilder, text: ?[*:0]const u8, signature: ?[*:0]const u8, signature_api_style: PantoAPIStyle, signature_base_url: ?[*:0]const u8, signature_model: ?[*:0]const u8) PantoStatus {
+ const b = unwrapBuilder(builder);
+ var tb = panto.textualBlockFromSlice(allocator, cstr(text)) catch |e| return setErr("{t}", .{e});
+ const sig: ?[]const u8 = if (cstr(signature).len != 0) (allocator.dupe(u8, cstr(signature)) catch |e| {
+ tb.deinit(allocator);
+ return setErr("{t}", .{e});
+ }) else null;
+ var origin: ?panto.SignatureOrigin = null;
+ if (sig != null and cstr(signature_base_url).len != 0) {
+ origin = panto.SignatureOrigin.init(allocator, @enumFromInt(@intFromEnum(signature_api_style)), cstr(signature_base_url), cstr(signature_model)) catch |e| {
+ tb.deinit(allocator);
+ if (sig) |s| allocator.free(s);
+ return setErr("{t}", .{e});
+ };
+ }
+ return pushBlock(b, .{ .Thinking = .{ .text = tb, .signature = sig, .signature_origin = origin } });
+}
+export fn panto_message_builder_add_tool_use(builder: *PantoMessageBuilder, id: ?[*:0]const u8, name: ?[*:0]const u8, input: ?[*:0]const u8) PantoStatus {
+ const b = unwrapBuilder(builder);
+ const id_d = allocator.dupe(u8, cstr(id)) catch |e| return setErr("{t}", .{e});
+ const name_d = allocator.dupe(u8, cstr(name)) catch |e| {
+ allocator.free(id_d);
+ return setErr("{t}", .{e});
+ };
+ const input_tb = panto.textualBlockFromSlice(allocator, cstr(input)) catch |e| {
+ allocator.free(id_d);
+ allocator.free(name_d);
+ return setErr("{t}", .{e});
+ };
+ return pushBlock(b, .{ .ToolUse = .{ .id = id_d, .name = name_d, .input = input_tb } });
+}
+export fn panto_message_builder_add_tool_result(builder: *PantoMessageBuilder, tool_use_id: ?[*:0]const u8, text: ?[*:0]const u8, is_error: bool) PantoStatus {
+ const b = unwrapBuilder(builder);
+ const id_d = allocator.dupe(u8, cstr(tool_use_id)) catch |e| return setErr("{t}", .{e});
+ var tb = panto.textualBlockFromSlice(allocator, cstr(text)) catch |e| {
+ allocator.free(id_d);
+ return setErr("{t}", .{e});
+ };
+ var parts: std.ArrayList(panto.ResultPartStored) = .empty;
+ parts.append(allocator, .{ .text = tb }) catch |e| {
+ allocator.free(id_d);
+ tb.deinit(allocator);
+ return setErr("{t}", .{e});
+ };
+ return pushBlock(b, .{ .ToolResult = .{ .tool_use_id = id_d, .parts = parts, .is_error = is_error } });
+}
+export fn panto_message_builder_add_system(builder: *PantoMessageBuilder, text: ?[*:0]const u8, mode: PantoSystemMode) PantoStatus {
+ const b = unwrapBuilder(builder);
+ const tb = panto.textualBlockFromSlice(allocator, cstr(text)) catch |e| return setErr("{t}", .{e});
+ return pushBlock(b, .{ .System = .{ .text = tb, .mode = @enumFromInt(@intFromEnum(mode)) } });
+}
+export fn panto_conversation_add_message(c: *PantoConversation, builder: *PantoMessageBuilder, has_usage: bool, u: PantoUsage) PantoStatus {
+ const b = unwrapBuilder(builder);
+ const cc = unwrapConversation(c);
+ // Consume the builder regardless of outcome. addMessage takes ownership of
+ // each block's bytes on success, so we only ever free the list backing
+ // array + the builder shell here — never the per-block bytes (that would
+ // double-free what the conversation now owns).
+ defer {
+ b.blocks.deinit(allocator);
+ allocator.destroy(b);
+ }
+ cc.addMessage(b.role, b.blocks.items, if (has_usage) zigUsage(u) else null) catch |e| return setErr("{t}", .{e});
+ return .ok;
+}
export fn panto_session_create_null(out: **PantoSession) PantoStatus {
const h = allocator.create(SessionHandle) catch return setErr("OutOfMemory", .{});
var ns = panto.NullStore.init(allocator);
@@ -405,11 +519,56 @@ fn cStoreLoad(ctx: *anyopaque, id: []const u8) anyerror!?panto.Conversation {
panto_conversation_destroy(out);
return conv;
}
+const empty_slice: PantoSlice = .{ .ptr = null, .len = 0 };
+/// Flatten an in-memory content block into the borrow-only C view. Slices
+/// alias the message's buffers except for ToolResult text, which is
+/// concatenated into `aa` (valid until the caller frees that allocator).
+fn ownedBlock(aa: std.mem.Allocator, b: *const panto.ContentBlock) !PantoOwnedContentBlock {
+ var out: PantoOwnedContentBlock = .{ .tag = @enumFromInt(@intFromEnum(b.*)), .text = empty_slice, .tool_id = empty_slice, .tool_name = empty_slice, .is_error = false, .system_mode = .append, .thinking_signature = empty_slice, .thinking_signature_api_style = .openai_chat, .thinking_signature_base_url = empty_slice, .thinking_signature_model = empty_slice };
+ switch (b.*) {
+ .Text => |t| out.text = outSlice(t.items),
+ .Thinking => |t| {
+ out.text = outSlice(t.text.items);
+ if (t.signature) |s| out.thinking_signature = outSlice(s);
+ if (t.signature_origin) |o| {
+ out.thinking_signature_api_style = @enumFromInt(@intFromEnum(o.api_style));
+ out.thinking_signature_base_url = outSlice(o.base_url);
+ out.thinking_signature_model = outSlice(o.model);
+ }
+ },
+ .ToolUse => |t| {
+ out.text = outSlice(t.input.items);
+ out.tool_id = outSlice(t.id);
+ out.tool_name = outSlice(t.name);
+ },
+ .ToolResult => |tr| {
+ var buf: panto.TextualBlock = .empty;
+ try tr.appendTextInto(aa, &buf);
+ out.text = outSlice(buf.items);
+ out.tool_id = outSlice(tr.tool_use_id);
+ out.is_error = tr.is_error;
+ },
+ .System => |s| {
+ out.text = outSlice(s.text.items);
+ out.system_mode = @enumFromInt(@intFromEnum(s.mode));
+ },
+ .CompactionSummary => |s| out.text = outSlice(s.text.items),
+ }
+ return out;
+}
fn cStoreAppend(ctx: *anyopaque, session_id: []const u8, messages: []panto.PersistentMessage) anyerror!void {
const h: *StoreHandle = @ptrCast(@alignCast(ctx));
const cmsgs = try allocator.alloc(PantoPersistentMessage, messages.len);
defer allocator.free(cmsgs);
- for (messages, 0..) |m, i| cmsgs[i] = .{ .role = @enumFromInt(@intFromEnum(m.message.role)), .has_usage = m.usage != null, .usage = if (m.usage) |u| usage(u) else .{ .input = 0, .output = 0, .cache_read = 0, .cache_write = 0, .reasoning = 0 }, .has_metadata = m.message.metadata != null, .metadata = outSlice(m.message.metadata orelse "") };
+ var arena = std.heap.ArenaAllocator.init(allocator);
+ defer arena.deinit();
+ const aa = arena.allocator();
+ for (messages, 0..) |m, i| {
+ const blocks = m.message.content.items;
+ const cblocks = try aa.alloc(PantoOwnedContentBlock, blocks.len);
+ for (blocks, 0..) |*b, j| cblocks[j] = try ownedBlock(aa, b);
+ cmsgs[i] = .{ .role = @enumFromInt(@intFromEnum(m.message.role)), .has_usage = m.usage != null, .usage = if (m.usage) |u| usage(u) else .{ .input = 0, .output = 0, .cache_read = 0, .cache_write = 0, .reasoning = 0 }, .has_metadata = m.message.metadata != null, .metadata = outSlice(m.message.metadata orelse ""), .api_style = @enumFromInt(@intFromEnum(m.identity.api_style)), .base_url = outSlice(m.identity.base_url), .model = outSlice(m.identity.model), .reasoning = @enumFromInt(@intFromEnum(m.identity.reasoning)), .content = if (cblocks.len == 0) null else cblocks.ptr, .content_len = cblocks.len };
+ }
if (h.vtable.append_messages(h.ctx, outSlice(session_id), cmsgs.ptr, cmsgs.len) != .ok) return error.CStoreAppendFailed;
}
fn cStoreListZ(ctx: *anyopaque) anyerror![]panto.SessionInfo {
@@ -491,11 +650,38 @@ fn builtinStoreLoad(ctx: *anyopaque, id: PantoSlice, out: **PantoConversation) c
out.* = @ptrCast(ch);
return .ok;
}
+/// Rebuild an in-memory content block from its borrow-only C view. The
+/// inverse of `ownedBlock`; allocates into `aa` (a transient arena freed by
+/// the caller after the store has copied what it keeps).
+fn zigBlock(aa: std.mem.Allocator, ob: PantoOwnedContentBlock) !panto.ContentBlock {
+ return switch (ob.tag) {
+ .Text => .{ .Text = try panto.textualBlockFromSlice(aa, slice(ob.text)) },
+ .Thinking => .{ .Thinking = .{ .text = try panto.textualBlockFromSlice(aa, slice(ob.text)), .signature = if (ob.thinking_signature.len != 0) try aa.dupe(u8, slice(ob.thinking_signature)) else null, .signature_origin = if (ob.thinking_signature.len != 0 and ob.thinking_signature_base_url.len != 0) try panto.SignatureOrigin.init(aa, @enumFromInt(@intFromEnum(ob.thinking_signature_api_style)), slice(ob.thinking_signature_base_url), slice(ob.thinking_signature_model)) else null } },
+ .ToolUse => .{ .ToolUse = .{ .id = try aa.dupe(u8, slice(ob.tool_id)), .name = try aa.dupe(u8, slice(ob.tool_name)), .input = try panto.textualBlockFromSlice(aa, slice(ob.text)) } },
+ .ToolResult => blk: {
+ var parts: std.ArrayList(panto.ResultPartStored) = .empty;
+ try parts.append(aa, .{ .text = try panto.textualBlockFromSlice(aa, slice(ob.text)) });
+ break :blk .{ .ToolResult = .{ .tool_use_id = try aa.dupe(u8, slice(ob.tool_id)), .parts = parts, .is_error = ob.is_error } };
+ },
+ .System => .{ .System = .{ .text = try panto.textualBlockFromSlice(aa, slice(ob.text)), .mode = @enumFromInt(@intFromEnum(ob.system_mode)) } },
+ .CompactionSummary => .{ .CompactionSummary = .{ .text = try panto.textualBlockFromSlice(aa, slice(ob.text)) } },
+ };
+}
fn builtinStoreAppend(ctx: *anyopaque, session_id: PantoSlice, messages: [*]const PantoPersistentMessage, len: usize) callconv(.c) PantoStatus {
const s: *panto.SessionStore = @ptrCast(@alignCast(ctx));
const pmsgs = allocator.alloc(panto.PersistentMessage, len) catch return setErr("OutOfMemory", .{});
defer allocator.free(pmsgs);
- for (messages[0..len], 0..) |m, i| pmsgs[i] = .{ .message = .{ .role = @enumFromInt(@intFromEnum(m.role)), .content = .empty, .metadata = if (m.has_metadata) slice(m.metadata) else null }, .usage = if (m.has_usage) zigUsage(m.usage) else null, .identity = .{ .api_style = .openai_chat, .base_url = "", .model = "" } };
+ var arena = std.heap.ArenaAllocator.init(allocator);
+ defer arena.deinit();
+ const aa = arena.allocator();
+ for (messages[0..len], 0..) |m, i| {
+ var content: std.ArrayList(panto.ContentBlock) = .empty;
+ if (m.content) |cptr| {
+ content.ensureTotalCapacity(aa, m.content_len) catch return setErr("OutOfMemory", .{});
+ for (cptr[0..m.content_len]) |ob| content.appendAssumeCapacity(zigBlock(aa, ob) catch |e| return setErr("{t}", .{e}));
+ }
+ pmsgs[i] = .{ .message = .{ .role = @enumFromInt(@intFromEnum(m.role)), .content = content, .metadata = if (m.has_metadata) slice(m.metadata) else null }, .usage = if (m.has_usage) zigUsage(m.usage) else null, .identity = .{ .api_style = @enumFromInt(@intFromEnum(m.api_style)), .base_url = slice(m.base_url), .model = slice(m.model), .reasoning = @enumFromInt(@intFromEnum(m.reasoning)) } };
+ }
s.appendMessages(slice(session_id), pmsgs) catch |e| return setErr("{t}", .{e});
return .ok;
}
@@ -664,6 +850,76 @@ test "c abi exports compile" {
try std.testing.expect(true);
}
+var test_append_ok: bool = false;
+fn testCaptureAppend(_: *anyopaque, _: PantoSlice, messages: [*]const PantoPersistentMessage, len: usize) callconv(.c) PantoStatus {
+ test_append_ok = blk: {
+ if (len != 1) break :blk false;
+ const m = messages[0];
+ if (m.role != .assistant) break :blk false;
+ const p = m.content orelse break :blk false;
+ if (m.content_len != 3) break :blk false;
+ const b = p[0..3];
+ if (b[0].tag != .ToolUse) break :blk false;
+ if (!std.mem.eql(u8, slice(b[0].tool_id), "call-1")) break :blk false;
+ if (!std.mem.eql(u8, slice(b[0].tool_name), "search")) break :blk false;
+ if (!std.mem.eql(u8, slice(b[0].text), "{\"q\":1}")) break :blk false;
+ if (b[1].tag != .Thinking) break :blk false;
+ if (!std.mem.eql(u8, slice(b[1].text), "hmm")) break :blk false;
+ if (!std.mem.eql(u8, slice(b[1].thinking_signature), "sig")) break :blk false;
+ if (b[1].thinking_signature_api_style != .anthropic_messages) break :blk false;
+ if (!std.mem.eql(u8, slice(b[1].thinking_signature_base_url), "https://api")) break :blk false;
+ if (!std.mem.eql(u8, slice(b[1].thinking_signature_model), "claude")) break :blk false;
+ if (b[2].tag != .ToolResult) break :blk false;
+ if (!b[2].is_error) break :blk false;
+ if (!std.mem.eql(u8, slice(b[2].tool_id), "call-1")) break :blk false;
+ if (!std.mem.eql(u8, slice(b[2].text), "boom")) break :blk false;
+ if (m.api_style != .anthropic_messages) break :blk false;
+ if (!std.mem.eql(u8, slice(m.base_url), "https://api")) break :blk false;
+ if (!std.mem.eql(u8, slice(m.model), "claude")) break :blk false;
+ if (m.reasoning != .high) break :blk false;
+ break :blk true;
+ };
+ return .ok;
+}
+
+test "cStoreAppend marshals content blocks (incl. thinking origin)" {
+ var content: std.ArrayList(panto.ContentBlock) = .empty;
+ defer {
+ for (content.items) |*b| b.deinit(allocator);
+ content.deinit(allocator);
+ }
+ try content.append(allocator, .{ .ToolUse = .{ .id = try allocator.dupe(u8, "call-1"), .name = try allocator.dupe(u8, "search"), .input = try panto.textualBlockFromSlice(allocator, "{\"q\":1}") } });
+ try content.append(allocator, .{ .Thinking = .{ .text = try panto.textualBlockFromSlice(allocator, "hmm"), .signature = try allocator.dupe(u8, "sig"), .signature_origin = try panto.SignatureOrigin.init(allocator, .anthropic_messages, "https://api", "claude") } });
+ var parts: std.ArrayList(panto.ResultPartStored) = .empty;
+ try parts.append(allocator, .{ .text = try panto.textualBlockFromSlice(allocator, "boom") });
+ try content.append(allocator, .{ .ToolResult = .{ .tool_use_id = try allocator.dupe(u8, "call-1"), .parts = parts, .is_error = true } });
+
+ var msgs = [_]panto.PersistentMessage{.{ .message = .{ .role = .assistant, .content = content }, .identity = .{ .api_style = .anthropic_messages, .base_url = "https://api", .model = "claude", .reasoning = .high } }};
+
+ var vt: PantoSessionStoreVTable = .{ .create = builtinStoreCreate, .list = builtinStoreList, .free_session_infos = builtinStoreFreeInfos, .resolve = builtinStoreResolve, .latest = builtinStoreLatest, .load = builtinStoreLoad, .append_messages = testCaptureAppend, .destroy = null };
+ var dummy: u8 = 0;
+ var handle: StoreHandle = .{ .ctx = &dummy, .vtable = &vt };
+
+ test_append_ok = false;
+ try cStoreAppend(@ptrCast(&handle), "sess-1", &msgs);
+ try std.testing.expect(test_append_ok);
+}
+
+test "zigBlock round-trips an owned thinking block" {
+ var arena = std.heap.ArenaAllocator.init(allocator);
+ defer arena.deinit();
+ const aa = arena.allocator();
+ const ob: PantoOwnedContentBlock = .{ .tag = .Thinking, .text = outSlice("reasoning"), .tool_id = empty_slice, .tool_name = empty_slice, .is_error = false, .system_mode = .append, .thinking_signature = outSlice("sig"), .thinking_signature_api_style = .anthropic_messages, .thinking_signature_base_url = outSlice("https://api"), .thinking_signature_model = outSlice("claude") };
+ const block = try zigBlock(aa, ob);
+ try std.testing.expect(block == .Thinking);
+ try std.testing.expectEqualStrings("reasoning", block.Thinking.text.items);
+ try std.testing.expectEqualStrings("sig", block.Thinking.signature.?);
+ const origin = block.Thinking.signature_origin.?;
+ try std.testing.expect(origin.api_style == .anthropic_messages);
+ try std.testing.expectEqualStrings("https://api", origin.base_url);
+ try std.testing.expectEqualStrings("claude", origin.model);
+}
+
fn cloneToolDecl(d: PantoToolDecl) !panto.ToolDecl {
return .{ .name = try dupSlice(d.name), .description = try dupSlice(d.description), .schema_json = try dupSlice(d.schema_json) };
}
@@ -725,17 +981,29 @@ fn convertResultParts(parts: PantoResultParts) !panto.ResultParts {
export fn panto_agent_register_tool(a: *PantoAgent, decl: PantoToolDecl, ctx: *anyopaque, invoke: PantoToolInvokeFn, destroy: ?PantoToolDestroyFn) PantoStatus {
const ah: *AgentHandle = @ptrCast(@alignCast(a));
const h = allocator.create(ToolHandle) catch return setErr("OutOfMemory", .{});
- h.* = .{ .ctx = ctx, .invoke = invoke, .destroy = destroy, .decl = cloneToolDecl(decl) catch |e| { allocator.destroy(h); return setErr("{t}", .{e}); } };
+ h.* = .{ .ctx = ctx, .invoke = invoke, .destroy = destroy, .decl = cloneToolDecl(decl) catch |e| {
+ allocator.destroy(h);
+ return setErr("{t}", .{e});
+ } };
ah.agent.registerTool(.{ .decl = h.decl, .ctx = h, .vtable = &.{ .invoke = toolInvoke, .deinit = toolDeinit } }) catch |e| return setErr("{t}", .{e});
return .ok;
}
export fn panto_agent_register_tool_source(a: *PantoAgent, name: PantoSlice, decls: [*]const PantoToolDecl, len: usize, ctx: *anyopaque, invoke_batch: PantoToolSourceInvokeBatchFn, destroy: ?PantoToolDestroyFn) PantoStatus {
const ah: *AgentHandle = @ptrCast(@alignCast(a));
const h = allocator.create(ToolSourceHandle) catch return setErr("OutOfMemory", .{});
- h.name = dupSlice(name) catch |e| { allocator.destroy(h); return setErr("{t}", .{e}); };
- h.decls = allocator.alloc(panto.ToolDecl, len) catch |e| { allocator.free(h.name); allocator.destroy(h); return setErr("{t}", .{e}); };
+ h.name = dupSlice(name) catch |e| {
+ allocator.destroy(h);
+ return setErr("{t}", .{e});
+ };
+ h.decls = allocator.alloc(panto.ToolDecl, len) catch |e| {
+ allocator.free(h.name);
+ allocator.destroy(h);
+ return setErr("{t}", .{e});
+ };
for (decls[0..len], 0..) |d, i| h.decls[i] = cloneToolDecl(d) catch |e| return setErr("{t}", .{e});
- h.ctx = ctx; h.invoke_batch = invoke_batch; h.destroy = destroy;
+ h.ctx = ctx;
+ h.invoke_batch = invoke_batch;
+ h.destroy = destroy;
ah.agent.registerToolSource(.{ .name = h.name, .tools = h.decls, .ctx = h, .vtable = &.{ .invoke_batch = sourceInvokeBatch, .deinit = sourceDeinit } }) catch |e| return setErr("{t}", .{e});
return .ok;
}
@@ -748,7 +1016,10 @@ export fn panto_result_parts_destroy(parts: PantoResultParts) void {
if (parts.ptr) |p| {
for (p[0..parts.len]) |part| switch (part.tag) {
.text => freePS(part.data.text),
- .media => { if (part.data.media.has_media_type) freePS(part.data.media.media_type); freePS(part.data.media.data); },
+ .media => {
+ if (part.data.media.has_media_type) freePS(part.data.media.media_type);
+ freePS(part.data.media.data);
+ },
};
allocator.free(p[0..parts.len]);
}