本地实现
上一节课的界面本身没有任何反应。我们需要一个后端。
本地沙箱才是无聊的。它包裹的是你一直使用的readFileSync和execSync 调用。不同的是,现在这些设备都隐藏在界面后面,所有工具都以相同的方式调用它们。
无聊才是重点。本地沙箱证明了该接口在不引入新复杂性的情况下可行。它是所有其他后端都会被比较的基线。
学习成果
src/sandbox-local.ts导出createLocalSandbox(dir),一个工厂,返回一个Sandbox,其方法包裹了Node的readFileSync和execSync。Agent运行方式和之前一样,但通过界面实现。
快速路径
- 创建
src/sandbox-local.ts导出createLocalSandbox(dir): Sandbox - 用
async readFile包裹readFileSync - 用
async exec包裹execSync,尝试/接球回{ stdout, exitCode } - 让
stop()变成异步无操作者
动手练习 4.2
实施本地沙箱。
要求:
createLocalSandbox(dir: string): Sandbox返回一个满足接口的对象readFile解析路径dir并读UTF-8exec在有cwd: dir和30秒超时的情况下执行该命令exec错误时,{ stdout: <whatever output there was>, exitCode: <non-zero> }回击而非投掷stop是async () => {}
实现提示:
- 整个文件大约有15行。如果你的后台更长,可能你处理的是云后端会关注而本地后端不关心的案件
exec绝不应该扔球,即使是在非零的出场口。工具期望结果对象。发现错误并返回是正确的形状type: "local"是sandboxType从模块3插入到系统提示词的部分
实现
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { execSync } from "node:child_process";
import type { Sandbox } from "./sandbox";
export function createLocalSandbox(dir: string): Sandbox {
return {
type: "local",
workingDirectory: dir,
readFile: async (p) => readFileSync(resolve(dir, p), "utf-8"),
exec: async (command) => {
try {
const stdout = execSync(command, {
cwd: dir,
encoding: "utf-8",
timeout: 30_000,
});
return { stdout, exitCode: 0 };
} catch (e: any) {
return {
stdout: e.stdout || e.stderr || e.message || "",
exitCode: e.status ?? 1,
};
}
},
stop: async () => {},
};
}这就是整个后端。stop是无营运的,因为没有什么需要清理的。本地文件系统和child_process会比Agent更持久。
完成接线
import { createLocalSandbox } from "./src/sandbox-local";
import { createReadTool, createGrepTool, createBashTool } from "./src/tools";
const sandbox = createLocalSandbox(cwd);
console.error(`Sandbox: ${sandbox.type}`);
const tools = {
read: createReadTool(sandbox),
grep: createGrepTool(sandbox),
bash: createBashTool(sandbox, createApproval({ mode: "interactive" })),
};现在工厂接管了沙箱。他们封闭它,从内部调用execute。同样的工具,同样的Agent,同样的提示词。
动手试试
用你一直在用的提示词。输出应保持不变:
bun run index.ts . "Read the tsconfig.json"
bun run index.ts . "Find all TODO comments"
bun run index.ts . "List all files in this directory"Agent应该完全一样。引擎盖下的管道不同。确认沙箱身份一次:
console.error(`Sandbox: ${sandbox.type}`);你应该看看Sandbox: local。
npx tsc --noEmit**注意:如果行为发生变化,重构就会泄露**
经过这节课,Agent在同一提示词上的行为应与模块3完全一致。如果有变化(路由不同、输出不同、新的错误),找一个工具仍然直接访问节点API而不是通过sandbox的地方。
提交
git add src/sandbox-local.ts index.ts
git commit -m "feat(sandbox): add local backend wrapping Node APIs"完成标准
- [ ]
src/sandbox-local.ts出口createLocalSandbox(dir) - [ ] 返回的对象满足
Sandbox接口 - [ ]
readFile和exec通过Node API路由,就像之前一样 - [ ]
stop是个不会崩溃的无操作者 - [ ] 这三种工具都还能用,和模块3一样
- [ ]
npx tsc --noEmit
**注意:把执行流变成 buffer 而不是 buffer**
execSync等命令结束后,一次性把所有stdout都丢掉。对于长构建来说,这很痛苦。试着切换到spawn,流式输出回每个区块。挑战在于:Sandbox.exec签名最后一次返回{ stdout, exitCode }。要流式传输,你需要不同的形状,比如异步迭代器。注意这种情绪如何反复影响到每一个调用 exec工具。界面决策很棘手。
参考实现
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { execSync } from "node:child_process";
import type { Sandbox } from "./sandbox";
export function createLocalSandbox(dir: string): Sandbox {
return {
type: "local",
workingDirectory: dir,
readFile: async (p) => readFileSync(resolve(dir, p), "utf-8"),
exec: async (command) => {
try {
const stdout = execSync(command, {
cwd: dir,
encoding: "utf-8",
timeout: 30_000,
});
return { stdout, exitCode: 0 };
} catch (e: any) {
return {
stdout: e.stdout || e.stderr || e.message || "",
exitCode: e.status ?? 1,
};
}
},
stop: async () => {},
};
}