Files
rose-ash/lib/agentic/tests/trace.sx
giles b92095ccaf agentic-sx Phase 3: trace — console output as attached CID objects (TDD)
Per-agent buffer = persist append-only log stream + kv drain cursor;
commit-with-trace! drains everything-since-last-commit into a console-trace
object and binds it git-note style (ref notes/trace/<commit-cid> -> trace
cid). Trace never enters the commit tree; binding is a re-bindable ref layer
over immutable objects; failed commits keep the buffer; plain commit! leaves
binding to the agent. 35/35 (153/153 total).

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

289 lines
7.2 KiB
Plaintext

; Phase 3 — trace: console output as attached content-addressed objects.
; Fixture story: tracer-1 logs console/tool entries and commits with traces
; (drain-at-commit granularity); quiet-1 stays silent and gets a manual
; genesis trace attached + rebound; a failed commit keeps the buffer; a
; plain commit! deliberately leaves the buffer alone (agent-chosen binding).
(define agt-db (persist/mem-backend))
(define agt-sp (agentic/space agt-db "agentic-trace-test"))
(define agt-repo (agentic/space-repo agt-sp))
(define
agt-a
(agentic/spawn!
agt-sp
"tracer-1"
(agentic/briefing "trace things" "exercise the trace layer" {})))
(define
agt-b
(agentic/spawn!
agt-sp
"quiet-1"
(agentic/briefing "stay quiet" "no console output" {})))
(agentic-test
"fresh agent has an empty buffer"
(= (agentic/trace-pending agt-sp "tracer-1") (list))
true)
(agentic-test
"trace! appends to the buffer"
(agentic/trace! agt-sp "tracer-1" "console" "$ compiling")
true)
(agentic/trace! agt-sp "tracer-1" "tool" "sx_eval (+ 1 2)")
(agentic-test
"pending sees logged entries"
(len (agentic/trace-pending agt-sp "tracer-1"))
2)
(agentic-test
"pending preserves log order"
(get (nth (agentic/trace-pending agt-sp "tracer-1") 0) :text)
"$ compiling")
(agentic-test
"buffers are per agent"
(= (agentic/trace-pending agt-sp "quiet-1") (list))
true)
; ---- commit drains the buffer into an attached trace ----
(define
agt-c1
(agentic/commit-with-trace!
agt-sp
"tracer-1"
"finding"
(assoc {} "notes.md" "found it\n")
{:message "first finding"}))
(agentic-test
"commit-with-trace! commits"
(starts-with? (get agt-c1 :cid) "sx1:")
true)
(agentic-test
"commit-with-trace! attaches a trace"
(starts-with? (get agt-c1 :trace) "sx1:")
true)
(agentic-test
"commit advances the head"
(= (agentic/head agt-sp "tracer-1") (get agt-c1 :cid))
true)
(agentic-test
"trace-for finds the bound trace"
(agentic/console-trace? (agentic/trace-for agt-sp (get agt-c1 :cid)))
true)
(agentic-test
"bound trace carries the entries"
(len (agentic/trace-entries (agentic/trace-for agt-sp (get agt-c1 :cid))))
2)
(agentic-test
"bound trace keeps entry order"
(get
(nth
(agentic/trace-entries (agentic/trace-for agt-sp (get agt-c1 :cid)))
0)
:text)
"$ compiling")
(agentic-test
"trace names its commit by cid"
(get (agentic/trace-for agt-sp (get agt-c1 :cid)) :commit)
(get agt-c1 :cid))
(agentic-test
"trace names its agent"
(get (agentic/trace-for agt-sp (get agt-c1 :cid)) :agent)
"tracer-1")
(agentic-test
"trace is NOT in the commit tree"
(=
(git/tree-names
(git/read
agt-repo
(git/commit-tree (git/read agt-repo (get agt-c1 :cid)))))
(list "notes.md"))
true)
(agentic-test
"buffer drained after commit"
(= (agentic/trace-pending agt-sp "tracer-1") (list))
true)
; ---- granularity = the commit: only entries since the last drain travel ----
(agentic/trace! agt-sp "tracer-1" "console" "$ second round")
(define
agt-c2
(agentic/commit-with-trace!
agt-sp
"tracer-1"
"refactor"
(assoc {} "notes.md" "refined\n")
{:message "second"}))
(agentic-test
"next trace carries only new entries"
(len (agentic/trace-entries (agentic/trace-for agt-sp (get agt-c2 :cid))))
1)
(agentic-test
"next trace text"
(get
(nth
(agentic/trace-entries (agentic/trace-for agt-sp (get agt-c2 :cid)))
0)
:text)
"$ second round")
(agentic-test
"earlier trace is unchanged"
(len (agentic/trace-entries (agentic/trace-for agt-sp (get agt-c1 :cid))))
2)
; ---- a silent commit binds nothing ----
(define
agt-c3
(agentic/commit-with-trace!
agt-sp
"tracer-1"
"decision"
(assoc {} "notes.md" "done\n")
{:message "silent"}))
(agentic-test "silent commit has no trace key" (has-key? agt-c3 :trace) false)
(agentic-test
"silent commit still commits"
(= (agentic/head agt-sp "tracer-1") (get agt-c3 :cid))
true)
(agentic-test
"trace-for nil on a traceless commit"
(agentic/trace-for agt-sp (get agt-c3 :cid))
nil)
; ---- attachment is external to the object layer ----
(agentic-test
"attached commit round-trips to the same cid"
(= (git/cid (git/read agt-repo (get agt-c1 :cid))) (get agt-c1 :cid))
true)
(agentic-test
"trace object is content-addressed"
(=
(get agt-c1 :trace)
(git/cid
(agentic/console-trace
(list
(agentic/trace-entry "console" "$ compiling")
(agentic/trace-entry "tool" "sx_eval (+ 1 2)"))
{:agent "tracer-1" :commit (get agt-c1 :cid)})))
true)
(define
agt-manual
(agentic/attach-trace!
agt-sp
(get agt-b :genesis)
(agentic/console-trace
(list (agentic/trace-entry "console" "spawn log"))
{:commit (get agt-b :genesis)})))
(agentic-test
"manual attach to any commit"
(starts-with? agt-manual "sx1:")
true)
(agentic-test
"manual attachment is found"
(= (agentic/trace-cid-for agt-sp (get agt-b :genesis)) agt-manual)
true)
(agentic-test
"attach validates the object type"
(get
(agentic/attach-trace!
agt-sp
(get agt-b :genesis)
(agentic/briefing "x" "y" {}))
:error)
"not-a-console-trace")
(define
agt-manual2
(agentic/attach-trace!
agt-sp
(get agt-b :genesis)
(agentic/console-trace
(list (agentic/trace-entry "console" "amended log"))
{:commit (get agt-b :genesis)})))
(agentic-test
"re-attach rebinds the note ref"
(= (agentic/trace-cid-for agt-sp (get agt-b :genesis)) agt-manual2)
true)
(agentic-test
"rebinding keeps the old object in the store"
(agentic/console-trace? (git/read agt-repo agt-manual))
true)
; ---- session-wide view ----
(agentic-test
"session-traces pairs commits with traces, newest first"
(=
(agentic/session-traces agt-sp "tracer-1")
(list
(list (get agt-c2 :cid) (get agt-c2 :trace))
(list (get agt-c1 :cid) (get agt-c1 :trace))))
true)
(agentic-test
"session-traces sees manual genesis attachments"
(=
(agentic/session-traces agt-sp "quiet-1")
(list (list (get agt-b :genesis) agt-manual2)))
true)
; ---- failed commits keep the buffer ----
(agentic/trace! agt-sp "tracer-1" "console" "$ doomed")
(define
agt-bad
(agentic/commit-with-trace!
agt-sp
"tracer-1"
"frobnicate"
{}
{}))
(agentic-test
"failed commit passes the error through"
(get agt-bad :error)
"unknown-kind")
(agentic-test
"failed commit keeps the buffer"
(len (agentic/trace-pending agt-sp "tracer-1"))
1)
(define
agt-c4
(agentic/commit-with-trace!
agt-sp
"tracer-1"
"test"
(assoc {} "notes.md" "recovered\n")
{:message "recover"}))
(agentic-test
"kept entries travel with the next commit"
(get
(nth
(agentic/trace-entries (agentic/trace-for agt-sp (get agt-c4 :cid)))
0)
:text)
"$ doomed")
; ---- binding is agent-chosen: plain commit! leaves the buffer alone ----
(agentic/trace! agt-sp "tracer-1" "console" "$ held back")
(define
agt-c5
(agentic/commit!
agt-sp
"tracer-1"
"decision"
(assoc {} "notes.md" "plain\n")
{:message "plain"}))
(agentic-test
"plain commit! binds nothing"
(agentic/trace-for agt-sp agt-c5)
nil)
(agentic-test
"plain commit! leaves the buffer"
(len (agentic/trace-pending agt-sp "tracer-1"))
1)