426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
992 lines
25 KiB
Plaintext
992 lines
25 KiB
Plaintext
---
|
|
title: "Patterns"
|
|
description: "Battle-tested patterns for building robust plugins"
|
|
---
|
|
|
|
## Real Patterns from Real Plugins
|
|
|
|
These aren't theoretical - they're extracted from production plugins. Each pattern solves a specific problem you'll encounter when building agents that actually work.
|
|
|
|
<CardGroup cols={2}>
|
|
<Card title="Action Chaining" icon="link">
|
|
Multi-step workflows where actions build on each other
|
|
</Card>
|
|
<Card title="Callbacks" icon="reply">
|
|
Send progress updates before actions complete
|
|
</Card>
|
|
<Card title="State Composition" icon="layer-group">
|
|
Combine multiple providers elegantly
|
|
</Card>
|
|
<Card title="Error Recovery" icon="rotate-right">
|
|
Graceful degradation and retry strategies
|
|
</Card>
|
|
</CardGroup>
|
|
|
|
## Action Chaining and Callbacks
|
|
|
|
Action chaining allows multiple actions to execute sequentially, with each action accessing previous results. This enables complex workflows where actions build upon each other's outputs.
|
|
|
|
### The ActionResult Interface
|
|
|
|
Actions return an `ActionResult` object that standardizes how actions communicate their outcomes. This interface includes:
|
|
|
|
- **success** (required): Boolean indicating whether the action completed successfully
|
|
- **text**: Optional human-readable description of the result
|
|
- **values**: Key-value pairs to merge into the state for subsequent actions
|
|
- **data**: Raw data payload with action-specific results
|
|
- **error**: Error information if the action failed
|
|
|
|
The `success` field is the only required field, making it easy to create simple results while supporting complex data passing for action chaining.
|
|
|
|
For interface definitions, see [Plugin Reference](/plugins/patterns#the-actionresult-interface). For component basics, see [Plugin Components](/plugins/architecture).
|
|
|
|
### Handler Callbacks
|
|
|
|
The `HandlerCallback` provides a mechanism for actions to send immediate feedback to users before the action completes:
|
|
|
|
```typescript
|
|
export type HandlerCallback = (
|
|
response: Content,
|
|
files?: Attachment[],
|
|
) => Promise<Memory[]>;
|
|
```
|
|
|
|
Example usage:
|
|
|
|
```typescript
|
|
async handler(
|
|
runtime: IAgentRuntime,
|
|
message: Memory,
|
|
_state?: State,
|
|
_options?: Record<string, unknown>,
|
|
callback?: HandlerCallback
|
|
): Promise<ActionResult> {
|
|
try {
|
|
// Send immediate feedback
|
|
await callback?.({
|
|
text: `Starting to process your request...`,
|
|
source: message.content.source
|
|
});
|
|
|
|
// Perform action logic
|
|
const result = await performComplexOperation();
|
|
|
|
// Send success message to user via callback
|
|
await callback?.({
|
|
text: `Created issue: ${result.title} (${result.identifier})\n\nView it at: ${result.url}`,
|
|
source: message.content.source
|
|
});
|
|
|
|
// Return structured result for potential chaining
|
|
return {
|
|
success: true,
|
|
text: `Created issue: ${result.title}`,
|
|
data: {
|
|
issueId: result.id,
|
|
identifier: result.identifier,
|
|
url: result.url
|
|
}
|
|
};
|
|
} catch (error) {
|
|
// Send error message to user
|
|
await callback?.({
|
|
text: `Failed to create issue: ${error.message}`,
|
|
source: message.content.source
|
|
});
|
|
|
|
return {
|
|
success: false,
|
|
text: `Failed to create issue: ${error.message}`,
|
|
error: error instanceof Error ? error : new Error(String(error))
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### Action Context and Previous Results
|
|
|
|
When multiple actions are executed in sequence, each action receives an `ActionContext` that provides access to previous action results:
|
|
|
|
```typescript
|
|
export interface ActionContext {
|
|
/** Results from previously executed actions in this run */
|
|
previousResults: ActionResult[];
|
|
|
|
/** Get a specific previous result by action name */
|
|
getPreviousResult?: (actionName: string) => ActionResult | undefined;
|
|
}
|
|
```
|
|
|
|
The runtime automatically provides this context in the `options` parameter:
|
|
|
|
```typescript
|
|
async handler(
|
|
runtime: IAgentRuntime,
|
|
message: Memory,
|
|
state?: State,
|
|
options?: Record<string, unknown>,
|
|
callback?: HandlerCallback
|
|
): Promise<ActionResult> {
|
|
// Access the action context
|
|
const context = options?.context as ActionContext;
|
|
|
|
// Get results from a specific previous action
|
|
const previousResult = context?.getPreviousResult?.('CREATE_LINEAR_ISSUE');
|
|
|
|
if (previousResult?.data?.issueId) {
|
|
// Use data from previous action
|
|
const issueId = previousResult.data.issueId;
|
|
// ... continue with logic using previous result ...
|
|
}
|
|
}
|
|
```
|
|
|
|
### Action Execution Flow
|
|
|
|
The planned-tool executor manages the execution flow:
|
|
|
|
1. **Action Planning**: When multiple actions are detected, the runtime creates an execution plan
|
|
2. **Sequential Execution**: Actions execute in the order specified by the agent
|
|
3. **State Accumulation**: Each action's results are merged into the accumulated state
|
|
4. **Working Memory**: Results are stored in working memory for access during execution
|
|
5. **Error Handling**: Failed actions don't stop the chain unless marked as critical
|
|
|
|
### Working Memory Management
|
|
|
|
The runtime maintains a working memory that stores recent action results:
|
|
|
|
```typescript
|
|
// Results are automatically stored in state.data.workingMemory
|
|
const memoryEntry: WorkingMemoryEntry = {
|
|
actionName: action.name,
|
|
result: actionResult,
|
|
timestamp: Date.now(),
|
|
};
|
|
```
|
|
|
|
The system keeps the most recent 50 entries (configurable) to prevent memory bloat.
|
|
|
|
## Action Patterns
|
|
|
|
### Decision-Making Actions
|
|
|
|
Actions can use the LLM to make intelligent decisions based on context:
|
|
|
|
```typescript
|
|
export const muteRoomAction: Action = {
|
|
name: "MUTE_ROOM",
|
|
similes: ["SHUT_UP", "BE_QUIET", "STOP_TALKING", "SILENCE"],
|
|
description: "Mutes a room if asked to or if the agent is being annoying",
|
|
|
|
validate: async (runtime, message) => {
|
|
// Check if already muted
|
|
const roomState = await runtime.getParticipantUserState(
|
|
message.roomId,
|
|
runtime.agentId,
|
|
);
|
|
return roomState !== "MUTED";
|
|
},
|
|
|
|
handler: async (runtime, message, state) => {
|
|
// Create a decision prompt
|
|
const shouldMuteTemplate = `# Task: Should {{agentName}} mute this room?
|
|
|
|
{{recentMessages}}
|
|
|
|
Should {{agentName}} mute and stop responding unless mentioned?
|
|
|
|
Respond YES if:
|
|
- User asked to stop/be quiet
|
|
- Agent responses are annoying users
|
|
- Conversation is hostile
|
|
|
|
Otherwise NO.`;
|
|
|
|
const prompt = composePromptFromState({
|
|
state,
|
|
template: shouldMuteTemplate,
|
|
});
|
|
const decision = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
prompt,
|
|
runtime,
|
|
});
|
|
|
|
if (decision.toLowerCase().includes("yes")) {
|
|
await runtime.setParticipantUserState(
|
|
message.roomId,
|
|
runtime.agentId,
|
|
"MUTED",
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
text: "Going silent in this room",
|
|
values: { roomMuted: true },
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
text: "Continuing to participate",
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
### Multi-Step Actions
|
|
|
|
Actions that need to perform multiple steps with intermediate feedback:
|
|
|
|
```typescript
|
|
export const deployContractAction: Action = {
|
|
name: "DEPLOY_CONTRACT",
|
|
description: "Deploy a smart contract with multiple steps",
|
|
|
|
handler: async (runtime, message, state, options, callback) => {
|
|
try {
|
|
// Step 1: Compile
|
|
await callback?.({
|
|
text: "📝 Compiling contract...",
|
|
actions: ["DEPLOY_CONTRACT"],
|
|
});
|
|
const compiled = await compileContract(state.contractCode);
|
|
|
|
// Step 2: Estimate gas
|
|
await callback?.({
|
|
text: "⛽ Estimating gas costs...",
|
|
actions: ["DEPLOY_CONTRACT"],
|
|
});
|
|
const gasEstimate = await estimateGas(compiled);
|
|
|
|
// Step 3: Deploy
|
|
await callback?.({
|
|
text: `🚀 Deploying with gas: ${gasEstimate}...`,
|
|
actions: ["DEPLOY_CONTRACT"],
|
|
});
|
|
const deployed = await deploy(compiled, gasEstimate);
|
|
|
|
// Step 4: Verify
|
|
await callback?.({
|
|
text: "✅ Verifying deployment...",
|
|
actions: ["DEPLOY_CONTRACT"],
|
|
});
|
|
await verifyContract(deployed.address);
|
|
|
|
return {
|
|
success: true,
|
|
text: `Contract deployed at ${deployed.address}`,
|
|
values: {
|
|
contractAddress: deployed.address,
|
|
transactionHash: deployed.txHash,
|
|
gasUsed: deployed.gasUsed,
|
|
},
|
|
data: {
|
|
deployment: deployed,
|
|
verification: true,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
text: `Deployment failed: ${error.message}`,
|
|
error,
|
|
};
|
|
}
|
|
},
|
|
};
|
|
```
|
|
|
|
### API Integration Actions
|
|
|
|
Pattern for external API calls with retries and error handling:
|
|
|
|
```typescript
|
|
export const apiAction: Action = {
|
|
name: "API_CALL",
|
|
|
|
handler: async (runtime, message, state, options, callback) => {
|
|
const maxRetries = 3;
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
await callback?.({
|
|
text: `Attempt ${attempt}/${maxRetries}...`,
|
|
});
|
|
|
|
const result = await callExternalAPI({
|
|
endpoint: state.endpoint,
|
|
data: state.data,
|
|
timeout: 5000,
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
text: "API call successful",
|
|
data: result,
|
|
};
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
|
|
if (attempt < maxRetries) {
|
|
await callback?.({
|
|
text: `Attempt ${attempt} failed, retrying...`,
|
|
});
|
|
await new Promise((r) => setTimeout(r, 1000 * attempt)); // Exponential backoff
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
text: `API call failed after ${maxRetries} attempts`,
|
|
error: lastError,
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
### Context-Aware Actions
|
|
|
|
Actions that adapt based on conversation context:
|
|
|
|
```typescript
|
|
export const contextAwareAction: Action = {
|
|
name: "CONTEXT_RESPONSE",
|
|
|
|
handler: async (runtime, message, state, options, callback) => {
|
|
// Analyze conversation sentiment
|
|
const sentiment = await analyzeSentiment(state.recentMessages);
|
|
|
|
// Adjust response based on context
|
|
let responseStrategy: string;
|
|
if (sentiment.score < -0.5) {
|
|
responseStrategy = "empathetic";
|
|
} else if (sentiment.score > 0.5) {
|
|
responseStrategy = "enthusiastic";
|
|
} else {
|
|
responseStrategy = "neutral";
|
|
}
|
|
|
|
// Generate context-appropriate response
|
|
const response = await generateResponse(state, responseStrategy, runtime);
|
|
|
|
return {
|
|
success: true,
|
|
text: response.text,
|
|
values: {
|
|
sentiment: sentiment.score,
|
|
strategy: responseStrategy,
|
|
},
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
## Action Composition
|
|
|
|
### Compose Multiple Actions
|
|
|
|
```typescript
|
|
// Compose multiple actions into higher-level operations
|
|
export const compositeAction: Action = {
|
|
name: "SEND_AND_TRACK",
|
|
description: "Send a message and track its delivery",
|
|
|
|
handler: async (runtime, message, state, options, callback) => {
|
|
// Execute sub-actions
|
|
const sendResult = await sendMessageAction.handler(
|
|
runtime,
|
|
message,
|
|
state,
|
|
options,
|
|
callback,
|
|
);
|
|
|
|
if (!sendResult.success) {
|
|
return sendResult; // Propagate failure
|
|
}
|
|
|
|
// Track the sent message
|
|
const trackingId = generateTrackingId();
|
|
await runtime.createMemory(
|
|
{
|
|
id: trackingId,
|
|
entityId: message.entityId,
|
|
roomId: message.roomId,
|
|
content: {
|
|
type: "message_tracking",
|
|
sentTo: sendResult.data.target,
|
|
sentAt: Date.now(),
|
|
messageContent: sendResult.data.messageContent,
|
|
},
|
|
},
|
|
"tracking",
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
text: `Message sent and tracked (${trackingId})`,
|
|
values: {
|
|
...sendResult.values,
|
|
trackingId,
|
|
tracked: true,
|
|
},
|
|
data: {
|
|
sendResult,
|
|
trackingId,
|
|
},
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
### Workflow Orchestration
|
|
|
|
```typescript
|
|
export const workflowAction: Action = {
|
|
name: "COMPLEX_WORKFLOW",
|
|
|
|
handler: async (runtime, message, state, options, callback) => {
|
|
const workflow = [
|
|
{ action: "VALIDATE_INPUT", required: true },
|
|
{ action: "FETCH_DATA", required: true },
|
|
{ action: "PROCESS_DATA", required: false },
|
|
{ action: "STORE_RESULTS", required: true },
|
|
{ action: "NOTIFY_USER", required: false },
|
|
];
|
|
|
|
const results: ActionResult[] = [];
|
|
|
|
for (const step of workflow) {
|
|
const action = runtime.getAction(step.action);
|
|
if (!action) {
|
|
if (step.required) {
|
|
return {
|
|
success: false,
|
|
text: `Required action ${step.action} not found`,
|
|
};
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const result = await action.handler(
|
|
runtime,
|
|
message,
|
|
state,
|
|
{ context: { previousResults: results } },
|
|
callback,
|
|
);
|
|
|
|
results.push(result);
|
|
|
|
if (!result.success && step.required) {
|
|
return {
|
|
success: false,
|
|
text: `Workflow failed at ${step.action}`,
|
|
data: { failedStep: step.action, results },
|
|
};
|
|
}
|
|
|
|
// Merge values into state for next action
|
|
state = {
|
|
...state,
|
|
values: {
|
|
...state.values,
|
|
...result.values,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
text: "Workflow completed successfully",
|
|
data: { workflowResults: results },
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
## Self-Modifying Actions
|
|
|
|
Actions that learn and adapt their behavior:
|
|
|
|
```typescript
|
|
export const learningAction: Action = {
|
|
name: "ADAPTIVE_RESPONSE",
|
|
|
|
handler: async (runtime, message, state) => {
|
|
// Retrieve past performance
|
|
const history = await runtime.getMemories({
|
|
tableName: "action_feedback",
|
|
roomId: message.roomId,
|
|
count: 100,
|
|
});
|
|
|
|
// Analyze what worked well
|
|
const analysis = await runtime.useModel(ModelType.TEXT_LARGE, {
|
|
prompt: `Analyze these past interactions and identify patterns:
|
|
${JSON.stringify(history)}
|
|
What response strategies were most effective?`,
|
|
});
|
|
|
|
// Adapt behavior based on learning
|
|
const strategy = determineStrategy(analysis);
|
|
const response = await generateResponse(state, strategy);
|
|
|
|
// Store for future learning
|
|
await runtime.createMemory(
|
|
{
|
|
id: generateId(),
|
|
content: {
|
|
type: "action_feedback",
|
|
strategy: strategy.name,
|
|
context: state.text,
|
|
response: response.text,
|
|
},
|
|
},
|
|
"action_feedback",
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
text: response.text,
|
|
values: {
|
|
strategyUsed: strategy.name,
|
|
confidence: strategy.confidence,
|
|
},
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
## Provider Patterns
|
|
|
|
### Conditional Providers
|
|
|
|
Providers that only provide data under certain conditions:
|
|
|
|
```typescript
|
|
export const conditionalProvider: Provider = {
|
|
name: "PREMIUM_DATA",
|
|
private: true,
|
|
|
|
get: async (runtime, message, state) => {
|
|
// Check if user has premium access
|
|
const user = await runtime.getUser(message.entityId);
|
|
|
|
if (!user.isPremium) {
|
|
return {
|
|
text: "",
|
|
values: {},
|
|
data: { available: false },
|
|
};
|
|
}
|
|
|
|
// Provide premium data
|
|
const premiumData = await fetchPremiumData(user);
|
|
|
|
return {
|
|
text: formatPremiumData(premiumData),
|
|
values: premiumData,
|
|
data: { available: true, ...premiumData },
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
### Aggregating Providers
|
|
|
|
Providers that combine data from multiple sources:
|
|
|
|
```typescript
|
|
export const aggregateProvider: Provider = {
|
|
name: "MARKET_OVERVIEW",
|
|
position: 50, // Run after individual data providers
|
|
|
|
get: async (runtime, message, state) => {
|
|
// Aggregate from multiple sources
|
|
const [stocks, crypto, forex] = await Promise.all([
|
|
fetchStockData(),
|
|
fetchCryptoData(),
|
|
fetchForexData(),
|
|
]);
|
|
|
|
const overview = {
|
|
stocksUp: stocks.filter((s) => s.change > 0).length,
|
|
stocksDown: stocks.filter((s) => s.change < 0).length,
|
|
cryptoMarketCap: crypto.reduce((sum, c) => sum + c.marketCap, 0),
|
|
forexVolatility: calculateVolatility(forex),
|
|
};
|
|
|
|
return {
|
|
text: `Market Overview:
|
|
- Stocks: ${overview.stocksUp} up, ${overview.stocksDown} down
|
|
- Crypto Market Cap: $${overview.cryptoMarketCap.toLocaleString()}
|
|
- Forex Volatility: ${overview.forexVolatility}`,
|
|
values: overview,
|
|
data: { stocks, crypto, forex },
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
## Best Practices for Action Chaining
|
|
|
|
1. **Always Return ActionResult**: Even for simple actions, return a proper `ActionResult` object:
|
|
|
|
```typescript
|
|
return {
|
|
success: true,
|
|
text: "Action completed",
|
|
data: {
|
|
/* any data for next actions */
|
|
},
|
|
};
|
|
```
|
|
|
|
2. **Use Callbacks for User Feedback**: Send immediate feedback via callbacks rather than waiting for the action to complete:
|
|
|
|
```typescript
|
|
await callback?.({
|
|
text: "Processing your request...",
|
|
source: message.content.source,
|
|
});
|
|
```
|
|
|
|
3. **Store Identifiers in Data**: When creating resources, store identifiers that subsequent actions might need:
|
|
|
|
```typescript
|
|
return {
|
|
success: true,
|
|
data: {
|
|
resourceId: created.id,
|
|
resourceUrl: created.url,
|
|
},
|
|
};
|
|
```
|
|
|
|
4. **Handle Missing Dependencies**: Check if required previous results exist:
|
|
|
|
```typescript
|
|
const previousResult = context?.getPreviousResult?.("REQUIRED_ACTION");
|
|
if (!previousResult?.success) {
|
|
return {
|
|
success: false,
|
|
text: "Required previous action did not complete successfully",
|
|
};
|
|
}
|
|
```
|
|
|
|
5. **Use ActionResult**: All actions must return `ActionResult` with a `success` field.
|
|
|
|
## Example: Multi-Step Workflow
|
|
|
|
Here's an example of a multi-step workflow using action chaining:
|
|
|
|
```typescript
|
|
// User: "Create a bug report for the login issue and assign it to John"
|
|
// Agent executes: REPLY, CREATE_LINEAR_ISSUE, UPDATE_LINEAR_ISSUE
|
|
|
|
// Action 1: CREATE_LINEAR_ISSUE
|
|
{
|
|
success: true,
|
|
data: {
|
|
issueId: "abc-123",
|
|
identifier: "BUG-456"
|
|
}
|
|
}
|
|
|
|
// Action 2: UPDATE_LINEAR_ISSUE (can access previous result)
|
|
async handler(runtime, message, state, options, callback) {
|
|
const context = options?.context as ActionContext;
|
|
const createResult = context?.getPreviousResult?.('CREATE_LINEAR_ISSUE');
|
|
|
|
if (createResult?.data?.issueId) {
|
|
// Use the issue ID from previous action
|
|
await updateIssue(createResult.data.issueId, { assignee: "John" });
|
|
|
|
return {
|
|
success: true,
|
|
text: "Issue assigned to John"
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
1. **Create and Configure**: Create a resource, then configure it
|
|
2. **Search and Update**: Find resources, then modify them
|
|
3. **Validate and Execute**: Check conditions, then perform actions
|
|
4. **Aggregate and Report**: Collect data from multiple sources, then summarize
|
|
|
|
The action chaining system provides a powerful way to build complex, multi-step workflows while maintaining clean separation between individual actions.
|
|
|
|
## Real-World Implementation Patterns
|
|
|
|
This section documents actual patterns and structures used in production elizaOS plugins based on examination of real implementations.
|
|
|
|
### Basic Plugin Structure Pattern
|
|
|
|
Every plugin follows this core structure pattern (from `plugin-starter`):
|
|
|
|
```typescript
|
|
import type { Plugin } from "@elizaos/core";
|
|
|
|
export const myPlugin: Plugin = {
|
|
name: "plugin-name",
|
|
description: "Plugin description",
|
|
|
|
// Core components
|
|
actions: [], // Actions the plugin provides
|
|
providers: [], // Data providers
|
|
services: [], // Background services
|
|
evaluators: [], // Response evaluators
|
|
|
|
// Optional components
|
|
init: async (config) => {}, // Initialization logic
|
|
models: {}, // Custom model implementations
|
|
routes: [], // HTTP routes
|
|
events: {}, // Event handlers
|
|
tests: [], // Test suites
|
|
dependencies: [], // Other required plugins
|
|
};
|
|
```
|
|
|
|
### BasicCapabilities Plugin Pattern
|
|
|
|
The basic-capabilities plugin is built into `@elizaos/core` and automatically registered during runtime initialization. It provides core functionality through:
|
|
|
|
```typescript
|
|
// The basic-capabilities plugin is created internally using createBasicCapabilitiesPlugin()
|
|
// It provides:
|
|
const basicCapabilities = {
|
|
actions: [
|
|
actions.replyAction,
|
|
actions.followRoomAction,
|
|
actions.ignoreAction,
|
|
actions.sendMessageAction,
|
|
actions.generateImageAction,
|
|
// ... more actions
|
|
],
|
|
providers: [
|
|
providers.timeProvider,
|
|
providers.entitiesProvider,
|
|
providers.characterProvider,
|
|
providers.recentMessagesProvider,
|
|
// ... more providers
|
|
],
|
|
services: [TaskService],
|
|
evaluators: [
|
|
evaluators.factMemoryEvaluator,
|
|
evaluators.relationshipEvaluator,
|
|
evaluators.identityEvaluator,
|
|
evaluators.successEvaluator,
|
|
],
|
|
events: {
|
|
[EventType.MESSAGE_RECEIVED]: [messageReceivedHandler],
|
|
[EventType.POST_GENERATED]: [postGeneratedHandler],
|
|
// ... more event handlers
|
|
},
|
|
};
|
|
```
|
|
|
|
<Note>
|
|
You don't need to import or configure the basic-capabilities plugin - it's automatically included when you create an AgentRuntime.
|
|
</Note>
|
|
|
|
### Service Plugin Pattern (Discord, Telegram)
|
|
|
|
Platform integration plugins focus on service implementation:
|
|
|
|
```typescript
|
|
// Discord Plugin
|
|
const discordPlugin: Plugin = {
|
|
name: "discord",
|
|
description: "Discord service plugin for integration with Discord servers",
|
|
services: [DiscordService],
|
|
actions: [
|
|
chatWithAttachments,
|
|
downloadMedia,
|
|
joinVoice,
|
|
leaveVoice,
|
|
summarize,
|
|
transcribeMedia,
|
|
],
|
|
providers: [channelStateProvider, voiceStateProvider],
|
|
tests: [new DiscordTestSuite()],
|
|
init: async (config, runtime) => {
|
|
// Check for required API tokens
|
|
const token = runtime.getSetting("DISCORD_API_TOKEN");
|
|
if (!token) {
|
|
logger.warn("Discord API Token not provided");
|
|
}
|
|
},
|
|
};
|
|
|
|
// Telegram Plugin (minimal)
|
|
const telegramPlugin: Plugin = {
|
|
name: TELEGRAM_SERVICE_NAME,
|
|
description: "Telegram client plugin",
|
|
services: [TelegramService],
|
|
tests: [new TelegramTestSuite()],
|
|
};
|
|
```
|
|
|
|
### Action Implementation Pattern
|
|
|
|
Actions follow a consistent structure with validation and execution:
|
|
|
|
```typescript
|
|
const helloWorldAction: Action = {
|
|
name: "HELLO_WORLD",
|
|
similes: ["GREET", "SAY_HELLO"], // Alternative names
|
|
description: "Responds with a simple hello world message",
|
|
|
|
validate: async (runtime, message, state) => {
|
|
// Return true if action can be executed
|
|
return true;
|
|
},
|
|
|
|
handler: async (runtime, message, state, options, callback, responses) => {
|
|
try {
|
|
const responseContent: Content = {
|
|
text: "hello world!",
|
|
actions: ["HELLO_WORLD"],
|
|
source: message.content.source,
|
|
};
|
|
|
|
if (callback) {
|
|
await callback(responseContent);
|
|
}
|
|
|
|
return responseContent;
|
|
} catch (error) {
|
|
logger.error("Error in HELLO_WORLD action:", error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
examples: [
|
|
[
|
|
{
|
|
name: "{{name1}}",
|
|
content: { text: "Can you say hello?" },
|
|
},
|
|
{
|
|
name: "{{name2}}",
|
|
content: {
|
|
text: "hello world!",
|
|
actions: ["HELLO_WORLD"],
|
|
},
|
|
},
|
|
],
|
|
],
|
|
};
|
|
```
|
|
|
|
### Complex Action Pattern (Reply Action)
|
|
|
|
```typescript
|
|
export const replyAction = {
|
|
name: "REPLY",
|
|
similes: ["GREET", "REPLY_TO_MESSAGE", "SEND_REPLY", "RESPOND"],
|
|
description: "Replies to the current conversation",
|
|
|
|
validate: async (runtime) => true,
|
|
|
|
handler: async (runtime, message, state, options, callback, responses) => {
|
|
// Compose state with providers
|
|
state = await runtime.composeState(message, ["RECENT_MESSAGES"]);
|
|
|
|
// Generate response using LLM
|
|
const prompt = composePromptFromState({ state, template: replyTemplate });
|
|
const response = await runtime.useModel(ModelType.OBJECT_LARGE, { prompt });
|
|
|
|
const responseContent = {
|
|
thought: response.thought,
|
|
text: response.message || "",
|
|
actions: ["REPLY"],
|
|
};
|
|
|
|
await callback(responseContent);
|
|
return true;
|
|
},
|
|
};
|
|
```
|
|
|
|
### Provider Implementation Pattern
|
|
|
|
Providers supply contextual data to the agent:
|
|
|
|
```typescript
|
|
export const timeProvider: Provider = {
|
|
name: "TIME",
|
|
get: async (runtime, message) => {
|
|
const currentDate = new Date();
|
|
const options = {
|
|
timeZone: "UTC",
|
|
dateStyle: "full" as const,
|
|
timeStyle: "long" as const,
|
|
};
|
|
const humanReadable = new Intl.DateTimeFormat("en-US", options).format(
|
|
currentDate,
|
|
);
|
|
|
|
return {
|
|
data: { time: currentDate },
|
|
values: { time: humanReadable },
|
|
text: `The current date and time is ${humanReadable}.`,
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
### Plugin Initialization Pattern
|
|
|
|
Plugins can have initialization logic:
|
|
|
|
```typescript
|
|
const myPlugin: Plugin = {
|
|
name: "my-plugin",
|
|
config: {
|
|
EXAMPLE_VARIABLE: process.env.EXAMPLE_VARIABLE,
|
|
},
|
|
async init(config: Record<string, string>) {
|
|
// Validate configuration
|
|
const validatedConfig = await configSchema.parseAsync(config);
|
|
|
|
// Set environment variables
|
|
for (const [key, value] of Object.entries(validatedConfig)) {
|
|
if (value) process.env[key] = value;
|
|
}
|
|
},
|
|
};
|
|
```
|
|
|
|
<Tip>
|
|
**Guides**: [Create a Plugin](/plugins/create-a-plugin) | [Publish a
|
|
Plugin](/plugins/publish)
|
|
</Tip>
|
|
|
|
## See Also
|
|
|
|
<CardGroup cols={2}>
|
|
<Card title="Plugin Architecture" icon="sitemap" href="/plugins/architecture">
|
|
Understand the overall plugin system design
|
|
</Card>
|
|
|
|
<Card title="Development Guide" icon="code" href="/plugins/development">
|
|
Build your first plugin with step-by-step guidance
|
|
</Card>
|
|
|
|
<Card title="Plugin Schemas" icon="list" href="/plugins/schemas">
|
|
Learn about configuration and validation schemas
|
|
</Card>
|
|
|
|
<Card title="Plugin Migration" icon="arrow-right" href="/plugins/plugin-eject">
|
|
Migrate existing plugins to new patterns
|
|
</Card>
|
|
</CardGroup>
|