工具与系统提示词 section 都通过 extension 提供给 Pi Harness。extension 属于 Pi Durable:带 name、tools 与 sections 的普通对象,安装在你传给 Harness.open() 的 registry 上。PiHarness 不会包装它们,因此所有 Pi Durable extension 功能都可用。
用 createRegistry() 创建 registry,并在 harness factory 中、Harness.open() 之前安装 extension:
import { DurableObject } from "cloudflare:workers";
import { Type } from "@earendil-works/pi-ai";
import { createModels } from "@earendil-works/pi-ai/models";
import { createRegistry, Harness } from "@earendil-works/pi-durable";
import { PiHarness } from "agents/harness/pi";
import { Lifecycle } from "agents/lifecycle";
import { createAI } from "agents/models/pi-ai";
const WordCount = Type.Object({ text: Type.String() });
const wordCount = {
name: "word_count",
description: "Count the words in a text.",
parameters: WordCount,
replay: "safe",
// `text` is typed from `parameters`, which Pi validates first.
async execute({ text }) {
const words = text.split(/\s+/).filter(Boolean).length;
return { output: [{ type: "text", text: String(words) }] };
},
};
export class Editor extends DurableObject {
ai = createAI({ binding: this.env.AI });
registry = createRegistry();
harness = new PiHarness({
harness: ({ storage, context, onWake }) => {
this.registry.install({
name: "editor",
sections: [
{ key: "preamble", render: () => "You are an editor.", tag: false },
],
tools: [wordCount],
});
const models = createModels();
models.setProvider(this.ai.provider);
return Harness.open(
storage,
{ models, registry: this.registry, onWake },
context,
);
},
defaults: { model: this.ai("@cf/zai-org/glm-5.3-flash") },
});
lifecycle = Lifecycle.install(this).use(this.harness);
}import { DurableObject } from "cloudflare:workers";
import { Type } from "@earendil-works/pi-ai";
import { createModels } from "@earendil-works/pi-ai/models";
import {
createRegistry,
Harness,
type ToolRegistration,
} from "@earendil-works/pi-durable";
import { PiHarness } from "agents/harness/pi";
import { Lifecycle } from "agents/lifecycle";
import { createAI } from "agents/models/pi-ai";
const WordCount = Type.Object({ text: Type.String() });
const wordCount: ToolRegistration<typeof WordCount> = {
name: "word_count",
description: "Count the words in a text.",
parameters: WordCount,
replay: "safe",
// `text` is typed from `parameters`, which Pi validates first.
async execute({ text }) {
const words = text.split(/\s+/).filter(Boolean).length;
return { output: [{ type: "text", text: String(words) }] };
},
};
export class Editor extends DurableObject<Env> {
ai = createAI({ binding: this.env.AI });
registry = createRegistry();
harness = new PiHarness({
harness: ({ storage, context, onWake }) => {
this.registry.install({
name: "editor",
sections: [
{ key: "preamble", render: () => "You are an editor.", tag: false },
],
tools: [wordCount],
});
const models = createModels();
models.setProvider(this.ai.provider);
return Harness.open(
storage,
{ models, registry: this.registry, onWake },
context,
);
},
defaults: { model: this.ai("@cf/zai-org/glm-5.3-flash") },
});
lifecycle = Lifecycle.install(this).use(this.harness);
}factory 在 object 启动期间运行;如果 harness 在存储错误后重新打开 Pi,它也会再次运行。registry.install() 会替换同名的已安装 extension,因此可以安全地在 factory 中重复安装 extension。
请为每个 object 创建 registry,并将其作为 class field,而不是在 module scope 中创建。Pi 会让每个 Harness 一直订阅其 registry,直到 Harness 关闭;被驱逐的 object 永远不会关闭它。
工具是 Pi Durable 的 ToolRegistration:
- 带类型的参数。 将工具类型写为
ToolRegistration<typeof Parameters>,execute会获得来自 schema 的参数类型。Pi 在调用execute前按parameters校验它们。裸ToolRegistration将参数类型为unknown。 - 运行中的调用可用内容。
execute(args, api, context)获得 Pi 为该调用提供的操作。api.output()流式输出运行中输出,api.details()发送供 UI 使用的数据,api.memo()存储在驱逐后仍然保留的值。 - Abort。 调用被 abort 时
context.abortSignal被 abort。请遵守它:session.abort()会等到每个正在运行的工具返回。 - 并行调用。 Pi 默认同时运行同一模型轮次中的工具调用。对必须依次运行的工具设置
executionMode: "sequential"。一次 sequential 调用会使其整轮变为 sequential。
const FetchPage = Type.Object({ url: Type.String() });
const fetchPage: ToolRegistration<typeof FetchPage> = {
name: "fetch_page",
description: "Fetch a web page as text.",
parameters: FetchPage,
replay: "safe",
async execute({ url }, api, context) {
api.output(`fetching ${url}\n`);
const response = await fetch(url, { signal: context.abortSignal });
return { output: [{ type: "text", text: await response.text() }] };
},
};驱逐中断工具调用时,Pi 根据工具的 replay 设置决定如何处理:
replay: "safe":对象重启后 Pi 再次运行该工具。replay: "unsafe"(默认):Pi 不会再次运行它。模型得到中断结果并决定下一步。
当两次运行无害时将工具标记为 safe,例如读取、搜索或整文件写入。当第二次运行会失败或重复副作用时保持 unsafe,例如已替换文本的编辑、运行代码或发送邮件。
section 的 render 在每次模型请求前运行。它接收对话、其 Agent 与提供的工具,以及已提交的读取,因此 section 可以随请求变化。除非 tag 为 false,Pi 会将输出包在 <key> 标签中:
this.registry.install({
name: "context",
sections: [
{ key: "preamble", render: () => "You are an editor.", tag: false },
{ key: "today", render: () => new Date().toDateString() },
],
});来自 agents/harness/pi 的 skills(sources) 会将 agents/skills 中的 Agent Skills source 转换为名为 agents.skills 的 Pi extension。source 可以从 Computer Workspace、R2 或 manifest 读取 skill。extension 有两个 tool:activate_skill 和 read_skill_resource,以及列出 skill 的 skills section。它会读取 source,因此请将其安装在 async factory 中:
import { skills } from "agents/harness/pi";
import { manifest } from "agents/skills";
const handbook = manifest({
id: "handbook",
skills: [
{
name: "release-notes",
description: "Write release notes.",
body: "One line per change.",
},
],
});
harness = new PiHarness({
harness: async ({ storage, context, onWake }) => {
this.registry.install(await skills([handbook]));
// ...models, then Harness.open() with onWake
},
});这些 tool 与 Think 提供的 tool 匹配,因此为其中一个编写的 skill 也适用于另一个。factory 构建 extension 时只读取一次 source。模型可以读取 skill 的说明和资源。要运行 skill script,请为模型提供 shell 或 Workspace tool。请参阅执行。
因为 registry 属于 Pi,Pi Durable extension 能做的其余事情同样可用:
- 模型请求、工具调用与 compaction 上的 hook,例如
hook(ToolTask, { beforeTool })。 - 重启后恢复的持久自定义任务。
- 包装另一 extension 的工具与 section。
- 按对话选择 extension。
- 带类型文档中的 extension 状态。
各项请参阅 Pi Durable README ↗。