← Blog

PocketPi architecture

Making Pocket Pi an Agent-Native App Development Environment

An ESP32-S3 cannot host a conventional JavaScript toolchain. PocketPi becomes a development environment by closing a smaller, stricter loop around the App already running on the board.

The ESP32-S3 used for this project has a 240 MHz CPU and 8 MB of PSRAM. It cannot run Node.js, npm, a TypeScript compiler, an unrestricted shell and an IDE in the way a Mac or Windows PC can.

Instead of reproducing that desktop environment, we started from a first-principles question:

Given PocketJS as a small but complete JavaScript runtime substrate, what is the minimal closed loop required for an Agent to develop software?

The answer is four responsibilities:

  1. inspect the source that defines the running App;
  2. modify that source without mutating the live App;
  3. validate the candidate against the App's current Data;
  4. commit it as one coherent new App version.

A compiler, a POSIX shell and an IDE are possible ways to implement those responsibilities. They are not the definition of a development environment. For PocketPi, the rest of the architecture exists to make these four steps complete on the device itself.

The model backend may run elsewhere. Here, on-device means the App source, workspace Tools, durable state, validation and activation all live on the target that runs the App.

Two earlier decisions supplied the foundation. In Taking a Step Further Towards an Agent-Native Runtime on Embedded Systems, we separated protected native mechanisms from editable JavaScript policy. In Designing Apps for Humans and Agents in an Agent-Native Runtime, we defined the editable product boundary as:

App = Data + Actions + View

The development loop works because each of its four steps maps back to one of those architecture choices.

PocketPi minimal on-device development loopPi Agent inspects ordinary App source from its privileged workspace, modifies an isolated checkout, validates it through human review and rehearsal, and commits one coherent App version.POCKETPI / MINIMAL ON-DEVICE DEVELOPMENT LOOPINSPECTGuesthierarchyPi Agent → workspaceApps → own Data onlyread · findgrep · lsVISIBLE, STILL ISOLATEDMODIFYApp checkoutrelease → candidateapp.jsonschema · migrationsactions.jsview.jsWRITE · EDITlive App unchangedVALIDATEReview +rehearse1app.submit2human review3SQLite rehearsal4load Actions + ViewNO LIVE MUTATIONCOMMITOne AppversionLIVE MIGRATIONRELEASE RENAMENEW GUESTSTools + Schedules refreshDURABLE APP DATACurrent SQLite stays outside release/ and checkout/REHEARSAL COPY → LIVE TRANSACTIONPocketJS / one substrate, one resident System Guest, many isolated App GuestsNative host / workspace bounds · SQLite ownership · lifecycle · crash recovery
The four responsibilities stay distinct. Human review and runtime rehearsal are the two validation gates before commit.

Inspect: make the running App legible

An Agent cannot evolve software it can only observe as pixels. The first requirement is therefore not an editing Tool. It is a filesystem model that makes App source visible without dissolving the boundaries between Apps.

/workspace/
├── system/app/                 resident Pi Agent System App
└── apps/
    ├── demo/
    │   ├── release/            source currently running
    │   ├── data/               Demo-owned SQLite and files
    │   └── tmp/                disposable Demo files
    └── research/
        ├── release/            separate App source
        └── data/               separate App-owned state

Ordinary Apps are isolated Guests. An ordinary View or Action Guest receives only the database and filesystem surface owned by that App. It cannot walk into a peer App or the native host. Source and durable Data are also separate, so replacing source does not grant a new release ownership of another App's state.

Pi Agent sits one level above those ordinary Guests. It is the firmware-embedded, resident System App, and the native host gives its workspace Tools a bounded view of/workspace. That is why Pi Agent can use read, find, grep and ls across ordinary App source while each ordinary App remains confined to its own surfaces. Path resolution rejects escapes beyond the workspace, and native credentials remain outside the source tree.

Inspection therefore needs no checkout. The running definition is already legible in apps/<id>/release. Checkout is needed only when Pi Agent intends to change that definition.

Resident AgentPocketPi Simulator Chat screen with the resident Pi Agent
Visible workspacePocketPi Simulator Files screen showing the ESP32 workspace
Human update gatePocketPi Simulator review screen for updating Demo from version 1.0.0 to 1.1.0
Three 480 × 800 frames from the real ESP32 product-contract simulator. In the third, Pi Agent has submitted a complete update, but activation still waits for human confirmation on the device.

Modify: edit a bounded candidate

Reading live source is safe; editing it in place is not. The first App-iteration lifecycle boundary is therefore app.checkout({ id }). It copies the complete installed release/ source once into an isolated checkout/ and returns that canonical path. Calling it again reopens the same candidate instead of overwriting work already in progress.

Checkout copies source only. The live SQLite database, temporary files and native credentials stay where they are. This creates the smallest useful write boundary: Pi Agent can change the complete App definition without mutating the running App or cloning its live state.

The candidate is small enough to understand as a directory, not as a build graph:

