Files
2026-08-14 23:41:57 +08:00

211 lines
7.1 KiB
Go

package agentcore
import (
"encoding/json"
"fmt"
)
// Message roles, matching pi's wire format.
const (
RoleUser = "user"
RoleAssistant = "assistant"
RoleToolResult = "toolResult"
// RoleCompaction marks a compaction checkpoint persisted inline in the
// message list: it replaces the history summarized before it (pi's
// "compactionSummary"). It is not sent to the model verbatim; the LLM
// conversion turns it into a user text block.
RoleCompaction = "compaction"
)
// Message is the sealed interface implemented by the three message roles.
// AgentMessage (the loop's message abstraction) is simply Message: custom
// message kinds implement the same interface and convertToLlm filters out any
// that are not LLM-bound. This deliberately replaces pi's declaration merging,
// which has no Go equivalent.
type Message interface {
isMessage()
// Role returns the discriminant ("user" | "assistant" | "toolResult").
Role() string
}
// AgentMessage is the loop-level message type. It is the same as Message; the
// alias documents intent at call sites that deal with the loop rather than raw
// LLM messages.
type AgentMessage = Message
// Usage reports token accounting for an assistant response.
type Usage struct {
InputTokens int `json:"inputTokens"`
OutputTokens int `json:"outputTokens"`
}
// UserMessage is input from the user. Content is restricted at construction to
// text/image blocks (runtime constraint, not a separate interface).
type UserMessage struct {
RoleField string `json:"role"`
Content ContentList `json:"content"`
Timestamp int64 `json:"timestamp"`
}
func (UserMessage) isMessage() {}
func (m UserMessage) Role() string { return RoleUser }
// AssistantMessage is a model response. Content may hold text/thinking/toolCall
// blocks. StopReason follows pi's set (end_turn/tool_use/length/error/aborted).
type AssistantMessage struct {
RoleField string `json:"role"`
Content ContentList `json:"content"`
API string `json:"api,omitempty"`
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
Usage *Usage `json:"usage,omitempty"`
StopReason string `json:"stopReason,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
Timestamp int64 `json:"timestamp"`
// Optional diagnostics, kept for cross-provider replay/observability.
ResponseModel string `json:"responseModel,omitempty"`
ResponseID string `json:"responseId,omitempty"`
}
func (AssistantMessage) isMessage() {}
func (m AssistantMessage) Role() string { return RoleAssistant }
// ToolCalls returns the tool call blocks in this assistant message, in order.
func (m AssistantMessage) ToolCalls() []ToolCallContent {
var calls []ToolCallContent
for _, c := range m.Content {
if tc, ok := c.(ToolCallContent); ok {
calls = append(calls, tc)
}
}
return calls
}
// ToolResultMessage carries the outcome of executing a single tool call.
// Content is restricted to text/image blocks at construction.
type ToolResultMessage struct {
RoleField string `json:"role"`
ToolCallID string `json:"toolCallId"`
ToolName string `json:"toolName"`
Content ContentList `json:"content"`
Details any `json:"details,omitempty"`
IsError bool `json:"isError"`
Timestamp int64 `json:"timestamp"`
}
func (ToolResultMessage) isMessage() {}
func (m ToolResultMessage) Role() string { return RoleToolResult }
// CompactionMessage is a summarization checkpoint persisted inline in the
// message list. It stands in for the history compacted before it: Summary is
// the structured checkpoint text and TokensBefore records the estimated context
// size at compaction time (for observability). Details optionally holds the
// file operations extracted from the compacted range. Mirrors pi's
// CompactionSummaryMessage + CompactionEntry.
type CompactionMessage struct {
RoleField string `json:"role"`
Summary string `json:"summary"`
TokensBefore int `json:"tokensBefore,omitempty"`
// Details is opaque at this layer (the compaction package owns its shape);
// kept as raw JSON so agentcore stays free of a compaction dependency.
Details json.RawMessage `json:"details,omitempty"`
Timestamp int64 `json:"timestamp"`
}
func (CompactionMessage) isMessage() {}
func (m CompactionMessage) Role() string { return RoleCompaction }
// compactionSummaryPrefix / compactionSummarySuffix wrap a compaction summary
// when it is rendered into an LLM user message, matching pi's
// COMPACTION_SUMMARY_PREFIX / COMPACTION_SUMMARY_SUFFIX.
const (
compactionSummaryPrefix = "The conversation history before this point was compacted into the following summary:\n\n<summary>\n"
compactionSummarySuffix = "\n</summary>"
)
// AsUserMessage renders a compaction checkpoint as the user text message that
// stands in for the compacted history when building the LLM request. The
// provider encoders call this so a persisted compaction line replays as
// context rather than being dropped.
func (m CompactionMessage) AsUserMessage() UserMessage {
return UserMessage{
RoleField: RoleUser,
Content: ContentList{NewTextContent(compactionSummaryPrefix + m.Summary + compactionSummarySuffix)},
Timestamp: m.Timestamp,
}
}
// StopReason values, matching pi.
const (
StopReasonEndTurn = "end_turn"
StopReasonToolUse = "tool_use"
StopReasonLength = "length"
StopReasonError = "error"
StopReasonAborted = "aborted"
)
// MessageList is a slice of Message with discriminated JSON (un)marshalling,
// dispatching on the "role" field. Used by AgentContext and session persistence.
type MessageList []Message
// UnmarshalJSON decodes a JSON array of messages, dispatching each element on
// its "role" discriminant.
func (ml *MessageList) UnmarshalJSON(data []byte) error {
var raws []json.RawMessage
if err := json.Unmarshal(data, &raws); err != nil {
return err
}
out := make(MessageList, 0, len(raws))
for i, raw := range raws {
m, err := decodeMessage(raw)
if err != nil {
return fmt.Errorf("message[%d]: %w", i, err)
}
out = append(out, m)
}
*ml = out
return nil
}
// decodeMessage peeks at the "role" field and decodes into the matching
// concrete message struct.
func decodeMessage(raw json.RawMessage) (Message, error) {
var probe struct {
Role string `json:"role"`
}
if err := json.Unmarshal(raw, &probe); err != nil {
return nil, fmt.Errorf("peek role: %w", err)
}
switch probe.Role {
case RoleUser:
var m UserMessage
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
return m, nil
case RoleAssistant:
var m AssistantMessage
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
return m, nil
case RoleToolResult:
var m ToolResultMessage
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
return m, nil
case RoleCompaction:
var m CompactionMessage
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
return m, nil
case "":
return nil, fmt.Errorf("missing role discriminant")
default:
return nil, fmt.Errorf("unknown role %q", probe.Role)
}
}