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