Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 concurrentlyResult
Edits of different definitions, a rename and a body edit, identical editsMerged
refine, fill or attach against a concurrent editReplayed on the new version
Merged program doesn’t typecheckE_MERGE, with the definitions that changed and a rebase to retry
Two bodies for one definition, an edit of a removed definitionE_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.