lenzcom's picture
Upload folder using huggingface_hub
e706de2 verified

Messages & Types

Part 1: Foundation - Lesson 2

Structuring conversation data for AI agents

Overview

In Lesson 1, you learned about Runnables - the foundation of composability. Now we'll explore how to structure the data that flows through these Runnables, specifically for conversational AI systems.

Messages are the lingua franca of AI agents. They provide a standardized way to represent conversations, tool calls, system instructions, and more. By the end of this lesson, you'll understand why proper message typing is crucial for building reliable agents.

You will see tools in use. Everything about tools will be explained in depth in the 3. main part of this tutorial.

Why Does This Matter?

The Problem: Unstructured Conversations

Imagine building a chatbot without message types:

// Bad: Everything is just strings
const conversation = [
  "You are a helpful assistant",
  "What's the weather?",
  "The weather is sunny",
  "Thanks!"
];

// Questions:
// - Which messages are from the user?
// - Which are from the AI?
// - Which is the system instruction?
// - How do we handle tool calls?
// - What about metadata like timestamps?

This quickly becomes unmaintainable. You can't tell who said what, when, or why.

The Solution: Typed Messages

// Good: Structured message types
const conversation = [
  new SystemMessage("You are a helpful assistant"),
  new HumanMessage("What's the weather?"),
  new AIMessage("The weather is sunny"),
  new HumanMessage("Thanks!")
];

// Now we can:
// - Filter by message type
// - Format differently for display
// - Track metadata automatically
// - Handle tool calls properly
// - Validate conversation structure

Learning Objectives

By the end of this lesson, you will:

  • βœ… Understand the four core message types
  • βœ… Implement a robust message class hierarchy
  • βœ… Add metadata and timestamps automatically
  • βœ… Format messages for LLM consumption
  • βœ… Build a conversation history manager
  • βœ… Handle special message types (tool calls, function results)

Core Message Types

Every conversational AI system needs these four fundamental message types:

1. SystemMessage

Purpose: Instructions that shape the AI's behavior

Characteristics:

  • Always at the start of conversations
  • Not visible to end users
  • Defines the AI's role, personality, constraints
  • Typically set by developers, not users

Example:

new SystemMessage(
  `You are a helpful Python programming tutor. 
   Explain concepts clearly with code examples. 
   Always encourage learning.`
)

When to use:

  • Setting AI personality
  • Defining response format
  • Adding constraints or rules
  • Providing context

2. HumanMessage

Purpose: Input from the user/human

Characteristics:

  • User questions, commands, or statements
  • What the AI should respond to
  • Can contain multiple paragraphs
  • May include context or files

Example:

new HumanMessage(
  "How do I reverse a string in Python?"
)

When to use:

  • User input in chatbots
  • Queries to agents
  • Commands to execute

3. AIMessage

Purpose: Responses from the AI/assistant

Characteristics:

  • The AI's text responses
  • Can include reasoning, answers, questions
  • May contain tool calls (function requests)
  • Generated by the LLM

Example:

new AIMessage(
  "Here's how to reverse a string in Python:\n\n" +
  "```python\ntext = 'hello'\nreversed_text = text[::-1]\n```"
)

When to use:

  • LLM responses
  • Agent outputs
  • Generated text

4. ToolMessage

Purpose: Results from tool/function execution

Characteristics:

  • Returns data from external functions
  • Links back to the AI's tool call
  • Often structured data (JSON)
  • Input for AI's next response

Example:

new ToolMessage(
  JSON.stringify({ temperature: 72, condition: "sunny" }),
  "get_weather" // tool name
)

When to use:

  • Returning function results to the AI
  • Providing external data
  • Completing tool calls

Message Flow in Conversations

Here's how messages typically flow in an agent conversation:

1. System    β†’ "You are a helpful assistant with access to a calculator"
2. Human     β†’ "What's 123 * 456?"
3. AI        β†’ [Calls calculator tool with 123, 456]
4. Tool      β†’ [Returns 56088]
5. AI        β†’ "The result of 123 * 456 is 56,088"
6. Human     β†’ "Thanks!"
7. AI        β†’ "You're welcome!"

Implementation

Let's build our message system from the ground up.

Step 1: Base Message Class

Every message type will inherit from this base:

Location: src/core/message.js

