Loom — Agent swarm canvas with interrupt-collaborate flow
Loom is a Tauri 2 desktop app where an agent swarm renders interactive widgets through a strict JSON contract (A2UI protocol), can pause execution to request human input, and resumes deterministically from SQLite-backed state.
GitHub: https://github.com/tuxclaw/loom
The Core Idea
Agents do not render HTML. They emit A2UI directives — JSON objects that map to a closed catalog of pre-compiled widgets (StatusFeed, ApprovalPanel, CodeDiffViewer, TaskForm, DataChart). The frontend validates every directive against an envelope schema and per-component JSON Schema before mounting. No arbitrary markup injection. No agent-authored CSS/JS.
The interesting part is the interrupt-collaborate mechanism. When a graph node needs human input, the executor:
- Persists full GraphState to SQLite (crash-safe)
- Registers a oneshot channel for the interrupt token
- Emits an A2UI directive to the canvas
- Blocks on the channel
When the human responds, the executor validates the token against SQLite first, consumes the interrupt in one transaction, then sends on the channel. Durability decides the race — first response wins, duplicates are rejected at the database layer.
Architecture
The system has four processes:
- Vite Frontend (React) — Component catalog + renderer
- Tauri Backend (Rust) — IPC relay, no business logic
- Graph Executor (Rust) — SQLite state, interrupt registry
- OpenClaw — Stateless worker pool
Transport between Tauri and executor is stdin/stdout JSON lines.
Stack
- Frontend: React 19, TypeScript, Vite, pure CSS (Tokyo Night)
- Backend: Tauri 2 (Rust)
- Executor: Standalone Rust binary, rusqlite WAL mode, tokio oneshot channels
What We Learned
Boot recovery is more than re-emitting. The executor needs to re-spawn graph tasks blocked on receivers, not just re-emit directives.
Stdout needs type discrimination. Two message types over one pipe requires a type field for routing.
Events beat blocking for Tauri-to-executor communication.
Channel lifetime in recovery is subtle. The oneshot receiver must live inside the spawned task.
The 7 Invariants
- SQLite is the source of truth for interrupt state
- Persist, register channel, emit directive — never reorder
- On resume: consume in SQLite first, then send on channel
- First response to clear pending_interrupt wins
- Interrupt tokens are executor-minted only
- The executor owns all durability and blocking
- Interrupt nodes sit on node boundaries
Built with a full agent squad flow — 6 phases, 12 dispatches, 8 review gates, 5 blockers caught and fixed. Happy to discuss the architecture.