A multi-agent codebase
An SSPUR codebase is a store of typechecked definitions, not a folder of text files. Many agents can edit one store at once, and stores can be replicated and synced like git repositories, except that merges are by definition and the merged result must typecheck before it lands. This tutorial has two agents, Alice and Bob, each with a replica of a small shop.
Two replicas
Alice imports the code into a store. Bob makes an empty store and pulls from hers. A remote is a directory or tcp://host:port, served by sspur sync serve:
type Item = {name: Str, price: Int where _ >= 0, qty: Int where _ > 0}
fn total(items: List[Item]) -> Int
= items.map(_.price * _.qty).sum
fn main() -> Unit ! log
= do
cart = [Item{name: "pen", price: 3, qty: 4}, Item{name: "book", price: 12, qty: 1}]
log("total: {total(cart)}")
test empty = total([]) == 0
$ cd alice
$ sspur init shop.ssp
ok 4 definitions
$ cd ../bob
$ sspur init
...
$ sspur sync pull ../alice
ok pulled 1 commits, HEAD #na5tm3mr3ex5 [Item empty main total]
Concurrent edits
Each agent writes its change to a file and applies it with sspur edit --test, which typechecks the whole codebase, runs every test, and applies everything or nothing. Bob adds a bulk discount:
fn total(items: List[Item]) -> Int
= do
s = items.map(_.price * _.qty).sum
if s >= 50 then s * 9 / 10 else s
test bulk = total([Item{name: "box", price: 25, qty: 2}]) == 45
Meanwhile Alice adds shipping, to the same function:
fn total(items: List[Item]) -> Int
= if items.is_empty then 0 else items.map(_.price * _.qty).sum + 5
test shipping = total([Item{name: "pen", price: 3, qty: 1}]) == 8
$ sspur edit --test bulk.ssp
ok +bulk ~total
2 passed, 0 failed
$ cd ../alice
$ sspur edit --test shipping.ssp
ok +shipping ~total
2 passed, 0 failed
A conflict
Changes to different definitions merge on their own, and so do a rename and a body edit of one definition, or identical edits. Two different bodies for total can’t, so Alice’s pull stops and shows both sides. Her HEAD doesn’t move:
$ sspur sync pull ../bob
pulled 1 commits not merged, HEAD unchanged
E_CONFLICT 'total' was changed on both sides
hint: resolve with ops {"op": "resolve", "path": P, "pick": "ours"|"theirs"} or 'sspur sync resolve ours|theirs'; fix type errors with a transaction that has "merge": true
...
ours:
fn total(items: List[Item]) -> Int
= if items.is_empty then 0 else items.map(_.price * _.qty).sum + 5
theirs:
fn total(items: List[Item]) -> Int
= do
s = items.map(_.price * _.qty).sum
if s >= 50 then s * 9 / 10 else s
She takes Bob’s version, then writes one total that does both, with both tests, and pushes the result:
fn total(items: List[Item]) -> Int
= do
s = items.map(_.price * _.qty).sum
discounted = if s >= 50 then s * 9 / 10 else s
if items.is_empty then 0 else discounted + 5
test bulk = total([Item{name: "box", price: 25, qty: 2}]) == 50
test shipping = total([Item{name: "pen", price: 3, qty: 1}]) == 8
$ sspur sync resolve theirs
ok merge, HEAD #uf27ruzq5s5n [bulk empty main shipping total]
$ sspur edit --test both.ssp
ok ~bulk ~shipping ~total
3 passed, 0 failed
$ sspur sync push ../bob
ok pushed 3 commits, HEAD #bocb4esnxu4z [bulk empty main shipping total]
Bob now has the merged code, and both replicas have the same root hash:
$ cd ../bob
$ sspur test
3 passed, 0 failed
$ sspur sync status
HEAD bocb4esnxu4z6tx72s6ohxaitbntmoy3zjruy3tace2lybassdga
...
Many agents on one store
Inside one store the same rules apply without any syncing. Every edit is a transaction against the HEAD it started from, merged into whatever HEAD is when it commits:
| What happened concurrently | Result |
|---|---|
| Edits of different definitions, a rename and a body edit, identical edits | Merged |
refine, fill or attach against a concurrent edit | Replayed on the new version |
| Merged program doesn’t typecheck | E_MERGE, with the definitions that changed and a rebase to retry |
| Two bodies for one definition, an edit of a removed definition | E_CONFLICT, with their source, agent and time |
A crash at any point leaves the old state or the new one. Check results are cached per definition and shared by every codebase on the machine, so an agent’s edit --test only rechecks what changed. ADR 0021 measured 100 agents editing one store concurrently with no lost work. SSPUR for AI agents has the workflow each agent should follow.