diff options
Diffstat (limited to 'libpanto-c')
| -rw-r--r-- | libpanto-c/build.zig | 26 | ||||
| -rw-r--r-- | libpanto-c/build.zig.zon | 9 | ||||
| -rw-r--r-- | libpanto-c/include/panto.h | 104 | ||||
| -rw-r--r-- | libpanto-c/src/lib.zig | 353 |
4 files changed, 492 insertions, 0 deletions
diff --git a/libpanto-c/build.zig b/libpanto-c/build.zig new file mode 100644 index 0000000..8c3cb8e --- /dev/null +++ b/libpanto-c/build.zig @@ -0,0 +1,26 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const panto_dep = b.dependency("panto", .{ .target = target, .optimize = optimize }); + + const mod = b.createModule(.{ + .root_source_file = b.path("src/lib.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + mod.addImport("panto", panto_dep.module("panto")); + + const lib = b.addLibrary(.{ .name = "panto", .root_module = mod, .linkage = .dynamic }); + lib.rdynamic = true; + b.installArtifact(lib); + b.installFile("include/panto.h", "include/panto.h"); + + const unit_tests = b.addTest(.{ .name = "panto-c-tests", .root_module = mod }); + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/libpanto-c/build.zig.zon b/libpanto-c/build.zig.zon new file mode 100644 index 0000000..13966ed --- /dev/null +++ b/libpanto-c/build.zig.zon @@ -0,0 +1,9 @@ +.{ + .fingerprint = 0xdae0d0294b1fb85c, + .name = .panto_c, + .version = "0.0.0", + .dependencies = .{ + .panto = .{ .path = "../libpanto" }, + }, + .paths = .{ "build.zig", "build.zig.zon", "src", "include" }, +} diff --git a/libpanto-c/include/panto.h b/libpanto-c/include/panto.h new file mode 100644 index 0000000..4430810 --- /dev/null +++ b/libpanto-c/include/panto.h @@ -0,0 +1,104 @@ +#ifndef PANTO_H +#define PANTO_H + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_WIN32) +#define PANTO_EXPORT __declspec(dllexport) +#else +#define PANTO_EXPORT __attribute__((visibility("default"))) +#endif + +typedef struct PantoAgent PantoAgent; +typedef struct PantoStream PantoStream; +typedef struct PantoConversation PantoConversation; +typedef struct PantoSession PantoSession; +typedef struct PantoSessionStore PantoSessionStore; + +typedef struct { const uint8_t *ptr; size_t len; } PantoSlice; + +typedef enum { PANTO_OK = 0, PANTO_ERROR = 1 } PantoStatus; +typedef enum { PANTO_NEXT_EVENT = 0, PANTO_NEXT_DONE = 1, PANTO_NEXT_ERROR = 2 } PantoNextStatus; +typedef enum { PANTO_OPENAI_CHAT = 0, PANTO_ANTHROPIC_MESSAGES = 1 } PantoAPIStyle; +typedef enum { PANTO_REASONING_DEFAULT = 0, PANTO_REASONING_OFF, PANTO_REASONING_MINIMAL, PANTO_REASONING_LOW, PANTO_REASONING_MEDIUM, PANTO_REASONING_HIGH } PantoReasoningEffort; +typedef enum { PANTO_THINKING_DISABLED = 0, PANTO_THINKING_ENABLED, PANTO_THINKING_ADAPTIVE } PantoThinking; +typedef enum { PANTO_EFFORT_LOW = 0, PANTO_EFFORT_MEDIUM, PANTO_EFFORT_HIGH, PANTO_EFFORT_XHIGH, PANTO_EFFORT_MAX } PantoEffort; +typedef enum { PANTO_ROLE_SYSTEM = 0, PANTO_ROLE_USER, PANTO_ROLE_ASSISTANT } PantoMessageRole; +typedef enum { PANTO_BLOCK_TEXT = 0, PANTO_BLOCK_THINKING, PANTO_BLOCK_TOOL_USE, PANTO_BLOCK_TOOL_RESULT, PANTO_BLOCK_SYSTEM, PANTO_BLOCK_COMPACTION_SUMMARY } PantoContentBlockTag; +typedef enum { PANTO_SYSTEM_APPEND = 0, PANTO_SYSTEM_REPLACE } PantoSystemMode; +typedef enum { PANTO_EVENT_MESSAGE_START = 0, PANTO_EVENT_BLOCK_START, PANTO_EVENT_TOOL_DETAILS, PANTO_EVENT_CONTENT_DELTA, PANTO_EVENT_BLOCK_COMPLETE, PANTO_EVENT_MESSAGE_COMPLETE, PANTO_EVENT_PROVIDER_RETRY, PANTO_EVENT_TOOL_DISPATCH_START, PANTO_EVENT_TOOL_DISPATCH_COMPLETE, PANTO_EVENT_TURN_COMPLETE } PantoEventTag; + +typedef struct { uint64_t input, output, cache_read, cache_write, reasoning; } PantoUsage; +typedef struct { PantoAPIStyle api_style; PantoSlice base_url; PantoSlice model; PantoReasoningEffort reasoning; PantoThinking thinking; PantoEffort effort; bool has_thinking_budget_tokens; uint32_t thinking_budget_tokens; bool thinking_interleaved; } PantoWireIdentity; +typedef struct { PantoSlice id, created, modified, last_user_message, base_url, model; size_t message_count; PantoAPIStyle api_style; PantoReasoningEffort reasoning; } PantoSessionInfo; +typedef struct { PantoSessionInfo *ptr; size_t len; } PantoSessionInfoList; +typedef struct { bool found; PantoSession *session; } PantoSessionResult; +typedef struct { PantoSlice api_key, base_url, model; PantoReasoningEffort reasoning; uint32_t max_tokens; } PantoOpenAIChatConfig; +typedef struct { PantoSlice api_key, base_url, model, api_version; uint32_t max_tokens; PantoThinking thinking; PantoEffort effort; bool has_thinking_budget_tokens; uint32_t thinking_budget_tokens; bool thinking_interleaved; } PantoAnthropicMessagesConfig; +typedef struct { PantoAPIStyle tag; union { PantoOpenAIChatConfig openai_chat; PantoAnthropicMessagesConfig anthropic_messages; } data; } PantoProviderConfig; +typedef struct { uint32_t keep_verbatim; bool has_model; PantoProviderConfig model; bool has_compaction_prompt; PantoSlice compaction_prompt; } PantoCompactionConfig; +typedef struct { size_t max_attempts; uint64_t initial_delay_ms, max_delay_ms; double multiplier; bool jitter; } PantoRetryConfig; +typedef struct { PantoProviderConfig provider; PantoCompactionConfig compaction; PantoRetryConfig retry; } PantoConfig; + +typedef struct { PantoContentBlockTag block_type; size_t index; } PantoBlockStart; +typedef struct { size_t index; PantoSlice id, name; } PantoToolDetails; +typedef struct { size_t index; PantoSlice delta; } PantoContentDelta; +typedef struct { size_t index; PantoContentBlockTag block_type; } PantoBlockComplete; +typedef struct { bool has_usage; PantoUsage usage; } PantoMessageComplete; +typedef struct { size_t attempt, max_attempts; uint64_t delay_ms; PantoSlice error_name; bool has_status_code; uint16_t status_code; bool has_retry_after_ms; uint64_t retry_after_ms; PantoSlice message; bool compaction; } PantoProviderRetry; +typedef struct { size_t count; } PantoToolDispatchStart; +typedef struct { size_t message_index; } PantoToolDispatchComplete; + +typedef struct { PantoEventTag tag; union { PantoMessageRole message_start; PantoBlockStart block_start; PantoToolDetails tool_details; PantoContentDelta content_delta; PantoBlockComplete block_complete; PantoMessageComplete message_complete; PantoProviderRetry provider_retry; PantoToolDispatchStart tool_dispatch_start; PantoToolDispatchComplete tool_dispatch_complete; } data; } PantoEvent; + +typedef struct { PantoMessageRole role; bool has_usage; PantoUsage usage; bool has_metadata; PantoSlice metadata; } PantoPersistentMessage; + +typedef struct { + PantoStatus (*create)(void *ctx, PantoSession **out); + PantoStatus (*list)(void *ctx, PantoSessionInfoList *out); + void (*free_session_infos)(void *ctx, PantoSessionInfoList infos); + PantoStatus (*resolve)(void *ctx, PantoSlice id, PantoSessionResult *out); + PantoStatus (*latest)(void *ctx, PantoSessionResult *out); + PantoStatus (*load)(void *ctx, PantoSlice id, PantoConversation **out); + PantoStatus (*append_messages)(void *ctx, PantoSlice session_id, const PantoPersistentMessage *messages, size_t len); + void (*destroy)(void *ctx); +} PantoSessionStoreVTable; + +PANTO_EXPORT void panto_init(void); +PANTO_EXPORT void panto_deinit(void); +PANTO_EXPORT PantoSlice panto_last_error(void); +PANTO_EXPORT void panto_error_free(PantoSlice s); +PANTO_EXPORT PantoStatus panto_conversation_create(PantoConversation **out); +PANTO_EXPORT void panto_conversation_destroy(PantoConversation *conversation); +PANTO_EXPORT PantoStatus panto_session_create_null(PantoSession **out); +PANTO_EXPORT PantoStatus panto_session_create(PantoSessionStore *store, PantoSessionInfo info, PantoSession **out); +PANTO_EXPORT void panto_session_destroy(PantoSession *session); +PANTO_EXPORT PantoStatus panto_session_store_create(void *ctx, const PantoSessionStoreVTable *vtable, PantoSessionStore **out); +PANTO_EXPORT void panto_session_store_destroy(PantoSessionStore *store); +PANTO_EXPORT PantoStatus panto_session_store_create_session(PantoSessionStore *store, PantoSession **out); +PANTO_EXPORT PantoStatus panto_session_store_list(PantoSessionStore *store, PantoSessionInfoList *out); +PANTO_EXPORT void panto_session_info_list_destroy(PantoSessionInfoList infos); +PANTO_EXPORT PantoStatus panto_session_store_resolve(PantoSessionStore *store, PantoSlice id, PantoSessionResult *out); +PANTO_EXPORT PantoStatus panto_session_store_latest(PantoSessionStore *store, PantoSessionResult *out); +PANTO_EXPORT PantoStatus panto_session_load(PantoSession *session, PantoConversation **out); +PANTO_EXPORT PantoStatus panto_agent_create(const PantoConfig *config, PantoSession *session, PantoConversation *conversation, PantoAgent **out); +PANTO_EXPORT void panto_agent_destroy(PantoAgent *agent); +PANTO_EXPORT PantoSlice panto_agent_session_id(PantoAgent *agent); +PANTO_EXPORT PantoStatus panto_agent_set_config(PantoAgent *agent, const PantoConfig *config); +PANTO_EXPORT PantoStatus panto_agent_add_system_message(PantoAgent *agent, PantoSlice text); +PANTO_EXPORT PantoStatus panto_agent_set_system_prompt(PantoAgent *agent, PantoSlice text); +PANTO_EXPORT PantoStatus panto_agent_run(PantoAgent *agent, PantoSlice user_text, PantoStream **out); +PANTO_EXPORT void panto_stream_destroy(PantoStream *stream); +PANTO_EXPORT PantoNextStatus panto_stream_next(PantoStream *stream, PantoEvent *out); +PANTO_EXPORT void panto_event_free(PantoEvent *event); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/libpanto-c/src/lib.zig b/libpanto-c/src/lib.zig new file mode 100644 index 0000000..dd01026 --- /dev/null +++ b/libpanto-c/src/lib.zig @@ -0,0 +1,353 @@ +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 }; + +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: PantoSlice, base_url: PantoSlice, model: PantoSlice, reasoning: PantoReasoningEffort, max_tokens: u32 }; +pub const PantoAnthropicMessagesConfig = extern struct { api_key: PantoSlice, base_url: PantoSlice, model: PantoSlice, api_version: PantoSlice, 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: PantoSlice }; +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 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, +}; + +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 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); + } +} +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_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: PantoSlice, out: *PantoSessionResult) PantoStatus { return cStoreResolve(store, 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 dupSlice(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 dupSlice(p.data.openai_chat.api_key), .base_url = try dupSlice(p.data.openai_chat.base_url), .model = try dupSlice(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 dupSlice(p.data.anthropic_messages.api_key), .base_url = try dupSlice(p.data.anthropic_messages.base_url), .model = try dupSlice(p.data.anthropic_messages.model), .api_version = if (p.data.anthropic_messages.api_version.len == 0) "2023-06-01" else try dupSlice(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 }; + +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_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: PantoSlice) PantoStatus { + const h: *AgentHandle = @ptrCast(@alignCast(a)); + h.agent.addSystemMessage(slice(text)) catch |e| return setErr("{t}", .{e}); + return .ok; +} +export fn panto_agent_set_system_prompt(a: *PantoAgent, text: PantoSlice) PantoStatus { + const h: *AgentHandle = @ptrCast(@alignCast(a)); + h.agent.setSystemPrompt(slice(text)) catch |e| return setErr("{t}", .{e}); + return .ok; +} +export fn panto_agent_run(a: *PantoAgent, text: PantoSlice, out: **PantoStream) PantoStatus { + const h: *AgentHandle = @ptrCast(@alignCast(a)); + const st = h.agent.run(.{ .text = slice(text) }) catch |e| return setErr("{t}", .{e}); + out.* = @ptrCast(st); + 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 }; +} + +test "c abi exports compile" { + try std.testing.expect(true); +} |
