生命周期钩子
制作沙箱是一半的工作。另一半则是围绕它发生的一切。
一个全新的云虚拟机没有你的git配置。它没有节点_modules。它没有你的.env。在Agent做任何有用的事情之前,必须先配置git、安装依赖、复制环境文件。在沙箱关闭之前,必须先检查是否有未承诺的工作,并决定如何处理。
这些东西是生命周期的挂钩。当地沙箱几乎不需要它们。没有它们,云端的沙箱就无法使用。
学习成果
沙箱以 index.ts 调用 afterStart 和 beforeStop 钩子进行设置,类型定义则用 src/sandbox.ts。本地沙箱用空钩。模块7的云和生命周期工作已具备完善。
快速路径
- 为
src/sandbox.ts添加一个SandboxLifecycle界面,并可选afterStart、beforeStop、onTimeout index.ts年创建沙箱后,调用await lifecycle.afterStart?.(sandbox)- 在我之前,
sandbox.stop(),调用await lifecycle.beforeStop?.(sandbox) - 保持生命周期为空,留给本地。钩点存在,身体不必有
动手练习 4.5
在沙箱周围装可选的生命周期钩线。
要求:
- 定义
SandboxLifecycle,包含三种可选方法,每个方法取一个Sandbox并返回Promise<void> - 在
index.ts中,将一个lifecycle对象传递到沙箱创建旁边 - 调用
await lifecycle.afterStart?.(sandbox)刚创建沙箱 - 调用
await lifecycle.beforeStop?.(sandbox)之前的sandbox.stop() - 默认为空
lifecycle = {},这样本地沙箱就不变了
实现提示:
- 可选链(
?.())会帮你完成条件性调用。不需要if (lifecycle.afterStart)块 - 即使是空无一物的生命周期,依然是生命循环。不要让它在外部层面变得可选
onTimeout是吊带触发的钩子,不是你。云后端在达到expiresAt时触发。现在就删掉它,在模块7里用
接口
export interface SandboxLifecycle {
afterStart?(sandbox: Sandbox): Promise<void>;
beforeStop?(sandbox: Sandbox): Promise<void>;
onTimeout?(sandbox: Sandbox): Promise<void>;
}这三者都是可选的。本地沙箱可能根本不需要这些。生产Harness中的云端沙箱很可能同时使用这三种设备。
每个钩子的作用
afterStart在沙箱创建并准备好接收指令后运行。这里是设置的地方:
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在沙箱关闭前逃跑,这样重要的东西就有机会逃脱:
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并进行一些日志记录:
onTimeout: async (sandbox) => {
console.error("Sandbox timed out, saving state");
await cloudLifecycle.beforeStop?.(sandbox);
},接到Agent 循环
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应与上一课完全相同,因为本地生命周期为空。
bun run index.ts . "Read the package.json"通过添加临时木桩确认管道类型是否有效:
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包围起来。
npx tsc --noEmit提交
git add src/sandbox.ts index.ts
git commit -m "feat(sandbox): add lifecycle hook points"完成标准
- [ ]
SandboxLifecycle接口定义了三种可选方法 - [ ]
afterStart在沙箱创建后被召唤一次 - [ ]
beforeStop在sandbox.stop()前被叫一次,在finally内 - [ ] 空
lifecycle时,Agent保持不变 - [ ] 使用日志记录钩时,火灾生命周期调用有序
- [ ]
npx tsc --noEmit
**注意:快照和恢复作为生命周期对**
生命周期钩子不仅仅是用来设置和拆除的。试试这个配对:afterStart检查已知位置的快照,找到后从中恢复。beforeStop自动快照后关机。现在你的Harness在调用点没有额外代码的情况下出现了崩溃-恢复行为。快照在哪里?你怎么分辨真正的新跑和恢复的跑? 当快照来自不同版本的代码时会发生什么?模块7对此有深入介绍,但形状来自你刚才定义的生命周期界面。
参考实现
export interface SandboxLifecycle {
afterStart?(sandbox: Sandbox): Promise<void>;
beforeStop?(sandbox: Sandbox): Promise<void>;
onTimeout?(sandbox: Sandbox): Promise<void>;
}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();
}