diff --git a/docs/shared/mitsein-agent-plugin-design.md b/docs/shared/mitsein-agent-plugin-design.md new file mode 100644 index 0000000..f1683c2 --- /dev/null +++ b/docs/shared/mitsein-agent-plugin-design.md @@ -0,0 +1,306 @@ +# Mitsein Agent 插件化设计:两个抽象,完整生命周期 + +!!! info "作者" + 沙洲 & 星月 🌙 | 2026-04-08 + +--- + +## 核心设计 + +Mitsein 的 Agent 插件化只需要**两个抽象**: + +1. **Lifecycle Hooks** — Agent 生命周期各阶段的拦截点 +2. **Context Middleware Chain** — 对 LLM 调用参数的上下文驱动变换链 + +这两个抽象覆盖了 Agent 从启动到关闭、从收到消息到返回响应的完整生命周期。 + +--- + +## 抽象 1:Lifecycle Hooks + +11 个 hook 点,分四层: + +```typescript +interface AgentLifecycleHooks { + // ═══ 系统层 ═══ + on_startup(): void; + on_shutdown(): void; + + // ═══ 会话层 ═══ + on_session_start(session: Session): void; + on_session_end(session: Session): void; + + // ═══ 消息层 ═══ + on_message_received(msg: InboundMessage): InboundMessage | void; + on_message_sending(msg: OutboundMessage): OutboundMessage | { cancel: true } | void; + + // ═══ 心跳 ═══ + on_heartbeat(ctx: HeartbeatContext): void; + + // ═══ 编排层 ═══ + before_llm_call(params: LlmCallParams): LlmCallParams | void; + after_llm_call(response: LlmResponse): LlmResponse | void; + before_tool_call(call: ToolCall): ToolCall | { block: true } | void; + after_tool_call(result: ToolResult): ToolResult | void; +} +``` + +### 层次模型 + +``` +系统层 on_startup ──── on_heartbeat (定时) ──── on_shutdown + │ ▲ + ▼ │ +会话层 on_session_start ────────────────── on_session_end + │ ▲ + ▼ │ +消息层 on_message_received ────────── on_message_sending + │ ▲ + ▼ │ +编排层 before_llm_call ──→ LLM ──→ after_llm_call + before_tool_call ─→ Tool ─→ after_tool_call +``` + +每层关注不同粒度。插件**按需注册**,不需要实现全部 hook。 + +### Hook 语义 + +| Hook | 返回值语义 | +|:-----|:----------| +| 返回修改后的对象 | 替换原始输入(拦截并修改) | +| 返回 `void` / `undefined` | 不修改,继续执行 | +| 返回 `{ block: true }` | 阻止执行(仅 `before_tool_call`) | +| 返回 `{ cancel: true }` | 取消发送(仅 `on_message_sending`) | +| 抛异常 | 中断整个链路 | + +多个插件注册同一 hook 时按优先级顺序执行,前一个的输出是后一个的输入(pipeline 模式)。 + +--- + +## 抽象 2:Context Middleware Chain + +一组有序的中间件,每个基于当前上下文对 LLM 调用参数产生副作用: + +```typescript +type ContextMiddleware = ( + ctx: OrchestratorContext, + params: LlmCallParams, +) => LlmCallParams; +``` + +### OrchestratorContext + +```typescript +interface OrchestratorContext { + // 会话 + session: Session; + user: User; + + // 对话 + messages: Message[]; // 当前对话历史 + lastToolResults: ToolResult[]; // 上一轮工具执行结果 + + // Agent 配置 + agentConfig: AgentConfig; // YAML 定义的 agent 配置 + availableTools: ToolDef[]; // 当前可用工具列表 + + // 运行时 + turnIndex: number; // 当前对话轮次 + parentContext?: OrchestratorContext; // 嵌套调用时的父上下文 +} +``` + +### LlmCallParams + +```typescript +interface LlmCallParams { + model: string; + messages: LlmMessage[]; + tools?: ToolSchema[]; + temperature?: number; + maxTokens?: number; + systemPrompt?: string; + // ...其他 LLM API 参数 +} +``` + +### 特征 + +- **有序执行** — 顺序可配置,有明确的优先级 +- **纯函数式** — 输入 context + params,输出 params,无隐式副作用 +- **可独立测试** — 每个 middleware 可以单独 mock context 测试 +- **可组合** — 插件注册自己的 middleware,与核心 middleware 共存 + +### 注册 + +```typescript +// 核心自带的 middleware(不可移除,可覆盖) +orchestrator.use(buildConversationHistory, { priority: 0 }); +orchestrator.use(truncateToContextWindow, { priority: 100 }); + +// 插件注册的 middleware +orchestrator.use(selectModelByPlan, { priority: 10 }); +orchestrator.use(injectRAGContext, { priority: 50 }); +orchestrator.use(applyThinkingMode, { priority: 60 }); +``` + +执行顺序按 priority 升序。同 priority 按注册顺序。 + +--- + +## 插件注册 + +```typescript +interface AgentPlugin { + id: string; + name: string; + version: string; + + hooks?: Partial; + middlewares?: Array<{ + fn: ContextMiddleware; + priority?: number; // 默认 50 + }>; +} + +function registerPlugin(plugin: AgentPlugin): void; +``` + +--- + +## 示例 + +### Billing 插件 + +```typescript +registerPlugin({ + id: "billing", + name: "Usage Billing", + version: "1.0.0", + hooks: { + on_session_start(session) { + initBillingSession(session.user); + }, + before_llm_call(params) { + if (getUserCredits(params) <= 0) { + throw new InsufficientCreditsError(); + } + }, + after_llm_call(response) { + deductCredits(response.usage); + }, + on_session_end(session) { + finalizeBilling(session); + }, + }, + middlewares: [{ + fn: (ctx, params) => ({ + ...params, + model: ctx.user.plan === "pro" ? "claude-sonnet-4" : "claude-haiku-4", + }), + priority: 10, + }], +}); +``` + +### 内容安全插件 + +```typescript +registerPlugin({ + id: "content-safety", + name: "Content Safety Filter", + version: "1.0.0", + hooks: { + on_message_received(msg) { + if (containsBlockedContent(msg.text)) { + return { ...msg, blocked: true, reason: "policy_violation" }; + } + }, + on_message_sending(msg) { + return { ...msg, text: redactPII(msg.text) }; + }, + before_tool_call(call) { + if (isDangerousTool(call.name)) { + return { block: true }; + } + }, + }, +}); +``` + +### RAG 插件 + +```typescript +registerPlugin({ + id: "rag", + name: "Retrieval Augmented Generation", + version: "1.0.0", + middlewares: [{ + fn: async (ctx, params) => { + const lastUserMsg = ctx.messages.findLast(m => m.role === "user"); + const docs = await vectorSearch(lastUserMsg.content); + return { + ...params, + systemPrompt: params.systemPrompt + formatRetrievedDocs(docs), + }; + }, + priority: 50, + }], +}); +``` + +### 监控插件 + +```typescript +registerPlugin({ + id: "monitoring", + name: "Observability", + version: "1.0.0", + hooks: { + on_startup() { + initMetricsCollector(); + }, + on_heartbeat(ctx) { + reportHealthMetrics(ctx); + }, + after_llm_call(response) { + recordLatency(response.model, response.timing); + recordTokenUsage(response.model, response.usage); + }, + after_tool_call(result) { + recordToolExecution(result.toolName, result.duration, result.success); + }, + on_shutdown() { + flushMetrics(); + }, + }, +}); +``` + +--- + +## 设计决策 + +### 为什么 Hook 而不是事件? + +Hook 是**同步拦截**(可以修改输入输出),事件是**异步通知**(只能观察)。Agent 编排需要拦截能力——比如 `before_llm_call` 要能改 model,`before_tool_call` 要能 block。纯事件做不到。 + +### 为什么 Middleware 是独立抽象? + +`before_llm_call` hook 虽然也能修改 params,但它是"一锤子"的——一个 hook 做一件事。Middleware chain 是"流水线"的——多个 middleware 依次变换 params,每个只关注一个维度(选模型、裁历史、注入 context...)。两者互补: + +- Hook = 拦截/决策(要不要做、做了之后怎么办) +- Middleware = 变换/准备(怎么准备 LLM 的输入) + +### 为什么不用 OpenClaw 的 44-hook 模式? + +OpenClaw 把 Provider 注册(catalog/auth/诊断)和 Orchestrator 运行时(params/stream/replay)混在同一个对象里,导致 hook 数量膨胀。我们做了关注点分离: + +- Provider 注册 = 静态元数据声明(不在这个插件体系里) +- Agent 运行时 = 两个抽象(11 hooks + middleware chain) + +分离后,运行时层更简洁、更易理解、更好测试。 + +--- + +*沙洲 & 星月 🌙 — 2026-04-08*