Skip to main content
This guide provides comprehensive documentation on the composeState method in ElizaOS and how it interacts with different types of providers to build contextual state for agent decision-making.

Introduction

The composeState method is a core function in ElizaOS that aggregates data from multiple providers to create a comprehensive state object. This state represents the agent’s understanding of the current context and is used for decision-making, action selection, and response generation.

What is State?

State in ElizaOS is a structured object containing:
  • values: Key-value pairs for direct access (used in templates)
  • data: Structured data from providers
  • text: Concatenated textual context from all providers

Quick Reference

Provider Summary Table

Provider NameDynamicPositionDefault IncludedPurpose
ACTIONSNo-1YesLists available actions
ACTION_STATENo150YesAction execution state
ANXIETYNoDefaultYesResponse style guidelines
ATTACHMENTSYesDefaultNoFile/media attachments
CAPABILITIESNoDefaultYesService capabilities
CHARACTERNoDefaultYesAgent personality
CHOICENoDefaultYesPending user choices
ENTITIESYesDefaultNoConversation participants
EVALUATORSNoDefaultNo (private)Post-processing options
FACTSYesDefaultNoStored knowledge
PROVIDERSNoDefaultYesAvailable providers list
RECENT_MESSAGESNo100YesConversation history
RELATIONSHIPSYesDefaultNoSocial connections
ROLESNoDefaultYesServer roles (groups only)
SETTINGSNoDefaultYesConfiguration state
TIMENoDefaultYesCurrent UTC time
WORLDYesDefaultNoServer/world context

Common Usage Patterns

// Default state (all non-dynamic, non-private providers)
const state = await runtime.composeState(message);

// Include specific dynamic providers
const state = await runtime.composeState(message, ['FACTS', 'ENTITIES']);

// Only specific providers
const state = await runtime.composeState(message, ['CHARACTER'], true);

// Force fresh data (skip cache)
const state = await runtime.composeState(message, null, false, true);

Understanding composeState

Method Signature

async composeState(
    message: Memory,
    includeList: string[] | null = null,
    onlyInclude = false,
    skipCache = false
): Promise<State>

Parameters

  • message: The current message/memory object being processed
  • includeList: Array of provider names to include (optional)
  • onlyInclude: If true, ONLY include providers from includeList
  • skipCache: If true, bypass cache and fetch fresh data

How It Works

  1. Provider Selection: Determines which providers to run based on filters
  2. Parallel Execution: Runs all selected providers concurrently
  3. Result Aggregation: Combines results from all providers
  4. Caching: Stores the composed state for reuse

Provider Types

ElizaOS includes various built-in providers, each serving a specific purpose:

Core Providers

1. Character Provider

Provides character information, personality, and behavior guidelines.
// Included data:
{
  values: {
    agentName: "Alice",
    bio: "AI assistant focused on...",
    topics: "technology, science, education",
    adjective: "helpful"
  },
  data: { character: {...} },
  text: "# About Alice\n..."
}

2. Recent Messages Provider

Provides conversation history and context.
// Included data:
{
  values: {
    recentMessages: "User: Hello\nAlice: Hi there!",
    recentInteractions: "..."
  },
  data: {
    recentMessages: [...],
    actionResults: [...]
  },
  text: "# Conversation Messages\n..."
}

3. Actions Provider

Lists available actions the agent can take.
// Included data:
{
  values: {
    actionNames: "Possible response actions: 'SEND_MESSAGE', 'SEARCH', 'CALCULATE'",
    actionExamples: "..."
  },
  data: { actionsData: [...] },
  text: "# Available Actions\n..."
}

Dynamic Providers

Dynamic providers are only executed when explicitly requested:
  • Facts Provider: Retrieves relevant facts from memory
  • Relationships Provider: Gets relationship information
  • Settings Provider: Fetches configuration settings
  • Roles Provider: Server role hierarchy

Private Providers

Private providers are internal and not included in default state composition:
  • Evaluators Provider: Post-interaction processing options

Detailed Provider Reference

Built-in Providers

