package panto /* #cgo CFLAGS: -I../libpanto-c/include #cgo LDFLAGS: -L../libpanto-c/zig-out/lib -lpanto #include #include */ import "C" import ( "errors" "fmt" "iter" "runtime" "unsafe" ) type APIStyle int type ReasoningEffort int type Thinking int type Effort int type MessageRole int type ContentBlockTag int type EventTag int type SystemMode int const ( OpenAIChat APIStyle = iota AnthropicMessages ) const ( ReasoningDefault ReasoningEffort = iota ReasoningOff ReasoningMinimal ReasoningLow ReasoningMedium ReasoningHigh ) const ( ThinkingDisabled Thinking = iota ThinkingEnabled ThinkingAdaptive ) const ( EffortLow Effort = iota EffortMedium EffortHigh EffortXHigh EffortMax ) const ( RoleSystem MessageRole = iota RoleUser RoleAssistant ) const ( BlockText ContentBlockTag = iota BlockThinking BlockToolUse BlockToolResult BlockSystem BlockCompactionSummary ) const ( SystemAppend SystemMode = iota SystemReplace ) const ( EventMessageStart EventTag = iota EventBlockStart EventToolDetails EventContentDelta EventBlockComplete EventMessageComplete EventProviderRetry EventToolDispatchStart EventToolDispatchComplete EventTurnComplete ) type Config struct { Provider ProviderConfig Compaction CompactionConfig Retry RetryConfig } type ProviderConfig struct { OpenAIChat *OpenAIChatConfig AnthropicMessages *AnthropicMessagesConfig } type OpenAIChatConfig struct { APIKey, BaseURL, Model string Reasoning ReasoningEffort MaxTokens uint32 } type AnthropicMessagesConfig struct { APIKey, BaseURL, Model, APIVersion string MaxTokens uint32 Thinking Thinking Effort Effort ThinkingBudgetTokens *uint32 ThinkingInterleaved bool } type CompactionConfig struct { KeepVerbatim uint32 Model *ProviderConfig CompactionPrompt *string } type RetryConfig struct { MaxAttempts uint InitialDelayMS, MaxDelayMS uint64 Multiplier float64 Jitter bool } type Usage struct{ Input, Output, CacheRead, CacheWrite, Reasoning uint64 } type CompactionResult struct { Compacted bool KeptTurns, SummarizedMessages uint } type Event struct { Tag EventTag MessageStart MessageRole BlockStart BlockStart ToolDetails ToolDetails ContentDelta ContentDelta BlockComplete BlockComplete MessageComplete MessageComplete ProviderRetry ProviderRetry ToolDispatchStart ToolDispatchStart ToolDispatchComplete ToolDispatchComplete } type BlockStart struct { BlockType ContentBlockTag Index uint } type ToolDetails struct { Index uint ID, Name string } type ContentDelta struct { Index uint Delta string } type BlockComplete struct { Index uint BlockType ContentBlockTag } type MessageComplete struct{ Usage *Usage } type ProviderRetry struct { Attempt, MaxAttempts uint DelayMS uint64 ErrorName string StatusCode *uint16 RetryAfterMS *uint64 Message string Compaction bool } type ToolDispatchStart struct{ Count uint } type ToolDispatchComplete struct{ MessageIndex uint } type Agent struct{ ptr *C.PantoAgent } type Stream struct { ptr *C.PantoStream err error done bool } type Conversation struct { ptr *C.PantoConversation owned bool } type Session struct{ ptr *C.PantoSession } type SessionInfo struct { ID, Created, Modified, LastUserMessage, BaseURL, Model string MessageCount uint APIStyle APIStyle Reasoning ReasoningEffort } type SessionStore interface { cptr() *C.PantoSessionStore Close() } type NullStore struct{ ptr *C.PantoSessionStore } type FileSystemJSONLStore struct{ ptr *C.PantoSessionStore } type cStrings []*C.char func (cs cStrings) free() { for _, s := range cs { C.free(unsafe.Pointer(s)) } } func Init() { C.panto_init() } func Deinit() { C.panto_deinit() } func lastError() error { s := C.panto_last_error() if s.ptr == nil || s.len == 0 { return errors.New("libpanto error") } return errors.New(C.GoStringN((*C.char)(unsafe.Pointer(s.ptr)), C.int(s.len))) } func NewNullStore() (*NullStore, error) { var out *C.PantoSessionStore if C.panto_null_store_create(&out) != C.PANTO_OK { return nil, lastError() } s := &NullStore{out} runtime.SetFinalizer(s, (*NullStore).Close) return s, nil } func (s *NullStore) cptr() *C.PantoSessionStore { return s.ptr } func (s *NullStore) Close() { if s != nil && s.ptr != nil { C.panto_session_store_destroy(s.ptr) s.ptr = nil } } func NewFileSystemJSONLStore(dir, metadata string) (*FileSystemJSONLStore, error) { cd := C.CString(dir) defer C.free(unsafe.Pointer(cd)) cm := C.CString(metadata) defer C.free(unsafe.Pointer(cm)) var out *C.PantoSessionStore if C.panto_file_system_jsonl_store_create(cd, cm, &out) != C.PANTO_OK { return nil, lastError() } s := &FileSystemJSONLStore{out} runtime.SetFinalizer(s, (*FileSystemJSONLStore).Close) return s, nil } func (s *FileSystemJSONLStore) cptr() *C.PantoSessionStore { return s.ptr } func (s *FileSystemJSONLStore) Close() { if s != nil && s.ptr != nil { C.panto_session_store_destroy(s.ptr) s.ptr = nil } } func CreateSession(store SessionStore) (*Session, error) { var out *C.PantoSession if C.panto_session_store_create_session(store.cptr(), &out) != C.PANTO_OK { return nil, lastError() } s := &Session{out} runtime.SetFinalizer(s, (*Session).Close) return s, nil } func ResolveSession(store SessionStore, id string) (*Session, bool, error) { cid := C.CString(id) defer C.free(unsafe.Pointer(cid)) var r C.PantoSessionResult if C.panto_session_store_resolve(store.cptr(), cid, &r) != C.PANTO_OK { return nil, false, lastError() } if !bool(r.found) { return nil, false, nil } s := &Session{r.session} runtime.SetFinalizer(s, (*Session).Close) return s, true, nil } func LatestSession(store SessionStore) (*Session, bool, error) { var r C.PantoSessionResult if C.panto_session_store_latest(store.cptr(), &r) != C.PANTO_OK { return nil, false, lastError() } if !bool(r.found) { return nil, false, nil } s := &Session{r.session} runtime.SetFinalizer(s, (*Session).Close) return s, true, nil } func ListSessions(store SessionStore) ([]SessionInfo, error) { var l C.PantoSessionInfoList if C.panto_session_store_list(store.cptr(), &l) != C.PANTO_OK { return nil, lastError() } defer C.panto_session_info_list_destroy(l) out := make([]SessionInfo, int(l.len)) if l.ptr != nil { xs := unsafe.Slice(l.ptr, int(l.len)) for i, x := range xs { out[i] = sessionInfo(x) } } return out, nil } func (s *Session) Close() { if s != nil && s.ptr != nil { C.panto_session_destroy(s.ptr) s.ptr = nil } } func (s *Session) Load() (*Conversation, error) { var out *C.PantoConversation if C.panto_session_load(s.ptr, &out) != C.PANTO_OK { return nil, lastError() } c := &Conversation{ptr: out, owned: true} runtime.SetFinalizer(c, (*Conversation).Close) return c, nil } func NewConversation() (*Conversation, error) { var out *C.PantoConversation if C.panto_conversation_create(&out) != C.PANTO_OK { return nil, lastError() } c := &Conversation{ptr: out, owned: true} runtime.SetFinalizer(c, (*Conversation).Close) return c, nil } func (c *Conversation) Close() { if c != nil && c.owned && c.ptr != nil { C.panto_conversation_destroy(c.ptr) c.ptr = nil } } func (c *Conversation) MessageCount() uint { return uint(C.panto_conversation_message_count(c.ptr)) } func (c *Conversation) Message(i uint) Message { return Message{conv: c, index: i} } func (c *Conversation) AddSystemMessage(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) if C.panto_conversation_add_system_message(c.ptr, ct) != C.PANTO_OK { return lastError() } return nil } func (c *Conversation) ReplaceSystemMessage(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) if C.panto_conversation_replace_system_message(c.ptr, ct) != C.PANTO_OK { return lastError() } return nil } func (c *Conversation) AddUserText(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) if C.panto_conversation_add_user_text(c.ptr, ct) != C.PANTO_OK { return lastError() } return nil } func (c *Conversation) AddAssistantText(text string, u *Usage) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) var cu C.PantoUsage has := false if u != nil { cu = cUsage(*u) has = true } if C.panto_conversation_add_assistant_text(c.ptr, ct, C.bool(has), cu) != C.PANTO_OK { return lastError() } return nil } func (c *Conversation) AddCompactionSummary(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) if C.panto_conversation_add_compaction_summary(c.ptr, ct) != C.PANTO_OK { return lastError() } return nil } type Message struct { conv *Conversation index uint } func (m Message) Role() MessageRole { return MessageRole(C.panto_conversation_message_role(m.conv.ptr, C.size_t(m.index))) } func (m Message) Usage() *Usage { if !bool(C.panto_conversation_message_has_usage(m.conv.ptr, C.size_t(m.index))) { return nil } u := usage(C.panto_conversation_message_usage(m.conv.ptr, C.size_t(m.index))) return &u } func (m Message) BlockCount() uint { return uint(C.panto_conversation_message_block_count(m.conv.ptr, C.size_t(m.index))) } func (m Message) Block(i uint) ContentBlock { return ContentBlock{conv: m.conv, msg: m.index, index: i} } type ContentBlock struct { conv *Conversation msg, index uint } func (b ContentBlock) Tag() ContentBlockTag { return ContentBlockTag(C.panto_conversation_block_tag(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index))) } func (b ContentBlock) Text() string { return goSlice(C.panto_conversation_block_text(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index))) } func (b ContentBlock) ToolID() string { return goSlice(C.panto_conversation_block_tool_id(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index))) } func (b ContentBlock) ToolName() string { return goSlice(C.panto_conversation_block_tool_name(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index))) } func (b ContentBlock) ToolResultIsError() bool { return bool(C.panto_conversation_block_tool_result_is_error(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index))) } func (b ContentBlock) SystemMode() SystemMode { return SystemMode(C.panto_conversation_block_system_mode(b.conv.ptr, C.size_t(b.msg), C.size_t(b.index))) } func NewAgent(cfg Config) (*Agent, error) { st, err := NewNullStore() if err != nil { return nil, err } sess, err := CreateSession(st) st.Close() if err != nil { return nil, err } return NewAgentWithSession(cfg, sess, nil) } func NewAgentWithSession(cfg Config, session *Session, conv *Conversation) (*Agent, error) { ccfg, strings, err := cfg.c() if err != nil { return nil, err } defer strings.free() var cconv *C.PantoConversation if conv != nil { cconv = conv.ptr conv.owned = false runtime.SetFinalizer(conv, nil) } var out *C.PantoAgent if C.panto_agent_create(&ccfg, session.ptr, cconv, &out) != C.PANTO_OK { return nil, lastError() } a := &Agent{out} runtime.SetFinalizer(a, (*Agent).Close) return a, nil } func (a *Agent) Close() { if a != nil && a.ptr != nil { C.panto_agent_destroy(a.ptr) a.ptr = nil } } func (a *Agent) SessionID() string { return goSlice(C.panto_agent_session_id(a.ptr)) } func (a *Agent) Conversation() *Conversation { return &Conversation{ptr: C.panto_agent_conversation(a.ptr), owned: false} } func (a *Agent) SetConfig(cfg Config) error { ccfg, strings, err := cfg.c() if err != nil { return err } defer strings.free() if C.panto_agent_set_config(a.ptr, &ccfg) != C.PANTO_OK { return lastError() } return nil } func (a *Agent) Compact(overrideSystemPrompt, extraInstructions *string) (CompactionResult, error) { var co, ce *C.char if overrideSystemPrompt != nil { co = C.CString(*overrideSystemPrompt) defer C.free(unsafe.Pointer(co)) } if extraInstructions != nil { ce = C.CString(*extraInstructions) defer C.free(unsafe.Pointer(ce)) } var out C.PantoCompactionResult if C.panto_agent_compact(a.ptr, co, ce, &out) != C.PANTO_OK { return CompactionResult{}, lastError() } return CompactionResult{Compacted: bool(out.compacted), KeptTurns: uint(out.kept_turns), SummarizedMessages: uint(out.summarized_messages)}, nil } func (a *Agent) Run(userText string) (*Stream, error) { ct := C.CString(userText) defer C.free(unsafe.Pointer(ct)) var out *C.PantoStream if C.panto_agent_run(a.ptr, ct, &out) != C.PANTO_OK { return nil, lastError() } s := &Stream{ptr: out} runtime.SetFinalizer(s, (*Stream).Close) return s, nil } func (a *Agent) AddSystemMessage(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) if C.panto_agent_add_system_message(a.ptr, ct) != C.PANTO_OK { return lastError() } return nil } func (a *Agent) SetSystemPrompt(text string) error { ct := C.CString(text) defer C.free(unsafe.Pointer(ct)) if C.panto_agent_set_system_prompt(a.ptr, ct) != C.PANTO_OK { return lastError() } return nil } func (s *Stream) Close() { if s != nil && s.ptr != nil { C.panto_stream_destroy(s.ptr) s.ptr = nil } } func (s *Stream) Next() (Event, bool, error) { if s.err != nil { return Event{}, false, s.err } if s.done { return Event{}, false, nil } var cev C.PantoEvent status := C.panto_stream_next(s.ptr, &cev) switch status { case C.PANTO_NEXT_EVENT: defer C.panto_event_free(&cev) ev := marshalEvent(&cev) if ev.Tag == EventTurnComplete { s.done = true } return ev, true, nil case C.PANTO_NEXT_DONE: s.done = true return Event{}, false, nil case C.PANTO_NEXT_ERROR: s.err = lastError() return Event{}, false, s.err default: s.err = fmt.Errorf("unknown panto next status %d", int(status)) return Event{}, false, s.err } } func (s *Stream) Err() error { return s.err } func (s *Stream) Iter() iter.Seq[Event] { return func(yield func(Event) bool) { for { ev, ok, err := s.Next() if err != nil || !ok { return } if !yield(ev) { return } if ev.Tag == EventTurnComplete { return } } } } func (s *Stream) Chan() <-chan Event { ch := make(chan Event) go func() { defer close(ch) for ev := range s.Iter() { ch <- ev } }() return ch } func (cfg Config) c() (C.PantoConfig, cStrings, error) { var cs cStrings provider, pstrings, err := cfg.Provider.c() cs = append(cs, pstrings...) if err != nil { cs.free() return C.PantoConfig{}, nil, err } out := C.PantoConfig{provider: provider} out.compaction.keep_verbatim = C.uint32_t(cfg.Compaction.KeepVerbatim) if cfg.Compaction.Model != nil { m, ms, err := cfg.Compaction.Model.c() cs = append(cs, ms...) if err != nil { cs.free() return C.PantoConfig{}, nil, err } out.compaction.has_model = true out.compaction.model = m } if cfg.Compaction.CompactionPrompt != nil { c := C.CString(*cfg.Compaction.CompactionPrompt) cs = append(cs, c) out.compaction.has_compaction_prompt = true out.compaction.compaction_prompt = c } out.retry.max_attempts = C.size_t(cfg.Retry.MaxAttempts) out.retry.initial_delay_ms = C.uint64_t(cfg.Retry.InitialDelayMS) out.retry.max_delay_ms = C.uint64_t(cfg.Retry.MaxDelayMS) out.retry.multiplier = C.double(cfg.Retry.Multiplier) out.retry.jitter = C.bool(cfg.Retry.Jitter) return out, cs, nil } func (p ProviderConfig) c() (C.PantoProviderConfig, cStrings, error) { var out C.PantoProviderConfig var cs cStrings if p.OpenAIChat != nil == (p.AnthropicMessages != nil) { return out, nil, errors.New("exactly one provider config must be set") } if p.OpenAIChat != nil { out.tag = C.PANTO_OPENAI_CHAT setOpenAI(&out, p.OpenAIChat, &cs) return out, cs, nil } out.tag = C.PANTO_ANTHROPIC_MESSAGES setAnthropic(&out, p.AnthropicMessages, &cs) return out, cs, nil } func setOpenAI(out *C.PantoProviderConfig, cfg *OpenAIChatConfig, cs *cStrings) { apiKey, baseURL, model := C.CString(cfg.APIKey), C.CString(cfg.BaseURL), C.CString(cfg.Model) *cs = append(*cs, apiKey, baseURL, model) c := (*C.PantoOpenAIChatConfig)(unsafe.Pointer(&out.data[0])) c.api_key = apiKey c.base_url = baseURL c.model = model c.reasoning = C.PantoReasoningEffort(cfg.Reasoning) c.max_tokens = C.uint32_t(cfg.MaxTokens) } func setAnthropic(out *C.PantoProviderConfig, cfg *AnthropicMessagesConfig, cs *cStrings) { apiKey, baseURL, model, ver := C.CString(cfg.APIKey), C.CString(cfg.BaseURL), C.CString(cfg.Model), C.CString(cfg.APIVersion) *cs = append(*cs, apiKey, baseURL, model, ver) c := (*C.PantoAnthropicMessagesConfig)(unsafe.Pointer(&out.data[0])) c.api_key = apiKey c.base_url = baseURL c.model = model c.api_version = ver c.max_tokens = C.uint32_t(cfg.MaxTokens) c.thinking = C.PantoThinking(cfg.Thinking) c.effort = C.PantoEffort(cfg.Effort) c.thinking_interleaved = C.bool(cfg.ThinkingInterleaved) if cfg.ThinkingBudgetTokens != nil { c.has_thinking_budget_tokens = true c.thinking_budget_tokens = C.uint32_t(*cfg.ThinkingBudgetTokens) } } func marshalEvent(cev *C.PantoEvent) Event { ev := Event{Tag: EventTag(cev.tag)} switch cev.tag { case C.PANTO_EVENT_MESSAGE_START: ev.MessageStart = MessageRole(*(*C.PantoMessageRole)(unsafe.Pointer(&cev.data[0]))) case C.PANTO_EVENT_BLOCK_START: b := (*C.PantoBlockStart)(unsafe.Pointer(&cev.data[0])) ev.BlockStart = BlockStart{BlockType: ContentBlockTag(b.block_type), Index: uint(b.index)} case C.PANTO_EVENT_TOOL_DETAILS: t := (*C.PantoToolDetails)(unsafe.Pointer(&cev.data[0])) ev.ToolDetails = ToolDetails{Index: uint(t.index), ID: goSlice(t.id), Name: goSlice(t.name)} case C.PANTO_EVENT_CONTENT_DELTA: d := (*C.PantoContentDelta)(unsafe.Pointer(&cev.data[0])) ev.ContentDelta = ContentDelta{Index: uint(d.index), Delta: goSlice(d.delta)} case C.PANTO_EVENT_BLOCK_COMPLETE: b := (*C.PantoBlockComplete)(unsafe.Pointer(&cev.data[0])) ev.BlockComplete = BlockComplete{Index: uint(b.index), BlockType: ContentBlockTag(b.block_type)} case C.PANTO_EVENT_MESSAGE_COMPLETE: m := (*C.PantoMessageComplete)(unsafe.Pointer(&cev.data[0])) if m.has_usage { u := usage(m.usage) ev.MessageComplete.Usage = &u } case C.PANTO_EVENT_PROVIDER_RETRY: r := (*C.PantoProviderRetry)(unsafe.Pointer(&cev.data[0])) ev.ProviderRetry = ProviderRetry{Attempt: uint(r.attempt), MaxAttempts: uint(r.max_attempts), DelayMS: uint64(r.delay_ms), ErrorName: goSlice(r.error_name), Message: goSlice(r.message), Compaction: bool(r.compaction)} if r.has_status_code { v := uint16(r.status_code) ev.ProviderRetry.StatusCode = &v } if r.has_retry_after_ms { v := uint64(r.retry_after_ms) ev.ProviderRetry.RetryAfterMS = &v } case C.PANTO_EVENT_TOOL_DISPATCH_START: t := (*C.PantoToolDispatchStart)(unsafe.Pointer(&cev.data[0])) ev.ToolDispatchStart = ToolDispatchStart{Count: uint(t.count)} case C.PANTO_EVENT_TOOL_DISPATCH_COMPLETE: t := (*C.PantoToolDispatchComplete)(unsafe.Pointer(&cev.data[0])) ev.ToolDispatchComplete = ToolDispatchComplete{MessageIndex: uint(t.message_index)} } return ev } func goSlice(s C.PantoSlice) string { if s.ptr == nil || s.len == 0 { return "" } return C.GoStringN((*C.char)(unsafe.Pointer(s.ptr)), C.int(s.len)) } func usage(u C.PantoUsage) Usage { return Usage{Input: uint64(u.input), Output: uint64(u.output), CacheRead: uint64(u.cache_read), CacheWrite: uint64(u.cache_write), Reasoning: uint64(u.reasoning)} } func cUsage(u Usage) C.PantoUsage { return C.PantoUsage{input: C.uint64_t(u.Input), output: C.uint64_t(u.Output), cache_read: C.uint64_t(u.CacheRead), cache_write: C.uint64_t(u.CacheWrite), reasoning: C.uint64_t(u.Reasoning)} } func sessionInfo(i C.PantoSessionInfo) SessionInfo { return SessionInfo{ID: goSlice(i.id), Created: goSlice(i.created), Modified: goSlice(i.modified), LastUserMessage: goSlice(i.last_user_message), BaseURL: goSlice(i.base_url), Model: goSlice(i.model), MessageCount: uint(i.message_count), APIStyle: APIStyle(i.api_style), Reasoning: ReasoningEffort(i.reasoning)} }