diff --git a/src/plugins/hook-registry.types.ts b/src/plugins/hook-registry.types.ts index c2e450dbdaf..dc53765ff70 100644 --- a/src/plugins/hook-registry.types.ts +++ b/src/plugins/hook-registry.types.ts @@ -1,5 +1,5 @@ import type { HookEntry } from "../hooks/types.js"; -import type { PluginHookRegistration as TypedPluginHookRegistration } from "./types.js"; +import type { PluginHookRegistration as TypedPluginHookRegistration } from "./hook-types.js"; export type PluginLegacyHookRegistration = { pluginId: string; diff --git a/src/plugins/hook-runner-global.ts b/src/plugins/hook-runner-global.ts index 991ee323cce..a25af87c09a 100644 --- a/src/plugins/hook-runner-global.ts +++ b/src/plugins/hook-runner-global.ts @@ -8,8 +8,8 @@ import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveGlobalSingleton } from "../shared/global-singleton.js"; import type { GlobalHookRunnerRegistry } from "./hook-registry.types.js"; +import type { PluginHookGatewayContext, PluginHookGatewayStopEvent } from "./hook-types.js"; import { createHookRunner, type HookRunner } from "./hooks.js"; -import type { PluginHookGatewayContext, PluginHookGatewayStopEvent } from "./types.js"; type HookRunnerGlobalState = { hookRunner: HookRunner | null; diff --git a/src/plugins/hook-types.ts b/src/plugins/hook-types.ts new file mode 100644 index 00000000000..76d4a54b382 --- /dev/null +++ b/src/plugins/hook-types.ts @@ -0,0 +1,693 @@ +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { + ReplyDispatchKind, + ReplyDispatcher, +} from "../auto-reply/reply/reply-dispatcher.types.js"; +import type { FinalizedMsgContext } from "../auto-reply/templating.js"; +import type { ReplyPayload } from "../auto-reply/types.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import type { TtsAutoMode } from "../config/types.tts.js"; +import { + PLUGIN_PROMPT_MUTATION_RESULT_FIELDS, + stripPromptMutationFieldsFromLegacyHookResult, +} from "./hook-before-agent-start.types.js"; +import type { + PluginHookBeforeAgentStartEvent, + PluginHookBeforeAgentStartResult, + PluginHookBeforeModelResolveEvent, + PluginHookBeforeModelResolveResult, + PluginHookBeforePromptBuildEvent, + PluginHookBeforePromptBuildResult, +} from "./hook-before-agent-start.types.js"; +import type { + PluginHookInboundClaimContext, + PluginHookInboundClaimEvent, + PluginHookMessageContext, + PluginHookMessageReceivedEvent, + PluginHookMessageSendingEvent, + PluginHookMessageSendingResult, + PluginHookMessageSentEvent, +} from "./hook-message.types.js"; + +export type { + PluginHookBeforeAgentStartEvent, + PluginHookBeforeAgentStartOverrideResult, + PluginHookBeforeAgentStartResult, + PluginHookBeforeModelResolveEvent, + PluginHookBeforeModelResolveResult, + PluginHookBeforePromptBuildEvent, + PluginHookBeforePromptBuildResult, +} from "./hook-before-agent-start.types.js"; +export { + PLUGIN_PROMPT_MUTATION_RESULT_FIELDS, + stripPromptMutationFieldsFromLegacyHookResult, +} from "./hook-before-agent-start.types.js"; +export type { + PluginHookInboundClaimContext, + PluginHookInboundClaimEvent, + PluginHookMessageContext, + PluginHookMessageReceivedEvent, + PluginHookMessageSendingEvent, + PluginHookMessageSendingResult, + PluginHookMessageSentEvent, +} from "./hook-message.types.js"; + +export type PluginHookName = + | "before_model_resolve" + | "before_prompt_build" + | "before_agent_start" + | "before_agent_reply" + | "llm_input" + | "llm_output" + | "agent_end" + | "before_compaction" + | "after_compaction" + | "before_reset" + | "inbound_claim" + | "message_received" + | "message_sending" + | "message_sent" + | "before_tool_call" + | "after_tool_call" + | "tool_result_persist" + | "before_message_write" + | "session_start" + | "session_end" + | "subagent_spawning" + | "subagent_delivery_target" + | "subagent_spawned" + | "subagent_ended" + | "gateway_start" + | "gateway_stop" + | "before_dispatch" + | "reply_dispatch" + | "before_install"; + +export const PLUGIN_HOOK_NAMES = [ + "before_model_resolve", + "before_prompt_build", + "before_agent_start", + "before_agent_reply", + "llm_input", + "llm_output", + "agent_end", + "before_compaction", + "after_compaction", + "before_reset", + "inbound_claim", + "message_received", + "message_sending", + "message_sent", + "before_tool_call", + "after_tool_call", + "tool_result_persist", + "before_message_write", + "session_start", + "session_end", + "subagent_spawning", + "subagent_delivery_target", + "subagent_spawned", + "subagent_ended", + "gateway_start", + "gateway_stop", + "before_dispatch", + "reply_dispatch", + "before_install", +] as const satisfies readonly PluginHookName[]; + +type MissingPluginHookNames = Exclude; +type AssertAllPluginHookNamesListed = MissingPluginHookNames extends never ? true : never; +const assertAllPluginHookNamesListed: AssertAllPluginHookNamesListed = true; +void assertAllPluginHookNamesListed; + +const pluginHookNameSet = new Set(PLUGIN_HOOK_NAMES); + +export const isPluginHookName = (hookName: unknown): hookName is PluginHookName => + typeof hookName === "string" && pluginHookNameSet.has(hookName as PluginHookName); + +export const PROMPT_INJECTION_HOOK_NAMES = [ + "before_prompt_build", + "before_agent_start", +] as const satisfies readonly PluginHookName[]; + +export type PromptInjectionHookName = (typeof PROMPT_INJECTION_HOOK_NAMES)[number]; + +const promptInjectionHookNameSet = new Set(PROMPT_INJECTION_HOOK_NAMES); + +export const isPromptInjectionHookName = (hookName: PluginHookName): boolean => + promptInjectionHookNameSet.has(hookName); + +export type PluginHookAgentContext = { + runId?: string; + agentId?: string; + sessionKey?: string; + sessionId?: string; + workspaceDir?: string; + modelProviderId?: string; + modelId?: string; + messageProvider?: string; + trigger?: string; + channelId?: string; +}; + +export type PluginHookBeforeAgentReplyEvent = { + cleanedBody: string; +}; + +export type PluginHookBeforeAgentReplyResult = { + handled: boolean; + reply?: ReplyPayload; + reason?: string; +}; + +export type PluginHookLlmInputEvent = { + runId: string; + sessionId: string; + provider: string; + model: string; + systemPrompt?: string; + prompt: string; + historyMessages: unknown[]; + imagesCount: number; +}; + +export type PluginHookLlmOutputEvent = { + runId: string; + sessionId: string; + provider: string; + model: string; + assistantTexts: string[]; + lastAssistant?: unknown; + usage?: { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + total?: number; + }; +}; + +export type PluginHookAgentEndEvent = { + messages: unknown[]; + success: boolean; + error?: string; + durationMs?: number; +}; + +export type PluginHookBeforeCompactionEvent = { + messageCount: number; + compactingCount?: number; + tokenCount?: number; + messages?: unknown[]; + sessionFile?: string; +}; + +export type PluginHookBeforeResetEvent = { + sessionFile?: string; + messages?: unknown[]; + reason?: string; +}; + +export type PluginHookAfterCompactionEvent = { + messageCount: number; + tokenCount?: number; + compactedCount: number; + sessionFile?: string; +}; + +export type PluginHookInboundClaimResult = { + handled: boolean; +}; + +export type PluginHookBeforeDispatchEvent = { + content: string; + body?: string; + channel?: string; + sessionKey?: string; + senderId?: string; + isGroup?: boolean; + timestamp?: number; +}; + +export type PluginHookBeforeDispatchContext = { + channelId?: string; + accountId?: string; + conversationId?: string; + sessionKey?: string; + senderId?: string; +}; + +export type PluginHookBeforeDispatchResult = { + handled: boolean; + text?: string; +}; + +export type PluginHookReplyDispatchEvent = { + ctx: FinalizedMsgContext; + runId?: string; + sessionKey?: string; + inboundAudio: boolean; + sessionTtsAuto?: TtsAutoMode; + ttsChannel?: string; + suppressUserDelivery?: boolean; + shouldRouteToOriginating: boolean; + originatingChannel?: string; + originatingTo?: string; + shouldSendToolSummaries: boolean; + sendPolicy: "allow" | "deny"; + isTailDispatch?: boolean; +}; + +export type PluginHookReplyDispatchContext = { + cfg: OpenClawConfig; + dispatcher: ReplyDispatcher; + abortSignal?: AbortSignal; + onReplyStart?: () => Promise | void; + recordProcessed: ( + outcome: "completed" | "skipped" | "error", + opts?: { + reason?: string; + error?: string; + }, + ) => void; + markIdle: (reason: string) => void; +}; + +export type PluginHookReplyDispatchResult = { + handled: boolean; + queuedFinal: boolean; + counts: Record; +}; + +export type PluginHookToolContext = { + agentId?: string; + sessionKey?: string; + sessionId?: string; + runId?: string; + toolName: string; + toolCallId?: string; +}; + +export type PluginHookBeforeToolCallEvent = { + toolName: string; + params: Record; + runId?: string; + toolCallId?: string; +}; + +export const PluginApprovalResolutions = { + ALLOW_ONCE: "allow-once", + ALLOW_ALWAYS: "allow-always", + DENY: "deny", + TIMEOUT: "timeout", + CANCELLED: "cancelled", +} as const; + +export type PluginApprovalResolution = + (typeof PluginApprovalResolutions)[keyof typeof PluginApprovalResolutions]; + +export type PluginHookBeforeToolCallResult = { + params?: Record; + block?: boolean; + blockReason?: string; + requireApproval?: { + title: string; + description: string; + severity?: "info" | "warning" | "critical"; + timeoutMs?: number; + timeoutBehavior?: "allow" | "deny"; + pluginId?: string; + onResolution?: (decision: PluginApprovalResolution) => Promise | void; + }; +}; + +export type PluginHookAfterToolCallEvent = { + toolName: string; + params: Record; + runId?: string; + toolCallId?: string; + result?: unknown; + error?: string; + durationMs?: number; +}; + +export type PluginHookToolResultPersistContext = { + agentId?: string; + sessionKey?: string; + toolName?: string; + toolCallId?: string; +}; + +export type PluginHookToolResultPersistEvent = { + toolName?: string; + toolCallId?: string; + message: AgentMessage; + isSynthetic?: boolean; +}; + +export type PluginHookToolResultPersistResult = { + message?: AgentMessage; +}; + +export type PluginHookBeforeMessageWriteEvent = { + message: AgentMessage; + sessionKey?: string; + agentId?: string; +}; + +export type PluginHookBeforeMessageWriteResult = { + block?: boolean; + message?: AgentMessage; +}; + +export type PluginHookSessionContext = { + agentId?: string; + sessionId: string; + sessionKey?: string; +}; + +export type PluginHookSessionStartEvent = { + sessionId: string; + sessionKey?: string; + resumedFrom?: string; +}; + +export type PluginHookSessionEndReason = + | "new" + | "reset" + | "idle" + | "daily" + | "compaction" + | "deleted" + | "unknown"; + +export type PluginHookSessionEndEvent = { + sessionId: string; + sessionKey?: string; + messageCount: number; + durationMs?: number; + reason?: PluginHookSessionEndReason; + sessionFile?: string; + transcriptArchived?: boolean; + nextSessionId?: string; + nextSessionKey?: string; +}; + +export type PluginHookSubagentContext = { + runId?: string; + childSessionKey?: string; + requesterSessionKey?: string; +}; + +export type PluginHookSubagentTargetKind = "subagent" | "acp"; + +type PluginHookSubagentSpawnBase = { + childSessionKey: string; + agentId: string; + label?: string; + mode: "run" | "session"; + requester?: { + channel?: string; + accountId?: string; + to?: string; + threadId?: string | number; + }; + threadRequested: boolean; +}; + +export type PluginHookSubagentSpawningEvent = PluginHookSubagentSpawnBase; + +export type PluginHookSubagentSpawningResult = + | { + status: "ok"; + threadBindingReady?: boolean; + } + | { + status: "error"; + error: string; + }; + +export type PluginHookSubagentDeliveryTargetEvent = { + childSessionKey: string; + requesterSessionKey: string; + requesterOrigin?: { + channel?: string; + accountId?: string; + to?: string; + threadId?: string | number; + }; + childRunId?: string; + spawnMode?: "run" | "session"; + expectsCompletionMessage: boolean; +}; + +export type PluginHookSubagentDeliveryTargetResult = { + origin?: { + channel?: string; + accountId?: string; + to?: string; + threadId?: string | number; + }; +}; + +export type PluginHookSubagentSpawnedEvent = PluginHookSubagentSpawnBase & { + runId: string; +}; + +export type PluginHookSubagentEndedEvent = { + targetSessionKey: string; + targetKind: PluginHookSubagentTargetKind; + reason: string; + sendFarewell?: boolean; + accountId?: string; + runId?: string; + endedAt?: number; + outcome?: "ok" | "error" | "timeout" | "killed" | "reset" | "deleted"; + error?: string; +}; + +export type PluginHookGatewayContext = { + port?: number; +}; + +export type PluginHookGatewayStartEvent = { + port: number; +}; + +export type PluginHookGatewayStopEvent = { + reason?: string; +}; + +export type PluginInstallTargetType = "skill" | "plugin"; +export type PluginInstallRequestKind = + | "skill-install" + | "plugin-dir" + | "plugin-archive" + | "plugin-file" + | "plugin-npm"; +export type PluginInstallSourcePathKind = "file" | "directory"; + +export type PluginInstallFinding = { + ruleId: string; + severity: "info" | "warn" | "critical"; + file: string; + line: number; + message: string; +}; + +export type PluginHookBeforeInstallRequest = { + kind: PluginInstallRequestKind; + mode: "install" | "update"; + requestedSpecifier?: string; +}; + +export type PluginHookBeforeInstallBuiltinScan = { + status: "ok" | "error"; + scannedFiles: number; + critical: number; + warn: number; + info: number; + findings: PluginInstallFinding[]; + error?: string; +}; + +export type PluginHookBeforeInstallSkillInstallSpec = { + id?: string; + kind: "brew" | "node" | "go" | "uv" | "download"; + label?: string; + bins?: string[]; + os?: string[]; + formula?: string; + package?: string; + module?: string; + url?: string; + archive?: string; + extract?: boolean; + stripComponents?: number; + targetDir?: string; +}; + +export type PluginHookBeforeInstallSkill = { + installId: string; + installSpec?: PluginHookBeforeInstallSkillInstallSpec; +}; + +export type PluginHookBeforeInstallPlugin = { + pluginId: string; + contentType: "bundle" | "package" | "file"; + packageName?: string; + manifestId?: string; + version?: string; + extensions?: string[]; +}; + +export type PluginHookBeforeInstallContext = { + targetType: PluginInstallTargetType; + requestKind: PluginInstallRequestKind; + origin?: string; +}; + +export type PluginHookBeforeInstallEvent = { + targetType: PluginInstallTargetType; + targetName: string; + sourcePath: string; + sourcePathKind: PluginInstallSourcePathKind; + origin?: string; + request: PluginHookBeforeInstallRequest; + builtinScan: PluginHookBeforeInstallBuiltinScan; + skill?: PluginHookBeforeInstallSkill; + plugin?: PluginHookBeforeInstallPlugin; +}; + +export type PluginHookBeforeInstallResult = { + findings?: PluginInstallFinding[]; + block?: boolean; + blockReason?: string; +}; + +export type PluginHookHandlerMap = { + before_model_resolve: ( + event: PluginHookBeforeModelResolveEvent, + ctx: PluginHookAgentContext, + ) => + | Promise + | PluginHookBeforeModelResolveResult + | void; + before_prompt_build: ( + event: PluginHookBeforePromptBuildEvent, + ctx: PluginHookAgentContext, + ) => Promise | PluginHookBeforePromptBuildResult | void; + before_agent_start: ( + event: PluginHookBeforeAgentStartEvent, + ctx: PluginHookAgentContext, + ) => Promise | PluginHookBeforeAgentStartResult | void; + before_agent_reply: ( + event: PluginHookBeforeAgentReplyEvent, + ctx: PluginHookAgentContext, + ) => Promise | PluginHookBeforeAgentReplyResult | void; + llm_input: (event: PluginHookLlmInputEvent, ctx: PluginHookAgentContext) => Promise | void; + llm_output: ( + event: PluginHookLlmOutputEvent, + ctx: PluginHookAgentContext, + ) => Promise | void; + agent_end: (event: PluginHookAgentEndEvent, ctx: PluginHookAgentContext) => Promise | void; + before_compaction: ( + event: PluginHookBeforeCompactionEvent, + ctx: PluginHookAgentContext, + ) => Promise | void; + after_compaction: ( + event: PluginHookAfterCompactionEvent, + ctx: PluginHookAgentContext, + ) => Promise | void; + before_reset: ( + event: PluginHookBeforeResetEvent, + ctx: PluginHookAgentContext, + ) => Promise | void; + inbound_claim: ( + event: PluginHookInboundClaimEvent, + ctx: PluginHookInboundClaimContext, + ) => Promise | PluginHookInboundClaimResult | void; + before_dispatch: ( + event: PluginHookBeforeDispatchEvent, + ctx: PluginHookBeforeDispatchContext, + ) => Promise | PluginHookBeforeDispatchResult | void; + reply_dispatch: ( + event: PluginHookReplyDispatchEvent, + ctx: PluginHookReplyDispatchContext, + ) => Promise | PluginHookReplyDispatchResult | void; + message_received: ( + event: PluginHookMessageReceivedEvent, + ctx: PluginHookMessageContext, + ) => Promise | void; + message_sending: ( + event: PluginHookMessageSendingEvent, + ctx: PluginHookMessageContext, + ) => Promise | PluginHookMessageSendingResult | void; + message_sent: ( + event: PluginHookMessageSentEvent, + ctx: PluginHookMessageContext, + ) => Promise | void; + before_tool_call: ( + event: PluginHookBeforeToolCallEvent, + ctx: PluginHookToolContext, + ) => Promise | PluginHookBeforeToolCallResult | void; + after_tool_call: ( + event: PluginHookAfterToolCallEvent, + ctx: PluginHookToolContext, + ) => Promise | void; + tool_result_persist: ( + event: PluginHookToolResultPersistEvent, + ctx: PluginHookToolResultPersistContext, + ) => PluginHookToolResultPersistResult | void; + before_message_write: ( + event: PluginHookBeforeMessageWriteEvent, + ctx: { agentId?: string; sessionKey?: string }, + ) => PluginHookBeforeMessageWriteResult | void; + session_start: ( + event: PluginHookSessionStartEvent, + ctx: PluginHookSessionContext, + ) => Promise | void; + session_end: ( + event: PluginHookSessionEndEvent, + ctx: PluginHookSessionContext, + ) => Promise | void; + subagent_spawning: ( + event: PluginHookSubagentSpawningEvent, + ctx: PluginHookSubagentContext, + ) => Promise | PluginHookSubagentSpawningResult | void; + subagent_delivery_target: ( + event: PluginHookSubagentDeliveryTargetEvent, + ctx: PluginHookSubagentContext, + ) => + | Promise + | PluginHookSubagentDeliveryTargetResult + | void; + subagent_spawned: ( + event: PluginHookSubagentSpawnedEvent, + ctx: PluginHookSubagentContext, + ) => Promise | void; + subagent_ended: ( + event: PluginHookSubagentEndedEvent, + ctx: PluginHookSubagentContext, + ) => Promise | void; + gateway_start: ( + event: PluginHookGatewayStartEvent, + ctx: PluginHookGatewayContext, + ) => Promise | void; + gateway_stop: ( + event: PluginHookGatewayStopEvent, + ctx: PluginHookGatewayContext, + ) => Promise | void; + before_install: ( + event: PluginHookBeforeInstallEvent, + ctx: PluginHookBeforeInstallContext, + ) => Promise | PluginHookBeforeInstallResult | void; +}; + +export type PluginHookRegistration = { + pluginId: string; + hookName: K; + handler: PluginHookHandlerMap[K]; + priority?: number; + source: string; +}; diff --git a/src/plugins/hooks.ts b/src/plugins/hooks.ts index eeef2362df7..b7d3e5f4d22 100644 --- a/src/plugins/hooks.ts +++ b/src/plugins/hooks.ts @@ -65,7 +65,7 @@ import type { PluginHookBeforeInstallContext, PluginHookBeforeInstallEvent, PluginHookBeforeInstallResult, -} from "./types.js"; +} from "./hook-types.js"; // Re-export types for consumers export type { diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 555309f5329..645fab3f788 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -17,11 +17,6 @@ import type { ModelProviderRequestTransportOverrides } from "../agents/provider- import type { ProviderSystemPromptContribution } from "../agents/system-prompt-contribution.js"; import type { PromptMode } from "../agents/system-prompt.types.js"; import type { AnyAgentTool } from "../agents/tools/common.js"; -import type { - ReplyDispatchKind, - ReplyDispatcher, -} from "../auto-reply/reply/reply-dispatcher.types.js"; -import type { FinalizedMsgContext } from "../auto-reply/templating.js"; import type { ThinkLevel } from "../auto-reply/thinking.shared.js"; import type { ReplyPayload } from "../auto-reply/types.js"; import type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; @@ -29,7 +24,6 @@ import type { ChannelId } from "../channels/plugins/types.public.js"; import type { ModelProviderConfig } from "../config/types.js"; import type { ModelCompatConfig } from "../config/types.models.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import type { TtsAutoMode } from "../config/types.tts.js"; import type { OperatorScope } from "../gateway/operator-scopes.js"; import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; import type { InternalHookHandler } from "../hooks/internal-hook-types.js"; @@ -86,27 +80,7 @@ import type { PluginConversationBindingResolvedEvent, PluginConversationBindingResolutionDecision, } from "./conversation-binding.types.js"; -import { - PLUGIN_PROMPT_MUTATION_RESULT_FIELDS, - stripPromptMutationFieldsFromLegacyHookResult, -} from "./hook-before-agent-start.types.js"; -import type { - PluginHookBeforeAgentStartEvent, - PluginHookBeforeAgentStartResult, - PluginHookBeforeModelResolveEvent, - PluginHookBeforeModelResolveResult, - PluginHookBeforePromptBuildEvent, - PluginHookBeforePromptBuildResult, -} from "./hook-before-agent-start.types.js"; -import type { - PluginHookInboundClaimContext, - PluginHookInboundClaimEvent, - PluginHookMessageContext, - PluginHookMessageReceivedEvent, - PluginHookMessageSendingEvent, - PluginHookMessageSendingResult, - PluginHookMessageSentEvent, -} from "./hook-message.types.js"; +import type { PluginHookHandlerMap, PluginHookName } from "./hook-types.js"; import type { PluginBundleFormat, PluginConfigUiHint, @@ -154,19 +128,6 @@ export type { } from "./tool-types.js"; export type { AnyAgentTool } from "../agents/tools/common.js"; export type { AgentHarness } from "../agents/harness/types.js"; -export type { - PluginHookBeforeAgentStartEvent, - PluginHookBeforeAgentStartOverrideResult, - PluginHookBeforeAgentStartResult, - PluginHookBeforeModelResolveEvent, - PluginHookBeforeModelResolveResult, - PluginHookBeforePromptBuildEvent, - PluginHookBeforePromptBuildResult, -} from "./hook-before-agent-start.types.js"; -export { - PLUGIN_PROMPT_MUTATION_RESULT_FIELDS, - stripPromptMutationFieldsFromLegacyHookResult, -} from "./hook-before-agent-start.types.js"; export type { PluginConversationBinding, PluginConversationBindingRequestParams, @@ -180,6 +141,7 @@ export type { PluginTextReplacement, PluginTextTransforms, } from "./cli-backend.types.js"; +export * from "./hook-types.js"; export type ProviderAuthOptionBag = { token?: string; @@ -197,15 +159,6 @@ export type PluginLogger = { }; export type { PluginKind } from "./plugin-kind.types.js"; -export type { - PluginHookInboundClaimContext, - PluginHookInboundClaimEvent, - PluginHookMessageContext, - PluginHookMessageReceivedEvent, - PluginHookMessageSendingEvent, - PluginHookMessageSendingResult, - PluginHookMessageSentEvent, -} from "./hook-message.types.js"; export type { ProviderExternalAuthProfile, ProviderExternalOAuthProfile, @@ -2096,739 +2049,5 @@ export type OpenClawPluginApi = { ) => void; }; -// ============================================================================ -// Plugin Hooks -// ============================================================================ - -export type PluginHookName = - | "before_model_resolve" - | "before_prompt_build" - | "before_agent_start" - | "before_agent_reply" - | "llm_input" - | "llm_output" - | "agent_end" - | "before_compaction" - | "after_compaction" - | "before_reset" - | "inbound_claim" - | "message_received" - | "message_sending" - | "message_sent" - | "before_tool_call" - | "after_tool_call" - | "tool_result_persist" - | "before_message_write" - | "session_start" - | "session_end" - | "subagent_spawning" - | "subagent_delivery_target" - | "subagent_spawned" - | "subagent_ended" - | "gateway_start" - | "gateway_stop" - | "before_dispatch" - | "reply_dispatch" - | "before_install"; - -export const PLUGIN_HOOK_NAMES = [ - "before_model_resolve", - "before_prompt_build", - "before_agent_start", - "before_agent_reply", - "llm_input", - "llm_output", - "agent_end", - "before_compaction", - "after_compaction", - "before_reset", - "inbound_claim", - "message_received", - "message_sending", - "message_sent", - "before_tool_call", - "after_tool_call", - "tool_result_persist", - "before_message_write", - "session_start", - "session_end", - "subagent_spawning", - "subagent_delivery_target", - "subagent_spawned", - "subagent_ended", - "gateway_start", - "gateway_stop", - "before_dispatch", - "reply_dispatch", - "before_install", -] as const satisfies readonly PluginHookName[]; - -type MissingPluginHookNames = Exclude; -type AssertAllPluginHookNamesListed = MissingPluginHookNames extends never ? true : never; -const assertAllPluginHookNamesListed: AssertAllPluginHookNamesListed = true; -void assertAllPluginHookNamesListed; - -const pluginHookNameSet = new Set(PLUGIN_HOOK_NAMES); - -export const isPluginHookName = (hookName: unknown): hookName is PluginHookName => - typeof hookName === "string" && pluginHookNameSet.has(hookName as PluginHookName); - -export const PROMPT_INJECTION_HOOK_NAMES = [ - "before_prompt_build", - "before_agent_start", -] as const satisfies readonly PluginHookName[]; - -export type PromptInjectionHookName = (typeof PROMPT_INJECTION_HOOK_NAMES)[number]; - -const promptInjectionHookNameSet = new Set(PROMPT_INJECTION_HOOK_NAMES); - -export const isPromptInjectionHookName = (hookName: PluginHookName): boolean => - promptInjectionHookNameSet.has(hookName); - -// Agent context shared across agent hooks -export type PluginHookAgentContext = { - /** Unique identifier for this agent run. */ - runId?: string; - agentId?: string; - sessionKey?: string; - sessionId?: string; - workspaceDir?: string; - /** Resolved model provider for this run (for example "openai"). */ - modelProviderId?: string; - /** Resolved model id for this run (for example "gpt-5.4"). */ - modelId?: string; - messageProvider?: string; - /** What initiated this agent run: "user", "heartbeat", "cron", or "memory". */ - trigger?: string; - /** Channel identifier (e.g. "telegram", "discord", "whatsapp"). */ - channelId?: string; -}; - -// before_agent_reply hook -export type PluginHookBeforeAgentReplyEvent = { - /** The final user message text heading to the LLM (after commands/directives). */ - cleanedBody: string; -}; - -export type PluginHookBeforeAgentReplyResult = { - /** Whether the plugin is claiming this message (short-circuits the LLM agent). */ - handled: boolean; - /** Synthetic reply that short-circuits the LLM agent. */ - reply?: ReplyPayload; - /** Reason for interception (for logging/debugging). */ - reason?: string; -}; - -// llm_input hook -export type PluginHookLlmInputEvent = { - runId: string; - sessionId: string; - provider: string; - model: string; - systemPrompt?: string; - prompt: string; - historyMessages: unknown[]; - imagesCount: number; -}; - -// llm_output hook -export type PluginHookLlmOutputEvent = { - runId: string; - sessionId: string; - provider: string; - model: string; - assistantTexts: string[]; - lastAssistant?: unknown; - usage?: { - input?: number; - output?: number; - cacheRead?: number; - cacheWrite?: number; - total?: number; - }; -}; - -// agent_end hook -export type PluginHookAgentEndEvent = { - messages: unknown[]; - success: boolean; - error?: string; - durationMs?: number; -}; - -// Compaction hooks -export type PluginHookBeforeCompactionEvent = { - /** Total messages in the session before any truncation or compaction */ - messageCount: number; - /** Messages being fed to the compaction LLM (after history-limit truncation) */ - compactingCount?: number; - tokenCount?: number; - messages?: unknown[]; - /** Path to the session JSONL transcript. All messages are already on disk - * before compaction starts, so plugins can read this file asynchronously - * and process in parallel with the compaction LLM call. */ - sessionFile?: string; -}; - -// before_reset hook - fired when /new or /reset clears a session -export type PluginHookBeforeResetEvent = { - sessionFile?: string; - messages?: unknown[]; - reason?: string; -}; - -export type PluginHookAfterCompactionEvent = { - messageCount: number; - tokenCount?: number; - compactedCount: number; - /** Path to the session JSONL transcript. All pre-compaction messages are - * preserved on disk, so plugins can read and process them asynchronously - * without blocking the compaction pipeline. */ - sessionFile?: string; -}; - -export type PluginHookInboundClaimResult = { - handled: boolean; -}; - -// before_dispatch hook -export type PluginHookBeforeDispatchEvent = { - /** Message text content. */ - content: string; - /** Body text prepared for agent (after command parsing). */ - body?: string; - /** Channel identifier (e.g. "telegram", "discord"). */ - channel?: string; - /** Session key for this message. */ - sessionKey?: string; - /** Sender identifier. */ - senderId?: string; - /** Whether this is a group message. */ - isGroup?: boolean; - /** Message timestamp. */ - timestamp?: number; -}; - -export type PluginHookBeforeDispatchContext = { - channelId?: string; - accountId?: string; - conversationId?: string; - sessionKey?: string; - senderId?: string; -}; - -export type PluginHookBeforeDispatchResult = { - /** Whether the plugin handled this message (skips default dispatch). */ - handled: boolean; - /** Plugin-defined reply text (used when handled=true). */ - text?: string; -}; - -// reply_dispatch hook -export type PluginHookReplyDispatchEvent = { - ctx: FinalizedMsgContext; - runId?: string; - sessionKey?: string; - inboundAudio: boolean; - sessionTtsAuto?: TtsAutoMode; - ttsChannel?: string; - suppressUserDelivery?: boolean; - shouldRouteToOriginating: boolean; - originatingChannel?: string; - originatingTo?: string; - shouldSendToolSummaries: boolean; - sendPolicy: "allow" | "deny"; - isTailDispatch?: boolean; -}; - -export type PluginHookReplyDispatchContext = { - cfg: OpenClawConfig; - dispatcher: ReplyDispatcher; - abortSignal?: AbortSignal; - onReplyStart?: () => Promise | void; - recordProcessed: ( - outcome: "completed" | "skipped" | "error", - opts?: { - reason?: string; - error?: string; - }, - ) => void; - markIdle: (reason: string) => void; -}; - -export type PluginHookReplyDispatchResult = { - handled: boolean; - queuedFinal: boolean; - counts: Record; -}; - -// Tool context -export type PluginHookToolContext = { - agentId?: string; - sessionKey?: string; - /** Ephemeral session UUID - regenerated on /new and /reset. */ - sessionId?: string; - /** Stable run identifier for this agent invocation. */ - runId?: string; - toolName: string; - /** Provider-specific tool call ID when available. */ - toolCallId?: string; -}; - -// before_tool_call hook -export type PluginHookBeforeToolCallEvent = { - toolName: string; - params: Record; - /** Stable run identifier for this agent invocation. */ - runId?: string; - /** Provider-specific tool call ID when available. */ - toolCallId?: string; -}; - -export const PluginApprovalResolutions = { - ALLOW_ONCE: "allow-once", - ALLOW_ALWAYS: "allow-always", - DENY: "deny", - TIMEOUT: "timeout", - CANCELLED: "cancelled", -} as const; - -export type PluginApprovalResolution = - (typeof PluginApprovalResolutions)[keyof typeof PluginApprovalResolutions]; - -export type PluginHookBeforeToolCallResult = { - params?: Record; - block?: boolean; - blockReason?: string; - requireApproval?: { - title: string; - description: string; - severity?: "info" | "warning" | "critical"; - timeoutMs?: number; - timeoutBehavior?: "allow" | "deny"; - /** Set automatically by the hook runner - plugins should not set this. */ - pluginId?: string; - /** - * Best-effort callback invoked with the final outcome after approval resolves, times out, or is cancelled. - * OpenClaw does not await this callback before allowing or denying the tool call. - */ - onResolution?: (decision: PluginApprovalResolution) => Promise | void; - }; -}; - -// after_tool_call hook -export type PluginHookAfterToolCallEvent = { - toolName: string; - params: Record; - /** Stable run identifier for this agent invocation. */ - runId?: string; - /** Provider-specific tool call ID when available. */ - toolCallId?: string; - result?: unknown; - error?: string; - durationMs?: number; -}; - -// tool_result_persist hook -export type PluginHookToolResultPersistContext = { - agentId?: string; - sessionKey?: string; - toolName?: string; - toolCallId?: string; -}; - -export type PluginHookToolResultPersistEvent = { - toolName?: string; - toolCallId?: string; - /** - * The toolResult message about to be written to the session transcript. - * Handlers may return a modified message (e.g. drop non-essential fields). - */ - message: AgentMessage; - /** True when the tool result was synthesized by a guard/repair step. */ - isSynthetic?: boolean; -}; - -export type PluginHookToolResultPersistResult = { - message?: AgentMessage; -}; - -// before_message_write hook -export type PluginHookBeforeMessageWriteEvent = { - message: AgentMessage; - sessionKey?: string; - agentId?: string; -}; - -export type PluginHookBeforeMessageWriteResult = { - block?: boolean; // If true, message is NOT written to JSONL - message?: AgentMessage; // Optional: modified message to write instead -}; - -// Session context -export type PluginHookSessionContext = { - agentId?: string; - sessionId: string; - sessionKey?: string; -}; - -// session_start hook -export type PluginHookSessionStartEvent = { - sessionId: string; - sessionKey?: string; - resumedFrom?: string; -}; - -// session_end hook -export type PluginHookSessionEndReason = - | "new" - | "reset" - | "idle" - | "daily" - | "compaction" - | "deleted" - | "unknown"; - -export type PluginHookSessionEndEvent = { - sessionId: string; - sessionKey?: string; - messageCount: number; - durationMs?: number; - reason?: PluginHookSessionEndReason; - sessionFile?: string; - transcriptArchived?: boolean; - nextSessionId?: string; - nextSessionKey?: string; -}; - -// Subagent context -export type PluginHookSubagentContext = { - runId?: string; - childSessionKey?: string; - requesterSessionKey?: string; -}; - -export type PluginHookSubagentTargetKind = "subagent" | "acp"; - -type PluginHookSubagentSpawnBase = { - childSessionKey: string; - agentId: string; - label?: string; - mode: "run" | "session"; - requester?: { - channel?: string; - accountId?: string; - to?: string; - threadId?: string | number; - }; - threadRequested: boolean; -}; - -// subagent_spawning hook -export type PluginHookSubagentSpawningEvent = PluginHookSubagentSpawnBase; - -export type PluginHookSubagentSpawningResult = - | { - status: "ok"; - threadBindingReady?: boolean; - } - | { - status: "error"; - error: string; - }; - -// subagent_delivery_target hook -export type PluginHookSubagentDeliveryTargetEvent = { - childSessionKey: string; - requesterSessionKey: string; - requesterOrigin?: { - channel?: string; - accountId?: string; - to?: string; - threadId?: string | number; - }; - childRunId?: string; - spawnMode?: "run" | "session"; - expectsCompletionMessage: boolean; -}; - -export type PluginHookSubagentDeliveryTargetResult = { - origin?: { - channel?: string; - accountId?: string; - to?: string; - threadId?: string | number; - }; -}; - -// subagent_spawned hook -export type PluginHookSubagentSpawnedEvent = PluginHookSubagentSpawnBase & { - runId: string; -}; - -// subagent_ended hook -export type PluginHookSubagentEndedEvent = { - targetSessionKey: string; - targetKind: PluginHookSubagentTargetKind; - reason: string; - sendFarewell?: boolean; - accountId?: string; - runId?: string; - endedAt?: number; - outcome?: "ok" | "error" | "timeout" | "killed" | "reset" | "deleted"; - error?: string; -}; - -// Gateway context -export type PluginHookGatewayContext = { - port?: number; -}; - -// gateway_start hook -export type PluginHookGatewayStartEvent = { - port: number; -}; - -// gateway_stop hook -export type PluginHookGatewayStopEvent = { - reason?: string; -}; - -export type PluginInstallTargetType = "skill" | "plugin"; -export type PluginInstallRequestKind = - | "skill-install" - | "plugin-dir" - | "plugin-archive" - | "plugin-file" - | "plugin-npm"; -export type PluginInstallSourcePathKind = "file" | "directory"; - -export type PluginInstallFinding = { - ruleId: string; - severity: "info" | "warn" | "critical"; - file: string; - line: number; - message: string; -}; - -export type PluginHookBeforeInstallRequest = { - /** Original install entrypoint/provenance. */ - kind: PluginInstallRequestKind; - /** Install mode requested by the caller. */ - mode: "install" | "update"; - /** Raw user-facing specifier or path when available. */ - requestedSpecifier?: string; -}; - -export type PluginHookBeforeInstallBuiltinScan = { - /** Whether the built-in scan completed successfully. */ - status: "ok" | "error"; - /** Number of files the built-in scanner actually inspected. */ - scannedFiles: number; - critical: number; - warn: number; - info: number; - findings: PluginInstallFinding[]; - /** Scanner failure reason when status=`error`. */ - error?: string; -}; - -export type PluginHookBeforeInstallSkillInstallSpec = { - id?: string; - kind: "brew" | "node" | "go" | "uv" | "download"; - label?: string; - bins?: string[]; - os?: string[]; - formula?: string; - package?: string; - module?: string; - url?: string; - archive?: string; - extract?: boolean; - stripComponents?: number; - targetDir?: string; -}; - -export type PluginHookBeforeInstallSkill = { - installId: string; - installSpec?: PluginHookBeforeInstallSkillInstallSpec; -}; - -export type PluginHookBeforeInstallPlugin = { - /** Canonical plugin id OpenClaw will install under. */ - pluginId: string; - /** Normalized installable content shape after source resolution. */ - contentType: "bundle" | "package" | "file"; - packageName?: string; - manifestId?: string; - version?: string; - extensions?: string[]; -}; - -// before_install hook -export type PluginHookBeforeInstallContext = { - /** Category of install target being checked. */ - targetType: PluginInstallTargetType; - /** Original install entrypoint/provenance. */ - requestKind: PluginInstallRequestKind; - /** Normalized origin of the install target (e.g. "openclaw-bundled", "plugin-package"). */ - origin?: string; -}; - -export type PluginHookBeforeInstallEvent = { - /** Category of install target being checked. */ - targetType: PluginInstallTargetType; - /** Human-readable skill or plugin name. */ - targetName: string; - /** Absolute path to the install target content being scanned. */ - sourcePath: string; - /** Whether the install target content is a file or directory. */ - sourcePathKind: PluginInstallSourcePathKind; - /** Normalized origin of the install target (e.g. "openclaw-bundled", "plugin-package"). */ - origin?: string; - /** Install request provenance and caller mode. */ - request: PluginHookBeforeInstallRequest; - /** Structured result of the built-in scanner. */ - builtinScan: PluginHookBeforeInstallBuiltinScan; - /** Present when targetType=`skill`. */ - skill?: PluginHookBeforeInstallSkill; - /** Present when targetType=`plugin`. */ - plugin?: PluginHookBeforeInstallPlugin; -}; - -export type PluginHookBeforeInstallResult = { - /** Additional findings to merge with built-in scanner results. */ - findings?: PluginInstallFinding[]; - /** If true, block the installation entirely. */ - block?: boolean; - /** Human-readable reason for blocking. */ - blockReason?: string; -}; - -// Hook handler types mapped by hook name -export type PluginHookHandlerMap = { - before_model_resolve: ( - event: PluginHookBeforeModelResolveEvent, - ctx: PluginHookAgentContext, - ) => - | Promise - | PluginHookBeforeModelResolveResult - | void; - before_prompt_build: ( - event: PluginHookBeforePromptBuildEvent, - ctx: PluginHookAgentContext, - ) => Promise | PluginHookBeforePromptBuildResult | void; - before_agent_start: ( - event: PluginHookBeforeAgentStartEvent, - ctx: PluginHookAgentContext, - ) => Promise | PluginHookBeforeAgentStartResult | void; - before_agent_reply: ( - event: PluginHookBeforeAgentReplyEvent, - ctx: PluginHookAgentContext, - ) => Promise | PluginHookBeforeAgentReplyResult | void; - llm_input: (event: PluginHookLlmInputEvent, ctx: PluginHookAgentContext) => Promise | void; - llm_output: ( - event: PluginHookLlmOutputEvent, - ctx: PluginHookAgentContext, - ) => Promise | void; - agent_end: (event: PluginHookAgentEndEvent, ctx: PluginHookAgentContext) => Promise | void; - before_compaction: ( - event: PluginHookBeforeCompactionEvent, - ctx: PluginHookAgentContext, - ) => Promise | void; - after_compaction: ( - event: PluginHookAfterCompactionEvent, - ctx: PluginHookAgentContext, - ) => Promise | void; - before_reset: ( - event: PluginHookBeforeResetEvent, - ctx: PluginHookAgentContext, - ) => Promise | void; - inbound_claim: ( - event: PluginHookInboundClaimEvent, - ctx: PluginHookInboundClaimContext, - ) => Promise | PluginHookInboundClaimResult | void; - before_dispatch: ( - event: PluginHookBeforeDispatchEvent, - ctx: PluginHookBeforeDispatchContext, - ) => Promise | PluginHookBeforeDispatchResult | void; - reply_dispatch: ( - event: PluginHookReplyDispatchEvent, - ctx: PluginHookReplyDispatchContext, - ) => Promise | PluginHookReplyDispatchResult | void; - message_received: ( - event: PluginHookMessageReceivedEvent, - ctx: PluginHookMessageContext, - ) => Promise | void; - message_sending: ( - event: PluginHookMessageSendingEvent, - ctx: PluginHookMessageContext, - ) => Promise | PluginHookMessageSendingResult | void; - message_sent: ( - event: PluginHookMessageSentEvent, - ctx: PluginHookMessageContext, - ) => Promise | void; - before_tool_call: ( - event: PluginHookBeforeToolCallEvent, - ctx: PluginHookToolContext, - ) => Promise | PluginHookBeforeToolCallResult | void; - after_tool_call: ( - event: PluginHookAfterToolCallEvent, - ctx: PluginHookToolContext, - ) => Promise | void; - tool_result_persist: ( - event: PluginHookToolResultPersistEvent, - ctx: PluginHookToolResultPersistContext, - ) => PluginHookToolResultPersistResult | void; - before_message_write: ( - event: PluginHookBeforeMessageWriteEvent, - ctx: { agentId?: string; sessionKey?: string }, - ) => PluginHookBeforeMessageWriteResult | void; - session_start: ( - event: PluginHookSessionStartEvent, - ctx: PluginHookSessionContext, - ) => Promise | void; - session_end: ( - event: PluginHookSessionEndEvent, - ctx: PluginHookSessionContext, - ) => Promise | void; - subagent_spawning: ( - event: PluginHookSubagentSpawningEvent, - ctx: PluginHookSubagentContext, - ) => Promise | PluginHookSubagentSpawningResult | void; - subagent_delivery_target: ( - event: PluginHookSubagentDeliveryTargetEvent, - ctx: PluginHookSubagentContext, - ) => - | Promise - | PluginHookSubagentDeliveryTargetResult - | void; - subagent_spawned: ( - event: PluginHookSubagentSpawnedEvent, - ctx: PluginHookSubagentContext, - ) => Promise | void; - subagent_ended: ( - event: PluginHookSubagentEndedEvent, - ctx: PluginHookSubagentContext, - ) => Promise | void; - gateway_start: ( - event: PluginHookGatewayStartEvent, - ctx: PluginHookGatewayContext, - ) => Promise | void; - gateway_stop: ( - event: PluginHookGatewayStopEvent, - ctx: PluginHookGatewayContext, - ) => Promise | void; - before_install: ( - event: PluginHookBeforeInstallEvent, - ctx: PluginHookBeforeInstallContext, - ) => Promise | PluginHookBeforeInstallResult | void; -}; - -export type PluginHookRegistration = { - pluginId: string; - hookName: K; - handler: PluginHookHandlerMap[K]; - priority?: number; - source: string; -}; +// Plugin hook contracts now live in hook-types.ts so hook runners can import a +// leaf contract surface instead of pulling the full plugin runtime barrel.