tool/call、tool/result、turn/* 是会话事件(L07 账本里的记录),要观察它们必须监听 session/event 再检查 event.type;tools/result、agent/request 这类才是 Cordis 插件事件。写监听前先问:要"账本记录"还是"拦截点"?// ✔ 观察账本里的工具结果(会话事件)
ctx.on('session/event', (_session, event) => {
if (event.type === 'tool/result') { /* 记录 */ }
})
// ✔ 拦截工具执行管线(Cordis 事件)
ctx.on('tools/result', (exec, result) => { /* 观察 */ })
| 模式 | 语义 | 典型用途 |
|---|---|---|
emit | 广播,无返回值 | 通知类:统计计数、日志 |
waterfall | 处理链,可改写可短路 | 拦截类主角:权限、改写、网关 |
parallel | 并发通知全部,await 全部 | 互不依赖的并行处理 |
serial | 按序执行,首个有效结果截断 | 有先后依赖的装配阶段 |
bail | 短路检查:第一个说"有"的定案 | 快速否决检查 |
监听器按注册顺序层层包裹,像洋葱(L05 讲过军规,这里画出形状):
ctx.on('tools/pre-execute', async (exec, next) => {
// ── 进门:请求经过我(可检查、可改写 exec)
if (!await isAllowed(exec)) {
return { kind: 'deny', reason: 'Denied by policy.' } // ← 不调 next = 短路定案
}
const decision = await next() // ← 调 next = 交给下一层(洋葱更深处)
// ── 出门:结果回来经过我(可再加工)
return decision
})
prepend: true 可把自己插到最前面执行(少用,需要时很关键)。import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
declare function isAllowed(exec: ToolExecution): Promise<boolean>
export const name = 'permission-gate'
export function apply(ctx: Context) {
ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
if (!(await isAllowed(exec))) {
return { kind: 'deny', reason: 'Denied by policy.' }
}
return next() // 放行给后续策略
})
}
返回 ask 则转人工审批(L09 讲过的审批弹窗就是走这条路)。注意这个插件没有 inject——监听事件不需要等服务,事件天然在双方都在场时才发生。
工具执行管线上有四个观察/拦截点,选错位置是这类插件最常见的 bug 来源:
| 扩展点 | 能做什么 | 什么时候选它 |
|---|---|---|
tools/pre-execute | 返回 allow / deny / ask | 可扩展的策略:要"问人"选项时(唯一能 ask 的) |
ctx.tools.guard() | 最终否决,后面的层改不回来 | 需要不可绕过的单调拒绝(安全底线) |
tools/execute | 包住整个执行生命周期 | 超时、重试、指标——包住"干活的全程" |
tools/post-execute | 替换展示内容/返回值、附加上下文 | 要改结果时 |
tools/result | 只观察不可变最终结果 | 审计、日志、统计——绝不改结果时 |
记忆口诀:问人 pre、底线 guard、包全程 execute、改结果 post、只看 result。
import type { Context } from '@deepseek-ai/cordis'
export const name = 'tool-logger'
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => {
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
const text = result.content
.map(b => b.type === 'text' ? b.text : '')
.join('')
console.log(`[result] ${text.slice(0, 100)}`)
})
}
选 tools/result 而非 post-execute,因为 logger 只观察不改——选 result 保证你物理上改不了结果,错误从类型系统层面消失。
| 事件 | 时机 | 能干嘛 |
|---|---|---|
agent/pre-step(瀑布) | 认领输入后、发给模型前 | 改写将进入模型的输入,或直接拒绝 |
agent/request(瀑布) | 模型请求组装完 | 最终改写请求(如注入系统级内容) |
agent/turn-stopping(串行) | 轮次将结束 | 没有 next();可强制再走一步 |
llm/stream(瀑布) | 流式响应 | 包住模型流 |
blockedTools: string[],名单内工具在 pre-execute 返回 deny。验证模型被拒后如何反应。