diff options
| author | t <t@tjp.lol> | 2026-06-18 13:38:32 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-18 13:39:49 -0600 |
| commit | 7c2d8825660f73198149c0b2ce26166b16ba3532 (patch) | |
| tree | ae57a23e5dcb905b29718c63a0ed2f6c65d93d53 /libpanto-go/panto.go | |
| parent | fc77c3900ef80ff0e2934c03ca37dfc309a6e7b9 (diff) | |
Preserve content blocks and thinking origin in C store append
Round-trip persistent message content through the C session store bridge
instead of dropping it, including tool-use, tool-result, system, and
thinking blocks. Reconstruct borrowed C content blocks with arena-backed
helpers, carry message identity fields like API style/model/reasoning, and
add tests covering append marshalling and thinking signature origin
round-tripping.
Diffstat (limited to 'libpanto-go/panto.go')
| -rw-r--r-- | libpanto-go/panto.go | 203 |
1 files changed, 196 insertions, 7 deletions
diff --git a/libpanto-go/panto.go b/libpanto-go/panto.go index da3bcb1..95e0cbd 100644 --- a/libpanto-go/panto.go +++ b/libpanto-go/panto.go @@ -252,6 +252,13 @@ type PersistentMessage struct { 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 @@ -266,6 +273,14 @@ type OwnedContentBlock struct { 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 MessageBuilder.AddThinking — + // Anthropic drops a thinking block whose signature lacks a matching origin. + ThinkingSignature string + ThinkingSignatureStyle APIStyle + ThinkingSignatureBaseURL string + ThinkingSignatureModel string } type cStrings []*C.char @@ -365,6 +380,37 @@ func CreateSession(store cSessionStore) (*Session, error) { 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)) @@ -495,6 +541,113 @@ func (c *Conversation) AddCompactionSummary(text string) error { return nil } +// MessageBuilder accumulates content blocks for one message, then commits them +// to a Conversation via Conversation.AddMessage. It is the block-level builder +// for rebuilding a persisted conversation that contains tool calls or thinking +// (the text-only AddUserText/AddAssistantText cannot). Add blocks in order, +// then call AddMessage (which consumes the builder). Call Destroy only to +// discard a builder you never commit. +type MessageBuilder struct { + ptr *C.PantoMessageBuilder +} + +func NewMessageBuilder(role MessageRole) (*MessageBuilder, error) { + p := C.panto_message_builder_create(C.PantoMessageRole(role)) + if p == nil { + return nil, lastError() + } + b := &MessageBuilder{ptr: p} + runtime.SetFinalizer(b, (*MessageBuilder).Destroy) + return b, nil +} + +func (b *MessageBuilder) AddText(text string) error { + ct := C.CString(text) + defer C.free(unsafe.Pointer(ct)) + if C.panto_message_builder_add_text(b.ptr, ct) != C.PANTO_OK { + return lastError() + } + return nil +} + +// AddThinking appends a thinking 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 (b *MessageBuilder) AddThinking(text, signature string, style APIStyle, baseURL, model string) error { + ct := C.CString(text) + defer C.free(unsafe.Pointer(ct)) + cs := C.CString(signature) + defer C.free(unsafe.Pointer(cs)) + cb := C.CString(baseURL) + defer C.free(unsafe.Pointer(cb)) + cm := C.CString(model) + defer C.free(unsafe.Pointer(cm)) + if C.panto_message_builder_add_thinking(b.ptr, ct, cs, C.PantoAPIStyle(style), cb, cm) != C.PANTO_OK { + return lastError() + } + return nil +} + +func (b *MessageBuilder) AddToolUse(id, name, input string) error { + cid := C.CString(id) + defer C.free(unsafe.Pointer(cid)) + cn := C.CString(name) + defer C.free(unsafe.Pointer(cn)) + ci := C.CString(input) + defer C.free(unsafe.Pointer(ci)) + if C.panto_message_builder_add_tool_use(b.ptr, cid, cn, ci) != C.PANTO_OK { + return lastError() + } + return nil +} + +func (b *MessageBuilder) AddToolResult(toolUseID, text string, isError bool) error { + cid := C.CString(toolUseID) + defer C.free(unsafe.Pointer(cid)) + ct := C.CString(text) + defer C.free(unsafe.Pointer(ct)) + if C.panto_message_builder_add_tool_result(b.ptr, cid, ct, C.bool(isError)) != C.PANTO_OK { + return lastError() + } + return nil +} + +func (b *MessageBuilder) AddSystem(text string, mode SystemMode) error { + ct := C.CString(text) + defer C.free(unsafe.Pointer(ct)) + if C.panto_message_builder_add_system(b.ptr, ct, C.PantoSystemMode(mode)) != C.PANTO_OK { + return lastError() + } + return nil +} + +// Destroy frees an uncommitted builder. Safe to call after AddMessage (no-op). +func (b *MessageBuilder) Destroy() { + if b != nil && b.ptr != nil { + C.panto_message_builder_destroy(b.ptr) + b.ptr = nil + } +} + +// AddMessage commits the builder's blocks as one message of the builder's role, +// with optional assistant usage. It consumes the builder (success or failure); +// the builder must not be used afterwards. +func (c *Conversation) AddMessage(b *MessageBuilder, u *Usage) error { + var cu C.PantoUsage + has := false + if u != nil { + cu = cUsage(*u) + has = true + } + st := C.panto_conversation_add_message(c.ptr, b.ptr, C.bool(has), cu) + b.ptr = nil + runtime.SetFinalizer(b, nil) + if st != C.PANTO_OK { + return lastError() + } + return nil +} + type Message struct { conv *Conversation index uint @@ -1209,7 +1362,16 @@ func pantoGoStoreAppendMessages(ctx unsafe.Pointer, sessionID C.PantoSlice, mess uu := usage(m.usage) u = &uu } - out[i] = PersistentMessage{Role: MessageRole(m.role), Usage: u, Metadata: goSlice(m.metadata)} + 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 @@ -1217,6 +1379,29 @@ func pantoGoStoreAppendMessages(ctx unsafe.Pointer, sessionID C.PantoSlice, mess 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), @@ -1231,18 +1416,22 @@ func cSessionInfo(i SessionInfo) C.PantoSessionInfo { } } +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 { - 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)) + freeCSessionInfo(i) } C.free(unsafe.Pointer(l.ptr)) } |