Actions Provider (ACTIONS)

  • Purpose: Lists all available actions the agent can execute
  • Position: -1 (runs early)
  • Dynamic: No (included by default)
  • Data Provided:
    • actionNames: Comma-separated list of action names
    • actionsWithDescriptions: Formatted action details
    • actionExamples: Example usage for each action
    • actionsData: Raw action objects

Action State Provider (ACTION_STATE)

  • Purpose: Shares execution state between chained actions
  • Position: 150
  • Dynamic: No (included by default)
  • Data Provided:
    • actionResults: Previous action execution results
    • actionPlan: Multi-step action execution plan
    • workingMemory: Temporary data shared between actions
    • recentActionMemories: Historical action executions

Anxiety Provider (ANXIETY)

  • Purpose: Provides behavioral guidelines based on channel type
  • Position: Default
  • Dynamic: No (included by default)
  • Behavior: Adjusts response style for DMs, groups, and voice channels

Attachments Provider (ATTACHMENTS)

  • Purpose: Lists files and media in the conversation
  • Position: Default
  • Dynamic: Yes (must be explicitly included)
  • Data Provided:
    • File names, URLs, descriptions
    • Text content from attachments
    • Media metadata

Capabilities Provider (CAPABILITIES)

  • Purpose: Lists agent’s available services and capabilities
  • Position: Default
  • Dynamic: No (included by default)
  • Data Provided: Service descriptions and available functions

Character Provider (CHARACTER)

  • Purpose: Core personality and behavior definition
  • Position: Default
  • Dynamic: No (included by default)
  • Data Provided:
    • agentName: Character name
    • bio: Character background
    • topics: Current interests
    • adjective: Current mood/state
    • directions: Style guidelines
    • examples: Example conversations/posts

Choice Provider (CHOICE)

  • Purpose: Lists pending decisions awaiting user input
  • Position: Default
  • Dynamic: No (included by default)
  • Use Case: Interactive workflows requiring user selection

Entities Provider (ENTITIES)

  • Purpose: Information about conversation participants
  • Position: Default
  • Dynamic: Yes (must be explicitly included)
  • Data Provided:
    • User names and aliases
    • Entity metadata
    • Sender identification

Facts Provider (FACTS)

  • Purpose: Retrieves relevant stored facts
  • Position: Default
  • Dynamic: Yes (must be explicitly included)
  • Behavior: Uses embedding search to find contextually relevant facts

Providers Provider (PROVIDERS)

  • Purpose: Meta-provider listing all available providers
  • Position: Default
  • Dynamic: No (included by default)
  • Use Case: Dynamic provider discovery

Recent Messages Provider (RECENT_MESSAGES)

  • Purpose: Conversation history and context
  • Position: 100 (runs later to access other data)
  • Dynamic: No (included by default)
  • Data Provided:
    • Recent dialogue messages
    • Action execution history
    • Formatted conversation context

Relationships Provider (RELATIONSHIPS)

  • Purpose: Social graph and interaction history
  • Position: Default
  • Dynamic: Yes (must be explicitly included)
  • Data Provided:
    • Known entities and their relationships
    • Interaction frequency
    • Relationship metadata

Roles Provider (ROLES)

  • Purpose: Server/group role hierarchy
  • Position: Default
  • Dynamic: No (included by default)
  • Restrictions: Only available in group channels

Settings Provider (SETTINGS)

  • Purpose: Configuration and onboarding state
  • Position: Default
  • Dynamic: No (included by default)
  • Behavior: Different output for DMs (onboarding) vs groups

Time Provider (TIME)

  • Purpose: Current time and timezone information
  • Position: Default
  • Dynamic: No (included by default)
  • Data Provided: Formatted timestamps and timezone data

World Provider (WORLD)

  • Purpose: Virtual world/server context
  • Position: Default
  • Dynamic: No (included by default)
  • Data Provided: World metadata and configuration

Cache Management

How Caching Works

The composeState method uses an in-memory cache (stateCache) to store composed states:
// Cache is stored by message ID
this.stateCache.set(message.id, newState);

Getting Cached Data

// Inside a runtime context (this.stateCache is a Map<UUID, State>)
// The cache is internal to the runtime and accessed via composeState with skipCache parameter

