Files
rose-ash/lib/agentic/branch.sx
giles 88c4963fd0 agentic-sx Phase 2: branch — one branch = one agent (TDD)
space handle (repo + relations Datalog db); spawn! = branch-from-briefing
with a genesis spawn commit at the fork point; commit! verb snapshots a full
worktree VALUE into a typed agent-commit and CAS-advances the branch (no
shared index — multi-agent safe). Topology: fork-point via merge-base,
agents from refs, typed edges sub-agent-of/reviews/merges. Session merges
always record a two-parent session-merge commit (no-ff); conflicts commit
nothing and conclude via merge-resolve!. 53/53 (118/118 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:53:58 +00:00

317 lines
9.9 KiB
Plaintext

; lib/agentic/branch.sx — agentic-sx Phase 2: one branch = one agent.
; spawn = branch-from-briefing: write the briefing, then a genesis "spawn"
; agent-commit whose parent is the fork point; the branch ref IS the agent.
; The commit verb snapshots a full worktree VALUE (path -> data) into a typed
; agent-commit and advances the branch by CAS — no shared index, multi-agent
; safe by construction. Branch topology = agent topology: fork points via
; git/merge-base, plus relations typed edges (sub-agent-of / reviews /
; merges) in a Datalog db carried on the space handle.
; Edge direction: (rel src dst kind) reads src=parent/actor, dst=child/object
; (root child sub-agent-of), (reviewer reviewee reviews), (into from merges).
; Session merges are always recorded as a two-parent "session-merge" commit
; (no fast-forward) so the merge itself is an agent action with metadata.
; Requires: lib/agentic/schema.sx, lib/git/*, lib/relations/* (+ datalog).
; ---- space: repo + relations db ----
(define agentic/space (fn (db name) {:repo (git/repo-named db name) :rels (relations-build-db (list))}))
(define agentic/space-repo (fn (sp) (get sp :repo)))
(define agentic/space-rels (fn (sp) (get sp :rels)))
(define agentic/branch-name (fn (agent) (str "agents/" agent)))
; ---- typed edges over relations ----
(define
agentic/relate!
(fn
(sp src dst kind)
(begin
(dl-assert! (agentic/space-rels sp) (relations-rel src dst kind))
true)))
(define
agentic/sub-agents
(fn
(sp agent)
(sort
(relations-children-of
(agentic/space-rels sp)
agent
(quote sub-agent-of)))))
(define
agentic/parent-agent
(fn
(sp agent)
(let
((ps (relations-parents-of (agentic/space-rels sp) agent (quote sub-agent-of))))
(if (= (len ps) 0) nil (nth ps 0)))))
(define
agentic/agent-tree
(fn
(sp agent)
(sort
(relations-descendants
(agentic/space-rels sp)
agent
(quote sub-agent-of)))))
(define
agentic/reviews!
(fn
(sp reviewer reviewee)
(agentic/relate! sp reviewer reviewee (quote reviews))))
(define
agentic/reviewers
(fn
(sp agent)
(sort
(relations-parents-of (agentic/space-rels sp) agent (quote reviews)))))
(define
agentic/reviewing
(fn
(sp agent)
(sort
(relations-children-of (agentic/space-rels sp) agent (quote reviews)))))
(define
agentic/merged-sessions
(fn
(sp agent)
(sort
(relations-children-of (agentic/space-rels sp) agent (quote merges)))))
(define
agentic/merged-into
(fn
(sp agent)
(sort
(relations-parents-of (agentic/space-rels sp) agent (quote merges)))))
; ---- spawn = branch-from-briefing ----
; base-cid nil => root agent over an empty tree; parent-agent nil => no edge.
; => {:agent :branch :briefing :genesis} | {:conflict true :actual cid}
(define
agentic/spawn-at!
(fn
(sp agent briefing base-cid parent-agent)
(let
((repo (agentic/space-repo sp)))
(let
((bcid (git/write repo briefing))
(tree
(if
(nil? base-cid)
(git/tree-from-files repo {})
(git/commit-tree (git/read repo base-cid))))
(parents (if (nil? base-cid) (list) (list base-cid))))
(let
((g (agentic/agent-commit tree parents "spawn" {:message (str "spawn: " (agentic/briefing-title briefing)) :agent agent :briefing bcid})))
(let
((gcid (git/write repo g)))
(let
((res (git/branch-create! repo (agentic/branch-name agent) gcid)))
(if
(and (dict? res) (has-key? res :conflict))
res
(begin
(if
(nil? parent-agent)
nil
(agentic/relate!
sp
parent-agent
agent
(quote sub-agent-of)))
{:agent agent :briefing bcid :branch (agentic/branch-name agent) :genesis gcid})))))))))
(define
agentic/spawn!
(fn (sp agent briefing) (agentic/spawn-at! sp agent briefing nil nil)))
(define
agentic/spawn-from!
(fn
(sp agent briefing parent-agent)
(let
((h (agentic/head sp parent-agent)))
(if
(nil? h)
{:agent parent-agent :error "no-such-agent"}
(agentic/spawn-at! sp agent briefing h parent-agent)))))
; ---- heads / listing ----
(define
agentic/head
(fn
(sp agent)
(git/branch-get (agentic/space-repo sp) (agentic/branch-name agent))))
(define
agentic/agents
(fn (sp) (git/refs-under (agentic/space-repo sp) "heads/agents/")))
; ---- the commit verb: snapshot + typed agent-commit + CAS advance ----
; files = the agent's FULL worktree value (path -> data). Briefing and agent
; identity propagate from the branch head. => cid | {:error ...} | {:conflict ...}
(define
agentic/commit!
(fn
(sp agent kind files meta)
(let
((repo (agentic/space-repo sp)))
(let
((head (agentic/head sp agent)))
(cond
((nil? head) {:agent agent :error "no-such-agent"})
((not (agentic/commit-kind? kind)) {:error "unknown-kind" :kind kind})
(else
(let
((tree (git/tree-from-files repo files))
(b (agentic/commit-briefing (git/read repo head))))
(let
((pm (if (nil? b) {:agent agent} {:agent agent :briefing b})))
(let
((cid (git/write repo (agentic/agent-commit tree (list head) kind (merge meta pm)))))
(let
((res (git/branch-cas! repo (agentic/branch-name agent) head cid)))
(if (and (dict? res) (has-key? res :conflict)) res cid)))))))))))
; commits authored by this agent, newest first, from its branch head
(define
agentic/session-log
(fn
(sp agent)
(let
((repo (agentic/space-repo sp)))
(let
((head (agentic/head sp agent)))
(if
(nil? head)
(list)
(filter
(fn (cid) (= (agentic/commit-agent (git/read repo cid)) agent))
(git/log repo head)))))))
; the branch's genesis spawn commit (oldest spawn authored by this agent)
(define
agentic/genesis
(fn
(sp agent)
(let
((repo (agentic/space-repo sp)))
(let
((head (agentic/head sp agent)))
(if
(nil? head)
nil
(let
((hits (filter (fn (cid) (let ((c (git/read repo cid))) (and (= (agentic/commit-kind c) "spawn") (= (agentic/commit-agent c) agent)))) (git/log repo head))))
(if (= (len hits) 0) nil (last hits))))))))
(define
agentic/briefing-of
(fn
(sp agent)
(let
((repo (agentic/space-repo sp)))
(let
((g (agentic/genesis sp agent)))
(if
(nil? g)
nil
(let
((bcid (agentic/commit-briefing (git/read repo g))))
(if (nil? bcid) nil (git/read repo bcid))))))))
; ---- topology: fork points via the DAG ----
(define
agentic/fork-point
(fn
(sp agent-a agent-b)
(let
((ha (agentic/head sp agent-a)) (hb (agentic/head sp agent-b)))
(if
(or (nil? ha) (nil? hb))
nil
(git/merge-base (agentic/space-repo sp) ha hb)))))
; ---- session merge: always an explicit two-parent session-merge commit ----
(define
agentic/merge-commit-at!
(fn
(sp into-agent from-agent ours theirs tree meta)
(let
((repo (agentic/space-repo sp)))
(let
((b (agentic/commit-briefing (git/read repo ours))))
(let
((pm (if (nil? b) {:agent into-agent :merged-agent from-agent} {:agent into-agent :briefing b :merged-agent from-agent})))
(let
((cid (git/write repo (agentic/agent-commit tree (list ours theirs) "session-merge" (merge meta pm)))))
(let
((res (git/branch-cas! repo (agentic/branch-name into-agent) ours cid)))
(if
(and (dict? res) (has-key? res :conflict))
res
(begin
(agentic/relate! sp into-agent from-agent (quote merges))
{:cid cid :result "merged"})))))))))
; => {:result "up-to-date"} | {:result "merged" :cid} |
; {:result "conflicts" :tree :conflicts (paths)} | {:error ...}
; Conflicts commit nothing — resolve with agentic/merge-resolve!.
(define
agentic/merge-session!
(fn
(sp into-agent from-agent meta)
(let
((repo (agentic/space-repo sp)))
(let
((ours (agentic/head sp into-agent))
(theirs (agentic/head sp from-agent)))
(cond
((nil? ours) {:agent into-agent :error "no-such-agent"})
((nil? theirs) {:agent from-agent :error "no-such-agent"})
(else
(let
((m (git/merge-commits repo ours theirs)))
(cond
((= (get m :result) "up-to-date") m)
((= (get m :result) "conflicts") m)
(else
(let
((tree (if (= (get m :result) "fast-forward") (git/commit-tree (git/read repo theirs)) (get m :tree))))
(agentic/merge-commit-at!
sp
into-agent
from-agent
ours
theirs
tree
meta)))))))))))
; conclude a conflicted session merge with resolved worktree files
(define
agentic/merge-resolve!
(fn
(sp into-agent from-agent files meta)
(let
((repo (agentic/space-repo sp)))
(let
((ours (agentic/head sp into-agent))
(theirs (agentic/head sp from-agent)))
(if
(or (nil? ours) (nil? theirs))
{:error "no-such-agent"}
(agentic/merge-commit-at!
sp
into-agent
from-agent
ours
theirs
(git/tree-from-files repo files)
meta))))))