Pi
Last updated Oct 22, 2026
is a minimal, extensible agent harness. The Agents SDK provides first-class support for building agents using the Pi harness, and this page shows how to run Pi Durable ↗︎ in a Cloudflare Durable Object with PiHarness from the Agents SDK.
Pi runs the agent loop: the transcript, the inbox of follow-ups and steers, model calls, tools, retries, and crash recovery. PiHarness gives Pi storage in the Durable Object's SQLite database, and wakes the object when Pi has work to finish.







Copy promptPrompt copied!
To start from a complete app, deploy the Pi harness example ↗︎. It streams sessions to a browser UI over WebSockets, and gives Pi a durable filesystem and JavaScript execution from @cloudflare/computer.
Beta
PiHarness is in beta. Pi Durable ↗︎ is a new, experimental package, and the PiHarness API will likely change as Pi Durable matures.
How it works
PiHarness is a new "Lifecycle capability" provided by the Cloudflare Agents SDK. Pi Durable provides the agent harness and the Lifecycle ↗︎ is responsible for keeping the agent running in the Durable Object. The Lifecycle is a core concept in the Agents SDK ensuring that long-running work can run in a Durable Object, surviving restarts, crashes, and network issues. A Lifecycle capability is a reusable piece of a Durable Object that the Lifecycle starts and gives SQLite storage and a durable job queue. PiHarness uses them in two ways:
- Storage. Pi keeps its transcripts, inbox, and tasks in its own tables in the object's SQLite database. The table names start with
pi_, so they do not collide with your own tables. - Wake-up. Pi's scheduler runs in memory, and an evicted object has none.
PiHarnessschedules one Lifecycle job for the object, driven by Pi'swake(). The job keeps a heartbeat while Pi works, is due when Pi's next sleeping task wakes, and completes when Pi has nothing to do. If the object is evicted mid-run, the job's alarm restarts it, Pi reopens its storage, and the run continues.
PiHarness does not choose how clients reach a session. It gives you each session's event stream, and you send it over WebSockets, HTTP, or RPC.
Install
Install agents with Pi Durable, Pi AI, and Chord:
npmyarnpnpmbun
npm i agents @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
yarn add agents @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
pnpm add agents @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
bun add agents @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
The three Pi packages are optional peer dependencies of agents, at ~1.4.0. Pi Durable is experimental and its API can change in minor releases, so each agents release supports one Pi minor version. Agents SDK 0.30.0 and later support Pi 1.4.
The agents/models/pi-ai entry point supports AI Gateway and Workers AI models, so you can get started with Cloudflare models right away or use your existing Pi AI provider. The examples on this page use Workers AI through the AI binding and the Pi AI model provider. Add the binding and a SQLite-backed Durable Object to your Wrangler configuration:
{
"name": "pi-agent",
"main": "src/index.ts",
"upload_source_maps": true,
// Set this to today's date
"compatibility_date": "2026-10-27",
"compatibility_flags": ["nodejs_compat"],
"ai": {
"binding": "AI",
},
"durable_objects": {
"bindings": [{ "name": "Assistant", "class_name": "Assistant" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["Assistant"] }],
}
name = "pi-agent"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-10-27"
compatibility_flags = [ "nodejs_compat" ]
[ai]
binding = "AI"
[[durable_objects.bindings]]
name = "Assistant"
class_name = "Assistant"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "Assistant" ]
Create the agent
PiHarness is a Lifecycle capability, so it runs on a plain Durable Object. Open Pi's Harness in a factory, install a Lifecycle on the object, and add the harness to it:
src/index.jsjs
import { DurableObject } from "cloudflare:workers";
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";
export class Assistant extends DurableObject {
ai = createAI({ binding: this.env.AI });
registry = createRegistry();
harness = new PiHarness({
harness: ({ storage, context, onWake }) => {
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);
async ask(prompt) {
const { text } = await this.harness.prompt(prompt);
return text;
}
}
export default {
async fetch(request, env) {
const agent = env.Assistant.getByName("demo");
return Response.json({ text: await agent.ask("What is 47 × 19?") });
},
};
src/index.tsts
import { DurableObject } from "cloudflare:workers";
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";
export class Assistant extends DurableObject<Env> {
ai = createAI({ binding: this.env.AI });
registry = createRegistry();
harness = new PiHarness({
harness: ({ storage, context, onWake }) => {
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);
async ask(prompt: string) {
const { text } = await this.harness.prompt(prompt);
return text;
}
}
export default {
async fetch(request: Request, env: Env) {
const agent = env.Assistant.getByName("demo");
return Response.json({ text: await agent.ask("What is 47 × 19?") });
},
};
The harness factory receives storage, Pi's storage over the object's SQLite database, context, a background context for opening Pi, and onWake, so the object can hibernate while Pi's tasks sleep. Everything else that Harness.open() takes is yours to build in the factory: models, the registry of tools and prompt sections, settings, env, and onReport.
The factory runs inside the object's startup, before any session is used, and again whenever the harness reopens Pi after a storage error (refer to Recovery). Keep it safe to run more than once. If it throws, the operation that started it fails, and the next operation tries again. Every PiHarness operation waits for startup, so a call that arrives over RPC before startup does not open Pi on its own.
Options
| Option | Description |
|---|---|
harness |
Required. Receives { storage, context, onWake } and returns Pi's Harness, usually from Harness.open(). Pass onWake on to Harness.open(). |
defaults |
What a new session starts with: model, a Pi AI Model such as ai("@cf/..."), and thinkingLevel. Change one session's model later with session.setModel(). |
Without a model, a session's prompts end unanswered until you set one.
Settings for the whole harness go in Pi's settings, passed to Harness.open(). For example, settings: { retry: { enabled: true, maxRetries: 2, baseDelayMs: 500 } } retries a failed model request. compaction and toolExecution set how Pi compacts long transcripts and runs a round's tools.
Add tools and a system prompt
Both tools and system prompt sections are provided to the Pi Harness via extensions. A Pi extension is a plain object with a name, tools, and sections. Install it on the registry you open Pi with:
import { Type } from "@earendil-works/pi-ai";
import type { ToolRegistration } from "@earendil-works/pi-durable";
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",
async execute({ text }) {
const words = text.split(/\s+/).filter(Boolean).length;
return { output: [{ type: "text", text: String(words) }] };
},
};
this.registry.install({
name: "editor",
sections: [
{ key: "preamble", render: () => "You are an editor.", tag: false },
],
tools: [wordCount],
});
For more information on creating and configuring extensions, including skills and replay safety, refer to Extensions.
Send prompts
harness.prompt() submits to the root session and waits for the answer. It returns the final assistant text, the status, and the session's transcript as messages.
For long runs, submit and wait separately. submit() returns once Pi has durably stored the input, before the model runs. wait() returns the result when Pi finishes:
const receipt = await this.harness.submit("Summarize the latest report", {
operationId: "report-summary-42",
});
// Later, possibly from another request:
const result = await this.harness.wait(receipt.operationId);
// { operationId, session, status: "done" | "unanswered", text?, reason? }
An operationId makes a submission idempotent. Submitting the same id again returns the same operation with accepted: false. Use an id that survives your own retries, such as an inbound event id.
A submission made while the session is running is queued and answered after the current run, as its own run. Set whenBusy: "steer", or call session.steer(), to join the running work after its current tool round instead.
harness.pending() lists submissions Pi has not finished, with their status: queued, running, or parked. A submission is parked when Pi's restart limit stopped its work (refer to Recovery).
