diff options
| -rw-r--r-- | libpanto-c/include/panto.h | 15 | ||||
| -rw-r--r-- | libpanto-c/src/lib.zig | 102 | ||||
| -rw-r--r-- | libpanto-go/panto.go | 611 |
3 files changed, 675 insertions, 53 deletions
diff --git a/libpanto-c/include/panto.h b/libpanto-c/include/panto.h index 0772e6e..94ddcb9 100644 --- a/libpanto-c/include/panto.h +++ b/libpanto-c/include/panto.h @@ -119,6 +119,21 @@ 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 diff --git a/libpanto-c/src/lib.zig b/libpanto-c/src/lib.zig index 81ce0ce..9b55bee 100644 --- a/libpanto-c/src/lib.zig +++ b/libpanto-c/src/lib.zig @@ -18,6 +18,8 @@ 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 }; pub const PantoSlice = extern struct { ptr: ?[*]const u8, len: usize }; pub const PantoStatus = enum(c_int) { ok = 0, err = 1 }; @@ -63,6 +65,16 @@ pub const PantoSessionStoreVTable = extern struct { 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); @@ -637,3 +649,93 @@ fn zigUsage(u: PantoUsage) panto.Usage { test "c abi exports compile" { try std.testing.expect(true); } + +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]); + } +} diff --git a/libpanto-go/panto.go b/libpanto-go/panto.go index afb66a9..786ff1c 100644 --- a/libpanto-go/panto.go +++ b/libpanto-go/panto.go @@ -5,6 +5,32 @@ package panto #cgo LDFLAGS: -L../libpanto-c/zig-out/lib -lpanto #include <panto.h> #include <stdlib.h> +extern PantoStatus pantoGoStoreCreate(void *ctx, PantoSession **out); +extern PantoStatus pantoGoStoreList(void *ctx, PantoSessionInfoList *out); +extern void pantoGoStoreFreeSessionInfos(void *ctx, PantoSessionInfoList infos); +extern PantoStatus pantoGoStoreResolve(void *ctx, PantoSlice id, PantoSessionResult *out); +extern PantoStatus pantoGoStoreLatest(void *ctx, PantoSessionResult *out); +extern PantoStatus pantoGoStoreLoad(void *ctx, PantoSlice id, PantoConversation **out); +extern PantoStatus pantoGoStoreAppendMessages(void *ctx, PantoSlice session_id, PantoPersistentMessage *messages, size_t len); +static PantoStatus pantoGoStoreAppendMessagesConst(void *ctx, PantoSlice session_id, const PantoPersistentMessage *messages, size_t len) { + return pantoGoStoreAppendMessages(ctx, session_id, (PantoPersistentMessage*)messages, len); +} +extern PantoStatus pantoGoToolInvoke(void *ctx, PantoSlice input, PantoResultParts *out); +extern PantoStatus pantoGoToolSourceInvokeBatch(void *ctx, PantoToolCall *calls, PantoToolCallResult *results, size_t len); +extern void pantoGoCallbackDestroy(void *ctx); +static PantoSessionStoreVTable panto_go_store_vtable = { + pantoGoStoreCreate, + pantoGoStoreList, + pantoGoStoreFreeSessionInfos, + pantoGoStoreResolve, + pantoGoStoreLatest, + pantoGoStoreLoad, + pantoGoStoreAppendMessagesConst, + pantoGoCallbackDestroy, +}; +static PantoStatus panto_go_session_store_create(void *ctx, PantoSessionStore **out) { + return panto_session_store_create(ctx, &panto_go_store_vtable, out); +} */ import "C" @@ -16,19 +42,22 @@ import ( "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 +type ( + APIStyle int + ReasoningEffort int + Thinking int + Effort int + MessageRole int + ContentBlockTag int + EventTag int + SystemMode int +) const ( OpenAIChat APIStyle = iota AnthropicMessages ) + const ( ReasoningDefault ReasoningEffort = iota ReasoningOff @@ -37,11 +66,13 @@ const ( ReasoningMedium ReasoningHigh ) + const ( ThinkingDisabled Thinking = iota ThinkingEnabled ThinkingAdaptive ) + const ( EffortLow Effort = iota EffortMedium @@ -49,11 +80,13 @@ const ( EffortXHigh EffortMax ) + const ( RoleSystem MessageRole = iota RoleUser RoleAssistant ) + const ( BlockText ContentBlockTag = iota BlockThinking @@ -62,10 +95,12 @@ const ( BlockSystem BlockCompactionSummary ) + const ( SystemAppend SystemMode = iota SystemReplace ) + const ( EventMessageStart EventTag = iota EventBlockStart @@ -112,11 +147,13 @@ type RetryConfig struct { Multiplier float64 Jitter bool } -type Usage struct{ Input, Output, CacheRead, CacheWrite, Reasoning uint64 } -type CompactionResult struct { - Compacted bool - KeptTurns, SummarizedMessages uint -} +type ( + Usage struct{ Input, Output, CacheRead, CacheWrite, Reasoning uint64 } + CompactionResult struct { + Compacted bool + KeptTurns, SummarizedMessages uint + } +) type Event struct { Tag EventTag @@ -146,43 +183,85 @@ 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 ( + MessageComplete struct{ Usage *Usage } + ProviderRetry struct { + Attempt, MaxAttempts uint + DelayMS uint64 + ErrorName string + StatusCode *uint16 + RetryAfterMS *uint64 + Message string + Compaction bool + } +) + +type ( + ToolDispatchStart struct{ Count uint } + ToolDispatchComplete struct{ MessageIndex uint } +) + +type ( + Agent struct{ ptr *C.PantoAgent } + Stream struct { + ptr *C.PantoStream + err error + done bool + } +) -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 ( + Session struct{ ptr *C.PantoSession } + SessionInfo struct { + ID, Created, Modified, LastUserMessage, BaseURL, Model string + MessageCount uint + APIStyle APIStyle + Reasoning ReasoningEffort + } +) type SessionStore interface { - cptr() *C.PantoSessionStore - Close() + Create() (*Session, error) + List() ([]SessionInfo, error) + Resolve(id string) (*Session, bool, error) + Latest() (*Session, bool, error) + Load(id string) (*Conversation, bool, error) + AppendMessages(sessionID string, messages []PersistentMessage) error +} +type ( + cSessionStore interface{ cptr() *C.PantoSessionStore } + NullStore struct{ ptr *C.PantoSessionStore } + FileSystemJSONLStore struct{ ptr *C.PantoSessionStore } + GoSessionStore struct { + ptr *C.PantoSessionStore + impl SessionStore + } +) + +type PersistentMessage struct { + Role MessageRole + Usage *Usage + Metadata string + Content []OwnedContentBlock +} +type OwnedMessage struct { + Role MessageRole + Usage *Usage + Metadata string + Content []OwnedContentBlock +} +type OwnedContentBlock struct { + Tag ContentBlockTag + Text string + ToolID string + ToolName string + IsError bool + SystemMode SystemMode } -type NullStore struct{ ptr *C.PantoSessionStore } -type FileSystemJSONLStore struct{ ptr *C.PantoSessionStore } type cStrings []*C.char @@ -210,13 +289,27 @@ func NewNullStore() (*NullStore, error) { runtime.SetFinalizer(s, (*NullStore).Close) return s, nil } -func (s *NullStore) cptr() *C.PantoSessionStore { return s.ptr } +func (s *NullStore) cptr() *C.PantoSessionStore { return s.ptr } +func (s *NullStore) Create() (*Session, error) { return CreateSession(s) } +func (s *NullStore) List() ([]SessionInfo, error) { return ListSessions(s) } +func (s *NullStore) Resolve(id string) (*Session, bool, error) { return ResolveSession(s, id) } +func (s *NullStore) Latest() (*Session, bool, error) { return LatestSession(s) } +func (s *NullStore) Load(id string) (*Conversation, bool, error) { + sess, ok, err := ResolveSession(s, id) + if err != nil || !ok { + return nil, ok, err + } + c, err := sess.Load() + return c, err == nil, err +} +func (s *NullStore) AppendMessages(sessionID string, messages []PersistentMessage) error { return nil } 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)) @@ -230,7 +323,26 @@ func NewFileSystemJSONLStore(dir, metadata string) (*FileSystemJSONLStore, error runtime.SetFinalizer(s, (*FileSystemJSONLStore).Close) return s, nil } -func (s *FileSystemJSONLStore) cptr() *C.PantoSessionStore { return s.ptr } +func (s *FileSystemJSONLStore) cptr() *C.PantoSessionStore { return s.ptr } +func (s *FileSystemJSONLStore) Create() (*Session, error) { return CreateSession(s) } +func (s *FileSystemJSONLStore) List() ([]SessionInfo, error) { return ListSessions(s) } +func (s *FileSystemJSONLStore) Resolve(id string) (*Session, bool, error) { + return ResolveSession(s, id) +} +func (s *FileSystemJSONLStore) Latest() (*Session, bool, error) { return LatestSession(s) } +func (s *FileSystemJSONLStore) Load(id string) (*Conversation, bool, error) { + sess, ok, err := ResolveSession(s, id) + if err != nil || !ok { + return nil, ok, err + } + c, err := sess.Load() + return c, err == nil, err +} + +func (s *FileSystemJSONLStore) AppendMessages(sessionID string, messages []PersistentMessage) error { + return nil +} + func (s *FileSystemJSONLStore) Close() { if s != nil && s.ptr != nil { C.panto_session_store_destroy(s.ptr) @@ -238,7 +350,7 @@ func (s *FileSystemJSONLStore) Close() { } } -func CreateSession(store SessionStore) (*Session, error) { +func CreateSession(store cSessionStore) (*Session, error) { var out *C.PantoSession if C.panto_session_store_create_session(store.cptr(), &out) != C.PANTO_OK { return nil, lastError() @@ -247,7 +359,8 @@ func CreateSession(store SessionStore) (*Session, error) { runtime.SetFinalizer(s, (*Session).Close) return s, nil } -func ResolveSession(store SessionStore, id string) (*Session, bool, error) { + +func ResolveSession(store cSessionStore, id string) (*Session, bool, error) { cid := C.CString(id) defer C.free(unsafe.Pointer(cid)) var r C.PantoSessionResult @@ -261,7 +374,8 @@ func ResolveSession(store SessionStore, id string) (*Session, bool, error) { runtime.SetFinalizer(s, (*Session).Close) return s, true, nil } -func LatestSession(store SessionStore) (*Session, bool, error) { + +func LatestSession(store cSessionStore) (*Session, bool, error) { var r C.PantoSessionResult if C.panto_session_store_latest(store.cptr(), &r) != C.PANTO_OK { return nil, false, lastError() @@ -273,7 +387,8 @@ func LatestSession(store SessionStore) (*Session, bool, error) { runtime.SetFinalizer(s, (*Session).Close) return s, true, nil } -func ListSessions(store SessionStore) ([]SessionInfo, error) { + +func ListSessions(store cSessionStore) ([]SessionInfo, error) { var l C.PantoSessionInfoList if C.panto_session_store_list(store.cptr(), &l) != C.PANTO_OK { return nil, lastError() @@ -288,12 +403,14 @@ func ListSessions(store SessionStore) ([]SessionInfo, error) { } 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 { @@ -313,6 +430,7 @@ func NewConversation() (*Conversation, error) { 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) @@ -329,6 +447,7 @@ func (c *Conversation) AddSystemMessage(text string) error { } return nil } + func (c *Conversation) ReplaceSystemMessage(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) @@ -337,6 +456,7 @@ func (c *Conversation) ReplaceSystemMessage(text string) error { } return nil } + func (c *Conversation) AddUserText(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) @@ -345,6 +465,7 @@ func (c *Conversation) AddUserText(text string) error { } return nil } + func (c *Conversation) AddAssistantText(text string, u *Usage) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) @@ -359,6 +480,7 @@ func (c *Conversation) AddAssistantText(text string, u *Usage) error { } return nil } + func (c *Conversation) AddCompactionSummary(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) @@ -376,6 +498,7 @@ type Message struct { 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 @@ -383,13 +506,23 @@ func (m Message) Usage() *Usage { 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} } +func (m Message) Snapshot() OwnedMessage { + blocks := make([]OwnedContentBlock, m.BlockCount()) + for i := range blocks { + blocks[i] = m.Block(uint(i)).Snapshot() + } + return OwnedMessage{Role: m.Role(), Usage: m.Usage(), Content: blocks} +} + type ContentBlock struct { conv *Conversation msg, index uint @@ -398,22 +531,38 @@ type ContentBlock struct { 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 (b ContentBlock) Snapshot() OwnedContentBlock { + return OwnedContentBlock{ + Tag: b.Tag(), + Text: b.Text(), + ToolID: b.ToolID(), + ToolName: b.ToolName(), + IsError: b.ToolResultIsError(), + SystemMode: b.SystemMode(), + } +} + func NewAgent(cfg Config) (*Agent, error) { st, err := NewNullStore() if err != nil { @@ -426,6 +575,7 @@ func NewAgent(cfg Config) (*Agent, error) { } return NewAgentWithSession(cfg, sess, nil) } + func NewAgentWithSession(cfg Config, session *Session, conv *Conversation) (*Agent, error) { ccfg, strings, err := cfg.c() if err != nil { @@ -446,6 +596,7 @@ func NewAgentWithSession(cfg Config, session *Session, conv *Conversation) (*Age runtime.SetFinalizer(a, (*Agent).Close) return a, nil } + func (a *Agent) Close() { if a != nil && a.ptr != nil { C.panto_agent_destroy(a.ptr) @@ -456,6 +607,7 @@ func (a *Agent) SessionID() string { return goSlice(C.panto_agent_session_id(a.p 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 { @@ -467,6 +619,7 @@ func (a *Agent) SetConfig(cfg Config) error { } return nil } + func (a *Agent) Compact(overrideSystemPrompt, extraInstructions *string) (CompactionResult, error) { var co, ce *C.char if overrideSystemPrompt != nil { @@ -481,8 +634,13 @@ func (a *Agent) Compact(overrideSystemPrompt, extraInstructions *string) (Compac 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 + 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)) @@ -494,6 +652,7 @@ func (a *Agent) Run(userText string) (*Stream, error) { 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)) @@ -502,6 +661,7 @@ func (a *Agent) AddSystemMessage(text string) error { } return nil } + func (a *Agent) SetSystemPrompt(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) @@ -517,6 +677,7 @@ func (s *Stream) Close() { s.ptr = nil } } + func (s *Stream) Next() (Event, bool, error) { if s.err != nil { return Event{}, false, s.err @@ -562,6 +723,7 @@ func (s *Stream) Iter() iter.Seq[Event] { } } } + func (s *Stream) Chan() <-chan Event { ch := make(chan Event) go func() { @@ -606,6 +768,7 @@ func (cfg Config) c() (C.PantoConfig, cStrings, error) { 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 @@ -621,6 +784,7 @@ func (p ProviderConfig) c() (C.PantoProviderConfig, cStrings, error) { 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) @@ -631,6 +795,7 @@ func setOpenAI(out *C.PantoProviderConfig, cfg *OpenAIChatConfig, cs *cStrings) 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) @@ -674,7 +839,14 @@ func marshalEvent(cev *C.PantoEvent) Event { } 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)} + 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 @@ -692,18 +864,351 @@ func marshalEvent(cev *C.PantoEvent) Event { } 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)} + 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)} + 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)} + 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), + } +} + +// Tool callback support. +type ( + ToolDecl struct{ Name, Description, SchemaJSON string } + ResultPart struct{ Text string } + ResultParts []ResultPart + ToolFunc func(input string) (ResultParts, error) + Tool struct { + Decl ToolDecl + Invoke ToolFunc + } +) + +type ( + ToolCall struct{ ToolName, Input string } + ToolSourceFunc func(calls []ToolCall) ([]ResultParts, []error, error) + ToolSource struct { + Name string + Tools []ToolDecl + InvokeBatch ToolSourceFunc + } +) + +var ( + callbackHandles = map[uintptr]any{} + nextCallbackHandle uintptr = 1 +) + +func saveCallback(v any) unsafe.Pointer { + h := nextCallbackHandle + nextCallbackHandle++ + callbackHandles[h] = v + return unsafe.Pointer(h) +} +func dropCallback(p unsafe.Pointer) { delete(callbackHandles, uintptr(p)) } + +//export pantoGoToolInvoke +func pantoGoToolInvoke(ctx unsafe.Pointer, input C.PantoSlice, out *C.PantoResultParts) C.PantoStatus { + t := callbackHandles[uintptr(ctx)].(Tool) + parts, err := t.Invoke(goSlice(input)) + if err != nil { + return C.PANTO_ERROR + } + *out = makeCResultParts(parts) + return C.PANTO_OK +} + +//export pantoGoToolSourceInvokeBatch +func pantoGoToolSourceInvokeBatch(ctx unsafe.Pointer, calls *C.PantoToolCall, results *C.PantoToolCallResult, n C.size_t) C.PantoStatus { + src := callbackHandles[uintptr(ctx)].(ToolSource) + cc := unsafe.Slice(calls, int(n)) + goCalls := make([]ToolCall, int(n)) + for i, c := range cc { + goCalls[i] = ToolCall{ToolName: goSlice(c.tool_name), Input: goSlice(c.input)} + } + parts, errs, err := src.InvokeBatch(goCalls) + if err != nil { + return C.PANTO_ERROR + } + rr := unsafe.Slice(results, int(n)) + for i := range rr { + if i < len(errs) && errs[i] != nil { + rr[i].ok = false + continue + } + rr[i].ok = true + if i < len(parts) { + rr[i].parts = makeCResultParts(parts[i]) + } + } + return C.PANTO_OK +} + +//export pantoGoCallbackDestroy +func pantoGoCallbackDestroy(ctx unsafe.Pointer) { dropCallback(ctx) } + +func (a *Agent) RegisterTool(t Tool) error { + ctx := saveCallback(t) + d, free := cToolDecl(t.Decl) + defer free() + if C.panto_agent_register_tool(a.ptr, d, ctx, (C.PantoToolInvokeFn)(C.pantoGoToolInvoke), (C.PantoToolDestroyFn)(C.pantoGoCallbackDestroy)) != C.PANTO_OK { + dropCallback(ctx) + return lastError() + } + return nil +} + +func (a *Agent) RegisterToolSource(src ToolSource) error { + ctx := saveCallback(src) + cname := cSlice(src.Name) + defer C.free(unsafe.Pointer(cname.ptr)) + decls := make([]C.PantoToolDecl, len(src.Tools)) + frees := make([]func(), 0, len(src.Tools)) + for i, d := range src.Tools { + cd, fr := cToolDecl(d) + decls[i] = cd + frees = append(frees, fr) + } + defer func() { + for _, f := range frees { + f() + } + }() + var ptr *C.PantoToolDecl + if len(decls) > 0 { + ptr = &decls[0] + } + if C.panto_agent_register_tool_source(a.ptr, cname, ptr, C.size_t(len(decls)), ctx, (C.PantoToolSourceInvokeBatchFn)(C.pantoGoToolSourceInvokeBatch), (C.PantoToolDestroyFn)(C.pantoGoCallbackDestroy)) != C.PANTO_OK { + dropCallback(ctx) + return lastError() + } + return nil +} + +func cToolDecl(d ToolDecl) (C.PantoToolDecl, func()) { + n := cSlice(d.Name) + desc := cSlice(d.Description) + schema := cSlice(d.SchemaJSON) + return C.PantoToolDecl{name: n, description: desc, schema_json: schema}, func() { + C.free(unsafe.Pointer(n.ptr)) + C.free(unsafe.Pointer(desc.ptr)) + C.free(unsafe.Pointer(schema.ptr)) + } +} + +func cSlice(s string) C.PantoSlice { + cs := C.CString(s) + return C.PantoSlice{ptr: (*C.uint8_t)(unsafe.Pointer(cs)), len: C.size_t(len(s))} +} + +func makeCResultParts(parts ResultParts) C.PantoResultParts { + if len(parts) == 0 { + return C.PantoResultParts{} + } + arr := (*C.PantoResultPart)(C.calloc(C.size_t(len(parts)), C.size_t(unsafe.Sizeof(C.PantoResultPart{})))) + xs := unsafe.Slice(arr, len(parts)) + for i, p := range parts { + cs := cSlice(p.Text) + xs[i].tag = C.PANTO_RESULT_TEXT + (*C.PantoSlice)(unsafe.Pointer(&xs[i].data[0])).ptr = cs.ptr + (*C.PantoSlice)(unsafe.Pointer(&xs[i].data[0])).len = cs.len + } + return C.PantoResultParts{ptr: arr, len: C.size_t(len(parts))} +} + +func NewGoSessionStore(impl SessionStore) (*GoSessionStore, error) { + ctx := saveCallback(impl) + var out *C.PantoSessionStore + if C.panto_go_session_store_create(ctx, &out) != C.PANTO_OK { + dropCallback(ctx) + return nil, lastError() + } + s := &GoSessionStore{ptr: out, impl: impl} + runtime.SetFinalizer(s, (*GoSessionStore).Close) + return s, nil +} +func (s *GoSessionStore) cptr() *C.PantoSessionStore { return s.ptr } +func (s *GoSessionStore) Create() (*Session, error) { return CreateSession(s) } +func (s *GoSessionStore) List() ([]SessionInfo, error) { return ListSessions(s) } +func (s *GoSessionStore) Resolve(id string) (*Session, bool, error) { return ResolveSession(s, id) } +func (s *GoSessionStore) Latest() (*Session, bool, error) { return LatestSession(s) } +func (s *GoSessionStore) Load(id string) (*Conversation, bool, error) { return s.impl.Load(id) } +func (s *GoSessionStore) AppendMessages(sessionID string, messages []PersistentMessage) error { + return s.impl.AppendMessages(sessionID, messages) +} + +func (s *GoSessionStore) Close() { + if s != nil && s.ptr != nil { + C.panto_session_store_destroy(s.ptr) + s.ptr = nil + } +} + +//export pantoGoStoreCreate +func pantoGoStoreCreate(ctx unsafe.Pointer, out **C.PantoSession) C.PantoStatus { + impl := callbackHandles[uintptr(ctx)].(SessionStore) + s, err := impl.Create() + if err != nil { + return C.PANTO_ERROR + } + *out = s.ptr + runtime.SetFinalizer(s, nil) + return C.PANTO_OK +} + +//export pantoGoStoreList +func pantoGoStoreList(ctx unsafe.Pointer, out *C.PantoSessionInfoList) C.PantoStatus { + impl := callbackHandles[uintptr(ctx)].(SessionStore) + infos, err := impl.List() + if err != nil { + return C.PANTO_ERROR + } + if len(infos) == 0 { + *out = C.PantoSessionInfoList{} + return C.PANTO_OK + } + arr := (*C.PantoSessionInfo)(C.calloc(C.size_t(len(infos)), C.size_t(unsafe.Sizeof(C.PantoSessionInfo{})))) + xs := unsafe.Slice(arr, len(infos)) + for i, info := range infos { + xs[i] = cSessionInfo(info) + } + *out = C.PantoSessionInfoList{ptr: arr, len: C.size_t(len(infos))} + return C.PANTO_OK +} + +//export pantoGoStoreFreeSessionInfos +func pantoGoStoreFreeSessionInfos(ctx unsafe.Pointer, infos C.PantoSessionInfoList) { + _ = ctx + freeCSessionInfoList(infos) +} + +//export pantoGoStoreResolve +func pantoGoStoreResolve(ctx unsafe.Pointer, id C.PantoSlice, out *C.PantoSessionResult) C.PantoStatus { + impl := callbackHandles[uintptr(ctx)].(SessionStore) + s, ok, err := impl.Resolve(goSlice(id)) + if err != nil { + return C.PANTO_ERROR + } + if !ok { + *out = C.PantoSessionResult{} + return C.PANTO_OK + } + *out = C.PantoSessionResult{found: true, session: s.ptr} + runtime.SetFinalizer(s, nil) + return C.PANTO_OK +} + +//export pantoGoStoreLatest +func pantoGoStoreLatest(ctx unsafe.Pointer, out *C.PantoSessionResult) C.PantoStatus { + impl := callbackHandles[uintptr(ctx)].(SessionStore) + s, ok, err := impl.Latest() + if err != nil { + return C.PANTO_ERROR + } + if !ok { + *out = C.PantoSessionResult{} + return C.PANTO_OK + } + *out = C.PantoSessionResult{found: true, session: s.ptr} + runtime.SetFinalizer(s, nil) + return C.PANTO_OK +} + +//export pantoGoStoreLoad +func pantoGoStoreLoad(ctx unsafe.Pointer, id C.PantoSlice, out **C.PantoConversation) C.PantoStatus { + impl := callbackHandles[uintptr(ctx)].(SessionStore) + c, ok, err := impl.Load(goSlice(id)) + if err != nil { + return C.PANTO_ERROR + } + if !ok { + return C.PANTO_ERROR + } + *out = c.ptr + c.owned = false + runtime.SetFinalizer(c, nil) + return C.PANTO_OK +} + +//export pantoGoStoreAppendMessages +func pantoGoStoreAppendMessages(ctx unsafe.Pointer, sessionID C.PantoSlice, messages *C.PantoPersistentMessage, n C.size_t) C.PantoStatus { + impl := callbackHandles[uintptr(ctx)].(SessionStore) + xs := unsafe.Slice(messages, int(n)) + out := make([]PersistentMessage, int(n)) + for i, m := range xs { + var u *Usage + if m.has_usage { + uu := usage(m.usage) + u = &uu + } + out[i] = PersistentMessage{Role: MessageRole(m.role), Usage: u, Metadata: goSlice(m.metadata)} + } + if err := impl.AppendMessages(goSlice(sessionID), out); err != nil { + return C.PANTO_ERROR + } + return C.PANTO_OK +} + +func cSessionInfo(i SessionInfo) C.PantoSessionInfo { + return C.PantoSessionInfo{ + id: cSlice(i.ID), + created: cSlice(i.Created), + modified: cSlice(i.Modified), + last_user_message: cSlice(i.LastUserMessage), + base_url: cSlice(i.BaseURL), + model: cSlice(i.Model), + message_count: C.size_t(i.MessageCount), + api_style: C.PantoAPIStyle(i.APIStyle), + reasoning: C.PantoReasoningEffort(i.Reasoning), + } +} + +func freeCSessionInfoList(l C.PantoSessionInfoList) { + if l.ptr == nil { + return + } + xs := unsafe.Slice(l.ptr, int(l.len)) + for _, i := range xs { + C.free(unsafe.Pointer(i.id.ptr)) + C.free(unsafe.Pointer(i.created.ptr)) + C.free(unsafe.Pointer(i.modified.ptr)) + C.free(unsafe.Pointer(i.last_user_message.ptr)) + C.free(unsafe.Pointer(i.base_url.ptr)) + C.free(unsafe.Pointer(i.model.ptr)) + } + C.free(unsafe.Pointer(l.ptr)) } |
