Message Types
When you interact with a Letta agent and retrieve its message history using client.agents.messages.list(), you’ll receive various types of messages that represent different aspects of the agent’s execution. This guide explains all message types and how to work with them.
Overview
Section titled “Overview”Letta uses a structured message system where each message has a specific message_type field that indicates its purpose. Messages are returned as instances of LettaMessageUnion, which is a discriminated union of all possible message types.
Message Type Categories
Section titled “Message Type Categories”User and System Messages
Section titled “User and System Messages”user_message
Section titled “user_message”Messages sent by the user or system events packaged as user input.
Structure:
{ id: string; date: datetime; message_type: "user_message"; content: string | Array<TextContent | ImageContent>; name?: string; otid?: string; sender_id?: string;}Special User Message Subtypes:
User messages can contain JSON with a type field indicating special message subtypes:
-
login- User login events{"type": "login","last_login": "Never (first login)","time": "2025-10-03 12:34:56 PM PDT-0700"} -
user_message- Standard user messages{"type": "user_message","message": "Hello, agent!","time": "2025-10-03 12:34:56 PM PDT-0700"} -
system_alert- System notifications and alerts{"type": "system_alert","message": "System notification text","time": "2025-10-03 12:34:56 PM PDT-0700"}
system_message
Section titled “system_message”Messages generated by the system, typically used for internal context.
Structure:
{ id: string; date: datetime; message_type: "system_message"; content: string; name?: string;}Note: System messages are never streamed back in responses; they’re only visible when paginating through message history.
Agent Reasoning and Responses
Section titled “Agent Reasoning and Responses”reasoning_message
Section titled “reasoning_message”Represents the agent’s internal reasoning or “chain of thought.”
Structure:
{ id: string; date: datetime; message_type: "reasoning_message"; reasoning: string; source: "reasoner_model" | "non_reasoner_model"; signature?: string;}Fields:
reasoning- The agent’s internal thought processsource- Whether this was generated by a model with native reasoning (like o1) or via promptingsignature- Optional cryptographic signature for reasoning verification (for models that support it)
hidden_reasoning_message
Section titled “hidden_reasoning_message”Represents reasoning that has been hidden from the response.
Structure:
{ id: string; date: datetime; message_type: "hidden_reasoning_message"; state: "redacted" | "omitted"; hidden_reasoning?: string;}Fields:
state: "redacted"- The provider redacted the reasoning contentstate: "omitted"- The API chose not to include reasoning (e.g., for o1/o3 models)
assistant_message
Section titled “assistant_message”The actual message content sent by the agent.
Structure:
{ id: string; date: datetime; message_type: "assistant_message"; content: string | Array<TextContent>; name?: string;}Tool Execution Messages
Section titled “Tool Execution Messages”tool_call_message
Section titled “tool_call_message”A request from the agent to execute a tool.
Structure:
{ id: string; date: datetime; message_type: "tool_call_message"; tool_call: { name: string; arguments: string; // JSON string tool_call_id: string; }}Example:
{ message_type: "tool_call_message", tool_call: { name: "archival_memory_search", arguments: '{"query": "user preferences", "page": 0}', tool_call_id: "call_abc123" }}tool_return_message
Section titled “tool_return_message”The result of a tool execution.
Structure:
{ id: string; date: datetime; message_type: "tool_return_message"; tool_return: string; status: "success" | "error"; tool_call_id: string; stdout?: string[]; stderr?: string[];}Fields:
tool_return- The formatted return value from the toolstatus- Whether the tool executed successfullystdout/stderr- Captured output from the tool execution (useful for debugging)
Human-in-the-Loop Messages
Section titled “Human-in-the-Loop Messages”approval_request_message
Section titled “approval_request_message”A request for human approval before executing a tool.
Structure:
{ id: string; date: datetime; message_type: "approval_request_message"; tool_call: { name: string; arguments: string; tool_call_id: string; }}See Human-in-the-Loop for more information on this experimental feature.
approval_response_message
Section titled “approval_response_message”The user’s response to an approval request.
Structure:
{ id: string; date: datetime; message_type: "approval_response_message"; approve: boolean; approval_request_id: string; reason?: string;}Working with Messages
Section titled “Working with Messages”Listing Messages
Section titled “Listing Messages”import { LettaClient } from "@letta-ai/letta-client";
const client = new LettaClient({ baseUrl: "https://api.letta.com",});
// List recent messagesconst messages = await client.agents.messages.list("agent-id", { limit: 50, useAssistantMessage: true,});
// Iterate through message typesfor (const message of messages) { switch (message.messageType) { case "user_message": console.log("User:", message.content); break; case "assistant_message": console.log("Agent:", message.content); break; case "reasoning_message": console.log("Reasoning:", message.reasoning); break; case "tool_call_message": console.log("Tool call:", message.toolCall.name); break; // ... handle other types }}from letta_client import Letta
client = Letta(base_url="https://api.letta.com")
# List recent messagesmessages = client.agents.messages.list( agent_id="agent-id", limit=50, use_assistant_message=True)
# Iterate through message typesfor message in messages: if message.message_type == "user_message": print(f"User: {message.content}") elif message.message_type == "assistant_message": print(f"Agent: {message.content}") elif message.message_type == "reasoning_message": print(f"Reasoning: {message.reasoning}") elif message.message_type == "tool_call_message": print(f"Tool call: {message.tool_call.name}") # ... handle other typesFiltering Messages by Type
Section titled “Filtering Messages by Type”// Get only assistant messages (what the agent said to the user)const agentMessages = messages.filter( (msg) => msg.messageType === "assistant_message",);
// Get all tool-related messagesconst toolMessages = messages.filter( (msg) => msg.messageType === "tool_call_message" || msg.messageType === "tool_return_message",);
// Get conversation history (user + assistant messages only)const conversation = messages.filter( (msg) => msg.messageType === "user_message" || msg.messageType === "assistant_message",);# Get only assistant messages (what the agent said to the user)agent_messages = [ msg for msg in messages if msg.message_type == "assistant_message"]
# Get all tool-related messagestool_messages = [ msg for msg in messages if msg.message_type in ["tool_call_message", "tool_return_message"]]
# Get conversation history (user + assistant messages only)conversation = [ msg for msg in messages if msg.message_type in ["user_message", "assistant_message"]]Pagination
Section titled “Pagination”Messages support cursor-based pagination:
// Get first pagelet messages = await client.agents.messages.list("agent-id", { limit: 100,});
// Get next page using the last message IDconst lastMessageId = messages[messages.length - 1].id;const nextPage = await client.agents.messages.list("agent-id", { limit: 100, before: lastMessageId,});# Get first pagemessages = client.agents.messages.list( agent_id="agent-id", limit=100)
# Get next page using the last message IDlast_message_id = messages[-1].idnext_page = client.agents.messages.list( agent_id="agent-id", limit=100, before=last_message_id)Message Metadata Fields
Section titled “Message Metadata Fields”All message types include these common fields:
id- Unique identifier for the messagedate- ISO 8601 timestamp of when the message was createdmessage_type- The discriminator field identifying the message typename- Optional name field (varies by message type)otid- Offline threading ID for message correlationsender_id- The ID of the sender (identity or agent ID)step_id- The step ID associated with this messageis_err- Whether this message is part of an error step (debugging only)seq_id- Sequence ID for orderingrun_id- The run ID associated with this message
Best Practices
Section titled “Best Practices”1. Use Type Discriminators
Section titled “1. Use Type Discriminators”Always check the message_type field to safely access type-specific fields:
if (message.messageType === "tool_call_message") { // TypeScript now knows message has a toolCall field console.log(message.toolCall.name);}if message.message_type == "tool_call_message": # Safe to access tool_call print(message.tool_call.name)2. Handle Special User Messages
Section titled “2. Handle Special User Messages”When displaying conversations to end users, filter out internal messages:
def is_internal_message(msg): """Check if a user message is internal (login, system_alert, etc.)""" if msg.message_type != "user_message": return False
if not isinstance(msg.content, str): return False
try: parsed = json.loads(msg.content) return parsed.get("type") in ["login", "system_alert"] except: return False
# Get user-facing messages onlydisplay_messages = [ msg for msg in messages if not is_internal_message(msg)]3. Track Tool Execution
Section titled “3. Track Tool Execution”Match tool calls with their returns using tool_call_id:
# Build a map of tool calls to their returnstool_calls = { msg.tool_call.tool_call_id: msg for msg in messages if msg.message_type == "tool_call_message"}
tool_returns = { msg.tool_call_id: msg for msg in messages if msg.message_type == "tool_return_message"}
# Find failed tool callsfor call_id, call_msg in tool_calls.items(): if call_id in tool_returns: return_msg = tool_returns[call_id] if return_msg.status == "error": print(f"Tool {call_msg.tool_call.name} failed:") print(f" {return_msg.tool_return}")See Also
Section titled “See Also”- Human-in-the-Loop - Using approval messages
- Streaming Responses - Receiving messages in real-time
- API Reference - Full API documentation