Skip to content
 
 

Repository files navigation

Terra

Python 3.12+ License: MIT Dataset on HF

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/.


Key Features

  • 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

How It Works

┌─────────────┐     ┌──────────────────┐     ┌────────────────┐     ┌────────────┐
│  GAIA Tasks │────▶│  Agent Execution │────▶│  Reward Oracle │────▶│   Traces   │
│  (10K)      │     │  (sandboxed)     │     │  (exact-match) │     │  (RLVR)    │
└─────────────┘     └──────────────────┘     └────────────────┘     └────────────┘
  1. Tasks — Multi-step questions requiring web search, file processing, and reasoning
  2. Agent execution — LLM agents solve tasks in sandboxed environments (browser, terminal, code interpreter)
  3. Reward signal — Binary exact-match against gold answers → clean signal for GRPO
  4. Traces — Full conversation histories stored for offline RL training

Quick Start

Prerequisites

  • Python 3.12+
  • uv ≥ 0.8.13

Installation

git clone git@github.com:EtharaOrion/terra-harness.git
cd terra-harness
make build

Configure LLM

# 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.json

Run Inference

TAVILY_API_KEY=xxx uv run gaia-infer .llm_config/your-model.json \
    --level 2023_level1 \
    --split validation \
    --num-workers 4

Score Results

uv run python -m benchmarks.gaia.get_score --file outputs/gaia/output.jsonl

Run the whole pipeline in one command

run_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-models

That 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-8opus-4-8, openai/gpt-5.6-solgpt-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-bridge

Three 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.

Command reference

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 + verifydataset/<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.)


Results

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.


Dataset

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.


Project Structure

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

Development

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 files

See CONTRIBUTING.md for guidelines.


Citation

@misc{terra2026,
  title={Terra: A Verifiable-Reward RL Environment for General AI Assistants},
  author={Ethara AI},
  year={2026},
  url={https://github.com/EtharaOrion/terra}
}

License

MIT — see LICENSE for details.

About

Terra is a reinforcement learning environment built on the GAIA framework for training agentic LLMs on real-world multi-step tasks with verifiable rewards.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages