From 93757f92e3ab6ef8c46ba1444e66c59a04c25caf Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Mon, 26 Jan 2026 14:20:08 -0800 Subject: [PATCH] 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. --- extensions/discord-status/README.md | 82 +++++++++++++++++++ .../discord-status/clawdbot.plugin.json | 42 ++++++++++ extensions/discord-status/index.ts | 79 ++++++++++++++++++ src/auto-reply/reply/dispatch-from-config.ts | 34 ++++++++ src/plugins/types.ts | 16 ++++ 5 files changed, 253 insertions(+) create mode 100644 extensions/discord-status/README.md create mode 100644 extensions/discord-status/clawdbot.plugin.json create mode 100644 extensions/discord-status/index.ts diff --git a/extensions/discord-status/README.md b/extensions/discord-status/README.md new file mode 100644 index 000000000..fe7577fcc --- /dev/null +++ b/extensions/discord-status/README.md @@ -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("gateway"); +// Add: api.setPresence = (options) => gateway.setPresence(options); +``` + +## License + +MIT - Clawdbot Contributors diff --git a/extensions/discord-status/clawdbot.plugin.json b/extensions/discord-status/clawdbot.plugin.json new file mode 100644 index 000000000..65fe0f7be --- /dev/null +++ b/extensions/discord-status/clawdbot.plugin.json @@ -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" + } + } +} diff --git a/extensions/discord-status/index.ts b/extensions/discord-status/index.ts new file mode 100644 index 000000000..bee0c5aaa --- /dev/null +++ b/extensions/discord-status/index.ts @@ -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(); + + // 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"); +} diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index f946c05f9..acfea5176 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -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 }; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 1ce9731ea..1a4df5a78 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -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