/**
 * BaseMessage - Foundation for all message types
 * 
 * Contains common functionality:
 * - Content storage
 * - Metadata tracking
 * - Timestamps
 * - Serialization
 */
export class BaseMessage {
  constructor(content, additionalKwargs = {}) {
    this.content = content;
    this.additionalKwargs = additionalKwargs;
    this.timestamp = Date.now();
    this.id = this.generateId();
  }

  /**
   * Generate unique ID for this message
   */
  generateId() {
    return `msg_${this.timestamp}_${Math.random().toString(36).substr(2, 9)}`;
  }

  /**
   * Get the message type (overridden in subclasses)
   */
  get type() {
    throw new Error('Subclass must implement type getter');
  }

  /**
   * Convert to JSON for storage/transmission
   */
  toJSON() {
    return {
      id: this.id,
      type: this.type,
      content: this.content,
      timestamp: this.timestamp,
      ...this.additionalKwargs
    };
  }

  /**
   * Create message from JSON
   */
  static fromJSON(json) {
    const MessageClass = MESSAGE_TYPES[json.type];
    if (!MessageClass) {
      throw new Error(`Unknown message type: ${json.type}`);
    }
    
    const message = new MessageClass(json.content, json.additionalKwargs);
    message.id = json.id;
    message.timestamp = json.timestamp;
    return message;
  }

  /**
   * Format for display
   */
  toString() {
    const date = new Date(this.timestamp).toLocaleTimeString();
    return `[${date}] ${this.type}: ${this.content}`;
  }
}

Key design decisions:

  • βœ… content is always a string (or can be converted to one)
  • βœ… additionalKwargs allows extension without changing the API
  • βœ… timestamp is added automatically for tracking
  • βœ… id ensures we can reference specific messages
  • βœ… toJSON() / fromJSON() enable persistence

Step 2: System Message

Location: src/core/message.js

/**
 * SystemMessage - Instructions for the AI
 * 
 * Sets the context, role, and constraints for the assistant.
 * Typically appears at the start of conversations.
 */
export class SystemMessage extends BaseMessage {
  constructor(content, additionalKwargs = {}) {
    super(content, additionalKwargs);
  }

  get type() {
    return 'system';
  }

  /**
   * System messages often need special formatting
   */
  toPromptFormat() {
    return {
      role: 'system',
      content: this.content
    };
  }
}

Usage:

const systemMsg = new SystemMessage(
  "You are a expert Python programmer. Be concise."
);

console.log(systemMsg.type); // "system"
console.log(systemMsg.content); // "You are a expert..."

Step 3: Human Message

Location: src/core/message.js

/**
 * HumanMessage - User input
 * 
 * Represents messages from the human/user.
 * The primary input the AI responds to.
 */
export class HumanMessage extends BaseMessage {
  constructor(content, additionalKwargs = {}) {
    super(content, additionalKwargs);
  }

  get type() {
    return 'human';
  }

  toPromptFormat() {
    return {
      role: 'user',
      content: this.content
    };
  }
}

Usage:

const humanMsg = new HumanMessage("What's the capital of France?");

console.log(humanMsg.type); // "human"

Step 4: AI Message (with Tool Calls)

Location: src/core/message.js

/**
 * AIMessage - Assistant responses
 * 
 * Represents messages from the AI assistant.
 * Can include tool calls for function execution.
 */
export class AIMessage extends BaseMessage {
  constructor(content, additionalKwargs = {}) {
    super(content, additionalKwargs);
    
    // Tool calls are requests to execute functions
    this.toolCalls = additionalKwargs.toolCalls || [];
  }

  get type() {
    return 'ai';
  }

  /**
   * Check if this message requests tool execution
   */
  hasToolCalls() {
    return this.toolCalls.length > 0;
  }

  /**
   * Get specific tool call by index
   */
  getToolCall(index = 0) {
    return this.toolCalls[index];
  }

  toPromptFormat() {
    const formatted = {
      role: 'assistant',
      content: this.content
    };

    if (this.hasToolCalls()) {
      formatted.tool_calls = this.toolCalls;
    }

    return formatted;
  }
}

Usage:

// Simple response
const aiMsg1 = new AIMessage("The capital of France is Paris.");

// Response with tool call
const aiMsg2 = new AIMessage("Let me calculate that for you.", {
  toolCalls: [{
    id: 'call_123',
    type: 'function',
    function: {
      name: 'calculator',
      arguments: JSON.stringify({ operation: 'multiply', a: 5, b: 3 })
    }
  }]
});

