From 27e37c62bc1483c0489d6db01344906472a1f8d3 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 11 Jun 2026 11:15:40 -0600 Subject: libpanto-go some libpanto-c coverage expansion was needed --- libpanto-c/include/panto.h | 19 ++ libpanto-c/src/lib.zig | 110 ++++++- libpanto-go/README.md | 48 +++ libpanto-go/go.mod | 3 + libpanto-go/panto.go | 709 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 888 insertions(+), 1 deletion(-) create mode 100644 libpanto-go/README.md create mode 100644 libpanto-go/go.mod create mode 100644 libpanto-go/panto.go 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 +#include +*/ +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)} +} -- cgit v1.3