| title | description |
|---|---|
Providers |
Provider system for context aggregation and state composition |
Providers supply contextual information that forms the agent's understanding of the current situation. They are the "senses" of the agent, gathering data from various sources to build comprehensive state.
interface Provider {
name: string;
description: string;
dynamic?: boolean; // Only executed when explicitly requested
private?: boolean; // Internal-only, not included in default state
position?: number; // Execution order (lower runs first)
get: (
runtime: IAgentRuntime,
message: Memory,
state?: State
) => Promise<ProviderResult>;
}
interface ProviderResult {
values: Record<string, any>; // Key-value pairs for templates
data: Record<string, any>; // Structured data
text: string; // Textual context
}- Standard Providers: Included by default in state composition
- Dynamic Providers: Only executed when explicitly requested
- Private Providers: Internal use only, not exposed in default state
| Provider Name | Dynamic | Position | Default Included | Purpose |
|---|---|---|---|---|
| ACTIONS | No | -1 | Yes | Lists available actions |
| ACTION_STATE | No | 150 | Yes | Action execution state |
| ANXIETY | No | Default | Yes | Response style guidelines |
| ATTACHMENTS | Yes | Default | No | File/media attachments |
| CAPABILITIES | No | Default | Yes | Service capabilities |
| CHARACTER | No | Default | Yes | Agent personality |
| CHOICE | No | Default | Yes | Pending user choices |
| ENTITIES | Yes | Default | No | Conversation participants |
| EVALUATORS | No | Default | No (private) | Post-processing options |
| FACTS | Yes | Default | No | Stored knowledge |
| PROVIDERS | No | Default | Yes | Available providers list |
| RECENT_MESSAGES | No | 100 | Yes | Conversation history |
| RELATIONSHIPS | Yes | Default | No | Social connections |
| ROLES | No | Default | Yes | Server roles (groups only) |
| SETTINGS | No | Default | Yes | Configuration state |
| TIME | No | Default | Yes | Current UTC time |
| WORLD | Yes | Default | No | Server/world context |
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 namesactionsWithDescriptions: Formatted action detailsactionExamples: Example usage for each actionactionsData: Raw action objects
{
values: {
actionNames: "Possible response actions: 'SEND_MESSAGE', 'SEARCH', 'CALCULATE'",
actionExamples: "..."
},
data: { actionsData: [...] },
text: "# Available Actions\n..."
}Shares execution state between chained actions.
- Position: 150 (runs later)
- Dynamic: No (included by default)
- Data Provided:
actionResults: Previous action execution resultsactionPlan: Multi-step action execution planworkingMemory: Temporary data shared between actionsrecentActionMemories: Historical action executions
Core personality and behavior definition.
- Dynamic: No (included by default)
- Data Provided:
agentName: Character namebio: Character backgroundtopics: Current interestsadjective: Current mood/statedirections: Style guidelinesexamples: Example conversations/posts
{
values: {
agentName: "Alice",
bio: "AI assistant focused on...",
topics: "technology, science, education",
adjective: "helpful"
},
data: { character: {...} },
text: "# About Alice\n..."
}Provides conversation history and context.
- Position: 100 (runs later to access other data)
- Dynamic: No (included by default)
- Data Provided:
recentMessages: Formatted conversation historyrecentInteractions: Previous interactionsactionResults: Results from recent actions
{
values: {
recentMessages: "User: Hello\nAlice: Hi there!",
recentInteractions: "..."
},
data: {
recentMessages: [...],
actionResults: [...]
},
text: "# Conversation Messages\n..."
}Retrieves contextually relevant stored facts.
- Dynamic: Yes (must be explicitly included)
- Behavior: Uses embedding search to find relevant facts
- Data Provided:
- Relevant facts based on context
- Fact metadata and sources
Social graph and interaction history.
- Dynamic: Yes (must be explicitly included)
- Data Provided:
- Known entities and their relationships
- Interaction frequency
- Relationship metadata
The composeState method aggregates data from multiple providers to create comprehensive state.
async composeState(
message: Memory,
includeList: string[] | null = null,
onlyInclude = false,
skipCache = false
): Promise<State>- 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
- Provider Selection: Determines which providers to run based on filters
- Parallel Execution: Runs all selected providers concurrently
- Result Aggregation: Combines results from all providers
- Caching: Stores the composed state for reuse
// 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);runtime.registerProvider(provider);Providers are registered during plugin initialization:
const myPlugin: Plugin = {
name: 'my-plugin',
providers: [customProvider],
init: async (config, runtime) => {
// Providers auto-registered
}
};Position determines execution order:
const earlyProvider: Provider = {
name: 'EARLY',
position: -100, // Runs very early
get: async () => {...}
};
const lateProvider: Provider = {
name: 'LATE',
position: 200, // Runs late
get: async () => {...}
};const customDataProvider: Provider = {
name: 'CUSTOM_DATA',
description: 'Custom data from external source',
dynamic: true,
position: 150,
get: async (runtime, message, state) => {
try {
// Fetch data from 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: '' };
}
},
};- Return quickly: Use timeouts for external calls
- Handle errors gracefully: Return empty result on failure
- Keep data size reasonable: Don't return excessive data
- Use appropriate flags: Set
dynamicfor optional providers - Consider position: Order matters for dependent providers
Providers can access data from previously executed providers through the state parameter:
const dependentProvider: Provider = {
name: 'DEPENDENT',
position: 200, // Runs after other providers
get: async (runtime, message, state) => {
// Access data from earlier providers
const characterData = state?.data?.providers?.CHARACTER?.data;
if (!characterData) {
return { values: {}, data: {}, text: '' };
}
// Process based on character data
const processed = processCharacterData(characterData);
return {
values: { processed: processed.summary },
data: { processed },
text: `Processed: ${processed.summary}`
};
}
};The runtime maintains an in-memory cache of composed states:
// Cache is stored by message ID
this.stateCache.set(message.id, newState);// Use cached data (default behavior)
const cachedState = await runtime.composeState(message);
// Force fresh data
const freshState = await runtime.composeState(message, null, false, true);// Clear 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 minuteflowchart TD
Start[composeState called] --> Select[Select Providers]
Select --> Check{Check Cache}
Check -->|Cache Hit| Return[Return Cached State]
Check -->|Cache Miss| Sort[Sort by Position]
Sort --> Execute[Execute in Parallel]
Execute --> Aggregate[Aggregate Results]
Aggregate --> Cache[Store in Cache]
Cache --> Return
classDef process fill:#2196f3,color:#fff
classDef decision fill:#ff9800,color:#fff
classDef execution fill:#4caf50,color:#fff
classDef result fill:#9c27b0,color:#fff
class Start,Select,Sort process
class Check decision
class Execute,Aggregate execution
class Return,Cache result
Providers run concurrently for optimal performance:
const results = await Promise.all(
providers.map(provider =>
provider.get(runtime, message, partialState)
)
);Implement timeouts to prevent slow providers from blocking:
const timeoutProvider: Provider = {
name: 'TIMEOUT_SAFE',
get: async (runtime, message) => {
const fetchData = async () => {
// Potentially slow operation
const data = await externalAPI.fetch();
return formatProviderResult(data);
};
return Promise.race([
fetchData(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
)
]).catch(error => {
runtime.logger.warn(`Provider timeout: ${error.message}`);
return { values: {}, data: {}, text: '' };
});
}
};Avoid providers that depend on each other circularly:
// BAD: Circular dependency
const providerA: Provider = {
get: async (runtime, message) => {
const state = await runtime.composeState(message, ['B']);
// Uses B's data
}
};
const providerB: Provider = {
get: async (runtime, message) => {
const state = await runtime.composeState(message, ['A']);
// Uses A's data - CIRCULAR!
}
};
// GOOD: Use position and state parameter
const providerA: Provider = {
position: 100,
get: async (runtime, message) => {
// Generate data independently
return { data: { aData: 'value' } };
}
};
const providerB: Provider = {
position: 200,
get: async (runtime, message, state) => {
// Access A's data from state
const aData = state?.data?.providers?.A?.data;
return { data: { bData: processData(aData) } };
}
};Prevent memory leaks with proper cache management:
class BoundedCache extends Map {
private maxSize: number;
constructor(maxSize: number = 1000) {
super();
this.maxSize = maxSize;
}
set(key: string, value: any) {
if (this.size >= this.maxSize) {
const firstKey = this.keys().next().value;
this.delete(firstKey);
}
return super.set(key, value);
}
}// 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;
}