console.log(aiMsg2.hasToolCalls()); // true

Step 5: Tool Message

Location: src/core/message.js

/**
 * ToolMessage - Tool execution results
 * 
 * Contains the output from executing a tool/function.
 * Sent back to the AI to inform its next response.
 */
export class ToolMessage extends BaseMessage {
  constructor(content, toolCallId, additionalKwargs = {}) {
    super(content, additionalKwargs);
    this.toolCallId = toolCallId;
  }

  get type() {
    return 'tool';
  }

  toPromptFormat() {
    return {
      role: 'tool',
      content: this.content,
      tool_call_id: this.toolCallId
    };
  }
}

Usage:

const toolMsg = new ToolMessage(
  JSON.stringify({ result: 15 }),
  'call_123' // Links back to the AI's tool call
);

console.log(toolMsg.type); // "tool"
console.log(toolMsg.toolCallId); // "call_123"

Step 6: Message Type Registry

To support fromJSON(), we need a registry:

/**
 * Registry mapping type strings to message classes
 */
export const MESSAGE_TYPES = {
  'system': SystemMessage,
  'human': HumanMessage,
  'ai': AIMessage,
  'tool': ToolMessage
};

Complete Implementation

Here's everything together:

/**
 * Message System - Typed conversation data structures
 * 
 * @module core/message
 */

/**
 * BaseMessage - Foundation for all message types
 */
export class BaseMessage {
  constructor(content, additionalKwargs = {}) {
    if (content === undefined || content === null) {
      throw new Error('Message content cannot be undefined or null');
    }

    this.content = String(content); // Ensure string
    this.additionalKwargs = additionalKwargs;
    this.timestamp = Date.now();
    this.id = this.generateId();
  }

  generateId() {
    return `msg_${this.timestamp}_${Math.random().toString(36).substr(2, 9)}`;
  }

  get type() {
    throw new Error('Subclass must implement type getter');
  }

  toJSON() {
    return {
      id: this.id,
      type: this.type,
      content: this.content,
      timestamp: this.timestamp,
      ...this.additionalKwargs
    };
  }

  static fromJSON(json) {
    const MessageClass = MESSAGE_TYPES[json.type];
    if (!MessageClass) {
      throw new Error(`Unknown message type: ${json.type}`);
    }
    
    const message = new MessageClass(json.content, json.additionalKwargs);
    message.id = json.id;
    message.timestamp = json.timestamp;
    return message;
  }

  toString() {
    const date = new Date(this.timestamp).toLocaleTimeString();
    return `[${date}] ${this.type.toUpperCase()}: ${this.content}`;
  }

  /**
   * Format for LLM consumption
   */
  toPromptFormat() {
    throw new Error('Subclass must implement toPromptFormat()');
  }
}

/**
 * SystemMessage - AI instructions and context
 */
export class SystemMessage extends BaseMessage {
  get type() {
    return 'system';
  }

  toPromptFormat() {
    return {
      role: 'system',
      content: this.content
    };
  }
}

/**
 * HumanMessage - User input
 */
export class HumanMessage extends BaseMessage {
  get type() {
    return 'human';
  }

  toPromptFormat() {
    return {
      role: 'user',
      content: this.content
    };
  }
}

/**
 * AIMessage - Assistant responses
 */
export class AIMessage extends BaseMessage {
  constructor(content, additionalKwargs = {}) {
    super(content, additionalKwargs);
    this.toolCalls = additionalKwargs.toolCalls || [];
  }

  get type() {
    return 'ai';
  }

  hasToolCalls() {
    return this.toolCalls.length > 0;
  }

  getToolCall(index = 0) {
    return this.toolCalls[index];
  }

  toPromptFormat() {
    const formatted = {
      role: 'assistant',
      content: this.content
    };

    if (this.hasToolCalls()) {
      formatted.tool_calls = this.toolCalls;
    }

    return formatted;
  }
}

/**
 * ToolMessage - Function execution results
 */
export class ToolMessage extends BaseMessage {
  constructor(content, toolCallId, additionalKwargs = {}) {
    super(content, additionalKwargs);
    this.toolCallId = toolCallId;
  }

  get type() {
    return 'tool';
  }

  toPromptFormat() {
    return {
      role: 'tool',
      content: this.content,
      tool_call_id: this.toolCallId
    };
  }
}

