feat(hooks): add message_received and message_sent hooks to chat completions endpoint

Trigger plugin hooks for API requests through /v1/chat/completions:

- message_received: fires before agent processing with prompt content,
  user field, model, sessionKey, and source metadata
- message_sent: fires after response generation (non-streaming only)

This enables plugins to react to API traffic, e.g., logging voice
shortcut interactions to Discord for history/context.

The hooks use the existing plugin hook system (not internal hooks),
so any plugin can register handlers via api.on('message_received', ...)

Note: Streaming responses do not yet fire message_sent; this could be
added in a follow-up by accumulating deltas and firing at lifecycle end.
This commit is contained in:
David Marsh 2026-01-30 06:57:23 -08:00
parent da71eaebd2
commit 7f34d730db

View File

@ -5,6 +5,7 @@ import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply
import { createDefaultDeps } from "../cli/deps.js"; import { createDefaultDeps } from "../cli/deps.js";
import { agentCommand } from "../commands/agent.js"; import { agentCommand } from "../commands/agent.js";
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js"; import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js"; import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js";
import { import {
@ -200,6 +201,35 @@ export async function handleOpenAiHttpRequest(
const runId = `chatcmpl_${randomUUID()}`; const runId = `chatcmpl_${randomUUID()}`;
const deps = createDefaultDeps(); const deps = createDefaultDeps();
// Trigger message_received hook for API requests (enables voice logging, etc.)
const hookRunner = getGlobalHookRunner();
if (hookRunner?.hasHooks("message_received")) {
void hookRunner
.runMessageReceived(
{
from: user ?? "api",
content: prompt.message,
timestamp: Date.now(),
metadata: {
apiUser: user,
model,
sessionKey,
source: "chat_completions",
runId,
},
},
{
channelId: "api",
accountId: undefined,
conversationId: sessionKey,
},
)
.catch((err) => {
// Fire-and-forget, just log errors
console.error(`[openai-http] message_received hook failed: ${String(err)}`);
});
}
if (!stream) { if (!stream) {
try { try {
const result = await agentCommand( const result = await agentCommand(
@ -225,6 +255,24 @@ export async function handleOpenAiHttpRequest(
.join("\n\n") .join("\n\n")
: "No response from OpenClaw."; : "No response from OpenClaw.";
// Trigger message_sent hook for API responses (non-streaming only for now)
if (hookRunner?.hasHooks("message_sent")) {
void hookRunner
.runMessageSent(
{
to: user ?? "api",
content,
success: true,
},
{
channelId: "api",
accountId: undefined,
conversationId: sessionKey,
},
)
.catch(() => {});
}
sendJson(res, 200, { sendJson(res, 200, {
id: runId, id: runId,
object: "chat.completion", object: "chat.completion",