Skip to content

Repository files navigation

A2App: Agentic App Building and Runtime System

A2App is a system for building and running agentic applications. It composes an inference endpoint, a shared workspace, an app runtime, and a coordination harness into a coherent whole.


Key Features

Five capabilities define the system, independent of any particular implementation:

A2App key features


Reference Implementation

One concrete way to implement A2App maps the four features onto three components:

A2App components


Getting Started

1. Install Pi

A2App is built on pi, a coding agent harness that runs LLM inference and exposes agent tool-use capabilities. Install it via npm:

npm install -g @earendil-works/pi-coding-agent

Verify the installation:

pi --version

2. The Makepad Extension

This repository includes a pi extension at .pi/extensions/makepad/ that teaches pi about the a2app_harness tools — launching Makepad Splash mini-apps, inspecting their widget trees, clicking buttons, sending prompts to sub-agents, and receiving streaming responses. It is loaded automatically when pi runs inside this project directory.

The extension registers custom tools (launch_makepad_app, check_debug_app, launch_app_with_agent, etc.) and an auto-handler for splash-to-agent communication. The workflow goes like this:

  1. You describe an app in the pi chat — e.g. "build a counter app with +/- buttons and send the count back to me when I click Send".
  2. Pi generates Splash DSL — a simple layout language (View, Label, Button, TextInput) and calls launch_makepad_app to render it inside the Makepad host window.
  3. Pi inspects the UI — it takes a widget_snapshot to discover widget positions (orphaned widgets have window-relative coordinates), then simulates clicks and text input to verify the app works.
  4. Interactive apps communicate back — splash buttons call ui.__pi_response.set_text(...) to send data to pi. Pi reads responses via inspect_makepad_doc or the async wait_for_response tool.
  5. AI-powered apps — using launch_app_with_agent, pi creates a blank-slate sub-agent session. The splash app sends prompts via ai:ask: messages, and the sub-agent's response streams token-by-token into an auto-displayed widget.

3. Build

cargo build -p harness -p makepad-host

4. Try It Out — AI Chat App

Start pi in this project directory:

pi

Then say something like:

Launch an AI chat app (using a sub agent).

Pi will generate the Splash DSL, launch it inside the Makepad host window with a blank-slate sub-agent session, and the response will stream token-by-token into the app.


Component Notes

A — Coding Agent handles inference (feature 1), workspace access (feature 2), and the main agent chat UI (feature 4). It runs the LLM loop, provides the user-facing chat interface, reads and writes the filesystem, and communicates outward via a WebSocket connection to the Rust process.

B — Rust Process is the harness (feature 5). It runs a WebSocket server that the coding agent connects to, and maintains a CRDT for state synchronisation with the app runtime. The CRDT could in principle live in the coding agent, but cross-language friction made a WebSocket bridge the pragmatic call.

C — App Runtime covers the runtime (feature 3). It hosts a Makepad application shell capable of launching Splash mini-apps, and consumes state from the Rust process via the CRDT.


Tools and Capabilities

The system has three components: a coding agent (pi), a harness (Rust bridge process), and a host (Makepad app runtime). Communication flows: pi ↔ harness (JSON WebSocket, port 2341) and harness ↔ host (samod CRDT sync, port 2342). The CRDT is an implementation detail used to keep the host's UI state in sync with the harness — all messages between pi and the host pass through the CRDT document (AgentDoc), but pi never interacts with the CRDT directly.

launch_makepad_app (Pi Agent → Harness → Host)

Sends a {"type": "launch"} message over JSON WS to the harness, which writes the splash body into the CRDT pending_app field. The host receives the CRDT update via samod sync, evaluates the Splash DSL, and renders it in an AgentSplash widget. The CRDT here acts as a write-once command queue: the harness writes, the host reads and processes, then clears the field.

launch_app_with_agent (Pi Agent → Harness → Host + Sub-Agent)

Like the above but also creates a sub-agent session in the pi extension. The splash body calls ui.__pi_response.set_text("ai:ask:" + message) which writes to the CRDT user_response field via the host. The harness detects the version increment and forwards the response to pi over JSON WS. The pi extension's auto-handler routes it to the sub-agent. When the sub-agent responds, streaming deltas are sent back through the harness (as send_streaming_delta → CRDT streaming_text) and the host's sync_streaming_text() method updates the UI on Event::Signal.

check_debug_app (Pi Agent → Harness → Host)

Sends a {"type": "debug"} message over JSON WS. The harness writes debug_command to the CRDT doc. The host's process_debug_commands() executes the command and writes the result back to debug_response, which the harness forwards to pi. For click/type_text interactions, the harness also sets a pending_interaction flag so the bridge loop waits for the host to process before reading stale user_response values.