apps/demo/checkout/
├── app.json                 identity, version, capabilities, Tools, Schedules
├── schema.sql               initial SQLite shape for a new installation
├── migrations/N.sql        forward Data changes for an existing installation
├── actions.js               actor-neutral behavior and SQLite writes
├── view.js                  human UI assembled with the shared View SDK
└── assets/*.json            manifest-declared static resources

The existing bounded write and exact-replacement edit Tools do the actual modification. Pi Agent advances the App version in app.json, changes behavior in actions.js, and changes presentation in view.js. If the durable data shape changes, it also advances schemaVersion and adds the next migration. If only values or behavior change, the schema version stays put.

In the physical demo, those edits were concrete. The Action changed what a button writes to SQLite, while the View changed the badge from blue to green for the new value. The same candidate could also have included a migration if the SQLite shape had changed.

Both entrypoints are raw JavaScript. actions.js uses the shared Framework to define Actions and Data access. view.js assembles View.Screen, View.Row, View.Text, View.Badge and View.ActionButton from the shared View SDK. Layout, semantic colors, typography, touch targets and viewport behavior live in that SDK, so Pi Agent describes the UI with a bounded vocabulary instead of reimplementing a renderer for each App.

There is no on-device TypeScript transform, npm installation or cross-module compile. The files Pi Agent edits are already the files PocketJS will evaluate. The second lifecycle boundary, app.submit, begins only after that candidate is ready to be validated.

Validate: review and rehearse the candidate

A candidate is not valid merely because its JavaScript parses. The requested change must make sense to the human who owns the device, and the implementation must work against the state accumulated by the App already running there.

app.submit({ path }) first verifies that Pi Agent is submitting the canonical checkout for that App. It validates the manifest identity, source layout, App version, Framework API and schema-update contract, then stages the complete candidate as an update request.

That request is deliberately human-in-the-loop. The user reviews what Pi Agent is asking to replace and either confirms it or rejects it and asks for a revision. The human answers the product question: is this the change we want? Native validation answers a different question: can this candidate safely become the running App?

After confirmation, PocketPi performs a rehearsal before any live mutation. This is possible because durable App state has one explicit owner: an App-local SQLite database outside every View and Action Guest heap. The updater copies the quiescent database into a rehearsal directory, applies the candidate migrations to that copy, and loads the candidate Actions and View against the rehearsed Data.

Missing migration steps, invalid Action routes, Framework errors and View construction failures stop the update while the installed release and its Data remain untouched. Source-only updates keep the same schemaVersion. A real SQLite shape change supplies the corresponding migrations/N.sql steps, while PocketPi owns the transaction and SQLite user_version.

Commit: activate one coherent App version

Once the human has approved the request and the rehearsal has succeeded, commit advances the whole App. It does not copy individual edited files over the running directory.

The updater moves the complete candidate under .update/release, then applies the rehearsed migrations to the live SQLite database in one transaction. It quiesces the old Action and View runtimes, preserves the old source temporarily, and uses same-filesystem directory renames to place the complete candidate at the one canonical release/ path.

Atomicity follows the actual ownership boundaries. SQLite migration is atomic in a database transaction. Source activation uses complete-directory renames rather than partial file writes. The .update directory is a crash-recovery record, so a reboot can finish an interrupted activation instead of inventing a mixed release.

This source switch can become executable immediately because PocketJS is one substrate designed to host multiple isolated Guests. The old cached Guests are discarded. A fresh Action Guest evaluates the platform-owned System Framework and then the new raw actions.js; a fresh View Guest evaluates the shared View SDK and then the new raw view.js. No module graph is compiled and no Firmware is rebuilt.

Finally, PocketPi replaces the App's Tool routes and Schedules, publishes the new catalog entry and removes the temporary old source. The App id, native credentials and SQLite owner remain stable. Data is preserved or migrated; Actions and View are replaced; runtime Guests are recreated. That is one coherent transition of Data + Actions + View rather than a set of unrelated patches.

The same Demo App before and after Pi Agent updated it directly on ESP32-S3: SQLite Data is preserved, the Action writes UPDATED, and the View badge changes from blue to green
One committed App transition on ESP32-S3: the existing SQLite value survives, the new Action changes subsequent writes and the new View presents the result differently. Data, Actions and View advanced together without rebuilding Firmware.

The development environment is the closed loop

PocketPi is not a general-purpose development machine. It cannot build an arbitrary npm project, provide Node compatibility or let an ordinary App rewrite Firmware and native security boundaries.

What it can do is more precise: Pi Agent can inspect, modify, validate and commit the complete source boundary of an admitted App on the device where that App is running. That is possible because earlier architecture decisions line up with the four responsibilities:

  • workspace ownership and App isolation make source inspectable;
  • bounded file and lifecycle Tools make a candidate editable;
  • App-owned SQLite and forward migrations make current Data testable;
  • raw JavaScript, the View SDK and one PocketJS substrate make a coherent version directly executable.

The result is not a small microcontroller pretending to be a Mac. It is an ESP32 that understands enough about its own App model to close a real software development loop on itself.