Current section

Files

Jump to
cmdc lib cmdc plugin.ex
Raw

lib/cmdc/plugin.ex

defmodule CMDC.Plugin do
@moduledoc """
Plugin behaviour 定义。所有插件实现此 behaviour。
Plugin 系统是 CMDC 与 DeepAgents 最重要的差异化设计之一:
提供 **11 个精细拦截点 × 7 种控制 action**,远超 DeepAgents 的线性 Middleware 链。
## 11 种事件(event)
| 事件 | 触发时机 | 可用 action |
|------|---------|------------|
| `:session_start` | Agent 会话刚启动 | continue/abort/emit |
| `:session_end` | Agent 会话正常结束 | continue/emit |
| `{:before_prompt, text}` | 用户 prompt 提交前 | continue/intervene/abort/emit |
| `{:before_request, messages}` | 发送 LLM 请求前,可修改消息列表 | continue/intervene/abort/emit |
| `{:after_response, assistant_msg}` | 收到 LLM 回复后,可分析内容 | continue/intervene/abort/emit |
| `{:before_tool, name, args}` | 工具执行前,可阻止或替换参数 | continue/block_tool/replace_tool_args/abort/emit |
| `{:on_tool_error, name, call_id, error, attempt}` | 工具执行失败、retry 前 | continue/abort/skip |
| `{:on_tool_error, name, call_id, error, attempt}` | 工具执行失败、retry 前 | continue/abort/skip |
| `{:after_tool, name, call_id, result}` | 单个工具执行完毕 | continue/intervene/abort/emit |
| `{:after_tool_batch, results}` | 一批工具全部执行完毕 | continue/intervene/abort/emit |
| `:before_finish` | Agent 准备返回最终结果前,可拦截注入新 prompt | continue/intervene/abort/emit |
| `{:before_compact, messages}` | 上下文压缩前,可修改待压缩消息 | continue/skip/emit |
## 7 种 Action
| Action | 元组形式 | 含义 | 典型使用者 |
|--------|---------|------|-----------|
| `continue` | `{:continue, state}` | 继续执行下一个 plugin | 所有 plugin 的默认返回 |
| `intervene` | `{:intervene, prompt, state}` | 注入新 prompt,Agent 继续工作 | SelfVerify、ReviewLoop |
| `abort` | `{:abort, reason, state}` | 强制停止 Agent | SecurityGuard、BudgetGuard |
| `skip` | `{:skip, state}` | 跳过后续 plugin(短路) | 短路优化 |
| `block_tool` | `{:block_tool, reason, state}` | 阻止当前工具执行(仅 before_tool) | SecurityGuard |
| `replace_tool_args` | `{:replace_tool_args, new_args, state}` | 替换工具参数(仅 before_tool) | SecurityGuard |
| `emit` | `{:emit, {event_name, payload}, state}` | 广播自定义事件 | 任何 plugin |
## 优先级范围
- P0 安全类:0-99(最先执行)
- P1 核心类:100-299
- P2 质量类:300-599
- P3 智能类:600-899
- 自定义:900+(最后执行)
## 实现示例
defmodule MyApp.AuditPlugin do
@moduledoc "记录所有工具调用。"
@behaviour CMDC.Plugin
@impl true
def init(_opts), do: {:ok, %{calls: []}}
@impl true
def priority, do: 500
@impl true
def handle_event({:before_tool, name, args}, state, _ctx) do
{:continue, %{state | calls: [{name, args} | state.calls]}}
end
def handle_event(_event, state, _ctx), do: {:continue, state}
end
"""
# ==========================================================================
# 类型定义
# ==========================================================================
@typedoc "插件自身的运行时状态,格式由各插件自行定义。"
@type plugin_state :: term()
@typedoc """
Plugin 接收的事件类型。
共 11 种,覆盖 Agent 完整生命周期:
- `:session_start` — 会话启动
- `:session_end` — 会话结束
- `{:before_prompt, text}` — 用户 prompt 提交前
- `{:before_request, messages}` — 发送 LLM 请求前
- `{:after_response, assistant_msg}` — 收到 LLM 回复后
- `{:before_tool, name, args}` — 工具执行前
- `{:on_tool_error, name, call_id, error, attempt}` — 工具执行失败、retry 前
- `{:after_tool, name, call_id, result}` — 单工具执行后
- `{:after_tool_batch, results}` — 批次工具全部执行后
- `:before_finish` — Agent 准备返回最终结果前
- `{:before_compact, messages}` — 上下文压缩前
"""
@type event ::
:session_start
| :session_end
| {:before_prompt, text :: String.t()}
| {:before_request, messages :: [CMDC.Message.t()]}
| {:after_response, assistant_msg :: CMDC.Message.t()}
| {:before_tool, tool_name :: String.t(), args :: map()}
| {:on_tool_error, tool_name :: String.t(), call_id :: String.t(), error :: String.t(),
attempt :: pos_integer()}
| {:after_tool, tool_name :: String.t(), call_id :: String.t(),
result :: {:ok, String.t()} | {:error, String.t()}}
| {:after_tool_batch,
results :: [{tool_name :: String.t(), {:ok, String.t()} | {:error, String.t()}}]}
| :before_finish
| {:before_compact, messages :: [CMDC.Message.t()]}
@typedoc """
Plugin 返回的控制 action。
共 7 种:
- `{:continue, state}` — 继续执行下一个 plugin(默认)
- `{:intervene, prompt, state}` — 注入新 prompt,Agent 继续工作
- `{:abort, reason, state}` — 强制停止 Agent
- `{:skip, state}` — 跳过后续所有 plugin(短路)
- `{:block_tool, reason, state}` — 阻止工具执行(仅 before_tool 有效)
- `{:replace_tool_args, new_args, state}` — 替换工具参数(仅 before_tool 有效)
- `{:emit, {event_name, payload}, state}` — 广播自定义事件
"""
@type action ::
{:continue, plugin_state()}
| {:intervene, prompt :: String.t(), plugin_state()}
| {:abort, reason :: String.t(), plugin_state()}
| {:skip, plugin_state()}
| {:block_tool, reason :: String.t(), plugin_state()}
| {:replace_tool_args, new_args :: map(), plugin_state()}
| {:emit, {event_name :: atom(), payload :: term()}, plugin_state()}
# ==========================================================================
# 必须实现的 Callbacks
# ==========================================================================
@doc """
初始化插件状态。
在 Agent 会话启动时被调用一次。返回 `{:ok, state}` 表示初始化成功,
返回 `{:error, reason}` 将阻止 Agent 启动。
## 参数
- `opts` — 注册 plugin 时传入的选项(keyword list)
"""
@callback init(opts :: keyword()) :: {:ok, plugin_state()} | {:error, term()}
@doc """
处理事件并返回一个 action。
Pipeline 按 `priority/0` 从小到大依次调用各 plugin 的 `handle_event/3`
遇到短路 action(`abort``block_tool``skip`)时,Pipeline 停止后续调用。
## 参数
- `event` — 当前事件,见 `t:event/0` 类型定义
- `state` — 当前插件的运行时状态
- `ctx` — Agent 执行上下文(含 session_id、working_dir、model 等)
"""
@callback handle_event(event(), plugin_state(), CMDC.Context.t()) :: action()
@doc """
插件优先级。数值越小越先执行。
建议范围:
- 0-99:安全/防护类
- 100-299:核心功能类
- 300-599:质量监控类
- 600-899:智能增强类
- 900+:自定义扩展
"""
@callback priority() :: non_neg_integer()
# ==========================================================================
# 可选 Callbacks
# ==========================================================================
@doc "会话结束时的清理操作(可选)。"
@callback on_session_end(plugin_state(), CMDC.Context.t()) :: :ok
@doc "返回插件的人类可读描述,用于调试和日志(可选)。"
@callback describe() :: String.t()
@optional_callbacks [on_session_end: 2, describe: 0]
# ==========================================================================
# 辅助函数
# ==========================================================================
@doc """
检查给定模块是否实现了 `CMDC.Plugin` behaviour。
## 示例
iex> CMDC.Plugin.plugin?(CMDC.Plugin.Builtin.EventLogger)
true
iex> CMDC.Plugin.plugin?(String)
false
"""
@spec plugin?(module()) :: boolean()
def plugin?(module) when is_atom(module) do
module.module_info(:attributes)
|> Keyword.get_values(:behaviour)
|> List.flatten()
|> Enum.member?(__MODULE__)
rescue
_ -> false
end
@doc """
判断 action 是否为短路类型。
短路 action 会让 Pipeline 停止调用后续 plugin:
- `{:abort, ...}` — 强制停止 Agent
- `{:block_tool, ...}` — 阻止工具执行
- `{:skip, ...}` — 跳过后续 plugin
## 示例
iex> CMDC.Plugin.short_circuit?({:abort, "危险操作", %{}})
true
iex> CMDC.Plugin.short_circuit?({:continue, %{}})
false
"""
@spec short_circuit?(action()) :: boolean()
def short_circuit?({:abort, _reason, _state}), do: true
def short_circuit?({:block_tool, _reason, _state}), do: true
def short_circuit?({:skip, _state}), do: true
def short_circuit?(_action), do: false
@doc """
从 action 元组中提取 plugin_state。
无论哪种 action 类型,state 始终是最后一个元素。
## 示例
iex> CMDC.Plugin.extract_state({:continue, %{count: 1}})
%{count: 1}
iex> CMDC.Plugin.extract_state({:abort, "stop", %{reason: "budget"}})
%{reason: "budget"}
"""
@spec extract_state(action()) :: plugin_state()
def extract_state({:continue, state}), do: state
def extract_state({:intervene, _prompt, state}), do: state
def extract_state({:abort, _reason, state}), do: state
def extract_state({:skip, state}), do: state
def extract_state({:block_tool, _reason, state}), do: state
def extract_state({:replace_tool_args, _args, state}), do: state
def extract_state({:emit, _event, state}), do: state
@doc """
获取 action 的类型标签(原子)。
## 示例
iex> CMDC.Plugin.action_type({:continue, %{}})
:continue
iex> CMDC.Plugin.action_type({:block_tool, "危险路径", %{}})
:block_tool
"""
@spec action_type(action()) :: atom()
def action_type({type, _}), do: type
def action_type({type, _, _}), do: type
def action_type({type, _, _, _}), do: type
end