// To get fresh data, skip the cache:
const freshState = await runtime.composeState(message, null, false, true); // skipCache = true

// To use cached data (default behavior):
const cachedState = await runtime.composeState(message); // uses cache if available

Clearing Cache

import { AgentRuntime } from '@elizaos/core';

// The stateCache is internal to the runtime instance
// In a custom runtime extension or plugin initialization:

class ExtendedRuntime extends AgentRuntime {
  clearOldStateCache() {
    const oneHourAgo = Date.now() - 60 * 60 * 1000;

    for (const [messageId, state] of this.stateCache.entries()) {
      // Check if we have the message in memory
      this.getMemoryById(messageId).then((memory) => {
        if (memory && memory.createdAt < oneHourAgo) {
          this.stateCache.delete(messageId);
        }
      });
    }
  }

  // Clear entire cache
  clearAllStateCache() {
    this.stateCache.clear();
  }
}

Usage Scenarios

Scenario 1: Default State Composition

Most common usage - compose state with all non-private, non-dynamic providers:
// Default usage - includes all standard providers
const state = await runtime.composeState(message);

// Access composed data
console.log(state.values.agentName); // Character name
console.log(state.values.recentMessages); // Recent conversation
console.log(state.values.actionNames); // Available actions

Scenario 2: Including Dynamic Providers

Include specific dynamic providers for additional context:
// Include facts and relationships providers
const state = await runtime.composeState(
  message,
  ['FACTS', 'RELATIONSHIPS'], // Include these dynamic providers
  false, // Don't exclude other providers
  false // Use cache if available
);

// Access additional data
console.log(state.values.facts); // Relevant facts
console.log(state.values.relationships); // Relationship data

Scenario 3: Selective Provider Inclusion

Only include specific providers for focused processing:
// Only get character and recent messages
const state = await runtime.composeState(
  message,
  ['CHARACTER', 'RECENT_MESSAGES'], // Only these providers
  true, // onlyInclude = true
  false // Use cache
);

// State will only contain data from specified providers

Scenario 4: Force Fresh Data

Skip cache for real-time data requirements:
// Force fresh data fetch
const state = await runtime.composeState(
  message,
  null, // Include default providers
  false, // Don't limit to includeList
  true // skipCache = true
);

Examples

Example 1: Action Processing with State

import type { IAgentRuntime, Memory, State } from '@elizaos/core';

// In an action handler
async function handleAction(runtime: IAgentRuntime, message: Memory) {
  // Compose state with action-specific providers
  const state = await runtime.composeState(
    message,
    ['CHARACTER', 'RECENT_MESSAGES', 'ACTION_STATE'],
    false,
    false
  );

  // Use state for decision making
  if (state.values.hasActionResults) {
    console.log('Previous actions completed:', state.values.completedActions);
  }

  // Process action based on state
  // Actions are typically processed through runtime.processActions
  // but can be executed directly if needed
  const action = runtime.actions.find((a) => a.name === 'SEND_MESSAGE');
  if (action && action.handler) {
    const result = await action.handler(runtime, message, state);
    return result;
  }
}

Example 2: Settings-Based Configuration

// Check settings in DM for configuration
async function checkConfiguration(runtime: IAgentRuntime, message: Memory) {
  // Include settings provider for DM configuration
  const state = await runtime.composeState(
    message,
    ['SETTINGS', 'CHARACTER'],
    false,
    true // Skip cache for fresh settings
  );

  // Helper function to check if all required settings are configured
  const hasRequiredSettings = (settings: any) => {
    if (!settings) return false;

    // Check if all required settings have values
    return Object.entries(settings).every(([key, setting]: [string, any]) => {
      if (setting.required) {
        return setting.value !== null && setting.value !== undefined;
      }
      return true;
    });
  };

  // Check if configuration is needed
  const settings = state.data.providers?.SETTINGS?.data?.settings;
  if (settings && hasRequiredSettings(settings)) {
    return 'Configuration complete!';
  } else {
    return "Let's configure your settings...";
  }
}

Example 3: Context-Aware Response Generation

