diff options
Diffstat (limited to 'libpanto-go/panto.go')
| -rw-r--r-- | libpanto-go/panto.go | 434 |
1 files changed, 280 insertions, 154 deletions
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 { |