/**
 * Message type registry
 */
export const MESSAGE_TYPES = {
  'system': SystemMessage,
  'human': HumanMessage,
  'ai': AIMessage,
  'tool': ToolMessage
};

export default {
  BaseMessage,
  SystemMessage,
  HumanMessage,
  AIMessage,
  ToolMessage,
  MESSAGE_TYPES
};

Real-World Examples

Example 1: Simple Conversation

const conversation = [
  new SystemMessage("You are a helpful math tutor."),
  new HumanMessage("What's 5 + 3?"),
  new AIMessage("5 + 3 equals 8."),
  new HumanMessage("Thanks!"),
  new AIMessage("You're welcome!")
];

// Display conversation
conversation.forEach(msg => {
  console.log(msg.toString());
});

// Output:
// [10:30:45] SYSTEM: You are a helpful math tutor.
// [10:30:46] HUMAN: What's 5 + 3?
// [10:30:47] AI: 5 + 3 equals 8.
// [10:30:48] HUMAN: Thanks!
// [10:30:49] AI: You're welcome!

Example 2: Tool Call Flow

// User asks a question requiring calculation
const messages = [
  new SystemMessage("You are an assistant with access to a calculator."),
  new HumanMessage("What's 1234 * 5678?")
];

// AI decides to use calculator
const aiWithToolCall = new AIMessage(
  "I'll calculate that for you.",
  {
    toolCalls: [{
      id: 'call_abc123',
      type: 'function',
      function: {
        name: 'calculator',
        arguments: JSON.stringify({ 
          operation: 'multiply', 
          a: 1234, 
          b: 5678 
        })
      }
    }]
  }
);

messages.push(aiWithToolCall);

// Tool executes and returns result
const toolResult = new ToolMessage(
  JSON.stringify({ result: 7006652 }),
  'call_abc123'
);

messages.push(toolResult);

// AI incorporates result in final response
const finalResponse = new AIMessage(
  "The result of 1234 Γ— 5678 is 7,006,652."
);

messages.push(finalResponse);

console.log('Has tool calls?', aiWithToolCall.hasToolCalls()); // true
console.log('Tool call:', aiWithToolCall.getToolCall(0));

Example 3: Conversation Persistence

// Save conversation to JSON
const conversation = [
  new SystemMessage("You are helpful."),
  new HumanMessage("Hello!"),
  new AIMessage("Hi there!")
];

const json = conversation.map(msg => msg.toJSON());
const saved = JSON.stringify(json, null, 2);
console.log('Saved:', saved);

// Later: Load conversation from JSON
const loaded = JSON.parse(saved);
const restored = loaded.map(msgData => BaseMessage.fromJSON(msgData));

console.log('Restored:', restored.length, 'messages');
restored.forEach(msg => console.log(msg.toString()));

Example 4: Filtering Messages

const history = [
  new SystemMessage("You are helpful."),
  new HumanMessage("Hi"),
  new AIMessage("Hello!"),
  new HumanMessage("How are you?"),
  new AIMessage("I'm doing well!")
];

// Get only human messages
const humanMessages = history.filter(msg => msg.type === 'human');
console.log('Human said:', humanMessages.map(m => m.content));

// Get only AI messages
const aiMessages = history.filter(msg => msg.type === 'ai');
console.log('AI said:', aiMessages.map(m => m.content));

// Get last N messages (sliding window)
const lastThree = history.slice(-3);
console.log('Recent:', lastThree.map(m => m.toString()));

Example 5: Custom Metadata

// Add custom metadata to messages
const userMsg = new HumanMessage("Hello!", {
  userId: 'user_123',
  sessionId: 'sess_456',
  language: 'en'
});

const aiMsg = new AIMessage("Hi there!", {
  model: 'llama-3.1',
  temperature: 0.7,
  tokens: 150
});

console.log('User metadata:', userMsg.additionalKwargs);
console.log('AI metadata:', aiMsg.additionalKwargs);

// Metadata is preserved in JSON
const json = userMsg.toJSON();
console.log(json.userId); // 'user_123'

Advanced Patterns

Pattern 1: Message Builder

For complex message construction:

class MessageBuilder {
  constructor() {
    this.messages = [];
  }

  system(content) {
    this.messages.push(new SystemMessage(content));
    return this; // Chainable
  }