Command Executed By Returns
widget_snapshot Host reads cx.widget_tree() JSON array of widgets (id, type, position, size, text, value)
click Host dispatches synthetic MouseDown/MouseUp to splash.handle_event() Debug response after execution
type_text Host walks splash children depth-first, fills first TextInput Debug response after execution

inspect_makepad_doc (Pi Agent → Harness)

Sends a {"type": "get_doc"} message over JSON WS. The harness reads the current CRDT AgentDoc state directly and returns app_id, user_response, error_message, and status. This is synchronous — no CRDT sync needed because the harness owns the doc.

__pi_response / __pi_data (Injected Widgets in Host)

The host's AgentSplash injects these widgets into every splash body via SPLASH_PREFIX / SPLASH_SUFFIX:

  • __pi_response := Label{text:""} — the splash app calls set_text() on this Label to send data back to pi. The host detects the text change in handle_event() (on every event type), writes it to the CRDT user_response field, and increments user_response_version (to detect same-value re-sends like toggles). The harness bridge loop compares version numbers and forwards the response to pi over JSON WS.

  • __pi_data := Label{text:" "} — the splash app reads text() from this Label, which gets its value from the CRDT pi_response field. Written by the harness when pi sends {"type": "send_pi_response"}, synced to the host on the next Event::Signal.

  • __ai_text := Label{text:" "} — auto-displays sub-agent responses. Updated by sync_streaming_text() from the CRDT streaming_text field (live streaming deltas) and sync_pi_data_to_splash() from pi_response (final response).

  • __run_splash := AgentSplash{is_root:false} — a nested AgentSplash that evaluates and renders \``runsplashcode blocks inline. Called bysync_streaming_text()during streaming andsync_pi_data_to_splash()on completion. Has built-in error recovery: ifeval_bodyfails,set_text()` restores the previous valid body.

Streaming Deltas (Sub-Agent → Pi Extension → Harness CRDT → Host)

When a sub-agent responds to an ai:ask: prompt:

  1. The pi extension's per-prompt subscription captures each text_delta event
  2. Each delta (raw new characters) is sent to the harness as {"type": "send_streaming_delta"}
  3. The harness appends the delta to the CRDT streaming_text field
  4. The CRDT syncs to the host via samod WebSocket
  5. On receiving Event::Signal, the host calls sync_streaming_text() which:
    • Compares self.last_streaming_text with the current CRDT value
    • If changed: updates __ai_text Label and extracts \``runsplash` code blocks
    • Passes extracted code to __run_splash.set_text() for inline rendering (error recovery on partial code)
  6. On completion, the extension sends {"type": "send_streaming_end"}; the harness sets pi_response to the final text and clears streaming_text

Key design: CRDT reads only happen on Event::Signal (not on 60fps Draw/Mouse events) to avoid CPU jank.


Session: Progressive Splash App Testing (2026-07-09)

Five splash apps launched and tested, from simple to meta. Full session log at app_gen.jsonl.

Counter

1. Countercounter-simple

Launched with launch_makepad_app. Interactive +/- buttons using let count = 0 variable persistence. Clicked with check_debug_app(debug_command="click") using window-relative orphan coordinates from widget_snapshot. "Send to Pi" button demonstrated splash→pi communication via __pi_response.set_text(), verified with inspect_makepad_doc.


Todo

2. Todo Listtodo-1

Launched with launch_makepad_app. Items added by typing into a TextInput via check_debug_app(debug_command="type_text") then clicking "Add". Uses while loop over a struct array with items.push()/items.remove() for list management. "Remove Last" removes items, "Send to Pi" returns the full list via __pi_response.


AI Chat

3. AI Chatchat-ai-1

Launched with launch_app_with_agent (creates a blank-slate DeepSeek V4 Flash sub-agent). Splash body sends __pi_response.set_text("ai:ask:" + msg) which the extension auto-handler routes to the sub-agent. Response streams token-by-token into the injected __ai_text widget via the streaming delta system. Asked "What is a CRDT?" — got a full streaming response with bullet points.


Splash Generator

4. 🌟 Splash Generatorsplash-gen-1

Launched with launch_app_with_agent using a system prompt teaching correct Splash DSL syntax (:= naming, on_click:||{}, width:Fill, no commas). Asked for "a simple counter with + and - buttons". The AI generated valid \`\`\`runsplash code which was automatically picked up by sync_streaming_text() and rendered inline via the nested __run_splash AgentSplash — height grew from 0 to 286px.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages