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\n" compactionSummarySuffix = "\n" ) // 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) } }