feat(hooks): add message:transcribed and message:preprocessed internal hooks

Adds two new internal hook events that fire after media/link processing:

- message:transcribed: fires when audio has been transcribed, providing
  the transcript text alongside the original body and media metadata.
  Useful for logging, analytics, or routing based on spoken content.

- message:preprocessed: fires for every message after all media + link
  understanding completes. Gives hooks access to the fully enriched body
  (transcripts, image descriptions, link summaries) before the agent sees it.

Both hooks are added in get-reply.ts, after applyMediaUnderstanding and
applyLinkUnderstanding. message:received and message:sent are already
in upstream (f07bb8e8) and are not duplicated here.

Typed contexts (MessageTranscribedHookContext, MessagePreprocessedHookContext)
and type guards (isMessageTranscribedEvent, isMessagePreprocessedEvent) added
to internal-hooks.ts alongside the existing received/sent types.

Test coverage in src/hooks/message-hooks.test.ts.
This commit is contained in:
Eric Lytle
2026-02-20 09:05:57 +00:00
committed by Peter Steinberger
parent 44183c6eb1
commit e0b8b80067
5 changed files with 564 additions and 2 deletions

View File

@@ -93,6 +93,92 @@ export type MessageSentHookEvent = InternalHookEvent & {
context: MessageSentHookContext;
};
export type MessageTranscribedHookContext = {
/** Sender identifier (e.g., phone number, user ID) */
from?: string;
/** Recipient identifier */
to?: string;
/** Original raw message body (e.g., "🎤 [Audio]") */
body?: string;
/** Enriched body shown to the agent, including transcript */
bodyForAgent?: string;
/** The transcribed text from audio */
transcript: string;
/** Unix timestamp when the message was received */
timestamp?: number;
/** Channel identifier (e.g., "telegram", "whatsapp") */
channelId: string;
/** Conversation/chat ID */
conversationId?: string;
/** Message ID from the provider */
messageId?: string;
/** Sender user ID */
senderId?: string;
/** Sender display name */
senderName?: string;
/** Sender username */
senderUsername?: string;
/** Provider name */
provider?: string;
/** Surface name */
surface?: string;
/** Path to the media file that was transcribed */
mediaPath?: string;
/** MIME type of the media */
mediaType?: string;
};
export type MessageTranscribedHookEvent = InternalHookEvent & {
type: "message";
action: "transcribed";
context: MessageTranscribedHookContext;
};
export type MessagePreprocessedHookContext = {
/** Sender identifier (e.g., phone number, user ID) */
from?: string;
/** Recipient identifier */
to?: string;
/** Original raw message body */
body?: string;
/** Fully enriched body shown to the agent (transcripts, image descriptions, link summaries) */
bodyForAgent?: string;
/** Transcribed audio text, if the message contained audio */
transcript?: string;
/** Unix timestamp when the message was received */
timestamp?: number;
/** Channel identifier (e.g., "telegram", "whatsapp") */
channelId: string;
/** Conversation/chat ID */
conversationId?: string;
/** Message ID from the provider */
messageId?: string;
/** Sender user ID */
senderId?: string;
/** Sender display name */
senderName?: string;
/** Sender username */
senderUsername?: string;
/** Provider name */
provider?: string;
/** Surface name */
surface?: string;
/** Path to the media file, if present */
mediaPath?: string;
/** MIME type of the media, if present */
mediaType?: string;
/** Whether this message was sent in a group/channel context */
isGroup?: boolean;
/** Group or channel identifier, if applicable */
groupId?: string;
};
export type MessagePreprocessedHookEvent = InternalHookEvent & {
type: "message";
action: "preprocessed";
context: MessagePreprocessedHookContext;
};
export interface InternalHookEvent {
/** The type of event (command, session, agent, gateway, etc.) */
type: InternalHookEventType;
@@ -282,3 +368,29 @@ export function isMessageSentEvent(event: InternalHookEvent): event is MessageSe
typeof context.success === "boolean"
);
}
export function isMessageTranscribedEvent(
event: InternalHookEvent,
): event is MessageTranscribedHookEvent {
if (event.type !== "message" || event.action !== "transcribed") {
return false;
}
const context = event.context as Partial<MessageTranscribedHookContext> | null;
if (!context || typeof context !== "object") {
return false;
}
return typeof context.transcript === "string" && typeof context.channelId === "string";
}
export function isMessagePreprocessedEvent(
event: InternalHookEvent,
): event is MessagePreprocessedHookEvent {
if (event.type !== "message" || event.action !== "preprocessed") {
return false;
}
const context = event.context as Partial<MessagePreprocessedHookContext> | null;
if (!context || typeof context !== "object") {
return false;
}
return typeof context.channelId === "string";
}