扩展点
上一课的注册表句柄“有哪些工具”。它不句柄“工具调用周围发生了什么”。记录每一个调用。阻断写入特定文件。操作系统级沙箱中对命令进行封装。关机时自动承诺。这些都是跨字段的问题,不适合放在任何单一工具里。
事件是正确的原始。该束带会发出生命周期事件。扩展订阅。每个用户都可以在Harness继续前通过、屏蔽或修改事件。多个订阅者连锁。
这堂课就是建筑草图。把事件总线装进工作Harness很简单,但价值在于在接线前先了解契约。
学习成果
你可以描述一个在关键生命周期事件触发的事件总线,可以阻塞或修改的处理程序,以及使多处理程序链可预测的顺序规则。
事件表面
type LifecycleEvent =
| "session_start"
| "tool_call"
| "tool_result"
| "session_before_compact"
| "session_shutdown";
type EventResult = { block?: boolean; reason?: string; modify?: any } | void;
interface EventBus {
on(event: LifecycleEvent, handler: (data: any) => Promise<EventResult>): void;
emit(event: LifecycleEvent, data: any): Promise<EventResult[]>;
}这些活动本身故意设计得很小。五个名字涵盖了分机通常需要插入的时刻。之后再加更多内容没问题;以五开頭是保持契约清晰可辨的关键。
四个例子
一旦你看到延伸的实际用途,形状就变得明显了。
每当记工具调用
bus.on("tool_call", async ({ toolName, input }) => {
console.error(`[${new Date().toISOString()}] ${toolName}: ${JSON.stringify(input)}`);
});没有回报价值。操作者通过。Harness继续和调用一起。
块写入受保护文件
const PROTECTED = [".env", "package-lock.json"];
bus.on("tool_call", async ({ toolName, input }) => {
if (toolName === "write" && PROTECTED.some((p) => input.path.endsWith(p))) {
return { block: true, reason: `${input.path} is protected by policy.` };
}
});处理函数回block: true。Harness会停止调用,并将理由反馈给模型作为工具结果。模型以明文形式看到策略,并向用户报告。
在压实前注入安全防提示词
bus.on("session_before_compact", async () => {
return {
modify: {
customInstructions:
"Preserve all safety constraints and approval rules across compaction.",
},
};
});处理函数回modify。Harness在继续前施加修改(此处为额外的指令线)。压缩是指指令可能泄露的时刻;这是防止泄漏的一种方法。
关机时自动提交
bus.on("session_shutdown", async ({ sandbox }) => {
const { stdout } = await sandbox.exec("git status --porcelain");
if (stdout.trim()) {
await sandbox.exec(`git add -A && git commit -m "WIP: auto-save"`);
}
});这是模块4中的云-沙箱 beforeStop钩子,推广版。任何因任何原因结束的会话都有机会检查其工作。
链式规则
多个处理器可以订阅同一个事件。它们按注册顺序运行。如果有处理者返回block: true,调用停止,原因会回到模型。如果有返回modify,后续处理程序会看到修改后的数据。
Tool call requested
-> emit "tool_call"
handler 1: log (pass through)
handler 2: check protected files (may block)
handler 3: project safety policy (may block)
-> if any blocked: return reason to model, do not execute
-> if all passed: execute tool
-> emit "tool_result"
handler 1: log result
handler 2: telemetry秩序很重要。在安全检查前记录,即使被阻止,也能捕捉到调用尝试。遥测结果后,记录了实际运行的部分。顺序正确,链条就能产生有用的痕迹。如果记错了,你就只记下了一半的故事。
事件如何与你已有的构建结合
这些片段开始以一种积极的方式重叠:
- 模块2的审批配置设定了操作模式(交互式、后台、委派)。它仍然在工具层面运行
- 事件总线绕着工具层运行。
tool_call处理者即使审批已通过,也可以进行封锁 - 模块4(
afterStart、beforeStop)的生命周期钩子与session_start和session_shutdown重叠。处理者签名更为通用;生命周期钩子是最常见的情况的方便名称 - 11.1的技能系统是一个独立的检索界面。它不会经历事件,因为是模型决定是否加载技能,而不是Harness
这个工具最终是分层的:工具在底部,围绕工具的事件,在会话边界有生命周期钩子,技能作为发现的知识,注册作为所有事物的入口。每一层都有自己的职责。
**注意:为什么这是最后一课**
事件总线是最柔软且最危险的延伸表面。不良处理者可能导致Agent死锁,通过日志泄露机密,或阻断合法工具调用。最后构建(在工具、工具、沙箱、提示词、上下文、子 Agent和生命周期钩子之后)意味着你在插入前已经清楚自己在接入什么。
如果你早点接线,可能会有诱惑去用事件钩子来句柄所有问题。先学会其他层次的纪律,正是防止Harness变成一个庞大的on('tool_call', ...)操作者的关键。
动手试试
这是一个概念课。自我检视:
- 不要回头,说出五个生命周期事件
- 对于每个扩展,描述一个现实可行的扩展
- 追踪当两名
tool_call员同时返回block: true时会发生什么。谁的理智会赢? - 请解释事件总线与模块2的审批配置之间的关系。它们在哪些地方重叠,哪些地方没有?
如果你想真正构建这个,实现很简单:一个Map<string, Handler[]>和一个emit,按顺序运行处理器,提前退出于块上。在工具执行前和工具结果后插入Agent 循环。模块4的生命周期钩子成为前两个订阅者。
提交
这节课没有代码,除非你接线总线。如果有,建议在另一个分支提交,并用日志扩展练习,再添加任何阻塞扩展。
完成标准
- [ ] 你可以说出五个生命周期事件
- [ ] 你可以描述直通、阻塞和修改返回值
- [ ] 你可以追踪多处理链并预测结果
- [ ] 你可以解释总线如何补充(而不是替代)审批配置
**注意:建一个遥测扩展**
订阅tool_call、tool_result和每步活动。记录时间戳、持续时间和token计数。在事件发生时将事件附加到JSONL文件上,这样崩溃时不会丢失追踪。会话结束时,生成一个单屏报告:总时间、工具调用计时、最慢工具、总token。现在用不同的系统提示词做两次任务,然后把遥测差异化。 哪个提示词工具的调用更少?减少浪费?这就是你如何进行A/B测试Agent行为而不猜测。