Status: Accepted (2026-08-12). D3 (agent loop on the Worker) and the loop-host role in D2 are superseded by ADR 0085 (2026-08-13): the loop moved to the browser client and the Worker narrowed to a keyed per-model-turn proxy. D1, D2's port/adapter principle, D4, D5, and D6 stand. Naming: Garzone — the bottega apprentice who executes the master's direction (Italian/Renaissance chord, ADR 0059). Literal fit: a tool-acting agent that does the work you direct.
id = grz, package@arno/garzone. Scale: 30K MAU ceiling per ADR 0021 — no dedicated inference infra beyond Cloudflare's free tier.
Context
We want an assistant inside Arno Studio that acts on the design: "add a card here", "align this row to the grid", "swap these tokens". It manipulates the node tree (punta/tela/ponte), not just answers questions. Upstream decisions, made in chat:
- Use case = tree-acting agent, not a text side-panel.
- Key model = hybrid — free-tier on Arno's pooled keys (metered) + power users bring their own key (BYOK).
- Streaming + tool-calling = yes — both core to the use case.
- Landing — branch
feat/sorg-layout, standtest-sorg-layout.arnomake.com.
Open engineering question: how to reach "any model on the market" without wiring N SDKs, and how to package it so the module is self-contained and independent.
Decision
D1. Abstraction = Vercel AI SDK (code) + Cloudflare AI Gateway (ops)
- AI SDK unifies tool-calling + streaming across providers (Anthropic / OpenAI / Google / Mistral / xAI / …), with OpenRouter available as one provider for the long tail. Free npm lib; keys stay ours.
- Cloudflare AI Gateway underneath as the ops layer: metering, per-user rate-limits, cache, fallback, analytics. Free on base tier; we already run on CF. This is what makes the free-tier meter cheap.
- Rejected as core: raw OpenRouter (~5% perpetual markup + all design traffic through a third party), hand-rolled per-provider adapters (weeks + perpetual format drift).
D2. Garzone is a self-contained module that RAISES a port; hosts bring adapters
Partially superseded by ADR 0085 (2026-08-13). The port/adapter principle stands. Only the loop-host role below — "apps/api Worker (agent loop host, keys, SSE)" in the diagram — moves: the loop host is now the browser client, and the Worker is a per-turn provider proxy.
@arno/garzone owns the engine (AI SDK, agent loop, streaming, tool-schema contract) and
depends on none of ponte/punta/tela. It exports:
- the agent runtime + SSE stream shape;
- a
ToolPortcontract — the tool catalog as typed schemas with no implementation; configureGarzone(port)/subscribeGarzoneConfigseam (same pattern as compassoCameraPortADR 0077, and the package-seam config injection).
The host (apps/web Studio) supplies a ToolPort adapter that wires each tool call to the
real ponte command. Garzone raises the capability; consumers take it and inject their hands.
This keeps Garzone independent, testable in isolation (mock port), and reusable by any surface
(Studio canvas, later a CLI or CI check) without dragging the tree packages along.
@arno/garzone (engine + ToolPort contract) ← depends only on AI SDK
▲ configureGarzone(adapter)
apps/web Studio (ToolPort adapter → ponte ops)
apps/api Worker (agent loop host, keys, SSE)D3. Agent loop runs on the Worker (never the client)
Superseded by ADR 0085 (2026-08-13). The loop runs in the browser client (the tree and StepBack undo both live there); the Worker narrowed to a keyed per-model-turn proxy streaming the turn-proxy union. The request body is now
{ messages, model? }, not{ prompt, selectionContext, model }. The BYOK-key-server-only guarantee below is unchanged.
Client sends { prompt, selectionContext, model } — no key. Worker runs the loop:
model ⇄ tool-call ⇄ ToolPort ⇄ result, streaming SSE back (text deltas + applied edits).
Keys (pool or BYOK) live only server-side.
D4. Key source is a resolved enum; metering is flag-gated middleware
keySource ∈ { arno_pool, user_byok }. Resolution: user has BYOK → use it, unmetered;
else → arno_pool, metered. Metering middleware (limit / usage / reset) is built but
flag-gated off for the BYOK-first MVP, flipped on when free-tier ships. MVP = pure BYOK;
hybrid is pre-wired, not retrofitted.
D5. Every tree mutation goes through the StepBack command path (ADR 0074)
The adapter's ToolPort implementation calls the same ponte commands a human click uses, so
every Garzone edit is undoable with one back/forward. Safety spine: a bad generation is
Cmd+Z, not a corrupted tree. Garzone itself never mutates a tree — it only emits tool calls.
D6. Tool catalog (contract in garzone, mapped to real ops in the host adapter)
Read tools (context, no mutation): getSelection, readSubtree(nodeId), listTokens(group).
Write tools (host adapter routes through StepBack-recorded commands):
| Tool | Backs onto (real exports) |
|---|---|
insertNode / insertSubtree | insertSubtree, insertFromPalette, insertDefaultChild, createInstance |
moveNode | moveNode, moveNodesAt, moveBetweenSlotsInZones |
removeNode | removeById, removeNodes, removeManyFromActiveTree |
duplicateNode | duplicateActiveBox, duplicateManyActiveBoxes |
groupNodes / ungroupNodes | groupNodes, groupActiveBoxes |
setProp | updateProp, updatePropInZones |
resizeArtboard | setTelaSize, setDefaultSize |
Tokens are set via origine authoring, not free-form setProp, to keep the token SSOT intact.
Data model (Neon)
garzone_conversation(project-scoped message history)garzone_byok_key(per user, encrypted at rest, provider + ciphertext, never returned to client)garzone_usage(per user/day: tokens, request count — feeds the metering middleware)
Security
- Keys server-only; BYOK encrypted at rest; never serialized to client or logs.
- Prompt-injection: tree/token content the model reads is untrusted data, not instructions.
Garzone may only call the fixed
ToolPortcatalog; it cannot execute arbitrary text as a command. - Metering + CF rate-limit cap free-tier abuse (one user can't drain the pool via a script).
Consequences
- MVP is small: BYOK-only, no billing, metering off. "Any model" = whatever AI SDK speaks (+ OpenRouter tail).
- Free-tier is a later flag flip + a funded pool key, not a rewrite.
- Undo-safety and token-SSOT preserved by construction (D5, D6).
- Garzone stays independent — mockable via
ToolPort, no studio-package dependency. - Risk: tool-calling quality varies by model — agent-grade models flagged; weak BYOK models get an honest quality label.
Alternatives rejected
- OpenRouter as core — 5% perpetual markup + third-party sees all design traffic.
- Hand-rolled N-provider adapters — weeks + perpetual API-format drift.
- Platform-pays from day one — full billing/fraud/limit stack before PMF; against the $5 floor.
- Garzone depends on ponte/punta directly — would break "self-contained, raises a port" and couple the engine to the tree.