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();
|
||||
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");
|
||||
markIdle("message_completed");
|
||||
return { queuedFinal, counts };
|
||||
|
||||
@ -346,6 +346,8 @@ export type PluginHookMessageContext = {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
conversationId?: string;
|
||||
sessionKey?: string;
|
||||
agentId?: string;
|
||||
};
|
||||
|
||||
// message_received hook
|
||||
@ -374,6 +376,20 @@ export type PluginHookMessageSentEvent = {
|
||||
content: string;
|
||||
success: boolean;
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user