Safe shared writes for agent swarms.

agentbranch turns a Postgres database into shared, queryable working memory for a team of agents, with a branch per agent for provisional work. Each agent still sees everyone else's merged work live, and its changes reach the shared tables only when a merge passes conflict detection and your checks.

Example: a swarm of research agents looking for drug targets for heart disease.

A simulated target-discovery swarm, start to finish

illustration, example values, not real results50 agents
  1. 01The question

    A coordinator asks one hard question

    Answering it takes genetics, expression, safety and literature evidence for twelve candidate genes: about 16M tokens of reading, 80 times what one agent can hold in its 200K-token context.

    coordinator

    Which of these 12 genes cause coronary artery disease, and could be safely silenced in the liver?

    coloc_h4
    the gene and the disease share one causal variant
    finemap_pip
    the variant is the likely causal one
    liver_tau
    the gene is expressed mainly in the liver
    pli
    people do not tolerate losing the gene

    Four kinds of evidence, each a probability or score from 0 to 1.

    • PCSK91p32
    • LPA6q25
    • ANGPTL31p31
    • APOC311q23
    • HMGCR5q13
    • NPC1L17p13
    • CETP16q13
    • LDLR19p13
    • APOB2p24
    • SORT11p13
    • ASGR117p13
    • IL6R1q21
  2. 02The swarm

    Fifty specialist agents fan out

    No single agent can read it all, so genetics, expression, safety and literature agents work in parallel. Each takes a few genes and one kind of evidence.

    Why it takes a swarm

    • 200K tokens one agent's context window
    • ≈15M 1,500 papers to read
    • ≈1M analysis tables and notebooks

    The evidence is about 16M tokens, 80 times one window. Split across 50 agents, each still reads about 320K, more than its own window, so it writes what it finds to the database as it goes. Estimate, sized on a Kosmos run.

    coordinator Genetics agents colocalization and fine-mapping writes coloc_h4, finemap_pip 07 12 23 +17 Expression agents single-cell liver atlases writes liver_tau 19 31 38 +7 Safety agents gene constraint and PheWAS writes pli 41 44 02 +7 Literature agents read papers, extract claims writes note 15 27 46 +7 50 agents, each reads ≈320K tokens
  3. 03Branches

    Each agent writes in its own branch

    A branch forks off the shared findings and copies no data. Its rows stay invisible to the other agents until they merge.

    findings(main) 07 PCSK9 · coloc_h4 0.97 19 ANGPTL3 · liver_tau 0.96 41 LPA · pli 0.00 12 PCSK9 · finemap_pip 0.94 private until merged copy-on-write: opening a branch copies no data +46 more branches
  4. 04Verified merge

    Merges land rows in main, with their writer

    Each merge is one transaction: conflict detection, then your checks, then commit. Every row keeps the agent that wrote it.

    findings(main) 07 PCSK9 · coloc_h4 0.97 19 ANGPTL3 · liver_tau 0.96 41 LPA · pli 0.00 no conflicts checks pass one transaction target evidence value _ab_writer LPApli0.00researcher-41 ANGPTL3liver_tau0.96researcher-19 PCSK9coloc_h40.97researcher-07
  5. 05A bad finding

    A hallucinated score is stopped at the merge

    researcher-23 records a probability of 1.40. The range check rejects the merge and rolls it back. The branch stays open, the value is fixed to 0.64, and the retry merges.

    findings(main) 23 SORT1 · coloc_h4 1.40 check failed value NOT BETWEEN 0 AND 1 rolled back, the branch stays open fixed in the branch coloc_h4 0.64 verified merge(checks=["SELECT … WHERE value NOT BETWEEN 0 AND 1"])
  6. 06A conflict

    Two agents vote at once, and no vote is lost

    Both vote PCSK9 up. One merges first and the other's merge conflicts, so it redoes the vote on a fresh branch and merges.

    targets(main) votes 5 12 PCSK9 · votes + 1 votes 6 31 PCSK9 · votes + 1 conflict: row changed first vote redone on a fresh branch 31 + 1 votes 7 two votes, both counted
  7. 07The answer

    One SQL query answers the question, live

    The coordinator joins and filters everything merged so far. Three of the twelve genes pass every test, and each row still names the agents behind it. Sixteen million tokens of reading come back as three rows.

    coordinator: a live query over main
    WITH ev AS (
      SELECT target_id,
             count(DISTINCT _ab_writer) FILTER (WHERE
               evidence = 'coloc_h4' AND value >= 0.8)       AS agents,
             avg(value) FILTER (WHERE evidence = 'coloc_h4')  AS coloc_h4,
             avg(value) FILTER (WHERE evidence = 'liver_tau') AS liver_tau,
             max(value) FILTER (WHERE evidence = 'pli')       AS pli
        FROM findings GROUP BY target_id
    )
    SELECT t.symbol, ev.*
      FROM targets t JOIN ev ON ev.target_id = t.id
     WHERE ev.agents >= 3        -- causal, and replicated by 3+ agents
       AND ev.pli < 0.1          -- people tolerate losing it
       AND ev.liver_tau >= 0.8   -- expressed mainly in the liver
     ORDER BY ev.agents DESC, ev.coloc_h4 DESC;

    How the query narrows twelve genes to three

    1. 12candidate genes

      PCSK9ANGPTL3LPAHMGCRIL6RAPOC3ASGR1NPC1L1CETPLDLRAPOBSORT1
    2. 5causal, replicated by 3+ agentscoloc_h4 ≥ 0.8

      PCSK9ANGPTL3LPAHMGCRIL6RAPOC3ASGR1NPC1L1CETPLDLRAPOBSORT1
    3. 4people tolerate losing itpli < 0.1

      PCSK9ANGPTL3LPAHMGCRIL6RAPOC3ASGR1NPC1L1CETPLDLRAPOBSORT1
      HMGCR drops out: pli 0.99
    4. 3expressed mainly in the liverliver_tau ≥ 0.8

      PCSK9ANGPTL3LPAHMGCRIL6RAPOC3ASGR1NPC1L1CETPLDLRAPOBSORT1
      IL6R drops out: liver_tau 0.34

    Genes that pass every test

    symbolagentscoloc_h4liver_taupliwritten by (_ab_writer)
    PCSK960.970.930.00050709121623
    ANGPTL340.930.960.0007091220
    LPA30.910.980.00051623

    Because a merge keeps each row's original writer, count(DISTINCT _ab_writer) counts the agents that did the work.

