fix(plugins): split hook contract types

This commit is contained in:
Vincent Koc
2026-04-11 18:06:18 +01:00
parent 4a799e77d7
commit 7f5a5a34db
5 changed files with 700 additions and 788 deletions

View File

@@ -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;

View File

@@ -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;

693
src/plugins/hook-types.ts Normal file
View File

@@ -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<PluginHookName, (typeof PLUGIN_HOOK_NAMES)[number]>;
type AssertAllPluginHookNamesListed = MissingPluginHookNames extends never ? true : never;
const assertAllPluginHookNamesListed: AssertAllPluginHookNamesListed = true;
void assertAllPluginHookNamesListed;
const pluginHookNameSet = new Set<PluginHookName>(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<PluginHookName>(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> | 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<ReplyDispatchKind, number>;
};
export type PluginHookToolContext = {
agentId?: string;
sessionKey?: string;
sessionId?: string;
runId?: string;
toolName: string;
toolCallId?: string;
};
export type PluginHookBeforeToolCallEvent = {
toolName: string;
params: Record<string, unknown>;
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<string, unknown>;
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> | void;
};
};
export type PluginHookAfterToolCallEvent = {
toolName: string;
params: Record<string, unknown>;
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>
| PluginHookBeforeModelResolveResult
| void;
before_prompt_build: (
event: PluginHookBeforePromptBuildEvent,
ctx: PluginHookAgentContext,
) => Promise<PluginHookBeforePromptBuildResult | void> | PluginHookBeforePromptBuildResult | void;
before_agent_start: (
event: PluginHookBeforeAgentStartEvent,
ctx: PluginHookAgentContext,
) => Promise<PluginHookBeforeAgentStartResult | void> | PluginHookBeforeAgentStartResult | void;
before_agent_reply: (
event: PluginHookBeforeAgentReplyEvent,
ctx: PluginHookAgentContext,
) => Promise<PluginHookBeforeAgentReplyResult | void> | PluginHookBeforeAgentReplyResult | void;
llm_input: (event: PluginHookLlmInputEvent, ctx: PluginHookAgentContext) => Promise<void> | void;
llm_output: (
event: PluginHookLlmOutputEvent,
ctx: PluginHookAgentContext,
) => Promise<void> | void;
agent_end: (event: PluginHookAgentEndEvent, ctx: PluginHookAgentContext) => Promise<void> | void;
before_compaction: (
event: PluginHookBeforeCompactionEvent,
ctx: PluginHookAgentContext,
) => Promise<void> | void;
after_compaction: (
event: PluginHookAfterCompactionEvent,
ctx: PluginHookAgentContext,
) => Promise<void> | void;
before_reset: (
event: PluginHookBeforeResetEvent,
ctx: PluginHookAgentContext,
) => Promise<void> | void;
inbound_claim: (
event: PluginHookInboundClaimEvent,
ctx: PluginHookInboundClaimContext,
) => Promise<PluginHookInboundClaimResult | void> | PluginHookInboundClaimResult | void;
before_dispatch: (
event: PluginHookBeforeDispatchEvent,
ctx: PluginHookBeforeDispatchContext,
) => Promise<PluginHookBeforeDispatchResult | void> | PluginHookBeforeDispatchResult | void;
reply_dispatch: (
event: PluginHookReplyDispatchEvent,
ctx: PluginHookReplyDispatchContext,
) => Promise<PluginHookReplyDispatchResult | void> | PluginHookReplyDispatchResult | void;
message_received: (
event: PluginHookMessageReceivedEvent,
ctx: PluginHookMessageContext,
) => Promise<void> | void;
message_sending: (
event: PluginHookMessageSendingEvent,
ctx: PluginHookMessageContext,
) => Promise<PluginHookMessageSendingResult | void> | PluginHookMessageSendingResult | void;
message_sent: (
event: PluginHookMessageSentEvent,
ctx: PluginHookMessageContext,
) => Promise<void> | void;
before_tool_call: (
event: PluginHookBeforeToolCallEvent,
ctx: PluginHookToolContext,
) => Promise<PluginHookBeforeToolCallResult | void> | PluginHookBeforeToolCallResult | void;
after_tool_call: (
event: PluginHookAfterToolCallEvent,
ctx: PluginHookToolContext,
) => Promise<void> | 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> | void;
session_end: (
event: PluginHookSessionEndEvent,
ctx: PluginHookSessionContext,
) => Promise<void> | void;
subagent_spawning: (
event: PluginHookSubagentSpawningEvent,
ctx: PluginHookSubagentContext,
) => Promise<PluginHookSubagentSpawningResult | void> | PluginHookSubagentSpawningResult | void;
subagent_delivery_target: (
event: PluginHookSubagentDeliveryTargetEvent,
ctx: PluginHookSubagentContext,
) =>
| Promise<PluginHookSubagentDeliveryTargetResult | void>
| PluginHookSubagentDeliveryTargetResult
| void;
subagent_spawned: (
event: PluginHookSubagentSpawnedEvent,
ctx: PluginHookSubagentContext,
) => Promise<void> | void;
subagent_ended: (
event: PluginHookSubagentEndedEvent,
ctx: PluginHookSubagentContext,
) => Promise<void> | void;
gateway_start: (
event: PluginHookGatewayStartEvent,
ctx: PluginHookGatewayContext,
) => Promise<void> | void;
gateway_stop: (
event: PluginHookGatewayStopEvent,
ctx: PluginHookGatewayContext,
) => Promise<void> | void;
before_install: (
event: PluginHookBeforeInstallEvent,
ctx: PluginHookBeforeInstallContext,
) => Promise<PluginHookBeforeInstallResult | void> | PluginHookBeforeInstallResult | void;
};
export type PluginHookRegistration<K extends PluginHookName = PluginHookName> = {
pluginId: string;
hookName: K;
handler: PluginHookHandlerMap[K];
priority?: number;
source: string;
};