import { ModelType } from '@elizaos/core';

// Generate response with full context
async function generateContextualResponse(runtime: IAgentRuntime, message: Memory) {
  // Get comprehensive state
  const state = await runtime.composeState(
    message,
    ['CHARACTER', 'RECENT_MESSAGES', 'FACTS', 'ENTITIES', 'ATTACHMENTS'],
    false,
    false
  );

  // Build prompt from state
  const prompt = `
${state.values.bio}

${state.values.recentMessages}

Known facts:
${state.values.facts || 'No relevant facts'}

People in conversation:
${state.values.entities || 'Just you and me'}

Respond to the last message appropriately.
    `;

  // Generate response using composed context
  const response = await runtime.useModel(ModelType.TEXT_LARGE, {
    prompt,
    maxTokens: 500,
  });

  return response;
}

Example 4: Custom Provider Integration

import type { Provider } from '@elizaos/core';

// Define a custom provider
const customDataProvider: Provider = {
  name: 'CUSTOM_DATA',
  description: 'Custom data from external source',
  dynamic: true,
  get: async (runtime, message) => {
    try {
      // Example: fetch data from a service or database
      const customData = await runtime.getService('customService')?.getData();

      if (!customData) {
        return { values: {}, data: {}, text: '' };
      }

      return {
        values: { customData: customData.summary },
        data: { customData },
        text: `Custom data: ${customData.summary}`,
      };
    } catch (error) {
      runtime.logger.error('Error in custom provider:', error);
      return { values: {}, data: {}, text: '' };
    }
  },
};

// Register the provider
runtime.registerProvider(customDataProvider);

// Use in state composition
const state = await runtime.composeState(
  message,
  ['CHARACTER', 'CUSTOM_DATA'], // Include custom provider
  false,
  true
);

console.log(state.values.customData); // Access custom data value

Example 5: Performance Monitoring

// Monitor provider performance
async function composeStateWithMetrics(runtime: IAgentRuntime, message: Memory) {
  const startTime = Date.now();

  // Compose state
  const state = await runtime.composeState(message);

  // Check individual provider timings
  const providers = state.data.providers;
  for (const [name, result] of Object.entries(providers)) {
    console.log(`Provider ${name} data size:`, JSON.stringify(result).length);
  }

  const totalTime = Date.now() - startTime;
  console.log(`Total composition time: ${totalTime}ms`);

  return state;
}

Advanced Examples

Multi-Step Action Coordination

import type { IAgentRuntime, Memory, State } from '@elizaos/core';

// Example ActionPlan type
interface ActionPlan {
  thought: string;
  steps: Array<{
    actionName: string;
    params?: any;
  }>;
  totalSteps: number;
}

// Coordinate multiple actions with shared state
async function executeMultiStepPlan(runtime: IAgentRuntime, message: Memory, plan: ActionPlan) {
  const workingMemory: { [key: string]: any } = {};

  for (let i = 0; i < plan.steps.length; i++) {
    // Update state with working memory
    const state = await runtime.composeState(
      message,
      ['CHARACTER', 'ACTION_STATE', 'RECENT_MESSAGES'],
      false,
      true // Fresh state for each step
    );

    // Inject working memory into state
    state.data.workingMemory = workingMemory;
    state.data.actionPlan = {
      ...plan,
      currentStep: i + 1,
    };

    // Execute action through runtime's processActions
    const action = runtime.actions.find((a) => a.name === plan.steps[i].actionName);
    if (action && action.handler) {
      const result = await action.handler(runtime, message, state);

      // Store result in working memory
      workingMemory[`step_${i + 1}_result`] = result;
    }
  }

  return workingMemory;
}

Provider Dependency Management

