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>
This commit is contained in:
@@ -13,7 +13,7 @@ if [ ! -x "$SX_SERVER" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SUITES=(schema branch)
|
||||
SUITES=(schema branch trace)
|
||||
|
||||
OUT_JSON="lib/agentic/scoreboard.json"
|
||||
OUT_MD="lib/agentic/scoreboard.md"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"suites": {
|
||||
"schema": {"pass": 65, "fail": 0},
|
||||
"branch": {"pass": 53, "fail": 0}
|
||||
"branch": {"pass": 53, "fail": 0},
|
||||
"trace": {"pass": 35, "fail": 0}
|
||||
},
|
||||
"total_pass": 118,
|
||||
"total_pass": 153,
|
||||
"total_fail": 0,
|
||||
"total": 118
|
||||
"total": 153
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ _Generated by `lib/agentic/conformance.sh`_
|
||||
|-------|-----:|-----:|------:|
|
||||
| schema | 65 | 0 | 65 |
|
||||
| branch | 53 | 0 | 53 |
|
||||
| **Total** | **118** | **0** | **118** |
|
||||
| trace | 35 | 0 | 35 |
|
||||
| **Total** | **153** | **0** | **153** |
|
||||
|
||||
289
lib/agentic/tests/trace.sx
Normal file
289
lib/agentic/tests/trace.sx
Normal file
@@ -0,0 +1,289 @@
|
||||
; 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)
|
||||
136
lib/agentic/trace.sx
Normal file
136
lib/agentic/trace.sx
Normal file
@@ -0,0 +1,136 @@
|
||||
; lib/agentic/trace.sx — agentic-sx Phase 3: console traces as ATTACHED
|
||||
; content-addressed objects. An agent's console/tool output accumulates in a
|
||||
; per-agent append-only persist log stream; the commit verb drains everything
|
||||
; since the last commit into a console-trace object and binds it to the new
|
||||
; commit git-note style: ref "notes/trace/<commit-cid>" -> trace cid. The
|
||||
; trace is NOT in the commit's tree — attaching never changes the commit cid,
|
||||
; and the note is a re-bindable ref layer over immutable objects.
|
||||
; Granularity = the commit, agent-chosen: whatever was logged since the last
|
||||
; drain travels with the next commit.
|
||||
; Requires: lib/agentic/branch.sx (and its deps).
|
||||
|
||||
; ---- buffer stream + drain cursor (namespaced under the repo prefix) ----
|
||||
(define
|
||||
agentic/trace-stream
|
||||
(fn
|
||||
(sp agent)
|
||||
(str (get (agentic/space-repo sp) :prefix) "/trace/" agent)))
|
||||
|
||||
(define
|
||||
agentic/trace-cursor-key
|
||||
(fn
|
||||
(sp agent)
|
||||
(str (get (agentic/space-repo sp) :prefix) "/trace-cursor/" agent)))
|
||||
|
||||
; append one console/tool entry to the agent's buffer => true
|
||||
(define
|
||||
agentic/trace!
|
||||
(fn
|
||||
(sp agent kind text)
|
||||
(begin
|
||||
(persist/append
|
||||
(git/repo-db (agentic/space-repo sp))
|
||||
(agentic/trace-stream sp agent)
|
||||
"trace-entry"
|
||||
0
|
||||
(agentic/trace-entry kind text))
|
||||
true)))
|
||||
|
||||
; entries logged since the last drain, oldest first
|
||||
(define
|
||||
agentic/trace-pending
|
||||
(fn
|
||||
(sp agent)
|
||||
(let
|
||||
((db (git/repo-db (agentic/space-repo sp))))
|
||||
(let
|
||||
((cur (persist/kv-get db (agentic/trace-cursor-key sp agent))))
|
||||
(map
|
||||
(fn (e) (persist/event-data e))
|
||||
(persist/read-from
|
||||
db
|
||||
(agentic/trace-stream sp agent)
|
||||
(+ (if (nil? cur) 0 cur) 1)))))))
|
||||
|
||||
; advance the drain cursor to the stream's high-water mark
|
||||
(define
|
||||
agentic/trace-mark!
|
||||
(fn
|
||||
(sp agent)
|
||||
(let
|
||||
((db (git/repo-db (agentic/space-repo sp))))
|
||||
(begin
|
||||
(persist/kv-put
|
||||
db
|
||||
(agentic/trace-cursor-key sp agent)
|
||||
(persist/last-seq db (agentic/trace-stream sp agent)))
|
||||
true))))
|
||||
|
||||
; ---- git-note-style binding: commit cid -> trace cid ----
|
||||
(define
|
||||
agentic/trace-note-ref
|
||||
(fn (commit-cid) (str "notes/trace/" commit-cid)))
|
||||
|
||||
; write the trace object and bind it to the commit => trace cid | {:error}
|
||||
(define
|
||||
agentic/attach-trace!
|
||||
(fn
|
||||
(sp commit-cid trace-obj)
|
||||
(let
|
||||
((repo (agentic/space-repo sp)))
|
||||
(if
|
||||
(not (agentic/console-trace? trace-obj))
|
||||
{:error "not-a-console-trace"}
|
||||
(let
|
||||
((tcid (git/write repo trace-obj)))
|
||||
(begin
|
||||
(git/ref-set! repo (agentic/trace-note-ref commit-cid) tcid)
|
||||
tcid))))))
|
||||
|
||||
(define
|
||||
agentic/trace-cid-for
|
||||
(fn
|
||||
(sp commit-cid)
|
||||
(git/ref-get (agentic/space-repo sp) (agentic/trace-note-ref commit-cid))))
|
||||
|
||||
(define
|
||||
agentic/trace-for
|
||||
(fn
|
||||
(sp commit-cid)
|
||||
(let
|
||||
((tcid (agentic/trace-cid-for sp commit-cid)))
|
||||
(if (nil? tcid) nil (git/read (agentic/space-repo sp) tcid)))))
|
||||
|
||||
; ---- the commit verb with trace binding ----
|
||||
; commit! then drain the buffer into an attached console-trace.
|
||||
; => {:cid cid :trace tcid} | {:cid cid} when nothing was logged
|
||||
; | commit!'s {:error ...}/{:conflict ...} passthrough (buffer kept)
|
||||
(define
|
||||
agentic/commit-with-trace!
|
||||
(fn
|
||||
(sp agent kind files meta)
|
||||
(let
|
||||
((cid (agentic/commit! sp agent kind files meta)))
|
||||
(if
|
||||
(dict? cid)
|
||||
cid
|
||||
(let
|
||||
((entries (agentic/trace-pending sp agent)))
|
||||
(if
|
||||
(= (len entries) 0)
|
||||
{:cid cid}
|
||||
(let
|
||||
((tcid (agentic/attach-trace! sp cid (agentic/console-trace entries {:agent agent :commit cid}))))
|
||||
(begin (agentic/trace-mark! sp agent) {:trace tcid :cid cid}))))))))
|
||||
|
||||
; (commit-cid trace-cid) pairs for the agent's session, newest first,
|
||||
; commits without a bound trace omitted
|
||||
(define
|
||||
agentic/session-traces
|
||||
(fn
|
||||
(sp agent)
|
||||
(filter
|
||||
(fn (p) (not (nil? (nth p 1))))
|
||||
(map
|
||||
(fn (cid) (list cid (agentic/trace-cid-for sp cid)))
|
||||
(agentic/session-log sp agent)))))
|
||||
Reference in New Issue
Block a user