summaryrefslogtreecommitdiff
path: root/libpanto-c/src
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-11 09:08:42 -0600
committert <t@tjp.lol>2026-06-11 09:23:28 -0600
commite2477f7d2d2a4eab870a2bd61534cfd146907915 (patch)
tree197dae8b7476558d7f105d3129778f30b91503e1 /libpanto-c/src
parent813ebd014a7614fdcdf4df5570b50944f2626076 (diff)
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
Diffstat (limited to 'libpanto-c/src')
-rw-r--r--libpanto-c/src/lib.zig238
1 files changed, 208 insertions, 30 deletions
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;
}