const std = @import("std"); const allocator = std.heap.c_allocator; const panto = @import("panto"); var threaded: ?std.Io.Threaded = null; threadlocal var last_error: ?[]u8 = null; const PantoAgent = opaque {}; const PantoStream = opaque {}; const PantoConversation = opaque {}; const PantoSession = opaque {}; const PantoSessionStore = opaque {}; const AgentHandle = struct { agent: *panto.Agent, config: *panto.Config }; const ConversationHandle = struct { conversation: panto.Conversation }; const SessionHandle = struct { session: panto.Session, owns_info: bool = true }; const StoreHandle = struct { ctx: *anyopaque, vtable: *const PantoSessionStoreVTable }; 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 }; pub const PantoSlice = extern struct { ptr: ?[*]const u8, len: usize }; pub const PantoStatus = enum(c_int) { ok = 0, err = 1 }; pub const PantoNextStatus = enum(c_int) { event = 0, done = 1, err = 2 }; pub const PantoAPIStyle = enum(c_int) { openai_chat = 0, anthropic_messages = 1 }; pub const PantoReasoningEffort = enum(c_int) { default = 0, off, minimal, low, medium, high }; pub const PantoThinking = enum(c_int) { disabled = 0, enabled, adaptive }; pub const PantoEffort = enum(c_int) { low = 0, medium, high, xhigh, max }; pub const PantoMessageRole = enum(c_int) { system = 0, user, assistant }; pub const PantoContentBlockTag = enum(c_int) { Text = 0, Thinking, ToolUse, ToolResult, System, CompactionSummary }; pub const PantoSystemMode = enum(c_int) { append = 0, replace }; pub const PantoEventTag = enum(c_int) { message_start = 0, block_start, tool_details, content_delta, block_complete, message_complete, provider_retry, tool_dispatch_start, tool_dispatch_complete, turn_complete }; pub const PantoUsage = extern struct { input: u64, output: u64, cache_read: u64, cache_write: u64, reasoning: u64 }; pub const PantoSessionInfo = extern struct { id: PantoSlice, created: PantoSlice, modified: PantoSlice, last_user_message: PantoSlice, base_url: PantoSlice, model: PantoSlice, message_count: usize, api_style: PantoAPIStyle, reasoning: PantoReasoningEffort }; pub const PantoSessionInfoList = extern struct { ptr: ?[*]PantoSessionInfo, len: usize }; pub const PantoSessionResult = extern struct { found: bool, session: ?*PantoSession }; pub const PantoOpenAIChatConfig = extern struct { api_key: ?[*:0]const u8, base_url: ?[*:0]const u8, model: ?[*:0]const u8, reasoning: PantoReasoningEffort, max_tokens: u32 }; pub const PantoAnthropicMessagesConfig = extern struct { api_key: ?[*:0]const u8, base_url: ?[*:0]const u8, model: ?[*:0]const u8, api_version: ?[*:0]const u8, max_tokens: u32, thinking: PantoThinking, effort: PantoEffort, has_thinking_budget_tokens: bool, thinking_budget_tokens: u32, thinking_interleaved: bool }; pub const PantoProviderConfig = extern struct { tag: PantoAPIStyle, data: extern union { openai_chat: PantoOpenAIChatConfig, anthropic_messages: PantoAnthropicMessagesConfig } }; pub const PantoCompactionConfig = extern struct { keep_verbatim: u32, has_model: bool, model: PantoProviderConfig, has_compaction_prompt: bool, compaction_prompt: ?[*:0]const u8 }; pub const PantoRetryConfig = extern struct { max_attempts: usize, initial_delay_ms: u64, max_delay_ms: u64, multiplier: f64, jitter: bool }; pub const PantoConfig = extern struct { provider: PantoProviderConfig, compaction: PantoCompactionConfig, retry: PantoRetryConfig }; pub const PantoBlockStart = extern struct { block_type: PantoContentBlockTag, index: usize }; pub const PantoToolDetails = extern struct { index: usize, id: PantoSlice, name: PantoSlice }; pub const PantoContentDelta = extern struct { index: usize, delta: PantoSlice }; pub const PantoBlockComplete = extern struct { index: usize, block_type: PantoContentBlockTag }; pub const PantoMessageComplete = extern struct { has_usage: bool, usage: PantoUsage }; pub const PantoCompactionResult = extern struct { compacted: bool, kept_turns: usize, summarized_messages: usize }; pub const PantoProviderRetry = extern struct { attempt: usize, max_attempts: usize, delay_ms: u64, error_name: PantoSlice, has_status_code: bool, status_code: u16, has_retry_after_ms: bool, retry_after_ms: u64, message: PantoSlice, compaction: bool }; 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_complete: PantoToolDispatchComplete } }; pub const PantoPersistentMessage = extern struct { role: PantoMessageRole, has_usage: bool, usage: PantoUsage, has_metadata: bool, metadata: PantoSlice }; pub const PantoSessionStoreVTable = extern struct { create: *const fn (*anyopaque, **PantoSession) callconv(.c) PantoStatus, list: *const fn (*anyopaque, *PantoSessionInfoList) callconv(.c) PantoStatus, free_session_infos: *const fn (*anyopaque, PantoSessionInfoList) callconv(.c) void, resolve: *const fn (*anyopaque, PantoSlice, *PantoSessionResult) callconv(.c) PantoStatus, latest: *const fn (*anyopaque, *PantoSessionResult) callconv(.c) PantoStatus, load: *const fn (*anyopaque, PantoSlice, **PantoConversation) callconv(.c) PantoStatus, append_messages: *const fn (*anyopaque, PantoSlice, [*]const PantoPersistentMessage, usize) callconv(.c) PantoStatus, destroy: ?*const fn (*anyopaque) callconv(.c) void, }; pub const PantoToolDecl = extern struct { name: PantoSlice, description: PantoSlice, schema_json: PantoSlice }; pub const PantoMediaPart = extern struct { media_type: PantoSlice, has_media_type: bool, data: PantoSlice }; pub const PantoResultPartTag = enum(c_int) { text = 0, media = 1 }; pub const PantoResultPart = extern struct { tag: PantoResultPartTag, data: extern union { text: PantoSlice, media: PantoMediaPart } }; pub const PantoResultParts = extern struct { ptr: ?[*]PantoResultPart, len: usize }; pub const PantoToolCall = extern struct { tool_name: PantoSlice, input: PantoSlice }; pub const PantoToolCallResult = extern struct { ok: bool, parts: PantoResultParts, err: PantoSlice }; pub const PantoToolInvokeFn = *const fn (*anyopaque, PantoSlice, *PantoResultParts) callconv(.c) PantoStatus; pub const PantoToolSourceInvokeBatchFn = *const fn (*anyopaque, [*]const PantoToolCall, [*]PantoToolCallResult, usize) callconv(.c) PantoStatus; pub const PantoToolDestroyFn = *const fn (*anyopaque) callconv(.c) void; fn setErr(comptime fmt: []const u8, args: anytype) PantoStatus { setErrVoid(fmt, args); return .err; } fn setErrNext(comptime fmt: []const u8, args: anytype) PantoNextStatus { setErrVoid(fmt, args); return .err; } fn setErrVoid(comptime fmt: []const u8, args: anytype) void { if (last_error) |e| allocator.free(e); last_error = std.fmt.allocPrint(allocator, fmt, args) catch null; } fn slice(s: PantoSlice) []const u8 { return if (s.ptr) |p| p[0..s.len] else &.{}; } fn outSlice(s: []const u8) PantoSlice { return .{ .ptr = s.ptr, .len = s.len }; } fn dupSlice(s: PantoSlice) ![]u8 { return allocator.dupe(u8, slice(s)); } fn cstr(s: ?[*:0]const u8) []const u8 { return if (s) |p| std.mem.span(p) else &.{}; } fn dupCStr(s: ?[*:0]const u8) ![]u8 { return allocator.dupe(u8, cstr(s)); } fn dupOut(s: []const u8) !PantoSlice { return outSlice(try allocator.dupe(u8, s)); } fn cloneSessionInfo(info: PantoSessionInfo) !panto.SessionInfo { return .{ .id = try dupSlice(info.id), .created = try dupSlice(info.created), .modified = try dupSlice(info.modified), .message_count = info.message_count, .last_user_message = try dupSlice(info.last_user_message), .api_style = @enumFromInt(@intFromEnum(info.api_style)), .base_url = try dupSlice(info.base_url), .model = try dupSlice(info.model), .reasoning = @enumFromInt(@intFromEnum(info.reasoning)) }; } fn marshalSessionInfo(info: panto.SessionInfo) !PantoSessionInfo { return .{ .id = try dupOut(info.id), .created = try dupOut(info.created), .modified = try dupOut(info.modified), .last_user_message = try dupOut(info.last_user_message), .base_url = try dupOut(info.base_url), .model = try dupOut(info.model), .message_count = info.message_count, .api_style = @enumFromInt(@intFromEnum(info.api_style)), .reasoning = @enumFromInt(@intFromEnum(info.reasoning)) }; } fn freePantoSessionInfo(info: PantoSessionInfo) void { freePS(info.id); freePS(info.created); freePS(info.modified); freePS(info.last_user_message); freePS(info.base_url); freePS(info.model); } fn wrapSession(session: panto.Session, owns_info: bool) !*PantoSession { const h = try allocator.create(SessionHandle); h.* = .{ .session = session, .owns_info = owns_info }; return @ptrCast(h); } fn unwrapStore(store: *PantoSessionStore) panto.SessionStore { return .{ .ptr = store, .vtable = &c_store_vtable }; } export fn panto_init() void { if (threaded == null) { threaded = std.Io.Threaded.init(allocator, .{}); panto.init(allocator, threaded.?.io()); } } export fn panto_deinit() void { panto.deinit(); if (threaded) |*t| t.deinit(); threaded = null; } export fn panto_last_error() PantoSlice { return if (last_error) |e| outSlice(e) else .{ .ptr = null, .len = 0 }; } export fn panto_error_free(s: PantoSlice) void { if (s.ptr) |p| allocator.free(@constCast(p[0..s.len])); } export fn panto_conversation_create(out: **PantoConversation) PantoStatus { const h = allocator.create(ConversationHandle) catch return setErr("OutOfMemory", .{}); h.* = .{ .conversation = panto.Conversation.init(allocator) }; out.* = @ptrCast(h); return .ok; } export fn panto_conversation_destroy(c: ?*PantoConversation) void { if (c) |ptr| { const h: *ConversationHandle = @ptrCast(@alignCast(ptr)); h.conversation.deinit(); allocator.destroy(h); } } fn unwrapConversation(c: *PantoConversation) *panto.Conversation { return &(@as(*ConversationHandle, @ptrCast(@alignCast(c))).conversation); } fn msgAt(c: *PantoConversation, index: usize) ?*panto.Message { const cc = unwrapConversation(c); if (index >= cc.messages.items.len) return null; return &cc.messages.items[index]; } fn blockAt(c: *PantoConversation, mi: usize, bi: usize) ?*panto.ContentBlock { const m = msgAt(c, mi) orelse return null; if (bi >= m.content.items.len) return null; return &m.content.items[bi]; } export fn panto_conversation_message_count(c: *PantoConversation) usize { return unwrapConversation(c).messages.items.len; } export fn panto_conversation_message_role(c: *PantoConversation, index: usize) PantoMessageRole { const m = msgAt(c, index) orelse return .user; return @enumFromInt(@intFromEnum(m.role)); } export fn panto_conversation_message_has_usage(c: *PantoConversation, index: usize) bool { const m = msgAt(c, index) orelse return false; return m.usage != null; } export fn panto_conversation_message_usage(c: *PantoConversation, index: usize) PantoUsage { const m = msgAt(c, index) orelse return .{ .input = 0, .output = 0, .cache_read = 0, .cache_write = 0, .reasoning = 0 }; return if (m.usage) |u| usage(u) else .{ .input = 0, .output = 0, .cache_read = 0, .cache_write = 0, .reasoning = 0 }; } export fn panto_conversation_message_block_count(c: *PantoConversation, index: usize) usize { const m = msgAt(c, index) orelse return 0; return m.content.items.len; } export fn panto_conversation_block_tag(c: *PantoConversation, mi: usize, bi: usize) PantoContentBlockTag { const b = blockAt(c, mi, bi) orelse return .Text; return @enumFromInt(@intFromEnum(b.*)); } export fn panto_conversation_block_text(c: *PantoConversation, mi: usize, bi: usize) PantoSlice { const b = blockAt(c, mi, bi) orelse return .{ .ptr = null, .len = 0 }; return switch (b.*) { .Text => |t| outSlice(t.items), .Thinking => |t| outSlice(t.text.items), .ToolUse => |t| outSlice(t.input.items), .ToolResult => |tr| blk: { for (tr.parts.items) |p| if (p == .text) break :blk outSlice(p.text.items); break :blk .{ .ptr = null, .len = 0 }; }, .System => |s| outSlice(s.text.items), .CompactionSummary => |s| outSlice(s.text.items), }; } 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 } }; } 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 } }; } 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 }; } 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 }; } 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}); return .ok; } export fn panto_conversation_replace_system_message(c: *PantoConversation, text: ?[*:0]const u8) PantoStatus { unwrapConversation(c).replaceSystemMessage(cstr(text)) catch |e| return setErr("{t}", .{e}); return .ok; } export fn panto_conversation_add_user_text(c: *PantoConversation, text: ?[*:0]const u8) PantoStatus { const cc = unwrapConversation(c); const tb = panto.textualBlockFromSlice(cc.allocator, cstr(text)) catch |e| return setErr("{t}", .{e}); const b: panto.ContentBlock = .{ .Text = tb }; cc.addUserMessage(&.{b}) catch |e| return setErr("{t}", .{e}); return .ok; } export fn panto_conversation_add_assistant_text(c: *PantoConversation, text: ?[*:0]const u8, has_usage: bool, u: PantoUsage) PantoStatus { const cc = unwrapConversation(c); const tb = panto.textualBlockFromSlice(cc.allocator, cstr(text)) catch |e| return setErr("{t}", .{e}); const b: panto.ContentBlock = .{ .Text = tb }; cc.addAssistantMessage(&.{b}, if (has_usage) zigUsage(u) else null) catch |e| return setErr("{t}", .{e}); return .ok; } export fn panto_conversation_add_compaction_summary(c: *PantoConversation, text: ?[*:0]const u8) PantoStatus { unwrapConversation(c).addCompactionSummary(cstr(text)) 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); h.* = .{ .session = ns.store().create() }; out.* = @ptrCast(h); return .ok; } export fn panto_session_create(store: *PantoSessionStore, info: PantoSessionInfo, out: **PantoSession) PantoStatus { const cloned = cloneSessionInfo(info) catch |e| return setErr("{t}", .{e}); out.* = wrapSession(.{ .info = cloned, .store = unwrapStore(store) }, true) catch |e| return setErr("{t}", .{e}); return .ok; } export fn panto_session_destroy(s: ?*PantoSession) void { if (s) |ptr| { const h: *SessionHandle = @ptrCast(@alignCast(ptr)); if (h.owns_info) h.session.info.deinit(allocator); allocator.destroy(h); } } export fn panto_session_store_create(ctx: *anyopaque, vt: *const PantoSessionStoreVTable, out: **PantoSessionStore) PantoStatus { const h = allocator.create(StoreHandle) catch return setErr("OutOfMemory", .{}); h.* = .{ .ctx = ctx, .vtable = vt }; out.* = @ptrCast(h); return .ok; } export fn panto_session_store_destroy(store: ?*PantoSessionStore) void { if (store) |ptr| { const h: *StoreHandle = @ptrCast(@alignCast(ptr)); if (h.vtable.destroy) |d| d(h.ctx); allocator.destroy(h); } } export fn panto_null_store_create(out: **PantoSessionStore) PantoStatus { const impl = allocator.create(NullStoreHandle) catch return setErr("OutOfMemory", .{}); impl.* = .{ .store = panto.NullStore.init(allocator) }; const h = allocator.create(StoreHandle) catch { allocator.destroy(impl); return setErr("OutOfMemory", .{}); }; h.* = .{ .ctx = &impl.store, .vtable = &null_store_vtable }; out.* = @ptrCast(h); return .ok; } export fn panto_file_system_jsonl_store_create(dir: ?[*:0]const u8, metadata: ?[*:0]const u8, out: **PantoSessionStore) PantoStatus { const impl = allocator.create(FileSystemJSONLStoreHandle) catch return setErr("OutOfMemory", .{}); impl.* = .{ .store = panto.FileSystemJSONLStore.initWithMetadata(allocator, threaded.?.io(), cstr(dir), if (metadata == null or cstr(metadata).len == 0) null else cstr(metadata)) catch |e| { allocator.destroy(impl); return setErr("{t}", .{e}); } }; const h = allocator.create(StoreHandle) catch { impl.store.deinit(); allocator.destroy(impl); return setErr("OutOfMemory", .{}); }; h.* = .{ .ctx = &impl.store, .vtable = &fs_jsonl_store_vtable }; out.* = @ptrCast(h); return .ok; } export fn panto_session_store_create_session(store: *PantoSessionStore, out: **PantoSession) PantoStatus { return cStoreCreate(store, out); } export fn panto_session_store_list(store: *PantoSessionStore, out: *PantoSessionInfoList) PantoStatus { return cStoreList(store, out); } export fn panto_session_info_list_destroy(infos: PantoSessionInfoList) void { if (infos.ptr) |p| { for (p[0..infos.len]) |info| freePantoSessionInfo(info); allocator.free(p[0..infos.len]); } } export fn panto_session_store_resolve(store: *PantoSessionStore, id: ?[*:0]const u8, out: *PantoSessionResult) PantoStatus { return cStoreResolve(store, outSlice(cstr(id)), out); } export fn panto_session_store_latest(store: *PantoSessionStore, out: *PantoSessionResult) PantoStatus { return cStoreLatest(store, out); } export fn panto_session_load(session: *PantoSession, out: **PantoConversation) PantoStatus { const h: *SessionHandle = @ptrCast(@alignCast(session)); const conv = h.session.load() catch |e| return setErr("{t}", .{e}); const ch = allocator.create(ConversationHandle) catch return setErr("OutOfMemory", .{}); ch.* = .{ .conversation = conv }; out.* = @ptrCast(ch); return .ok; } fn convertConfig(c: *const PantoConfig) !*panto.Config { const out = try allocator.create(panto.Config); errdefer allocator.destroy(out); out.* = .{ .provider = try convertProvider(c.provider), .compaction = .{ .keep_verbatim = c.compaction.keep_verbatim, .model = if (c.compaction.has_model) try convertProvider(c.compaction.model) else null, .compaction_prompt = if (c.compaction.has_compaction_prompt) try dupCStr(c.compaction.compaction_prompt) else null }, .retry = .{ .max_attempts = c.retry.max_attempts, .initial_delay_ms = c.retry.initial_delay_ms, .max_delay_ms = c.retry.max_delay_ms, .multiplier = c.retry.multiplier, .jitter = c.retry.jitter } }; return out; } fn convertProvider(p: PantoProviderConfig) !panto.ProviderConfig { return switch (p.tag) { .openai_chat => .{ .openai_chat = .{ .api_key = try dupCStr(p.data.openai_chat.api_key), .base_url = try dupCStr(p.data.openai_chat.base_url), .model = try dupCStr(p.data.openai_chat.model), .reasoning = @enumFromInt(@intFromEnum(p.data.openai_chat.reasoning)), .max_tokens = p.data.openai_chat.max_tokens } }, .anthropic_messages => .{ .anthropic_messages = .{ .api_key = try dupCStr(p.data.anthropic_messages.api_key), .base_url = try dupCStr(p.data.anthropic_messages.base_url), .model = try dupCStr(p.data.anthropic_messages.model), .api_version = if (cstr(p.data.anthropic_messages.api_version).len == 0) "2023-06-01" else try dupCStr(p.data.anthropic_messages.api_version), .max_tokens = p.data.anthropic_messages.max_tokens, .thinking = @enumFromInt(@intFromEnum(p.data.anthropic_messages.thinking)), .effort = @enumFromInt(@intFromEnum(p.data.anthropic_messages.effort)), .thinking_budget_tokens = if (p.data.anthropic_messages.has_thinking_budget_tokens) p.data.anthropic_messages.thinking_budget_tokens else null, .thinking_interleaved = p.data.anthropic_messages.thinking_interleaved } }, }; } fn freeConfig(c: *panto.Config) void { freeProvider(&c.provider); if (c.compaction.model) |*m| freeProvider(m); if (c.compaction.compaction_prompt) |s| allocator.free(s); allocator.destroy(c); } fn freeProvider(pv: *panto.ProviderConfig) void { switch (pv.*) { .openai_chat => |c| { allocator.free(c.api_key); allocator.free(c.base_url); allocator.free(c.model); }, .anthropic_messages => |c| { allocator.free(c.api_key); allocator.free(c.base_url); allocator.free(c.model); if (c.api_version.ptr != "2023-06-01".ptr) allocator.free(c.api_version); }, } } fn cStoreCreate(ctx: *anyopaque, out: **PantoSession) PantoStatus { const h: *StoreHandle = @ptrCast(@alignCast(ctx)); return h.vtable.create(h.ctx, out); } fn cStoreList(ctx: *anyopaque, out: *PantoSessionInfoList) PantoStatus { const h: *StoreHandle = @ptrCast(@alignCast(ctx)); return h.vtable.list(h.ctx, out); } fn cStoreResolve(ctx: *anyopaque, id: PantoSlice, out: *PantoSessionResult) PantoStatus { const h: *StoreHandle = @ptrCast(@alignCast(ctx)); return h.vtable.resolve(h.ctx, id, out); } fn cStoreLatest(ctx: *anyopaque, out: *PantoSessionResult) PantoStatus { const h: *StoreHandle = @ptrCast(@alignCast(ctx)); return h.vtable.latest(h.ctx, out); } fn cStoreLoad(ctx: *anyopaque, id: []const u8) anyerror!?panto.Conversation { const h: *StoreHandle = @ptrCast(@alignCast(ctx)); var out: *PantoConversation = undefined; if (h.vtable.load(h.ctx, outSlice(id), &out) != .ok) return error.CStoreLoadFailed; const ch: *ConversationHandle = @ptrCast(@alignCast(out)); const conv = ch.conversation; ch.conversation = panto.Conversation.init(allocator); panto_conversation_destroy(out); return conv; } 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 "") }; 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 { const h: *StoreHandle = @ptrCast(@alignCast(ctx)); var list: PantoSessionInfoList = undefined; if (h.vtable.list(h.ctx, &list) != .ok) return error.CStoreListFailed; defer h.vtable.free_session_infos(h.ctx, list); const out = try allocator.alloc(panto.SessionInfo, list.len); errdefer allocator.free(out); if (list.ptr) |p| { for (p[0..list.len], 0..) |info, i| out[i] = try cloneSessionInfo(info); } return out; } fn cStoreFreeInfos(_: *anyopaque, infos: []panto.SessionInfo) void { for (infos) |info| info.deinit(allocator); allocator.free(infos); } fn cStoreResolveZ(ctx: *anyopaque, id: []const u8) anyerror!?panto.Session { const h: *StoreHandle = @ptrCast(@alignCast(ctx)); var res: PantoSessionResult = undefined; if (h.vtable.resolve(h.ctx, outSlice(id), &res) != .ok) return error.CStoreResolveFailed; if (!res.found) return null; const sh: *SessionHandle = @ptrCast(@alignCast(res.session.?)); return sh.session; } fn cStoreLatestZ(ctx: *anyopaque) anyerror!?panto.Session { const h: *StoreHandle = @ptrCast(@alignCast(ctx)); var res: PantoSessionResult = undefined; if (h.vtable.latest(h.ctx, &res) != .ok) return error.CStoreLatestFailed; if (!res.found) return null; const sh: *SessionHandle = @ptrCast(@alignCast(res.session.?)); return sh.session; } fn cStoreCreateZ(ctx: *anyopaque) panto.Session { var out: *PantoSession = undefined; if (cStoreCreate(ctx, &out) != .ok) { var ns = panto.NullStore.init(allocator); return ns.store().create(); } const sh: *SessionHandle = @ptrCast(@alignCast(out)); return sh.session; } const c_store_vtable: panto.SessionStore.VTable = .{ .create = cStoreCreateZ, .list = cStoreListZ, .freeSessionInfos = cStoreFreeInfos, .resolve = cStoreResolveZ, .latest = cStoreLatestZ, .load = cStoreLoad, .appendMessages = cStoreAppend }; fn builtinStoreCreate(ctx: *anyopaque, out: **PantoSession) callconv(.c) PantoStatus { return wrapBuiltinSession(@as(*panto.SessionStore, @ptrCast(@alignCast(ctx))).create(), out); } fn builtinStoreList(ctx: *anyopaque, out: *PantoSessionInfoList) callconv(.c) PantoStatus { const s: *panto.SessionStore = @ptrCast(@alignCast(ctx)); const infos = s.list() catch |e| return setErr("{t}", .{e}); defer s.freeSessionInfos(infos); const c_infos = allocator.alloc(PantoSessionInfo, infos.len) catch return setErr("OutOfMemory", .{}); for (infos, 0..) |info, i| c_infos[i] = marshalSessionInfo(info) catch return setErr("OutOfMemory", .{}); out.* = .{ .ptr = c_infos.ptr, .len = c_infos.len }; return .ok; } fn builtinStoreFreeInfos(_: *anyopaque, infos: PantoSessionInfoList) callconv(.c) void { panto_session_info_list_destroy(infos); } fn builtinStoreResolve(ctx: *anyopaque, id: PantoSlice, out: *PantoSessionResult) callconv(.c) PantoStatus { const s: *panto.SessionStore = @ptrCast(@alignCast(ctx)); if (s.resolve(slice(id)) catch |e| return setErr("{t}", .{e})) |sess| return wrapBuiltinResult(sess, out); out.* = .{ .found = false, .session = null }; return .ok; } fn builtinStoreLatest(ctx: *anyopaque, out: *PantoSessionResult) callconv(.c) PantoStatus { const s: *panto.SessionStore = @ptrCast(@alignCast(ctx)); if (s.latest() catch |e| return setErr("{t}", .{e})) |sess| return wrapBuiltinResult(sess, out); out.* = .{ .found = false, .session = null }; return .ok; } fn builtinStoreLoad(ctx: *anyopaque, id: PantoSlice, out: **PantoConversation) callconv(.c) PantoStatus { const s: *panto.SessionStore = @ptrCast(@alignCast(ctx)); const conv = s.load(slice(id)) catch |e| return setErr("{t}", .{e}); if (conv == null) return setErr("NotFound", .{}); const ch = allocator.create(ConversationHandle) catch return setErr("OutOfMemory", .{}); ch.* = .{ .conversation = conv.? }; out.* = @ptrCast(ch); return .ok; } 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 = "" } }; s.appendMessages(slice(session_id), pmsgs) catch |e| return setErr("{t}", .{e}); return .ok; } fn wrapBuiltinSession(sess: panto.Session, out: **PantoSession) PantoStatus { out.* = wrapSession(sess, true) catch |e| return setErr("{t}", .{e}); return .ok; } fn wrapBuiltinResult(sess: panto.Session, out: *PantoSessionResult) PantoStatus { var ps: *PantoSession = undefined; const st = wrapBuiltinSession(sess, &ps); if (st != .ok) return st; out.* = .{ .found = true, .session = ps }; return .ok; } fn nullStoreDestroy(ctx: *anyopaque) callconv(.c) void { const s: *panto.NullStore = @ptrCast(@alignCast(ctx)); const h: *NullStoreHandle = @fieldParentPtr("store", s); allocator.destroy(h); } fn fsJSONLStoreDestroy(ctx: *anyopaque) callconv(.c) void { const s: *panto.FileSystemJSONLStore = @ptrCast(@alignCast(ctx)); const h: *FileSystemJSONLStoreHandle = @fieldParentPtr("store", s); h.store.deinit(); allocator.destroy(h); } const null_store_vtable: PantoSessionStoreVTable = .{ .create = builtinStoreCreate, .list = builtinStoreList, .free_session_infos = builtinStoreFreeInfos, .resolve = builtinStoreResolve, .latest = builtinStoreLatest, .load = builtinStoreLoad, .append_messages = builtinStoreAppend, .destroy = nullStoreDestroy }; const fs_jsonl_store_vtable: PantoSessionStoreVTable = .{ .create = builtinStoreCreate, .list = builtinStoreList, .free_session_infos = builtinStoreFreeInfos, .resolve = builtinStoreResolve, .latest = builtinStoreLatest, .load = builtinStoreLoad, .append_messages = builtinStoreAppend, .destroy = fsJSONLStoreDestroy }; export fn panto_agent_create(c: *const PantoConfig, s: ?*PantoSession, conv: ?*PantoConversation, out: **PantoAgent) PantoStatus { const cfg = convertConfig(c) catch |e| return setErr("{t}", .{e}); const sess = if (s) |sp| blk: { const sh: *SessionHandle = @ptrCast(@alignCast(sp)); sh.owns_info = false; break :blk sh.session; } else blk: { var ns = panto.NullStore.init(allocator); break :blk ns.store().create(); }; var maybe_conv: ?panto.Conversation = null; if (conv) |cp| { const ch: *ConversationHandle = @ptrCast(@alignCast(cp)); maybe_conv = ch.conversation; ch.conversation = panto.Conversation.init(allocator); } const a = panto.Agent.init(allocator, threaded.?.io(), cfg, sess, maybe_conv) catch |e| { freeConfig(cfg); return setErr("{t}", .{e}); }; const h = allocator.create(AgentHandle) catch return setErr("OutOfMemory", .{}); h.* = .{ .agent = a, .config = cfg }; out.* = @ptrCast(h); return .ok; } export fn panto_agent_destroy(a: ?*PantoAgent) void { if (a) |ptr| { const h: *AgentHandle = @ptrCast(@alignCast(ptr)); h.agent.deinit(); freeConfig(h.config); allocator.destroy(h); } } export fn panto_agent_session_id(a: *PantoAgent) PantoSlice { const h: *AgentHandle = @ptrCast(@alignCast(a)); return outSlice(h.agent.sessionId()); } export fn panto_agent_conversation(a: *PantoAgent) *PantoConversation { const h: *AgentHandle = @ptrCast(@alignCast(a)); return @ptrCast(&h.agent.conversation); } export fn panto_agent_set_config(a: *PantoAgent, c: *const PantoConfig) PantoStatus { const h: *AgentHandle = @ptrCast(@alignCast(a)); const cfg = convertConfig(c) catch |e| return setErr("{t}", .{e}); const old = h.config; h.config = cfg; h.agent.setConfig(cfg); freeConfig(old); return .ok; } export fn panto_agent_add_system_message(a: *PantoAgent, text: ?[*:0]const u8) PantoStatus { const h: *AgentHandle = @ptrCast(@alignCast(a)); h.agent.addSystemMessage(cstr(text)) catch |e| return setErr("{t}", .{e}); return .ok; } export fn panto_agent_set_system_prompt(a: *PantoAgent, text: ?[*:0]const u8) PantoStatus { const h: *AgentHandle = @ptrCast(@alignCast(a)); h.agent.setSystemPrompt(cstr(text)) catch |e| return setErr("{t}", .{e}); return .ok; } export fn panto_agent_compact(a: *PantoAgent, override_system_prompt: ?[*:0]const u8, extra_instructions: ?[*:0]const u8, out: *PantoCompactionResult) PantoStatus { const h: *AgentHandle = @ptrCast(@alignCast(a)); const osp = if (override_system_prompt == null or cstr(override_system_prompt).len == 0) null else cstr(override_system_prompt); const extra = if (extra_instructions == null or cstr(extra_instructions).len == 0) null else cstr(extra_instructions); const r = h.agent.compact(osp, extra) catch |e| return setErr("{t}", .{e}); out.* = .{ .compacted = r.compacted, .kept_turns = r.kept_turns, .summarized_messages = r.summarized_messages }; return .ok; } export fn panto_agent_run(a: *PantoAgent, text: ?[*:0]const u8, out: **PantoStream) PantoStatus { const h: *AgentHandle = @ptrCast(@alignCast(a)); const st = h.agent.run(.{ .text = cstr(text) }) catch |e| return setErr("{t}", .{e}); out.* = @ptrCast(st); return .ok; } export fn panto_stream_reopen(s: *PantoStream) PantoStatus { const st: *panto.Stream = @ptrCast(@alignCast(s)); st.reopen() catch |e| return setErr("{t}", .{e}); return .ok; } export fn panto_stream_destroy(s: ?*PantoStream) void { if (s) |ptr| { const st: *panto.Stream = @ptrCast(@alignCast(ptr)); st.deinit(); } } export fn panto_stream_next(s: *PantoStream, out: *PantoEvent) PantoNextStatus { const st: *panto.Stream = @ptrCast(@alignCast(s)); const ev = st.next() catch |e| return setErrNext("{t}", .{e}); if (ev) |e| { out.* = marshalEvent(e) catch |err| return setErrNext("{t}", .{err}); return .event; } return .done; } export fn panto_event_free(ev: *PantoEvent) void { switch (ev.tag) { .tool_details => { freePS(ev.data.tool_details.id); freePS(ev.data.tool_details.name); }, .content_delta => freePS(ev.data.content_delta.delta), .provider_retry => { freePS(ev.data.provider_retry.error_name); freePS(ev.data.provider_retry.message); }, else => {}, } ev.* = undefined; } fn freePS(s: PantoSlice) void { if (s.ptr) |p| allocator.free(@constCast(p[0..s.len])); } fn marshalEvent(ev: panto.Event) !PantoEvent { return switch (ev) { .message_start => |r| .{ .tag = .message_start, .data = .{ .message_start = @enumFromInt(@intFromEnum(r)) } }, .block_start => |b| .{ .tag = .block_start, .data = .{ .block_start = .{ .block_type = @enumFromInt(@intFromEnum(b.block_type)), .index = b.index } } }, .tool_details => |t| .{ .tag = .tool_details, .data = .{ .tool_details = .{ .index = t.index, .id = try dupOut(t.id), .name = try dupOut(t.name) } } }, .content_delta => |d| .{ .tag = .content_delta, .data = .{ .content_delta = .{ .index = d.index, .delta = try dupOut(d.delta) } } }, .block_complete => |b| .{ .tag = .block_complete, .data = .{ .block_complete = .{ .index = b.index, .block_type = @enumFromInt(@intFromEnum(b.block)) } } }, .message_complete => |m| .{ .tag = .message_complete, .data = .{ .message_complete = .{ .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 } } } }, .provider_retry => |r| .{ .tag = .provider_retry, .data = .{ .provider_retry = .{ .attempt = r.attempt, .max_attempts = r.max_attempts, .delay_ms = r.delay_ms, .error_name = try dupOut(@errorName(r.err)), .has_status_code = r.status_code != null, .status_code = r.status_code orelse 0, .has_retry_after_ms = r.retry_after_ms != null, .retry_after_ms = r.retry_after_ms orelse 0, .message = try dupOut(r.message orelse ""), .compaction = r.compaction } } }, .tool_dispatch_start => |t| .{ .tag = .tool_dispatch_start, .data = .{ .tool_dispatch_start = .{ .count = t.count } } }, .tool_dispatch_complete => .{ .tag = .tool_dispatch_complete, .data = .{ .tool_dispatch_complete = .{ .message_index = 0 } } }, .turn_complete => .{ .tag = .turn_complete, .data = undefined }, }; } fn usage(u: panto.Usage) PantoUsage { return .{ .input = u.input, .output = u.output, .cache_read = u.cache_read, .cache_write = u.cache_write, .reasoning = u.reasoning }; } fn zigUsage(u: PantoUsage) panto.Usage { return .{ .input = u.input, .output = u.output, .cache_read = u.cache_read, .cache_write = u.cache_write, .reasoning = u.reasoning }; } test "c abi exports compile" { try std.testing.expect(true); } fn cloneToolDecl(d: PantoToolDecl) !panto.ToolDecl { return .{ .name = try dupSlice(d.name), .description = try dupSlice(d.description), .schema_json = try dupSlice(d.schema_json) }; } fn freeToolDecl(d: panto.ToolDecl) void { allocator.free(d.name); allocator.free(d.description); allocator.free(d.schema_json); } fn toolInvoke(ctx: *anyopaque, input: []const u8, alloc: std.mem.Allocator) anyerror!panto.ResultParts { _ = alloc; const h: *ToolHandle = @ptrCast(@alignCast(ctx)); var out: PantoResultParts = .{ .ptr = null, .len = 0 }; if (h.invoke(h.ctx, outSlice(input), &out) != .ok) return error.ToolCallbackFailed; return convertResultParts(out); } fn toolDeinit(ctx: *anyopaque, alloc: std.mem.Allocator) void { _ = alloc; const h: *ToolHandle = @ptrCast(@alignCast(ctx)); if (h.destroy) |d| d(h.ctx); freeToolDecl(h.decl); allocator.destroy(h); } fn sourceInvokeBatch(ctx: *anyopaque, calls: []const panto.ToolCall, results: []panto.ToolCallResult, alloc: std.mem.Allocator) anyerror!void { _ = alloc; const h: *ToolSourceHandle = @ptrCast(@alignCast(ctx)); const ccalls = try allocator.alloc(PantoToolCall, calls.len); defer allocator.free(ccalls); const cresults = try allocator.alloc(PantoToolCallResult, results.len); defer allocator.free(cresults); for (calls, 0..) |c, i| ccalls[i] = .{ .tool_name = outSlice(c.tool_name), .input = outSlice(c.input) }; for (cresults) |*r| r.* = .{ .ok = false, .parts = .{ .ptr = null, .len = 0 }, .err = .{ .ptr = null, .len = 0 } }; if (h.invoke_batch(h.ctx, ccalls.ptr, cresults.ptr, cresults.len) != .ok) return error.ToolSourceCallbackFailed; for (cresults, 0..) |r, i| { if (r.ok) results[i] = .{ .ok = try convertResultParts(r.parts) } else results[i] = .{ .err = error.ToolCallbackFailed }; } } fn sourceDeinit(ctx: *anyopaque, alloc: std.mem.Allocator) void { _ = alloc; const h: *ToolSourceHandle = @ptrCast(@alignCast(ctx)); if (h.destroy) |d| d(h.ctx); allocator.free(h.name); for (h.decls) |d| freeToolDecl(d); allocator.free(h.decls); allocator.destroy(h); } fn convertResultParts(parts: PantoResultParts) !panto.ResultParts { const out = try allocator.alloc(panto.ResultPart, parts.len); errdefer allocator.free(out); if (parts.ptr) |p| { for (p[0..parts.len], 0..) |part, i| { out[i] = switch (part.tag) { .text => .{ .text = try dupSlice(part.data.text) }, .media => .{ .media = .{ .media_type = if (part.data.media.has_media_type) try dupSlice(part.data.media.media_type) else null, .data = try dupSlice(part.data.media.data) } }, }; } } return .{ .items = out }; } 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}); } }; 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}); }; 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; 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; } export fn panto_result_parts_text(text: ?[*:0]const u8) PantoResultParts { const part = allocator.alloc(PantoResultPart, 1) catch return .{ .ptr = null, .len = 0 }; part[0] = .{ .tag = .text, .data = .{ .text = dupOut(cstr(text)) catch .{ .ptr = null, .len = 0 } } }; return .{ .ptr = part.ptr, .len = 1 }; } 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); }, }; allocator.free(p[0..parts.len]); } }