{"id":"2083222443603210635","url":"https://x.com/harshil1712/status/2083222443603210635","text":"The experiment of running @pidotdev on @CloudflareDev got too big. Wrote down how it is being built, with the explanation of each Cloudflare primitive used.","author":{"name":"Harshil","username":"harshil1712","avatarUrl":"https://pbs.twimg.com/profile_images/1659495153504010241/n8WyRY-K_200x200.jpg"},"createdAt":"Fri Jul 31 16:05:09 +0000 2026","engagement":{"replies":1,"retweets":0,"likes":3,"views":2566},"article":{"title":"How I Run the Pi Coding Agent on Cloudflare","previewText":"I recently came across a tweet about Pi's programmatic APIs.  I knew Pi as a coding agent, but its portable internals caught my  attention. The agent loop, model providers, and session storage are","coverImageUrl":"https://pbs.twimg.com/media/HOjw_JqXIAAD1Ny.jpg","content":"I recently came across a [tweet](https://x.com/pidotdev/status/2081703649374281736?s=20) about [Pi's programmatic APIs](https://pi.dev/docs/latest/sdk).  I knew Pi as a coding agent, but its portable internals caught my  attention. The agent loop, model providers, and session storage are  available through lower-level packages rather than being tied to its  terminal interface. This project uses @earendil-works/pi-agent-core and @earendil-works/pi-ai directly instead of embedding the full @earendil-works/pi-coding-agent SDK.\n\nThat made me think: can I run Pi entirely on Cloudflare's Developer Platform?\n\nMost coding agents assume they are running on a developer's machine.  They expect a persistent local filesystem, a shell, long-lived  processes, and somewhere to store conversation history. Cloudflare  Workers provides a very different runtime. There is no machine waiting  for the agent. Workers has a request-scoped virtual filesystem, but it  does not persist files across requests or provide the host filesystem a  local coding agent expects.\n\nThis sounded like a fun challenge. Instead of making Workers behave  like a laptop, I wanted to find the Cloudflare primitive for each  capability Pi needed.\n\n[Pi on Cloudflare](https://github.com/harshil1712/pi-on-cf) is what came out of that experiment. It is a browser-based coding agent  where each session has durable conversation history, an isolated  filesystem, streaming model output, searchable prior sessions, generated  application previews, and one-click deployment to Cloudflare Workers.\n\n![Screenshot of the porject](https://pbs.twimg.com/media/HOjtZlrWoAAKmLo.jpg)\n\nIt is still a single-user prototype, not a production multi-tenant  service. However, building it taught me a lot about what a coding agent  needs from its runtime. Let me walk you through the design decisions,  the Cloudflare primitives I used, and the limitations I accepted.\n\n## What I Wanted to Preserve from Pi\n\nPi is more than a chat interface around an LLM. Its core packages provide the parts required to run a coding agent:\n\n- An agent harness that coordinates model calls and tool execution\n\n- A provider abstraction for connecting models\n\n- Streaming events for text, reasoning, and tools, plus lifecycle events such as save points\n\n- An append-only session tree rather than a flat message list\n\n- Branch navigation, labels, forks, clones, and compaction\n\n- A storage interface that applications can implement for different runtimes\n\n- Steering, follow-up, and abort operations\n\nThe storage interface in pi-agent-core was the part that  made this experiment possible. It doesn't require the transcript to  live in a local JSON file. I could provide my own implementation while  retaining Pi's native entry types and tree structure.\n\nI didn't want to flatten Pi into another chat application with a list  of messages. I wanted to keep its branches, compaction, tool loop, and  model abstraction, then replace only the things that normally come from a  local machine.\n\nThe Cloudflare version currently supports:\n\n- Creating, naming, cloning, forking, searching, and deleting sessions\n\n- Streaming assistant text, reasoning, and tool activity\n\n- Navigating branches without deleting later history\n\n- Automatic and manual context compaction\n\n- Durable workspace file operations\n\n- Cross-session search and learned memory\n\n- Building, previewing, and deploying generated React applications\n\nPi on Cloudflare isn't a browser port of Pi's terminal interface. It is a new host for Pi's portable core.\n\n## The Architecture\n\nI started by listing the capabilities that a local runtime normally  gives Pi: identity for each session, durable state, files, model access,  real-time communication, a way to execute generated code, and a place  to keep deployed source.\n\nOnce I mapped those capabilities to Cloudflare products, the architecture looked like this:\n\n![](https://pbs.twimg.com/media/HOjtpdkXwAEw6Pf.png)\n\nThere are 2 Durable Object classes:\n\n- PiSession owns one Pi conversation and its workspace\n\n- PiRegistry owns the session catalog, transcript search index, lineage, and global memory\n\nThe application itself is a [TanStack Start](https://tanstack.com/start) application deployed on [Cloudflare Worker](https://developers.cloudflare.com/workers/). The browser connects to the Durable Objects through the [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/). Model requests go through [AI Gateway](https://developers.cloudflare.com/ai-gateway/). Generated applications run in [Dynamic Workers](https://developers.cloudflare.com/dynamic-workers/) during preview and become independent Workers when deployed.\n\n## One Durable Object per Pi Session\n\nThe first question I had to answer was: what owns a Pi session when there is no long-lived local process?\n\nThis is where Durable Objects clicked for me. I assigned one PiSession Durable Object to each conversation.\n\nA coding-agent session is a natural coordination boundary. It has one  active transcript, one selected branch, one workspace, and at most one  model turn changing those resources at a time.\n\nEach PiSession owns:\n\n- The low-level Session and AgentHarness from pi-agent-core\n\n- The append-only transcript tree\n\n- The active leaf in that tree\n\n- Compaction settings\n\n- An isolated filesystem\n\n- Indexing and memory-extraction cursors\n\n- Generated application metadata\n\n- An in-memory build cache\n\nThe code for this mapping is surprisingly small:\n\nThe Agents SDK Agent class extends Durable Objects with  WebSocket connections, RPC, and browser clients. Durable Objects provide  the identity, coordination, and SQLite storage underneath.\n\nThis gives each session an independent address and storage boundary. A  busy session doesn't need to coordinate transcript writes with every  other session.\n\nIt also gives me a straightforward exclusivity rule. Before Pi starts  a prompt, the session marks itself active. Concurrent operations that  could change the same transcript or workspace are rejected until the  turn finishes.\n\nDurable Objects solved the ownership and coordination problem, but I  still needed to fit Pi's storage model into the Durable Object. That led  to the next decision.\n\nDurable Objects are single-threaded, but requests can interleave while awaiting non-storage I/O such as model calls. The active flag provides application-level exclusivity across the complete agent turn, not only an individual storage statement.\n\n## Preserving Pi's Session Tree in SQLite\n\nA conventional chat application often stores messages in a table and  updates or deletes rows when users retry a prompt. Pi doesn't work that  way.\n\nEach session entry has an ID, a parent ID, a type, and a timestamp.  Together, those entries form a tree. Changing the active branch doesn't  delete another branch. In the low-level pi-agent-core session model used here, Pi appends a leaf-selection entry that points to the newly selected part of the tree.\n\nI preserve that representation directly:\n\nThe serialized entry can be a message whose role is user, assistant,  or tool result. Other entry types represent model, thinking-level, or  active-tool changes, compaction, branch summaries, custom data, labels,  session information, or leaf selection.\n\nFlattening everything into chat messages would discard the exact  semantics I wanted to preserve. Pi's branch navigation and compaction  logic expect the original entry relationships.\n\nAppending non-empty user or assistant text also creates an indexing  event within the same synchronous SQLite transaction. Every entry  creates a separate event that updates the registry's session metadata.  In simplified form:\n\nThere is a small but important failure case here. If I stored the  transcript first and indexing failed afterwards, search could silently  miss a completed message. If I indexed first, search could return a  message that had never been committed to the session.\n\nTo avoid both cases, a background operation delivers the events to PiRegistry,  but writing indexable transcript text and its outbox event happens  atomically. Delivery can be asynchronous and idempotent because  non-empty user or assistant text cannot be stored without also becoming  eligible for indexing.\n\nThis is the transactional outbox pattern applied inside a Durable Object.\n\n## Giving Pi a Durable Filesystem\n\nAt this point, I had durable conversations, but that alone doesn't make a coding agent. Pi also needed files.\n\nA Worker doesn't expose the host machine's persistent POSIX filesystem. This is where the experimental [@cloudflare/shell](https://www.npmjs.com/package/@cloudflare/shell) package came in. It gave me a virtual filesystem backed by the session's Durable Object SQLite database.\n\nEvery Pi session therefore gets its own isolated workspace. Files  survive Durable Object eviction because they live in durable storage  rather than in the object's memory.\n\nI expose 6 core filesystem tools to the model:\n\n- read\n\n- write\n\n- edit\n\n- list\n\n- find\n\n- grep\n\nEach tool operates against the virtual workspace:\n\nWrite and edit tools run sequentially to prevent concurrent changes from racing against each other.\n\nI also expose the workspace in the browser so I can inspect, refresh,  and download files. I didn't want the filesystem to be an invisible  implementation detail hidden behind the model.\n\n[ ](https://dev.to/cloudflare/how-i-run-the-pi-coding-agent-on-cloudflare-2gd1-temp-slug-4347967?preview=6d7577421a29a029f1ac483649aad792aa7aada46c15c1fbce2aa7b64f131c69707a5f52395db5fe3489cdbabc28d5f46af380c9f8990dde5bb30b04#but-what-about-a-shell)  But What About a Shell?\n\nThe obvious next question is why I didn't give Pi a process sandbox. This version doesn't provide:\n\n- A POSIX shell\n\n- Native processes\n\n- npm install\n\n- Arbitrary package scripts\n\n- A development server\n\n- A general network-fetch tool\n\n- Native test or compiler commands\n\nThis is deliberate, and it is probably the biggest difference from running Pi locally.\n\nThe first version needed persistent source files and a way to run  Workers applications. It didn't need a general-purpose Linux  environment. Durable Object SQLite and Dynamic Workers cover that  narrower requirement with fewer moving parts.\n\nThe system prompt makes that limitation explicit:\n\nHere, \"no POSIX filesystem\" means the agent is not given a persistent  POSIX workspace or a process environment. I don't expose Workers'  request-scoped node:fs filesystem as an agent tool.\n\nIf I wanted the agent to compile arbitrary native projects or run existing CLI tools, I would use [Cloudflare Containers](https://developers.cloudflare.com/containers/) or the process-oriented [Cloudflare Sandbox SDK](https://developers.cloudflare.com/sandbox/). For React and Workers applications built from a controlled template, the Worker-native path is enough.\n\n## Running Pi's Model Loop Through AI Gateway\n\nWith sessions and files in place, the next piece was model inference.  Pi already separates the agent loop from its model provider, so I  registered a custom provider backed by AI Gateway's OpenAI-compatible  API.\n\nThe contextWindow and maxTokens values  above are conservative budgets configured in this adapter, not  advertised model limits. The currently selected GLM-5.2 model has a  documented 262,144-token context window on Workers AI.\n\nThe committed configuration currently selects a Workers AI model:\n\nI liked this approach because AI Gateway gives the agent one model  endpoint while the model remains a configuration choice. It also gives  me a natural place to attach request metadata:\n\nThe session ID makes model traffic attributable to the Pi session  that produced it. Memory extraction requests include an additional  purpose field.\n\nThe UI doesn't currently expose model or reasoning-level selection. The agent uses a fixed medium thinking level and server-side model configuration.\n\n## Getting the Stream Back to the Browser\n\nThe agent loop was now running inside a Durable Object, but a coding  agent doesn't feel useful if the browser waits for the entire turn to  finish. I wanted to show text, reasoning, and tool activity as Pi  produced them.\n\nThe application routes /api/agents/* through the Agents SDK before passing other requests to TanStack Start:\n\nOrdinary session operations use callable RPC methods. Prompts use a streaming callable method:\n\nA prompt moves through the system as follows:\n\n1. The browser appends the user message optimistically.\n\n1. The Agents SDK sends the prompt to the session's Durable Object.\n\n1. PiSession validates configuration and prevents a second active prompt.\n\n1. The session creates or reuses its in-memory AgentHarness.\n\n1. Pi compacts the active branch if it is approaching the context budget.\n\n1. The model produces text, reasoning, and tool calls through AI Gateway.\n\n1. Pi events are translated into browser-safe stream events.\n\n1. Completed messages become part of the durable session tree during the turn.\n\n1. After a turn, a save point schedules asynchronous registry outbox delivery.\n\n1. The browser reloads the authoritative branch and workspace.\n\nThe client batches incoming deltas once per animation frame. Model  streams can produce updates faster than React should render them, so  batching reduces unnecessary UI work without changing what the user  sees.\n\nThere is a catch. Only completed Pi entries are durable. Partial  token deltas and the browser's position within an active stream are not.  If the Durable Object restarts during a turn, the client can reload  completed durable entries but cannot reattach to the interrupted stream.  Resumable active turns are something I still want to explore.\n\n## Searching Across Sessions\n\nOne Durable Object per session worked well until I needed to answer  questions across them. How do I list every session, search old  conversations, or carry a useful preference from one session into  another?\n\nThis is why I added a singleton PiRegistry Durable Object.\n\nThe registry handles:\n\n- Session discovery and metadata\n\n- Names and timestamps\n\n- Parent and source lineage\n\n- Full-text transcript search\n\n- Regular-expression search\n\n- Idempotent index-event processing\n\n- Deletion tombstones\n\n- Learned memory shared across sessions\n\nThe registry indexes non-empty user and assistant text with SQLite  FTS5. It intentionally excludes reasoning, tool output, compaction  summaries, and workspace files.\n\nI deliberately kept the search lexical rather than adding embeddings  and a vector database. SQLite FTS5 has lower operational complexity and  produces predictable results for session names, phrases, and code  fragments. The agent can call a session_search tool to retrieve relevant text from earlier sessions.\n\nDeletion tombstones handle an important race. Index events travel  asynchronously from a session to the registry. Without a tombstone, a  delayed event could recreate metadata for a deleted session. The  registry records the deletion and rejects later events for that session.\n\n## Learned Memory\n\nThe registry also stores global memories classified as:\n\n- Preferences\n\n- Facts\n\n- Instructions\n\n- Decisions\n\nMemory can change through an explicit model tool or through  background extraction after completed turns. The extraction prompt  treats transcript contents as untrusted data and requires every change  to cite a user-authored source entry.\n\nLater sessions receive those memories in their system prompt.\n\nMemory can become dangerous quickly if an agent stores everything it  sees. I therefore kept this feature conservative. It limits the number  and total size of memories, uses optimistic version checks for  background extraction updates and deletions, and rejects selected secret  patterns. Explicit updates through the memory tool do not currently use  the same version check. Those checks are guardrails, not a complete  data-loss-prevention system.\n\nMemory is currently global to the deployment. It isn't separated by  user because the application doesn't yet have a user identity model.\n\n## Building an App Without a Build Process\n\nThis was the part I was most curious about. Writing source files in  SQLite is useful, but I wanted Pi to build something I could actually  open and deploy. How do you build a React application when you cannot  spawn Vite as a process?\n\nWhen the user asks Pi to build an application, the model calls initialize_app. That tool copies a React template pinned to a full Git commit into the durable workspace.\n\nPinning the commit gives the agent a known project shape:\n\n- React code lives under /src\n\n- Worker API code lives at /worker/index.ts\n\n- The host controls the build inputs and compatibility settings\n\n- The model cannot introduce arbitrary installation scripts into the build\n\nWhen the user requests a preview or deployment, the application  creates a deterministic source snapshot. It excludes directories such as  .git, node_modules, dist, and .wrangler, then enforces explicit limits:\n\n- 1,000 source files\n\n- 5 MiB per file\n\n- 25 MiB total source size\n\nThe snapshot is sorted and hashed. The source hash lets the UI detect  changes and lets the session reuse a matching in-memory build.\n\nInstead of starting a process, I call the experimental, Workers-runtime-only @cloudflare/worker-bundler package directly inside the Worker:\n\nThe catch is that this is not equivalent to spawning vite build.  The hosted builder uses a controlled entrypoint, fixed compatibility  settings, and an in-memory source map. It doesn't execute arbitrary  package scripts or custom Vite plugins.\n\nThat constraint makes builds repeatable and keeps generated code inside the runtime model I designed for.\n\n## Previewing Generated Code with Dynamic Workers\n\nA successful build left me with another interesting problem: the  generated code didn't exist when I deployed the host Worker, but I still  needed to execute it for a preview. [Dynamic Workers](https://developers.cloudflare.com/dynamic-workers/) is currently in open beta.\n\nThe Worker Loader binding lets the application create a Dynamic  Worker from the generated modules. I use the bundle hash as its ID:\n\nUsing get() instead of loading a new Worker for every  request lets the runtime reuse a warm isolate when available. Reuse is  not guaranteed, and the callback may run again if the runtime needs a  new isolate.\n\nThe preview Worker receives these application-configured limits and bindings:\n\n- The generated Worker modules\n\n- Bundled static assets\n\n- No environment bindings\n\n- A 50ms CPU limit\n\n- A 20-subrequest limit\n\n- Outbound network access inherited from the parent Worker\n\nThe wrapper serves static files first, delegates API requests to the generated Worker, and falls back to index.html for client-side routes.\n\nThis is isolated Worker execution, not a Linux process sandbox.  Generated code can handle requests using Workers APIs, but it can't  spawn a process or access the Pi session's Durable Object unless I  explicitly provide a binding. Because the current loader configuration  does not set globalOutbound: null, generated code can still make outbound requests with fetch() or connect().\n\nFor this project, the binding isolation is useful: the generated app  receives no session storage or secrets. Outbound access remains a  capability I would need to restrict before treating previews as  untrusted code.\n\n## Versioning and Deploying Generated Applications\n\nOnce preview worked, I wanted a path from \"the agent built this\" to  \"this is a real deployed application.\" A preview is temporary. A  deployed application needs durable source history and an independent  Worker.\n\nThe deployment pipeline has 2 destinations:\n\n1. Source goes to [Cloudflare Artifacts](https://developers.cloudflare.com/artifacts/), which is currently in beta and requires enrollment.\n\n1. The built application goes to the Workers API.\n\nArtifacts provides Git-compatible, versioned storage. I create one  repository per Pi session and publish the exact source snapshot used for  the deployment.\n\nThe application uses a short-lived repository token and a JavaScript  Git client over the virtual workspace. This gives each deployment a  source commit without requiring a native git process.\n\nAfter publishing the source, the deployment client:\n\n1. Uploads the generated static assets.\n\n1. Creates a Worker version.\n\n1. Creates a deployment that sends 100% of traffic to that version.\n\n1. Returns the resulting workers.dev URL.\n\n1. Stores the source hash, bundle hash, commit SHA, version ID, and deployment ID in the session.\n\nEach generated application becomes its own Worker. It doesn't keep running inside the original Pi session after deployment.\n\nDeleting a session also deletes its generated Worker and Artifacts repository when deployment metadata exists.\n\n## Why Each Cloudflare Primitive Is Here\n\nBy this point, every primitive had a specific job. I didn't want to  add a product because it might be useful later. I added it only when the  implementation had a concrete requirement.\n\nI didn't add KV, R2, D1, Queues, Workflows, Containers, or Vectorize.  None of them solved a requirement that the existing primitives couldn't  already cover.\n\n## Current Trade-offs\n\nI am happy with how much of Pi works on Cloudflare, but I don't want  to present this as a production-ready replacement for a local coding  agent. The architecture works because it chooses a narrower definition  of a coding environment.\n\nThe main limitations, in order of importance, are:\n\n- The application has no user or tenant isolation\n\n- Tool writes do not require user approval\n\n- Global memory has no management interface and is shared across sessions\n\n- There is no native shell or arbitrary process execution\n\n- Only the controlled React template can use the hosted build path\n\n- Active streams cannot resume after a Durable Object restart\n\n- Forks copy the current workspace, not the filesystem as it existed at the selected transcript entry\n\n- Search is lexical rather than semantic\n\n- Generated previews have no bindings or secrets, but retain outbound network access\n\n- Model and reasoning-level selection are server-controlled\n\n- The singleton registry could become a coordination bottleneck at larger scale\n\nThe first point is the most important.\n\nThis repository is a single-user prototype. Session UUIDs create  storage boundaries, but they are not authorization controls. Anyone who  can reach the deployment can potentially inspect sessions, modify files,  call the model, influence global memory, or deploy applications using  server-side credentials.\n\nImportant: Protect the entire Worker with [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/access-controls/) or another authentication layer before exposing it to the Internet. When using Access, follow Cloudflare's guidance to [validate the Access JWT](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/#cloudflare-workers-example) in the Worker as well.\n\n## Run Pi on Cloudflare\n\nIf you want to try the same setup, here is the shortest path.  Remember that the project has no application-level authentication, so  protect a deployed instance before giving it privileged credentials.\n\nThe committed GLM-5.2 configuration requires the Workers Paid plan.  Generated app deployment also requires enrollment in the Artifacts beta  and an Artifacts namespace named pi-apps, which you can create in the Cloudflare dashboard.\n\nClone the repository and install its dependencies:\n\nBecause the project uses a remote Artifacts binding during local development, authenticate Wrangler first:\n\nCreate a local environment file, then add your Cloudflare account ID and an API token with AI Gateway - Read, AI Gateway - Edit, and Workers AI - Read permissions:\n\nStart the local Worker runtime:\n\nFor a production deployment, including generated app deployment,  configure these Worker secrets. Use the AI Gateway token described above  for CLOUDFLARE_API_TOKEN. WORKERS_DEPLOY_API_TOKEN needs account-scoped Workers Scripts Read and Workers Scripts Edit permissions so the application can create and delete Workers, upload  assets, create versions and deployments, and read the account's workers.dev subdomain.\n\nDeploy the host application with:\n\nThe repository includes UI tests running in jsdom and a separate test suite running inside the Workers runtime:\n\n## What I Learned\n\nI started with a simple question after seeing a tweet: can Pi run on Cloudflare?\n\nWhat I learned is that Pi's lower-level agent, provider, and session  abstractions didn't need its normal terminal host. They needed a set of  capabilities around them: durable identity, coordinated state, files,  model access, streaming, and a constrained way to execute generated  code. Pi's portability let me keep the difficult parts of the agent,  including its tool loop, session tree, model abstraction, streaming  events, and compaction behaviour.\n\nCloudflare supplied the runtime around it:\n\n1. Durable Objects turned each Pi session into an isolated, stateful service.\n\n1. SQLite and @cloudflare/shell replaced local transcript and filesystem storage.\n\n1. Dynamic Workers, Worker Bundler, and Artifacts created a Worker-native path from generated source to preview and deployment.\n\nThe project doesn't imitate a local machine. It treats Workers as the  target environment from the beginning. That forced me to be explicit  about what the agent can do, and equally explicit about what it cannot  do.\n\nI also have a new appreciation for Pi's design. Making the agent loop  and session storage portable is what made this entire experiment  possible.\n\nThe project is [open source](https://github.com/harshil1712/pi-on-cf).  If you are experimenting with coding agents on Cloudflare, take a look  and let me know what you build. I am especially interested in how you  would approach resumable turns, historical workspace snapshots, or  multi-user isolation.\n\nI also recommend looking at [Project Think](https://developers.cloudflare.com/agents/harnesses/think/).  It is an opinionated harness from Cloudflare whose design is inspired  by Pi. It packages Cloudflare-native capabilities including workspace  tools, persistence, stream resumption, durable recovery, extensions, and  sub-agent support, so less custom infrastructure is required. I  personally am building [Yukt](https://getyukt.app), using Think.\n\nIf you have questions or feedback, feel free to reach out on [LinkedIn](https://www.linkedin.com/in/harshil1712/) or [X](https://x.com/harshil1712).\n\n## References\n\n- [Pi](https://github.com/earendil-works/pi)\n\n- [Pi on Cloudflare source](https://github.com/harshil1712/pi-on-cf)\n\n- [Cloudflare Workers](https://developers.cloudflare.com/workers/)\n\n- [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/)\n\n- [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/)\n\n- [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/)\n\n- [Cloudflare Dynamic Workers](https://developers.cloudflare.com/dynamic-workers/)\n\n- [Cloudflare Artifacts](https://developers.cloudflare.com/artifacts/)"}}