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

在 sandbox 中运行 Pi

最后更新 查看 MarkdownAgent 设置

在 Linux sandbox 中运行 Pi ↗ 编程 Agent,根据提示编辑 GitHub 仓库,然后将其更改作为 diff 读取。Pi 通过内置 provider 经 AI Gateway 调用模型。你的 Worker 会为这些请求添加 gateway token,且 sandbox 只能访问你的 gateway 和 github.com。

前提条件

  • 来自 构建编程 Agent runner 的 sandbox-coding-agent 项目。本页会替换镜像、模型,以及 Durable Object 中与 Agent 相关的部分。
  • AI Gateway 中的 Anthropic 凭据,可以是 Unified Billing 额度,或作为 provider key 存储的 Anthropic API key。

将 Worker 切换到 Pi

  1. 替换 Dockerfile。只有高亮行与教程中的 Dockerfile 不同:

    Dockerfiledockerfile
    FROM node:24-trixie-slim
    
    RUN apt-get update \
    	&& apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \
    	&& rm -rf /var/lib/apt/lists/*
    
    RUN npm install --global --ignore-scripts @earendil-works/[email protected]
    
    COPY --from=docker.io/cloudflare/sandbox:1.0.1 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim
    
    WORKDIR /workspace
    CMD ["sleep", "infinity"]

    镜像会固定版本,因为 Pi 的标志和事件格式可能在版本之间变化。

  2. 在 wrangler.jsonc 中,将 MODEL 设为 Pi 中 cloudflare-ai-gateway provider 的模型 ID:

    {
    	"vars": {
    		"AI_GATEWAY_ACCOUNT_ID": "<ACCOUNT_ID>",
    		"AI_GATEWAY_ID": "default",
    		"MODEL": "claude-sonnet-5",
    	},
    }
    [vars]
    AI_GATEWAY_ACCOUNT_ID = "<ACCOUNT_ID>"
    AI_GATEWAY_ID = "default"
    MODEL = "claude-sonnet-5"

    请使用 Anthropic 等 provider 的模型。对于其目录中的大多数 Workers AI 模型,Pi 0.87.1 会请求与上下文窗口一样多的输出 token。Workers AI 会拒绝这些请求。

  3. 在 src/sandbox.ts 中,将 ResultEvent 常量替换为报告结果的 Pi 事件 schema:

    src/sandbox.tsts
    const PiEvent = z.union([
    	z.object({ type: z.literal("agent_settled") }),
    	z.object({
    		type: z.literal("message_end"),
    		message: z.object({
    			role: z.literal("assistant"),
    			content: z.array(
    				z.object({ type: z.string(), text: z.string().optional() }),
    			),
    			stopReason: z.string(),
    			errorMessage: z.string().optional(),
    		}),
    	}),
    ]);
    
    type PiMessage = Extract<
    	z.infer<typeof PiEvent>,
    	{ type: "message_end" }
    >["message"];
  4. 替换 agentCommand() 和 agentEnv() 方法:

    src/sandbox.tsts
    export class AgentSandbox extends DurableObject<Env> {
    	// ...
    
    	private agentCommand(prompt: string): string[] {
    		return [
    			"pi",
    			// Run one task, write one JSON event per line to the events file, and exit
    			"--mode",
    			"json",
    			// Keep the session in memory instead of saving it to disk
    			"--no-session",
    			// Select the built-in AI Gateway provider in Pi, which reads the account
    			// and gateway IDs from the environment
    			"--provider",
    			"cloudflare-ai-gateway",
    			// Select the model from `MODEL`
    			"--model",
    			this.env.MODEL,
    			"--",
    			prompt,
    		];
    	}
    
    	private agentEnv(): Record<string, string> {
    		return {
    			CLOUDFLARE_API_KEY: "provided-by-worker",
    			CLOUDFLARE_ACCOUNT_ID: this.env.AI_GATEWAY_ACCOUNT_ID,
    			CLOUDFLARE_GATEWAY_ID: this.env.AI_GATEWAY_ID,
    			PI_OFFLINE: "1",
    			PI_SKIP_VERSION_CHECK: "1",
    			PI_TELEMETRY: "0",
    		};
    	}
    }

    Pi 运行工具时不会出现批准提示。container 会限制 Pi 命令可以访问的范围。

    没有 API key 时 Pi 不会启动,因此 CLOUDFLARE_API_KEY 是占位符。Pi 会在 cf-aig-authorization header 中发送该 key,Outbound entrypoint 会用 gateway token 替换它。PI_OFFLINE、PI_SKIP_VERSION_CHECK 和 PI_TELEMETRY 会关闭 Pi 中的模型目录刷新、版本检查和遥测。

    Pi 仍会从仓库加载 AGENTS.md 和 CLAUDE.md 文件。没有 --approve 标志时,它会跳过仓库 .pi 目录中受保护的项目资源。对于你不信任的仓库,请不要加上 --approve。

  5. 替换 outcome() 方法:

    src/sandbox.tsts
    export class AgentSandbox extends DurableObject<Env> {
    	// ...
    
    	private async outcome(exitCode: number): Promise<TaskStatus> {
    		if (exitCode !== 0) {
    			const stderr = (await this.readOptionalText(stderrPath)) ?? "";
    			return {
    				state: "failed",
    				error: `pi exited with ${exitCode}: ${stderr.slice(-2000)}`,
    			};
    		}
    
    		let settled = false;
    		let final: PiMessage | undefined;
    		const events = await this.files.readFile(eventsPath);
    
    		for await (const line of readLines(events.body)) {
    			const parsed = PiEvent.safeParse(parseJson(line));
    
    			if (!parsed.success) {
    				continue;
    			}
    
    			if (parsed.data.type === "agent_settled") {
    				settled = true;
    			} else {
    				final = parsed.data.message;
    			}
    		}
    
    		if (!settled || final === undefined) {
    			return { state: "failed", error: "pi exited before the agent settled" };
    		}
    
    		if (final.stopReason === "error" || final.stopReason === "aborted") {
    			return {
    				state: "failed",
    				error: final.errorMessage ?? `pi stopped: ${final.stopReason}`,
    			};
    		}
    
    		const text = final.content.flatMap((block) =>
    			block.type === "text" ? [block.text ?? ""] : [],
    		);
    		return { state: "succeeded", result: text.join("") };
    	}
    }

    即使模型请求失败,Pi 也会以成功状态退出,因此结果来自其事件。当 Pi 发送 agent_settled 事件,且其最后一条 assistant 消息的 stopReason 不是 error 或 aborted 时,任务成功。否则,任务会以 Pi 的错误消息失败。

  6. 部署你的 Worker:

    npx wrangler deploy

    教程中的 AI_GATEWAY_TOKEN secret 会继续保留。正在运行的 container 会保留启动时使用的镜像,因此请为 Pi 使用新的 sandbox 名称。

运行任务

  1. 将仓库克隆到名为 pi-1 的 sandbox:

    curl "$WORKER_URL/sandboxes/pi-1/repository" \
    	--json '{"url": "https://github.com/octocat/Hello-World"}'
  2. 启动任务:

    curl "$WORKER_URL/sandboxes/pi-1/task" \
    	--data "Add a file NOTES.md with a one-sentence summary of this repository. Do not commit."
  3. 检查任务,直到其状态为 succeeded 或 failed:

    curl "$WORKER_URL/sandboxes/pi-1/task"

    succeeded 任务会包含 Pi 最后一条回复的文本。

  4. 读取更改:

    curl "$WORKER_URL/sandboxes/pi-1/diff"

    diff 会显示新的 NOTES.md 文件。

相关资源

这篇文档对您有帮助吗?