跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

文件系统

最后更新 查看 MarkdownAgent 设置

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 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

在 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。

向模型提供文件 tools

Computer 为一套 workspace 构建一组文件 tools,并为不同 Agent 库提供 adapter:

工具 作用
read 按行读取文本文件,或将图片或 PDF 传给接受它的模型。
ls 分页列出一个目录。
find 使用 *、** 和 ? glob 查找路径。
grep 使用正则表达式或固定字符串搜索文件内容。
write 写入整个文件。
edit 替换文件中的目标文本并返回 diff。
delete 删除文件或目录。
exec 在 execution backend 上运行代码。仅在 workspace 有 backend 时出现。

Think

每个 Think Agent 都在 this.workspace 拥有 Computer Workspace,Think 会将工作区 tools 添加到每一轮。要更改 workspace,请重写 workspace。getTools() 中与工作区 tool 同名的自定义 tool 会替换它。

更多信息请参阅内置工作区工具。

AI SDK

@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;
    }
}

Pi

@cloudflare/computer/tools/pi-ai 中的 createPiTools() 返回 Pi AI tool 声明,以及按名称运行调用的 execute 函数。Pi Durable 自行运行 Agent 循环,并期望每个 tool 携带自己的 execute,因此将它们配对为 ToolRegistration 对象:

src/workspace-tools.jsjs
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 };
		},
	}));
}
src/workspace-tools.tsts
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

OpenCode 通过插件接收 tools。以下插件用工作区 tools 替换需要本地文件系统的 OpenCode tools:

src/workspace-plugin.jsjs
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 };
						},
					});
				}
			});
		},
	};
}
src/workspace-plugin.tsts
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)],
});

配置 tools

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 });

在 workspace 中运行 JavaScript

添加 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,因此在模块顶层访问文件系统会导致运行失败。

从 Agent 运行代码

你自己的代码可以通过 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 选项解析。

配置 backend

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 实例。

API 参考

Workspace

@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() 保留多少内容。

workspace.fs

每个路径都是绝对路径。每个方法都返回 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。

workspace.runtime

方法 描述
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。

Tool 函数

函数 入口 返回值
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 设计文档 ↗。

相关资源

沙箱

为 Agent 提供 Linux 沙箱,用于运行 shell 命令、脚本和测试。

Think 工具

Think Agent 的内置工作区 tools、自定义 tools 和审批。

这篇文档对您有帮助吗?