Tracking & Results¶
This page covers the tools available inside a scenario for recording what happened during a run, returning results, and understanding what gets written to disk.
Concepts¶
Three distinct concerns map to three distinct APIs:
| Concern | API | Written to |
|---|---|---|
| What happened during the run | observations module |
run.manifest.json, report.md |
| What the run concluded | RunResult / RunResultBuilder |
run.manifest.json, results.yaml |
| Numeric time-series metrics | MetricTracker / MeasurementEngine |
metrics/*.csv, summarized in manifest |
The manifest (run.manifest.json) is the aggregate document that brings all three together. The markdown report (report.md) is generated from the manifest automatically at run end.
Observations¶
Use ObservationWriter to record observations. It namespaces every entry with a component name and merges in default tags, making run reports much easier to read when multiple subsystems are active.
from adgtk.tracking import ObservationWriter
obs = ObservationWriter("extractor", tags=["extraction"])
The writer delegates to the module-level observation store, which is reset automatically by the runner at the start of each run — you do not need to manage lifecycle yourself.
obs.note — general findings¶
Record a researcher observation. Use this for anything you want to remember: a finding, a hypothesis, a comparison.
obs.note("Prompt variant A consistently outperformed B on coherence tasks")
obs.note("Output length spiked on items with ambiguous entity types", tags=["edge-case"])
Parameters:
message: str— the observation texttags: list[str]— extra labels for this observation (merged with the writer's default tags)
Every observation also receives the writer's component:<name> tag automatically.
obs.warn — anomalies and surprises¶
Use this when something unexpected happened that you want to flag but didn't stop the run.
obs.warn("Agent looped 7 times before producing a final answer")
obs.warn("Response contained refusal text despite benign prompt", tags=["refusal", "investigate"])
obs.agent_turn — prompt/response pairs¶
Record a single exchange with an agent. This is the most important observation type for agent research — it gives you a full trace of what the agent saw and said, alongside cost and latency data.
response = client.chat(prompt)
obs.agent_turn(
prompt=prompt,
response=response.text,
model="claude-sonnet-4-6",
tokens_in=response.usage.prompt_tokens,
tokens_out=response.usage.completion_tokens,
latency_ms=response.elapsed_ms,
tags=["extraction-step"],
)
Parameters:
prompt: str— the full prompt sent to the agentresponse: str— the agent's responsemodel: str(optional) — model identifier, e.g."gpt-4o","claude-sonnet-4-6"tokens_in: int(optional) — input token counttokens_out: int(optional) — output token countlatency_ms: float(optional) — round-trip latency in millisecondstags: list[str](optional)
obs.config_note — parameter decisions¶
Record why you chose a configuration value. This becomes invaluable when reviewing runs weeks later or comparing across experiments.
obs.config_note(
parameter="temperature",
value=0.7,
rationale="Lower temperature reduced hallucination rate in pilot runs"
)
Parameters:
parameter: str— the parameter namevalue: Any— the value usedrationale: str— why this value was chosen
obs.metric_event — spot annotations on metrics¶
Annotate a specific metric value at a point in the run. Useful for flagging a breakpoint, a phase transition, or an unexpected jump.
if step == warmup_steps:
obs.metric_event(
metric="accuracy",
value=score,
step=step,
note="Post-warmup baseline"
)
Parameters:
metric: str— the metric namevalue: float— the value at this momentstep: int(optional) — iteration or step indexnote: str(optional) — free-text annotation
@observation_track_step — automatic entry/exit/error recording¶
Wrap any function to automatically record entry, exit timing, and exceptions as observations, without adding boilerplate to the function body:
from adgtk.tracking import ObservationWriter, observation_track_step
obs = ObservationWriter("retriever")
@observation_track_step(obs)
def fetch_documents(query: str) -> list:
...
# Exceptions become warnings but are still re-raised.
# Set log_errors=False to capture timing only:
@observation_track_step(obs, log_errors=False)
def best_effort_step(query: str) -> list:
...
Multiple components in one scenario¶
Create one writer per logical subsystem. Each writer uses its own component name, so observations from different parts of your code are clearly labelled in the report:
retriever_obs = ObservationWriter("retriever", tags=["retrieval"])
evaluator_obs = ObservationWriter("evaluator", tags=["eval"])
retriever_obs.note("Loaded index")
evaluator_obs.metric_event("f1", 0.82)
Module-level API¶
The underlying module-level functions (adgtk.tracking.observations) are still available directly. They are the storage backend that ObservationWriter delegates to. Prefer the writer in scenario code; the module-level API is useful in framework utilities that run outside a scenario context.
Returning results from a scenario¶
Your run_scenario method must return a RunResult. There are two ways to build one.
Option 1: RunResultBuilder (recommended for longer runs)¶
Build results incrementally throughout the run, then finalize at the end.
from adgtk.experiment.result import RunResultBuilder
class MyScenario:
def run_scenario(self, result_folders):
result = RunResultBuilder()
result.tag("model", "gpt-4o")
result.tag("prompt_variant", "chain-of-thought")
# ... run logic ...
accuracy = 0.91
result.set("accuracy", accuracy)
result.pass_if(
lambda m: m["accuracy"] >= 0.85,
on_fail=f"Accuracy {accuracy:.3f} below threshold"
)
result.summarize(f"Chain-of-thought: accuracy={accuracy:.3f}")
return result.finalize()
Option 2: RunResult directly (for simpler scenarios)¶
from adgtk.experiment.result import RunResult
return RunResult(
metrics={"accuracy": 0.91, "f1": 0.88},
verdict="pass",
verdict_note="Both metrics above threshold",
tags={"model": "gpt-4o", "prompt_variant": "v2"},
summary="Baseline run with standard prompting",
)
RunResultBuilder method reference¶
| Method | Description |
|---|---|
.set(key, value) |
Record a result metric |
.tag(key, value) |
Attach a label for cross-run filtering |
.mark_pass(note="") |
Set verdict to "pass" |
.mark_fail(reason) |
Set verdict to "fail" |
.mark_inconclusive(note="") |
Set verdict to "inconclusive" |
.pass_if(condition, on_fail="") |
Evaluate a lambda against current metrics |
.summarize(text) |
Set a free-text summary for the report |
.finalize() |
Return the RunResult |
All methods return self, so calls can be chained:
Tags¶
Tags are str → str key-value pairs attached to a run. They serve two purposes:
- Cross-run comparison —
adgtk-results exportincludes tags as columns - Filtering — useful for grouping runs by configuration axis
Good tag candidates: model name, prompt variant, temperature, dataset split, any parameter you're sweeping. Values are always coerced to strings.
Numeric metrics with MetricTracker¶
MetricTracker records numeric time-series values. At run end it writes each label to a CSV and the manifest summarizes them (mean, std, min, max, n).
from adgtk.tracking import MetricTracker
class MyScenario:
def __init__(self, ...):
self.tracker = MetricTracker(name="eval", purpose="measurement")
self.tracker.register_metric("accuracy")
self.tracker.register_metric("latency_ms")
def run_scenario(self, result_folders):
for item in self.dataset:
score = self.evaluate(item)
self.tracker.add_data("accuracy", score)
self.tracker.save_data(result_folders)
result = RunResultBuilder()
result.set("accuracy_mean", self.tracker.get_average("accuracy"))
result.pass_if(lambda m: m["accuracy_mean"] >= 0.85)
return result.finalize()
Key MetricTracker methods¶
| Method | Description |
|---|---|
register_metric(label) |
Declare a metric before recording data |
add_data(label, value) |
Append a single float or int |
add_raw_data(label, values) |
Append multiple values from an iterable |
get_average(label) |
Mean of all recorded values |
get_latest_value(label) |
Most recently recorded value |
get_sum(label) |
Sum of all recorded values |
get_all_data(label) |
Full list of all recorded values |
measurement_count(label) |
Number of data points recorded |
save_data(result_folders) |
Write CSVs and register as artifacts |
Measurements with MeasurementEngine¶
MeasurementEngine wraps MetricTracker with registered measurement functions from the factory. Use it when you want to apply standard measurements to a dataset without writing the evaluation loop yourself.
from adgtk.measurements import MeasurementEngine
engine = MeasurementEngine(add_by_tag="text")
engine.measure(generated_outputs)
engine.save_data(result_folders)
for label, value in engine.export_last_val_to_dict().items():
result.set(label, value)
What gets written to disk¶
After every run the following files are written to results/{run_id}/:
results/
{run_id}/
run.exp.config.yaml ← full experiment YAML as run
conclusions/
run.manifest.json ← canonical JSON (all run data)
report.md ← human-readable markdown report
results.yaml ← RunResult fields
metrics/
{name}.{label}.csv ← one file per tracked metric label
datasets/
images/
other/
run.manifest.json¶
The canonical document. Contains everything: timing, verdict, tags, result metrics, per-metric summaries, all observations, all artifacts, and the full config snapshot.
{
"manifest_version": "1.0",
"run_id": "3.my-experiment",
"experiment_name": "my-experiment",
"timestamp_start": "2026-05-09 14:02:11",
"timestamp_end": "2026-05-09 14:06:44",
"duration_seconds": 273.4,
"status": "complete",
"verdict": "pass",
"tags": {"model": "gpt-4o", "prompt_variant": "chain-of-thought"},
"result_metrics": {"accuracy_mean": 0.91, "n_evaluated": 120},
"metric_summaries": {
"eval.accuracy": {"n": 120, "mean": 0.91, "std": 0.04, "min": 0.75, "max": 1.0}
},
"observations": [
{"kind": "config_note", "parameter": "temperature", "value": 0.7, "rationale": "..."},
{"kind": "agent_turn", "model": "gpt-4o", "tokens_in": 312, "tokens_out": 88, "latency_ms": 940.0}
]
}
report.md¶
Auto-generated from the manifest. Shows verdict, tags, result metrics, measurement summaries, observations, and artifacts. Check this after a run for a quick human summary.
Cross-run comparison¶
Experiment rollup report¶
After accumulating several runs, generate a rolled-up view with:
This writes two files:
results/my-experiment/experiment_report.md— a Markdown document covering verdict and status distributions, timing statistics, a per-run summary table, cross-run metric statistics, tag distributions, and a config-consistency warning if any run deviated from the majority configuration.results/my-experiment/common/results.csv— a flat CSV with one row per run, ready for notebook analysis.
Config consistency check: The report compares run.exp.config.yaml across all runs. If any run used a different configuration than the majority, those runs are listed in a warning block at the top and marked with ⚠ in the per-run table. This catches runs made after editing a blueprint, accidental re-runs against a modified config, or batch jobs that mixed different experiment versions.
Raw export with adgtk-results¶
Tags appear as columns:
run_id,verdict,accuracy_mean,model,prompt_variant,temperature
3.my-experiment,pass,0.91,gpt-4o,chain-of-thought,0.7
2.my-experiment,fail,0.79,gpt-4o,zero-shot,0.9
Load in a notebook:
import pandas as pd
df = pd.read_csv("runs.csv")
df.groupby("prompt_variant")["accuracy_mean"].mean()
Studies — cross-experiment rollup¶
An experiment rollup operates within a single experiment (multiple runs of the same blueprint). A study operates across experiments — collecting results from several different blueprints into a single comparison.
Use a study when you want to compare, for example:
- Two models running the same scenario
- Three prompt strategies against the same dataset
- A baseline experiment and its successors over time
Create a study blueprint¶
The interactive wizard lists available experiments and lets you select them by index or name. The blueprint is saved to studies/{name}.yaml:
name: model-comparison
description: GPT-4o vs Claude on the extraction task
tags:
- extraction
experiments:
- exp-gpt4o
- exp-claude
Edit this file at any time to change which experiments are included.
Generate the study report¶
This reads all run manifests for each listed experiment, recalculates statistics from scratch, and writes two files to study-results/model-comparison/:
| File | Purpose |
|---|---|
study_report.md |
Overview table + per-experiment config, verdicts, timing, metrics |
study_results.csv |
Combined CSV — one row per run across all experiments |
The study_report.md opens with a cross-experiment overview table — one row per experiment showing run count, pass/fail counts, average duration, and mean values for every result metric and measurement. This is the at-a-glance comparison.
Each experiment then gets its own section with its configuration from the first run (the canonical "what was this experiment?"), verdict distribution, timing stats, and per-run metric tables.
Study CSV for notebooks¶
The combined CSV uses the same column schema as results.csv but spans all experiments and leads with experiment_name:
import pandas as pd
df = pd.read_csv("study-results/model-comparison/study_results.csv")
# Compare mean accuracy per experiment
df.groupby("experiment_name")["metric_accuracy"].mean()
# Filter to passing runs only
df[df["verdict"] == "pass"].groupby("experiment_name").size()
See adgtk-study for the full CLI reference.
Complete scenario example¶
See the Creating Scenarios page for a full end-to-end example combining observations, RunResultBuilder, and MetricTracker.