Skip to content
skyl

Message

One turn in a conversation — a role and its ordered content.

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.
  • Part is a closed interface — only skyl can implement it. See Part.
  • Text() ignores non-text parts, so a turn of pure tool calls returns "".
  • ToolCalls() returns nil when there are none, so range is safe.

Constructors#

ConstructorProduces
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

goCompiles
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

goCompiles
// 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.

Edit this page on GitHub