Let multiple AI agents edit the same codebase at the same time without stepping on each other. This is an experiment and exploration of how you can agent edits can be backed by CRDTs.
It's almost certainly broken in many ways. There are probably a million edge cases that are not handled correctly. That's okay, though, because we are just having fun.
Built on Loro CRDT and the Vercel AI SDK.
If you're running multiple agents on the same codebase, they're going to step on each other. There are ways around this:
- run a single a single agent all the time (acceptable, how the world works today)
- run many agents, let them do the classic:
- attempt to write a file
- oops, modified since last write, read it
- read file
- write file (hope there are no conflicts between the last read and write, otherwise start over)
- lock files, let agents write after locks clear
- something else (this codebase explores a "something else")
- Conflict-free merging — Agents write concurrently. The CRDT handles it. No one overwrites anyone.
- Intent coordination — Before an agent edits a region, it declares what it's about to do. Other agents can see that and avoid stepping on the same code.
- Region-scoped edits — Agents target named declarations (a function, a class, a type alias), not entire files. Keeps changes surgical.
- Structural awareness — Tree-sitter parses your code, so agents operate on real AST boundaries.
- Auto disk sync — Every CRDT write flushes back to disk.
CRDTs guarantee that concurrent edits merge deterministically. Every peer converges to the same result regardless of operation order. What makes Loro especially useful here is stable cursors.
When an agent reads a region, we need to remember where it read so we can detect conflicts later. Character offsets are fragile. If another agent inserts 50 characters above your region, your offsets are wrong. Loro's stable cursors are bound to a position in the CRDT's history, and it moves as other peers insert and delete around it. When agent A reads calculateTotal at offsets [200, 350] and agent B inserts code above it, agent A's cursors automatically resolve to [250, 400] at conflict-check time.
This is what makes read-set conflict detection reliable. We record stable cursors when an agent reads, and resolve them to current positions when the agent tries to write. If another agent's operations overlap the cursor-resolved region, we flag the conflict. Without stable cursors, we'd be comparing stale offsets against a document that has shifted underneath us in unknown ways.
CRDTs solve the merging problem for tree-structured data. Two agents editing different branches of a tree can merge cleanly. But code is more like a graph.
A function calls other functions. A type is imported across files. A component renders children that depend on props defined three layers up. When one agent changes a function signature and another agent is calling that function, a clean CRDT merge doesn't mean the result compiles. The merge is structurally valid but semantically broken.
So this library builds two things on top of the CRDT, and thinks a third could help:
-
Structural indexing — Tree-sitter parses the code into named regions (functions, classes, types). Agents operate on these boundaries instead of raw character offsets. When the CRDT shifts text around, region boundaries shift with it.
-
Intent coordination — Before writing, an agent declares what it plans to edit. The system checks for overlapping intents across agents and flags conflicts before anyone writes.
-
Cross-file relationship tracking (not implemented, just an idea) — When an agent changes an export in one file, other agents working on files that import from it get notified. The system tracks these dependency edges so agents can re-read and adapt, rather than writing against stale signatures.
There are demos that you can run (bun dev), that use React Vite projects the agents can do real edits against. You'll see everything you need in the UI (default http://localhost:4000), including a chat interface, files, and an iframe that shows the Vite app.
If it breaks, kill the process and re-run it. Like this experiment, its also jank.
import { createSession } from "multi-agent-crdt";
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
const session = await createSession({ filePath: "./src/utils.ts" });
const { tools, systemPrompt, stopCondition } = await session.registerAgent({
id: "agent-1",
peerId: "1",
task: "Add input validation to processOrder",
});
const result = streamText({
model: openai("gpt-5.4"),
system: systemPrompt,
messages: [{ role: "user", content: "Implement the task." }],
tools,
stopWhen: stopCondition,
});
await result.consumeStream();const session = await createSession({
rootDir: "./src",
include: ["**/*.ts", "**/*.tsx"],
});
// Each agent gets a primary file but can read/write across the whole project
const agent1 = await session.registerAgent({
id: "types-agent",
peerId: "1",
task: "Add a discount field to OrderItem",
primaryFile: "types.ts",
});
const agent2 = await session.registerAgent({
id: "utils-agent",
peerId: "2",
task: "Update calculateTotal to apply discounts",
primaryFile: "utils.ts",
});
// Run them both at the same time. That's the whole point.
await Promise.all([runWithStreamText(agent1), runWithStreamText(agent2)]);This gives you back tools, a system prompt, and a stop condition. You call streamText yourself with whatever model, temperature, max tokens—whatever you want. Full control.
const { tools, systemPrompt, stopCondition } = await session.registerAgent({
id: "agent-1",
peerId: "1",
task: "Refactor error handling",
});
const result = streamText({
model: yourModel,
system: systemPrompt,
messages: [{ role: "user", content: "Implement the task." }],
tools,
stopWhen: stopCondition,
});If you don't need fine-grained control, this handles the streamText loop internally. You get callbacks for observability.
const agent = await session.createAgent({
id: "agent-1",
peerId: "1",
task: "Add logging to all public methods",
languageModel: openai("gpt-5.4"),
callbacks: {
onStep: (step) => console.log(`Step ${step.stepIndex}`, step.toolCalls),
onAgentComplete: (id, content) => console.log(`${id} done`),
},
});
const result = await agent.execute();The workflow is declare → read → write → finalize:
declare_intent— Agent says "I'm going to editcalculateTotal." It gets back the current content of that region and any conflicts—maybe another agent already declared intent on the same region.read_file— Read a specific region or the full file. Always gets the latest CRDT state.write_file/edit_region— Replace a full region, or edit specific lines within one.finalize— Agent signals it's done. Clean exit.
When two agents target the same region, the system flags the overlap and tells both of them. They can either wait for the other to finish or proceed with complementary changes. The CRDT merges either way, but the intent system makes it much more likely the result is coherent.
There is more here, look at demo/ for examples of code and what an orchestrator looks like.
After agents finish, check the result:
const result = await session.validate();
// result.passed — true if no syntax errors
// result.errors — array of error messages
// result.perFile — per-file results (multi-file mode)
// result.typeDiagnostics — type errors from tsgo (if LSP enabled)The DocumentManager + AgentHandle API gives you the same coordination primitives with zero LLM framework dependency:
import { createDocumentManager } from "multi-agent-crdt";
const manager = await createDocumentManager({ rootDir: "./src" });
const handle = manager.addAgentForFile({
id: "agent-1",
peerId: "1",
primaryFile: "utils.ts",
});
// Structured results, no string formatting
const intent = await handle.declareIntent({
region: "calculateTotal",
task: "Optimize performance",
});
// intent.regionContent, intent.overlaps, intent.collisions
const read = await handle.read({ region: "calculateTotal" });
// read.content, read.location, read.isFullFile
await handle.writeRegion({
region: "calculateTotal",
content: newImplementation,
});- Bun runtime (uses
Bun.file,Bun.write,Bun.Glob) - AI SDK v6+ (peer dependency)