In Python

Manage the tables, open a branch, merge with a check.

Given the two ordinary tables from the demo, molecules and findings:

example.py
from agentbranch import AgentBranch

db = AgentBranch("postgresql://agentbranch:agentbranch@localhost:55432/agentbranch")
db.install()
db.manage("molecules")                        # the tables keep their names; their SQL keeps working
db.manage("findings", require_branch=True)    # writes to findings must go through a branch

branch = db.agent("researcher-07").branch(note="solubility sweep")
branch.execute(
    "INSERT INTO findings (molecule_id, property, value, note) VALUES (%s, %s, %s, %s)",
    (2, "solubility", 0.42, "estimate from logP"),
)
branch.query("SELECT * FROM findings WHERE molecule_id = 2")    # sees its own write; no one else does yet
branch.merge(checks=["SELECT id, value FROM findings WHERE value NOT BETWEEN 0 AND 1"])
  1. 1
    Manage the tables. They keep their names, and their SQL keeps working.
  2. 2
    Require a branch. Writes to findings must go through one.
  3. 3
    Write and read in the branch. It sees its own write; no one else does yet.
  4. 4
    Merge with a range check. If anything fails, all of it rolls back and the branch stays open to fix and retry.

Ports in the bundled docker-compose.yml: 55432 is Postgres directly, for install, manage and admin work; 56432 is PgBouncer in transaction mode, for agents.

I. The problem

Let many agents write to one shared store at once, and four things go wrong.

bad rows

A bad row lands in everyone's view

One agent's wrong or hallucinated result is visible to every other agent the moment it is written, and they start building on it.

half-finished work

Half-finished work is visible mid-task

Other agents read the first three rows of a ten-row change before the rest exist.

lost updates

Read-modify-write loses updates

Two agents read a counter, both add one, both write it back, and one increment is gone.

no provenance

No easy record of who wrote what

When a row looks wrong, you cannot easily find the agent, or the rest of what it wrote.

agentbranch

A branch per task, instead of one long transaction

Postgres transactions solve these for short units of work. Agent tasks are rarely short: one task can run for minutes across many tool calls, often through a connection pool, and its output should be checked before anyone builds on it. A branch holds the agent's provisional work for as long as it needs, sees the shared state as it changes, and lands in one transaction at merge, after conflict detection and your checks.

