From e2477f7d2d2a4eab870a2bd61534cfd146907915 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 11 Jun 2026 09:08:42 -0600 Subject: libpanto API fixes - FSJSONLStore: don't require `cwd`, instead panto CLI provides it in the session metadata - Lua: expose the `SessionStore` interface and the `FSJSONLStore` and `NullStore` implementations - C: use idiomatic `const char *` for incoming strings - C: expose the `FSJSONLStore` and `NullStore` implementations --- libpanto-c/src/lib.zig | 238 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 208 insertions(+), 30 deletions(-) (limited to 'libpanto-c/src/lib.zig') diff --git a/libpanto-c/src/lib.zig b/libpanto-c/src/lib.zig index dd01026..046236a 100644 --- a/libpanto-c/src/lib.zig +++ b/libpanto-c/src/lib.zig @@ -16,6 +16,8 @@ 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 }; pub const PantoSlice = extern struct { ptr: ?[*]const u8, len: usize }; pub const PantoStatus = enum(c_int) { ok = 0, err = 1 }; @@ -33,10 +35,10 @@ pub const PantoUsage = extern struct { input: u64, output: u64, cache_read: 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 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: PantoSlice }; +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 }; @@ -82,6 +84,12 @@ fn outSlice(s: []const u8) PantoSlice { 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)); } @@ -91,9 +99,22 @@ fn cloneSessionInfo(info: PantoSessionInfo) !panto.SessionInfo { 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 }; } +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) { @@ -152,13 +173,56 @@ export fn panto_session_store_create(ctx: *anyopaque, vt: *const PantoSessionSto 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); } + 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_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}); @@ -171,13 +235,13 @@ export fn panto_session_load(session: *PantoSession, out: **PantoConversation) P 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 } }; + 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 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 } }, + .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 { @@ -202,12 +266,39 @@ fn freeProvider(pv: *panto.ProviderConfig) void { } } -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 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; @@ -220,9 +311,26 @@ fn cStoreListZ(ctx: *anyopaque) anyerror![]panto.SessionInfo { } 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 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) { @@ -234,6 +342,76 @@ fn cStoreCreateZ(ctx: *anyopaque) panto.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 = &.{}, .metadata = if (m.has_metadata) slice(m.metadata) else null }, .usage = if (m.has_usage) m.usage else null }; + 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: { @@ -280,19 +458,19 @@ export fn panto_agent_set_config(a: *PantoAgent, c: *const PantoConfig) PantoSta freeConfig(old); return .ok; } -export fn panto_agent_add_system_message(a: *PantoAgent, text: PantoSlice) PantoStatus { +export fn panto_agent_add_system_message(a: *PantoAgent, text: ?[*:0]const u8) PantoStatus { const h: *AgentHandle = @ptrCast(@alignCast(a)); - h.agent.addSystemMessage(slice(text)) catch |e| return setErr("{t}", .{e}); + h.agent.addSystemMessage(cstr(text)) catch |e| return setErr("{t}", .{e}); return .ok; } -export fn panto_agent_set_system_prompt(a: *PantoAgent, text: PantoSlice) PantoStatus { +export fn panto_agent_set_system_prompt(a: *PantoAgent, text: ?[*:0]const u8) PantoStatus { const h: *AgentHandle = @ptrCast(@alignCast(a)); - h.agent.setSystemPrompt(slice(text)) catch |e| return setErr("{t}", .{e}); + h.agent.setSystemPrompt(cstr(text)) catch |e| return setErr("{t}", .{e}); return .ok; } -export fn panto_agent_run(a: *PantoAgent, text: PantoSlice, out: **PantoStream) PantoStatus { +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 = slice(text) }) catch |e| return setErr("{t}", .{e}); + const st = h.agent.run(.{ .text = cstr(text) }) catch |e| return setErr("{t}", .{e}); out.* = @ptrCast(st); return .ok; } -- cgit v1.3