Multi-Agent Canvas
Beobachtbares Multi-Agent-System: Sonnet-Orchestrator delegiert an parallele Haiku-Worker, zwei Kritiker debattieren, ein Judge entscheidet — alles streamt live auf einen Graph-Canvas, jede Sitzung als event-sourced Permalink reproduzierbar.
Schlüsselentscheidung
Mid-stream Critic-Interrupt rausgenommen — sah spektakulär aus, produzierte aber schlechteren Output (das Modell reagiert auf sein eigenes halbfertiges Ergebnis). Die Debatte feuert jetzt nach Layout v1 gegen das fertige Ergebnis. Weniger visuell aufregend, dafür inhaltlich besser. Das gleiche Muster zieht sich durch: keine UX-Theater-Lügen, lieber den Defer offen kommunizieren (steer_queued → steer_applied).
Skizziere ein Layout, leg ein Markenbild dazu, tipp einen Prompt — eine Sonnet-4.6-Orchestrator-Instanz schlägt eine Besetzung aus Haiku-4.5-Workern vor, fächert parallele Entwürfe auf, lässt zwei Kritiker (Performance + Ästhetik) gegen einen Judge debattieren und liefert eine fertige Landing-Page. Jeder Token streamt in Echtzeit auf einen Graph-Canvas; jede Nachricht zwischen den Agenten ist event-sourced und per Permalink wiederholbar. Die Landing-Page ist nur das Vehikel — das eigentliche Artefakt ist das Engineering-Substrat darunter: WebSocket-Multiplexing über parallele Anthropic-Streams, Event-Source-Replay, Prompt-Caching mit Live-Anzeige der Cache-Hits, ein ehrlicher Cancel- und Steer-Lebenszyklus, eine auf dem Canvas sichtbar verzweigende und wieder zusammenführende Topologie.
Die These
Die Landing-Page ist nur das Vehikel — das eigentliche Artefakt ist das Engineering-Substrat darunter: WebSocket-Multiplexing über parallele Anthropic-Streams, ein event-sourced Replay-Modell, Prompt-Caching mit Live-Anzeige der Cache-Hits, ein ehrlicher Cancel- und Steer-Lebenszyklus und eine Multi-Agent-Debatte, die auf dem Canvas sichtbar verzweigt und wieder zusammenführt.
Der Flow
Sechs Stufen, vom Skizze-Input zum auto-publizierten Layout. Jede Stufe ist auf dem Canvas live sichtbar — Nodes erscheinen, streamen, kollabieren oder leuchten je nach Lifecycle-Zustand.
┌─────────────┐
│ Inputs │ prompt · reference image · paint-canvas sketch
└──────┬──────┘
▼
┌─────────────┐ propose_roster (tool use, vision)
│ Orchestrator│ delegate(Designer, alts=3) ─┐
│ Sonnet 4.6 │ delegate(Mascot, alts=3) ─┤ ← parallel
│ + thinking │ delegate(Copywriter) ─┤
└──────┬──────┘ delegate(Layout) ─┘
▼
┌────────────────────────────┐
│ Workers (Haiku 4.5) │
│ Designer×3 · Mascot×3 │
│ Copywriter · Layout │
└──────┬─────────────────────┘
▼
┌─────────────┐ ┌──── Critic-Performance ┐
│ Layout v1 │──────►│ │── Judge ─► one ISSUE/FIX
└──────┬──────┘ └──── Critic-Aesthetics ──┘ │
│ │
└─────► (user approves) ────► Layout v2 ────► auto-publish ◄─┘
│
▼
/r/<id> permalink replaySechs Engineering-Probleme
Jedes hat eine eigene Tiefen-Doku im Repo unter docs/hard-problems/. Das sind die Sachen, die nicht aus einem One-Shot-Prompt herausfallen.
WebSocket-Multiplexing über N parallele Anthropic-Streams
Ein einziger Socket, monotonisch wachsende Per-Session-Sequence-IDs, Per-Agent-Demux auf dem Client. Kein paralleler Socket pro Worker — eine Verbindung, viele Sub-Streams.
Cancel und Steer mitten im Stream — ohne State-Korruption
Echter Cancel über AbortController; Steer ist ehrlich darüber, dass es erst im nächsten Zug greift (steer_queued vs. steer_applied als zwei sichtbare Events). Kein UX-Theater — die Grenzen des Streaming-Protokolls werden offen kommuniziert.
Spekulative parallele Entwürfe mit hartem Kosten-Deckel
Designer ×3, Mascot ×3 laufen parallel über Promise.all. USD-Caps pro Session und pro Monat werden vor jedem Modell-Call geprüft; enge maxTokens-Budgets pro Worker verhindern Ausreißer bei einzelnen Calls.
Skizze → Vision → eingeschränkte Layout-Generierung
Natives HTML-Canvas als Mal-Eingabe (keine Excalidraw/tldraw-Abhängigkeit) → base64-PNG → Vision-Payload an den Orchestrator → ein Layout-Prompt mit harten Constraints, das die Skizze respektiert.
Event-sourced Replay über non-deterministische LLM-Outputs
Jeder WebSocket-Frame wird an libSQL angehängt; Permalink-Replay liest aus dem Log — keine Re-Inference. Die Permalinks sind bit-identische visuelle Replays, kein erneuter Prompt.
Prompt-Cached Orchestrator-System-Prompt mit Live-HUD
cache_control: ephemeral auf dem System-Prompt; das Cache-Hit-Signal wird als Live-HUD-Badge sichtbar. Cold-vs-Warm-Run-Kosten werden gemessen und im README dokumentiert.
Stack — pragmatisch ausgewählt
Eine Schicht pro Entscheidung. Wo nötig erklärt — z.B. Sonnet vs. Opus für Tool-Routing, Bun statt Node für Native-WS.
| Schicht | Wahl |
|---|---|
| Runtime | Bun 1.3 |
| API | Hono + native Bun WebSocket |
| Event-Store | libSQL (file-mode lokal, Turso-kompatibel) |
| Anthropic-SDK | messages.stream() mit adaptivem Thinking |
| Orchestrator | claude-sonnet-4-6 (5× günstiger als Opus, gleichwertig für Tool-Routing) |
| Worker | claude-haiku-4-5 |
| Frontend | Next.js 15 App Router + React 19 |
| Canvas | @xyflow/react + custom labeled-bead edges |
| State | Zustand mit per-Agent-Slices (keine Context-Re-Renders) |
| UI | Tailwind + Radix Dialog + Framer Motion + Sonner |
| Skizzen-Input | natives <canvas> (keine Excalidraw/tldraw-Dep) |
| Deploy | Docker + Caddy auf einer VPS |
Tradeoffs — offen genannt
Die unangenehmen Entscheidungen, die in den meisten Demos versteckt werden. Hier sichtbar gemacht.
Replay reproduziert den aufgenommenen Lauf, keinen neuen Modell-Aufruf
Permalinks sind bit-identische visuelle Replays aus dem Event-Log — kein neuer Prompt an das Modell. Der Footer der Landing-Page sagt das offen.
Steer greift erst im nächsten Zug
Das Anthropic-Streaming-Protokoll hat keinen Interrupt mitten in der Message; so zu tun wäre UX-Theater. Das Zwei-Event-Protokoll steer_queued → steer_applied macht die Verzögerung sichtbar.
Die Besetzung ist nicht voll emergent
Der System-Prompt drängt den Orchestrator dazu, eine Debatte aus Performance-Critic, Aesthetics-Critic und Judge vorzuschlagen — aus Layout-Gründen für den Canvas. Voll emergent habe ich es ausprobiert: das Ergebnis waren flachere Besetzungen und schwächere Reviews.
Kosten-Deckel zwischen Calls, nicht innerhalb eines Calls
Mitigation: enge maxTokens-Budgets pro Worker. Dokumentiert in docs/hard-problems/03-speculative-drafts.md.
Critic-Interrupt mitten im Stream haben wir wieder rausgenommen
Echte Interrupts sahen spektakulär aus — produzierten aber schlechteren Output (das Modell reagiert auf sein eigenes halbfertiges Ergebnis). Die Debatte startet jetzt nach Layout v1, gegen das fertige Ergebnis. Weniger spektakulär anzusehen, dafür inhaltlich besser.
UI bewusst auf Deutsch
System-Marker (ISSUE, FIX, HEADLINE, <!doctype html>, JSON-Keys) bleiben englisch — die Parser hängen daran. Der Worker-Output ist deutsch.
Was bewusst draußen ist
- Multi-Tenant — ein einziger Anthropic-Key, ein globaler Kosten-Pool.
- Auth — die Demo ist öffentlich, Cost-Caps sind die einzige Sperre.
- Mobiler Canvas — Desktop-only, Mobile zeigt einen Fallback-Banner mit Demo-Video.
- Verzweigen aus einem Permalink — das Datenmodell unterstützt es, die UI nicht.
- Versionierte Event-Log-Schemata — additive Typen verträgt das Schema problemlos, Umbenennungen würden alte Replays zerschießen.
Deployment
Docker-Compose plus Caddy auf einer VPS. Caddy holt TLS automatisch; die API serviert WebSocket auf /ws, Replay auf /r/<id>, alles andere routet zu Next.js. Live unter multi.prototyp.ms — Quelle offen unter github.com/stackola/multi.