summaryrefslogtreecommitdiff
path: root/libpanto-c
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto-c')
-rw-r--r--libpanto-c/build.zig33
-rw-r--r--libpanto-c/build.zig.zon9
-rw-r--r--libpanto-c/include/panto.h223
-rw-r--r--libpanto-c/src/lib.zig1090
4 files changed, 0 insertions, 1355 deletions
diff --git a/libpanto-c/build.zig b/libpanto-c/build.zig
deleted file mode 100644
index 2e3160c..0000000
--- a/libpanto-c/build.zig
+++ /dev/null
@@ -1,33 +0,0 @@
-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 linkage = b.option(std.builtin.LinkMode, "linkage", "Library linkage: dynamic (default) or static") orelse .dynamic;
- const lib = b.addLibrary(.{ .name = "panto", .root_module = mod, .linkage = linkage });
- if (linkage == .dynamic) {
- lib.rdynamic = true;
- } else {
- // Bundle compiler_rt into the archive so the final consumer link
- // (e.g. cgo) doesn't need Zig's runtime support library separately.
- lib.bundle_compiler_rt = 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
deleted file mode 100644
index 13966ed..0000000
--- a/libpanto-c/build.zig.zon
+++ /dev/null
@@ -1,9 +0,0 @@
-.{
- .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
deleted file mode 100644
index 4302bb8..0000000
--- a/libpanto-c/include/panto.h
+++ /dev/null
@@ -1,223 +0,0 @@
-#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, PANTO_OPENAI_RESPONSES = 2, PANTO_OPENAI_CODEX_RESPONSES = 3 } 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_RESULT, 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 { const char *api_key, *base_url, *model; PantoReasoningEffort reasoning; uint32_t max_tokens; } PantoOpenAIChatConfig;
-typedef struct { const char *api_key, *base_url, *model; PantoReasoningEffort reasoning; uint32_t max_tokens; } PantoOpenAIResponsesConfig;
-typedef struct { const char *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; PantoOpenAIResponsesConfig openai_responses; } data; } PantoProviderConfig;
-typedef struct { uint32_t keep_verbatim; bool has_model; PantoProviderConfig model; bool has_compaction_prompt; const char *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;
-/* One assistant message finished streaming. `message_index` indexes the
- * agent's live conversation (`panto_agent_conversation`), whose tail is this
- * message when the event is delivered; fetch its `block_count` blocks from
- * there for full content. `usage` is the wire-reported token usage. */
-typedef struct { PantoMessageRole role; size_t message_index; size_t block_count; bool has_usage; PantoUsage usage; } PantoMessageComplete;
-typedef struct { bool compacted; size_t kept_turns; size_t summarized_messages; } PantoCompactionResult;
-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_result; PantoToolDispatchComplete tool_dispatch_complete; } data; } PantoEvent;
-
-/* One content block of a message being appended. A flattened, borrow-only
- * view: every slice points into the in-memory message and is valid only for
- * the duration of the append_messages call. `text` carries the Text body,
- * Thinking text, ToolUse input JSON, concatenated ToolResult text, or
- * System/CompactionSummary text depending on `tag`. `tool_id` is the ToolUse
- * id or ToolResult tool_use_id; `tool_name` is the ToolUse name; `is_error`
- * is the ToolResult error flag; `system_mode` is the System block mode;
- * `thinking_signature` is the Thinking signature (empty when absent), and the
- * `thinking_signature_*` fields record the provider/style/model that emitted
- * it. To replay a thinking block faithfully (Anthropic drops thinking blocks
- * whose signature lacks a matching origin) a store must persist the signature
- * AND all three origin fields, then feed them back via the message builder. */
-typedef struct {
- PantoContentBlockTag tag;
- PantoSlice text;
- PantoSlice tool_id;
- PantoSlice tool_name;
- bool is_error;
- PantoSystemMode system_mode;
- PantoSlice thinking_signature;
- PantoAPIStyle thinking_signature_api_style;
- PantoSlice thinking_signature_base_url;
- PantoSlice thinking_signature_model;
-} PantoOwnedContentBlock;
-
-/* `api_style`/`base_url`/`model`/`reasoning` are the per-message wire identity
- * (the provider that produced this message), which can drift within a single
- * conversation (compaction on a different model, a mid-chat model switch). A
- * store should persist these per message rather than from static config. */
-typedef struct { PantoMessageRole role; bool has_usage; PantoUsage usage; bool has_metadata; PantoSlice metadata; PantoAPIStyle api_style; PantoSlice base_url; PantoSlice model; PantoReasoningEffort reasoning; const PantoOwnedContentBlock *content; size_t content_len; } 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 size_t panto_conversation_message_count(PantoConversation *conversation);
-PANTO_EXPORT PantoMessageRole panto_conversation_message_role(PantoConversation *conversation, size_t index);
-PANTO_EXPORT bool panto_conversation_message_has_usage(PantoConversation *conversation, size_t index);
-PANTO_EXPORT PantoUsage panto_conversation_message_usage(PantoConversation *conversation, size_t index);
-PANTO_EXPORT size_t panto_conversation_message_block_count(PantoConversation *conversation, size_t message_index);
-PANTO_EXPORT PantoContentBlockTag panto_conversation_block_tag(PantoConversation *conversation, size_t message_index, size_t block_index);
-PANTO_EXPORT PantoSlice panto_conversation_block_text(PantoConversation *conversation, size_t message_index, size_t block_index);
-PANTO_EXPORT PantoSlice panto_conversation_block_tool_id(PantoConversation *conversation, size_t message_index, size_t block_index);
-PANTO_EXPORT PantoSlice panto_conversation_block_tool_name(PantoConversation *conversation, size_t message_index, size_t block_index);
-PANTO_EXPORT bool panto_conversation_block_tool_result_is_error(PantoConversation *conversation, size_t message_index, size_t block_index);
-PANTO_EXPORT PantoSystemMode panto_conversation_block_system_mode(PantoConversation *conversation, size_t message_index, size_t block_index);
-PANTO_EXPORT PantoStatus panto_conversation_add_system_message(PantoConversation *conversation, const char *text);
-PANTO_EXPORT PantoStatus panto_conversation_replace_system_message(PantoConversation *conversation, const char *text);
-PANTO_EXPORT PantoStatus panto_conversation_add_user_text(PantoConversation *conversation, const char *text);
-PANTO_EXPORT PantoStatus panto_conversation_add_assistant_text(PantoConversation *conversation, const char *text, bool has_usage, PantoUsage usage);
-PANTO_EXPORT PantoStatus panto_conversation_add_compaction_summary(PantoConversation *conversation, const char *text);
-
-/* Rich, block-level message builder. Accumulate content blocks, then commit
- * the whole message to a conversation in one call — the path for rebuilding a
- * persisted conversation that contains tool calls or thinking. The builder
- * uses the same allocator as the conversation it is committed to.
- *
- * Usage: create -> add_* (one per block, in order) -> conversation_add_message.
- * `panto_conversation_add_message` ALWAYS consumes the builder (it is freed on
- * both success and failure); do not destroy it afterwards. Call
- * `panto_message_builder_destroy` only to discard a builder you never commit.
- * Each `text`/`signature`/etc. argument is copied. */
-typedef struct PantoMessageBuilder PantoMessageBuilder;
-PANTO_EXPORT PantoMessageBuilder *panto_message_builder_create(PantoMessageRole role);
-PANTO_EXPORT void panto_message_builder_destroy(PantoMessageBuilder *builder);
-PANTO_EXPORT PantoStatus panto_message_builder_add_text(PantoMessageBuilder *builder, const char *text);
-/* `signature` may be NULL/empty when the provider emitted no signature, in
- * which case the `signature_*` origin args are ignored. When a signature is
- * supplied you may pass the origin (style/base_url/model) it was produced
- * under so the block can be replayed; an Anthropic request drops thinking
- * blocks whose signature origin does not match the request's base_url/model.
- * The origin args are OPTIONAL: pass PANTO_API_STYLE 0 and NULL base_url/model
- * to omit them and instead rely on the enclosing message's identity (see
- * `panto_message_builder_set_identity`), which is the preferred, per-message
- * source of truth — no per-block identity copy required. */
-PANTO_EXPORT PantoStatus panto_message_builder_add_thinking(PantoMessageBuilder *builder, const char *text, const char *signature, PantoAPIStyle signature_api_style, const char *signature_base_url, const char *signature_model);
-PANTO_EXPORT PantoStatus panto_message_builder_add_tool_use(PantoMessageBuilder *builder, const char *id, const char *name, const char *input);
-PANTO_EXPORT PantoStatus panto_message_builder_add_tool_result(PantoMessageBuilder *builder, const char *tool_use_id, const char *text, bool is_error);
-PANTO_EXPORT PantoStatus panto_message_builder_add_system(PantoMessageBuilder *builder, const char *text, PantoSystemMode mode);
-/* Attach the wire identity (provider/style/model + reasoning) that produced
- * this message. This is the per-message source of truth for replaying thinking
- * signatures and is preserved through compaction, so a kept-verbatim turn is
- * not re-stamped with the compaction model. Strings are copied. Optional; when
- * omitted, thinking replay relies on each block's own signature origin. */
-PANTO_EXPORT PantoStatus panto_message_builder_set_identity(PantoMessageBuilder *builder, PantoAPIStyle api_style, const char *base_url, const char *model, PantoReasoningEffort reasoning);
-/* Commit the builder's blocks as one message of the builder's role. Consumes
- * (frees) the builder. `usage` applies to assistant turns; pass has_usage=false
- * otherwise. */
-PANTO_EXPORT PantoStatus panto_conversation_add_message(PantoConversation *conversation, PantoMessageBuilder *builder, bool has_usage, PantoUsage usage);
-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_null_store_create(PantoSessionStore **out);
-PANTO_EXPORT PantoStatus panto_file_system_jsonl_store_create(const char *dir, const char *metadata, PantoSessionStore **out);
-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, const char *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 PantoConversation *panto_agent_conversation(PantoAgent *agent);
-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, const char *text);
-PANTO_EXPORT PantoStatus panto_agent_set_system_prompt(PantoAgent *agent, const char *text);
-PANTO_EXPORT PantoStatus panto_agent_compact(PantoAgent *agent, const char *override_system_prompt, const char *extra_instructions, PantoCompactionResult *out);
-/* Open a turn from a user-role message built with the PantoMessageBuilder API
- * (the same builder used to rebuild persisted messages): one `add_text` block
- * for a plain chat turn, or `add_tool_result` blocks to resume after the
- * embedder handled tool calls itself. Consumes `user_message` (the builder is
- * destroyed); do not reuse or destroy it afterwards. */
-PANTO_EXPORT PantoStatus panto_agent_run(PantoAgent *agent, PantoMessageBuilder *user_message, PantoStream **out);
-/* Reset a failed stream back to its turn-open boundary so the caller can
- * resume panto_stream_next() after changing the agent config (e.g. catch a
- * terminal bad-request, swap the provider config, and retry the SAME turn
- * without re-appending the user message). Errors if the stream has not
- * failed. */
-PANTO_EXPORT PantoStatus panto_stream_reopen(PantoStream *stream);
-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);
-
-/* Go/callback-oriented tool ABI. */
-typedef struct { PantoSlice name, description, schema_json; } PantoToolDecl;
-typedef struct { PantoSlice media_type; bool has_media_type; PantoSlice data; } PantoMediaPart;
-typedef enum { PANTO_RESULT_TEXT = 0, PANTO_RESULT_MEDIA = 1 } PantoResultPartTag;
-typedef struct { PantoResultPartTag tag; union { PantoSlice text; PantoMediaPart media; } data; } PantoResultPart;
-typedef struct { PantoResultPart *ptr; size_t len; } PantoResultParts;
-typedef struct { PantoSlice tool_name, input; } PantoToolCall;
-typedef struct { bool ok; PantoResultParts parts; PantoSlice error; } PantoToolCallResult;
-typedef PantoStatus (*PantoToolInvokeFn)(void *ctx, PantoSlice input, PantoResultParts *out);
-typedef PantoStatus (*PantoToolSourceInvokeBatchFn)(void *ctx, const PantoToolCall *calls, PantoToolCallResult *results, size_t len);
-typedef void (*PantoToolDestroyFn)(void *ctx);
-PANTO_EXPORT PantoStatus panto_agent_register_tool(PantoAgent *agent, PantoToolDecl decl, void *ctx, PantoToolInvokeFn invoke, PantoToolDestroyFn destroy);
-PANTO_EXPORT PantoStatus panto_agent_register_tool_source(PantoAgent *agent, PantoSlice name, const PantoToolDecl *decls, size_t len, void *ctx, PantoToolSourceInvokeBatchFn invoke_batch, PantoToolDestroyFn destroy);
-PANTO_EXPORT PantoResultParts panto_result_parts_text(const char *text);
-PANTO_EXPORT void panto_result_parts_destroy(PantoResultParts parts);
-#ifdef __cplusplus
-}
-#endif
-#endif
diff --git a/libpanto-c/src/lib.zig b/libpanto-c/src/lib.zig
deleted file mode 100644
index 4fcf94b..0000000
--- a/libpanto-c/src/lib.zig
+++ /dev/null
@@ -1,1090 +0,0 @@
-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 PantoMessageBuilder = 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 };
-const MessageBuilder = struct { role: panto.MessageRole, blocks: std.ArrayList(panto.ContentBlock) = .empty, identity: ?panto.WireIdentity = null };
-
-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, openai_responses = 2, openai_codex_responses = 3 };
-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_result, 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 PantoOpenAIResponsesConfig = 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, openai_responses: PantoOpenAIResponsesConfig } };
-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 { role: PantoMessageRole, message_index: usize, block_count: usize, 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_result: PantoToolDispatchComplete, tool_dispatch_complete: PantoToolDispatchComplete } };
-pub const PantoOwnedContentBlock = extern struct { tag: PantoContentBlockTag, text: PantoSlice, tool_id: PantoSlice, tool_name: PantoSlice, is_error: bool, system_mode: PantoSystemMode, thinking_signature: PantoSlice, thinking_signature_api_style: PantoAPIStyle, thinking_signature_base_url: PantoSlice, thinking_signature_model: PantoSlice };
-pub const PantoPersistentMessage = extern struct { role: PantoMessageRole, has_usage: bool, usage: PantoUsage, has_metadata: bool, metadata: PantoSlice, api_style: PantoAPIStyle, base_url: PantoSlice, model: PantoSlice, reasoning: PantoReasoningEffort, content: ?[*]const PantoOwnedContentBlock, content_len: usize };
-pub const PantoSessionStoreVTable = extern struct {
- create: *const fn (*anyopaque, **PantoSession) callconv(.c) PantoStatus,
- list: *const fn (*anyopaque, *PantoSessionInfoList) callconv(.c) PantoStatus,
- 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;
-}
-fn unwrapBuilder(b: *PantoMessageBuilder) *MessageBuilder {
- return @ptrCast(@alignCast(b));
-}
-/// Take ownership of `block` into the builder, freeing it on OOM.
-fn pushBlock(b: *MessageBuilder, block: panto.ContentBlock) PantoStatus {
- b.blocks.append(allocator, block) catch |e| {
- var blk = block;
- blk.deinit(allocator);
- return setErr("{t}", .{e});
- };
- return .ok;
-}
-export fn panto_message_builder_create(role: PantoMessageRole) ?*PantoMessageBuilder {
- const b = allocator.create(MessageBuilder) catch {
- _ = setErr("OutOfMemory", .{});
- return null;
- };
- b.* = .{ .role = @enumFromInt(@intFromEnum(role)) };
- return @ptrCast(b);
-}
-export fn panto_message_builder_destroy(builder: ?*PantoMessageBuilder) void {
- if (builder) |ptr| {
- const b = unwrapBuilder(ptr);
- for (b.blocks.items) |*blk| blk.deinit(allocator);
- b.blocks.deinit(allocator);
- if (b.identity) |id| panto.freeWireIdentity(allocator, id);
- allocator.destroy(b);
- }
-}
-/// Attach the per-message wire identity that produced this message. Used when
-/// rebuilding a conversation for resumption so the identity survives a later
-/// compaction (and so a thinking signature replays to the right model without
-/// a per-block identity copy). The strings are copied.
-export fn panto_message_builder_set_identity(builder: *PantoMessageBuilder, api_style: PantoAPIStyle, base_url: ?[*:0]const u8, model: ?[*:0]const u8, reasoning: PantoReasoningEffort) PantoStatus {
- const b = unwrapBuilder(builder);
- const id = panto.dupeWireIdentity(allocator, .{
- .api_style = @enumFromInt(@intFromEnum(api_style)),
- .base_url = cstr(base_url),
- .model = cstr(model),
- .reasoning = @enumFromInt(@intFromEnum(reasoning)),
- }) catch |e| return setErr("{t}", .{e});
- if (b.identity) |old| panto.freeWireIdentity(allocator, old);
- b.identity = id;
- return .ok;
-}
-export fn panto_message_builder_add_text(builder: *PantoMessageBuilder, text: ?[*:0]const u8) PantoStatus {
- const b = unwrapBuilder(builder);
- const tb = panto.textualBlockFromSlice(allocator, cstr(text)) catch |e| return setErr("{t}", .{e});
- return pushBlock(b, .{ .Text = tb });
-}
-export fn panto_message_builder_add_thinking(builder: *PantoMessageBuilder, text: ?[*:0]const u8, signature: ?[*:0]const u8, signature_api_style: PantoAPIStyle, signature_base_url: ?[*:0]const u8, signature_model: ?[*:0]const u8) PantoStatus {
- const b = unwrapBuilder(builder);
- var tb = panto.textualBlockFromSlice(allocator, cstr(text)) catch |e| return setErr("{t}", .{e});
- const sig: ?[]const u8 = if (cstr(signature).len != 0) (allocator.dupe(u8, cstr(signature)) catch |e| {
- tb.deinit(allocator);
- return setErr("{t}", .{e});
- }) else null;
- var origin: ?panto.SignatureOrigin = null;
- if (sig != null and cstr(signature_base_url).len != 0) {
- origin = panto.SignatureOrigin.init(allocator, @enumFromInt(@intFromEnum(signature_api_style)), cstr(signature_base_url), cstr(signature_model)) catch |e| {
- tb.deinit(allocator);
- if (sig) |s| allocator.free(s);
- return setErr("{t}", .{e});
- };
- }
- return pushBlock(b, .{ .Thinking = .{ .text = tb, .signature = sig, .signature_origin = origin } });
-}
-export fn panto_message_builder_add_tool_use(builder: *PantoMessageBuilder, id: ?[*:0]const u8, name: ?[*:0]const u8, input: ?[*:0]const u8) PantoStatus {
- const b = unwrapBuilder(builder);
- const id_d = allocator.dupe(u8, cstr(id)) catch |e| return setErr("{t}", .{e});
- const name_d = allocator.dupe(u8, cstr(name)) catch |e| {
- allocator.free(id_d);
- return setErr("{t}", .{e});
- };
- const input_tb = panto.textualBlockFromSlice(allocator, cstr(input)) catch |e| {
- allocator.free(id_d);
- allocator.free(name_d);
- return setErr("{t}", .{e});
- };
- return pushBlock(b, .{ .ToolUse = .{ .id = id_d, .name = name_d, .input = input_tb } });
-}
-export fn panto_message_builder_add_tool_result(builder: *PantoMessageBuilder, tool_use_id: ?[*:0]const u8, text: ?[*:0]const u8, is_error: bool) PantoStatus {
- const b = unwrapBuilder(builder);
- const id_d = allocator.dupe(u8, cstr(tool_use_id)) catch |e| return setErr("{t}", .{e});
- var tb = panto.textualBlockFromSlice(allocator, cstr(text)) catch |e| {
- allocator.free(id_d);
- return setErr("{t}", .{e});
- };
- var parts: std.ArrayList(panto.ResultPartStored) = .empty;
- parts.append(allocator, .{ .text = tb }) catch |e| {
- allocator.free(id_d);
- tb.deinit(allocator);
- return setErr("{t}", .{e});
- };
- return pushBlock(b, .{ .ToolResult = .{ .tool_use_id = id_d, .parts = parts, .is_error = is_error } });
-}
-export fn panto_message_builder_add_system(builder: *PantoMessageBuilder, text: ?[*:0]const u8, mode: PantoSystemMode) PantoStatus {
- const b = unwrapBuilder(builder);
- const tb = panto.textualBlockFromSlice(allocator, cstr(text)) catch |e| return setErr("{t}", .{e});
- return pushBlock(b, .{ .System = .{ .text = tb, .mode = @enumFromInt(@intFromEnum(mode)) } });
-}
-export fn panto_conversation_add_message(c: *PantoConversation, builder: *PantoMessageBuilder, has_usage: bool, u: PantoUsage) PantoStatus {
- const b = unwrapBuilder(builder);
- const cc = unwrapConversation(c);
- // Consume the builder regardless of outcome. addMessage takes ownership of
- // each block's bytes on success, so we only ever free the list backing
- // array + the builder shell here — never the per-block bytes (that would
- // double-free what the conversation now owns).
- defer {
- b.blocks.deinit(allocator);
- if (b.identity) |id| panto.freeWireIdentity(allocator, id);
- allocator.destroy(b);
- }
- cc.addMessage(b.role, b.blocks.items, if (has_usage) zigUsage(u) else null) catch |e| return setErr("{t}", .{e});
- // Transfer the builder's identity onto the freshly appended message. The
- // `defer` frees the builder's copy; null it out so ownership moves cleanly.
- if (b.identity) |id| {
- cc.messages.items[cc.messages.items.len - 1].identity = id;
- b.identity = null;
- }
- 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 } },
- .openai_responses => .{ .openai_responses = .{ .api_key = try dupCStr(p.data.openai_responses.api_key), .base_url = try dupCStr(p.data.openai_responses.base_url), .model = try dupCStr(p.data.openai_responses.model), .reasoning = @enumFromInt(@intFromEnum(p.data.openai_responses.reasoning)), .max_tokens = p.data.openai_responses.max_tokens } },
- .openai_codex_responses => return error.UnsupportedProvider,
- };
-}
-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);
- },
- .openai_responses, .openai_codex_responses => |c| {
- allocator.free(c.api_key);
- allocator.free(c.base_url);
- allocator.free(c.model);
- },
- }
-}
-
-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;
-}
-const empty_slice: PantoSlice = .{ .ptr = null, .len = 0 };
-/// Flatten an in-memory content block into the borrow-only C view. Slices
-/// alias the message's buffers except for ToolResult text, which is
-/// concatenated into `aa` (valid until the caller frees that allocator).
-fn ownedBlock(aa: std.mem.Allocator, b: *const panto.ContentBlock) !PantoOwnedContentBlock {
- var out: PantoOwnedContentBlock = .{ .tag = @enumFromInt(@intFromEnum(b.*)), .text = empty_slice, .tool_id = empty_slice, .tool_name = empty_slice, .is_error = false, .system_mode = .append, .thinking_signature = empty_slice, .thinking_signature_api_style = .openai_chat, .thinking_signature_base_url = empty_slice, .thinking_signature_model = empty_slice };
- switch (b.*) {
- .Text => |t| out.text = outSlice(t.items),
- .Thinking => |t| {
- out.text = outSlice(t.text.items);
- if (t.signature) |s| out.thinking_signature = outSlice(s);
- if (t.signature_origin) |o| {
- out.thinking_signature_api_style = @enumFromInt(@intFromEnum(o.api_style));
- out.thinking_signature_base_url = outSlice(o.base_url);
- out.thinking_signature_model = outSlice(o.model);
- }
- },
- .ToolUse => |t| {
- out.text = outSlice(t.input.items);
- out.tool_id = outSlice(t.id);
- out.tool_name = outSlice(t.name);
- },
- .ToolResult => |tr| {
- var buf: panto.TextualBlock = .empty;
- try tr.appendTextInto(aa, &buf);
- out.text = outSlice(buf.items);
- out.tool_id = outSlice(tr.tool_use_id);
- out.is_error = tr.is_error;
- },
- .System => |s| {
- out.text = outSlice(s.text.items);
- out.system_mode = @enumFromInt(@intFromEnum(s.mode));
- },
- .CompactionSummary => |s| out.text = outSlice(s.text.items),
- }
- return out;
-}
-fn cStoreAppend(ctx: *anyopaque, session_id: []const u8, messages: []panto.PersistentMessage) anyerror!void {
- const h: *StoreHandle = @ptrCast(@alignCast(ctx));
- const cmsgs = try allocator.alloc(PantoPersistentMessage, messages.len);
- defer allocator.free(cmsgs);
- var arena = std.heap.ArenaAllocator.init(allocator);
- defer arena.deinit();
- const aa = arena.allocator();
- for (messages, 0..) |m, i| {
- const blocks = m.message.content.items;
- const cblocks = try aa.alloc(PantoOwnedContentBlock, blocks.len);
- for (blocks, 0..) |*b, j| cblocks[j] = try ownedBlock(aa, b);
- cmsgs[i] = .{ .role = @enumFromInt(@intFromEnum(m.message.role)), .has_usage = m.usage != null, .usage = if (m.usage) |u| usage(u) else .{ .input = 0, .output = 0, .cache_read = 0, .cache_write = 0, .reasoning = 0 }, .has_metadata = m.message.metadata != null, .metadata = outSlice(m.message.metadata orelse ""), .api_style = @enumFromInt(@intFromEnum(m.identity.api_style)), .base_url = outSlice(m.identity.base_url), .model = outSlice(m.identity.model), .reasoning = @enumFromInt(@intFromEnum(m.identity.reasoning)), .content = if (cblocks.len == 0) null else cblocks.ptr, .content_len = cblocks.len };
- }
- if (h.vtable.append_messages(h.ctx, outSlice(session_id), cmsgs.ptr, cmsgs.len) != .ok) return error.CStoreAppendFailed;
-}
-fn cStoreListZ(ctx: *anyopaque) anyerror![]panto.SessionInfo {
- 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;
-}
-/// Rebuild an in-memory content block from its borrow-only C view. The
-/// inverse of `ownedBlock`; allocates into `aa` (a transient arena freed by
-/// the caller after the store has copied what it keeps).
-fn zigBlock(aa: std.mem.Allocator, ob: PantoOwnedContentBlock) !panto.ContentBlock {
- return switch (ob.tag) {
- .Text => .{ .Text = try panto.textualBlockFromSlice(aa, slice(ob.text)) },
- .Thinking => .{ .Thinking = .{ .text = try panto.textualBlockFromSlice(aa, slice(ob.text)), .signature = if (ob.thinking_signature.len != 0) try aa.dupe(u8, slice(ob.thinking_signature)) else null, .signature_origin = if (ob.thinking_signature.len != 0 and ob.thinking_signature_base_url.len != 0) try panto.SignatureOrigin.init(aa, @enumFromInt(@intFromEnum(ob.thinking_signature_api_style)), slice(ob.thinking_signature_base_url), slice(ob.thinking_signature_model)) else null } },
- .ToolUse => .{ .ToolUse = .{ .id = try aa.dupe(u8, slice(ob.tool_id)), .name = try aa.dupe(u8, slice(ob.tool_name)), .input = try panto.textualBlockFromSlice(aa, slice(ob.text)) } },
- .ToolResult => blk: {
- var parts: std.ArrayList(panto.ResultPartStored) = .empty;
- try parts.append(aa, .{ .text = try panto.textualBlockFromSlice(aa, slice(ob.text)) });
- break :blk .{ .ToolResult = .{ .tool_use_id = try aa.dupe(u8, slice(ob.tool_id)), .parts = parts, .is_error = ob.is_error } };
- },
- .System => .{ .System = .{ .text = try panto.textualBlockFromSlice(aa, slice(ob.text)), .mode = @enumFromInt(@intFromEnum(ob.system_mode)) } },
- .CompactionSummary => .{ .CompactionSummary = .{ .text = try panto.textualBlockFromSlice(aa, slice(ob.text)) } },
- };
-}
-fn builtinStoreAppend(ctx: *anyopaque, session_id: PantoSlice, messages: [*]const PantoPersistentMessage, len: usize) callconv(.c) PantoStatus {
- const s: *panto.SessionStore = @ptrCast(@alignCast(ctx));
- const pmsgs = allocator.alloc(panto.PersistentMessage, len) catch return setErr("OutOfMemory", .{});
- defer allocator.free(pmsgs);
- var arena = std.heap.ArenaAllocator.init(allocator);
- defer arena.deinit();
- const aa = arena.allocator();
- for (messages[0..len], 0..) |m, i| {
- var content: std.ArrayList(panto.ContentBlock) = .empty;
- if (m.content) |cptr| {
- content.ensureTotalCapacity(aa, m.content_len) catch return setErr("OutOfMemory", .{});
- for (cptr[0..m.content_len]) |ob| content.appendAssumeCapacity(zigBlock(aa, ob) catch |e| return setErr("{t}", .{e}));
- }
- pmsgs[i] = .{ .message = .{ .role = @enumFromInt(@intFromEnum(m.role)), .content = content, .metadata = if (m.has_metadata) slice(m.metadata) else null }, .usage = if (m.has_usage) zigUsage(m.usage) else null, .identity = .{ .api_style = @enumFromInt(@intFromEnum(m.api_style)), .base_url = slice(m.base_url), .model = slice(m.model), .reasoning = @enumFromInt(@intFromEnum(m.reasoning)) } };
- }
- s.appendMessages(slice(session_id), pmsgs) catch |e| return setErr("{t}", .{e});
- return .ok;
-}
-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;
-}
-/// Open a turn from a user-role message built with the `PantoMessageBuilder`
-/// API — the same builder used to rebuild persisted messages. A plain chat
-/// turn is one `add_text` block; a turn that resumes after the embedder handled
-/// tool calls is one or more `add_tool_result` blocks. Consumes the builder
-/// (success or failure): `Agent.run` adopts the block bytes, so only the
-/// builder's list backing + shell are freed here, never the per-block bytes.
-export fn panto_agent_run(a: *PantoAgent, user_message: *PantoMessageBuilder, out: **PantoStream) PantoStatus {
- const h: *AgentHandle = @ptrCast(@alignCast(a));
- const b = unwrapBuilder(user_message);
- defer {
- b.blocks.deinit(allocator);
- if (b.identity) |id| panto.freeWireIdentity(allocator, id);
- allocator.destroy(b);
- }
- const st = h.agent.run(.{ .blocks = b.blocks.items }) 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});
- // The just-completed assistant message is the conversation's current
- // tail (no further message is committed within this `next()` call), so
- // its index is `len - 1`. Stamp it so the embedder can fetch the full
- // message via `panto_agent_conversation` rather than marshalling its
- // blocks onto every event.
- if (out.tag == .message_complete) {
- out.data.message_complete.message_index = st._agent.conversation.messages.items.len - 1;
- }
- 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 = .{ .role = @enumFromInt(@intFromEnum(m.message.role)), .message_index = 0, .block_count = m.message.content.items.len, .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_result => .{ .tag = .tool_dispatch_result, .data = .{ .tool_dispatch_result = .{ .message_index = 0 } } },
- .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);
-}
-
-var test_append_ok: bool = false;
-fn testCaptureAppend(_: *anyopaque, _: PantoSlice, messages: [*]const PantoPersistentMessage, len: usize) callconv(.c) PantoStatus {
- test_append_ok = blk: {
- if (len != 1) break :blk false;
- const m = messages[0];
- if (m.role != .assistant) break :blk false;
- const p = m.content orelse break :blk false;
- if (m.content_len != 3) break :blk false;
- const b = p[0..3];
- if (b[0].tag != .ToolUse) break :blk false;
- if (!std.mem.eql(u8, slice(b[0].tool_id), "call-1")) break :blk false;
- if (!std.mem.eql(u8, slice(b[0].tool_name), "search")) break :blk false;
- if (!std.mem.eql(u8, slice(b[0].text), "{\"q\":1}")) break :blk false;
- if (b[1].tag != .Thinking) break :blk false;
- if (!std.mem.eql(u8, slice(b[1].text), "hmm")) break :blk false;
- if (!std.mem.eql(u8, slice(b[1].thinking_signature), "sig")) break :blk false;
- if (b[1].thinking_signature_api_style != .anthropic_messages) break :blk false;
- if (!std.mem.eql(u8, slice(b[1].thinking_signature_base_url), "https://api")) break :blk false;
- if (!std.mem.eql(u8, slice(b[1].thinking_signature_model), "claude")) break :blk false;
- if (b[2].tag != .ToolResult) break :blk false;
- if (!b[2].is_error) break :blk false;
- if (!std.mem.eql(u8, slice(b[2].tool_id), "call-1")) break :blk false;
- if (!std.mem.eql(u8, slice(b[2].text), "boom")) break :blk false;
- if (m.api_style != .anthropic_messages) break :blk false;
- if (!std.mem.eql(u8, slice(m.base_url), "https://api")) break :blk false;
- if (!std.mem.eql(u8, slice(m.model), "claude")) break :blk false;
- if (m.reasoning != .high) break :blk false;
- break :blk true;
- };
- return .ok;
-}
-
-test "cStoreAppend marshals content blocks (incl. thinking origin)" {
- var content: std.ArrayList(panto.ContentBlock) = .empty;
- defer {
- for (content.items) |*b| b.deinit(allocator);
- content.deinit(allocator);
- }
- try content.append(allocator, .{ .ToolUse = .{ .id = try allocator.dupe(u8, "call-1"), .name = try allocator.dupe(u8, "search"), .input = try panto.textualBlockFromSlice(allocator, "{\"q\":1}") } });
- try content.append(allocator, .{ .Thinking = .{ .text = try panto.textualBlockFromSlice(allocator, "hmm"), .signature = try allocator.dupe(u8, "sig"), .signature_origin = try panto.SignatureOrigin.init(allocator, .anthropic_messages, "https://api", "claude") } });
- var parts: std.ArrayList(panto.ResultPartStored) = .empty;
- try parts.append(allocator, .{ .text = try panto.textualBlockFromSlice(allocator, "boom") });
- try content.append(allocator, .{ .ToolResult = .{ .tool_use_id = try allocator.dupe(u8, "call-1"), .parts = parts, .is_error = true } });
-
- var msgs = [_]panto.PersistentMessage{.{ .message = .{ .role = .assistant, .content = content }, .identity = .{ .api_style = .anthropic_messages, .base_url = "https://api", .model = "claude", .reasoning = .high } }};
-
- var vt: PantoSessionStoreVTable = .{ .create = builtinStoreCreate, .list = builtinStoreList, .free_session_infos = builtinStoreFreeInfos, .resolve = builtinStoreResolve, .latest = builtinStoreLatest, .load = builtinStoreLoad, .append_messages = testCaptureAppend, .destroy = null };
- var dummy: u8 = 0;
- var handle: StoreHandle = .{ .ctx = &dummy, .vtable = &vt };
-
- test_append_ok = false;
- try cStoreAppend(@ptrCast(&handle), "sess-1", &msgs);
- try std.testing.expect(test_append_ok);
-}
-
-test "zigBlock round-trips an owned thinking block" {
- var arena = std.heap.ArenaAllocator.init(allocator);
- defer arena.deinit();
- const aa = arena.allocator();
- const ob: PantoOwnedContentBlock = .{ .tag = .Thinking, .text = outSlice("reasoning"), .tool_id = empty_slice, .tool_name = empty_slice, .is_error = false, .system_mode = .append, .thinking_signature = outSlice("sig"), .thinking_signature_api_style = .anthropic_messages, .thinking_signature_base_url = outSlice("https://api"), .thinking_signature_model = outSlice("claude") };
- const block = try zigBlock(aa, ob);
- try std.testing.expect(block == .Thinking);
- try std.testing.expectEqualStrings("reasoning", block.Thinking.text.items);
- try std.testing.expectEqualStrings("sig", block.Thinking.signature.?);
- const origin = block.Thinking.signature_origin.?;
- try std.testing.expect(origin.api_style == .anthropic_messages);
- try std.testing.expectEqualStrings("https://api", origin.base_url);
- try std.testing.expectEqualStrings("claude", origin.model);
-}
-
-test "message builder set_identity attaches per-message identity" {
- var conv: *PantoConversation = undefined;
- try std.testing.expectEqual(PantoStatus.ok, panto_conversation_create(&conv));
- defer panto_conversation_destroy(conv);
-
- const b = panto_message_builder_create(.assistant).?;
- try std.testing.expectEqual(PantoStatus.ok, panto_message_builder_add_thinking(b, "reasoned", "sig", .openai_chat, null, null));
- try std.testing.expectEqual(PantoStatus.ok, panto_message_builder_set_identity(b, .anthropic_messages, "https://api.anthropic.com", "claude", .default));
- try std.testing.expectEqual(PantoStatus.ok, panto_conversation_add_message(conv, b, false, undefined));
-
- const cc = unwrapConversation(conv);
- const id = cc.messages.items[0].identity.?;
- try std.testing.expect(id.api_style == .anthropic_messages);
- try std.testing.expectEqualStrings("https://api.anthropic.com", id.base_url);
- try std.testing.expectEqualStrings("claude", id.model);
- // No per-block origin was supplied; the message identity carries replay
- // provenance on its own.
- try std.testing.expect(cc.messages.items[0].content.items[0].Thinking.signature_origin == null);
-}
-
-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]);
- }
-}