Download src/hooks/internal-hooks.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 12.4 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/hooks/internal-hooks.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/hooks/internal-hooks.ts
-
curl -L -o internal-hooks.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/hooks/internal-hooks.ts
12.4 kB
| /** | |
| * Hook system for OpenClaw agent events | |
| * | |
| * Provides an extensible event-driven hook system for agent events | |
| * like command processing, session lifecycle, etc. | |
| */ | |
| import type { SessionsPatchParams } from "../../packages/gateway-protocol/src/schema/sessions.js"; | |
| import type { WorkspaceBootstrapFile } from "../agents/workspace.js"; | |
| import type { CliDeps } from "../cli/outbound-send-deps.js"; | |
| import type { SessionEntry } from "../config/sessions/types.js"; | |
| import type { OpenClawConfig } from "../config/types.openclaw.js"; | |
| import { formatErrorMessage } from "../infra/errors.js"; | |
| import { createSubsystemLogger } from "../logging/subsystem.js"; | |
| import { | |
| clearLegacyPluginInternalHooks, | |
| listLegacyPluginInternalHookEventKeys, | |
| listLegacyPluginInternalHooks, | |
| } from "../plugins/legacy-internal-hook-state.js"; | |
| import { resolveGlobalSingleton } from "../shared/global-singleton.js"; | |
| import type { | |
| InternalHookEvent, | |
| InternalHookEventType, | |
| InternalHookHandler, | |
| } from "./internal-hook-types.js"; | |
| import type { MessageHookMediaFact } from "./message-hook-media.js"; | |
| export type { InternalHookEvent, InternalHookEventType, InternalHookHandler }; | |
| export type AgentBootstrapHookContext = { | |
| workspaceDir: string; | |
| bootstrapFiles: WorkspaceBootstrapFile[]; | |
| cfg?: OpenClawConfig; | |
| sessionKey?: string; | |
| sessionId?: string; | |
| agentId?: string; | |
| }; | |
| export type AgentBootstrapHookEvent = InternalHookEvent & { | |
| type: "agent"; | |
| action: "bootstrap"; | |
| context: AgentBootstrapHookContext; | |
| }; | |
| export type GatewayStartupHookContext = { | |
| cfg?: OpenClawConfig; | |
| deps?: CliDeps; | |
| workspaceDir?: string; | |
| }; | |
| export type GatewayStartupHookEvent = InternalHookEvent & { | |
| type: "gateway"; | |
| action: "startup"; | |
| context: GatewayStartupHookContext; | |
| }; | |
| // ============================================================================ | |
| // Message Hook Events | |
| // ============================================================================ | |
| export type MessageReceivedHookContext = { | |
| /** Sender identifier (e.g., phone number, user ID) */ | |
| from: string; | |
| /** Message content */ | |
| content: string; | |
| /** Unix timestamp when the message was received */ | |
| timestamp?: number; | |
| /** Channel identifier (for example "chat" or "support-chat") */ | |
| channelId: string; | |
| /** Provider account ID for multi-account setups */ | |
| accountId?: string; | |
| /** Conversation/chat ID */ | |
| conversationId?: string; | |
| /** Message ID from the provider */ | |
| messageId?: string; | |
| /** Staged, locally usable attachments in stable source order. */ | |
| media?: MessageHookMediaFact[]; | |
| /** Original attachment facts when local staging has not completed yet. */ | |
| originalMedia?: MessageHookMediaFact[]; | |
| /** True when originalMedia is present but media is withheld pending staging. */ | |
| mediaStagingPending?: boolean; | |
| /** Additional provider-specific metadata */ | |
| metadata?: Record<string, unknown>; | |
| }; | |
| export type MessageSentHookContext = { | |
| /** Recipient identifier */ | |
| to: string; | |
| /** Message content */ | |
| content: string; | |
| /** Whether the message was sent successfully */ | |
| success: boolean; | |
| /** Error message if sending failed */ | |
| error?: string; | |
| /** Channel identifier (for example "chat" or "support-chat") */ | |
| channelId: string; | |
| /** Provider account ID for multi-account setups */ | |
| accountId?: string; | |
| /** Conversation/chat ID */ | |
| conversationId?: string; | |
| /** Message ID returned by the provider */ | |
| messageId?: string; | |
| /** Whether this message was sent in a group/channel context */ | |
| isGroup?: boolean; | |
| /** Group or channel identifier, if applicable */ | |
| groupId?: string; | |
| }; | |
| type MessageEnrichedBodyHookContext = { | |
| /** 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; | |
| /** Unix timestamp when the message was received */ | |
| timestamp?: number; | |
| /** Channel identifier (for example "chat" or "support-chat") */ | |
| 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; | |
| /** Ordered media facts available to preprocessing/transcription hooks. */ | |
| media?: MessageHookMediaFact[]; | |
| /** Original facts when local staging has not completed yet. */ | |
| originalMedia?: MessageHookMediaFact[]; | |
| /** True when originalMedia is present but media is withheld pending staging. */ | |
| mediaStagingPending?: boolean; | |
| /** @deprecated Use `media?.[0]?.path`. */ | |
| mediaPath?: string; | |
| /** @deprecated Use `media?.[0]?.contentType` or `.kind`. */ | |
| mediaType?: string; | |
| }; | |
| export type MessageTranscribedHookContext = MessageEnrichedBodyHookContext & { | |
| /** The transcribed text from audio */ | |
| transcript: string; | |
| }; | |
| export type MessagePreprocessedHookContext = MessageEnrichedBodyHookContext & { | |
| /** Transcribed audio text, if the message contained audio */ | |
| transcript?: string; | |
| /** Whether this message was sent in a group/channel context */ | |
| isGroup?: boolean; | |
| /** Group or channel identifier, if applicable */ | |
| groupId?: string; | |
| }; | |
| export type SessionPatchHookContext = { | |
| sessionEntry: SessionEntry; | |
| patch: SessionsPatchParams; | |
| cfg: OpenClawConfig; | |
| }; | |
| export type SessionPatchHookEvent = InternalHookEvent & { | |
| type: "session"; | |
| action: "patch"; | |
| context: SessionPatchHookContext; | |
| }; | |
| /** | |
| * Registry of hook handlers by event key. | |
| * | |
| * Uses a globalThis singleton so that registerInternalHook and | |
| * triggerInternalHook always share the same Map even when the bundler | |
| * emits multiple copies of this module into separate chunks (bundle | |
| * splitting). Without the singleton, handlers registered in one chunk | |
| * are invisible to triggerInternalHook in another chunk, causing hooks | |
| * to silently fire with zero handlers. | |
| */ | |
| const INTERNAL_HOOK_HANDLERS_KEY = Symbol.for("openclaw.internalHookHandlers"); | |
| const handlers = resolveGlobalSingleton<Map<string, InternalHookHandler[]>>( | |
| INTERNAL_HOOK_HANDLERS_KEY, | |
| () => new Map<string, InternalHookHandler[]>(), | |
| ); | |
| const INTERNAL_HOOKS_ENABLED_KEY = Symbol.for("openclaw.internalHooksEnabled"); | |
| const internalHooksEnabledState = resolveGlobalSingleton<{ enabled: boolean }>( | |
| INTERNAL_HOOKS_ENABLED_KEY, | |
| () => ({ enabled: true }), | |
| ); | |
| const log = createSubsystemLogger("internal-hooks"); | |
| /** | |
| * Register a hook handler for a specific event type or event:action combination | |
| * | |
| * @param eventKey - Event type (e.g., 'command') or specific action (e.g., 'command:new') | |
| * @param handler - Function to call when the event is triggered | |
| * | |
| * @example | |
| * ```ts | |
| * // Listen to all command events | |
| * registerInternalHook('command', async (event) => { | |
| * console.log('Command:', event.action); | |
| * }); | |
| * | |
| * // Listen only to /new commands | |
| * registerInternalHook('command:new', async (event) => { | |
| * await saveSessionToMemory(event); | |
| * }); | |
| * ``` | |
| */ | |
| export function registerInternalHook(eventKey: string, handler: InternalHookHandler): void { | |
| if (!handlers.has(eventKey)) { | |
| handlers.set(eventKey, []); | |
| } | |
| handlers.get(eventKey)!.push(handler); | |
| } | |
| /** | |
| * Unregister a specific hook handler | |
| * | |
| * @param eventKey - Event key the handler was registered for | |
| * @param handler - The handler function to remove | |
| */ | |
| export function unregisterInternalHook(eventKey: string, handler: InternalHookHandler): void { | |
| const eventHandlers = handlers.get(eventKey); | |
| if (!eventHandlers) { | |
| return; | |
| } | |
| const index = eventHandlers.indexOf(handler); | |
| if (index !== -1) { | |
| eventHandlers.splice(index, 1); | |
| } | |
| // Clean up empty handler arrays | |
| if (eventHandlers.length === 0) { | |
| handlers.delete(eventKey); | |
| } | |
| } | |
| /** | |
| * Clear all registered hooks (useful for testing) | |
| */ | |
| export function clearInternalHooks(): void { | |
| handlers.clear(); | |
| clearLegacyPluginInternalHooks(); | |
| } | |
| export function setInternalHooksEnabled(enabled: boolean): void { | |
| internalHooksEnabledState.enabled = enabled; | |
| } | |
| /** | |
| * Get all registered event keys (useful for debugging) | |
| */ | |
| export function getRegisteredEventKeys(): string[] { | |
| return [...new Set([...handlers.keys(), ...listLegacyPluginInternalHookEventKeys()])]; | |
| } | |
| export function hasInternalHookListeners(type: InternalHookEventType, action: string): boolean { | |
| return ( | |
| (handlers.get(type)?.length ?? 0) + listLegacyPluginInternalHooks(type).length > 0 || | |
| (handlers.get(`${type}:${action}`)?.length ?? 0) + | |
| listLegacyPluginInternalHooks(`${type}:${action}`).length > | |
| 0 | |
| ); | |
| } | |
| /** | |
| * Trigger a hook event | |
| * | |
| * Calls all handlers registered for: | |
| * 1. The general event type (e.g., 'command') | |
| * 2. The specific event:action combination (e.g., 'command:new') | |
| * | |
| * Handlers are called in registration order. Errors are caught and logged | |
| * but don't prevent other handlers from running. | |
| * | |
| * @param event - The event to trigger | |
| */ | |
| export async function triggerInternalHook(event: InternalHookEvent): Promise<void> { | |
| if (!internalHooksEnabledState.enabled) { | |
| return; | |
| } | |
| if (!hasInternalHookListeners(event.type, event.action)) { | |
| return; | |
| } | |
| // An admitted event finishes its snapshot even if a handler awaits across a reload or disable. | |
| const typeHandlers = [ | |
| ...(handlers.get(event.type) ?? []), | |
| ...listLegacyPluginInternalHooks(event.type), | |
| ]; | |
| const specificKey = `${event.type}:${event.action}`; | |
| const specificHandlers = [ | |
| ...(handlers.get(specificKey) ?? []), | |
| ...listLegacyPluginInternalHooks(specificKey), | |
| ]; | |
| const allHandlers = [...typeHandlers, ...specificHandlers]; | |
| for (const handler of allHandlers) { | |
| try { | |
| await handler(event); | |
| } catch (err) { | |
| const message = formatErrorMessage(err); | |
| log.error(`Hook error [${event.type}:${event.action}]: ${message}`); | |
| } | |
| } | |
| } | |
| /** | |
| * Create a hook event with common fields filled in | |
| * | |
| * @param type - The event type | |
| * @param action - The action within that type | |
| * @param sessionKey - The session key | |
| * @param context - Additional context | |
| */ | |
| export function createInternalHookEvent( | |
| type: InternalHookEventType, | |
| action: string, | |
| sessionKey: string, | |
| context: Record<string, unknown> = {}, | |
| ): InternalHookEvent { | |
| return { | |
| type, | |
| action, | |
| sessionKey, | |
| context, | |
| timestamp: new Date(), | |
| messages: [], | |
| }; | |
| } | |
| function isHookEventTypeAndAction( | |
| event: InternalHookEvent, | |
| type: InternalHookEventType, | |
| action: string, | |
| ): boolean { | |
| return event.type === type && event.action === action; | |
| } | |
| function getHookContext<T extends Record<string, unknown>>( | |
| event: InternalHookEvent, | |
| ): Partial<T> | null { | |
| const context = event.context as Partial<T> | null; | |
| if (!context || typeof context !== "object") { | |
| return null; | |
| } | |
| return context; | |
| } | |
| function hasStringContextField<T extends Record<string, unknown>>( | |
| context: Partial<T>, | |
| key: keyof T, | |
| ): boolean { | |
| return typeof context[key] === "string"; | |
| } | |
| export function isAgentBootstrapEvent(event: InternalHookEvent): event is AgentBootstrapHookEvent { | |
| if (!isHookEventTypeAndAction(event, "agent", "bootstrap")) { | |
| return false; | |
| } | |
| const context = getHookContext<AgentBootstrapHookContext>(event); | |
| if (!context) { | |
| return false; | |
| } | |
| if (!hasStringContextField(context, "workspaceDir")) { | |
| return false; | |
| } | |
| return Array.isArray(context.bootstrapFiles); | |
| } | |
| export function isGatewayStartupEvent(event: InternalHookEvent): event is GatewayStartupHookEvent { | |
| if (!isHookEventTypeAndAction(event, "gateway", "startup")) { | |
| return false; | |
| } | |
| return Boolean(getHookContext<GatewayStartupHookContext>(event)); | |
| } | |
| export function isSessionPatchEvent(event: InternalHookEvent): event is SessionPatchHookEvent { | |
| if (!isHookEventTypeAndAction(event, "session", "patch")) { | |
| return false; | |
| } | |
| const context = getHookContext<SessionPatchHookContext>(event); | |
| if (!context) { | |
| return false; | |
| } | |
| return ( | |
| typeof context.patch === "object" && | |
| context.patch !== null && | |
| typeof context.cfg === "object" && | |
| context.cfg !== null && | |
| typeof context.sessionEntry === "object" && | |
| context.sessionEntry !== null | |
| ); | |
| } | |