  human(content, metadata = {}) {
    this.messages.push(new HumanMessage(content, metadata));
    return this;
  }

  ai(content, metadata = {}) {
    this.messages.push(new AIMessage(content, metadata));
    return this;
  }

  build() {
    return this.messages;
  }
}

// Usage
const conversation = new MessageBuilder()
  .system("You are helpful.")
  .human("Hello!")
  .ai("Hi there!")
  .human("How are you?")
  .ai("I'm great!")
  .build();

Pattern 2: Conversation History Manager

class ConversationHistory {
  constructor(maxMessages = 100) {
    this.messages = [];
    this.maxMessages = maxMessages;
  }

  add(message) {
    this.messages.push(message);
    
    // Keep only last N messages (sliding window)
    if (this.messages.length > this.maxMessages) {
      // Always keep system message if it exists
      const systemMsg = this.messages.find(m => m.type === 'system');
      const recentMessages = this.messages.slice(-this.maxMessages + 1);
      
      this.messages = systemMsg 
        ? [systemMsg, ...recentMessages.filter(m => m.type !== 'system')]
        : recentMessages;
    }
  }

  getAll() {
    return [...this.messages]; // Return copy
  }

  getLast(n = 1) {
    return this.messages.slice(-n);
  }

  getByType(type) {
    return this.messages.filter(msg => msg.type === type);
  }

  clear() {
    // Keep system message
    const systemMsg = this.messages.find(m => m.type === 'system');
    this.messages = systemMsg ? [systemMsg] : [];
  }

  toPromptFormat() {
    return this.messages.map(msg => msg.toPromptFormat());
  }

  save() {
    return JSON.stringify(this.messages.map(m => m.toJSON()));
  }

  static load(json) {
    const data = JSON.parse(json);
    const history = new ConversationHistory();
    history.messages = data.map(msgData => BaseMessage.fromJSON(msgData));
    return history;
  }
}

// Usage
const history = new ConversationHistory(maxMessages: 50);
history.add(new SystemMessage("You are helpful."));
history.add(new HumanMessage("Hi"));
history.add(new AIMessage("Hello!"));

console.log('Total messages:', history.getAll().length);
console.log('Last message:', history.getLast()[0].content);

// Format for LLM
const formatted = history.toPromptFormat();
// [{ role: 'system', content: '...' }, { role: 'user', content: '...' }, ...]

Pattern 3: Message Validation

class MessageValidator {
  static validate(message) {
    const errors = [];

    // Check content
    if (!message.content || message.content.trim().length === 0) {
      errors.push('Message content cannot be empty');
    }

    // Check type
    if (!MESSAGE_TYPES[message.type]) {
      errors.push(`Invalid message type: ${message.type}`);
    }

    // Check tool messages have tool call ID
    if (message.type === 'tool' && !message.toolCallId) {
      errors.push('Tool messages must have a toolCallId');
    }

    return {
      valid: errors.length === 0,
      errors
    };
  }

  static validateConversation(messages) {
    const errors = [];

    // First message should be system message (recommended)
    if (messages.length > 0 && messages[0].type !== 'system') {
      errors.push('Conversation should start with a system message');
    }

    // Tool messages should follow AI messages with tool calls
    for (let i = 1; i < messages.length; i++) {
      const prev = messages[i - 1];
      const curr = messages[i];

      if (curr.type === 'tool') {
        if (prev.type !== 'ai' || !prev.hasToolCalls()) {
          errors.push(
            `Tool message at index ${i} should follow AI message with tool calls`
          );
        }
      }
    }

    return {
      valid: errors.length === 0,
      errors
    };
  }
}

// Usage
const msg = new HumanMessage("Hello!");
const result = MessageValidator.validate(msg);
console.log('Valid?', result.valid);
console.log('Errors:', result.errors);

Pattern 4: Message Formatting Utilities

class MessageFormatter {
  /**
   * Format messages for display in UI
   */
  static toDisplayFormat(messages) {
    return messages.map(msg => {
      const time = new Date(msg.timestamp).toLocaleTimeString();
      const icon = this.getIcon(msg.type);
      
      return {
        id: msg.id,
        icon,
        time,
        type: msg.type,
        content: msg.content,
        sender: this.getSenderName(msg.type)
      };
    });
  }

