feat(hooks): Wire up message_sent hook with usage statistics
- Extended PluginHookMessageSentEvent to include usage stats (tokens, model, provider, duration) - Extended PluginHookMessageContext to include sessionKey and agentId - Wired up runMessageSent hook call in dispatch-from-config.ts after final replies - Added discord-status example plugin demonstrating how to use the hook The message_sent hook now fires after each reply is delivered, enabling plugins to: - Track token usage across sessions - Update external status indicators (e.g., Discord bot presence) - Log message delivery for analytics/monitoring Note: Full Discord presence support requires exposing setPresence via PluginRuntime. The example plugin currently logs the status it would set.
This commit is contained in:
parent
9c0c5866db
commit
93757f92e3
82
extensions/discord-status/README.md
Normal file
82
extensions/discord-status/README.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Discord Status Plugin
|
||||||
|
|
||||||
|
Updates Discord bot presence/status with session information after each message.
|
||||||
|
|
||||||
|
## Current Status: Partial Implementation
|
||||||
|
|
||||||
|
This plugin demonstrates the `message_sent` hook usage. It currently **logs** what status it would set, rather than actually updating Discord presence.
|
||||||
|
|
||||||
|
### What Works
|
||||||
|
- ✅ Hook triggers after each message is sent
|
||||||
|
- ✅ Session tracking via sessionKey
|
||||||
|
- ✅ Channel filtering (only fires for Discord)
|
||||||
|
- ✅ Custom status format strings
|
||||||
|
|
||||||
|
### What's Pending
|
||||||
|
- ⏳ Usage statistics (tokens, model, etc.) - requires wiring usage data through the reply pipeline
|
||||||
|
- ⏳ Actual Discord presence updates - requires exposing `setPresence` via PluginRuntime
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
entries: {
|
||||||
|
"discord-status": {
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
format: "📊 {tokens} tokens",
|
||||||
|
activityType: "Custom" // Playing, Watching, Listening, Competing, Custom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format Placeholders
|
||||||
|
|
||||||
|
- `{tokens}` - Total tokens used in session
|
||||||
|
- `{input}` - Input tokens (when usage tracking is implemented)
|
||||||
|
- `{output}` - Output tokens (when usage tracking is implemented)
|
||||||
|
- `{model}` - Model name
|
||||||
|
- `{provider}` - Provider name
|
||||||
|
- `{session}` - Session key
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To complete this plugin, the following changes are needed:
|
||||||
|
|
||||||
|
### 1. Usage Statistics (in `src/auto-reply/reply/dispatch-from-config.ts`)
|
||||||
|
|
||||||
|
The `getReplyFromConfig` return type needs to include usage metadata:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type ReplyWithMeta = {
|
||||||
|
payload: ReplyPayload | ReplyPayload[];
|
||||||
|
meta?: {
|
||||||
|
usage?: {
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
model?: string;
|
||||||
|
provider?: string;
|
||||||
|
};
|
||||||
|
durationMs?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Discord Presence API (in Discord channel runtime)
|
||||||
|
|
||||||
|
Expose `setPresence` method via `PluginRuntime.channel.discord`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In src/discord/monitor/provider.ts
|
||||||
|
const gateway = client.getPlugin<GatewayPlugin>("gateway");
|
||||||
|
// Add: api.setPresence = (options) => gateway.setPresence(options);
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT - Clawdbot Contributors
|
||||||
42
extensions/discord-status/clawdbot.plugin.json
Normal file
42
extensions/discord-status/clawdbot.plugin.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"id": "discord-status",
|
||||||
|
"name": "Discord Status",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Updates Discord bot status with session context usage after each message",
|
||||||
|
"author": "Clawdbot Contributors",
|
||||||
|
"homepage": "https://github.com/clawdbot/clawdbot",
|
||||||
|
"entrypoint": "./index.ts",
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable Discord status updates"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Status format string. Use {tokens} for token count.",
|
||||||
|
"default": "📊 {tokens} tokens"
|
||||||
|
},
|
||||||
|
"activityType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["Playing", "Watching", "Listening", "Competing", "Custom"],
|
||||||
|
"description": "Discord activity type",
|
||||||
|
"default": "Custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uiHints": {
|
||||||
|
"enabled": {
|
||||||
|
"label": "Enable Status Updates"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"label": "Status Format",
|
||||||
|
"placeholder": "📊 {tokens} tokens"
|
||||||
|
},
|
||||||
|
"activityType": {
|
||||||
|
"label": "Activity Type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
extensions/discord-status/index.ts
Normal file
79
extensions/discord-status/index.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Discord Status Plugin
|
||||||
|
*
|
||||||
|
* Updates Discord bot presence/status with session information after each message.
|
||||||
|
*
|
||||||
|
* NOTE: This plugin requires Discord presence API support to be added to the
|
||||||
|
* Discord channel runtime. Currently it logs the status that would be set.
|
||||||
|
*
|
||||||
|
* To enable full functionality:
|
||||||
|
* 1. Add `setPresence` method to Discord runtime
|
||||||
|
* 2. Expose it via PluginRuntime.channel.discord.setPresence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
type DiscordStatusConfig = {
|
||||||
|
enabled?: boolean;
|
||||||
|
format?: string;
|
||||||
|
activityType?: "Playing" | "Watching" | "Listening" | "Competing" | "Custom";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function register(api: PluginApi) {
|
||||||
|
const logger = api.logger;
|
||||||
|
const config = (api.config ?? {}) as DiscordStatusConfig;
|
||||||
|
|
||||||
|
if (config.enabled === false) {
|
||||||
|
logger.info("Discord status plugin is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const format = config.format ?? "📊 {tokens} tokens";
|
||||||
|
const activityType = config.activityType ?? "Custom";
|
||||||
|
|
||||||
|
// Track cumulative tokens per session
|
||||||
|
const sessionTokens = new Map<string, number>();
|
||||||
|
|
||||||
|
// Register message_sent hook
|
||||||
|
api.registerHook({
|
||||||
|
hookName: "message_sent",
|
||||||
|
priority: 10,
|
||||||
|
handler: async (event, ctx) => {
|
||||||
|
// Only update for Discord channel
|
||||||
|
if (ctx.channelId !== "discord") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionKey = ctx.sessionKey ?? "unknown";
|
||||||
|
|
||||||
|
// Accumulate token usage (if available in future)
|
||||||
|
const currentTokens = sessionTokens.get(sessionKey) ?? 0;
|
||||||
|
const newTokens = event.usage?.totalTokens ?? 0;
|
||||||
|
const totalTokens = currentTokens + newTokens;
|
||||||
|
sessionTokens.set(sessionKey, totalTokens);
|
||||||
|
|
||||||
|
// Format the status string
|
||||||
|
const statusText = format
|
||||||
|
.replace("{tokens}", totalTokens.toLocaleString())
|
||||||
|
.replace("{input}", (event.usage?.inputTokens ?? 0).toLocaleString())
|
||||||
|
.replace("{output}", (event.usage?.outputTokens ?? 0).toLocaleString())
|
||||||
|
.replace("{model}", event.usage?.model ?? "unknown")
|
||||||
|
.replace("{provider}", event.usage?.provider ?? "unknown")
|
||||||
|
.replace("{session}", sessionKey);
|
||||||
|
|
||||||
|
// Log what we would set (until Discord presence API is exposed)
|
||||||
|
logger.info(`[discord-status] Would set presence: ${activityType} "${statusText}"`);
|
||||||
|
|
||||||
|
// TODO: When Discord runtime exposes setPresence:
|
||||||
|
// await api.runtime.channel.discord.setPresence({
|
||||||
|
// status: "online",
|
||||||
|
// activities: [{
|
||||||
|
// name: statusText,
|
||||||
|
// type: activityTypeToNumber(activityType),
|
||||||
|
// }],
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Discord status plugin loaded - listening for message_sent events");
|
||||||
|
}
|
||||||
@ -331,6 +331,40 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
|
|
||||||
const counts = dispatcher.getQueuedCounts();
|
const counts = dispatcher.getQueuedCounts();
|
||||||
counts.final += routedFinalCount;
|
counts.final += routedFinalCount;
|
||||||
|
|
||||||
|
// Trigger message_sent hook for final replies
|
||||||
|
if (hookRunner?.hasHooks("message_sent") && replies.length > 0) {
|
||||||
|
const targetSessionKey =
|
||||||
|
ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey?.trim() : undefined;
|
||||||
|
const agentSessionKey = (targetSessionKey ?? ctx.SessionKey)?.trim();
|
||||||
|
const agentId = agentSessionKey
|
||||||
|
? resolveSessionAgentId({ sessionKey: agentSessionKey, config: cfg })
|
||||||
|
: undefined;
|
||||||
|
const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase();
|
||||||
|
const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? undefined;
|
||||||
|
|
||||||
|
for (const reply of replies) {
|
||||||
|
void hookRunner
|
||||||
|
.runMessageSent(
|
||||||
|
{
|
||||||
|
to: conversationId ?? "",
|
||||||
|
content: reply.text ?? "",
|
||||||
|
success: queuedFinal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channelId,
|
||||||
|
accountId: ctx.AccountId,
|
||||||
|
conversationId,
|
||||||
|
sessionKey: agentSessionKey,
|
||||||
|
agentId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
logVerbose(`dispatch-from-config: message_sent hook failed: ${String(err)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
recordProcessed("completed");
|
recordProcessed("completed");
|
||||||
markIdle("message_completed");
|
markIdle("message_completed");
|
||||||
return { queuedFinal, counts };
|
return { queuedFinal, counts };
|
||||||
|
|||||||
@ -346,6 +346,8 @@ export type PluginHookMessageContext = {
|
|||||||
channelId: string;
|
channelId: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
|
sessionKey?: string;
|
||||||
|
agentId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// message_received hook
|
// message_received hook
|
||||||
@ -374,6 +376,20 @@ export type PluginHookMessageSentEvent = {
|
|||||||
content: string;
|
content: string;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
/** Usage statistics from the agent run (if available) */
|
||||||
|
usage?: {
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
cacheReadTokens?: number;
|
||||||
|
cacheCreationTokens?: number;
|
||||||
|
/** Model used for this response */
|
||||||
|
model?: string;
|
||||||
|
/** Provider used (e.g., "anthropic", "openai") */
|
||||||
|
provider?: string;
|
||||||
|
};
|
||||||
|
/** Duration of the agent run in milliseconds */
|
||||||
|
durationMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tool context
|
// Tool context
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user