Message types¶
conversation.go
defines the conversation model: the types every adapter reads and writes. Nothing
here is tied to a provider. An adapter turns these neutral types into a provider's
wire format on the way out, and rebuilds the same types from the response stream
on the way in.
Structure overview¶
A Context is made up of a system prompt, the conversation messages,
and the available tools. Each message is one entry in the conversation history —
user input, an assistant reply, or a tool result — and inside each entry, content
blocks carry the actual payload.
Context
├── SystemPrompt: string
├── Messages: []Message
│ ├── UserMessage → []UserContent
│ ├── AssistantMessage → []AssistantContent
│ └── ToolResultMessage → []ToolResultContent
└── Tools: []ToolDefinition
Closing types with marker interfaces¶
Go has no native sum types, so there is no way to declare a closed set of types directly. The workaround is to give an interface an unexported method: no type outside the package can implement it, so the possible implementations are limited to the few inside this package:
type Message interface {
isMessage()
}
func (*UserMessage) isMessage() {}
func (*AssistantMessage) isMessage() {}
func (*ToolResultMessage) isMessage() {}
The set of message kinds is therefore closed: when you type-switch over it, you
can be sure every branch is listed — no new type can be added from outside. The
methods sit on pointer receivers, so the concrete values flowing through the
package are always *UserMessage, *AssistantMessage, and *ToolResultMessage.
The same interfaces do double duty for content blocks: blocks are split into three role interfaces, and which one a block implements declares which messages it may appear in — the ones it does not implement will not compile if placed there:
// UserContent can appear in a user message
type UserContent interface {
isUserContent()
}
// AssistantContent can appear in an assistant message
type AssistantContent interface {
isAssistantContent()
}
// ToolResultContent can appear in a tool result message
type ToolResultContent interface {
isToolResultContent()
}
func (*TextContent) isUserContent() {}
func (*TextContent) isAssistantContent() {} // all three messages
func (*TextContent) isToolResultContent() {}
func (*ImageContent) isUserContent() {} // user and tool result only
func (*ImageContent) isToolResultContent() {}
func (*ThinkingContent) isAssistantContent() {} // assistant only
func (*ToolCall) isAssistantContent() {} // assistant only
Placement rules¶
The role interfaces turn "which block goes where" into a compile-time rule. A
ThinkingContent does not implement UserContent, so putting one in a
UserMessage will not compile.
| Block | UserMessage | AssistantMessage | ToolResultMessage |
|---|---|---|---|
TextContent |
✓ | ✓ | ✓ |
ImageContent |
✓ | — | ✓ |
ThinkingContent |
— | ✓ | — |
ToolCall |
— | ✓ | — |
The four content blocks¶
type TextContent struct {
Text string `json:"text"`
TextSignature string `json:"textSignature,omitempty"`
}
type ImageContent struct {
Data string `json:"data"` // base64-encoded bytes
MIMEType string `json:"mimeType"`
}
type ThinkingContent struct {
Thinking string `json:"thinking"`
ThinkingSignature string `json:"thinkingSignature,omitempty"`
Redacted bool `json:"redacted,omitempty"`
}
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments map[string]any `json:"arguments"`
ThoughtSignature string `json:"thoughtSignature,omitempty"`
}
A few fields carry weight beyond the obvious payload:
ToolCall.IDis the correlation key. AToolResultMessageanswers a call by echoing it inToolCallID, which is how a result is matched to its request across a turn.ToolCall.Argumentsis a decoded JSON object (map[string]any), not a raw string. The streamed argument text is parsed — best-effort, so a truncated stream still yields a value — before it lands here.ThinkingContent.Redactedmarks reasoning the provider returned in redacted form: the text is withheld, but the block is kept so the turn stays well-formed and its signature can be replayed.
The signature fields
TextSignature, ThinkingSignature, and ThoughtSignature are opaque
provider metadata. The package never reads their contents; it only stores
them and replays them on later turns, so a provider can verify the
continuity of its own reasoning and tool use across requests. See
Switching models for how they are preserved or dropped when
the target model changes.
The three messages¶
UserMessage and ToolResultMessage are small. A user message is just a content
list; a tool result adds the call ID and error flag that tie it back to its
ToolCall:
type UserMessage struct {
Content []UserContent
}
type ToolResultMessage struct {
ToolCallID string
ToolName string
Content []ToolResultContent
IsError bool
}
AssistantMessage is the larger one — model output plus the response metadata an
adapter fills in:
type AssistantMessage struct {
Content []AssistantContent
Protocol Protocol // wire protocol used
Provider string // vendor key
Model string // requested model ID
Usage Usage // tokens and calculated cost
StopReason StopReason // why generation stopped
Diagnostics []Diagnostic // non-fatal events, nil when clean
Timestamp int64 // Unix milliseconds
// ... ResponseModel, ResponseID, ErrorMessage omitted
}
An adapter does not fill these fields from scratch. NewAssistantMessage(model)
seeds the provider-independent metadata — Protocol, Provider, Model, and
Timestamp — so an adapter starts from a half-built message and only appends
content and the response-specific fields.
Token usage and stop reasons¶
Usage and StopReason on an AssistantMessage are each a small value type.
Usage counts tokens by category and carries the calculated UsageCost; the
categories line up with ModelCost so cost is a per-category
multiply:
type Usage struct {
Input, Output, CacheRead, CacheWrite, TotalTokens int64
Cost UsageCost
}
type UsageCost struct {
Input, Output, CacheRead, CacheWrite, Total float64
}
StopReason is a fixed set of values that maps each provider's stop signal onto
one neutral set:
| Value | Meaning |
|---|---|
stop |
normal completion |
length |
truncated by the output-token cap |
toolUse |
stopped so the caller can run tool calls |
error |
provider or runtime failure |
aborted |
the request was cancelled |
Reading a response¶
Two helpers walk Content so callers do not type-switch by hand. Both are
nil-safe and preserve block order:
func (message *AssistantMessage) Text() string // joins every text block
func (message *AssistantMessage) ToolCalls() []ToolCall // every tool call, in order
Text() skips thinking and tool-call blocks; ToolCalls() returns nil when
the model requested no tools, which reads naturally next to a StopReason of
toolUse. ToolCalls() returns values, not pointers — copies the caller can pass
to a tool runner without aliasing the message's own blocks.
Context¶
A request is assembled from three fields:
ToolDefinition keeps its parameter schema as raw JSON (json.RawMessage), so a
schema generated elsewhere is passed through untouched.
How these types serialize to self-describing JSON — and decode back without a
manual dispatch table — is covered in
messages.go.