// Provider that depends on other providers
const enhancedFactsProvider: Provider = {
  name: 'ENHANCED_FACTS',
  description: 'Facts with relationship context',
  dynamic: true,
  position: 200, // Run after other providers

  get: async (runtime, message, state) => {
    // First ensure we have entities and relationships
    const enhancedState = await runtime.composeState(
      message,
      ['ENTITIES', 'RELATIONSHIPS', 'FACTS'],
      false,
      false
    );

    // Enhance facts with relationship context
    const facts = enhancedState.data.providers?.FACTS?.data?.facts || [];
    const relationships = enhancedState.data.providers?.RELATIONSHIPS?.data?.relationships || [];

    // Helper function to find related entities
    const findRelatedEntities = (fact: any, relationships: any[]) => {
      // Simple implementation - match entities mentioned in fact text
      return relationships.filter((rel) => fact.content?.text?.includes(rel.targetEntityId));
    };

    const formatEnhancedFacts = (facts: any[]) => {
      return facts
        .map(
          (fact) => `${fact.content?.text} [Related: ${fact.relatedEntities?.length || 0} entities]`
        )
        .join('\n');
    };

    const enhancedFacts = facts.map((fact) => ({
      ...fact,
      relatedEntities: findRelatedEntities(fact, relationships),
    }));

    return {
      values: { enhancedFacts: formatEnhancedFacts(enhancedFacts) },
      data: { enhancedFacts },
      text: `Enhanced facts with relationships:\n${formatEnhancedFacts(enhancedFacts)}`,
    };
  },
};

Dynamic Provider Loading

import { ChannelType, type IAgentRuntime, type Memory } from '@elizaos/core';

// Conditionally load providers based on context
async function adaptiveStateComposition(runtime: IAgentRuntime, message: Memory) {
  // Determine context
  const room = await runtime.getRoom(message.roomId);
  const isDM = room?.type === ChannelType.DM;
  const isVoice = room?.type === ChannelType.VOICE_GROUP || room?.type === ChannelType.VOICE_DM;

  // Build provider list based on context
  const providers: string[] = ['CHARACTER', 'RECENT_MESSAGES'];

  if (isDM) {
    providers.push('SETTINGS'); // Configuration in DMs
  } else {
    providers.push('ROLES', 'ENTITIES'); // Group context
  }

  if (isVoice) {
    // Voice channels might need different providers
    // For example, you might want to limit providers for performance
    providers = ['CHARACTER', 'ANXIETY']; // Minimal providers for voice
  }

  // Check if user mentioned facts or history
  if (message.content.text.match(/remember|fact|history/i)) {
    providers.push('FACTS', 'RELATIONSHIPS');
  }

  // Compose adaptive state
  return runtime.composeState(message, providers, false, false);
}

State Transformation Pipeline

// Transform state through multiple stages
async function stateTransformationPipeline(runtime: IAgentRuntime, message: Memory) {
  // Stage 1: Base state
  const baseState = await runtime.composeState(
    message,
    ['CHARACTER', 'RECENT_MESSAGES'],
    true,
    false
  );

  // Helper function to determine if facts are needed
  const needsFactEnrichment = (state: State) => {
    // Check if the message mentions facts, history, or memory
    const messageText = state.values.recentMessages || '';
    return /remember|fact|history|memory/i.test(messageText);
  };

  // Stage 2: Enrich with facts if needed
  let enrichedState = baseState;
  if (needsFactEnrichment(baseState)) {
    const factsState = await runtime.composeState(message, ['FACTS'], false, false);
    enrichedState = mergeStates(baseState, factsState);
  }

  // Stage 3: Add action context
  const actionState = await runtime.composeState(
    message,
    ['ACTIONS', 'ACTION_STATE'],
    false,
    false
  );

  // Final merged state
  return mergeStates(enrichedState, actionState);
}

function mergeStates(state1: State, state2: State): State {
  return {
    values: { ...state1.values, ...state2.values },
    data: {
      ...state1.data,
      ...state2.data,
      providers: {
        ...state1.data.providers,
        ...state2.data.providers,
      },
    },
    text: `${state1.text}\n\n${state2.text}`,
  };
}

Troubleshooting

Common Issues and Solutions

1. Stale Cache Data

Problem: State contains outdated information
// Symptom: Old messages or incorrect data
const state = await runtime.composeState(message);
console.log(state.values.recentMessages); // Shows old messages
Solution: Force fresh data or implement cache TTL
// Option 1: Skip cache
const freshState = await runtime.composeState(message, null, false, true);

