package panto /* #cgo CFLAGS: -I../libpanto-c/include #cgo LDFLAGS: -L../zig-out/lib -lpanto #cgo darwin LDFLAGS: -Wl,-rpath,${SRCDIR}/../zig-out/lib #include #include 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 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" import ( "errors" "fmt" "iter" "runtime" "unsafe" ) type ( APIStyle int ReasoningEffort int Thinking int Effort int MessageRole int ContentBlockTag int EventTag int SystemMode int ) // These enums mirror libpanto-c's C enums. Each constant wraps its C value // (rather than a hand-kept iota sequence) so the Go side can never silently // drift from the ABI: if a variant is inserted in panto.h, the values shift // with it instead of desyncing. Keep one Go constant per C value. const ( OpenAIChat APIStyle = APIStyle(C.PANTO_OPENAI_CHAT) AnthropicMessages APIStyle = APIStyle(C.PANTO_ANTHROPIC_MESSAGES) OpenAIResponses APIStyle = APIStyle(C.PANTO_OPENAI_RESPONSES) OpenAICodexResponses APIStyle = APIStyle(C.PANTO_OPENAI_CODEX_RESPONSES) ) const ( ReasoningDefault ReasoningEffort = ReasoningEffort(C.PANTO_REASONING_DEFAULT) ReasoningOff ReasoningEffort = ReasoningEffort(C.PANTO_REASONING_OFF) ReasoningMinimal ReasoningEffort = ReasoningEffort(C.PANTO_REASONING_MINIMAL) ReasoningLow ReasoningEffort = ReasoningEffort(C.PANTO_REASONING_LOW) ReasoningMedium ReasoningEffort = ReasoningEffort(C.PANTO_REASONING_MEDIUM) ReasoningHigh ReasoningEffort = ReasoningEffort(C.PANTO_REASONING_HIGH) ) const ( ThinkingDisabled Thinking = Thinking(C.PANTO_THINKING_DISABLED) ThinkingEnabled Thinking = Thinking(C.PANTO_THINKING_ENABLED) ThinkingAdaptive Thinking = Thinking(C.PANTO_THINKING_ADAPTIVE) ) const ( EffortLow Effort = Effort(C.PANTO_EFFORT_LOW) EffortMedium Effort = Effort(C.PANTO_EFFORT_MEDIUM) EffortHigh Effort = Effort(C.PANTO_EFFORT_HIGH) EffortXHigh Effort = Effort(C.PANTO_EFFORT_XHIGH) EffortMax Effort = Effort(C.PANTO_EFFORT_MAX) ) const ( RoleSystem MessageRole = MessageRole(C.PANTO_ROLE_SYSTEM) RoleUser MessageRole = MessageRole(C.PANTO_ROLE_USER) RoleAssistant MessageRole = MessageRole(C.PANTO_ROLE_ASSISTANT) ) const ( BlockText ContentBlockTag = ContentBlockTag(C.PANTO_BLOCK_TEXT) BlockThinking ContentBlockTag = ContentBlockTag(C.PANTO_BLOCK_THINKING) BlockToolUse ContentBlockTag = ContentBlockTag(C.PANTO_BLOCK_TOOL_USE) BlockToolResult ContentBlockTag = ContentBlockTag(C.PANTO_BLOCK_TOOL_RESULT) BlockSystem ContentBlockTag = ContentBlockTag(C.PANTO_BLOCK_SYSTEM) BlockCompactionSummary ContentBlockTag = ContentBlockTag(C.PANTO_BLOCK_COMPACTION_SUMMARY) ) const ( SystemAppend SystemMode = SystemMode(C.PANTO_SYSTEM_APPEND) SystemReplace SystemMode = SystemMode(C.PANTO_SYSTEM_REPLACE) ) const ( EventMessageStart EventTag = EventTag(C.PANTO_EVENT_MESSAGE_START) EventBlockStart EventTag = EventTag(C.PANTO_EVENT_BLOCK_START) EventToolDetails EventTag = EventTag(C.PANTO_EVENT_TOOL_DETAILS) EventContentDelta EventTag = EventTag(C.PANTO_EVENT_CONTENT_DELTA) EventBlockComplete EventTag = EventTag(C.PANTO_EVENT_BLOCK_COMPLETE) EventMessageComplete EventTag = EventTag(C.PANTO_EVENT_MESSAGE_COMPLETE) EventProviderRetry EventTag = EventTag(C.PANTO_EVENT_PROVIDER_RETRY) EventToolDispatchStart EventTag = EventTag(C.PANTO_EVENT_TOOL_DISPATCH_START) EventToolDispatchResult EventTag = EventTag(C.PANTO_EVENT_TOOL_DISPATCH_RESULT) EventToolDispatchComplete EventTag = EventTag(C.PANTO_EVENT_TOOL_DISPATCH_COMPLETE) EventTurnComplete EventTag = EventTag(C.PANTO_EVENT_TURN_COMPLETE) ) type Config struct { Provider ProviderConfig Compaction CompactionConfig Retry RetryConfig } type ProviderConfig struct { OpenAIChat *OpenAIChatConfig AnthropicMessages *AnthropicMessagesConfig OpenAIResponses *OpenAIResponsesConfig } type OpenAIChatConfig struct { APIKey, BaseURL, Model string Reasoning ReasoningEffort MaxTokens uint32 } type OpenAIResponsesConfig 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 } CompactionResult struct { Compacted bool KeptTurns, SummarizedMessages uint } ) // Event is one streaming-progress notification from a Stream. The concrete // type is one of the *Event structs below; type-switch on it, or branch on // EventType() when only the kind matters. The terminal event of a turn is // TurnCompleteEvent (EventType() == EventTurnComplete). type Event interface { // EventType reports the kind of event, mirroring the concrete type. It // lets callers branch without a type switch (e.g. skip everything until // EventMessageComplete) and is the cheap terminal check used by Iter. EventType() EventTag } // MessageStartEvent: an assistant message began streaming. type MessageStartEvent struct{ Role MessageRole } // BlockStartEvent: a content block opened. type BlockStartEvent struct { BlockType ContentBlockTag Index uint } // ToolDetailsEvent: the tool identity (id + name) for a ToolUse block resolved. type ToolDetailsEvent struct { Index uint ID, Name string } // ContentDeltaEvent: streaming content for the open block (text/thinking/args). type ContentDeltaEvent struct { Index uint Delta string } // BlockCompleteEvent: a content block closed. type BlockCompleteEvent struct { Index uint BlockType ContentBlockTag } // MessageCompleteEvent: one assistant message finished streaming. The message // is the current tail of Agent.Conversation(); MessageIndex is its index there // and BlockCount its number of content blocks, so the full content can be read // back via Agent.Conversation().Message(MessageIndex) without marshalling every // block onto the event. Usage is the wire-reported token usage, if any. type MessageCompleteEvent struct { Role MessageRole MessageIndex uint BlockCount uint Usage *Usage } // ProviderRetryEvent: a provider retry was scheduled before the next attempt. type ProviderRetryEvent struct { Attempt, MaxAttempts uint DelayMS uint64 ErrorName string StatusCode *uint16 RetryAfterMS *uint64 Message string Compaction bool } // ToolDispatchStartEvent: the agent began dispatching the just-completed // message's tool calls. type ToolDispatchStartEvent struct{ Count uint } // ToolDispatchResultEvent: a single tool's result became available, before the // aggregate ToolDispatchCompleteEvent. MessageIndex is the index of the // ToolResult-carrying message in the conversation. type ToolDispatchResultEvent struct{ MessageIndex uint } // ToolDispatchCompleteEvent: the agent appended a user(ToolResult) message. type ToolDispatchCompleteEvent struct{ MessageIndex uint } // TurnCompleteEvent: the turn terminal. Emitted exactly once; every Next after // it returns nil. type TurnCompleteEvent struct{} func (MessageStartEvent) EventType() EventTag { return EventMessageStart } func (BlockStartEvent) EventType() EventTag { return EventBlockStart } func (ToolDetailsEvent) EventType() EventTag { return EventToolDetails } func (ContentDeltaEvent) EventType() EventTag { return EventContentDelta } func (BlockCompleteEvent) EventType() EventTag { return EventBlockComplete } func (MessageCompleteEvent) EventType() EventTag { return EventMessageComplete } func (ProviderRetryEvent) EventType() EventTag { return EventProviderRetry } func (ToolDispatchStartEvent) EventType() EventTag { return EventToolDispatchStart } func (ToolDispatchResultEvent) EventType() EventTag { return EventToolDispatchResult } func (ToolDispatchCompleteEvent) EventType() EventTag { return EventToolDispatchComplete } func (TurnCompleteEvent) EventType() EventTag { return EventTurnComplete } type ( Agent struct { ptr *C.PantoAgent retainedSession *Session retainedConv *Conversation retainedStoreCloser func() } Stream struct { ptr *C.PantoStream err error done bool } ) type Conversation struct { ptr *C.PantoConversation owned bool } type ( Session struct{ ptr *C.PantoSession } SessionInfo struct { ID, Created, Modified, LastUserMessage, BaseURL, Model string MessageCount uint APIStyle APIStyle Reasoning ReasoningEffort } ) type SessionStore interface { 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 // Per-message wire identity: the provider that produced this message. May // drift within a conversation (compaction on another model, mid-chat // switch), so persist these per message rather than from static config. APIStyle APIStyle BaseURL string Model string Reasoning ReasoningEffort } 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 // Thinking-block signature provenance. Populated for blocks delivered to // AppendMessages; empty otherwise. To replay a thinking block faithfully, // persist all four and feed them back via ThinkingBlock — Anthropic drops // a thinking block whose signature lacks a matching origin. ThinkingSignature string ThinkingSignatureStyle APIStyle ThinkingSignatureBaseURL string ThinkingSignatureModel string } 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) 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)) 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) 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) s.ptr = nil } } 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() } s := &Session{out} runtime.SetFinalizer(s, (*Session).Close) return s, nil } // NewSession mints a *Session bound to store, carrying info (at least a // store-assigned ID). Unlike CreateSession/ResolveSession/LatestSession it does // NOT dispatch into the store's vtable, so a custom SessionStore can call it // from its own Create/Resolve/Latest to build the *Session it must return // without recursing back into itself. The session's bound store is used for // later load/append; pass the store the session belongs to. func NewSession(store cSessionStore, info SessionInfo) (*Session, error) { ci := cSessionInfo(info) defer freeCSessionInfo(ci) var out *C.PantoSession if C.panto_session_create(store.cptr(), ci, &out) != C.PANTO_OK { return nil, lastError() } s := &Session{out} runtime.SetFinalizer(s, (*Session).Close) return s, nil } // NewNullSession mints a standalone *Session backed by a null store — load and // append are no-ops. Useful as a placeholder when a store performs its own // persistence outside the session's bound store. func NewNullSession() (*Session, error) { var out *C.PantoSession if C.panto_session_create_null(&out) != C.PANTO_OK { return nil, lastError() } s := &Session{out} runtime.SetFinalizer(s, (*Session).Close) return s, nil } func ResolveSession(store cSessionStore, 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 cSessionStore) (*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 cSessionStore) ([]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 } // Block is one immutable content block — text, thinking, tool use, tool // result, or system text. Build it with the constructors below (TextBlock, // ToolResultBlock, …) and assemble blocks into a message with UserMessage / // AssistantMessage. This is the single content-block vocabulary used both to // open a turn (Agent.Run) and to rebuild a persisted message for resumption // (Conversation.AddMessage). type Block struct { tag ContentBlockTag text string toolID string toolName string input string isError bool systemMode SystemMode thinkingSignature string thinkingSigStyle APIStyle thinkingSigBaseURL string thinkingSigModel string } // TextBlock is a plain text block. func TextBlock(text string) Block { return Block{tag: BlockText, text: text} } // ThinkingBlock is an assistant reasoning block. signature/baseURL/model carry // the signature provenance (see OwnedContentBlock); pass "" for signature when // the provider emitted none, in which case style/baseURL/model are ignored. func ThinkingBlock(text, signature string, style APIStyle, baseURL, model string) Block { return Block{tag: BlockThinking, text: text, thinkingSignature: signature, thinkingSigStyle: style, thinkingSigBaseURL: baseURL, thinkingSigModel: model} } // ToolUseBlock is an assistant tool call. func ToolUseBlock(id, name, input string) Block { return Block{tag: BlockToolUse, toolID: id, toolName: name, input: input} } // ToolResultBlock is a user-role tool result keyed by the originating tool-use // id. Used to resume a turn whose tool calls the embedder handled itself. func ToolResultBlock(toolUseID, text string, isError bool) Block { return Block{tag: BlockToolResult, toolID: toolUseID, text: text, isError: isError} } // SystemBlock is a system-prompt block (append or replace mode). func SystemBlock(text string, mode SystemMode) Block { return Block{tag: BlockSystem, text: text, systemMode: mode} } // MessageIdentity is the wire identity (provider style/baseURL/model + // reasoning) that produced a message. Attach it to an InputMessage when // rebuilding a conversation for resumption: it is the per-message source of // truth for replaying thinking signatures and is preserved through compaction, // so a kept-verbatim turn is not re-stamped with the compaction model. Set it // from a PersistentMessage's APIStyle/BaseURL/Model/Reasoning. type MessageIdentity struct { APIStyle APIStyle BaseURL string Model string Reasoning ReasoningEffort } // InputMessage is one role-tagged message assembled from value Blocks, with // optional usage and wire identity. Build it with UserMessage / AssistantMessage // (or set the fields directly), then pass it to Agent.Run (user role) or // Conversation.AddMessage (any role, for replay). type InputMessage struct { Role MessageRole Usage *Usage Identity *MessageIdentity Blocks []Block } // UserMessage assembles a user-role message — the argument to Agent.Run. A // plain chat turn is a single TextBlock; a turn resuming after embedder-handled // tool calls is one or more ToolResultBlocks. func UserMessage(blocks ...Block) InputMessage { return InputMessage{Role: RoleUser, Blocks: blocks} } // AssistantMessage assembles an assistant-role message with optional usage — // used to rebuild a persisted turn via Conversation.AddMessage. func AssistantMessage(usage *Usage, blocks ...Block) InputMessage { return InputMessage{Role: RoleAssistant, Usage: usage, Blocks: blocks} } // WithIdentity returns a copy of the message tagged with the given wire // identity (see MessageIdentity). func (m InputMessage) WithIdentity(id MessageIdentity) InputMessage { m.Identity = &id return m } // buildCBuilder materializes an InputMessage as a transient C message builder, // pushing each block in order and attaching identity if present. The returned // builder is owned by the caller and must be consumed by exactly one of // panto_agent_run / panto_conversation_add_message (both of which destroy it). // On error it is destroyed here and nil is returned. func buildCBuilder(msg InputMessage) (*C.PantoMessageBuilder, error) { p := C.panto_message_builder_create(C.PantoMessageRole(msg.Role)) if p == nil { return nil, lastError() } committed := false defer func() { if !committed { C.panto_message_builder_destroy(p) } }() if msg.Identity != nil { cb := C.CString(msg.Identity.BaseURL) defer C.free(unsafe.Pointer(cb)) cm := C.CString(msg.Identity.Model) defer C.free(unsafe.Pointer(cm)) if C.panto_message_builder_set_identity(p, C.PantoAPIStyle(msg.Identity.APIStyle), cb, cm, C.PantoReasoningEffort(msg.Identity.Reasoning)) != C.PANTO_OK { return nil, lastError() } } for _, b := range msg.Blocks { if err := pushBlock(p, b); err != nil { return nil, err } } committed = true return p, nil } func pushBlock(p *C.PantoMessageBuilder, b Block) error { switch b.tag { case BlockText: ct := C.CString(b.text) defer C.free(unsafe.Pointer(ct)) if C.panto_message_builder_add_text(p, ct) != C.PANTO_OK { return lastError() } case BlockThinking: ct := C.CString(b.text) defer C.free(unsafe.Pointer(ct)) cs := C.CString(b.thinkingSignature) defer C.free(unsafe.Pointer(cs)) cb := C.CString(b.thinkingSigBaseURL) defer C.free(unsafe.Pointer(cb)) cm := C.CString(b.thinkingSigModel) defer C.free(unsafe.Pointer(cm)) if C.panto_message_builder_add_thinking(p, ct, cs, C.PantoAPIStyle(b.thinkingSigStyle), cb, cm) != C.PANTO_OK { return lastError() } case BlockToolUse: cid := C.CString(b.toolID) defer C.free(unsafe.Pointer(cid)) cn := C.CString(b.toolName) defer C.free(unsafe.Pointer(cn)) ci := C.CString(b.input) defer C.free(unsafe.Pointer(ci)) if C.panto_message_builder_add_tool_use(p, cid, cn, ci) != C.PANTO_OK { return lastError() } case BlockToolResult: cid := C.CString(b.toolID) defer C.free(unsafe.Pointer(cid)) ct := C.CString(b.text) defer C.free(unsafe.Pointer(ct)) if C.panto_message_builder_add_tool_result(p, cid, ct, C.bool(b.isError)) != C.PANTO_OK { return lastError() } case BlockSystem: ct := C.CString(b.text) defer C.free(unsafe.Pointer(ct)) if C.panto_message_builder_add_system(p, ct, C.PantoSystemMode(b.systemMode)) != C.PANTO_OK { return lastError() } default: return fmt.Errorf("unknown content block tag %d", int(b.tag)) } return nil } // AddMessage commits msg as one message of msg.Role (with its optional usage // and identity) — the block-level path for rebuilding a persisted conversation // containing tool calls or thinking that the text-only AddUserText/ // AddAssistantText cannot. func (c *Conversation) AddMessage(msg InputMessage) error { b, err := buildCBuilder(msg) if err != nil { return err } var cu C.PantoUsage has := false if msg.Usage != nil { cu = cUsage(*msg.Usage) has = true } // panto_conversation_add_message consumes (destroys) the builder on every // outcome, so there is nothing to free here afterwards. if C.panto_conversation_add_message(c.ptr, b, C.bool(has), cu) != 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} } 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 } 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) { return NewAgentWithSession(cfg, nil, 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 csession *C.PantoSession if session != nil { csession = session.ptr runtime.SetFinalizer(session, nil) } 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, csession, cconv, &out) != C.PANTO_OK { return nil, lastError() } a := &Agent{ptr: out, retainedSession: session, retainedConv: conv} 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 } if a != nil { if a.retainedSession != nil { a.retainedSession.ptr = nil a.retainedSession = nil } if a.retainedConv != nil { a.retainedConv.ptr = nil a.retainedConv = nil } if a.retainedStoreCloser != nil { a.retainedStoreCloser() a.retainedStoreCloser = 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 } // Run opens a turn from a user-role message and returns a resumable Stream. // Build the message with UserMessage: a single TextBlock for a plain prompt, // or ToolResultBlocks to resume a turn whose tool calls the embedder handled // itself after breaking out of a prior stream. The message must be user-role. func (a *Agent) Run(msg InputMessage) (*Stream, error) { if msg.Role != RoleUser { return nil, fmt.Errorf("Run requires a user-role message, got role %d", int(msg.Role)) } b, err := buildCBuilder(msg) if err != nil { return nil, err } // panto_agent_run consumes (destroys) the builder on every outcome. var out *C.PantoStream if C.panto_agent_run(a.ptr, b, &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 } } // Reopen resets a failed stream back to its turn-open boundary so the caller // can resume Next() after changing the agent config (e.g. catch a terminal // bad-request, swap the provider config via Agent.SetConfig, and retry the // SAME turn without re-appending the user message). It is valid only on a // stream whose Next() has surfaced a terminal error; otherwise it returns an // error. On success the stream's local error/done state is cleared so the // iteration can continue. func (s *Stream) Reopen() error { if s == nil || s.ptr == nil { return fmt.Errorf("reopen on a closed stream") } if C.panto_stream_reopen(s.ptr) != C.PANTO_OK { return lastError() } s.err = nil s.done = false return nil } func (s *Stream) Next() (Event, bool, error) { if s.err != nil { return nil, false, s.err } if s.done { return nil, 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.EventType() == EventTurnComplete { s.done = true } return ev, true, nil case C.PANTO_NEXT_DONE: s.done = true return nil, false, nil case C.PANTO_NEXT_ERROR: s.err = lastError() return nil, false, s.err default: s.err = fmt.Errorf("unknown panto next status %d", int(status)) return nil, 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.EventType() == 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 n := 0 if p.OpenAIChat != nil { n++ } if p.AnthropicMessages != nil { n++ } if p.OpenAIResponses != nil { n++ } if n != 1 { 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 } if p.OpenAIResponses != nil { out.tag = C.PANTO_OPENAI_RESPONSES setOpenAIResponses(&out, p.OpenAIResponses, &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 setOpenAIResponses(out *C.PantoProviderConfig, cfg *OpenAIResponsesConfig, 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.PantoOpenAIResponsesConfig)(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 { switch cev.tag { case C.PANTO_EVENT_MESSAGE_START: return MessageStartEvent{Role: MessageRole(*(*C.PantoMessageRole)(unsafe.Pointer(&cev.data[0])))} case C.PANTO_EVENT_BLOCK_START: b := (*C.PantoBlockStart)(unsafe.Pointer(&cev.data[0])) return BlockStartEvent{BlockType: ContentBlockTag(b.block_type), Index: uint(b.index)} case C.PANTO_EVENT_TOOL_DETAILS: t := (*C.PantoToolDetails)(unsafe.Pointer(&cev.data[0])) return ToolDetailsEvent{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])) return ContentDeltaEvent{Index: uint(d.index), Delta: goSlice(d.delta)} case C.PANTO_EVENT_BLOCK_COMPLETE: b := (*C.PantoBlockComplete)(unsafe.Pointer(&cev.data[0])) return BlockCompleteEvent{Index: uint(b.index), BlockType: ContentBlockTag(b.block_type)} case C.PANTO_EVENT_MESSAGE_COMPLETE: m := (*C.PantoMessageComplete)(unsafe.Pointer(&cev.data[0])) ev := MessageCompleteEvent{ Role: MessageRole(m.role), MessageIndex: uint(m.message_index), BlockCount: uint(m.block_count), } if m.has_usage { u := usage(m.usage) ev.Usage = &u } return ev case C.PANTO_EVENT_PROVIDER_RETRY: r := (*C.PantoProviderRetry)(unsafe.Pointer(&cev.data[0])) ev := ProviderRetryEvent{ 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.StatusCode = &v } if r.has_retry_after_ms { v := uint64(r.retry_after_ms) ev.RetryAfterMS = &v } return ev case C.PANTO_EVENT_TOOL_DISPATCH_START: t := (*C.PantoToolDispatchStart)(unsafe.Pointer(&cev.data[0])) return ToolDispatchStartEvent{Count: uint(t.count)} case C.PANTO_EVENT_TOOL_DISPATCH_RESULT: t := (*C.PantoToolDispatchComplete)(unsafe.Pointer(&cev.data[0])) return ToolDispatchResultEvent{MessageIndex: uint(t.message_index)} case C.PANTO_EVENT_TOOL_DISPATCH_COMPLETE: t := (*C.PantoToolDispatchComplete)(unsafe.Pointer(&cev.data[0])) return ToolDispatchCompleteEvent{MessageIndex: uint(t.message_index)} case C.PANTO_EVENT_TURN_COMPLETE: return TurnCompleteEvent{} } return TurnCompleteEvent{} } 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), } } // Tool callback support. type ( ToolDecl struct{ Name, Description, SchemaJSON string } ResultPart struct { Text string // Media carries raw bytes, not base64. libpanto detects/resizes/encodes // supported images and PDFs for the provider; MediaType is an optional // MIME hint. MediaType string Media []byte } ResultParts []ResultPart ToolFunc func(input string) (ResultParts, error) Tool struct { Decl ToolDecl Invoke ToolFunc } ) 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 pantoGoCallbackDestroy func pantoGoCallbackDestroy(ctx unsafe.Pointer) { dropCallback(ctx) } // RegisterTool registers a single tool. Note: libpanto invokes each tool call // on its own thread, so Invoke may run concurrently — guard any shared state. // // TODO: back all Go tool registrations with one process-global Zig ToolSource so // libpanto delivers the whole turn's batch on a single thread and we fan out onto // goroutines instead of libpanto threads. Pure efficiency (cheaper than N pthreads // per turn); the public API stays exactly this. Punted as not worth the work yet. 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 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 { if p.Media == nil { 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 continue } xs[i].tag = C.PANTO_RESULT_MEDIA m := (*C.PantoMediaPart)(unsafe.Pointer(&xs[i].data[0])) m.data = C.PantoSlice{ptr: (*C.uint8_t)(C.CBytes(p.Media)), len: C.size_t(len(p.Media))} if p.MediaType != "" { m.media_type = cSlice(p.MediaType) m.has_media_type = C.bool(true) } } 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), Content: goOwnedBlocks(m.content, m.content_len), APIStyle: APIStyle(m.api_style), BaseURL: goSlice(m.base_url), Model: goSlice(m.model), Reasoning: ReasoningEffort(m.reasoning), } } if err := impl.AppendMessages(goSlice(sessionID), out); err != nil { return C.PANTO_ERROR } return C.PANTO_OK } func goOwnedBlocks(ptr *C.PantoOwnedContentBlock, n C.size_t) []OwnedContentBlock { if ptr == nil || n == 0 { return nil } xs := unsafe.Slice(ptr, int(n)) out := make([]OwnedContentBlock, int(n)) for i, b := range xs { out[i] = OwnedContentBlock{ Tag: ContentBlockTag(b.tag), Text: goSlice(b.text), ToolID: goSlice(b.tool_id), ToolName: goSlice(b.tool_name), IsError: bool(b.is_error), SystemMode: SystemMode(b.system_mode), ThinkingSignature: goSlice(b.thinking_signature), ThinkingSignatureStyle: APIStyle(b.thinking_signature_api_style), ThinkingSignatureBaseURL: goSlice(b.thinking_signature_base_url), ThinkingSignatureModel: goSlice(b.thinking_signature_model), } } return out } 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 freeCSessionInfo(i C.PantoSessionInfo) { 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)) } func freeCSessionInfoList(l C.PantoSessionInfoList) { if l.ptr == nil { return } xs := unsafe.Slice(l.ptr, int(l.len)) for _, i := range xs { freeCSessionInfo(i) } C.free(unsafe.Pointer(l.ptr)) }