  static getIcon(type) {
    const icons = {
      'system': 'βš™οΈ',
      'human': 'πŸ‘€',
      'ai': 'πŸ€–',
      'tool': 'πŸ› οΈ'
    };
    return icons[type] || 'πŸ’¬';
  }

  static getSenderName(type) {
    const names = {
      'system': 'System',
      'human': 'You',
      'ai': 'Assistant',
      'tool': 'Tool'
    };
    return names[type] || 'Unknown';
  }

  /**
   * Format messages for LLM (OpenAI-style)
   */
  static toOpenAIFormat(messages) {
    return messages.map(msg => {
      const formatted = msg.toPromptFormat();
      
      // Map our types to OpenAI's expected roles
      const roleMap = {
        'human': 'user',
        'ai': 'assistant',
        'system': 'system',
        'tool': 'tool'
      };
      
      formatted.role = roleMap[msg.type] || msg.type;
      return formatted;
    });
  }

  /**
   * Create markdown representation
   */
  static toMarkdown(messages) {
    return messages.map(msg => {
      const sender = this.getSenderName(msg.type);
      const time = new Date(msg.timestamp).toLocaleString();
      return `**${sender}** (${time})\n\n${msg.content}\n\n---\n`;
    }).join('\n');
  }
}

// Usage
const messages = [
  new SystemMessage("You are helpful."),
  new HumanMessage("Hi"),
  new AIMessage("Hello!")
];

console.log('Display format:', MessageFormatter.toDisplayFormat(messages));
console.log('OpenAI format:', MessageFormatter.toOpenAIFormat(messages));
console.log('Markdown:', MessageFormatter.toMarkdown(messages));

Integration with LLMs

Messages need to be formatted for the LLM. Here's how different models expect them:

OpenAI-Style Format

function formatForOpenAI(messages) {
  return messages.map(msg => ({
    role: msg.type === 'human' ? 'user' : msg.type,
    content: msg.content
  }));
}

Llama-Style Format (with chat template)

function formatForLlama(messages) {
  // Llama uses special tokens
  let formatted = '';
  
  for (const msg of messages) {
    if (msg.type === 'system') {
      formatted += `<<SYS>>\n${msg.content}\n<</SYS>>\n\n`;
    } else if (msg.type === 'human') {
      formatted += `[INST] ${msg.content} [/INST]`;
    } else if (msg.type === 'ai') {
      formatted += `${msg.content}`;
    }
  }
  
  return formatted;
}

Our Flexible Approach

class MessageFormatter {
  static format(messages, style = 'openai') {
    const formatters = {
      'openai': this.formatOpenAI,
      'llama': this.formatLlama,
      'raw': this.formatRaw
    };
    
    const formatter = formatters[style];
    if (!formatter) {
      throw new Error(`Unknown format style: ${style}`);
    }
    
    return formatter(messages);
  }
}

Debugging Tips

Tip 1: Pretty Print Conversations

function printConversation(messages) {
  console.log('\n=== Conversation ===\n');
  messages.forEach((msg, idx) => {
    console.log(`${idx + 1}. ${msg.toString()}`);
  });
  console.log('\n===================\n');
}

Tip 2: Visualize Message Flow

function visualizeFlow(messages) {
  const flow = messages.map(msg => {
    const icon = msg.type === 'human' ? 'β†’' : '←';
    return `${icon} ${msg.type}: ${msg.content.substring(0, 50)}...`;
  });
  
  console.log('\nMessage Flow:');
  flow.forEach(line => console.log(line));
}

Tip 3: Inspect Metadata

function inspectMetadata(message) {
  console.log('Message Details:');
  console.log('- ID:', message.id);
  console.log('- Type:', message.type);
  console.log('- Timestamp:', new Date(message.timestamp).toISOString());
  console.log('- Content length:', message.content.length);
  console.log('- Metadata:', message.additionalKwargs);
  
  if (message.type === 'ai' && message.hasToolCalls()) {
    console.log('- Tool calls:', message.toolCalls.length);
  }
}

Common Mistakes

❌ Mistake 1: Wrong Message Order

// Bad: AI message before human input
const bad = [
  new AIMessage("Hello!"),
  new HumanMessage("Hi")
];

Fix: Always respond TO something

const good = [
  new HumanMessage("Hi"),
  new AIMessage("Hello!")
];

❌ Mistake 2: Forgetting System Message

// Bad: No context for the AI
const bad = [
  new HumanMessage("Write code")
];