// Option 2: Implement cache TTL
class CacheWithTTL extends Map {
  private ttl: number;
  private timestamps = new Map<string, number>();

  constructor(ttl: number = 5 * 60 * 1000) {
    // 5 minutes default
    super();
    this.ttl = ttl;
  }

  set(key: string, value: any) {
    this.timestamps.set(key, Date.now());
    return super.set(key, value);
  }

  get(key: string) {
    const timestamp = this.timestamps.get(key);
    if (timestamp && Date.now() - timestamp > this.ttl) {
      this.delete(key);
      return undefined;
    }
    return super.get(key);
  }
}

2. Provider Timeout Issues

Problem: Slow providers blocking state composition
// Provider that might hang
const slowProvider: Provider = {
  name: 'SLOW_API',
  get: async (runtime, message) => {
    // This could take forever without proper timeout handling
    try {
      // Simulate a slow external API call
      const response = await fetch('https://slow-api.example.com/data');
      const data = await response.json();

      return {
        data: { apiResponse: data },
        values: { slowApiStatus: 'success' },
        text: `Fetched data from slow API`,
      };
    } catch (error) {
      // This might never complete if the API hangs
      return { data: {}, values: {}, text: '' };
    }
  },
};
Solution: Implement timeouts
const timeoutProvider: Provider = {
  name: 'TIMEOUT_SAFE',
  get: async (runtime, message) => {
    try {
      // Example of a provider that might take too long
      const fetchData = async () => {
        // Simulate an API call or expensive operation
        const service = runtime.getService('externalAPI');
        if (!service) {
          return { values: {}, data: {}, text: '' };
        }
        const data = await service.fetchData();
        return {
          values: { apiData: data.summary },
          data: { apiData: data },
          text: `API data: ${data.summary}`,
        };
      };

      const result = await Promise.race([
        fetchData(),
        new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000)),
      ]);
      return result;
    } catch (error) {
      runtime.logger.warn(`Provider TIMEOUT_SAFE timed out`);
      return { values: {}, data: {}, text: '' };
    }
  },
};

3. Memory Leaks from Cache

Problem: Cache grows indefinitely
// This will grow forever
for (const message of messages) {
  await runtime.composeState(message);
}
Solution: Implement cache size limits
class BoundedCache extends Map {
  private maxSize: number;

  constructor(maxSize: number = 1000) {
    super();
    this.maxSize = maxSize;
  }

  set(key: string, value: any) {
    // Remove oldest entries if at capacity
    if (this.size >= this.maxSize) {
      const firstKey = this.keys().next().value;
      this.delete(firstKey);
    }
    return super.set(key, value);
  }
}

4. Circular Provider Dependencies

Problem: Providers depending on each other
// DON'T DO THIS
const providerA: Provider = {
  name: 'A',
  get: async (runtime, message) => {
    const state = await runtime.composeState(message, ['B']);
    // Uses B's data
  },
};

const providerB: Provider = {
  name: 'B',
  get: async (runtime, message) => {
    const state = await runtime.composeState(message, ['A']);
    // Uses A's data - CIRCULAR!
  },
};
Solution: Use provider positioning and cached state
const providerA: Provider = {
  name: 'A',
  position: 100, // Runs first
  get: async (runtime, message) => {
    // Generate data independently
    return { data: { aData: 'value' } };
  },
};

const providerB: Provider = {
  name: 'B',
  position: 200, // Runs after A
  get: async (runtime, message, cachedState) => {
    // Access A's data from cached state
    const aData = cachedState.data?.providers?.A?.data?.aData;

    // Process the data from provider A
    const processedData = aData
      ? {
          enhanced: true,
          originalValue: aData,
          processedAt: Date.now(),
        }
      : null;

    return {
      data: { bData: processedData },
      values: { bProcessed: processedData ? 'success' : 'no data' },
      text: processedData ? `Processed data from A: ${aData}` : '',
    };
  },
};

Debugging State Composition

