summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-11 11:15:40 -0600
committert <t@tjp.lol>2026-06-11 11:15:52 -0600
commit27e37c62bc1483c0489d6db01344906472a1f8d3 (patch)
tree0c80d2c9e1fd1020e56d34d389d9c743085c2a50
parente2477f7d2d2a4eab870a2bd61534cfd146907915 (diff)
libpanto-go
some libpanto-c coverage expansion was needed
-rw-r--r--libpanto-c/include/panto.h19
-rw-r--r--libpanto-c/src/lib.zig110
-rw-r--r--libpanto-go/README.md48
-rw-r--r--libpanto-go/go.mod3
-rw-r--r--libpanto-go/panto.go709
5 files changed, 888 insertions, 1 deletions
diff --git a/libpanto-c/include/panto.h b/libpanto-c/include/panto.h
index 4f69465..0772e6e 100644
--- a/libpanto-c/include/panto.h
+++ b/libpanto-c/include/panto.h
@@ -51,6 +51,7 @@ typedef struct { size_t index; PantoSlice id, name; } PantoToolDetails;
typedef struct { size_t index; PantoSlice delta; } PantoContentDelta;
typedef struct { size_t index; PantoContentBlockTag block_type; } PantoBlockComplete;
typedef struct { bool has_usage; PantoUsage usage; } PantoMessageComplete;
+typedef struct { 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;
@@ -76,6 +77,22 @@ 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);
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);
@@ -90,11 +107,13 @@ PANTO_EXPORT PantoStatus panto_session_store_resolve(PantoSessionStore *store, c
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);
PANTO_EXPORT PantoStatus panto_agent_run(PantoAgent *agent, const char *user_text, PantoStream **out);
PANTO_EXPORT void panto_stream_destroy(PantoStream *stream);
PANTO_EXPORT PantoNextStatus panto_stream_next(PantoStream *stream, PantoEvent *out);
diff --git a/libpanto-c/src/lib.zig b/libpanto-c/src/lib.zig
index 046236a..81ce0ce 100644
--- a/libpanto-c/src/lib.zig
+++ b/libpanto-c/src/lib.zig
@@ -47,6 +47,7 @@ pub const PantoToolDetails = extern struct { index: usize, id: PantoSlice, name:
pub const PantoContentDelta = extern struct { index: usize, delta: PantoSlice };
pub const PantoBlockComplete = extern struct { index: usize, block_type: PantoContentBlockTag };
pub const PantoMessageComplete = extern struct { has_usage: bool, usage: PantoUsage };
+pub const PantoCompactionResult = extern struct { compacted: bool, kept_turns: usize, summarized_messages: usize };
pub const PantoProviderRetry = extern struct { attempt: usize, max_attempts: usize, delay_ms: u64, error_name: PantoSlice, has_status_code: bool, status_code: u16, has_retry_after_ms: bool, retry_after_ms: u64, message: PantoSlice, compaction: bool };
pub const PantoToolDispatchStart = extern struct { count: usize };
pub const PantoToolDispatchComplete = extern struct { message_index: usize };
@@ -147,6 +148,98 @@ export fn panto_conversation_destroy(c: ?*PantoConversation) void {
allocator.destroy(h);
}
}
+fn unwrapConversation(c: *PantoConversation) *panto.Conversation {
+ return &(@as(*ConversationHandle, @ptrCast(@alignCast(c))).conversation);
+}
+fn msgAt(c: *PantoConversation, index: usize) ?*panto.Message {
+ const cc = unwrapConversation(c);
+ if (index >= cc.messages.items.len) return null;
+ return &cc.messages.items[index];
+}
+fn blockAt(c: *PantoConversation, mi: usize, bi: usize) ?*panto.ContentBlock {
+ const m = msgAt(c, mi) orelse return null;
+ if (bi >= m.content.items.len) return null;
+ return &m.content.items[bi];
+}
+export fn panto_conversation_message_count(c: *PantoConversation) usize {
+ return unwrapConversation(c).messages.items.len;
+}
+export fn panto_conversation_message_role(c: *PantoConversation, index: usize) PantoMessageRole {
+ const m = msgAt(c, index) orelse return .user;
+ return @enumFromInt(@intFromEnum(m.role));
+}
+export fn panto_conversation_message_has_usage(c: *PantoConversation, index: usize) bool {
+ const m = msgAt(c, index) orelse return false;
+ return m.usage != null;
+}
+export fn panto_conversation_message_usage(c: *PantoConversation, index: usize) PantoUsage {
+ const m = msgAt(c, index) orelse return .{ .input = 0, .output = 0, .cache_read = 0, .cache_write = 0, .reasoning = 0 };
+ return if (m.usage) |u| usage(u) else .{ .input = 0, .output = 0, .cache_read = 0, .cache_write = 0, .reasoning = 0 };
+}
+export fn panto_conversation_message_block_count(c: *PantoConversation, index: usize) usize {
+ const m = msgAt(c, index) orelse return 0;
+ return m.content.items.len;
+}
+export fn panto_conversation_block_tag(c: *PantoConversation, mi: usize, bi: usize) PantoContentBlockTag {
+ const b = blockAt(c, mi, bi) orelse return .Text;
+ return @enumFromInt(@intFromEnum(b.*));
+}
+export fn panto_conversation_block_text(c: *PantoConversation, mi: usize, bi: usize) PantoSlice {
+ const b = blockAt(c, mi, bi) orelse return .{ .ptr = null, .len = 0 };
+ return switch (b.*) {
+ .Text => |t| outSlice(t.items),
+ .Thinking => |t| outSlice(t.text.items),
+ .ToolUse => |t| outSlice(t.input.items),
+ .ToolResult => |tr| blk: {
+ for (tr.parts.items) |p| if (p == .text) break :blk outSlice(p.text.items);
+ break :blk .{ .ptr = null, .len = 0 };
+ },
+ .System => |s| outSlice(s.text.items),
+ .CompactionSummary => |s| outSlice(s.text.items),
+ };
+}
+export fn panto_conversation_block_tool_id(c: *PantoConversation, mi: usize, bi: usize) PantoSlice {
+ const b = blockAt(c, mi, bi) orelse return .{ .ptr = null, .len = 0 };
+ return switch (b.*) { .ToolUse => |t| outSlice(t.id), .ToolResult => |t| outSlice(t.tool_use_id), else => .{ .ptr = null, .len = 0 } };
+}
+export fn panto_conversation_block_tool_name(c: *PantoConversation, mi: usize, bi: usize) PantoSlice {
+ const b = blockAt(c, mi, bi) orelse return .{ .ptr = null, .len = 0 };
+ return switch (b.*) { .ToolUse => |t| outSlice(t.name), else => .{ .ptr = null, .len = 0 } };
+}
+export fn panto_conversation_block_tool_result_is_error(c: *PantoConversation, mi: usize, bi: usize) bool {
+ const b = blockAt(c, mi, bi) orelse return false;
+ return switch (b.*) { .ToolResult => |t| t.is_error, else => false };
+}
+export fn panto_conversation_block_system_mode(c: *PantoConversation, mi: usize, bi: usize) PantoSystemMode {
+ const b = blockAt(c, mi, bi) orelse return .append;
+ return switch (b.*) { .System => |s| @enumFromInt(@intFromEnum(s.mode)), else => .append };
+}
+export fn panto_conversation_add_system_message(c: *PantoConversation, text: ?[*:0]const u8) PantoStatus {
+ unwrapConversation(c).addSystemMessage(cstr(text)) catch |e| return setErr("{t}", .{e});
+ return .ok;
+}
+export fn panto_conversation_replace_system_message(c: *PantoConversation, text: ?[*:0]const u8) PantoStatus {
+ unwrapConversation(c).replaceSystemMessage(cstr(text)) catch |e| return setErr("{t}", .{e});
+ return .ok;
+}
+export fn panto_conversation_add_user_text(c: *PantoConversation, text: ?[*:0]const u8) PantoStatus {
+ const cc = unwrapConversation(c);
+ const tb = panto.textualBlockFromSlice(cc.allocator, cstr(text)) catch |e| return setErr("{t}", .{e});
+ const b: panto.ContentBlock = .{ .Text = tb };
+ cc.addUserMessage(&.{b}) catch |e| return setErr("{t}", .{e});
+ return .ok;
+}
+export fn panto_conversation_add_assistant_text(c: *PantoConversation, text: ?[*:0]const u8, has_usage: bool, u: PantoUsage) PantoStatus {
+ const cc = unwrapConversation(c);
+ const tb = panto.textualBlockFromSlice(cc.allocator, cstr(text)) catch |e| return setErr("{t}", .{e});
+ const b: panto.ContentBlock = .{ .Text = tb };
+ cc.addAssistantMessage(&.{b}, if (has_usage) zigUsage(u) else null) catch |e| return setErr("{t}", .{e});
+ return .ok;
+}
+export fn panto_conversation_add_compaction_summary(c: *PantoConversation, text: ?[*:0]const u8) PantoStatus {
+ unwrapConversation(c).addCompactionSummary(cstr(text)) catch |e| return setErr("{t}", .{e});
+ return .ok;
+}
export fn panto_session_create_null(out: **PantoSession) PantoStatus {
const h = allocator.create(SessionHandle) catch return setErr("OutOfMemory", .{});
var ns = panto.NullStore.init(allocator);
@@ -382,7 +475,7 @@ fn builtinStoreAppend(ctx: *anyopaque, session_id: PantoSlice, messages: [*]cons
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 };
+ for (messages[0..len], 0..) |m, i| pmsgs[i] = .{ .message = .{ .role = @enumFromInt(@intFromEnum(m.role)), .content = .empty, .metadata = if (m.has_metadata) slice(m.metadata) else null }, .usage = if (m.has_usage) zigUsage(m.usage) else null, .identity = .{ .api_style = .openai_chat, .base_url = "", .model = "" } };
s.appendMessages(slice(session_id), pmsgs) catch |e| return setErr("{t}", .{e});
return .ok;
}
@@ -449,6 +542,10 @@ 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});
@@ -468,6 +565,14 @@ export fn panto_agent_set_system_prompt(a: *PantoAgent, text: ?[*:0]const u8) Pa
h.agent.setSystemPrompt(cstr(text)) catch |e| return setErr("{t}", .{e});
return .ok;
}
+export fn panto_agent_compact(a: *PantoAgent, override_system_prompt: ?[*:0]const u8, extra_instructions: ?[*:0]const u8, out: *PantoCompactionResult) PantoStatus {
+ const h: *AgentHandle = @ptrCast(@alignCast(a));
+ const osp = if (override_system_prompt == null or cstr(override_system_prompt).len == 0) null else cstr(override_system_prompt);
+ const extra = if (extra_instructions == null or cstr(extra_instructions).len == 0) null else cstr(extra_instructions);
+ const r = h.agent.compact(osp, extra) catch |e| return setErr("{t}", .{e});
+ out.* = .{ .compacted = r.compacted, .kept_turns = r.kept_turns, .summarized_messages = r.summarized_messages };
+ return .ok;
+}
export fn panto_agent_run(a: *PantoAgent, text: ?[*:0]const u8, out: **PantoStream) PantoStatus {
const h: *AgentHandle = @ptrCast(@alignCast(a));
const st = h.agent.run(.{ .text = cstr(text) }) catch |e| return setErr("{t}", .{e});
@@ -525,6 +630,9 @@ fn marshalEvent(ev: panto.Event) !PantoEvent {
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);
diff --git a/libpanto-go/README.md b/libpanto-go/README.md
new file mode 100644
index 0000000..a19e788
--- /dev/null
+++ b/libpanto-go/README.md
@@ -0,0 +1,48 @@
+# libpanto-go
+
+Idiomatic Go bindings for `libpanto-c` via cgo.
+
+## Build
+
+Build the C ABI first:
+
+```sh
+cd ../libpanto-c
+zig build
+```
+
+Then use the Go package from this directory:
+
+```sh
+go test ./...
+```
+
+At runtime, make sure `../libpanto-c/zig-out/lib` (or the installed equivalent) is on your platform's dynamic library path.
+
+## Example
+
+```go
+panto.Init()
+defer panto.Deinit()
+
+agent, err := panto.NewAgent(panto.Config{
+ Provider: panto.ProviderConfig{
+ OpenAIChat: &panto.OpenAIChatConfig{
+ APIKey: os.Getenv("OPENAI_API_KEY"),
+ BaseURL: "https://api.openai.com/v1",
+ Model: "gpt-4.1-mini",
+ },
+ },
+})
+if err != nil { /* handle */ }
+defer agent.Close()
+
+stream, err := agent.Run("hello")
+if err != nil { /* handle */ }
+defer stream.Close()
+
+for ev := range stream.Iter() {
+ // ev.Tag identifies the active payload.
+}
+if err := stream.Err(); err != nil { /* handle */ }
+```
diff --git a/libpanto-go/go.mod b/libpanto-go/go.mod
new file mode 100644
index 0000000..805022f
--- /dev/null
+++ b/libpanto-go/go.mod
@@ -0,0 +1,3 @@
+module github.com/travisjeffery/pantograph/libpanto-go
+
+go 1.24
diff --git a/libpanto-go/panto.go b/libpanto-go/panto.go
new file mode 100644
index 0000000..afb66a9
--- /dev/null
+++ b/libpanto-go/panto.go
@@ -0,0 +1,709 @@
+package panto
+
+/*
+#cgo CFLAGS: -I../libpanto-c/include
+#cgo LDFLAGS: -L../libpanto-c/zig-out/lib -lpanto
+#include <panto.h>
+#include <stdlib.h>
+*/
+import "C"
+
+import (
+ "errors"
+ "fmt"
+ "iter"
+ "runtime"
+ "unsafe"
+)
+
+type APIStyle int
+type ReasoningEffort int
+type Thinking int
+type Effort int
+type MessageRole int
+type ContentBlockTag int
+type EventTag int
+type SystemMode int
+
+const (
+ OpenAIChat APIStyle = iota
+ AnthropicMessages
+)
+const (
+ ReasoningDefault ReasoningEffort = iota
+ ReasoningOff
+ ReasoningMinimal
+ ReasoningLow
+ ReasoningMedium
+ ReasoningHigh
+)
+const (
+ ThinkingDisabled Thinking = iota
+ ThinkingEnabled
+ ThinkingAdaptive
+)
+const (
+ EffortLow Effort = iota
+ EffortMedium
+ EffortHigh
+ EffortXHigh
+ EffortMax
+)
+const (
+ RoleSystem MessageRole = iota
+ RoleUser
+ RoleAssistant
+)
+const (
+ BlockText ContentBlockTag = iota
+ BlockThinking
+ BlockToolUse
+ BlockToolResult
+ BlockSystem
+ BlockCompactionSummary
+)
+const (
+ SystemAppend SystemMode = iota
+ SystemReplace
+)
+const (
+ EventMessageStart EventTag = iota
+ EventBlockStart
+ EventToolDetails
+ EventContentDelta
+ EventBlockComplete
+ EventMessageComplete
+ EventProviderRetry
+ EventToolDispatchStart
+ EventToolDispatchComplete
+ EventTurnComplete
+)
+
+type Config struct {
+ Provider ProviderConfig
+ Compaction CompactionConfig
+ Retry RetryConfig
+}
+type ProviderConfig struct {
+ OpenAIChat *OpenAIChatConfig
+ AnthropicMessages *AnthropicMessagesConfig
+}
+type OpenAIChatConfig struct {
+ APIKey, BaseURL, Model string
+ Reasoning ReasoningEffort
+ MaxTokens uint32
+}
+type AnthropicMessagesConfig struct {
+ APIKey, BaseURL, Model, APIVersion string
+ MaxTokens uint32
+ Thinking Thinking
+ Effort Effort
+ ThinkingBudgetTokens *uint32
+ ThinkingInterleaved bool
+}
+type CompactionConfig struct {
+ KeepVerbatim uint32
+ Model *ProviderConfig
+ CompactionPrompt *string
+}
+type RetryConfig struct {
+ MaxAttempts uint
+ InitialDelayMS, MaxDelayMS uint64
+ Multiplier float64
+ Jitter bool
+}
+type Usage struct{ Input, Output, CacheRead, CacheWrite, Reasoning uint64 }
+type CompactionResult struct {
+ Compacted bool
+ KeptTurns, SummarizedMessages uint
+}
+
+type Event struct {
+ Tag EventTag
+ MessageStart MessageRole
+ BlockStart BlockStart
+ ToolDetails ToolDetails
+ ContentDelta ContentDelta
+ BlockComplete BlockComplete
+ MessageComplete MessageComplete
+ ProviderRetry ProviderRetry
+ ToolDispatchStart ToolDispatchStart
+ ToolDispatchComplete ToolDispatchComplete
+}
+type BlockStart struct {
+ BlockType ContentBlockTag
+ Index uint
+}
+type ToolDetails struct {
+ Index uint
+ ID, Name string
+}
+type ContentDelta struct {
+ Index uint
+ Delta string
+}
+type BlockComplete struct {
+ Index uint
+ BlockType ContentBlockTag
+}
+type MessageComplete struct{ Usage *Usage }
+type ProviderRetry struct {
+ Attempt, MaxAttempts uint
+ DelayMS uint64
+ ErrorName string
+ StatusCode *uint16
+ RetryAfterMS *uint64
+ Message string
+ Compaction bool
+}
+type ToolDispatchStart struct{ Count uint }
+type ToolDispatchComplete struct{ MessageIndex uint }
+
+type Agent struct{ ptr *C.PantoAgent }
+type Stream struct {
+ ptr *C.PantoStream
+ err error
+ done bool
+}
+type Conversation struct {
+ ptr *C.PantoConversation
+ owned bool
+}
+type Session struct{ ptr *C.PantoSession }
+type SessionInfo struct {
+ ID, Created, Modified, LastUserMessage, BaseURL, Model string
+ MessageCount uint
+ APIStyle APIStyle
+ Reasoning ReasoningEffort
+}
+
+type SessionStore interface {
+ cptr() *C.PantoSessionStore
+ Close()
+}
+type NullStore struct{ ptr *C.PantoSessionStore }
+type FileSystemJSONLStore struct{ ptr *C.PantoSessionStore }
+
+type cStrings []*C.char
+
+func (cs cStrings) free() {
+ for _, s := range cs {
+ C.free(unsafe.Pointer(s))
+ }
+}
+func Init() { C.panto_init() }
+func Deinit() { C.panto_deinit() }
+func lastError() error {
+ s := C.panto_last_error()
+ if s.ptr == nil || s.len == 0 {
+ return errors.New("libpanto error")
+ }
+ return errors.New(C.GoStringN((*C.char)(unsafe.Pointer(s.ptr)), C.int(s.len)))
+}
+
+func NewNullStore() (*NullStore, error) {
+ var out *C.PantoSessionStore
+ if C.panto_null_store_create(&out) != C.PANTO_OK {
+ return nil, lastError()
+ }
+ s := &NullStore{out}
+ runtime.SetFinalizer(s, (*NullStore).Close)
+ return s, nil
+}
+func (s *NullStore) cptr() *C.PantoSessionStore { return s.ptr }
+func (s *NullStore) Close() {
+ if s != nil && s.ptr != nil {
+ C.panto_session_store_destroy(s.ptr)
+ s.ptr = nil
+ }
+}
+func NewFileSystemJSONLStore(dir, metadata string) (*FileSystemJSONLStore, error) {
+ cd := C.CString(dir)
+ defer C.free(unsafe.Pointer(cd))
+ cm := C.CString(metadata)
+ defer C.free(unsafe.Pointer(cm))
+ var out *C.PantoSessionStore
+ if C.panto_file_system_jsonl_store_create(cd, cm, &out) != C.PANTO_OK {
+ return nil, lastError()
+ }
+ s := &FileSystemJSONLStore{out}
+ runtime.SetFinalizer(s, (*FileSystemJSONLStore).Close)
+ return s, nil
+}
+func (s *FileSystemJSONLStore) cptr() *C.PantoSessionStore { return s.ptr }
+func (s *FileSystemJSONLStore) Close() {
+ if s != nil && s.ptr != nil {
+ C.panto_session_store_destroy(s.ptr)
+ s.ptr = nil
+ }
+}
+
+func CreateSession(store SessionStore) (*Session, error) {
+ var out *C.PantoSession
+ if C.panto_session_store_create_session(store.cptr(), &out) != C.PANTO_OK {
+ return nil, lastError()
+ }
+ s := &Session{out}
+ runtime.SetFinalizer(s, (*Session).Close)
+ return s, nil
+}
+func ResolveSession(store SessionStore, id string) (*Session, bool, error) {
+ cid := C.CString(id)
+ defer C.free(unsafe.Pointer(cid))
+ var r C.PantoSessionResult
+ if C.panto_session_store_resolve(store.cptr(), cid, &r) != C.PANTO_OK {
+ return nil, false, lastError()
+ }
+ if !bool(r.found) {
+ return nil, false, nil
+ }
+ s := &Session{r.session}
+ runtime.SetFinalizer(s, (*Session).Close)
+ return s, true, nil
+}
+func LatestSession(store SessionStore) (*Session, bool, error) {
+ var r C.PantoSessionResult
+ if C.panto_session_store_latest(store.cptr(), &r) != C.PANTO_OK {
+ return nil, false, lastError()
+ }
+ if !bool(r.found) {
+ return nil, false, nil
+ }
+ s := &Session{r.session}
+ runtime.SetFinalizer(s, (*Session).Close)
+ return s, true, nil
+}
+func ListSessions(store SessionStore) ([]SessionInfo, error) {
+ var l C.PantoSessionInfoList
+ if C.panto_session_store_list(store.cptr(), &l) != C.PANTO_OK {
+ return nil, lastError()
+ }
+ defer C.panto_session_info_list_destroy(l)
+ out := make([]SessionInfo, int(l.len))
+ if l.ptr != nil {
+ xs := unsafe.Slice(l.ptr, int(l.len))
+ for i, x := range xs {
+ out[i] = sessionInfo(x)
+ }
+ }
+ return out, nil
+}
+func (s *Session) Close() {
+ if s != nil && s.ptr != nil {
+ C.panto_session_destroy(s.ptr)
+ s.ptr = nil
+ }
+}
+func (s *Session) Load() (*Conversation, error) {
+ var out *C.PantoConversation
+ if C.panto_session_load(s.ptr, &out) != C.PANTO_OK {
+ return nil, lastError()
+ }
+ c := &Conversation{ptr: out, owned: true}
+ runtime.SetFinalizer(c, (*Conversation).Close)
+ return c, nil
+}
+
+func NewConversation() (*Conversation, error) {
+ var out *C.PantoConversation
+ if C.panto_conversation_create(&out) != C.PANTO_OK {
+ return nil, lastError()
+ }
+ c := &Conversation{ptr: out, owned: true}
+ runtime.SetFinalizer(c, (*Conversation).Close)
+ return c, nil
+}
+func (c *Conversation) Close() {
+ if c != nil && c.owned && c.ptr != nil {
+ C.panto_conversation_destroy(c.ptr)
+ c.ptr = nil
+ }
+}
+func (c *Conversation) MessageCount() uint { return uint(C.panto_conversation_message_count(c.ptr)) }
+func (c *Conversation) Message(i uint) Message { return Message{conv: c, index: i} }
+func (c *Conversation) AddSystemMessage(text string) error {
+ ct := C.CString(text)
+ defer C.free(unsafe.Pointer(ct))
+ if C.panto_conversation_add_system_message(c.ptr, ct) != C.PANTO_OK {
+ return lastError()
+ }
+ return nil
+}
+func (c *Conversation) ReplaceSystemMessage(text string) error {
+ ct := C.CString(text)
+ defer C.free(unsafe.Pointer(ct))
+ if C.panto_conversation_replace_system_message(c.ptr, ct) != C.PANTO_OK {
+ return lastError()
+ }
+ return nil
+}
+func (c *Conversation) AddUserText(text string) error {
+ ct := C.CString(text)
+ defer C.free(unsafe.Pointer(ct))
+ if C.panto_conversation_add_user_text(c.ptr, ct) != C.PANTO_OK {
+ return lastError()
+ }
+ return nil
+}
+func (c *Conversation) AddAssistantText(text string, u *Usage) error {
+ ct := C.CString(text)
+ defer C.free(unsafe.Pointer(ct))
+ var cu C.PantoUsage
+ has := false
+ if u != nil {
+ cu = cUsage(*u)
+ has = true
+ }
+ if C.panto_conversation_add_assistant_text(c.ptr, ct, C.bool(has), cu) != C.PANTO_OK {
+ return lastError()
+ }
+ return nil
+}
+func (c *Conversation) AddCompactionSummary(text string) error {
+ ct := C.CString(text)
+ defer C.free(unsafe.Pointer(ct))
+ if C.panto_conversation_add_compaction_summary(c.ptr, ct) != C.PANTO_OK {
+ return lastError()
+ }
+ return nil
+}
+
+type Message struct {
+ conv *Conversation
+ index uint
+}
+
+func (m Message) Role() MessageRole {
+ return MessageRole(C.panto_conversation_message_role(m.conv.ptr, C.size_t(m.index)))
+}
+func (m Message) Usage() *Usage {
+ if !bool(C.panto_conversation_message_has_usage(m.conv.ptr, C.size_t(m.index))) {
+ return nil
+ }
+ u := usage(C.panto_conversation_message_usage(m.conv.ptr, C.size_t(m.index)))
+ return &u
+}
+func (m Message) BlockCount() uint {
+ return uint(C.panto_conversation_message_block_count(m.conv.ptr, C.size_t(m.index)))
+}
+func (m Message) Block(i uint) ContentBlock {
+ return ContentBlock{conv: m.conv, msg: m.index, index: i}
+}
+
+type ContentBlock struct {
+ conv *Conversation
+ msg, index uint
+}
+
+func (b ContentBlock) Tag() ContentBlockTag {
+ return ContentBlockTag(C.panto_conversation_block_tag(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index)))
+}
+func (b ContentBlock) Text() string {
+ return goSlice(C.panto_conversation_block_text(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index)))
+}
+func (b ContentBlock) ToolID() string {
+ return goSlice(C.panto_conversation_block_tool_id(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index)))
+}
+func (b ContentBlock) ToolName() string {
+ return goSlice(C.panto_conversation_block_tool_name(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index)))
+}
+func (b ContentBlock) ToolResultIsError() bool {
+ return bool(C.panto_conversation_block_tool_result_is_error(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index)))
+}
+func (b ContentBlock) SystemMode() SystemMode {
+ return SystemMode(C.panto_conversation_block_system_mode(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index)))
+}
+
+func NewAgent(cfg Config) (*Agent, error) {
+ st, err := NewNullStore()
+ if err != nil {
+ return nil, err
+ }
+ sess, err := CreateSession(st)
+ st.Close()
+ if err != nil {
+ return nil, err
+ }
+ return NewAgentWithSession(cfg, sess, nil)
+}
+func NewAgentWithSession(cfg Config, session *Session, conv *Conversation) (*Agent, error) {
+ ccfg, strings, err := cfg.c()
+ if err != nil {
+ return nil, err
+ }
+ defer strings.free()
+ var cconv *C.PantoConversation
+ if conv != nil {
+ cconv = conv.ptr
+ conv.owned = false
+ runtime.SetFinalizer(conv, nil)
+ }
+ var out *C.PantoAgent
+ if C.panto_agent_create(&ccfg, session.ptr, cconv, &out) != C.PANTO_OK {
+ return nil, lastError()
+ }
+ a := &Agent{out}
+ runtime.SetFinalizer(a, (*Agent).Close)
+ return a, nil
+}
+func (a *Agent) Close() {
+ if a != nil && a.ptr != nil {
+ C.panto_agent_destroy(a.ptr)
+ a.ptr = nil
+ }
+}
+func (a *Agent) SessionID() string { return goSlice(C.panto_agent_session_id(a.ptr)) }
+func (a *Agent) Conversation() *Conversation {
+ return &Conversation{ptr: C.panto_agent_conversation(a.ptr), owned: false}
+}
+func (a *Agent) SetConfig(cfg Config) error {
+ ccfg, strings, err := cfg.c()
+ if err != nil {
+ return err
+ }
+ defer strings.free()
+ if C.panto_agent_set_config(a.ptr, &ccfg) != C.PANTO_OK {
+ return lastError()
+ }
+ return nil
+}
+func (a *Agent) Compact(overrideSystemPrompt, extraInstructions *string) (CompactionResult, error) {
+ var co, ce *C.char
+ if overrideSystemPrompt != nil {
+ co = C.CString(*overrideSystemPrompt)
+ defer C.free(unsafe.Pointer(co))
+ }
+ if extraInstructions != nil {
+ ce = C.CString(*extraInstructions)
+ defer C.free(unsafe.Pointer(ce))
+ }
+ var out C.PantoCompactionResult
+ if C.panto_agent_compact(a.ptr, co, ce, &out) != C.PANTO_OK {
+ return CompactionResult{}, lastError()
+ }
+ return CompactionResult{Compacted: bool(out.compacted), KeptTurns: uint(out.kept_turns), SummarizedMessages: uint(out.summarized_messages)}, nil
+}
+func (a *Agent) Run(userText string) (*Stream, error) {
+ ct := C.CString(userText)
+ defer C.free(unsafe.Pointer(ct))
+ var out *C.PantoStream
+ if C.panto_agent_run(a.ptr, ct, &out) != C.PANTO_OK {
+ return nil, lastError()
+ }
+ s := &Stream{ptr: out}
+ runtime.SetFinalizer(s, (*Stream).Close)
+ return s, nil
+}
+func (a *Agent) AddSystemMessage(text string) error {
+ ct := C.CString(text)
+ defer C.free(unsafe.Pointer(ct))
+ if C.panto_agent_add_system_message(a.ptr, ct) != C.PANTO_OK {
+ return lastError()
+ }
+ return nil
+}
+func (a *Agent) SetSystemPrompt(text string) error {
+ ct := C.CString(text)
+ defer C.free(unsafe.Pointer(ct))
+ if C.panto_agent_set_system_prompt(a.ptr, ct) != C.PANTO_OK {
+ return lastError()
+ }
+ return nil
+}
+
+func (s *Stream) Close() {
+ if s != nil && s.ptr != nil {
+ C.panto_stream_destroy(s.ptr)
+ s.ptr = nil
+ }
+}
+func (s *Stream) Next() (Event, bool, error) {
+ if s.err != nil {
+ return Event{}, false, s.err
+ }
+ if s.done {
+ return Event{}, false, nil
+ }
+ var cev C.PantoEvent
+ status := C.panto_stream_next(s.ptr, &cev)
+ switch status {
+ case C.PANTO_NEXT_EVENT:
+ defer C.panto_event_free(&cev)
+ ev := marshalEvent(&cev)
+ if ev.Tag == EventTurnComplete {
+ s.done = true
+ }
+ return ev, true, nil
+ case C.PANTO_NEXT_DONE:
+ s.done = true
+ return Event{}, false, nil
+ case C.PANTO_NEXT_ERROR:
+ s.err = lastError()
+ return Event{}, false, s.err
+ default:
+ s.err = fmt.Errorf("unknown panto next status %d", int(status))
+ return Event{}, false, s.err
+ }
+}
+func (s *Stream) Err() error { return s.err }
+func (s *Stream) Iter() iter.Seq[Event] {
+ return func(yield func(Event) bool) {
+ for {
+ ev, ok, err := s.Next()
+ if err != nil || !ok {
+ return
+ }
+ if !yield(ev) {
+ return
+ }
+ if ev.Tag == EventTurnComplete {
+ return
+ }
+ }
+ }
+}
+func (s *Stream) Chan() <-chan Event {
+ ch := make(chan Event)
+ go func() {
+ defer close(ch)
+ for ev := range s.Iter() {
+ ch <- ev
+ }
+ }()
+ return ch
+}
+
+func (cfg Config) c() (C.PantoConfig, cStrings, error) {
+ var cs cStrings
+ provider, pstrings, err := cfg.Provider.c()
+ cs = append(cs, pstrings...)
+ if err != nil {
+ cs.free()
+ return C.PantoConfig{}, nil, err
+ }
+ out := C.PantoConfig{provider: provider}
+ out.compaction.keep_verbatim = C.uint32_t(cfg.Compaction.KeepVerbatim)
+ if cfg.Compaction.Model != nil {
+ m, ms, err := cfg.Compaction.Model.c()
+ cs = append(cs, ms...)
+ if err != nil {
+ cs.free()
+ return C.PantoConfig{}, nil, err
+ }
+ out.compaction.has_model = true
+ out.compaction.model = m
+ }
+ if cfg.Compaction.CompactionPrompt != nil {
+ c := C.CString(*cfg.Compaction.CompactionPrompt)
+ cs = append(cs, c)
+ out.compaction.has_compaction_prompt = true
+ out.compaction.compaction_prompt = c
+ }
+ out.retry.max_attempts = C.size_t(cfg.Retry.MaxAttempts)
+ out.retry.initial_delay_ms = C.uint64_t(cfg.Retry.InitialDelayMS)
+ out.retry.max_delay_ms = C.uint64_t(cfg.Retry.MaxDelayMS)
+ out.retry.multiplier = C.double(cfg.Retry.Multiplier)
+ out.retry.jitter = C.bool(cfg.Retry.Jitter)
+ return out, cs, nil
+}
+func (p ProviderConfig) c() (C.PantoProviderConfig, cStrings, error) {
+ var out C.PantoProviderConfig
+ var cs cStrings
+ if p.OpenAIChat != nil == (p.AnthropicMessages != nil) {
+ return out, nil, errors.New("exactly one provider config must be set")
+ }
+ if p.OpenAIChat != nil {
+ out.tag = C.PANTO_OPENAI_CHAT
+ setOpenAI(&out, p.OpenAIChat, &cs)
+ return out, cs, nil
+ }
+ out.tag = C.PANTO_ANTHROPIC_MESSAGES
+ setAnthropic(&out, p.AnthropicMessages, &cs)
+ return out, cs, nil
+}
+func setOpenAI(out *C.PantoProviderConfig, cfg *OpenAIChatConfig, cs *cStrings) {
+ apiKey, baseURL, model := C.CString(cfg.APIKey), C.CString(cfg.BaseURL), C.CString(cfg.Model)
+ *cs = append(*cs, apiKey, baseURL, model)
+ c := (*C.PantoOpenAIChatConfig)(unsafe.Pointer(&out.data[0]))
+ c.api_key = apiKey
+ c.base_url = baseURL
+ c.model = model
+ c.reasoning = C.PantoReasoningEffort(cfg.Reasoning)
+ c.max_tokens = C.uint32_t(cfg.MaxTokens)
+}
+func setAnthropic(out *C.PantoProviderConfig, cfg *AnthropicMessagesConfig, cs *cStrings) {
+ apiKey, baseURL, model, ver := C.CString(cfg.APIKey), C.CString(cfg.BaseURL), C.CString(cfg.Model), C.CString(cfg.APIVersion)
+ *cs = append(*cs, apiKey, baseURL, model, ver)
+ c := (*C.PantoAnthropicMessagesConfig)(unsafe.Pointer(&out.data[0]))
+ c.api_key = apiKey
+ c.base_url = baseURL
+ c.model = model
+ c.api_version = ver
+ c.max_tokens = C.uint32_t(cfg.MaxTokens)
+ c.thinking = C.PantoThinking(cfg.Thinking)
+ c.effort = C.PantoEffort(cfg.Effort)
+ c.thinking_interleaved = C.bool(cfg.ThinkingInterleaved)
+ if cfg.ThinkingBudgetTokens != nil {
+ c.has_thinking_budget_tokens = true
+ c.thinking_budget_tokens = C.uint32_t(*cfg.ThinkingBudgetTokens)
+ }
+}
+
+func marshalEvent(cev *C.PantoEvent) Event {
+ ev := Event{Tag: EventTag(cev.tag)}
+ switch cev.tag {
+ case C.PANTO_EVENT_MESSAGE_START:
+ ev.MessageStart = MessageRole(*(*C.PantoMessageRole)(unsafe.Pointer(&cev.data[0])))
+ case C.PANTO_EVENT_BLOCK_START:
+ b := (*C.PantoBlockStart)(unsafe.Pointer(&cev.data[0]))
+ ev.BlockStart = BlockStart{BlockType: ContentBlockTag(b.block_type), Index: uint(b.index)}
+ case C.PANTO_EVENT_TOOL_DETAILS:
+ t := (*C.PantoToolDetails)(unsafe.Pointer(&cev.data[0]))
+ ev.ToolDetails = ToolDetails{Index: uint(t.index), ID: goSlice(t.id), Name: goSlice(t.name)}
+ case C.PANTO_EVENT_CONTENT_DELTA:
+ d := (*C.PantoContentDelta)(unsafe.Pointer(&cev.data[0]))
+ ev.ContentDelta = ContentDelta{Index: uint(d.index), Delta: goSlice(d.delta)}
+ case C.PANTO_EVENT_BLOCK_COMPLETE:
+ b := (*C.PantoBlockComplete)(unsafe.Pointer(&cev.data[0]))
+ ev.BlockComplete = BlockComplete{Index: uint(b.index), BlockType: ContentBlockTag(b.block_type)}
+ case C.PANTO_EVENT_MESSAGE_COMPLETE:
+ m := (*C.PantoMessageComplete)(unsafe.Pointer(&cev.data[0]))
+ if m.has_usage {
+ u := usage(m.usage)
+ ev.MessageComplete.Usage = &u
+ }
+ case C.PANTO_EVENT_PROVIDER_RETRY:
+ r := (*C.PantoProviderRetry)(unsafe.Pointer(&cev.data[0]))
+ ev.ProviderRetry = ProviderRetry{Attempt: uint(r.attempt), MaxAttempts: uint(r.max_attempts), DelayMS: uint64(r.delay_ms), ErrorName: goSlice(r.error_name), Message: goSlice(r.message), Compaction: bool(r.compaction)}
+ if r.has_status_code {
+ v := uint16(r.status_code)
+ ev.ProviderRetry.StatusCode = &v
+ }
+ if r.has_retry_after_ms {
+ v := uint64(r.retry_after_ms)
+ ev.ProviderRetry.RetryAfterMS = &v
+ }
+ case C.PANTO_EVENT_TOOL_DISPATCH_START:
+ t := (*C.PantoToolDispatchStart)(unsafe.Pointer(&cev.data[0]))
+ ev.ToolDispatchStart = ToolDispatchStart{Count: uint(t.count)}
+ case C.PANTO_EVENT_TOOL_DISPATCH_COMPLETE:
+ t := (*C.PantoToolDispatchComplete)(unsafe.Pointer(&cev.data[0]))
+ ev.ToolDispatchComplete = ToolDispatchComplete{MessageIndex: uint(t.message_index)}
+ }
+ return ev
+}
+func goSlice(s C.PantoSlice) string {
+ if s.ptr == nil || s.len == 0 {
+ return ""
+ }
+ return C.GoStringN((*C.char)(unsafe.Pointer(s.ptr)), C.int(s.len))
+}
+func usage(u C.PantoUsage) Usage {
+ return Usage{Input: uint64(u.input), Output: uint64(u.output), CacheRead: uint64(u.cache_read), CacheWrite: uint64(u.cache_write), Reasoning: uint64(u.reasoning)}
+}
+func cUsage(u Usage) C.PantoUsage {
+ return C.PantoUsage{input: C.uint64_t(u.Input), output: C.uint64_t(u.Output), cache_read: C.uint64_t(u.CacheRead), cache_write: C.uint64_t(u.CacheWrite), reasoning: C.uint64_t(u.Reasoning)}
+}
+func sessionInfo(i C.PantoSessionInfo) SessionInfo {
+ return SessionInfo{ID: goSlice(i.id), Created: goSlice(i.created), Modified: goSlice(i.modified), LastUserMessage: goSlice(i.last_user_message), BaseURL: goSlice(i.base_url), Model: goSlice(i.model), MessageCount: uint(i.message_count), APIStyle: APIStyle(i.api_style), Reasoning: ReasoningEffort(i.reasoning)}
+}