Agent API and harness
Reticle exposes its whole editing engine as a small, serializable command surface, so a program (or a language model) can build and check layouts through the same operations a human uses. On top of that surface sits a propose-verify-correct harness that drives a model against objective checks, writes a replayable transcript, and can mirror its edits onto the live collaboration document.
The command surface (reticle-agent-api)
AgentCommand is a tagged, serde-serializable
enum of 30 operations over the engine: create a cell, add a rectangle, polygon, or
path, add a label or pin, transform or delete shapes, set the technology, run DRC,
check a connectivity intent, extract nets, compare against a netlist, export GDS or
OASIS, and render a region to PNG. A [Session] owns an editable document and a
stable element-id allocator, so a command that adds geometry returns an
[ElementId] that later commands and the transcript can refer to even across
deletions (ADR 0018).
Five of those operations lift the in-app editor’s productivity actions to the
command surface (ADR 0031): BooleanCombine (union, intersection, difference, or xor
over a set of shapes, onto a target layer), AlignShapes, DistributeShapes,
OffsetShapes (grow or shrink), and BuildViaStack (a cut plus enclosures sized
from the technology’s enclosure rules). They run on the same reticle-geometry
primitives as the editor, so agent-built and hand-built geometry match exactly.
Every command applied to a session is recorded as a [CommandRecord] in a
[Transcript], and the model’s document has a [document_hash]. Replaying a
transcript reproduces that hash exactly, so a run is deterministic and auditable:
the verify_replay path recomputes the hash and rejects a tampered transcript.
The propose-verify-correct loop (reticle-agent)
flowchart LR
M[Model] -->|AgentCommands| A[Apply to Session]
A --> V{Verify: DRC + intent}
V -->|clean| D[Done, write artifacts]
V -->|violations| F[Feed violations back]
F --> M
The harness asks a [ModelClient] for a batch of commands, applies them to a private
session, and verifies the result with the SKY130 DRC subset plus, where a task
carries an intent spec, the connectivity checker. Violations become correcting
context for the next proposal, up to an iteration bound. Success is defined by the
checker, not by the model’s say-so, and a failure is recorded as a failure, never
retro-edited to a pass. Each run writes four artifacts: the transcript, the final
GDS, a rendered PNG, and a result record.
The model is either the real AnthropicModel (an Anthropic-compatible endpoint; the
API key is read from the environment only and never printed, serialized, or written
to an artifact) or the deterministic MockModel used offline and in tests.
Iterative refinement (mid-session constraints)
A session does not have to be one fixed prompt. A user watching the loop can add a new constraint between iterations (“make the wire wider”, “keep it on met1”, “shrink the guard ring”) and the loop folds it into the very next proposal without tearing the run down and starting over. This is the refinement protocol.
flowchart LR
U[User constraint] -->|between iterations| C[Fold into context]
V[Checker feedback] --> C
C --> M[Model]
M -->|AgentCommands| A[Apply]
A --> K{Verify}
K -->|clean and constraint met| D[Done]
K -->|violations| V
K -.->|user adds more| U
The mechanism is deliberately small. run_agent_task_refined takes a
RefinementSource: a trait whose one method, drain(iteration), returns the
constraint strings that have arrived since the loop last asked. The loop drains it once
per iteration, just before it asks the model, and accumulates the constraints. Each
iteration it builds the model context from the accumulated user constraints followed by
the checker’s own failure reasons, so the model sees both in one place. Because the
constraints accumulate rather than replace, a constraint added once keeps conditioning
every later proposal; because the checker still runs every iteration, success is still
defined by objective verification, not by the model’s agreement that it complied.
Three properties make this safe and testable:
- No restart. Folding a constraint into the running context is one continuous run:
the session, its transcript, and its element ids are never rebuilt. The
refinement_is_folded_in_and_loop_converges_without_restarttest starts a task, injects a widen constraint before the second iteration, and asserts the final layout carries the wider wire while the run stays a single loop. - Bounded on conflict. A constraint that cannot be satisfied (it conflicts with the
task, or asks for something no proposal can reach) does not spin forever: the same
max_iterationscap applies, and the run is recorded as an honest failure. Theconflicting_refinement_is_bounded_by_max_iterations_not_infinitetest proves the loop stops at the cap rather than looping. - Frozen context type. The refinements ride in the existing
Context::feedbackchannel, soreticle_bench’sContexttype is unchanged. A caller with no mid-session constraints usesrun_agent_task, which isrun_agent_task_refinedwith aNoRefinementssource and behaves exactly as before.
The source is whatever a caller needs: a channel receiver behind a live UI or HTTP
endpoint, or a scripted RefinementFn closure in a deterministic test. The loop owns no
channel of its own, so the same code path serves an interactive session and a reproducible
convergence test on the mock model.
Planning transparency (PlanStep)
Before each iteration’s proposal, the harness records a PlanStep: the iteration’s
goal (the task prompt), the intended tools (the op names of the commands the model
proposed, in order), and the expected checks (the always-on DRC oracle plus the
task’s own checker). These steps accumulate into Transcript::plan, a parallel log
that rides alongside the command records, and the agent panel renders them as a
Plan section so a viewer can see the agent’s stated intent next to what it did.
A plan step is narration, not a contract: nothing enforces that an iteration used
exactly the tools it listed or that its checks passed. That is deliberate, so the
stated plan and the recorded outcome can be compared after the fact for failure
mining. The field is additive and replay-neutral: it carries #[serde(default)], so
a transcript written before the plan log existed still deserializes (as an empty
plan), and replay reads only the records and the final hash.
Live collaboration (reticle-agent::collab)
An [AgentCollaborator] mirrors each agent step onto the reticle-sync CRDT under a
distinct actor id, as one atomic transaction per step, so a human peer watching the
room never sees a half-drawn step and can edit alongside the agent (ADR 0022). The
same transcript the harness writes is what the in-app replay theater plays back
through a live session.
Id-addressed edits: transform and delete (ADR 0066)
The geometry-creating commands change what a human sees, so those are mirrored. So are
the two edits that address existing elements, TransformShapes and DeleteShapes: the
gap ADR 0022 documented and ADR 0066 closes. They name the command surface’s stable
ElementIds, which are not the CRDT’s actor:counter element ids, so the collaborator
drives an authoritative internal session in lockstep with the mirror: applying each
command there validates it and hands back the ElementId it assigned, and the bridge
records ElementId -> CRDT id for every shape it creates. A later transform resolves each
addressed id and moves that record’s geometry in place (the shape keeps its identity); a
delete removes it. An id the bridge never learned (a shape created before it attached) is
skipped and reported in StepReport.skipped, never applied incorrectly or dropped
silently. Convergence tests exchange these edits with a concurrent human peer in both
orders and confirm the transform actually moved the shape and the delete removed it.
Joining a real room (reticle-agent::live)
reticle_agent::live::run_in_room connects an AgentCollaborator to a real
reticle-server relay room over a native WebSocket (ws://host/ws/{room}). For each
step it ships that step’s CRDT delta as one binary frame in the same SyncMessage framing
the browser transport uses (ADR 0058): one step is one frame, so a watcher never sees a
half-step. It publishes the agent’s presence (cursor at the last placement, an amber color,
a display name), and applies inbound peer frames back into its own document. The agent is
then indistinguishable from any other participant: its edits reach browser humans, and
their concurrent edits reach it. An in-process integration test proves both directions over
a real socket.
The agent_live_room example is the small CLI entry: --relay <ws-url> --room <name> joins
a room and runs the demo; --emit <path> regenerates the committed transcript.
The DRC-fix demo transcript
examples/collab/agent_drc_fix.transcript.jsonl is a committed, replayable transcript of
the agent fixing a seeded design-rule violation in a live room: it seeds two met1
rectangles closer than the SKY130-subset m1.2 spacing (140 DBU), runs DRC (flagged),
TransformShapes the second rectangle to a legal gap, and runs DRC again (clean). It
replays to a pinned document hash, checked by a test.
This is a deterministic scripted run, not a live model: the commands are the fixed
script in reticle_agent::live::scripted_drc_fix_steps, driven by the harness with no LLM,
no API key, and no network beyond the relay socket. The transcript trailer says so, and so
does this paragraph: the label is honest everywhere.
Scoped sessions and context packs (reticle-agent::context_pack)
A session can be opened on a region of the document rather than the whole thing.
What the DRC error browser’s “ask the agent to fix this” button does, and what it does
not. The button assembles the selected violation’s rule, layer, region and measured-against-
required numbers into a context string and opens a session with it. It does not constrain
the run to that region: the region travels in the prompt as context, and scoped-region
enforcement is a separate seam this action does not use. The handler says the same thing about
itself at crates/reticle-app/src/app/verify.rs:1080-1082. Check it with:
cargo nextest run -p reticle-app --test qa139_drc_ask_agent
CORRECTED 2026-08-01 (session 36). This paragraph previously read “it hands the harness a rectangle and the specific violation, and the agent works only that corner”, which was false in its second half and was contradicted by the handler’s own doc comment. It is recorded here rather than quietly reworded because this repository does not backdate its record. Found by an independent review, as collateral while confirming that the button exists at all, which it does.
For a scoped run the harness does not condition the model on the whole-document
snapshot. Instead a [ContextPack] assembles a minimal, region-local context string:
- the scoped region;
- the shapes whose bounding box overlaps that region, each as kind, layer, and bounding box, capped so a dense region still yields a bounded prompt;
- the violated rule, stated structurally (kind, layers, measured value, required value);
- only the technology rules whose layers appear in those shapes or in the violation.
The overlap test is the same inclusive touch-or-overlap test the query_shapes command
uses, so a pack and a query_shapes on the same rectangle agree on which shapes are in
scope. The pack is a pure function of the document and the region, so it plugs into the
same context hook the whole-document path uses: a scoped run sets the model’s document
context to the pack instead of the summary.
Why it saves tokens. The compact per-cell summary the unscoped hook sends is already
small, but it is lossy: it lists how many shapes a cell holds, not where they are. To
reason about a local geometric fix a model needs the coordinates of the nearby shapes,
so the honest whole-document baseline is the full per-shape listing (whole_document_context),
not the summary. A pack replaces that full listing with a region-scoped slice.
On a synthetic document of one cell with 200 shapes, a met1 width rule and a met2 spacing rule, and a repair region overlapping exactly one shape, the measured estimates (characters over four) are:
| Context | Tokens | Characters |
|---|---|---|
| Whole-document, full per-shape fidelity | 1878 | 7509 |
| Scoped pack (one shape + violation + one relevant rule) | 62 | 245 |
That is roughly a 30x reduction, about 97% fewer tokens, and the pack’s size tracks the
region rather than the design, so the saving grows with the design. The measurement is
pinned by a test in the context_pack module.
Where it runs
reticle-mcpexposes this command surface to a model over the Model Context Protocol; see the MCP chapter.reticle-demo-serverruns the loop behind a rate-limited public endpoint and streams each step to a watchable room; see Deployment.- The benchmark suite scores the loop across 63 graded tasks.