Agent session trace libraries extracted from cosmtrek/mindwalk. Classify tool actions, tail live session logs, and emit OpenTelemetry spans. Used by Harness for idle detection and trajectory export to Cairn.
This module covers mindwalk's trace parsing, classification, and event/mark emission only. Not extracted: stats computation (internal/model/stats.go), the agent-graph builder (internal/model/agent.go), judge integration (internal/judge/), and the 3D citymap renderer.
Pure classification of agent tool calls into semantic actions (search, read, edit, exec, verify) and file targets. No I/O — the Options struct lets callers inject a FileExists func and home/tmp dirs for weak-target filtering and outside-scope detection, plus VerifyPatterns to extend the built-in verify command list (just test, bun test, …). Pass nil Options to keep all weak targets and the default verify patterns.
import "github.com/stump-wtf/agent-trace/classify"
event := classify.BuildEvent(seq, cwd, call, result)
// event.Action == "edit", event.Targets == [{Path: "foo.go", Touch: "edit"}]For I/O-aware classification:
opts := &classify.Options{
FileExists: func(cwd, rel string) bool { _, err := os.Stat(filepath.Join(cwd, rel)); return err == nil },
HomeDir: home,
TmpDir: os.TempDir(),
}
event := classify.BuildEventWith(opts, seq, cwd, call, result)Live session log discovery and per-agent JSONL parsing. Watches agent session directories, tails growing files, and emits classified Events. Supports Claude Code, Codex, Crush, OpenCode, and Pi via the Adapter interface — DefaultAdapters() returns all five.
import "github.com/stump-wtf/agent-trace/tail"
watcher := tail.NewWatcherWithConfig(tail.DefaultWatchConfig(), tail.DefaultAdapters())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go watcher.Start(ctx)
for ev := range watcher.Events() {
fmt.Printf("%s: %s\n", ev.Session.Title, ev.Classified.Summary)
}Idle detection uses the session's own event timestamps, not scan time:
if watcher.IsIdle(sessionKey) {
// session has been quiet longer than IdleAfter
}Each adapter has a Dir field for testing with temp directories. The Codex adapter also has IndexPath for title resolution from session_index.jsonl.
Converts classified events and marks into OpenTelemetry span structs. Maps user messages to parent spans, tool calls to child spans, errors to status codes. Deterministic trace/span IDs enable idempotent re-submission.
import "github.com/stump-wtf/agent-trace/otel"
trace := otel.BuildTrace(session, events, marks)
// trace.Spans[0].Name, .StartTime, .EndTime, .ParentSpanID
err := otel.WriteJSON(os.Stdout, trace)Span.Attributes is map[string]any so numeric attributes stay numeric — agent.result.bytes and agent.outside_count serialize as JSON numbers, not quoted strings. Values must be one of the types OTel permits: string, bool, int64, float64, or a slice of those.
WriteJSON emits this package's own {traceId, session, spans} shape, not the OTLP/HTTP wire format — a collector-bound exporter has to translate it first.
No time.Now() — missing timestamps fall back to the nearest event, then SessionMeta.StartedAt, then zero. Building the same trace twice produces identical timings.
tail (parse JSONL) → classify (ToolCall+ToolResult → Event) → otel (Event+Mark → Span tree)
One-way data flow. classify is the pure core. tail does I/O and passes Options to classify. otel is a stateless transformation.
make test # go test ./...
make lint # gofmt + go vet
make check # lint + testExtracted from cosmtrek/mindwalk internal/adapter and internal/model packages. MIT license, preserving upstream's copyright.
MIT — see LICENSE.