summaryrefslogtreecommitdiff
path: root/libpanto-go/panto.go
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-07-07 11:26:32 -0600
committert <t@tjp.lol>2026-07-07 11:26:45 -0600
commitf83578fdc9264019a1a1cef8c5484a161167d3dd (patch)
tree888f11767f944d61e5ca8eb92fa1b2dba295a4b8 /libpanto-go/panto.go
initial commit, moved libpanto over from the pantograph repo
Diffstat (limited to 'libpanto-go/panto.go')
-rw-r--r--libpanto-go/panto.go1592
1 files changed, 1592 insertions, 0 deletions
diff --git a/libpanto-go/panto.go b/libpanto-go/panto.go
new file mode 100644
index 0000000..abaa84e
--- /dev/null
+++ b/libpanto-go/panto.go
@@ -0,0 +1,1592 @@
+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 <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 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))
+}