Fix: Always set context

const good = [
  new SystemMessage("You are a coding assistant."),
  new HumanMessage("Write code")
];

❌ Mistake 3: Not Linking Tool Messages

// Bad: Tool message without proper ID
const bad = new ToolMessage("result", undefined);

Fix: Always link to the tool call

const toolCallId = aiMessage.getToolCall(0).id;
const good = new ToolMessage("result", toolCallId);

❌ Mistake 4: Modifying Message Content

// Bad: Changing message after creation
const msg = new HumanMessage("Hello");
msg.content = "Hi"; // Don't do this!

Fix: Create a new message

const newMsg = new HumanMessage("Hi");

Mental Model

Think of messages as a timeline:

Time β†’
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→

βš™οΈ System: "You are helpful"
   β”‚
   β”œβ†’ Human: "What's 2+2?"
   β”‚
   β”œβ†’ AI: "Let me calculate..."
   β”‚  └─ [Tool Call: calculator(2, 2)]
   β”‚
   β”œβ†’  Tool: "4"
   β”‚
   β””β†’ AI: "2+2 equals 4"

Each message is immutable - once created, it represents a moment in time.

Exercises

Practice what you've learned!

Exercise 5: Build a Message Formatter

Create a function that formats messages for console display with colors and icons.

Requirements:

  • Different colors for each message type
  • Icons for visual distinction
  • Timestamp display
  • Content truncation for long messages

Starter code: exercises/02-message-formatter.js

Exercise 6: Implement Conversation Validation

Build a validator that checks conversation structure.

Rules to check:

  • System message should be first (if present)
  • Tool messages must follow AI messages with tool calls
  • No empty messages
  • Alternating human/AI after system message

Starter code: exercises/02-conversation-validator.js

Exercise 7: Create a Chat History Manager

Build a class that manages conversation history with:

  • Add messages
  • Get last N messages
  • Filter by type
  • Save/load from JSON
  • Sliding window (max messages)

Starter code: exercises/02-chat-history.js

Exercise 8: Tool Call Flow

Simulate a complete tool call flow:

  1. Human asks a question requiring a tool
  2. AI responds with a tool call
  3. Tool executes and returns result
  4. AI incorporates result in response

Starter code: exercises/02-tool-flow.js

Summary

Congratulations! You now understand message types and why they're crucial for AI agents.

Key Takeaways

  1. Four core types: System, Human, AI, Tool
  2. Structured data: Messages have types, timestamps, IDs, metadata
  3. Immutability: Messages represent moments in time
  4. Tool calls: AI messages can request function execution
  5. Serialization: Messages can be saved/loaded as JSON

Why This Matters

  • βœ… Clarity: Know who said what, when
  • βœ… Debugging: Track conversation flow easily
  • βœ… Persistence: Save and restore conversations
  • βœ… Validation: Ensure proper structure
  • βœ… Formatting: Adapt to different LLM formats

Building Blocks

Messages are the data structure that flows through Runnables:

// Messages flow through Runnables
const conversation = [
  new SystemMessage("You are helpful"),
  new HumanMessage("Hello")
];

// Runnables process messages
const response = await chatModel.invoke(conversation);
// Returns: AIMessage("Hi there!")

Next Steps

In the next lesson, we'll wrap node-llama-cpp as a Runnable that works with our message types!

Preview: You'll learn:

  • Loading local LLMs
  • Converting messages to prompts
  • Streaming responses
  • Managing model context

➑️ Continue to Lesson 3: The LLM Wrapper

Additional Resources

Questions & Discussion

Q: Why not just use strings?

A: Strings don't capture metadata, can't distinguish between types, and make debugging harder. Typed messages provide structure and context.

Q: Can I add custom message types?

A: Yes! Extend BaseMessage and register in MESSAGE_TYPES:

class CustomMessage extends BaseMessage {
  get type() { return 'custom'; }
}
MESSAGE_TYPES['custom'] = CustomMessage;

Q: How do I handle multi-modal content (images)?

A: Store in additionalKwargs:

new HumanMessage("What's in this image?", {
  images: ['data:image/png;base64,...']
})

Q: Should I validate every message?

A: For production, yes. For development, optional but helpful for catching bugs early.


Built with ❀️ for learners who want to understand AI agents deeply

← Previous: Runnable | Tutorial Index | Next: LLM Wrapper β†’