View File

@@ -65,7 +65,7 @@ import type {
PluginHookBeforeInstallContext,
PluginHookBeforeInstallEvent,
PluginHookBeforeInstallResult,
} from "./types.js";
} from "./hook-types.js";
// Re-export types for consumers
export type {

View File

@@ -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<PluginHookName, (typeof PLUGIN_HOOK_NAMES)[number]>;
type AssertAllPluginHookNamesListed = MissingPluginHookNames extends never ? true : never;
const assertAllPluginHookNamesListed: AssertAllPluginHookNamesListed = true;
void assertAllPluginHookNamesListed;
const pluginHookNameSet = new Set<PluginHookName>(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<PluginHookName>(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> | 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<ReplyDispatchKind, number>;
};
// 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<string, unknown>;
/** 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<string, unknown>;
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> | void;
};
};
// after_tool_call hook
export type PluginHookAfterToolCallEvent = {
toolName: string;
params: Record<string, unknown>;
/** 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>
| PluginHookBeforeModelResolveResult
| void;
before_prompt_build: (
event: PluginHookBeforePromptBuildEvent,
ctx: PluginHookAgentContext,
) => Promise<PluginHookBeforePromptBuildResult | void> | PluginHookBeforePromptBuildResult | void;
before_agent_start: (
event: PluginHookBeforeAgentStartEvent,
ctx: PluginHookAgentContext,
) => Promise<PluginHookBeforeAgentStartResult | void> | PluginHookBeforeAgentStartResult | void;
before_agent_reply: (
event: PluginHookBeforeAgentReplyEvent,
ctx: PluginHookAgentContext,
) => Promise<PluginHookBeforeAgentReplyResult | void> | PluginHookBeforeAgentReplyResult | void;
llm_input: (event: PluginHookLlmInputEvent, ctx: PluginHookAgentContext) => Promise<void> | void;
llm_output: (
event: PluginHookLlmOutputEvent,
ctx: PluginHookAgentContext,
) => Promise<void> | void;
agent_end: (event: PluginHookAgentEndEvent, ctx: PluginHookAgentContext) => Promise<void> | void;
before_compaction: (
event: PluginHookBeforeCompactionEvent,
ctx: PluginHookAgentContext,
) => Promise<void> | void;
after_compaction: (
event: PluginHookAfterCompactionEvent,
ctx: PluginHookAgentContext,
) => Promise<void> | void;
before_reset: (
event: PluginHookBeforeResetEvent,
ctx: PluginHookAgentContext,
) => Promise<void> | void;
inbound_claim: (
event: PluginHookInboundClaimEvent,
ctx: PluginHookInboundClaimContext,
) => Promise<PluginHookInboundClaimResult | void> | PluginHookInboundClaimResult | void;
before_dispatch: (
event: PluginHookBeforeDispatchEvent,
ctx: PluginHookBeforeDispatchContext,
) => Promise<PluginHookBeforeDispatchResult | void> | PluginHookBeforeDispatchResult | void;
reply_dispatch: (
event: PluginHookReplyDispatchEvent,
ctx: PluginHookReplyDispatchContext,
) => Promise<PluginHookReplyDispatchResult | void> | PluginHookReplyDispatchResult | void;
message_received: (
event: PluginHookMessageReceivedEvent,
ctx: PluginHookMessageContext,
) => Promise<void> | void;
message_sending: (
event: PluginHookMessageSendingEvent,
ctx: PluginHookMessageContext,
) => Promise<PluginHookMessageSendingResult | void> | PluginHookMessageSendingResult | void;
message_sent: (
event: PluginHookMessageSentEvent,
ctx: PluginHookMessageContext,
) => Promise<void> | void;
before_tool_call: (
event: PluginHookBeforeToolCallEvent,
ctx: PluginHookToolContext,
) => Promise<PluginHookBeforeToolCallResult | void> | PluginHookBeforeToolCallResult | void;
after_tool_call: (
event: PluginHookAfterToolCallEvent,
ctx: PluginHookToolContext,
) => Promise<void> | 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> | void;
session_end: (
event: PluginHookSessionEndEvent,
ctx: PluginHookSessionContext,
) => Promise<void> | void;
subagent_spawning: (
event: PluginHookSubagentSpawningEvent,
ctx: PluginHookSubagentContext,
) => Promise<PluginHookSubagentSpawningResult | void> | PluginHookSubagentSpawningResult | void;
subagent_delivery_target: (
event: PluginHookSubagentDeliveryTargetEvent,
ctx: PluginHookSubagentContext,
) =>
| Promise<PluginHookSubagentDeliveryTargetResult | void>
| PluginHookSubagentDeliveryTargetResult
| void;
subagent_spawned: (
event: PluginHookSubagentSpawnedEvent,
ctx: PluginHookSubagentContext,
) => Promise<void> | void;
subagent_ended: (
event: PluginHookSubagentEndedEvent,
ctx: PluginHookSubagentContext,
) => Promise<void> | void;
gateway_start: (
event: PluginHookGatewayStartEvent,
ctx: PluginHookGatewayContext,
) => Promise<void> | void;
gateway_stop: (
event: PluginHookGatewayStopEvent,
ctx: PluginHookGatewayContext,
) => Promise<void> | void;
before_install: (
event: PluginHookBeforeInstallEvent,
ctx: PluginHookBeforeInstallContext,
) => Promise<PluginHookBeforeInstallResult | void> | PluginHookBeforeInstallResult | void;
};
export type PluginHookRegistration<K extends PluginHookName = PluginHookName> = {
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.