跳到正文

生命周期钩子

制作沙箱是一半的工作。另一半则是围绕它发生的一切。

一个全新的云虚拟机没有你的git配置。它没有节点_modules。它没有你的.env。在Agent做任何有用的事情之前,必须先配置git、安装依赖、复制环境文件。在沙箱关闭之前,必须先检查是否有未承诺的工作,并决定如何处理。

这些东西是生命周期的挂钩。当地沙箱几乎不需要它们。没有它们,云端的沙箱就无法使用。

学习成果

沙箱以 index.ts 调用 afterStartbeforeStop 钩子进行设置,类型定义则用 src/sandbox.ts。本地沙箱用空钩。模块7的云和生命周期工作已具备完善。

快速路径

  1. src/sandbox.ts添加一个SandboxLifecycle界面,并可选afterStartbeforeStoponTimeout
  2. index.ts年创建沙箱后,调用 await lifecycle.afterStart?.(sandbox)
  3. 在我之前,sandbox.stop(),调用 await lifecycle.beforeStop?.(sandbox)
  4. 保持生命周期为空,留给本地。钩点存在,身体不必有

动手练习 4.5

在沙箱周围装可选的生命周期钩线。

要求:

  1. 定义SandboxLifecycle,包含三种可选方法,每个方法取一个Sandbox并返回Promise<void>
  2. index.ts中,将一个lifecycle对象传递到沙箱创建旁边
  3. 调用 await lifecycle.afterStart?.(sandbox)刚创建沙箱
  4. 调用 await lifecycle.beforeStop?.(sandbox)之前的sandbox.stop()
  5. 默认为空lifecycle = {},这样本地沙箱就不变了

实现提示:

  • 可选链(?.())会帮你完成条件性调用。不需要if (lifecycle.afterStart)
  • 即使是空无一物的生命周期,依然是生命循环。不要让它在外部层面变得可选
  • onTimeout是吊带触发的钩子,不是你。云后端在达到expiresAt时触发。现在就删掉它,在模块7里用

接口

ts
export interface SandboxLifecycle {
  afterStart?(sandbox: Sandbox): Promise<void>;
  beforeStop?(sandbox: Sandbox): Promise<void>;
  onTimeout?(sandbox: Sandbox): Promise<void>;
}

这三者都是可选的。本地沙箱可能根本不需要这些。生产Harness中的云端沙箱很可能同时使用这三种设备。

每个钩子的作用

afterStart在沙箱创建并准备好接收指令后运行。这里是设置的地方:

ts
const cloudLifecycle: SandboxLifecycle = {
  afterStart: async (sandbox) => {
    await sandbox.exec('git config user.name "Agent"');
    await sandbox.exec('git config user.email "agent@example.com"');
    await sandbox.exec("npm install");
    await sandbox.exec("cp .env.example .env");
  },
};

beforeStop在沙箱关闭前逃跑,这样重要的东西就有机会逃脱:

ts
beforeStop: 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"');
  }
  if (sandbox.snapshot) {
    await sandbox.snapshot();
  }
},

onTimeout在沙箱达到时间限制时运行。云端调用的是这个,不是你。尸体通常会重复利用一些beforeStop并进行一些日志记录:

ts
onTimeout: async (sandbox) => {
  console.error("Sandbox timed out, saving state");
  await cloudLifecycle.beforeStop?.(sandbox);
},

接到Agent 循环

ts
const sandbox = await createSandboxByEnv(cwd);
const lifecycle: SandboxLifecycle = {};

await lifecycle.afterStart?.(sandbox);

try {
  const { text, steps } = await agent.generate({ prompt });
  console.log(text);
  console.log(`\n(${steps.length} steps)`);
} finally {
  await lifecycle.beforeStop?.(sandbox);
  await sandbox.stop();
}

try/finally很重要。即使Agent在跑动中投掷,beforeStop也应该开火。这才是未完成工作的支票该去的地方。

对于本地空lifecycle = {}沙箱,钩子都不会运行。Agent的行为和之前完全一样。这个结构是为我们在模块7中添加真实钩子时准备的。

**注意:钩子是在云端,而不是本地的,而非本地**

对于本地后端,生命周期钩子大多是仪式性的。对于云后端来说,跳过beforeStop意味着虚拟机死机时会失去未提交的工作。界面让你同时思考这两者正是重点。局部情况是云壳的更简单形状,而不是不同的形状。

动手试试

Agent应与上一课完全相同,因为本地生命周期为空。

bash
bun run index.ts . "Read the package.json"

通过添加临时木桩确认管道类型是否有效:

ts
const lifecycle: SandboxLifecycle = {
  afterStart: async (sb) => console.error(`[lifecycle] after start: ${sb.type}`),
  beforeStop: async (sb) => console.error(`[lifecycle] before stop: ${sb.type}`),
};

随便跑任何提示词。你应该能看到两条原木线,把Agent的Opus包围起来。

bash
npx tsc --noEmit

提交

bash
git add src/sandbox.ts index.ts
git commit -m "feat(sandbox): add lifecycle hook points"

完成标准

  • [ ] SandboxLifecycle接口定义了三种可选方法
  • [ ] afterStart在沙箱创建后被召唤一次
  • [ ] beforeStopsandbox.stop()前被叫一次,在finally
  • [ ] 空lifecycle时,Agent保持不变
  • [ ] 使用日志记录钩时,火灾生命周期调用有序
  • [ ] npx tsc --noEmit

**注意:快照和恢复作为生命周期对**

生命周期钩子不仅仅是用来设置和拆除的。试试这个配对:afterStart检查已知位置的快照,找到后从中恢复。beforeStop自动快照后关机。现在你的Harness在调用点没有额外代码的情况下出现了崩溃-恢复行为。快照在哪里?你怎么分辨真正的新跑和恢复的跑? 当快照来自不同版本的代码时会发生什么?模块7对此有深入介绍,但形状来自你刚才定义的生命周期界面。

参考实现

ts
export interface SandboxLifecycle {
  afterStart?(sandbox: Sandbox): Promise<void>;
  beforeStop?(sandbox: Sandbox): Promise<void>;
  onTimeout?(sandbox: Sandbox): Promise<void>;
}
ts
import type { SandboxLifecycle } from "./src/sandbox";

const sandbox = await createSandboxByEnv(cwd);
const lifecycle: SandboxLifecycle = {};

await lifecycle.afterStart?.(sandbox);

try {
  const { text, steps } = await agent.generate({ prompt });
  console.log(text);
  console.log(`\n(${steps.length} steps)`);
} finally {
  await lifecycle.beforeStop?.(sandbox);
  await sandbox.stop();
}

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