// Debug helper to trace provider execution
async function debugComposeState(runtime: IAgentRuntime, message: Memory, includeList?: string[]) {
  console.log('=== State Composition Debug ===');
  console.log('Message ID:', message.id);
  console.log('Include List:', includeList || 'default');

  // Monkey patch provider execution
  const originalProviders = runtime.providers;
  runtime.providers = runtime.providers.map((provider) => ({
    ...provider,
    get: async (...args) => {
      const start = Date.now();
      console.log(`[${provider.name}] Starting...`);

      try {
        const result = await provider.get(...args);
        const duration = Date.now() - start;
        console.log(`[${provider.name}] Completed in ${duration}ms`);
        console.log(`[${provider.name}] Data size:`, JSON.stringify(result).length);
        return result;
      } catch (error) {
        console.error(`[${provider.name}] Error:`, error);
        throw error;
      }
    },
  }));

  const state = await runtime.composeState(message, includeList);

  // Restore original providers
  runtime.providers = originalProviders;

  console.log('=== Final State Summary ===');
  console.log('Total providers run:', Object.keys(state.data.providers || {}).length);
  console.log('State text length:', state.text.length);
  console.log('===============================');

  return state;
}

Best Practices

1. Provider Selection

  • Use default providers for general conversation
  • Include dynamic providers only when needed
  • Use onlyInclude for performance-critical paths

2. Cache Management

// Good: Use cache for repeated operations
const state1 = await runtime.composeState(message); // First call
const state2 = await runtime.composeState(message); // Uses cache

// Good: Skip cache for real-time data
const freshState = await runtime.composeState(message, null, false, true);

// Bad: Always skipping cache
// This defeats the purpose of caching and hurts performance

3. Error Handling

try {
  const state = await runtime.composeState(message, ['CUSTOM_PROVIDER']);
  // Use state
} catch (error) {
  console.error('State composition failed:', error);
  // Fallback to minimal state
  const minimalState = await runtime.composeState(
    message,
    ['CHARACTER'], // Just character data
    true,
    true
  );
}

4. Memory Optimization

// Clean up old cache entries periodically
setInterval(() => {
  const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
  for (const [messageId, _] of runtime.stateCache.entries()) {
    runtime.getMemoryById(messageId).then((memory) => {
      if (memory && memory.createdAt < fiveMinutesAgo) {
        runtime.stateCache.delete(messageId);
      }
    });
  }
}, 60000); // Run every minute

5. Custom Provider Guidelines

When creating custom providers:
const customProvider: Provider = {
  name: 'CUSTOM_DATA',
  description: 'Provides custom data',
  dynamic: true, // Set to true if not always needed
  position: 150, // Higher numbers run later
  private: false, // Set to true for internal-only providers

  get: async (runtime, message, state) => {
    // Best practices:
    // 1. Return quickly - use timeouts
    // 2. Handle errors gracefully
    // 3. Return empty result on failure
    // 4. Keep data size reasonable

    try {
      // Example: Fetch data with timeout
      const fetchDataWithTimeout = async (timeout: number) => {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);

        try {
          const response = await fetch('https://api.example.com/data', {
            signal: controller.signal,
          });
          clearTimeout(timeoutId);
          return await response.json();
        } catch (error) {
          clearTimeout(timeoutId);
          throw error;
        }
      };

      const data = await fetchDataWithTimeout(5000);
      return {
        values: { customValue: data.summary || 'No summary' },
        data: { fullData: data },
        text: `Custom data: ${data.summary || 'No data available'}`,
      };
    } catch (error) {
      runtime.logger.error('Error in CUSTOM_DATA provider:', error);
      // Return empty result on error
      return {
        values: {},
        data: {},
        text: '',
      };
    }
  },
};

Summary

The composeState method is central to ElizaOS’s context management system. It provides a flexible way to aggregate data from multiple sources, manage caching for performance, and create rich contextual state for agent decision-making. By understanding how to effectively use providers and manage state composition, you can build more intelligent and context-aware agents. Key takeaways:
  • Use default composition for most scenarios
  • Include dynamic providers when specific data is needed
  • Leverage caching for performance
  • Clear cache when data freshness is critical
  • Monitor provider performance in production
  • Handle errors gracefully