Messages
Messages represent the conversation between users and the assistant. Riffer uses strongly-typed message objects to ensure consistency and type safety.
Message Types
System
System messages provide instructions to the LLM:
msg = Riffer::Messages::System.new("You are a helpful assistant.") msg.role # => :system msg.content # => "You are a helpful assistant." msg.to_h # => {role: :system, content: "You are a helpful assistant."}
System messages are typically set via agent instructions and automatically prepended to conversations.
User
User messages represent input from the user:
msg = Riffer::Messages::User.new("Hello, how are you?") msg.role # => :user msg.content # => "Hello, how are you?" msg.files # => [] msg.to_h # => {role: :user, content: "Hello, how are you?"}
User messages can include file attachments:
file = Riffer::FilePart.from_path("photo.jpg") msg = Riffer::Messages::User.new("Describe this image", files: [file]) msg.files # => [#<Riffer::FilePart ...>] msg.to_h # => {role: :user, content: "Describe this image", files: [{...}]}
Assistant
Assistant messages represent LLM responses, potentially including tool calls and token usage data:
# Text-only response msg = Riffer::Messages::Assistant.new("I'm doing well, thank you!") msg.role # => :assistant msg.content # => "I'm doing well, thank you!" msg.tool_calls # => [] msg.token_usage # => nil or Riffer::TokenUsage # Response with tool calls msg = Riffer::Messages::Assistant.new("", tool_calls: [ {id: "call_123", call_id: "call_123", name: "weather_tool", arguments: '{"city":"Tokyo"}'} ]) msg.tool_calls # => [{id: "call_123", ...}] msg.to_h # => {role: "assistant", content: "", tool_calls: [...]} # Accessing token usage data (when available from provider) if msg.token_usage puts "Input tokens: #{msg.token_usage.input_tokens}" puts "Output tokens: #{msg.token_usage.output_tokens}" puts "Total tokens: #{msg.token_usage.total_tokens}" end
Structured Output on Messages
When an agent has structured_output configured, the final assistant message stores the parsed hash directly. The structured_output? predicate checks for a non-nil value:
msg = Riffer::Messages::Assistant.new('{"sentiment":"positive"}', structured_output: {sentiment: "positive"}) msg.structured_output? # => true msg.structured_output # => {sentiment: "positive"} # When not provided, structured_output returns nil msg = Riffer::Messages::Assistant.new('{"sentiment":"positive"}') msg.structured_output? # => false msg.structured_output # => nil
The to_h representation includes structured_output only when present:
msg = Riffer::Messages::Assistant.new('{"sentiment":"positive"}', structured_output: {sentiment: "positive"}) msg.to_h # => {role: :assistant, content: '{"sentiment":"positive"}', structured_output: {sentiment: "positive"}}
Tool
Tool messages contain the results of tool executions:
msg = Riffer::Messages::Tool.new( "The weather in Tokyo is 22C and sunny.", tool_call_id: "call_123", name: "weather_tool" ) msg.role # => :tool msg.content # => "The weather in Tokyo is 22C and sunny." msg.tool_call_id # => "call_123" msg.name # => "weather_tool" msg.error? # => false # Error result msg = Riffer::Messages::Tool.new( "API rate limit exceeded", tool_call_id: "call_123", name: "weather_tool", error: "API rate limit exceeded", error_type: :execution_error ) msg.error? # => true msg.error # => "API rate limit exceeded" msg.error_type # => :execution_error
File Parts
Riffer::FilePart represents a file attachment (image or document) that can be included with user messages.
Supported Media Types
Images: image/jpeg, image/png, image/gif, image/webp
Documents: application/pdf, text/plain, text/csv, text/html
Creating File Parts
# From a file path (reads eagerly, detects media type from extension) file = Riffer::FilePart.from_path("photo.jpg") file.media_type # => "image/jpeg" file.filename # => "photo.jpg" file.image? # => true # From a URL (stored directly, resolved lazily if provider needs bytes) file = Riffer::FilePart.from_url("https://example.com/doc.pdf") file.url? # => true file.document? # => true # From raw base64 data file = Riffer::FilePart.new(media_type: "image/png", data: base64_string, filename: "chart.png")
Hash Shorthand
When passing files to agents or messages, hashes are automatically converted:
# Path shorthand {path: "photo.jpg"} # URL shorthand (media_type auto-detected from extension, or provide explicitly) {url: "https://example.com/photo.jpg"} {url: "https://example.com/file", media_type: "application/pdf"} # Data shorthand {data: base64_string, media_type: "image/png", filename: "chart.png"}
Predicates
file.image? # true for image/* media types file.document? # true for non-image media types file.url? # true when source was a URL
Using Messages with Agents
String Prompts
The simplest way to interact with an agent:
agent = MyAgent.new response = agent.generate("Hello!")
This creates a User message internally.
Message Arrays
For multi-turn conversations, pass an array of messages:
messages = [ {role: :user, content: "What's the weather?"}, {role: :assistant, content: "I'll check that for you."}, {role: :user, content: "Thanks, I meant in Tokyo specifically."} ] response = agent.generate(messages)
Messages can be hashes or Riffer::Messages::Base objects:
messages = [ Riffer::Messages::User.new("Hello"), Riffer::Messages::Assistant.new("Hi there!"), Riffer::Messages::User.new("How are you?") ] response = agent.generate(messages)
Accessing Message History
After calling generate or stream, access the full conversation:
agent = MyAgent.new agent.generate("Hello!") agent.messages.each do |msg| puts "[#{msg.role}] #{msg.content}" end # [system] You are a helpful assistant. # [user] Hello! # [assistant] Hi there! How can I help you today?
Tool Call Structure
Tool calls in assistant messages have this structure:
{
id: "item_123", # Item identifier
call_id: "call_456", # Call identifier for response matching
name: "weather_tool", # Tool name
arguments: '{"city":"Tokyo"}' # JSON string of arguments
}
When creating tool result messages, use the id as tool_call_id.
Message Emission
Agents can emit messages as they’re added during generation via the on_message callback. This is useful for persistence or real-time logging. Only agent-generated messages (Assistant, Tool) are emitted—not inputs (System, User).
See Agent Lifecycle - on_message for details.
Consecutive Message Merging
Before messages reach a provider adapter, Riffer merges consecutive messages that share the same role into a single message. This guarantees every provider receives an identical message list, regardless of how it handles consecutive same-role messages internally.
Without this step, the same model can receive different input depending on the provider. Anthropic’s API silently merges consecutive user messages server-side, while Bedrock and Gemini reject them outright. Normalizing at the Riffer level removes that divergence.
Merge rules
| Message type | Content | Auxiliary data | Merged? |
|---|---|---|---|
System |
Joined with "\n\n" |
— | Yes |
User |
Joined with "\n\n" |
files arrays concatenated |
Yes |
Assistant |
Joined with "\n\n" |
tool_calls arrays concatenated |
Yes |
Tool |
— | — | Never (each has a unique tool_call_id) |
Example
When a context message is injected before the user’s turn, two consecutive user messages are merged into one:
messages = [ Riffer::Messages::System.new("You are a code reviewer."), Riffer::Messages::User.new("The repository uses RSpec for testing."), Riffer::Messages::User.new("Review this pull request.") ] agent.generate(messages) # The provider receives two messages: # 1. System — "You are a code reviewer." # 2. User — "The repository uses RSpec for testing.\n\nReview this pull request."
Merging happens at serialization time only. The agent’s messages array still contains the original separate messages for logging, evals, and debugging.
Base Class
All messages inherit from Riffer::Messages::Base:
class Riffer::Messages::Base attr_reader :content def role raise NotImplementedError end def to_h {role: role, content: content} end end
Subclasses implement role and optionally extend to_h with additional fields.