Built for workloads where many agents write shared state: research swarms, data labeling, large coding fleets. With one agent, or agents that only read, a plain database is enough.

II. What a branch is

One agent's private, writable version of the database.

Like a git branch, but for rows. Main is the shared state every agent sees. Inside its branch an agent uses the tables with ordinary SQL, as if it had the database to itself.

mainagentbranch_main.findings

what every agent sees

moleculescore
caffeine0.300.42
aspirin0.77

One row, shared by every agent.researcher-12's verified merge just landed.researcher-07's change landed in one transaction.

researcher-07's branchmain + its overlay

what researcher-07 sees

moleculescorefrom
caffeine0.300.42mainits overlay
aspirin0.77main, live

The branch starts as a catalog row. No data was copied.The new value lives in the overlay, out of main until it merges.It sees the merged row at once. Its own change still wins for caffeine.

branch closed, merged into main
StepWhat main showsWhat researcher-07's branch shows
researcher-07 opens a branchcaffeine 0.30caffeine 0.30
it runs UPDATE findings SET score = 0.42 ...caffeine 0.30caffeine 0.42
researcher-12 merges a new row, aspirin 0.77caffeine 0.30
aspirin 0.77
caffeine 0.42
aspirin 0.77
researcher-07 mergescaffeine 0.42
aspirin 0.77
branch closed
  • Out of main until merged. Its changes stay out of the shared tables until it merges them.
  • Not a snapshot. It still sees everyone else's merged work as soon as it lands.
  • Copies no data. A branch starts as a catalog row. Changed rows are stored on the side (copy-on-write), and every query reads main with them laid on top.
  • Merge or discard. Merging applies the changes to main in one transaction, after checking for conflicts and running your checks. Discarding throws them away.

III. What it does

Live branches, gated merges, plain Postgres.

Live branches

A branch sees main's merged work as it lands, not a snapshot.

Merges gated by checks

Row-level conflict detection is built in. Domain rules are checks: queries you pass to a merge or store with add_check.

Plain Postgres

Existing tables keep their names, and any Postgres client, ORM or BI tool reads them.

Provenance and identity you can query

Every row carries its writer in _ab_writer, and role identity makes that writer a database login. An append-only event log records every write that reaches main and every branch created, merged, discarded, expired or refused.

Branches copy no data

A branch starts as a catalog row, and only the rows it changes are stored on the side. Branches are cheap, not free: each costs a catalog row, overlay rows, some overhead on queries through the view, and cleanup.

Self-hosted

A Python library plus SQL functions installed into your own database. PgBouncer is optional, and a small pool serves hundreds of agents.

IV. How it works

A view over main, an overlay per branch, one verified merge.

manage("findings") moves the table to schema agentbranch_main and puts a view named findings in its place. Each agent reads and writes through that view.

How agentbranch works Agents send ordinary SQL to a view named findings. The view shows main with the current branch's overlay laid over it. Writes inside a branch go to an overlay table in agentbranch_branch; writes outside a branch go to agentbranch_main. branch.merge first calls verify on the diff without locks, then runs one transaction: lock, refuse if the branch changed after verify, find conflicts, apply in foreign-key order, run the stored checks and close the branch, run the checks passed to the merge, then commit into main. Any failure rolls back and leaves the branch open. AGENTS each transaction sets its branch with SET LOCAL researcher-07 own branch, writes freely researcher-12 own branch, writes freely coordinator no branch: queries merged work live ordinary SQL VIEW findings keeps the table's name reads: main, with the current branch's overlay laid over it writes: INSTEAD OF triggers route them to main or to the overlay, and check the row version they read inside a branch outside a branch OVERLAY · agentbranch_branch researcher-07's changes, nothing else key new values or delete marker main version it saw caffeine score = 0.51 3 aspirin delete marker 1 MAIN · agentbranch_main findings: the shared rows molecule score _ab_version _ab_writer caffeine 0.42 3 researcher-07 aspirin 0.77 1 researcher-12 branch.merge() verify(diff) runs first, without locks commit into main MERGE · ONE TRANSACTION any failure rolls back all of it; the branch stays open to fix and retry 1 Lock branch, then tables in a fixed order 2 Re-check refused if changed after verify read it 3 Conflicts version it saw vs main's current one 4 Apply foreign-key order, original writers kept 5 Stored checks then the branch is closed 6 Your checks merge(checks=...), same locks How agentbranch works: agents, the view, the branch overlay, the verified merge, and main AGENTS researcher-07 own branch, writes freely researcher-12 own branch, writes freely coordinator no branch: queries merged work live ordinary SQL, SET LOCAL branch VIEW findings reads: main, with the current branch's overlay laid over it writes: INSTEAD OF triggers route them inside a branch OVERLAY · agentbranch_branch researcher-07's changes only key new values version seen caffeine score = 0.51 3 aspirin delete marker 1 branch.merge() MERGE · ONE TRANSACTION 1 Lock branch, then tables 2 Re-check changed after verify? 3 Conflicts seen vs current version 4 Apply foreign-key order 5 Stored checks then close 6 Your checks same locks any failure rolls back all of it; the branch stays open to fix and retry commit into main MAIN · agentbranch_main findings: the shared rows writes outside a branch land here directly molecule score _ab_version _ab_writer caffeine 0.42 3 researcher-07 aspirin 0.77 1 researcher-12

