Conversations & Messages 💬
In Loom, a conversation is represented as an ordered list of messages. Because Loom is "state-first," these messages are typically stored in your GraphState and passed to LLMs to provide context.
1. Message Roles
Loom defines four core roles, each representing a different participant in the conversation:
| Role | Description | Concrete Type |
|---|---|---|
| System | High-level instructions for the model (e.g., "You are a helpful assistant"). | message.System |
| User | Messages sent by the human user. | message.User |
| Assistant | Responses generated by the AI model. | message.Assistant |
| Tool | The result of an external tool execution. | message.Tool |
Creating Messages
import "github.com/masterkeysrd/loom/message"
systemMsg := message.NewSystemText("You are a math tutor.")
userMsg := message.NewUserText("What is the square root of 144?")
2. Content Blocks
Loom messages are composed of one or more Blocks. This flexible structure supports multimodal inputs and advanced features like "chain-of-thought" thinking.
Text Blocks
The most common block type, used for simple text.
Multimodal Blocks (Images, Documents)
You can attach images, PDFs, or other media to a user message.
imageMsg := message.NewUserImage(imageData, "image/png")
// OR via URL
imageMsg := message.NewUserImageURL("https://example.com/photo.jpg")
Thinking Blocks
Some models (like Claude 3.5 or Gemini 2.0) can return their internal reasoning. Loom surfaces this as a ThinkingBlock.
3. Managing History (MessageList)
A MessageList is a specialized slice of messages that provides critical functionality for state management and persistence. Always use MessageList instead of a raw []message.Message when storing history in your Graph State.
Why use MessageList?
- Polymorphic Serialization:
MessageListautomatically handles the complex task of serializing different message types (User, Assistant, etc.) into a single JSON array with a "role" discriminator. - Type-Safe Deserialization: When loading a checkpoint from a database (SQL or PG),
MessageListensures that raw JSON is correctly restored into the proper Go structs. - Fluent Helpers: Provides methods like
Last()andText()to easily access content without range loops.
history := message.MessageList{
message.NewSystemText("..."),
message.NewUserText("..."),
}
// Add a new response
history = append(history, assistantResponse)
4. Token Metrics
Loom tracks token usage for every assistant response. This information is attached to the message.Assistant struct.
if resp.Metrics != nil {
fmt.Printf("Input: %d, Output: %d, Total: %d, Estimated Cost: %s\n",
resp.Metrics.Tokens.Input,
resp.Metrics.Tokens.Output,
resp.Metrics.TotalTokens,
resp.Metrics.TotalCost)
}
5. Message Extensions & Metadata
Loom provides two ways to attach auxiliary information to a message:
Extensions (Type-Safe Hints)
Extensions are provider-specific structs that influence model behavior. Unlike raw metadata, Extensions are type-safe and are automatically restored to their concrete types when loaded from storage.
Loom provides two methods for adding extensions:
WithExtension(Fluent): Available on concrete message types (User,Assistant, etc.). It returns the same concrete type, allowing for method chaining without type erasure.AddExtension(Polymorphic): Available on theMessageinterface. Use this when you are working with a generic message object.
// 1. Fluent usage (preserves *message.User type)
msg := message.NewUserText("Analyze this...").
WithExtension(&loomanthropic.MessageCache{Enabled: true})
// 2. Interface usage
var m message.Message = msg
m.AddExtension(&someother.Extension{})
Metadata (Arbitrary Data)
Metadata is a simple map[string]any for application-level data (IDs, timestamps, custom flags) that doesn't need a formal schema.
Summary
- Roles: Use
System,User,Assistant, andToolto distinguish participants. - Blocks: Mix and match
TextBlock,ImageBlock, andThinkingBlockfor rich interactions. - MessageList: Use this type to manage and persist your conversation history in your graph state.