Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,31 @@ jobs:
working-directory: agents/langgraph-reference
run: pytest -q

schema:
name: schema conformance
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.25"

- name: JSON Schema stays in sync with the Go types
run: go test ./trajectory/ -run "TestSchema" -v

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install the schema validator
working-directory: agents/langgraph-reference
run: pip install -r requirements.txt

- name: Fixtures and emitter output conform to the published JSON Schema
working-directory: agents/langgraph-reference
run: pytest -q tests/test_schema.py

trazo-gate:
name: trazo gate demo
runs-on: ubuntu-latest
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Release

# Cut a release by pushing a semver tag, e.g. `git tag v0.2.0 && git push --tags`.
# The tag must match the compiled-in const Version in cmd/trazo/version.go, or the
# guard below fails the build before anything is published.
on:
push:
tags: ["v*"]

permissions:
contents: write # required to create the GitHub release and upload artifacts

jobs:
release:
name: build and publish binaries
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # tags and full history so build info is stamped correctly

- uses: actions/setup-go@v5
with:
go-version: "1.25"

- name: Tag matches the compiled-in version
run: |
tag="${GITHUB_REF_NAME#v}"
code="$(sed -n 's/.*const Version = "\([^"]*\)".*/\1/p' cmd/trazo/version.go)"
echo "tag=$tag code=$code"
if [ "$tag" != "$code" ]; then
echo "::error::tag v$tag does not match const Version $code in cmd/trazo/version.go"
exit 1
fi

- name: Build, vet, test before releasing
run: |
go vet ./...
go test ./...

- name: Cross-compile release archives
run: make dist VERSION="${GITHUB_REF_NAME}"

- name: Create the GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${GITHUB_REF_NAME}" \
--title "${GITHUB_REF_NAME}" \
--generate-notes \
dist/*
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
# Test binary, built with `go test -c`
*.test

# trazo build artifacts (Makefile `build`/`install` and ad-hoc `go build -o trazo`)
/bin/
/trazo

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

Expand Down
93 changes: 93 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Changelog

All notable changes to trazo are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project aims
to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for the
product version. The trace **schema** version is tracked separately; see
[docs/versioning.md](docs/versioning.md).

## [Unreleased]

### Added

- `trazo version` subcommand and `-version` flag, reporting the product version,
the supported trace schema version, the git commit, and the build time. Commit
and build metadata come from the Go toolchain's build info (no ldflags needed).
- [docs/versioning.md](docs/versioning.md): the product and schema versioning
policy, the schema compatibility contract, and the release process.
- Versioned, self-describing JSON output: `-format json` now emits an envelope
with `outputVersion`, `trazoVersion`, `traceSchemaVersion`, `generatedAt`, an
aggregate `summary`, `results`, and structured `errors`. Documented in
[docs/output.md](docs/output.md).
- Evaluator policy file: `-config <path>` loads a versioned, stdlib-only JSON
policy (the new `config` package) that pins which evaluators run and their
thresholds, for reproducibility across dev, CI, and teams. Precedence is
defaults < config < explicitly-set flags. Documented in
[docs/config.md](docs/config.md), with an example config.
- Black-box CLI tests (`cmd/trazo/cli_test.go`) that build the binary and assert
exit codes (0/1/2), each output format, invalid flags, missing paths, empty
directories, and config/flag precedence.
- A dedicated `schema conformance` CI job that runs the Go schema-sync tests and
validates fixtures and emitter output against the published JSON Schema, plus a
test that the shipped example config always loads.
- `-verbose` flag: prints operational metrics to stderr after a run
(`loaded=N valid=V invalid=I evaluated=E duration=Xms`), so batch runs are
observable without parsing the results.
- Typed file errors: `runner.FileError` now carries an `ErrorKind`
(`read_file`, `invalid_json`, `invalid_trace`, `evaluator`, `canceled`),
surfaced as `errors[].kind` in the JSON output (bumped to `outputVersion` 1.1,
additive) and tagged in the text output.
- A realistic integration fixture (`testdata/complex_run.json`) and test that
runs the full structural evaluator set over one interleaved trace, asserting
how the work divides across judgments; it is also covered by the Python schema
conformance suite.
- [docs/llm-judge.md](docs/llm-judge.md): the opt-in LLM-as-judge evaluator, its
determinism (temperature 0, capped tokens), timeout and cancellation model, and
the split between a malformed verdict (neutral finding) and a transport failure
(a run error; structural evaluators still run).
- [docs/security.md](docs/security.md): what a trace holds, the single component
that sends data off-machine (the judge, final output only), how to disable
network egress, redaction at emit time, and not committing private traces.
- Packaging and release: `LICENSE` (MIT), a `Makefile` (build, check, gate,
install, cross-compiled `dist` archives with checksums), a tag-triggered GitHub
Actions release workflow that guards the tag against the compiled-in version and
publishes binaries for Linux, macOS, and Windows (amd64/arm64), and a README
with badges, `go install` instructions, and links to the core docs.
- This changelog.

### Changed

- BREAKING (JSON output): the top-level keys `evaluations` and `fileErrors` are
now `results` and `errors`, nested under the new envelope. Consumers should
read `outputVersion` and ignore unknown fields.

## [0.1.0]

The hardening round: formalize the trace contract and round the core out from a
working MVP toward production-grade.

### Added

- Formal JSON Schema (`trajectory/trace.schema.json`, draft 2020-12) as the
strict external contract, kept in sync with the Go types by a dependency-free
test and validated against fixtures and emitter output from Python.
- Trace schema versioning: `version` is required and semver-gated by major (see
docs/versioning.md).
- Explicit `toolCallId` for authoritative tool call/result pairing, with
name/FIFO as a documented fallback.
- CLI: single-file input, `-recursive`, `-validate` (structure-only), up-front
`-format` validation, and a proper `-help`.
- Golden tests for the text, JSON, and Markdown output formats.

### Changed

- `Evaluator` interface takes a `context.Context`, so Ctrl+C or a CI timeout
aborts in-flight work (notably the LLM judge).
- `Run.Validate` deepened: non-negative quantities, monotonic step timestamps,
steps within the run interval, per-type required fields.
- Text output regrouped by run with a summary footer.
- Output formatting centralized in the `report` package; `Evaluator.go` renamed
to `evaluator.go`.

[Unreleased]: https://github.com/Cro22/trazo/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/Cro22/trazo/releases/tag/v0.1.0
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Jesús Núñez

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
78 changes: 78 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# trazo build and release tasks.
#
# The Go core is standard-library only, so these targets need nothing but the Go
# toolchain (plus tar/zip/sha256sum for `dist`, which are present on CI runners
# and most Unix hosts). On Windows, run under Git Bash or WSL.

BIN := trazo
CMD := ./cmd/trazo
BINDIR := bin
DISTDIR := dist

# Version stamped into archive names. The binary itself reports the compiled-in
# const Version plus git metadata from the Go toolchain (see cmd/trazo/version.go);
# this is only for naming the release artifacts.
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)

# Release target matrix (GOOS/GOARCH).
PLATFORMS := linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64

.DEFAULT_GOAL := build
.PHONY: build check test vet fmt install gate agent-test dist clean

build: ## Build the CLI into ./bin
go build -trimpath -o $(BINDIR)/$(BIN) $(CMD)

check: vet test ## Full Go gate: vet then test
go build ./...

test: ## Run the Go test suite
go test ./...

vet: ## Run go vet
go vet ./...

fmt: ## Format all Go sources
go fmt ./...

install: ## Install the CLI into GOBIN (go install)
go install $(CMD)

# Mirror the CI trazo-gate job locally: clean traces pass, bad traces must fail.
gate: build ## Demonstrate the evaluator gate on the CI fixtures
$(BINDIR)/$(BIN) -dir testdata/ci/clean
@echo "clean traces passed (exit 0)"
@if $(BINDIR)/$(BIN) -dir testdata/ci/failing; then \
echo "gate did not fail on a bad finding" >&2; exit 1; \
fi
@echo "bad traces correctly failed the gate (exit non-zero)"

# Run the Python reference-agent tests. Requires the venv from
# agents/langgraph-reference (see the README quickstart).
agent-test: ## Run the reference-agent pytest suite
cd agents/langgraph-reference && pytest -q

# Cross-compile release archives plus a checksums file into ./dist. Each archive
# holds one static binary; unix targets are .tar.gz, windows is .zip.
dist: ## Build release archives for every target platform
rm -rf $(DISTDIR)
mkdir -p $(DISTDIR)
@for p in $(PLATFORMS); do \
os=$${p%/*}; arch=$${p#*/}; \
ext=; [ $$os = windows ] && ext=.exe; \
echo "building $$os/$$arch"; \
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch \
go build -trimpath -o $(DISTDIR)/$(BIN)$$ext $(CMD) || exit 1; \
base=$(BIN)_$(VERSION)_$$os_$$arch; \
if [ $$os = windows ]; then \
(cd $(DISTDIR) && zip -q $$base.zip $(BIN)$$ext && rm $(BIN)$$ext); \
else \
(cd $(DISTDIR) && tar czf $$base.tar.gz $(BIN)$$ext && rm $(BIN)$$ext); \
fi; \
done
@(cd $(DISTDIR) && sha256sum * > checksums.txt)
@echo "artifacts in $(DISTDIR):"
@ls -1 $(DISTDIR)

clean: ## Remove build and release artifacts
rm -rf $(BINDIR) $(DISTDIR) $(BIN) $(BIN).exe
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
# trazo

[![CI](https://github.com/Cro22/trazo/actions/workflows/ci.yml/badge.svg)](https://github.com/Cro22/trazo/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/Cro22/trazo?sort=semver)](https://github.com/Cro22/trazo/releases)
[![Go version](https://img.shields.io/github/go-mod/go-version/Cro22/trazo)](go.mod)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Trajectory evaluation for LLM agents. trazo ingests agent run traces as JSON and
runs evaluators over them, producing findings with a severity taxonomy. The core
is written in Go; a Python LangGraph reference agent proves the framework end to
end: real agent produces real traces, and the unmodified Go core evaluates them.

## Install

The CLI is a single self-contained binary with no runtime dependencies.

```bash
# With the Go toolchain (installs into $GOBIN):
go install github.com/Cro22/trazo/cmd/trazo@latest

# Or from source:
git clone https://github.com/Cro22/trazo && cd trazo
make build # -> ./bin/trazo (or: go build -o trazo ./cmd/trazo)
```

Prebuilt binaries for Linux, macOS, and Windows (amd64 and arm64) are attached to
each [GitHub release](https://github.com/Cro22/trazo/releases). Verify a download
against the release's `checksums.txt`, then put the binary on your `PATH`.

`trazo version` reports the product version, the supported trace schema version,
and the build's git commit and time.

## The severity taxonomy

Every finding carries one judgment (`evaluator/Evaluator.go`):
Expand Down Expand Up @@ -62,6 +87,18 @@ directory with a bad finding (`testdata/ci/failing`) makes trazo exit non-zero.
The Markdown report is written to the job summary. This is how trazo fails a
build on a `JudgmentBad`.

## Core documentation

- [docs/config.md](docs/config.md) — the `-config` evaluator policy file and
the defaults < config < flags precedence.
- [docs/output.md](docs/output.md) — the versioned JSON output envelope.
- [docs/versioning.md](docs/versioning.md) — product versus trace-schema
versioning and the release process.
- [docs/llm-judge.md](docs/llm-judge.md) — the opt-in LLM-as-judge evaluator:
determinism, timeouts, cost, and failure handling.
- [docs/security.md](docs/security.md) — what a trace holds, the one component
that sends data off-machine, and how to keep private traces private.

## Reference agent (LangGraph)

`agents/langgraph-reference/` is a GitHub-issue triage agent built with
Expand Down Expand Up @@ -148,3 +185,7 @@ Model tier is a cheap flash model (`gemini-2.5-flash` by default) with
temperature 0 and a low output-token cap. A full triage of one repo is about
2,400 tokens and costs roughly **$0.002** per run. Override the model with
`--model` and bound the loop with `--iteration-cap`.

## License

MIT. See [LICENSE](LICENSE).
3 changes: 3 additions & 0 deletions agents/langgraph-reference/docs/trace-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ stamps it on every trace by default. Keep the two constants in sync; a Go test
(`trajectory/schema_test.go`) checks the JSON Schema requires `version` and that
`SchemaVersion` passes the gate.

The full compatibility contract, the product-vs-schema version distinction, and
the release process live in [`docs/versioning.md`](../../../docs/versioning.md).

## Tool call / result pairing (what the emitter must respect)

`ToolCallEvaluator` (`evaluator/toolcalls.go`) walks the steps in order and pairs
Expand Down
4 changes: 2 additions & 2 deletions agents/langgraph-reference/tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_agent_trace_evaluates_clean_in_go(tmp_path) -> None:
)
assert proc.stdout, proc.stderr
out = json.loads(proc.stdout)
assert out["fileErrors"] == []
assert out["errors"] == []
# Clean means every evaluator (tool_calls, loops, cost_latency, node) is clean.
findings = [f for e in out["evaluations"] if e["runId"] == result.run_id for f in e["findings"]]
findings = [f for e in out["results"] if e["runId"] == result.run_id for f in e["findings"]]
assert findings == [], findings
6 changes: 3 additions & 3 deletions agents/langgraph-reference/tests/test_cross_language.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ def _run_trazo(traces_dir: Path) -> dict:


def _findings_for(result: dict, run_id: str) -> list[dict]:
if not any(ev["runId"] == run_id for ev in result["evaluations"]):
if not any(ev["runId"] == run_id for ev in result["results"]):
raise AssertionError(f"run {run_id} not found in {result}")
# Aggregate findings across every evaluator for this run.
return [f for ev in result["evaluations"] if ev["runId"] == run_id for f in ev["findings"]]
return [f for ev in result["results"] if ev["runId"] == run_id for f in ev["findings"]]


def test_clean_run_evaluates_with_no_findings(tmp_path) -> None:
Expand All @@ -55,7 +55,7 @@ def test_clean_run_evaluates_with_no_findings(tmp_path) -> None:
rec.flush(tmp_path, end_time=_dt(4))

result = _run_trazo(tmp_path)
assert result["fileErrors"] == []
assert result["errors"] == []
assert _findings_for(result, "run-clean") == []


Expand Down
Loading
Loading