openclaw/src/plugins/types.ts

1423 lines
42 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from "node:http";
import type { TopLevelComponents } from "@buape/carbon";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Api, Model } from "@mariozechner/pi-ai";
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
import type { Command } from "commander";
import type {
ApiKeyCredential,
AuthProfileCredential,
OAuthCredential,
} from "../agents/auth-profiles/types.js";
import type { ProviderCapabilities } from "../agents/provider-capabilities.js";
import type { AnyAgentTool } from "../agents/tools/common.js";
import type { ThinkLevel } from "../auto-reply/thinking.js";
import type { ReplyPayload } from "../auto-reply/types.js";
import type { ChannelDock } from "../channels/dock.js";
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js";
import type { createVpsAwareOAuthHandlers } from "../commands/oauth-flow.js";
import type { OnboardOptions } from "../commands/onboard-types.js";
import type { OpenClawConfig } from "../config/config.js";
import type { ModelProviderConfig } from "../config/types.js";
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
import type { InternalHookHandler } from "../hooks/internal-hooks.js";
import type { HookEntry } from "../hooks/types.js";
import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import type { PluginRuntime } from "./runtime/types.js";
export type { PluginRuntime } from "./runtime/types.js";
export type { AnyAgentTool } from "../agents/tools/common.js";
export type PluginLogger = {
debug?: (message: string) => void;
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
};
export type PluginConfigUiHint = {
label?: string;
help?: string;
tags?: string[];
advanced?: boolean;
sensitive?: boolean;
placeholder?: string;
};
export type PluginKind = "memory" | "context-engine";
export type PluginConfigValidation =
| { ok: true; value?: unknown }
| { ok: false; errors: string[] };
export type OpenClawPluginConfigSchema = {
safeParse?: (value: unknown) => {
success: boolean;
data?: unknown;
error?: {
issues?: Array<{ path: Array<string | number>; message: string }>;
};
};
parse?: (value: unknown) => unknown;
validate?: (value: unknown) => PluginConfigValidation;
uiHints?: Record<string, PluginConfigUiHint>;
jsonSchema?: Record<string, unknown>;
};
export type OpenClawPluginToolContext = {
config?: OpenClawConfig;
workspaceDir?: string;
agentDir?: string;
agentId?: string;
sessionKey?: string;
/** Ephemeral session UUID — regenerated on /new and /reset. Use for per-conversation isolation. */
sessionId?: string;
messageChannel?: string;
agentAccountId?: string;
/** Trusted sender id from inbound context (runtime-provided, not tool args). */
requesterSenderId?: string;
/** Whether the trusted sender is an owner. */
senderIsOwner?: boolean;
sandboxed?: boolean;
};
export type OpenClawPluginToolFactory = (
ctx: OpenClawPluginToolContext,
) => AnyAgentTool | AnyAgentTool[] | null | undefined;
export type OpenClawPluginToolOptions = {
name?: string;
names?: string[];
optional?: boolean;
};
export type OpenClawPluginHookOptions = {
entry?: HookEntry;
name?: string;
description?: string;
register?: boolean;
};
export type ProviderAuthKind = "oauth" | "api_key" | "token" | "device_code" | "custom";
export type ProviderAuthResult = {
profiles: Array<{ profileId: string; credential: AuthProfileCredential }>;
configPatch?: Partial<OpenClawConfig>;
defaultModel?: string;
notes?: string[];
};
export type ProviderAuthContext = {
config: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
prompter: WizardPrompter;
runtime: RuntimeEnv;
isRemote: boolean;
openUrl: (url: string) => Promise<void>;
oauth: {
createVpsAwareHandlers: typeof createVpsAwareOAuthHandlers;
};
};
export type ProviderNonInteractiveApiKeyResult = {
key: string;
source: "profile" | "env" | "flag";
envVarName?: string;
};
export type ProviderResolveNonInteractiveApiKeyParams = {
provider: string;
flagValue?: string;
flagName: `--${string}`;
envVar: string;
envVarName?: string;
allowProfile?: boolean;
required?: boolean;
};
export type ProviderNonInteractiveApiKeyCredentialParams = {
provider: string;
resolved: ProviderNonInteractiveApiKeyResult;
email?: string;
metadata?: Record<string, string>;
};
export type ProviderAuthMethodNonInteractiveContext = {
authChoice: string;
config: OpenClawConfig;
baseConfig: OpenClawConfig;
opts: OnboardOptions;
runtime: RuntimeEnv;
agentDir?: string;
workspaceDir?: string;
resolveApiKey: (
params: ProviderResolveNonInteractiveApiKeyParams,
) => Promise<ProviderNonInteractiveApiKeyResult | null>;
toApiKeyCredential: (
params: ProviderNonInteractiveApiKeyCredentialParams,
) => ApiKeyCredential | null;
};
export type ProviderAuthMethod = {
id: string;
label: string;
hint?: string;
kind: ProviderAuthKind;
run: (ctx: ProviderAuthContext) => Promise<ProviderAuthResult>;
runNonInteractive?: (
ctx: ProviderAuthMethodNonInteractiveContext,
) => Promise<OpenClawConfig | null>;
};
export type ProviderCatalogOrder = "simple" | "profile" | "paired" | "late";
export type ProviderCatalogContext = {
config: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
resolveProviderApiKey: (providerId?: string) => {
apiKey: string | undefined;
discoveryApiKey?: string;
};
};
export type ProviderCatalogResult =
| { provider: ModelProviderConfig }
| { providers: Record<string, ModelProviderConfig> }
| null
| undefined;
export type ProviderPluginCatalog = {
order?: ProviderCatalogOrder;
run: (ctx: ProviderCatalogContext) => Promise<ProviderCatalogResult>;
};
/**
* Fully-resolved runtime model shape used by the embedded runner.
*
* Catalog hooks publish config-time `models.providers` entries.
* Runtime hooks below operate on the final `pi-ai` model object after
* discovery/override merging, just before inference runs.
*/
export type ProviderRuntimeModel = Model<Api>;
export type ProviderRuntimeProviderConfig = {
baseUrl?: string;
api?: ModelProviderConfig["api"];
models?: ModelProviderConfig["models"];
headers?: unknown;
};
/**
* Sync hook for provider-owned model ids that are not present in the local
* registry/catalog yet.
*
* Use this for pass-through providers or provider-specific forward-compat
* behavior. The hook should be cheap and side-effect free; async refreshes
* belong in `prepareDynamicModel`.
*/
export type ProviderResolveDynamicModelContext = {
config?: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
provider: string;
modelId: string;
modelRegistry: ModelRegistry;
providerConfig?: ProviderRuntimeProviderConfig;
};
/**
* Optional async warm-up for dynamic model resolution.
*
* Called only from async model resolution paths, before retrying
* `resolveDynamicModel`. This is the place to refresh caches or fetch provider
* metadata over the network.
*/
export type ProviderPrepareDynamicModelContext = ProviderResolveDynamicModelContext;
/**
* Last-chance rewrite hook for provider-owned transport normalization.
*
* Runs after OpenClaw resolves an explicit/discovered/dynamic model and before
* the embedded runner uses it. Typical uses: swap API ids, fix base URLs, or
* patch provider-specific compat bits.
*/
export type ProviderNormalizeResolvedModelContext = {
config?: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
provider: string;
modelId: string;
model: ProviderRuntimeModel;
};
/**
* Runtime auth input for providers that need an extra exchange step before
* inference. The incoming `apiKey` is the raw credential resolved from auth
* profiles/env/config. The returned value should be the actual token/key to use
* for the request.
*/
export type ProviderPrepareRuntimeAuthContext = {
config?: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
provider: string;
modelId: string;
model: ProviderRuntimeModel;
apiKey: string;
authMode: string;
profileId?: string;
};
/**
* Result of `prepareRuntimeAuth`.
*
* `apiKey` is required and becomes the runtime credential stored in auth
* storage. `baseUrl` is optional and lets providers like GitHub Copilot swap to
* an entitlement-specific endpoint at request time. `expiresAt` enables generic
* background refresh in long-running turns.
*/
export type ProviderPreparedRuntimeAuth = {
apiKey: string;
baseUrl?: string;
expiresAt?: number;
};
/**
* Provider-owned extra-param normalization before OpenClaw builds its generic
* stream option wrapper.
*
* Use this to set provider defaults or rewrite provider-specific config keys
* into the merged `extraParams` object. Return the full next extraParams object.
*/
export type ProviderPrepareExtraParamsContext = {
config?: OpenClawConfig;
agentDir?: string;
workspaceDir?: string;
provider: string;
modelId: string;
extraParams?: Record<string, unknown>;
thinkingLevel?: ThinkLevel;
};
/**
* Provider-owned stream wrapper hook after OpenClaw applies its generic
* transport-independent wrappers.
*
* Use this for provider-specific payload/header/model mutations that still run
* through the normal `pi-ai` stream path.
*/
export type ProviderWrapStreamFnContext = ProviderPrepareExtraParamsContext & {
streamFn?: StreamFn;
};
/**
* Provider-owned prompt-cache eligibility.
*
* Return `true` or `false` to override OpenClaw's built-in provider cache TTL
* detection for this provider. Return `undefined` to fall back to core rules.
*/
export type ProviderCacheTtlEligibilityContext = {
provider: string;
modelId: string;
};
/**
* @deprecated Use ProviderCatalogOrder.
*/
export type ProviderDiscoveryOrder = ProviderCatalogOrder;
/**
* @deprecated Use ProviderCatalogContext.
*/
export type ProviderDiscoveryContext = ProviderCatalogContext;
/**
* @deprecated Use ProviderCatalogResult.
*/
export type ProviderDiscoveryResult = ProviderCatalogResult;
/**
* @deprecated Use ProviderPluginCatalog.
*/
export type ProviderPluginDiscovery = ProviderPluginCatalog;
export type ProviderPluginWizardOnboarding = {
choiceId?: string;
choiceLabel?: string;
choiceHint?: string;
groupId?: string;
groupLabel?: string;
groupHint?: string;
methodId?: string;
};
export type ProviderPluginWizardModelPicker = {
label?: string;
hint?: string;
methodId?: string;
};
export type ProviderPluginWizard = {
onboarding?: ProviderPluginWizardOnboarding;
modelPicker?: ProviderPluginWizardModelPicker;
};
export type ProviderModelSelectedContext = {
config: OpenClawConfig;
model: string;
prompter: WizardPrompter;
agentDir?: string;
workspaceDir?: string;
};
export type ProviderPlugin = {
id: string;
pluginId?: string;
label: string;
docsPath?: string;
aliases?: string[];
envVars?: string[];
auth: ProviderAuthMethod[];
/**
* Preferred hook for plugin-defined provider catalogs.
* Returns provider config/model definitions that merge into models.providers.
*/
catalog?: ProviderPluginCatalog;
/**
* Legacy alias for catalog.
* Kept for compatibility with existing provider plugins.
*/
discovery?: ProviderPluginDiscovery;
/**
* Sync runtime fallback for model ids not present in the local catalog.
*
* Hook order:
* 1. discovered/static model lookup
* 2. plugin `resolveDynamicModel`
* 3. core fallback heuristics
* 4. generic provider-config fallback
*
* Keep this hook cheap and deterministic. If you need network I/O first, use
* `prepareDynamicModel` to prime state for the async retry path.
*/
resolveDynamicModel?: (
ctx: ProviderResolveDynamicModelContext,
) => ProviderRuntimeModel | null | undefined;
/**
* Optional async prefetch for dynamic model resolution.
*
* OpenClaw calls this only from async model resolution paths. After it
* completes, `resolveDynamicModel` is called again.
*/
prepareDynamicModel?: (ctx: ProviderPrepareDynamicModelContext) => Promise<void>;
/**
* Provider-owned transport normalization.
*
* Use this to rewrite a resolved model without forking the generic runner:
* swap API ids, update base URLs, or adjust compat flags for a provider's
* transport quirks.
*/
normalizeResolvedModel?: (
ctx: ProviderNormalizeResolvedModelContext,
) => ProviderRuntimeModel | null | undefined;
/**
* Static provider capability overrides consumed by shared transcript/tooling
* logic.
*
* Use this when the provider behaves like OpenAI/Anthropic, needs transcript
* sanitization quirks, or requires provider-family hints.
*/
capabilities?: Partial<ProviderCapabilities>;
/**
* Provider-owned extra-param normalization before generic stream option
* wrapping.
*
* Typical uses: set provider-default `transport`, map provider-specific
* config aliases, or inject extra request metadata sourced from
* `agents.defaults.models.<provider>/<model>.params`.
*/
prepareExtraParams?: (
ctx: ProviderPrepareExtraParamsContext,
) => Record<string, unknown> | null | undefined;
/**
* Provider-owned stream wrapper applied after generic OpenClaw wrappers.
*
* Typical uses: provider attribution headers, request-body rewrites, or
* provider-specific compat payload patches that do not justify a separate
* transport implementation.
*/
wrapStreamFn?: (ctx: ProviderWrapStreamFnContext) => StreamFn | null | undefined;
/**
* Runtime auth exchange hook.
*
* Called after OpenClaw resolves the raw configured credential but before the
* runner stores it in runtime auth storage. This lets plugins exchange a
* source credential (for example a GitHub token) into a short-lived runtime
* token plus optional base URL override.
*/
prepareRuntimeAuth?: (
ctx: ProviderPrepareRuntimeAuthContext,
) => Promise<ProviderPreparedRuntimeAuth | null | undefined>;
/**
* Provider-owned cache TTL eligibility.
*
* Use this when a proxy provider supports Anthropic-style prompt caching for
* only a subset of upstream models.
*/
isCacheTtlEligible?: (ctx: ProviderCacheTtlEligibilityContext) => boolean | undefined;
wizard?: ProviderPluginWizard;
formatApiKey?: (cred: AuthProfileCredential) => string;
refreshOAuth?: (cred: OAuthCredential) => Promise<OAuthCredential>;
onModelSelected?: (ctx: ProviderModelSelectedContext) => Promise<void>;
};
export type OpenClawPluginGatewayMethod = {
method: string;
handler: GatewayRequestHandler;
};
// =============================================================================
// Plugin Commands
// =============================================================================
/**
* Context passed to plugin command handlers.
*/
export type PluginCommandContext = {
/** The sender's identifier (e.g., Telegram user ID) */
senderId?: string;
/** The channel/surface (e.g., "telegram", "discord") */
channel: string;
/** Provider channel id (e.g., "telegram") */
channelId?: ChannelId;
/** Whether the sender is on the allowlist */
isAuthorizedSender: boolean;
/** Raw command arguments after the command name */
args?: string;
/** The full normalized command body */
commandBody: string;
/** Current OpenClaw configuration */
config: OpenClawConfig;
/** Raw "From" value (channel-scoped id) */
from?: string;
/** Raw "To" value (channel-scoped id) */
to?: string;
/** Account id for multi-account channels */
accountId?: string;
/** Thread/topic id if available */
messageThreadId?: number;
requestConversationBinding: (
params?: PluginConversationBindingRequestParams,
) => Promise<PluginConversationBindingRequestResult>;
detachConversationBinding: () => Promise<{ removed: boolean }>;
getCurrentConversationBinding: () => Promise<PluginConversationBinding | null>;
};
export type PluginConversationBindingRequestParams = {
summary?: string;
detachHint?: string;
};
export type PluginConversationBinding = {
bindingId: string;
pluginId: string;
pluginName?: string;
pluginRoot: string;
channel: string;
accountId: string;
conversationId: string;
parentConversationId?: string;
threadId?: string | number;
boundAt: number;
summary?: string;
detachHint?: string;
};
export type PluginConversationBindingRequestResult =
| {
status: "bound";
binding: PluginConversationBinding;
}
| {
status: "pending";
approvalId: string;
reply: ReplyPayload;
}
| {
status: "error";
message: string;
};
/**
* Result returned by a plugin command handler.
*/
export type PluginCommandResult = ReplyPayload;
/**
* Handler function for plugin commands.
*/
export type PluginCommandHandler = (
ctx: PluginCommandContext,
) => PluginCommandResult | Promise<PluginCommandResult>;
/**
* Definition for a plugin-registered command.
*/
export type OpenClawPluginCommandDefinition = {
/** Command name without leading slash (e.g., "tts") */
name: string;
/**
* Optional native-command aliases for slash/menu surfaces.
* `default` applies to all native providers unless a provider-specific
* override exists (for example `{ default: "talkvoice", discord: "voice2" }`).
*/
nativeNames?: Partial<Record<string, string>> & { default?: string };
/** Description shown in /help and command menus */
description: string;
/** Whether this command accepts arguments */
acceptsArgs?: boolean;
/** Whether only authorized senders can use this command (default: true) */
requireAuth?: boolean;
/** The handler function */
handler: PluginCommandHandler;
};
export type PluginInteractiveChannel = "telegram" | "discord";
export type PluginInteractiveButtons = Array<
Array<{ text: string; callback_data: string; style?: "danger" | "success" | "primary" }>
>;
export type PluginInteractiveTelegramHandlerResult = {
handled?: boolean;
} | void;
export type PluginInteractiveTelegramHandlerContext = {
channel: "telegram";
accountId: string;
callbackId: string;
conversationId: string;
parentConversationId?: string;
senderId?: string;
senderUsername?: string;
threadId?: number;
isGroup: boolean;
isForum: boolean;
auth: {
isAuthorizedSender: boolean;
};
callback: {
data: string;
namespace: string;
payload: string;
messageId: number;
chatId: string;
messageText?: string;
};
respond: {
reply: (params: { text: string; buttons?: PluginInteractiveButtons }) => Promise<void>;
editMessage: (params: { text: string; buttons?: PluginInteractiveButtons }) => Promise<void>;
editButtons: (params: { buttons: PluginInteractiveButtons }) => Promise<void>;
clearButtons: () => Promise<void>;
deleteMessage: () => Promise<void>;
};
requestConversationBinding: (
params?: PluginConversationBindingRequestParams,
) => Promise<PluginConversationBindingRequestResult>;
detachConversationBinding: () => Promise<{ removed: boolean }>;
getCurrentConversationBinding: () => Promise<PluginConversationBinding | null>;
};
export type PluginInteractiveDiscordHandlerResult = {
handled?: boolean;
} | void;
export type PluginInteractiveDiscordHandlerContext = {
channel: "discord";
accountId: string;
interactionId: string;
conversationId: string;
parentConversationId?: string;
guildId?: string;
senderId?: string;
senderUsername?: string;
auth: {
isAuthorizedSender: boolean;
};
interaction: {
kind: "button" | "select" | "modal";
data: string;
namespace: string;
payload: string;
messageId?: string;
values?: string[];
fields?: Array<{ id: string; name: string; values: string[] }>;
};
respond: {
acknowledge: () => Promise<void>;
reply: (params: { text: string; ephemeral?: boolean }) => Promise<void>;
followUp: (params: { text: string; ephemeral?: boolean }) => Promise<void>;
editMessage: (params: { text?: string; components?: TopLevelComponents[] }) => Promise<void>;
clearComponents: (params?: { text?: string }) => Promise<void>;
};
requestConversationBinding: (
params?: PluginConversationBindingRequestParams,
) => Promise<PluginConversationBindingRequestResult>;
detachConversationBinding: () => Promise<{ removed: boolean }>;
getCurrentConversationBinding: () => Promise<PluginConversationBinding | null>;
};
export type PluginInteractiveTelegramHandlerRegistration = {
channel: "telegram";
namespace: string;
handler: (
ctx: PluginInteractiveTelegramHandlerContext,
) => Promise<PluginInteractiveTelegramHandlerResult> | PluginInteractiveTelegramHandlerResult;
};
export type PluginInteractiveDiscordHandlerRegistration = {
channel: "discord";
namespace: string;
handler: (
ctx: PluginInteractiveDiscordHandlerContext,
) => Promise<PluginInteractiveDiscordHandlerResult> | PluginInteractiveDiscordHandlerResult;
};
export type PluginInteractiveHandlerRegistration =
| PluginInteractiveTelegramHandlerRegistration
| PluginInteractiveDiscordHandlerRegistration;
export type OpenClawPluginHttpRouteAuth = "gateway" | "plugin";
export type OpenClawPluginHttpRouteMatch = "exact" | "prefix";
export type OpenClawPluginHttpRouteHandler = (
req: IncomingMessage,
res: ServerResponse,
) => Promise<boolean | void> | boolean | void;
export type OpenClawPluginHttpRouteParams = {
path: string;
handler: OpenClawPluginHttpRouteHandler;
auth: OpenClawPluginHttpRouteAuth;
match?: OpenClawPluginHttpRouteMatch;
replaceExisting?: boolean;
};
export type OpenClawPluginCliContext = {
program: Command;
config: OpenClawConfig;
workspaceDir?: string;
logger: PluginLogger;
};
export type OpenClawPluginCliRegistrar = (ctx: OpenClawPluginCliContext) => void | Promise<void>;
export type OpenClawPluginServiceContext = {
config: OpenClawConfig;
workspaceDir?: string;
stateDir: string;
logger: PluginLogger;
};
export type OpenClawPluginService = {
id: string;
start: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
stop?: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
};
export type OpenClawPluginChannelRegistration = {
plugin: ChannelPlugin;
dock?: ChannelDock;
};
export type OpenClawPluginDefinition = {
id?: string;
name?: string;
description?: string;
version?: string;
kind?: PluginKind;
configSchema?: OpenClawPluginConfigSchema;
register?: (api: OpenClawPluginApi) => void | Promise<void>;
activate?: (api: OpenClawPluginApi) => void | Promise<void>;
};
export type OpenClawPluginModule =
| OpenClawPluginDefinition
| ((api: OpenClawPluginApi) => void | Promise<void>);
export type OpenClawPluginApi = {
id: string;
name: string;
version?: string;
description?: string;
source: string;
rootDir?: string;
config: OpenClawConfig;
pluginConfig?: Record<string, unknown>;
runtime: PluginRuntime;
logger: PluginLogger;
registerTool: (
tool: AnyAgentTool | OpenClawPluginToolFactory,
opts?: OpenClawPluginToolOptions,
) => void;
registerHook: (
events: string | string[],
handler: InternalHookHandler,
opts?: OpenClawPluginHookOptions,
) => void;
registerHttpRoute: (params: OpenClawPluginHttpRouteParams) => void;
registerChannel: (registration: OpenClawPluginChannelRegistration | ChannelPlugin) => void;
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
registerCli: (registrar: OpenClawPluginCliRegistrar, opts?: { commands?: string[] }) => void;
registerService: (service: OpenClawPluginService) => void;
registerProvider: (provider: ProviderPlugin) => void;
registerInteractiveHandler: (registration: PluginInteractiveHandlerRegistration) => void;
/**
* Register a custom command that bypasses the LLM agent.
* Plugin commands are processed before built-in commands and before agent invocation.
* Use this for simple state-toggling or status commands that don't need AI reasoning.
*/
registerCommand: (command: OpenClawPluginCommandDefinition) => void;
/** Register a context engine implementation (exclusive slot — only one active at a time). */
registerContextEngine: (
id: string,
factory: import("../context-engine/registry.js").ContextEngineFactory,
) => void;
resolvePath: (input: string) => string;
/** Register a lifecycle hook handler */
on: <K extends PluginHookName>(
hookName: K,
handler: PluginHookHandlerMap[K],
opts?: { priority?: number },
) => void;
};
export type PluginOrigin = "bundled" | "global" | "workspace" | "config";
export type PluginDiagnostic = {
level: "warn" | "error";
message: string;
pluginId?: string;
source?: string;
};
// ============================================================================
// Plugin Hooks
// ============================================================================
export type PluginHookName =
| "before_model_resolve"
| "before_prompt_build"
| "before_agent_start"
| "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";
export const PLUGIN_HOOK_NAMES = [
"before_model_resolve",
"before_prompt_build",
"before_agent_start",
"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",
] 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 = {
agentId?: string;
sessionKey?: string;
sessionId?: string;
workspaceDir?: 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_model_resolve hook
export type PluginHookBeforeModelResolveEvent = {
/** User prompt for this run. No session messages are available yet in this phase. */
prompt: string;
};
export type PluginHookBeforeModelResolveResult = {
/** Override the model for this agent run. E.g. "llama3.3:8b" */
modelOverride?: string;
/** Override the provider for this agent run. E.g. "ollama" */
providerOverride?: string;
};
// before_prompt_build hook
export type PluginHookBeforePromptBuildEvent = {
prompt: string;
/** Session messages prepared for this run. */
messages: unknown[];
};
export type PluginHookBeforePromptBuildResult = {
systemPrompt?: string;
prependContext?: string;
/**
* Prepended to the agent system prompt so providers can cache it (e.g. prompt caching).
* Use for static plugin guidance instead of prependContext to avoid per-turn token cost.
*/
prependSystemContext?: string;
/**
* Appended to the agent system prompt so providers can cache it (e.g. prompt caching).
* Use for static plugin guidance instead of prependContext to avoid per-turn token cost.
*/
appendSystemContext?: string;
};
export const PLUGIN_PROMPT_MUTATION_RESULT_FIELDS = [
"systemPrompt",
"prependContext",
"prependSystemContext",
"appendSystemContext",
] as const satisfies readonly (keyof PluginHookBeforePromptBuildResult)[];
type MissingPluginPromptMutationResultFields = Exclude<
keyof PluginHookBeforePromptBuildResult,
(typeof PLUGIN_PROMPT_MUTATION_RESULT_FIELDS)[number]
>;
type AssertAllPluginPromptMutationResultFieldsListed =
MissingPluginPromptMutationResultFields extends never ? true : never;
const assertAllPluginPromptMutationResultFieldsListed: AssertAllPluginPromptMutationResultFieldsListed = true;
void assertAllPluginPromptMutationResultFieldsListed;
// before_agent_start hook (legacy compatibility: combines both phases)
export type PluginHookBeforeAgentStartEvent = {
prompt: string;
/** Optional because legacy hook can run in pre-session phase. */
messages?: unknown[];
};
export type PluginHookBeforeAgentStartResult = PluginHookBeforePromptBuildResult &
PluginHookBeforeModelResolveResult;
export type PluginHookBeforeAgentStartOverrideResult = Omit<
PluginHookBeforeAgentStartResult,
keyof PluginHookBeforePromptBuildResult
>;
export const stripPromptMutationFieldsFromLegacyHookResult = (
result: PluginHookBeforeAgentStartResult | void,
): PluginHookBeforeAgentStartOverrideResult | void => {
if (!result || typeof result !== "object") {
return result;
}
const remaining: Partial<PluginHookBeforeAgentStartResult> = { ...result };
for (const field of PLUGIN_PROMPT_MUTATION_RESULT_FIELDS) {
delete remaining[field];
}
return Object.keys(remaining).length > 0
? (remaining as PluginHookBeforeAgentStartOverrideResult)
: undefined;
};
// 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;
};
// Message context
export type PluginHookMessageContext = {
channelId: string;
accountId?: string;
conversationId?: string;
};
export type PluginHookInboundClaimContext = PluginHookMessageContext & {
parentConversationId?: string;
senderId?: string;
messageId?: string;
};
export type PluginHookInboundClaimEvent = {
content: string;
body?: string;
bodyForAgent?: string;
transcript?: string;
timestamp?: number;
channel: string;
accountId?: string;
conversationId?: string;
parentConversationId?: string;
senderId?: string;
senderName?: string;
senderUsername?: string;
threadId?: string | number;
messageId?: string;
isGroup: boolean;
commandAuthorized?: boolean;
wasMentioned?: boolean;
metadata?: Record<string, unknown>;
};
export type PluginHookInboundClaimResult = {
handled: boolean;
};
// message_received hook
export type PluginHookMessageReceivedEvent = {
from: string;
content: string;
timestamp?: number;
metadata?: Record<string, unknown>;
};
// message_sending hook
export type PluginHookMessageSendingEvent = {
to: string;
content: string;
metadata?: Record<string, unknown>;
};
export type PluginHookMessageSendingResult = {
content?: string;
cancel?: boolean;
};
// message_sent hook
export type PluginHookMessageSentEvent = {
to: string;
content: string;
success: boolean;
error?: string;
};
// 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 type PluginHookBeforeToolCallResult = {
params?: Record<string, unknown>;
block?: boolean;
blockReason?: string;
};
// 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 PluginHookSessionEndEvent = {
sessionId: string;
sessionKey?: string;
messageCount: number;
durationMs?: number;
};
// 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;
};
// 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;
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;
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;
};
export type PluginHookRegistration<K extends PluginHookName = PluginHookName> = {
pluginId: string;
hookName: K;
handler: PluginHookHandlerMap[K];
priority?: number;
source: string;
};