The branch lives in an overlay

Each branch's changes live in an overlay table in agentbranch_branch: one row per key the branch touched, holding the new values or a delete marker, plus the version of the main row it saw the first time it touched that key.

The current branch is a SET LOCAL

It is a transaction-local setting, so the SDK works unchanged behind a transaction-pooling PgBouncer. A small pool serves hundreds of agents. INSTEAD OF triggers route writes on the view to main or to the overlay.

No silently lost updates

The trigger compares the version a statement read with the row's current one. If they differ, the statement fails with serialization_failure (SQLSTATE 40001) instead of overwriting. The SDK retries single-statement writes, so UPDATE ... SET n = n + 1 from many agents at once loses no increments.

What a merge does, in one transaction

First, if you passed verify, the merge reads the branch and calls verify(diff) without holding any lock, so a slow verifier does not stall other agents. Then one database call, agentbranch_sys.merge, does the rest in one transaction.

A check is a single read-only statement, and every check runs in a block that is always rolled back, so anything a check writes to tables is rolled back.

  1. Locks the branch, then each touched table in a fixed order, so concurrent merges cannot deadlock. If there are checks, it locks every managed table, so two merges cannot each pass a check that their combined result fails.
  2. Refuses the merge if verify ran and the branch changed after it read it, so what is applied is exactly what was verified.
  3. Compares each touched row's recorded version with main's current one. A difference is a conflict.
  4. Applies the branch in foreign-key order, keeping each row's original writer. Deletes from tables no foreign key points at go first, so a branch can delete a row and reuse its unique value. Deferred constraints are checked here too, so a dry run reports them.
  5. Runs the stored checks, then closes the branch.
  6. Runs the checks passed to merge(checks=...), in the same transaction and under the same locks.
  7. Any failure rolls back all of it, and the branch stays open to fix and retry.

V. Quickstart

Clone, start, install, run.

Everything runs locally: Docker for the database, a clone of the repository for the Python package.

1

Start Postgres and PgBouncer

One compose file self-hosts both: Postgres on :55432, PgBouncer in transaction pooling mode on :56432.

shell
$ git clone https://github.com/arun-elr/agentbranch
$ cd agentbranch
$ docker compose up -d   # Postgres on :55432, PgBouncer (transaction pooling) on :56432
2

Install from source

An editable install of the SDK, the CLI and the MCP server, from the clone.

shell
$ pip install -e '.[mcp]'

noteagentbranch is not published on PyPI yet, so pip install agentbranch will not work. Install from the clone as shown.

3

Run the fifty-agent demo

A simulated concurrent-worker run: fifty threads, each acting as one agent, with random numbers in place of real research. Each records three findings in its own branch and merges them with a range check. One worker in eight deliberately writes out-of-range scores, so the check has something to reject. Then all fifty race to vote molecules up, retrying on conflict, while a coordinator joins everyone's merged findings live.

shell
$ python examples/research_swarm.py    # 50 simulated agents, one database
output
50 agents finished in 0.9s

  findings merged                  44
  findings rejected by check       6
  vote retries after conflict      48
  votes merged                     50

  total votes in main             50
  out-of-range findings in main   0
  coordinator snapshots taken     10

Invalid scores are rejected at merge and never reach main, and no vote is lost to a race. A range check is not fact checking; it catches only what you write a check for.

4

Use it from Python

