跳到正文

扩展点

上一课的注册表句柄“有哪些工具”。它不句柄“工具调用周围发生了什么”。记录每一个调用。阻断写入特定文件。操作系统级沙箱中对命令进行封装。关机时自动承诺。这些都是跨字段的问题,不适合放在任何单一工具里。

事件是正确的原始。该束带会发出生命周期事件。扩展订阅。每个用户都可以在Harness继续前通过、屏蔽或修改事件。多个订阅者连锁。

这堂课就是建筑草图。把事件总线装进工作Harness很简单,但价值在于在接线前先了解契约。

学习成果

你可以描述一个在关键生命周期事件触发的事件总线,可以阻塞或修改的处理程序,以及使多处理程序链可预测的顺序规则。

事件表面

ts
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[]>;
}

这些活动本身故意设计得很小。五个名字涵盖了分机通常需要插入的时刻。之后再加更多内容没问题;以五开頭是保持契约清晰可辨的关键。

四个例子

一旦你看到延伸的实际用途,形状就变得明显了。

每当记工具调用

ts
bus.on("tool_call", async ({ toolName, input }) => {
  console.error(`[${new Date().toISOString()}] ${toolName}: ${JSON.stringify(input)}`);
});

没有回报价值。操作者通过。Harness继续和调用一起。

块写入受保护文件

ts
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会停止调用,并将理由反馈给模型作为工具结果。模型以明文形式看到策略,并向用户报告。

在压实前注入安全防提示词

ts
bus.on("session_before_compact", async () => {
  return {
    modify: {
      customInstructions:
        "Preserve all safety constraints and approval rules across compaction.",
    },
  };
});

处理函数回modify。Harness在继续前施加修改(此处为额外的指令线)。压缩是指指令可能泄露的时刻;这是防止泄漏的一种方法。

关机时自动提交

ts
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(afterStartbeforeStop)的生命周期钩子与session_startsession_shutdown重叠。处理者签名更为通用;生命周期钩子是最常见的情况的方便名称
  • 11.1的技能系统是一个独立的检索界面。它不会经历事件,因为是模型决定是否加载技能,而不是Harness

这个工具最终是分层的:工具在底部,围绕工具的事件,在会话边界有生命周期钩子,技能作为发现的知识,注册作为所有事物的入口。每一层都有自己的职责。

**注意:为什么这是最后一课**

事件总线是最柔软且最危险的延伸表面。不良处理者可能导致Agent死锁,通过日志泄露机密,或阻断合法工具调用。最后构建(在工具、工具、沙箱、提示词、上下文、子 Agent和生命周期钩子之后)意味着你在插入前已经清楚自己在接入什么。

如果你早点接线,可能会有诱惑去用事件钩子来句柄所有问题。先学会其他层次的纪律,正是防止Harness变成一个庞大的on('tool_call', ...)操作者的关键。

动手试试

这是一个概念课。自我检视:

  1. 不要回头,说出五个生命周期事件
  2. 对于每个扩展,描述一个现实可行的扩展
  3. 追踪当两名tool_call员同时返回block: true时会发生什么。谁的理智会赢?
  4. 请解释事件总线与模块2的审批配置之间的关系。它们在哪些地方重叠,哪些地方没有?

如果你想真正构建这个,实现很简单:一个Map<string, Handler[]>和一个emit,按顺序运行处理器,提前退出于块上。在工具执行前和工具结果后插入Agent 循环。模块4的生命周期钩子成为前两个订阅者。

提交

这节课没有代码,除非你接线总线。如果有,建议在另一个分支提交,并用日志扩展练习,再添加任何阻塞扩展。

完成标准

  • [ ] 你可以说出五个生命周期事件
  • [ ] 你可以描述直通、阻塞和修改返回值
  • [ ] 你可以追踪多处理链并预测结果
  • [ ] 你可以解释总线如何补充(而不是替代)审批配置

**注意:建一个遥测扩展**

订阅tool_calltool_result和每步活动。记录时间戳、持续时间和token计数。在事件发生时将事件附加到JSONL文件上,这样崩溃时不会丢失追踪。会话结束时,生成一个单屏报告:总时间、工具调用计时、最慢工具、总token。现在用不同的系统提示词做两次任务,然后把遥测差异化。 哪个提示词工具的调用更少?减少浪费?这就是你如何进行A/B测试Agent行为而不猜测。

非官方简体中文翻译 · 原课程来自 Vercel Academy