summaryrefslogtreecommitdiff
path: root/libpanto-go
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-19 14:53:03 -0600
committert <t@tjp.lol>2026-06-19 14:55:35 -0600
commit81ddb61dcfcc9bc85585f262bf303e1a9da507b0 (patch)
treeb38b39a937de047d35246a3e64df6fda5737850e /libpanto-go
parent270cd00129551647e7c764a13836e03e0b2dc4e2 (diff)
libpanto fixes
- ensuring stream.next() sequences are interruptible - Agent.run() takes any user message, not just chat text
Diffstat (limited to 'libpanto-go')
-rw-r--r--libpanto-go/README.md12
-rw-r--r--libpanto-go/panto.go434
-rw-r--r--libpanto-go/panto_test.go60
3 files changed, 311 insertions, 195 deletions
diff --git a/libpanto-go/README.md b/libpanto-go/README.md
index a19e788..063f800 100644
--- a/libpanto-go/README.md
+++ b/libpanto-go/README.md
@@ -37,12 +37,20 @@ agent, err := panto.NewAgent(panto.Config{
if err != nil { /* handle */ }
defer agent.Close()
-stream, err := agent.Run("hello")
+stream, err := agent.Run(panto.UserMessage(panto.TextBlock("hello")))
if err != nil { /* handle */ }
defer stream.Close()
for ev := range stream.Iter() {
- // ev.Tag identifies the active payload.
+ // ev is an Event interface; type-switch on the concrete event, or branch
+ // on ev.EventType(). Breaking out of the loop is safe: the session store
+ // is current at every step.
+ switch ev := ev.(type) {
+ case panto.ContentDeltaEvent:
+ fmt.Print(ev.Delta)
+ case panto.MessageCompleteEvent:
+ // full message at agent.Conversation().Message(ev.MessageIndex)
+ }
}
if err := stream.Err(); err != nil { /* handle */ }
```
diff --git a/libpanto-go/panto.go b/libpanto-go/panto.go
index 5bfc5d8..f692b6f 100644
--- a/libpanto-go/panto.go
+++ b/libpanto-go/panto.go
@@ -155,51 +155,88 @@ type (
}
)
-type Event struct {
- Tag EventTag
- MessageStart MessageRole
- BlockStart BlockStart
- ToolDetails ToolDetails
- ContentDelta ContentDelta
- BlockComplete BlockComplete
- MessageComplete MessageComplete
- ProviderRetry ProviderRetry
- ToolDispatchStart ToolDispatchStart
- ToolDispatchComplete ToolDispatchComplete
+// 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
}
-type BlockStart struct {
+
+// MessageStartEvent: an assistant message began streaming.
+type MessageStartEvent struct{ Role MessageRole }
+
+// BlockStartEvent: a content block opened.
+type BlockStartEvent struct {
BlockType ContentBlockTag
Index uint
}
-type ToolDetails struct {
+
+// ToolDetailsEvent: the tool identity (id + name) for a ToolUse block resolved.
+type ToolDetailsEvent struct {
Index uint
ID, Name string
}
-type ContentDelta struct {
+
+// ContentDeltaEvent: streaming content for the open block (text/thinking/args).
+type ContentDeltaEvent struct {
Index uint
Delta string
}
-type BlockComplete struct {
+
+// BlockCompleteEvent: a content block closed.
+type BlockCompleteEvent struct {
Index uint
BlockType ContentBlockTag
}
-type (
- MessageComplete struct{ Usage *Usage }
- ProviderRetry struct {
- Attempt, MaxAttempts uint
- DelayMS uint64
- ErrorName string
- StatusCode *uint16
- RetryAfterMS *uint64
- Message string
- Compaction bool
- }
-)
-type (
- ToolDispatchStart struct{ Count uint }
- ToolDispatchComplete struct{ MessageIndex uint }
-)
+// 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 }
+
+// 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 (ToolDispatchCompleteEvent) EventType() EventTag { return EventToolDispatchComplete }
+func (TurnCompleteEvent) EventType() EventTag { return EventTurnComplete }
type (
Agent struct {
@@ -275,8 +312,8 @@ type OwnedContentBlock struct {
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.
+ // 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
@@ -541,127 +578,198 @@ 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
+// 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
}
-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
+// 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}
}
-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
+// ToolUseBlock is an assistant tool call.
+func ToolUseBlock(id, name, input string) Block {
+ return Block{tag: BlockToolUse, toolID: id, toolName: name, input: input}
}
-// 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
+// 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}
}
-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
+// 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}
}
-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
+// 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
}
-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
+// 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
}
-// SetIdentity attaches the wire identity (provider style/baseURL/model +
-// reasoning) that produced this message. This 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. Prefer
-// this over per-block origins on AddThinking when rebuilding a conversation for
-// resumption: set it from PersistentMessage's APIStyle/BaseURL/Model and the
-// thinking signatures replay correctly without copying the identity onto every
-// thinking block.
-func (b *MessageBuilder) SetIdentity(style APIStyle, baseURL, model string, reasoning ReasoningEffort) error {
- 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_set_identity(b.ptr, C.PantoAPIStyle(style), cb, cm, C.PantoReasoningEffort(reasoning)) != C.PANTO_OK {
- return lastError()
+// 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()
}
- return nil
+ 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
}
-// 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
+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 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 {
+// 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 u != nil {
- cu = cUsage(*u)
+ if msg.Usage != nil {
+ cu = cUsage(*msg.Usage)
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 {
+ // 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
@@ -828,11 +936,21 @@ func (a *Agent) Compact(overrideSystemPrompt, extraInstructions *string) (Compac
}, nil
}
-func (a *Agent) Run(userText string) (*Stream, error) {
- ct := C.CString(userText)
- defer C.free(unsafe.Pointer(ct))
+// 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, ct, &out) != C.PANTO_OK {
+ if C.panto_agent_run(a.ptr, b, &out) != C.PANTO_OK {
return nil, lastError()
}
s := &Stream{ptr: out}
@@ -886,10 +1004,10 @@ func (s *Stream) Reopen() error {
func (s *Stream) Next() (Event, bool, error) {
if s.err != nil {
- return Event{}, false, s.err
+ return nil, false, s.err
}
if s.done {
- return Event{}, false, nil
+ return nil, false, nil
}
var cev C.PantoEvent
status := C.panto_stream_next(s.ptr, &cev)
@@ -897,19 +1015,19 @@ func (s *Stream) Next() (Event, bool, error) {
case C.PANTO_NEXT_EVENT:
defer C.panto_event_free(&cev)
ev := marshalEvent(&cev)
- if ev.Tag == EventTurnComplete {
+ if ev.EventType() == EventTurnComplete {
s.done = true
}
return ev, true, nil
case C.PANTO_NEXT_DONE:
s.done = true
- return Event{}, false, nil
+ return nil, false, nil
case C.PANTO_NEXT_ERROR:
s.err = lastError()
- return Event{}, false, s.err
+ return nil, false, s.err
default:
s.err = fmt.Errorf("unknown panto next status %d", int(status))
- return Event{}, false, s.err
+ return nil, false, s.err
}
}
func (s *Stream) Err() error { return s.err }
@@ -923,7 +1041,7 @@ func (s *Stream) Iter() iter.Seq[Event] {
if !yield(ev) {
return
}
- if ev.Tag == EventTurnComplete {
+ if ev.EventType() == EventTurnComplete {
return
}
}
@@ -1021,31 +1139,36 @@ func setAnthropic(out *C.PantoProviderConfig, cfg *AnthropicMessagesConfig, cs *
}
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])))
+ 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]))
- ev.BlockStart = BlockStart{BlockType: ContentBlockTag(b.block_type), Index: uint(b.index)}
+ 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]))
- ev.ToolDetails = ToolDetails{Index: uint(t.index), ID: goSlice(t.id), Name: goSlice(t.name)}
+ 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]))
- ev.ContentDelta = ContentDelta{Index: uint(d.index), Delta: goSlice(d.delta)}
+ return ContentDeltaEvent{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)}
+ 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.MessageComplete.Usage = &u
+ ev.Usage = &u
}
+ return ev
case C.PANTO_EVENT_PROVIDER_RETRY:
r := (*C.PantoProviderRetry)(unsafe.Pointer(&cev.data[0]))
- ev.ProviderRetry = ProviderRetry{
+ ev := ProviderRetryEvent{
Attempt: uint(r.attempt),
MaxAttempts: uint(r.max_attempts),
DelayMS: uint64(r.delay_ms),
@@ -1055,20 +1178,23 @@ func marshalEvent(cev *C.PantoEvent) Event {
}
if r.has_status_code {
v := uint16(r.status_code)
- ev.ProviderRetry.StatusCode = &v
+ ev.StatusCode = &v
}
if r.has_retry_after_ms {
v := uint64(r.retry_after_ms)
- ev.ProviderRetry.RetryAfterMS = &v
+ ev.RetryAfterMS = &v
}
+ return ev
case C.PANTO_EVENT_TOOL_DISPATCH_START:
t := (*C.PantoToolDispatchStart)(unsafe.Pointer(&cev.data[0]))
- ev.ToolDispatchStart = ToolDispatchStart{Count: uint(t.count)}
+ return ToolDispatchStartEvent{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 ToolDispatchCompleteEvent{MessageIndex: uint(t.message_index)}
+ case C.PANTO_EVENT_TURN_COMPLETE:
+ return TurnCompleteEvent{}
}
- return ev
+ return TurnCompleteEvent{}
}
func goSlice(s C.PantoSlice) string {
diff --git a/libpanto-go/panto_test.go b/libpanto-go/panto_test.go
index fd9df70..af8cafc 100644
--- a/libpanto-go/panto_test.go
+++ b/libpanto-go/panto_test.go
@@ -59,7 +59,7 @@ func TestCustomStoreMintsSession(t *testing.T) {
}
// Gap #3: rebuild a conversation containing tool_use / tool_result / thinking
-// via MessageBuilder, then read the blocks back losslessly.
+// via the value-block API, then read the blocks back losslessly.
func TestMessageBuilderRoundTrip(t *testing.T) {
Init()
defer Deinit()
@@ -70,31 +70,17 @@ func TestMessageBuilderRoundTrip(t *testing.T) {
}
defer conv.Close()
- ub, err := NewMessageBuilder(RoleUser)
- if err != nil {
- t.Fatalf("NewMessageBuilder(user): %v", err)
- }
- if err := ub.AddToolResult("call-1", "result text", false); err != nil {
- t.Fatalf("AddToolResult: %v", err)
- }
- if err := conv.AddMessage(ub, nil); err != nil {
+ if err := conv.AddMessage(UserMessage(
+ ToolResultBlock("call-1", "result text", false),
+ )); err != nil {
t.Fatalf("AddMessage(user): %v", err)
}
- ab, err := NewMessageBuilder(RoleAssistant)
- if err != nil {
- t.Fatalf("NewMessageBuilder(assistant): %v", err)
- }
- if err := ab.AddText("let me search"); err != nil {
- t.Fatalf("AddText: %v", err)
- }
- if err := ab.AddThinking("hmm", "sig", AnthropicMessages, "https://api", "claude"); err != nil {
- t.Fatalf("AddThinking: %v", err)
- }
- if err := ab.AddToolUse("call-1", "search", `{"q":1}`); err != nil {
- t.Fatalf("AddToolUse: %v", err)
- }
- if err := conv.AddMessage(ab, &Usage{Input: 10, Output: 5}); err != nil {
+ if err := conv.AddMessage(AssistantMessage(&Usage{Input: 10, Output: 5},
+ TextBlock("let me search"),
+ ThinkingBlock("hmm", "sig", AnthropicMessages, "https://api", "claude"),
+ ToolUseBlock("call-1", "search", `{"q":1}`),
+ )); err != nil {
t.Fatalf("AddMessage(assistant): %v", err)
}
@@ -141,22 +127,18 @@ func TestMessageBuilderSetIdentity(t *testing.T) {
}
defer conv.Close()
- ab, err := NewMessageBuilder(RoleAssistant)
- if err != nil {
- t.Fatalf("NewMessageBuilder(assistant): %v", err)
- }
- // Thinking block with a signature but NO per-block origin.
- if err := ab.AddThinking("reasoned", "sig", OpenAIChat, "", ""); err != nil {
- t.Fatalf("AddThinking: %v", err)
- }
- if err := ab.AddText("answer"); err != nil {
- t.Fatalf("AddText: %v", err)
- }
- // Provenance is carried at the message level instead.
- if err := ab.SetIdentity(AnthropicMessages, "https://api.anthropic.com", "claude", ReasoningDefault); err != nil {
- t.Fatalf("SetIdentity: %v", err)
- }
- if err := conv.AddMessage(ab, &Usage{Input: 10, Output: 5}); err != nil {
+ // Thinking block with a signature but NO per-block origin; provenance is
+ // carried at the message level instead, via WithIdentity.
+ msg := AssistantMessage(&Usage{Input: 10, Output: 5},
+ ThinkingBlock("reasoned", "sig", OpenAIChat, "", ""),
+ TextBlock("answer"),
+ ).WithIdentity(MessageIdentity{
+ APIStyle: AnthropicMessages,
+ BaseURL: "https://api.anthropic.com",
+ Model: "claude",
+ Reasoning: ReasoningDefault,
+ })
+ if err := conv.AddMessage(msg); err != nil {
t.Fatalf("AddMessage: %v", err)
}