import type { ChatMessage, ToolResult } from "../agent/types.js";
import type { ToolContract } from "../agent/tool-contract.js";
* 插件日志器:所有输出自动带 [plugin:<id>] 前缀,便于在 server 日志里区分来源。
*/
export type PluginLogger = {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
};
export type PluginToolContext = {
workspace: string;
signal?: AbortSignal;
logger: PluginLogger;
};
* 插件注册的自定义工具。parameters 为 JSON Schema(OpenAI function-calling 格式的
* parameters 字段),execute 抛出的异常由 executeTool 外层兜底 catch 并转为失败结果。
*/
export type PluginTool = {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, unknown>;
required?: string[];
additionalProperties?: boolean;
};
execute: (input: Record<string, unknown>, ctx: PluginToolContext) => Promise<ToolResult>;
requiresApproval?: boolean;
contract?: ToolContract;
};
export type PluginHookName =
| "tool_call"
| "tool_result"
| "prompt_submit"
| "permission_request"
| "turn_start"
| "turn_stop"
| "turn_end"
| "session_start"
| "session_end"
| "session_info_changed"
| "model_switch"
| "subagent_start"
| "subagent_stop"
| "compaction_start"
| "compaction_end"
| "interrupt"
| "tool_execution_start"
| "tool_execution_end"
| "assistant_message_start"
| "assistant_message_end"
| "context_inject"
| "before_provider_request";
export type PluginHookScope = {
rootSessionId?: string;
agentId?: string;
agentType?: string;
parentSessionId?: string;
};
export type ToolCallHookEvent = PluginHookScope & {
hook: "tool_call";
sessionId: string;
callId: string;
tool: string;
input: Record<string, unknown>;
};
* block=true → 工具不执行,reason 作为工具结果返回给模型;
* permission="ask" → 即使自动审批/full 模式也强制人工确认;
* permission="allow" → 不预批准(调用方忽略);input → 替换实际执行参数(链式传递)。
*/
export type ToolCallHookResult = {
block?: boolean;
reason?: string;
permission?: "allow" | "ask" | "deny";
input?: Record<string, unknown>;
};
export type ToolResultHookEvent = PluginHookScope & {
hook: "tool_result";
sessionId: string;
callId: string;
tool: string;
input: Record<string, unknown>;
output: string;
ok: boolean;
truncated?: boolean;
};
export type ToolResultHookResult = {
output?: string;
ok?: boolean;
};
export type PromptSubmitHookEvent = PluginHookScope & {
hook: "prompt_submit";
sessionId: string;
text: string;
};
export type PromptSubmitHookResult = {
block?: boolean;
reason?: string;
text?: string;
};
* 审批弹窗出现前的自动应答出口(CC PermissionRequest 语义):
* allow → 关闭弹窗并放行;deny → 关闭弹窗并拒绝。没有决定则照常等用户。
*/
export type PermissionRequestHookEvent = PluginHookScope & {
hook: "permission_request";
sessionId: string;
callId: string;
tool: string;
input: Record<string, unknown>;
};
export type PermissionRequestHookResult = {
permission?: "allow" | "deny";
reason?: string;
};
export type TurnStartHookEvent = {
hook: "turn_start";
sessionId: string;
turnId: string;
text: string;
};
* 回合收尾闸(Claude Code Stop hook 语义):模型没有工具调用、准备结束回合时触发。
* block=true → reason 作为新的 user 消息注入,循环继续(模型接着干活);
* 不 block → 回合正常收尾。stopHookActive 对应 CC 的 stop_hook_active:
* 已是"拦截后续跑"的一次,脚本可自行避让防止无限循环(循环侧也有次数上限兜底)。
*/
export type TurnStopHookEvent = PluginHookScope & {
hook: "turn_stop";
sessionId: string;
turnId: string;
stopHookActive: boolean;
text: string;
};
export type TurnStopHookResult = {
block?: boolean;
reason?: string;
};
export type TurnEndHookEvent = {
hook: "turn_end";
sessionId: string;
turnId: string;
status: "completed" | "aborted" | "error" | "interrupted";
error?: string;
};
export type SessionStartHookEvent = PluginHookScope & {
hook: "session_start";
sessionId: string;
cwd: string;
};
export type SessionEndHookEvent = PluginHookScope & {
hook: "session_end";
sessionId: string;
cwd?: string;
reason: "switch" | "shutdown" | "delete" | "subagent_close";
};
export type SessionInfoChangedHookEvent = PluginHookScope & {
hook: "session_info_changed";
sessionId: string;
title: string;
};
export type ModelSwitchHookEvent = PluginHookScope & {
hook: "model_switch";
sessionId: string;
from: { providerId?: string; model?: string };
to: { providerId?: string; model?: string };
source: "user" | "fallback" | "restore";
};
export type SubagentStartHookEvent = PluginHookScope & {
hook: "subagent_start";
sessionId: string;
agentId: string;
agentType?: string;
description: string;
background?: boolean;
prompt?: string;
};
export type SubagentStopHookEvent = PluginHookScope & {
hook: "subagent_stop";
sessionId: string;
agentId: string;
agentType?: string;
description?: string;
status: "completed" | "failed" | "aborted";
error?: string;
durationMs?: number;
};
export type CompactionStartHookEvent = PluginHookScope & {
hook: "compaction_start";
sessionId: string;
compactionId: string;
reason: "manual" | "overflow" | "threshold" | "warmup";
beforeTokens?: number;
messageCount?: number;
};
export type CompactionEndHookEvent = PluginHookScope & {
hook: "compaction_end";
sessionId: string;
compactionId: string;
status: "completed" | "failed" | "skipped";
summary?: string;
afterTokens?: number;
};
export type InterruptHookEvent = PluginHookScope & {
hook: "interrupt";
sessionId: string;
reason: "user_abort" | "steer" | "shutdown" | "subagent_abort";
source?: "user" | "system";
turnId?: string;
};
* 工具「真的开始执行」。
* 与 tool_call 的区别:tool_call 在决策前(可能被 block / 门禁 / 计划模式拦下),
* 本事件只在 executeTool 真正被调用时触发 —— 两者数量不等即为被拦下的调用。
*/
export type ToolExecutionStartHookEvent = PluginHookScope & {
hook: "tool_execution_start";
sessionId: string;
callId: string;
tool: string;
input: Record<string, unknown>;
parallel?: boolean;
startedAt: number;
};
export type ToolExecutionEndHookEvent = PluginHookScope & {
hook: "tool_execution_end";
sessionId: string;
callId: string;
tool: string;
input: Record<string, unknown>;
output: string;
ok: boolean;
durationMs: number;
error?: string;
};
export type AssistantMessageStartHookEvent = PluginHookScope & {
hook: "assistant_message_start";
sessionId: string;
turnId?: string;
roundId?: string;
};
export type AssistantMessageEndHookEvent = PluginHookScope & {
hook: "assistant_message_end";
sessionId: string;
turnId?: string;
text: string;
toolCallCount: number;
};
* 往上下文注入内容(缓存安全):返回的每条 notes 作为 context_note 追加到历史尾部,
* 不进入系统提示、不改请求前缀,因此不会让 prompt cache 整段失效。
*/
export type ContextInjectHookEvent = PluginHookScope & {
hook: "context_inject";
sessionId: string;
turnId: string;
text: string;
phase: "turn_start";
};
export type ContextInjectHookResult = {
notes?: string[];
};
* 请求发出前的语义层改写:改的是「将要发给模型的消息与工具」,不是各协议最终 JSON。
* 注意:改写 messages 前缀会让 prompt cache 整段失效,只有在确实必要时才动。
*/
export type BeforeProviderRequestHookEvent = PluginHookScope & {
hook: "before_provider_request";
sessionId: string;
providerId: string;
model: string;
protocol: string;
messages: ChatMessage[];
toolNames: string[];
maxTokens?: number;
cacheBreakpoints?: number;
};
export type BeforeProviderRequestHookResult = {
messages?: ChatMessage[];
tools?: unknown[];
extraBody?: Record<string, unknown>;
};
export type PluginHookEventMap = {
tool_call: ToolCallHookEvent;
tool_result: ToolResultHookEvent;
prompt_submit: PromptSubmitHookEvent;
permission_request: PermissionRequestHookEvent;
turn_start: TurnStartHookEvent;
turn_stop: TurnStopHookEvent;
turn_end: TurnEndHookEvent;
session_start: SessionStartHookEvent;
session_end: SessionEndHookEvent;
session_info_changed: SessionInfoChangedHookEvent;
model_switch: ModelSwitchHookEvent;
subagent_start: SubagentStartHookEvent;
subagent_stop: SubagentStopHookEvent;
compaction_start: CompactionStartHookEvent;
compaction_end: CompactionEndHookEvent;
interrupt: InterruptHookEvent;
tool_execution_start: ToolExecutionStartHookEvent;
tool_execution_end: ToolExecutionEndHookEvent;
assistant_message_start: AssistantMessageStartHookEvent;
assistant_message_end: AssistantMessageEndHookEvent;
context_inject: ContextInjectHookEvent;
before_provider_request: BeforeProviderRequestHookEvent;
};
export type PluginHookResultMap = {
tool_call: ToolCallHookResult;
tool_result: ToolResultHookResult;
prompt_submit: PromptSubmitHookResult;
permission_request: PermissionRequestHookResult;
turn_start: void;
turn_stop: TurnStopHookResult;
turn_end: void;
session_start: void;
session_end: void;
session_info_changed: void;
model_switch: void;
subagent_start: void;
subagent_stop: void;
compaction_start: void;
compaction_end: void;
interrupt: void;
tool_execution_start: void;
tool_execution_end: void;
assistant_message_start: void;
assistant_message_end: void;
context_inject: ContextInjectHookResult;
before_provider_request: BeforeProviderRequestHookResult;
};
export type HookCommandApprovalRequest = {
event: string;
matcher?: string;
command: string;
timeout?: number;
source?: string;
};
* 第二个 handler 参数:敏感操作(hook 命令审批)与中止信号。
* 现有只关心 event 的插件可忽略。
*/
export type PluginHookContext = {
requestApproval?: (request: HookCommandApprovalRequest) => Promise<boolean>;
signal?: AbortSignal;
permissionMode?: string;
chatMode?: string;
};
export type PluginHandler<N extends PluginHookName> = (
event: PluginHookEventMap[N],
context?: PluginHookContext,
) => PluginHookResultMap[N] | void | Promise<PluginHookResultMap[N] | void>;
export type PluginCommand = {
name: string;
description?: string;
run: (args: string) => string | Promise<string>;
};
export type PluginCommandInfo = {
name: string;
description?: string;
source: string;
};
export type PluginStorage = {
get<T>(key: string): Promise<T | undefined>;
set(key: string, value: unknown): Promise<void>;
};
* 插件入口:与 metis ExtensionFactory 同构。模块默认导出(或模块本身)是工厂函数,
* 加载器调用一次完成全部注册;抛错则该插件被禁用,不影响其他插件。
*/
export type PluginFactory = (api: PluginAPI) => void | Promise<void>;
* AI 自建技能 / 插件的宿主能力:由 server 注入,内置创作插件通过 api.host 调用。
* 插件本身不碰文件系统,落盘、标记写入与热重载都由宿主统一处理。
*/
export type ExtensionHost = {
createSkill(input: {
id: string;
name?: string;
description: string;
body: string;
always?: boolean;
}): Promise<
| { ok: true; id: string; path: string }
| { ok: false; error: string; stage: "validate" | "write" | "refresh" }
>;
createPlugin(input: {
id: string;
name: string;
description: string;
code: string;
}): Promise<
| { ok: true; id: string; path: string; toolNames?: string[]; error?: string }
| { ok: false; error: string; stage: "validate" | "write" | "reload" }
>;
};
export interface PluginAPI {
registerTool(tool: PluginTool): void;
on<N extends PluginHookName>(event: N, handler: PluginHandler<N>): void;
registerCommand(cmd: PluginCommand): void;
storage: PluginStorage;
logger: PluginLogger;
readonly workspace: string;
readonly host?: ExtensionHost;
}
export type PluginSource = "project" | "user" | "builtin";
export type PluginDescriptor = {
id: string;
path: string;
source: PluginSource;
enabled: boolean;
error?: string;
toolNames?: string[];
commandNames?: string[];
hookEvents?: string[];
displayName?: string;
description?: string;
iconUrl?: string;
* 插件自带的界面面板:`<插件目录>/ui/index.html` 存在时自动登记。
* 渲染层用 iframe 加载(`GET /plugin-ui/<id>/...`),单文件插件无法携带 UI。
*/
ui?: PluginUiDescriptor;
};
export type PluginLoadError = { path: string; error: string };
export type PluginUiDescriptor = {
panel: {
title: string;
entry: string;
};
};
export type LoadPluginsResult = {
descriptors: PluginDescriptor[];
errors: PluginLoadError[];
};