Reinforcement learning environment for training agentic LLMs on real-world multi-step tasks with verifiable rewards.
Terra extends the GAIA evaluation methodology into a full RL training loop — producing clean binary reward signals suitable for GRPO-based RLVR training. Built on the goku multimodal agentic-evaluation engine, OpenHands, and the Agent SDK.
This is the harness submodule of the Terra project. Companion repos: dataset (the 10K corpus) · trajectories (recorded runs) · delivery (client deliverable) · samples (public evaluation sample). Terra's task path lives at
benchmarks/gaia/.
- 10K-task corpus — GAIA-derived tasks across 3 difficulty levels
- Sandboxed tool surface — web search, file I/O, code execution
- Binary reward oracle — exact-match scoring for GRPO/RLVR
- Trace collection — full action/observation histories for offline training
- End-to-end pipeline — inference → scoring → trace export in one harness
┌─────────────┐ ┌──────────────────┐ ┌────────────────┐ ┌────────────┐
│ GAIA Tasks │────▶│ Agent Execution │────▶│ Reward Oracle │────▶│ Traces │
│ (10K) │ │ (sandboxed) │ │ (exact-match) │ │ (RLVR) │
└─────────────┘ └──────────────────┘ └────────────────┘ └────────────┘
- Tasks — Multi-step questions requiring web search, file processing, and reasoning
- Agent execution — LLM agents solve tasks in sandboxed environments (browser, terminal, code interpreter)
- Reward signal — Binary exact-match against gold answers → clean signal for GRPO
- Traces — Full conversation histories stored for offline RL training
- Python 3.12+
- uv ≥ 0.8.13
git clone git@github.com:EtharaOrion/terra-harness.git
cd terra-harness
make build# Create config
cat > .llm_config/your-model.json << 'EOF'
{
"model": "your-model-name",
"base_url": "https://your-endpoint",
"api_key": "YOUR_API_KEY"
}
EOF
# Validate
uv run validate-cfg .llm_config/your-model.jsonTAVILY_API_KEY=xxx uv run gaia-infer .llm_config/your-model.json \
--level 2023_level1 \
--split validation \
--num-workers 4uv run python -m benchmarks.gaia.get_score --file outputs/gaia/output.jsonlrun_terra.sh drives every stage from the Harbor task bundle to the final delivery
bundle. It lives in harness/ but operates on the Terra root (the same
TERRA_ROOT := .. convention the Makefile uses), so it works from any directory.
harness/run_terra.sh --infer --oracle-run --uuid <uuid> --probe-modelsThat single invocation runs, in order: the pre-spend gate (LLM proxy reachable,
docker daemon up, and the mandatory oracle/NOP task test) → Harbor bundle
converted to JSONL in a temp file → oracle reference run → model run → generated
pytest suite and rubric frozen → judge council and pytest executed → verifier
output written under trajectories/<uuid>/verification/ → final bundle assembled
under delivery/<uuid>/ → delivery layout conformance check.
Runs are stored as trajectories/<uuid>/<model>/run_N/, and each --infer pass
allocates the next free N rather than overwriting. A trajectory is already
scored by the time a second one exists, so there is no flag to overwrite it —
re-running a model adds run_2 beside run_1.
<model> is the canonical slug: the model id with its routing vendor and the
claude- product prefix stripped, the rest kept verbatim — anthropic/claude-opus-4-8
→ opus-4-8, openai/gpt-5.6-sol → gpt-5.6-sol. canonical_model_slug in
verification/terra/harbor.py is the only implementation; this script and the
delivery emitter both call it, and --model is normalized through it as well, so
one model cannot end up owning two directories.
Each run carries its own verifiers/ (report.json, rubric_results.json,
pytest_results.json), the same shape the delivered bundle uses, so a run
directory is self-describing. The instrument those verdicts came from is not
per-run: the rubric and generated suite are frozen once per task under
verification/verifiers/, because a rubric stored per run is a rubric that can
be edited per run.
harness/run_terra.sh --help # every flag and its default
harness/run_terra.sh --dry-run … # print the exact commands, execute nothing
# score and deliver what is already on disk, no model calls at all
harness/run_terra.sh --rubric-deterministic --skip-judge --no-bridgeThree properties are enforced rather than merely defaulted. The oracle/NOP gate
has no skip flag anywhere in the script: the task's own solution must score 1.0
and a no-op must score 0.0, because a corpus built on a task that does not
discriminate is worse than no corpus — the numbers look fine. The JSONL is never
written into dataset/: the bundle is content-addressed (uuid5 over its own file
hashes), so an extra file would re-identify the task and break recompute.py verify; conversion goes to a temp path and the converter refuses any output path
inside the dataset root. And a delivered run must be judged: --skip-judge
implies --skip-deliver, because an unjudged run has fewer decided checks and so
reports a misleadingly higher graded_score than a judged one.
Exit codes: 0 every run accepted · 1 at least one quarantined or the delivered
layout does not conform · 2 blocked by the pre-spend gate.
Every flag accepted by run_terra.sh. Defaults in brackets.
Task authoring — off by default; the bundle usually already exists.
| Flag | Effect |
|---|---|
--author-task |
seed/recompute.py stage + finalize + verify → dataset/<uuid>/ |
--seed-dir DIR |
Task seed, e.g. seed/tasks/task-01-weighted-mean. Required by --author-task: seeds live one per task under seed/tasks/, so there is no seed/grounding.yaml to default to. finalize is idempotent when the re-derived bundle is bit-identical and refuses to overwrite when it is not |
Trajectory generation — off by default; enable with --infer.
| Flag | Effect |
|---|---|
--infer |
Generate a model run with gaia-infer |
--oracle-run |
Also generate the reference run and mark it .oracle_reference; anchors the rubric's oracle half, without which oracle_enforced is false |
--llm-config PATH |
LLM JSON config [harness/proxy/claude-code-oauth.json] |
--dataset-file FILE |
Use this JSONL instead of converting the bundle |
--level LEVEL |
GAIA level [2023_level1] |
--split SPLIT |
Dataset split [validation] |
--model SLUG |
Model directory under trajectories/<uuid>/; the run lands in its next free run_N [from the LLM config's model] |
--max-iter N |
Max agent iterations [100] |
--num-workers N |
Inference workers [4] |
--critic MODE |
Critic mode [pass] |
--no-bridge |
Do not start proxy/claude_code_bridge.sh |
Scope
| Flag | Effect |
|---|---|
--uuid UUID |
One task instead of every discovered one. Scores every run under it |
--dataset DIR |
Dataset root [dataset] |
--trajectories DIR |
Trajectory root [trajectories] |
--output-dir DIR |
Where the delivery bundle is assembled [delivery] |
Pre-spend checks — run before any model call.
| Flag | Effect |
|---|---|
--probe-models |
One-token completion per configured model; catches an unroutable council seat before the run |
--allow-degraded |
Downgrade a bridge failure to a warning. Never applies to the oracle/NOP gate |
Stages — all run by default.
| Flag | Effect |
|---|---|
--skip-author |
Do not author/freeze the rubric + pytest suite |
--skip-pytest |
Do not execute the frozen suite |
--skip-judge |
Do not run the judge council (implies --skip-deliver) |
--skip-deliver |
Do not assemble the delivery bundle |
--skip-infer |
Force inference off even if --infer was given |
--force-author |
Re-freeze rubric and suite. Breaks pre-registration — a rubric written after seeing the run it grades is a description of that run, not evidence about it |
--rubric-deterministic |
Extract criteria from TRUTH.md instead of generating them; offline, no bridge |
Judge — a council of distinct models; the verdict is the vote, not one model.
| Flag | Effect |
|---|---|
--rubric-model NAME |
Authors the rubric [claude-sonnet-5]. Pinned separately from --gen-model so the definition of graded_score stays fixed when the test generator moves |
--judge-model NAME |
Fallback judge when the council cannot seat two reachable models [claude-opus-4-8] |
--judge-council LIST |
Comma-separated seats [claude-opus-4-8,claude-haiku-4-5]. Both seats are the claude family, so cross_family stays false until a gpt seat such as gpt-5.6-sol is added |
--judge-council-size N |
Seats when not named explicitly [2] |
--cross-family |
Require at least one seat from another model family |
--gen-model NAME |
Generates the pytest suite [claude-sonnet-5]. Held fixed: the suite is what a run is scored against, so regenerating it against a different model changes what passing means |
General
| Flag | Effect |
|---|---|
--force |
Re-run stages whose outputs already exist |
--dry-run |
Print every command without executing it |
--python PATH |
Interpreter for the verification module [harness/terra-venv/bin/python] |
-h, --help |
Usage |
Environment — every judge knob is also an env var; the flags above just export them.
| Variable | Default |
|---|---|
TAVILY_API_KEY |
(required by gaia-infer) |
ANTHROPIC_API_BASE |
http://127.0.0.1:8765 |
OPENAI_BASE_URL |
http://127.0.0.1:8788 |
TERRA_RUBRIC_MODEL |
claude-sonnet-5 |
TERRA_JUDGE_MODEL |
claude-opus-4-8 |
TERRA_GEN_MODEL |
claude-sonnet-5 |
TERRA_JUDGE_COUNCIL |
claude-opus-4-8,claude-haiku-4-5 |
TERRA_JUDGE_COUNCIL_SIZE |
2 |
TERRA_JUDGE_CROSS_FAMILY |
0 |
TERRA_BRIDGE_RETRIES |
8 |
The stages can also be run individually — see
COMMANDS.md in the
knowledge repo for the per-stage invocations and the make targets. (harness/ is
its own repository, so that file is one level up in the parent checkout rather than
in this tree.)
Validation subset (20 tasks; Level 1 = 4, Level 2 = 7, Level 3 = 9), end-to-end with full execution traces:
| Model | Level 1 | Level 2 | Level 3 | Overall |
|---|---|---|---|---|
| Kimi K2.5 | 100.0% | 85.7% | 0.0% | 50.0% (10/20) |
| Qwen3 VL | 25.0% | 14.3% | 0.0% | 10.0% (2/20) |
Frontier cohort —
PENDING PILOT. The next evaluation cohort is GPT-5.6-Sol and Claude Opus 4.8. Those runs are not yet complete, so no frontier numbers are reported here; this table is populated only from real scored trajectories. See the knowledge repo and the dashboard.
ethara/terra on HuggingFace — 10,000 GAIA-derived tasks.
| Field | Description |
|---|---|
task_id |
Unique identifier |
Question |
Multi-step task prompt |
Level |
Difficulty (1–3) |
Final answer |
Gold answer for exact-match scoring |
file_name |
Attached file (PNG, CSV, XLSX, PDF, MP4) |
Also includes full agent execution traces for Kimi K2.5 and Qwen3 VL.
terra-harness/
├── benchmarks/
│ └── gaia/
│ ├── run_infer.py # Inference pipeline
│ ├── get_score.py # Exact-match scoring
│ └── README.md # Benchmark-specific docs
├── vendor/
│ └── software-agent-sdk/ # OpenHands Agent SDK (submodule)
├── .llm_config/ # LLM configuration files
├── outputs/ # Inference results & traces
├── Makefile # Build automation
└── pyproject.toml # Dependencies & CLI entrypoints
make build # Install deps + pre-commit hooks
make format # Format with ruff
make lint # Lint with ruff
make pre-commit # Run all pre-commit checks
make clean # Remove cache filesSee CONTRIBUTING.md for guidelines.
@misc{terra2026,
title={Terra: A Verifiable-Reward RL Environment for General AI Assistants},
author={Ethara AI},
year={2026},
url={https://github.com/EtharaOrion/terra}
}MIT — see LICENSE for details.