Eval-Driven Loop
Shipping agent quality is a loop, not a one-off test: capture real runs into a
golden dataset, run that dataset against your agent, score the outputs, and
gate merges when scores regress. Chronos builds this loop on top of the
existing evals package (evaluators + LLM-as-judge).
trace/run ──► dataset ──► run vs target ──► score ──► gate (CI)
capture curate DatasetRunner Eval Gate
1. Capture — traces → dataset
Turn a stored session's conversation into a golden dataset (each user turn → an input, the assistant reply → the expected output):
ds, err := evals.CaptureFromSession(ctx, store, sessionID, "capitals")
data, _ := evals.MarshalDataset(ds) // JSON for on-disk storage
Or from the CLI:
chronos evals capture <sessionID> --name capitals --out capitals.json
Capture reads the append-only event ledger and is tenant-scoped, so you only ever capture your own sessions.
2. Run — dataset → scored report
Run the dataset against a target (any func(ctx, input) (string, error) — wrap
agent.Chat, a graph, or a remote agent) and score each output with one or more
evaluators:
runner := &evals.DatasetRunner{
Target: func(ctx context.Context, input string) (string, error) {
resp, err := myAgent.Chat(ctx, input)
if err != nil { return "", err }
return resp.Content, nil
},
Evaluators: []evals.Eval{
&evals.ExactMatchEval{EvalName: "exact"},
&evals.AccuracyEval{EvalName: "judge", Judge: judgeModel}, // LLM-as-judge
},
}
report, _ := runner.Run(ctx, ds)
fmt.Printf("avg_score=%.3f pass_rate=%.3f\n", report.AvgScore, report.PassRate)
Available evaluators: ExactMatchEval, ContainsEval, AccuracyEval
(LLM-as-judge, falls back to word-overlap without a judge), PerformanceEval,
ReliabilityEval.
3. Gate — block regressions
A Gate compares a report against thresholds and, optionally, a baseline
(typically the previous run) to catch regressions:
result := evals.Gate(report, baseline, evals.GateConfig{
MinAvgScore: 0.9,
MinPassRate: 0.9,
MaxRegression: 0.05, // fail if avg score drops >0.05 from baseline
})
if !result.Passed {
log.Fatal(result.String()) // non-zero exit blocks CI
}
In CI
Produce a report from your run, write it as JSON, then gate it:
chronos evals gate report.json --min-score 0.9 --baseline last-report.json --max-regression 0.05
The command exits non-zero when the gate fails, so it blocks the merge. Chronos's own CI has an Eval Gate job that runs the loop end-to-end and asserts a regressed report is rejected.
Trend history
Scores are queryable over time via a ReportStore, scoped to the tenant. Save each
run and use the most recent as the next run's baseline:
history := evals.NewStorageReportStore(store) // or NewMemReportStore()
past, _ := history.History(ctx, ds.Name)
baseline := evals.BaselineFrom(past) // nil on the first run
// … run + gate against baseline …
_ = history.SaveReport(ctx, report)
StorageReportStore records each run as an append-only checkpoint keyed by a
tenant-scoped, per-dataset session id, so runs never overwrite one another and one
tenant never sees another's history. Inspect it from the CLI:
chronos evals history <dataset>
Complete example
See examples/eval_loop/
for a runnable, key-free demonstration that captures a dataset from a seeded
session, runs a good agent (gate passes) and a regressed agent (gate fails).