A pi coding agent running inside a Cloudflare Worker with a persistent virtual filesystem and git — no servers required.
Each session gets its own isolated Durable Object with a SQLite-backed filesystem (@cloudflare/shell) and a full git repository (isomorphic-git). The agent can read, write, edit, search, and version-control files across requests.
┌─────────────────────────────────────────────────────────┐
│ Worker (router) │
│ Routes requests by session ID │
└────────────┬────────────────────────┬───────────────────┘
│ │
┌────────────▼──────────┐ ┌─────────▼──────────────────┐
│ Durable Object (A) │ │ Durable Object (B) │
│ │ │ │
│ Workspace (SQLite) │ │ Workspace (SQLite) │
│ └─ /src/index.ts │ │ └─ /app/main.py │
│ └─ /package.json │ │ └─ /requirements.txt │
│ └─ ... │ │ └─ ... │
│ isomorphic-git │ │ isomorphic-git │
│ └─ .git/ │ │ └─ .git/ │
│ pi-agent-core │ │ pi-agent-core │
│ └─ Claude API │ │ └─ Claude API │
│ └─ 14 tools │ │ └─ 14 tools │
└───────────────────────┘ └──────────────────────────────┘
# Install
npm install
# Set your Anthropic API key
echo 'ANTHROPIC_API_KEY=sk-ant-...' > .dev.vars
# Run locally
npm run devThen talk to it:
# Create a session
SESSION=$(curl -s -X POST http://localhost:8787/session/new | jq -r .sessionId)
# Chat
curl -s -X POST "http://localhost:8787/session/$SESSION/chat" \
-H "Content-Type: application/json" \
-d '{"message": "List all files and show git log"}' | jq .
# Files persist across requests
curl -s -X POST "http://localhost:8787/session/$SESSION/chat" \
-H "Content-Type: application/json" \
-d '{"message": "Add a multiply function to src/index.ts, then git add and commit"}' | jq .
# Streaming (SSE)
curl -N -X POST "http://localhost:8787/session/$SESSION/chat" \
-H "Content-Type: application/json" \
-d '{"message": "Refactor src/utils.ts", "stream": true}'Create a new session. Returns { sessionId: string }.
Send a message to the agent in a session. Creates the session on first use.
Request body:
{
"message": "Your prompt here",
"stream": false,
"model": "claude-sonnet-4-20250514"
}Response (non-streaming):
{
"response": "Agent's text response",
"toolCalls": [
{ "tool": "read", "args": { "path": "/src/index.ts" } },
{ "tool": "git_commit", "args": { "message": "Add feature" } }
]
}Response (streaming): Server-Sent Events with text_delta, tool_start, tool_end, done, and error events.
Get workspace metadata (file count, directory count, total bytes).
Returns ok.
The agent has 14 tools available:
Filesystem tools (backed by @cloudflare/shell Workspace)
| Tool | Description |
|---|---|
read |
Read file contents with optional offset/limit |
write |
Create or overwrite a file (auto-creates parent dirs) |
edit |
Find-and-replace exact text in a file |
ls |
List directory contents |
grep |
Search for text patterns across files |
tree |
Show filesystem tree structure |
Git tools (backed by isomorphic-git)
| Tool | Description |
|---|---|
git_status |
Show working tree status |
git_add |
Stage files (or "." for all) |
git_commit |
Commit staged changes |
git_log |
Show commit history |
git_diff |
Show changed files since last commit |
git_branch |
List or create branches |
git_checkout |
Switch branches |
git_init |
Initialize a new repository |
- Worker receives
POST /session/:id/chatand routes to a Durable Object keyed by session ID - Durable Object owns a
Workspace(SQLite-backed virtual filesystem) and an isomorphic-git instance - On first access, the workspace is seeded with a sample project and
git init+ initial commit - pi-agent-core runs the agent loop: sends the prompt to Claude, executes tool calls against the workspace, loops until done
- All state persists in the DO's SQLite storage — files, git objects, refs — across requests
Durable Object SQLite
┌──────────────────────────────────────────┐
│ cf_workspace_default table │
│ ┌──────────────────────────────────┐ │
│ │ path (PK) │ content │ type │ │
│ │ /src/app.ts│ "export…"│ file │ │ ← small files inline
│ │ /src/ │ NULL │ directory│ │
│ │ /.git/... │ <binary> │ file │ │ ← git objects
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────┘
│
│ files > 1.5 MB spill to
▼
┌──────────────────────┐
│ R2 Bucket (optional)│
│ └─ {doId}/default/… │
└──────────────────────┘
- Files < 1.5 MB are stored inline in SQLite (the
contentcolumn) - Files ≥ 1.5 MB spill to R2 if an
R2_BUCKETbinding is configured - Each DO instance is fully isolated — its own database, its own git repo
- D1 max row size is 2 MB; the 1.5 MB threshold keeps inline storage safe
The pi SDK (@mariozechner/pi-agent-core, @mariozechner/pi-ai) was designed for Node.js. Running it in a Cloudflare Worker requires a custom esbuild step (build.mjs) that bundles everything into a single ESM file with no nodejs_compat dependency:
-
Patches
config.js— the SDK's config module usesimport.meta.urlandfs.readFileSyncat module scope to locate package assets. We replace it with static stubs since Workers have no filesystem at boot time. -
Removes
new Function()calls —ajv(used by@sinclair/typeboxfor tool parameter validation) compiles JSON schemas vianew Function(). Workers blockeval/new Functionat parse time. We patch the compiled output to disable runtime codegen (the SDK falls back to interpretation-based validation). -
Stubs Node-only dependencies —
node-fetch(Workers have nativefetch),supports-color/debug(no terminal),https-proxy-agent(no proxies),node:diagnostics_channel(observability). These are transitive deps from@google/genaiandgaxiosthat aren't needed at runtime. -
Shims
node:crypto— mapped to Web Crypto API (crypto.subtle.digest). Used by@cloudflare/shellfor file hashing. -
Polyfills
Buffer— isomorphic-git uses Node'sBuffer; we inject thebufferpolyfill globally. -
Externalizes
cloudflare:workers— the Durable Object base class import (provided by the Workers runtime).
src/
├── index.ts # Worker entry point — thin router to DOs
├── workspace-do.ts # Durable Object — owns Workspace + git + agent
├── tools.ts # Filesystem tools (read, write, edit, ls, grep, tree)
└── git-tools.ts # Git tools (status, add, commit, log, diff, branch, checkout, init)
build.mjs # Custom esbuild build with patches for Workers compat
wrangler.toml # Cloudflare Workers config
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key (set in .dev.vars for local dev, or via wrangler secret for production) |
| Binding | Type | Description |
|---|---|---|
R2_BUCKET |
R2 Bucket | Large file storage overflow (files > 1.5 MB spill here) |
To enable R2, add to wrangler.toml:
[[r2_buckets]]
binding = "R2_BUCKET"
bucket_name = "pi-workspace-files"# Set your API key
wrangler secret put ANTHROPIC_API_KEY
# Deploy
npm run deploy- No bash/shell execution — the agent can read, write, and edit files but cannot run arbitrary commands. The filesystem is a virtual JS implementation, not a real OS.
- No network from tools — tools operate on the local virtual filesystem only. Git clone/push could be wired up with auth tokens but is not yet exposed to the agent.
- Stateless conversation — each request is a fresh agent context (no multi-turn memory across requests). The filesystem persists, but the conversation history does not. Adding conversation persistence in the DO's SQLite is a natural next step.
- Single model — currently hardcoded to Anthropic Claude. The pi-ai transport layer supports OpenAI, Gemini, and others; wiring them up is straightforward.
MIT