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; message: string }>; }; }; parse?: (value: unknown) => unknown; validate?: (value: unknown) => PluginConfigValidation; uiHints?: Record; jsonSchema?: Record; }; 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; defaultModel?: string; notes?: string[]; }; export type ProviderAuthContext = { config: OpenClawConfig; agentDir?: string; workspaceDir?: string; prompter: WizardPrompter; runtime: RuntimeEnv; isRemote: boolean; openUrl: (url: string) => Promise; 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; }; export type ProviderAuthMethodNonInteractiveContext = { authChoice: string; config: OpenClawConfig; baseConfig: OpenClawConfig; opts: OnboardOptions; runtime: RuntimeEnv; agentDir?: string; workspaceDir?: string; resolveApiKey: ( params: ProviderResolveNonInteractiveApiKeyParams, ) => Promise; toApiKeyCredential: ( params: ProviderNonInteractiveApiKeyCredentialParams, ) => ApiKeyCredential | null; }; export type ProviderAuthMethod = { id: string; label: string; hint?: string; kind: ProviderAuthKind; run: (ctx: ProviderAuthContext) => Promise; runNonInteractive?: ( ctx: ProviderAuthMethodNonInteractiveContext, ) => Promise; }; 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 } | null | undefined; export type ProviderPluginCatalog = { order?: ProviderCatalogOrder; run: (ctx: ProviderCatalogContext) => Promise; }; /** * 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; 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; 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; /** * 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; /** * 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./.params`. */ prepareExtraParams?: ( ctx: ProviderPrepareExtraParamsContext, ) => Record | 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; /** * 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; onModelSelected?: (ctx: ProviderModelSelectedContext) => Promise; }; 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; detachConversationBinding: () => Promise<{ removed: boolean }>; getCurrentConversationBinding: () => Promise; }; 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; /** * 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> & { 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; editMessage: (params: { text: string; buttons?: PluginInteractiveButtons }) => Promise; editButtons: (params: { buttons: PluginInteractiveButtons }) => Promise; clearButtons: () => Promise; deleteMessage: () => Promise; }; requestConversationBinding: ( params?: PluginConversationBindingRequestParams, ) => Promise; detachConversationBinding: () => Promise<{ removed: boolean }>; getCurrentConversationBinding: () => Promise; }; 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; reply: (params: { text: string; ephemeral?: boolean }) => Promise; followUp: (params: { text: string; ephemeral?: boolean }) => Promise; editMessage: (params: { text?: string; components?: TopLevelComponents[] }) => Promise; clearComponents: (params?: { text?: string }) => Promise; }; requestConversationBinding: ( params?: PluginConversationBindingRequestParams, ) => Promise; detachConversationBinding: () => Promise<{ removed: boolean }>; getCurrentConversationBinding: () => Promise; }; export type PluginInteractiveTelegramHandlerRegistration = { channel: "telegram"; namespace: string; handler: ( ctx: PluginInteractiveTelegramHandlerContext, ) => Promise | PluginInteractiveTelegramHandlerResult; }; export type PluginInteractiveDiscordHandlerRegistration = { channel: "discord"; namespace: string; handler: ( ctx: PluginInteractiveDiscordHandlerContext, ) => Promise | 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; 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; export type OpenClawPluginServiceContext = { config: OpenClawConfig; workspaceDir?: string; stateDir: string; logger: PluginLogger; }; export type OpenClawPluginService = { id: string; start: (ctx: OpenClawPluginServiceContext) => void | Promise; stop?: (ctx: OpenClawPluginServiceContext) => void | Promise; }; 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; activate?: (api: OpenClawPluginApi) => void | Promise; }; export type OpenClawPluginModule = | OpenClawPluginDefinition | ((api: OpenClawPluginApi) => void | Promise); export type OpenClawPluginApi = { id: string; name: string; version?: string; description?: string; source: string; rootDir?: string; config: OpenClawConfig; pluginConfig?: Record; 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: ( 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; 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 = { 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 = { ...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; }; export type PluginHookInboundClaimResult = { handled: boolean; }; // message_received hook export type PluginHookMessageReceivedEvent = { from: string; content: string; timestamp?: number; metadata?: Record; }; // message_sending hook export type PluginHookMessageSendingEvent = { to: string; content: string; metadata?: Record; }; 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; /** Stable run identifier for this agent invocation. */ runId?: string; /** Provider-specific tool call ID when available. */ toolCallId?: string; }; export type PluginHookBeforeToolCallResult = { params?: Record; block?: boolean; blockReason?: string; }; // 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 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; before_prompt_build: ( event: PluginHookBeforePromptBuildEvent, ctx: PluginHookAgentContext, ) => Promise | PluginHookBeforePromptBuildResult | void; before_agent_start: ( event: PluginHookBeforeAgentStartEvent, ctx: PluginHookAgentContext, ) => Promise | PluginHookBeforeAgentStartResult | 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; 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; }; export type PluginHookRegistration = { pluginId: string; hookName: K; handler: PluginHookHandlerMap[K]; priority?: number; source: string; };