Building with AI

Layer Contracts: Taming a Swarm of AI Agents

Running several coding agents in parallel without collisions. How scoping each agent to one typed layer — with a contract the compiler enforces — kept the build safe.

Val OvinnikovVal Ovinnikov3 min read
Three glowing translucent teal layers stacked in a warm amber dark, joined by luminous vertical seams, with a single bright agent-light resting inside each layer's own boundary.

Running one agent is easy. Running several without them tripping over each other is the actual problem — two agents editing the same seam produce merge conflicts and, worse, quiet contradictions. My answer on this blog was to stop thinking about agents and start thinking about layers: give each agent one layer of the stack and a contract at its boundary that the compiler enforces. Part of the Building with AI series.

A contract is a boundary you can typecheck

The monorepo already had the boundaries; I just made them load-bearing. Three layers, each depending only on the one beneath it: the Sanity schema in apps/cms, the typed query layer in packages/service, and the Next.js app in apps/web. The contract between them isn't a document — it's an exported type.

packages/service/src/index.ts
1// The whole contract apps/web is allowed to know about:
2export async function getPostCards(): Promise<PostCard[]>;
3export async function getPostBySlug(slug: string): Promise<Post | null>;

An agent working in apps/web sees only these signatures. It never learns GROQ, never imports the Sanity client, never guesses a field name — the typed service layer is the only surface it touches.

Give each agent one layer

Scoping an agent to a layer is mostly a matter of telling it where the walls are, and the walls are already spelled out in CLAUDE.md: components never import @sanity/client, and data access goes through packages/service. So a subagent assigned the post page gets the service contract and the design tokens, and nothing else it could reach past to be unhelpfully clever.

A contract an agent can typecheck is a boundary it can't cross by accident.

The graph has to stay acyclic

Parallelism only works if the layers form a line, not a loop. If apps/web depended on apps/cms and cms reached back into web, two agents could deadlock on the same edit forever. Turborepo keeps the graph honest: typecheck waits on the upstream build, so a cycle stops compiling before it can ship.

turbo.json
1{
2  "tasks": {
3    "typecheck": { "dependsOn": ["^build"] }
4  }
5}

That ^build means service must build before web can typecheck. Wire a cycle and the task runner simply refuses — the same acyclic discipline I argued into the content model shows up again in the build graph.

Why this made a swarm safe

When each agent owns one layer behind a typed contract, a mistake is local and loud. Change the shape of getPostCards and every caller fails to compile at the seam — not silently at runtime three packages away. That's what let me run agents in parallel across the stack without babysitting each one, and it's a big part of why the 24-day retro counts more wins than scars.

Don't manage agents; manage the contracts between their layers. Fence each one into a single typed boundary and the compiler becomes the referee — which is the whole reason the afternoon that started this could scale into a build.

3 min

CLAUDE.md Is the Steering Wheel

One markdown file at the repo root turns scattered AI help into a fleet that stays on-rails. How CLAUDE.md encodes the house rules every agent reads before it touches a line.

building with ai
3 min

This Blog Was a Weekend Idea

How an idea over coffee became a production blog in 24 days and ~1,250 commits, built mostly by AI agents — with a map to every post that tells the story.

building with ai