ADRs
ADR 0084 — Garzone: the in-Studio AI assistant (provider-agnostic, tree-acting)

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, stand test-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 ToolPort contract — the tool catalog as typed schemas with no implementation;
  • configureGarzone(port) / subscribeGarzoneConfig seam (same pattern as compasso CameraPort ADR 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):

ToolBacks onto (real exports)
insertNode / insertSubtreeinsertSubtree, insertFromPalette, insertDefaultChild, createInstance
moveNodemoveNode, moveNodesAt, moveBetweenSlotsInZones
removeNoderemoveById, removeNodes, removeManyFromActiveTree
duplicateNodeduplicateActiveBox, duplicateManyActiveBoxes
groupNodes / ungroupNodesgroupNodes, groupActiveBoxes
setPropupdateProp, updatePropInZones
resizeArtboardsetTelaSize, 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 ToolPort catalog; 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

  1. OpenRouter as core — 5% perpetual markup + third-party sees all design traffic.
  2. Hand-rolled N-provider adapters — weeks + perpetual API-format drift.
  3. Platform-pays from day one — full billing/fraud/limit stack before PMF; against the $5 floor.
  4. Garzone depends on ponte/punta directly — would break "self-contained, raises a port" and couple the engine to the tree.