Cloudflare Computer ↗ 为 Agent 提供由 SQLite 支持的持久化工作目录,以及用于处理文件系统的标准 tool 集合,包括 read、write、edit 和 grep。可选的 exec tool 让 Agent 以编程方式操作文件系统并运行 tools。
在 Agent 需要以下能力时使用文件系统:
- 在多轮对话和重启之间保留笔记、草稿、计划或生成的文件。
- 使用面向模型的 tools 读取、搜索和编辑文本文件。
- 针对文件编写并运行 JavaScript,而无需启动容器。
- 向 Agent 提供 skills、源文件和其他外部文档,而不占满其上下文窗口。
Workspace 位于 Durable Object 内,并将每个文件保存在该对象的 SQLite 存储中。Durable Object 被 evict 或重启后文件仍然保留,无需配置外部存储。
workspace.fs 读取、写入、列出、搜索和删除文件。workspace.runtime 通过 execution backend 针对同一批文件运行代码。Computer 包含 JavaScript runtime,以及用于运行 shell 命令的实验性基于容器的 runtime。
npm i @cloudflare/computer zodyarn add @cloudflare/computer zodpnpm add @cloudflare/computer zodbun add @cloudflare/computer zod每个 tool 入口都需要 zod。还需要什么取决于 Agent 如何调用 tools:
- Think 包含工作区 tools。将其与
@cloudflare/computer一起安装。 - AI SDK ↗ tools 还需要
ai。 - Pi 和 OpenCode 使用 Pi AI tools,除
zod外无需其他依赖。
Worker 需要 nodejs_compat 兼容性标志。
在 Durable Object 类中创建 Workspace,并传入 storage 对象。文件系统可立即使用。
import { Workspace } from "@cloudflare/computer";
import { DurableObject } from "cloudflare:workers";
export class NotesAgent extends DurableObject {
workspace = new Workspace({ storage: this.ctx.storage });
async addTodo(item) {
const path = "/workspace/notes/todo.md";
await this.workspace.fs.mkdir("/workspace/notes", { recursive: true });
let todo = "";
try {
todo = await this.workspace.fs.readFile(path, "utf8");
} catch (error) {
// A missing file means there are no to-do items yet.
if (error.code !== "ENOENT") throw error;
}
todo += `- [ ] ${item}\n`;
await this.workspace.fs.writeFile(path, todo);
return todo;
}
}
export default {
async fetch(request, env) {
const agent = env.NotesAgent.getByName("user-123");
return new Response(await agent.addTodo("Ship it"));
},
};import { Workspace } from "@cloudflare/computer";
import { DurableObject } from "cloudflare:workers";
export class NotesAgent extends DurableObject<Env> {
workspace = new Workspace({ storage: this.ctx.storage });
async addTodo(item: string) {
const path = "/workspace/notes/todo.md";
await this.workspace.fs.mkdir("/workspace/notes", { recursive: true });
let todo = "";
try {
todo = await this.workspace.fs.readFile(path, "utf8");
} catch (error) {
// A missing file means there are no to-do items yet.
if ((error as { code?: string }).code !== "ENOENT") throw error;
}
todo += `- [ ] ${item}\n`;
await this.workspace.fs.writeFile(path, todo);
return todo;
}
}
export default {
async fetch(request: Request, env: Env) {
const agent = env.NotesAgent.getByName("user-123");
return new Response(await agent.addTodo("Ship it"));
},
} satisfies ExportedHandler<Env>;在 Wrangler 配置中配置 Durable Object 绑定(binding)和 SQLite migration:
{
// Set this to today's date
"compatibility_date": "2026-11-14",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"class_name": "NotesAgent",
"name": "NotesAgent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["NotesAgent"]
}
]
}# Set this to today's date
compatibility_date = "2026-11-14"
compatibility_flags = [ "nodejs_compat" ]
[[durable_objects.bindings]]
class_name = "NotesAgent"
name = "NotesAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "NotesAgent" ]workspace.fs 对象提供处理文件系统的常用方法。它接受 string 或二进制 Uint8Array 内容。使用 ReadableStream 读取或写入大文件。
const { fs } = this.workspace;
// Write a string, bytes, or a stream.
await fs.writeFile("/workspace/data/blob.bin", new Uint8Array([1, 2, 3]));
await fs.writeFile("/workspace/uploads/report.csv", request.body);
// Read a file as text, or as a stream when you omit the encoding.
const text = await fs.readFile("/workspace/uploads/report.csv", "utf8");
const stream = await fs.readFile("/workspace/uploads/report.csv");
// List, search, and remove.
for (const entry of await fs.readdir("/workspace/notes")) {
console.log(entry.isDirectory ? `d ${entry.name}` : `f ${entry.name}`);
}
const matches = await fs.grep("TODO", "/workspace", { ignoreCase: true });
await fs.rm("/workspace/notes", { recursive: true });const { fs } = this.workspace;
// Write a string, bytes, or a stream.
await fs.writeFile("/workspace/data/blob.bin", new Uint8Array([1, 2, 3]));
await fs.writeFile("/workspace/uploads/report.csv", request.body!);
// Read a file as text, or as a stream when you omit the encoding.
const text = await fs.readFile("/workspace/uploads/report.csv", "utf8");
const stream = await fs.readFile("/workspace/uploads/report.csv");
// List, search, and remove.
for (const entry of await fs.readdir("/workspace/notes")) {
console.log(entry.isDirectory ? `d ${entry.name}` : `f ${entry.name}`);
}
const matches = await fs.grep("TODO", "/workspace", { ignoreCase: true });
await fs.rm("/workspace/notes", { recursive: true });所有文件操作请参阅 API 参考中的 workspace.fs。
Computer 为一套 workspace 构建一组文件 tools,并为不同 Agent 库提供 adapter:
| 工具 | 作用 |
|---|---|
read |
按行读取文本文件,或将图片或 PDF 传给接受它的模型。 |
ls |
分页列出一个目录。 |
find |
使用 *、** 和 ? glob 查找路径。 |
grep |
使用正则表达式或固定字符串搜索文件内容。 |
write |
写入整个文件。 |
edit |
替换文件中的目标文本并返回 diff。 |
delete |
删除文件或目录。 |
exec |
在 execution backend 上运行代码。仅在 workspace 有 backend 时出现。 |
每个 Think Agent 都在 this.workspace 拥有 Computer Workspace,Think 会将工作区 tools 添加到每一轮。要更改 workspace,请重写 workspace。getTools() 中与工作区 tool 同名的自定义 tool 会替换它。
更多信息请参阅内置工作区工具。
@cloudflare/computer/tools/ai-sdk 中的 createAITools() 会为 workspace 返回 AI SDK ToolSet。将其传给 generateText()、streamText(),或 Agent 的 getTools() 方法。
import { Workspace } from "@cloudflare/computer";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";
import { generateText, stepCountIs } from "ai";
import { DurableObject } from "cloudflare:workers";
import { createAI } from "agents/models/ai-sdk";
export class WriterAgent extends DurableObject {
workspace = new Workspace({ storage: this.ctx.storage });
async write(prompt) {
const { text } = await generateText({
model: createAI({ binding: this.env.AI })("@cf/moonshotai/kimi-k2.6"),
system:
"You are a writing assistant. Keep drafts and notes as files under /workspace.",
prompt,
tools: createAITools({
workspace: this.workspace,
read: { maxBytes: 32 * 1024, maxLines: 800 },
}),
stopWhen: stepCountIs(20),
});
return text;
}
}import { Workspace } from "@cloudflare/computer";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";
import { generateText, stepCountIs } from "ai";
import { DurableObject } from "cloudflare:workers";
import { createAI } from "agents/models/ai-sdk";
export class WriterAgent extends DurableObject<Env> {
workspace = new Workspace({ storage: this.ctx.storage });
async write(prompt: string) {
const { text } = await generateText({
model: createAI({ binding: this.env.AI })(
"@cf/moonshotai/kimi-k2.6",
),
system:
"You are a writing assistant. Keep drafts and notes as files under /workspace.",
prompt,
tools: createAITools({
workspace: this.workspace,
read: { maxBytes: 32 * 1024, maxLines: 800 },
}),
stopWhen: stepCountIs(20),
});
return text;
}
}@cloudflare/computer/tools/pi-ai 中的 createPiTools() 返回 Pi AI tool 声明,以及按名称运行调用的 execute 函数。Pi Durable 自行运行 Agent 循环,并期望每个 tool 携带自己的 execute,因此将它们配对为 ToolRegistration 对象:
import { createPiTools } from "@cloudflare/computer/tools/pi-ai";
// Running these again after an eviction is harmless.
const REPLAY_SAFE = new Set(["read", "ls", "find", "grep", "write", "delete"]);
export function workspaceTools(workspace) {
const { tools, execute } = createPiTools({ workspace });
return tools.map((tool) => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
replay: REPLAY_SAFE.has(tool.name) ? "safe" : "unsafe",
async execute(args, api, context) {
const { content, isError } = await execute(
{ id: api.callId, name: tool.name, arguments: args },
{ abortSignal: context.abortSignal },
);
return { output: content, isError };
},
}));
}import type { Workspace } from "@cloudflare/computer";
import { createPiTools } from "@cloudflare/computer/tools/pi-ai";
import type { ToolRegistration } from "@earendil-works/pi-durable";
// Running these again after an eviction is harmless.
const REPLAY_SAFE = new Set(["read", "ls", "find", "grep", "write", "delete"]);
export function workspaceTools(workspace: Workspace): ToolRegistration[] {
const { tools, execute } = createPiTools({ workspace });
return tools.map((tool): ToolRegistration => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
replay: REPLAY_SAFE.has(tool.name) ? "safe" : "unsafe",
async execute(args, api, context) {
const { content, isError } = await execute(
{ id: api.callId, name: tool.name, arguments: args },
{ abortSignal: context.abortSignal },
);
return { output: content, isError };
},
}));
}在 harness factory 内、Harness.open() 之前,将 tools 安装到 extension 中:
this.registry.install({
name: "workspace",
tools: workspaceTools(this.workspace),
});this.registry.install({
name: "workspace",
tools: workspaceTools(this.workspace),
});execute 会根据 tool 的 schema 验证参数,并将失败报告为 isError 结果,以便模型重试。
OpenCode 通过插件接收 tools。以下插件用工作区 tools 替换需要本地文件系统的 OpenCode tools:
import { createPiTools } from "@cloudflare/computer/tools/pi-ai";
// OpenCode tools that need a local file system or process.
const LOCAL_TOOLS = ["read", "write", "edit", "patch", "glob", "grep", "shell"];
export function workspacePlugin(workspace) {
const { tools, execute } = createPiTools({ workspace });
return {
id: "workspace",
async setup(ctx) {
await ctx.tool.transform((editor) => {
for (const id of LOCAL_TOOLS) editor.remove(id);
for (const tool of tools) {
editor.add({
name: tool.name,
description: tool.description,
input: tool.parameters,
// Offer each tool by name instead of through OpenCode's code mode.
options: { codemode: false },
async execute(input, context) {
const { content, isError } = await execute(
{ id: context.id, name: tool.name, arguments: input },
{ abortSignal: context.signal },
);
const text = content
.map((part) => (part.type === "text" ? part.text : ""))
.join("\n");
if (isError) throw new Error(text || `${tool.name} failed`);
return { content: text };
},
});
}
});
},
};
}import type { Workspace } from "@cloudflare/computer";
import { createPiTools } from "@cloudflare/computer/tools/pi-ai";
import type { Plugin } from "@opencode/plugin";
// OpenCode tools that need a local file system or process.
const LOCAL_TOOLS = ["read", "write", "edit", "patch", "glob", "grep", "shell"];
export function workspacePlugin(workspace: Workspace): Plugin.Plugin {
const { tools, execute } = createPiTools({ workspace });
return {
id: "workspace",
async setup(ctx) {
await ctx.tool.transform((editor) => {
for (const id of LOCAL_TOOLS) editor.remove(id);
for (const tool of tools) {
editor.add({
name: tool.name,
description: tool.description,
input: tool.parameters,
// Offer each tool by name instead of through OpenCode's code mode.
options: { codemode: false },
async execute(input, context) {
const { content, isError } = await execute(
{ id: context.id, name: tool.name, arguments: input },
{ abortSignal: context.signal },
);
const text = content
.map((part) => (part.type === "text" ? part.text : ""))
.join("\n");
if (isError) throw new Error(text || `${tool.name} failed`);
return { content: text };
},
});
}
});
},
};
}将插件添加到你在 harness factory 中传给 OpenCodeWorkerd.create() 的 plugins 数组:
OpenCodeWorkerd.create({
storage,
plugins: [this.ai.plugin, workspacePlugin(this.workspace)],
});OpenCodeWorkerd.create({
storage,
plugins: [this.ai.plugin, workspacePlugin(this.workspace)],
});createAITools()、createPiTools() 和 createTanStackTools() 接受相同选项:
| 选项 | 默认值 | 描述 |
|---|---|---|
workspace |
必填 | Workspace,或来自 getWorkspace() 的 client。 |
readonly |
false |
只提供 read、ls、find 和 grep。 |
read |
默认上限 | read 的限制,例如 maxBytes、maxLines 和 includeLineNumbers。 |
write |
默认上限 | write 的限制。 |
edit |
默认上限 | edit 的限制。 |
exec |
每个 backend | 从 backend ID 到 { description } 的映射。传入 {} 可省略 exec tool。 |
要让模型读取和搜索文件但不能更改它们,请传入 readonly: true:
createAITools({ workspace: this.workspace, readonly: true });createAITools({ workspace: this.workspace, readonly: true });添加 WorkerJavaScriptBackend,让 Agent 或模型针对工作区文件运行 JavaScript。每次运行都是在新的 Dynamic Worker 中执行的 ECMAScript 模块。它比容器更快、更便宜。网络访问可配置,但默认关闭;模块通过 node:fs/promises 访问 workspace。
import { Workspace } from "@cloudflare/computer";
import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
import { DurableObject } from "cloudflare:workers";
export class NotesAgent extends DurableObject {
workspace = new Workspace({
storage: this.ctx.storage,
backends: [new WorkerJavaScriptBackend({ loader: this.env.LOADER })],
});
}import { Workspace } from "@cloudflare/computer";
import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
import { DurableObject } from "cloudflare:workers";
export class NotesAgent extends DurableObject<Env> {
workspace = new Workspace({
storage: this.ctx.storage,
backends: [new WorkerJavaScriptBackend({ loader: this.env.LOADER })],
});
}在 Think Agent 中,将同一个 Workspace 设为 override workspace。一旦 workspace 有 backend,每个 tool adapter 都会包含 exec。
该 backend 需要 Worker Loader 绑定:
{
// Set this to today's date
"compatibility_date": "2026-11-14",
"compatibility_flags": ["nodejs_compat"],
"worker_loaders": [
{
"binding": "LOADER"
}
]
}# Set this to today's date
compatibility_date = "2026-11-14"
compatibility_flags = [ "nodejs_compat" ]
[[worker_loaders]]
binding = "LOADER"exec tool 会向模型描述 backend,包括其代码可以导入的模块,因此模型无需额外说明即可编写如下模块:
import fs from "node:fs/promises";
export default async function (input) {
const names = await fs.readdir("/workspace/notes");
const counts = {};
for (const name of names) {
const text = await fs.readFile(`/workspace/notes/${name}`, "utf8");
counts[name] = text.split(/\s+/).filter(Boolean).length;
}
return counts;
}模块必须有 default export。default-exported 函数会使用本次运行的 input 调用,其返回值成为结果。将文件和模块调用放在该函数内。Workers runtime 不允许在模块加载时进行 I/O,因此在模块顶层访问文件系统会导致运行失败。
你自己的代码可以通过 workspace.runtime.exec() 运行模块。将结构化数据作为 input 传入,并从 value 读取返回值:
using run = await this.workspace.runtime.exec(
`
import fs from "node:fs/promises";
export default async function ({ path }) {
const text = await fs.readFile(path, "utf8");
return { lines: text.split("\\n").length };
}
`,
{ input: { path: "/workspace/notes/todo.md" }, encoding: "utf8" },
);
const { exitCode, value, stdout, stderr } = await run.result();using run = await this.workspace.runtime.exec(
`
import fs from "node:fs/promises";
export default async function ({ path }) {
const text = await fs.readFile(path, "utf8");
return { lines: text.split("\\n").length };
}
`,
{ input: { path: "/workspace/notes/todo.md" }, encoding: "utf8" },
);
const { exitCode, value, stdout, stderr } = await run.result();console.log() 写入 stdout,console.error() 写入 stderr。input 和返回值必须是 JSON 兼容的普通数据。模块也可以用相对导入从 workspace 导入其他文件,例如 import { summarize } from "./lib/summarize.js",它们会相对本次运行的 cwd 选项解析。
WorkerJavaScriptBackend 接受以下常用选项:
| 选项 | 默认值 | 描述 |
|---|---|---|
loader |
必填 | Worker Loader 绑定。 |
id |
"worker-javascript" |
exec 和 runtime.exec() 用于选择该 backend 的 ID。 |
root |
/workspace |
代码可以访问的目录。其外的路径会被拒绝。 |
access |
"read-write" |
设为 "read" 可让代码读取文件但不能更改它们。 |
defaultTimeoutMs |
60000 |
调用方未设置超时时,一次运行可持续的时间。 |
maxTimeoutMs |
180000 |
调用方可请求的最长超时。 |
globalOutbound |
null |
接收代码发出的每个出站请求的 Fetcher。null 会阻止网络访问。 |
modules |
— | 代码可以导入的额外模块,例如打包的库或在 Agent 中运行的函数。 |
onInterrupt |
"fail" |
重启中断运行时会发生什么。请参阅重启后恢复运行。 |
onWait |
— | 宿主模块调用开始等待答复时调用。 |
自定义模块可以注册在 ws: 命名空间下。使用这些模块让模型访问你的应用逻辑或文件系统 tools,或执行外部操作。
// In the `backends` list of the workspace.
new WorkerJavaScriptBackend({
loader: this.env.LOADER,
modules: {
"ws:orders": {
// Arguments arrive as an array of JSON-compatible values.
lookup: ([orderId]) => this.lookUpOrder(String(orderId)),
},
},
});// In the `backends` list of the workspace.
new WorkerJavaScriptBackend({
loader: this.env.LOADER,
modules: {
"ws:orders": {
// Arguments arrive as an array of JSON-compatible values.
lookup: ([orderId]) => this.lookUpOrder(String(orderId)),
},
},
});代码像导入其他模块一样导入它:
import { lookup } from "ws:orders";
export default async function ({ ids }) {
return Promise.all(ids.map((id) => lookup(id)));
}始终验证传入模块函数的 input,因为它直接来自模型。Computer 还包含用于 Git、Artifacts 和 Agent 自身 tools 的预构建 host modules。更多信息请参阅 API 参考中的 Host modules。
部署或崩溃会重启 Durable Object,并结束所有正在运行的 JavaScript。onInterrupt 选项决定这些运行的后续处理方式。请在后端设置默认值,也可以在 exec() 中为单次运行覆盖该值:
new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay" });
await this.workspace.runtime.exec(source, { onInterrupt: "fail" });new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay" });
await this.workspace.runtime.exec(source, { onInterrupt: "fail" });| 值 | 重启后 |
|---|---|
"fail"(默认值) |
运行失败。 |
"restart" |
运行会从头开始。已进行的调用会再次执行,输出也会重复。 |
"replay" |
运行会从头开始,但已进行的调用会从日志中获取答复,输出不会重复。随后运行会继续实时执行。 |
无论采用哪种策略,都无需修改正在运行的代码。
可重放运行会在代码看到结果之前,将每次文件系统调用、宿主模块调用和 fetch 响应写入 SQLite 日志。重放时,代码会按相同顺序收到相同响应。时钟、计时器、Math.random() 和 crypto.randomUUID() 也会返回相同值,因此运行会遵循相同路径。运行完成后,会删除该运行的所有存储内容。
中断的运行会在下次调用 workspace.runtime 时恢复,例如通过 exec() 或 getExec() 调用。部署后,Durable Object 不会因其他原因被唤醒。若要在没有流量时恢复运行,请从 Durable Object 的闹钟调用 workspace.runtime.recover()。
重启时仍在运行的调用尚未返回,因此重放时会再次执行。每次宿主模块调用都会获得一个在重新运行期间保持不变的 callId,你可以将它用作幂等键。若某个函数无法使用幂等键,请将其包装在 unsafeToRepeat() 中。此调用期间发生中断时,运行会失败。
import { unsafeToRepeat } from "@cloudflare/computer";
new WorkerJavaScriptBackend({
loader: this.env.LOADER,
onInterrupt: "replay",
modules: {
"ws:billing": {
refund: ([order], { callId }) =>
this.refund(String(order), { idempotencyKey: callId }),
charge: unsafeToRepeat(([order, cents]) =>
this.charge(String(order), Number(cents)),
),
},
},
});import { unsafeToRepeat } from "@cloudflare/computer";
new WorkerJavaScriptBackend({
loader: this.env.LOADER,
onInterrupt: "replay",
modules: {
"ws:billing": {
refund: ([order], { callId }) =>
this.refund(String(order), { idempotencyKey: callId }),
charge: unsafeToRepeat(([order, cents]) =>
this.charge(String(order), Number(cents)),
),
},
},
});宿主模块函数可以暂停可重放运行,直到有人作出答复,例如批准某项操作。隔离区中的代码照常调用该函数,并在收到答复后获得结果。
new WorkerJavaScriptBackend({
loader: this.env.LOADER,
onInterrupt: "replay",
modules: {
"ws:github": {
postComment: async ([issue, body], ctx) => {
const { approved } = await ctx.interrupt({
issue,
body,
});
if (!approved) throw new Error("The comment was rejected.");
return this.postComment(issue, body, { idempotencyKey: ctx.callId });
},
},
},
});new WorkerJavaScriptBackend({
loader: this.env.LOADER,
onInterrupt: "replay",
modules: {
"ws:github": {
postComment: async ([issue, body], ctx) => {
const { approved } = await ctx.interrupt<{ approved: boolean }>({
issue,
body,
});
if (!approved) throw new Error("The comment was rejected.");
return this.postComment(issue, body, { idempotencyKey: ctx.callId });
},
},
},
});ctx.interrupt(payload) 会在该运行的其他调用完成后释放其隔离区。运行仍保持运行状态,等待所花费的时间不会计入超时。等待中的运行与其他中断运行一样,可以跨部署保留。
使用 workspace.runtime.pending() 列出等待中的调用,并使用 respond() 回答其中一个:
for (const call of await this.workspace.runtime.pending()) {
await this.workspace.runtime.respond(call.id, call.callId, {
approved: true,
});
}for (const call of await this.workspace.runtime.pending()) {
await this.workspace.runtime.respond(call.id, call.callId, {
approved: true,
});
}当没有调用处于等待状态时,运行会重放到该调用处,函数会以相同的 callId 再次运行。这一次,ctx.interrupt() 会立即返回答复。如需在调用开始等待时通知某人,请将 onWait 函数传给后端。它每次等待最多调用一次,并接收与 pending() 相同的字段。killExec() 会结束等待中的运行。
- 一次调用只能中断一次。同一调用中第二次调用
ctx.interrupt()会抛出异常。 - 只有
onInterrupt: "replay"的运行可以等待。其他运行调用ctx.interrupt()都会抛出异常。 - 对于未处于等待状态的调用(例如已答复的调用),
respond()会抛出ENOTWAITING错误。
以下限制适用于实验版本,未来版本中可能会有所变化。
- 重放必须遵循与首次尝试相同的路径。如果调用或输出了不同内容,运行就会失败。
crypto.subtle.generateKey()、node:crypto随机数、node:timers和cloudflare:sockets生成的值不会重放。 - 可重放运行会缓冲
fetch响应,而非流式传输;默认情况下,每个响应正文必须能放入 1 MiB。请求会经过 Durable Object,因此比其他运行方式慢。重启时正在处理的请求会再次发送。 - 宿主模块函数会在收到答复后再次运行,因此
ctx.interrupt()之前的工作必须能够安全重做。每次答复都会从头重放运行。 - 每次记录调用时,运行最多尝试三次。运行必须在其原始超时时间内完成,超时从首次启动时开始计算。
- 运行只能在相同运行时上恢复。新的兼容性日期或标志、后端上的新大小限制,或改变运行重放方式的程序包升级,都会导致被其打断的运行失败。
- 中断的运行会一次重启四个,因为一个 Durable Object 最多可同时运行十个 Dynamic Worker。
- 运行完成前,其日志会占用 Durable Object 存储空间,大小约为每个响应的大小总和再加上该运行的代码。
- 每个 workspace 共享其 Durable Object 的存储。存储限制请参阅 Durable Objects 限制。请按 Agent 的工作文件为 workspace 规划容量,而不是完整 monorepo 或大型依赖树。
- JavaScript backend 运行 ECMAScript 模块,而不是 shell 命令或原生二进制。当 Agent 需要 Linux 环境时,使用沙箱。
- 每次 JavaScript 运行都受 CPU 时间、wall-clock 时间、输出大小以及文件操作次数和大小的限制。已完成的运行默认保留 60 分钟。
模型会读取文件 tools 和 exec 返回的所有内容。文件可能包含用于引导模型的文本,例如指示删除其他文件。在 Agent 于 workspace 外根据 tool 结果采取行动之前请先检查这些结果,并对有副作用的 tool 要求审批。
JavaScript backend 运行的代码无法读取 Agent 的环境变量、绑定或存储。它只能访问 root 下的文件、你配置的模块,以及在传入 globalOutbound 时的网络。Host modules 以 Agent 的完整权限运行,因此只暴露模型编写的代码所需的函数。
与同一 Agent 实例对话的每个用户共享其 workspace。当用户文件必须隔离时,请为每个用户提供独立的 Agent 实例。
@cloudflare/computer 中的 new Workspace(options) 创建 workspace。它接受以下选项:
| 选项 | 描述 |
|---|---|
storage |
必填。Durable Object 的存储,this.ctx.storage。 |
backends |
Execution backends,例如 WorkerJavaScriptBackend。第一个 backend 是默认值。 |
mounts |
挂载到路径的只读内容,来自 R2 bucket(R2Bucket())或 Worker bundle(WorkerBundle())。 |
git |
来自 @cloudflare/computer/git 中 createGitClient() 的 Git client,会启用 workspace.git。 |
defaultGitIdentity |
Git commits 使用的 { name, email }。 |
artifacts |
Artifacts 绑定,形式为 { binding }。 |
output |
长命令输出的保存位置,以及 result() 保留多少内容。 |
每个路径都是绝对路径。每个方法都返回 promise。
| 方法 | 描述 |
|---|---|
readFile(path, encoding?) |
读取文件。使用 "utf8" 返回字符串,不提供 encoding 时返回 ReadableStream。接受 { byteOffset, byteLength } 以读取文件的一部分。 |
writeFile(path, content, options?) |
写入字符串、Uint8Array 或 ReadableStream。设置 options.mode 以指定权限。 |
mkdir(path, options?) |
创建目录。设置 recursive: true 以创建父目录。 |
readdir(path) |
将目录列为带 name、parentPath、isFile 和 isDirectory 的条目。 |
stat(path) |
返回 name、mode、mtime、size、isFile 和 isDirectory,并跟随符号链接。 |
lstat(path) |
类似 stat,但报告符号链接本身,带 isSymbolicLink。 |
rm(path, options?) |
删除文件或目录。对非空目录设置 recursive: true,对缺失路径设置 force: true 以忽略。 |
rename(oldPath, newPath) |
在一次事务中移动文件、目录或符号链接。 |
symlink(target, path) |
在 path 创建指向 target 的符号链接。 |
readlink(path) |
返回符号链接的目标。 |
chmod(path, mode) |
更改路径的权限位。 |
find(directory, pattern?, options?) |
查找相对 directory 匹配 glob 的路径。接受 exclude、limit 和 offset。 |
ls(prefix) |
列出 prefix 处或其下的每个文件。 |
grep(pattern, path, options?) |
搜索文件内容。接受 regex、ignoreCase、context、include、exclude、limit 和 offset。 |
失败的操作会以错误拒绝,其 code 是 POSIX 错误代码,例如缺失路径时的 ENOENT。
| 方法 | 描述 |
|---|---|
exec(source, options?) |
在 backend 上运行 source 并返回 handle。在 handle 上调用 result() 以获取最终结果。 |
getExec(id, options?) |
按 ID 重新附着到一次运行。 |
killExec(id, options?) |
停止一次运行。 |
disposeExec(id, options?) |
丢弃已完成的运行。 |
backends() |
列出 backends 以及每个 backend 提供给模型的描述。 |
recover() |
恢复中断的运行。请从闹钟中调用。实验性功能。 |
pending() |
列出正在等待答复的宿主模块调用。实验性功能。 |
respond(id, callId, answer, options?) |
答复等待中的调用。实验性功能。 |
exec() 接受以下选项:
| 选项 | 描述 |
|---|---|
backend |
要运行的 backend ID。默认为第一个 backend。 |
input |
传给 JavaScript 模块 default export 的 JSON 兼容数据。 |
encoding |
设为 "utf8" 可将 stdout 和 stderr 作为字符串而不是字节接收。 |
cwd |
工作目录,也是相对导入的基准。 |
env |
本次运行的环境变量,JavaScript 可通过 process.env 使用。 |
stdin |
标准输入,为字符串或 Uint8Array。 |
timeoutMs |
运行可持续的时间,最长为 backend 的 maxTimeoutMs。 |
onInterrupt |
重启中断此运行时的处理方式。此选项会覆盖后端默认值。 |
结果包含 status("completed"、"failed" 或 "cancelled")、exitCode、stdout、stderr,以及 JavaScript 模块的返回值 value。当输出过长时,truncated 会给出保存全部输出的工作区文件名。
用 using 声明来自 exec() 的 handle,以便它们离开作用域时被 dispose。
| 函数 | 入口 | 返回值 |
|---|---|---|
createAITools() |
@cloudflare/computer/tools/ai-sdk |
AI SDK 的 ToolSet。 |
createPiTools() |
@cloudflare/computer/tools/pi-ai |
Pi AI tool 声明和 execute 函数。 |
createTanStackTools() |
@cloudflare/computer/tools/tanstack-ai |
TanStack AI ↗ tools 列表。 |
将这些传给 WorkerJavaScriptBackend 的 modules 选项:
| 函数 | 入口 | 模块 | 描述 |
|---|---|---|---|
createGitModule() |
@cloudflare/computer/modules/git |
ws:git |
对 workspace 的 Git 操作。需要 git workspace 选项。传入 { allowNetwork: true } 以 clone、fetch 和 push。 |
createArtifactsModule() |
@cloudflare/computer/modules/artifacts |
ws:artifacts |
Artifacts 仓库。需要 artifacts workspace 选项。 |
createToolBindings() |
@cloudflare/computer/modules/tools |
ws:tools |
Agent 自身的 tools,可从代码调用。使用 forAISDKTools() 或 forPiTools() 构建绑定。 |
完整接口请参阅 Computer 设计文档 ↗。