A Message is a role plus an ordered list of parts. skyl models the superset of
what vendors accept, because they disagree about whether content is a string or
a list of typed blocks.
Reference#
type Message struct {
Role Role
Parts []Part
}
Methods#
func (m Message) Text() string // every Text part concatenated
func (m Message) ToolCalls() []ToolCall // every ToolCall part, in order
Caveats
- A message with no parts is rejected by
Validate— it is meaningless on every provider. - There is no system role. System prompts live on
Request.System. - skyl does not police role ordering. Providers disagree about what is legal, and their own error is more informative than a local rejection.
Partis a closed interface — only skyl can implement it. SeePart.Text()ignores non-text parts, so a turn of pure tool calls returns"".ToolCalls()returnsnilwhen there are none, sorangeis safe.
Constructors#
| Constructor | Produces |
|---|---|
| UserText(s) | A user message with one Text part |
| AssistantText(s) | An assistant message with one Text part |
| UserImage(mediaType, data, caption) | A user message with an Image and optional Text |
| ToolResultMessage(callID, content) | A tool message answering a call |
| ToolErrorMessage(callID, content) | The same, with IsError set |
Usage#
A conversation
msgs := []skyl.Message{
skyl.UserText("What is a nil map?"),
skyl.AssistantText("A map that is declared but not allocated."),
skyl.UserText("Can I read from one?"),
}msgs := []skyl.Message{
skyl.UserText("What is a nil map?"),
skyl.AssistantText("A map that is declared but not allocated."),
skyl.UserText("Can I read from one?"),
}Mixed content
skyl.Message{
Role: skyl.RoleAssistant,
Parts: []skyl.Part{
skyl.Text{Text: "Let me check both cities."},
skyl.ToolCall{ID: "c1", Name: "get_weather", Arguments: a1},
skyl.ToolCall{ID: "c2", Name: "get_weather", Arguments: a2},
},
}skyl.Message{
Role: skyl.RoleAssistant,
Parts: []skyl.Part{
skyl.Text{Text: "Let me check both cities."},
skyl.ToolCall{ID: "c1", Name: "get_weather", Arguments: a1},
skyl.ToolCall{ID: "c2", Name: "get_weather", Arguments: a2},
},
}Persisting a conversation
// Part is an interface, so Message does not round-trip through encoding/json.
// Store your own shape — and note this one is lossy: it keeps text and drops
// tool calls.
type storedTurn struct {
Role string `json:"role"`
Text string `json:"text"`
}
func store(msgs []skyl.Message) []storedTurn {
out := make([]storedTurn, 0, len(msgs))
for _, m := range msgs {
out = append(out, storedTurn{Role: string(m.Role), Text: m.Text()})
}
return out
}// Part is an interface, so Message does not round-trip through encoding/json.
// Store your own shape — and note this one is lossy: it keeps text and drops
// tool calls.
type storedTurn struct {
Role string `json:"role"`
Text string `json:"text"`
}
func store(msgs []skyl.Message) []storedTurn {
out := make([]storedTurn, 0, len(msgs))
for _, m := range msgs {
out = append(out, storedTurn{Role: string(m.Role), Text: m.Text()})
}
return out
}Troubleshooting#
ErrBadRequest: message N has no parts
Every message needs at least one part. An empty skyl.Message{Role: ...} is
rejected locally.
The model has no memory of earlier turns
You are not appending resp.Message after each response. Append it — and append
the response message itself, not AssistantText(resp.Text()), which drops tool
calls.
json.Marshal on a Message produced empty parts
Part is an interface with unexported methods; it does not marshal. Persist
your own shape.