Point AgentBranch at the database, install it once, and manage an existing table. Its SQL keeps working. Each agent then opens a branch, writes, reads its own writes, and merges with checks, as in the example above.

VI. Agent frameworks

One MCP server per agent.

Using it from an agent framework: start one server per agent, each with its own agent name. The default transport is stdio; --transport streamable-http serves it over HTTP on 127.0.0.1:8765.

shell
$ agentbranch --dsn "$AGENTBRANCH_DSN" mcp --agent researcher-07
queryexactly one read-only statement
executeexactly one INSERT, UPDATE or DELETE, in your branch
branch_createopen a branch off main
branch_switchswitch branch, or back to main
branch_diffwhat the branch changed, and would-be conflicts
branch_mergemerge into main with checks
branch_discardthrow the branch away
brancheslist branches
describedescribe the managed tables
  • Schema up front. The managed tables' schema is written into the server instructions and into the query tool's description, so a model can write correct SQL without spending calls on discovery.
  • Branching by default. The first execute opens a branch automatically, so an agent that never thinks about branching still writes into a branch. For a hard rule, manage the table with require_branch=True.
  • Refusals the model can act on. Every tool returns JSON with ok. A refused merge returns the conflicting rows or the violating check results.
  • One statement per call. A small SQL lexer enforces it, so UPDATE ...; DROP TABLE ... is refused.

VII. Who decides when to branch

Three levels, from safest to most flexible.

The first holds whatever the client. The last leaves it to the model, for speculative work.

Level 1, safest

Policy in the database

manage(table, require_branch=True) refuses any write outside a branch, whatever the client.

db.manage("findings",
          require_branch=True)
Level 2

The framework or orchestrator

Open a branch when a task starts and merge it with your checks when the task ends. The model never has to remember.

branch = db.agent(name).branch()
# ... the task runs ...
branch.merge(checks=checks)
Level 3, most flexible

The agent itself

Through branch_create and branch_merge, for speculative work.

branch_create()
# ... speculative writes ...
branch_merge(checks=[...])

VIII. What is tested

Concurrency tested against real Postgres.

170+ tests run against real Postgres 17. Races are timed so they happen every run, and the safety tests were checked by breaking the protection on purpose.

170+tests, run against real Postgres 17
100agents branching, writing and merging at once through a 10-connection pool
1 / 49fifty agents contend for one row: exactly one winner and 49 conflicts
60agents multiplexed through PgBouncer in transaction mode, with no branch ever visible to another agent or to a raw client

Races timed with pg_sleep, so they happen every run

  • a row changing while an update is in flight
  • two increments of one row in one branch
  • two inserts of one key in one branch
  • unmanage racing a branch write
  • two merges whose checks read each other's tables

Also covered: writes racing a merge of the same branch, with none lost.

Checked by breaking the protection on purpose

The safety tests were verified by removing each protection in turn. Removing any one of these makes a test fail:

  • removedthe branch lock
  • removedconflict detection
  • removedSET LOCAL
  • removedforeign-key ordering

IX. Limitations

Read these before relying on it.

A few of them are below. The README has the full list.

In the default mode, agent identity is a label.

Any client can claim any agent name, so owner checks stop accidents, not attacks. Use role identity, where an agent is its database login, when agents are not trusted.

Concurrent writes to one row can fail with 40001

The SDK retries single statements up to 30 times. A retried statement runs again in full, so a volatile function inside it can run more than once. Your own multi-statement transactions must retry on psycopg.errors.SerializationFailure.

Constraints are checked at merge time

The overlay holds no UNIQUE, CHECK or foreign-key constraints, because a delete marker carries only the key. Violations surface when the branch merges, and the merge then rolls back.

No INSERT ... ON CONFLICT on managed tables

Postgres rejects it on a view. Use upsert(), or UPDATE then INSERT.

Not supported yet

Branches of branches, GENERATED ALWAYS identity and generated columns, deferrable primary keys, partitioned tables, and names containing %.

Read every limitation in the README

On the roadmap

  • plannedBranches of branches, for agents that delegate and for exploration trees.
  • plannedSchema changes on managed tables without unmanage.
  • plannedCheckpoints, and an optional snapshot mode for branches that should not see main move.
  • plannedA short note recorded when a branch is discarded, saying why.
  • plannedOptional staleness flags for rows derived from source files, so a row can be marked out of date when its source changes.

Give every agent a branch.

Self-host with one compose file, install from source, and run the fifty-agent demo.