Skip to content

Trajectories (ETIF) — one vocabulary for every evolutionary loop

ETIF, the Evolutionary Trajectory Interchange Format, records a run as two connected views:

  • candidates[] — the stable identity, content, generation, and lineage of every evolvable object;
  • events[] — every behavior that selected, created, scored, moved, removed, or changed how those objects evolve.

This is broader than a list of proposed programs. ETIF v1 can represent evaluator failures, crossover, migration, eviction, dynamic islands, model calls that produce no program, agent/tool actions, search-strategy swaps, guidance activation, checkpoints, and final evaluation with one scaffold-neutral schema.

Evaluator-invalid candidates are intentionally trajectory-only across all bundled scaffolds. Their source, lineage, proposal/model evidence, full evaluator result, and rejected population decision are recorded, while they are excluded from every active population and future selection surface. A valid zero score is different and remains eligible for method-specific admission.

Schema

ETIF-v1.1

Timeline unit

one evolutionary behavior, not one genome

Lineage

parent_ids is plural, so mutation and crossover share one representation

The document

EvolutionaryTrajectory
├── run              ← runtime identity, lifecycle, and resume segments
├── candidates[]     ← solutions, search strategies, guidance, or future candidate types
├── events[]         ← chronological sequence + causal graph
├── agent_sessions[] ← external-agent receipts, usage, and native traces
├── provenance       ← config/task/card/component digests
├── summary          ← event-derived counts and best result
└── extensions       ← document-level export metadata

A regular program is candidate_type: solution. EvoX search code is candidate_type: search_strategy; AdaEvolve breakthrough guidance is candidate_type: guidance. The candidate vocabulary is open, so a scaffold that evolves prompts, workflows, or another representation does not require a new ETIF schema.

{
  "candidate_id": "g_82f1",
  "candidate_type": "solution",
  "generation": 8,
  "parent_ids": ["g_129a", "g_71cd"],
  "created_by_event_id": "evt_40d9",
  "content": "def solve():\n    return 42\n",
  "attributes": {}
}

Plural parent_ids makes crossover ordinary: mutation has one parent, crossover has two or more, and a seed has none.

One event envelope

Every event has the same structural keys. Values that do not apply are null, [], or {} rather than disappearing.

{
  "event_id": "evt_40d9",
  "sequence": 42,
  "recorded_at": "2026-07-16T20:15:23.456Z",
  "iteration": 12,
  "attempt_number": 2,
  "evolution_track": "solution",
  "event_type": "proposal",
  "action": "crossover",
  "status": "completed",
  "component": {"role": "proposer", "name": "CrossoverProposer"},
  "caused_by_event_ids": ["evt_40d7", "evt_40d8"],
  "inputs": [
    {"entity_type": "candidate", "entity_id": "g_129a", "role": "parent"},
    {"entity_type": "candidate", "entity_id": "g_71cd", "role": "parent"}
  ],
  "outputs": [
    {"entity_type": "candidate", "entity_id": "g_82f1", "role": "created"}
  ],
  "duration_seconds": 8.4,
  "error": null,
  "details": {"operator": "crossover", "content_changed": true},
  "extensions": {}
}

The most common event types are:

Type Examples
selection select a parent and inspirations
model_call proposal, guide, summary, or strategy-generation call
proposal initialize, mutate, rewrite, crossover, or snapshot a candidate
evaluation evaluate, validate, or explicitly reuse a score
population admit/reject decisions, eviction, migration, replication, partition or frontier updates
adaptation trigger, update, activate, retain, or restore a search policy
agent, tool, memory agent sessions, diagnostics, and search-relevant memory changes
run, checkpoint start/resume/complete/fail/interrupt and save/load

event_type is the broad common category. action is its readable verb. Scaffold-specific evidence goes in a namespaced extensions map; the core schema and exporter never branch on a scaffold name. The category is validated against the table above, so a new scaffold cannot silently create its own incompatible event family. New behavior chooses the nearest common category and an intuitive verb.

Portable details keys follow the same rule:

Event Shared detail keys
selection selection_strategy, selection_mode, parent_candidate_id, inspiration_candidate_ids, pool_size
model call purpose, call_id, model, prompt, response, usage, parameters, tooling
tool execution model_call_id, tool_call_id, call_index, round, tool_name, tool_type, arguments, result, result_artifact, result_media_type
evaluation evaluation_stage, data_split, result, reason
population decision, reason, source_location, destination_location, new_best
adaptation target, reason, state_changed
memory memory_name, entry_type, content

Only applicable keys need values inside details; the fixed event envelope itself always retains all keys. Policy-private snapshots or provider-specific evidence go under a namespaced extensions key rather than introducing a scaffold-specific core field.

Completed, invalid, failed, and skipped are different

status reports whether the operation ran. An evaluator's domain verdict is independent:

{
  "event_type": "evaluation",
  "action": "evaluate",
  "status": "completed",
  "error": null,
  "details": {
    "evaluation_stage": "search",
    "result": {"is_valid": false, "metrics": {"combined_score": 0.0}}
  }
}

The scorer ran successfully and judged the candidate invalid. An infrastructure failure instead has status: failed, details.result: null, and a structured error. A no-diff or too-long proposal has an explicit evaluation event with status: skipped. Population admission is another independent event, so “not evaluated,” “invalid,” and “valid but rejected by crowding” cannot be conflated.

sequence gives the exact timeline. caused_by_event_ids gives the explanation graph:

selection ─┐
           ├─> model_call ─┬────────────────> proposal ─> evaluation ─> admission
guidance ──┘               └─> tool/execute ─┘

A model call is recorded once. One call may cause several Meta Harness candidates; an AdaEvolve guide call may create guidance rather than a solution; an EvoX compression call may create no candidate. This keeps prompt/response/usage accounting exact without duplicating cost per child.

For a tool-enabled proposer, that one model_call/generate is the logical call. Its details.usage aggregates every internal LLM round and its envelope duration is the full wall time, including tool work. details.tooling gives the breakdown: available tools, LLM/tool round counts, attempted/succeeded/failed calls, separate LLM/tool latencies, and recovery errors. Each attempted call is then a separate tool/execute event. The proposal depends on both the logical model call and its tool events, so a reader can see exactly which evidence preceded the candidate without counting model cost twice.

One generous tool vocabulary

ETIF does not define a Tavily event, a shell event, and a database event. They all use event_type: tool, action: execute, and the same detail vocabulary:

{
  "event_type": "tool",
  "action": "execute",
  "status": "completed",
  "component": {"role": "tool", "name": "web_search"},
  "duration_seconds": 0.42,
  "error": null,
  "details": {
    "model_call_id": "call_7a91",
    "tool_call_id": "toolu_01",
    "call_index": 1,
    "round": 1,
    "tool_name": "web_search",
    "tool_type": "function",
    "arguments": {"query": "current circle packing bound"},
    "result": {"sources": [{"url": "https://example.org/reference"}]},
    "result_artifact": null,
    "result_media_type": null
  }
}

arguments and result may be any JSON value. That deliberate openness lets web search, code execution, file operations, browsers, databases, and user-defined tools share one schema. Identity, order, status, timing, and errors remain stable. A failed attempt uses status: failed, keeps the attempted arguments, sets result: null, and places a structured EventError on the event envelope. Provider-only data belongs under a namespaced extensions key.

call_index is the total order of attempts within the logical call. round is the 1-based tool budget slot; malformed retries that never consume the budget may share a round, while details.tooling.llm_round_count reports every provider request.

Large string or JSON results are moved to a content-addressed Artifact; result_artifact then contains its path, SHA-256, and size while result_media_type explains how to read it. Consumers therefore get rich arbitrary results without allowing one browser page or command output to bloat the main ETIF document. summary.tool_call_count and summary.tool_status_counts are derived from the same events.

A complete tool-assisted example

The repository includes a complete, model-validated ETIF v1.1 document with one completed web search and one failed code-execution attempt. The essential portion is shown below; the full file retains every common event-envelope field.

{
  "events": [
    {
      "event_id": "evt_006_model_call",
      "event_type": "model_call",
      "action": "generate",
      "status": "completed",
      "details": {
        "call_id": "call_example_01",
        "tooling": {
          "llm_round_count": 3,
          "call_count": 2,
          "status_counts": {
            "completed": 1,
            "failed": 1,
            "skipped": 0,
            "interrupted": 0
          }
        }
      }
    },
    {
      "event_id": "evt_007_web_search",
      "event_type": "tool",
      "action": "execute",
      "status": "completed",
      "caused_by_event_ids": ["evt_006_model_call"],
      "details": {
        "model_call_id": "call_example_01",
        "tool_call_id": "tool_search_01",
        "call_index": 1,
        "round": 1,
        "tool_name": "web_search",
        "arguments": {"query": "current reference value"},
        "result": {"results": [{"url": "https://example.org/reference"}]}
      }
    },
    {
      "event_id": "evt_008_code_execution",
      "event_type": "tool",
      "action": "execute",
      "status": "failed",
      "caused_by_event_ids": ["evt_006_model_call"],
      "error": {
        "error_type": "SandboxUnavailable",
        "message": "the example sandbox was unavailable",
        "traceback": null
      },
      "details": {
        "model_call_id": "call_example_01",
        "tool_call_id": "tool_code_01",
        "call_index": 2,
        "round": 2,
        "tool_name": "code_execution",
        "arguments": {
          "language": "python",
          "code": "assert abs(1.25 - 1.25) < 1e-9"
        },
        "result": null
      }
    },
    {
      "event_id": "evt_009_proposal",
      "event_type": "proposal",
      "action": "generate",
      "status": "completed",
      "caused_by_event_ids": [
        "evt_006_model_call",
        "evt_007_web_search",
        "evt_008_code_execution"
      ]
    }
  ],
  "summary": {
    "model_call_count": 1,
    "tool_call_count": 2,
    "tool_status_counts": {
      "completed": 1,
      "failed": 1,
      "skipped": 0,
      "interrupted": 0
    }
  }
}

Read this as “one billed logical model call attempted two tools; one succeeded, one failed, and the model still produced a candidate using the evidence available.” Failure evidence is preserved, but it does not turn into a second model call or disappear from the trajectory. The accompanying example notes also show the artifact-backed form used for oversized results.

For proposer.children_per_model_call > 1, every response still receives a normal opaque Genome ID and its own candidate/evaluation row. Siblings share parent_id and proposal call_id; their extensions.response_index, extensions.response_count, extensions.batch_rank, and extensions.batch_selected identify the response inside the call and the best-of-N decision. Evaluated valid siblings outside children_to_keep have admission.status: rejected and admission.verdict: not_selected. Genome ID syntax and the one-row-per-Genome trajectory schema do not change.

Migration and score reuse

Migration is a population/migrate event with source and destination locations. When an implementation copies a measured candidate rather than invoking the scorer again, ETIF follows it with evaluation/reuse. Counting evaluation/evaluate therefore gives actual evaluator calls; a migrant never inflates that number.

Multiple evolutionary tracks

Most runs use evolution_track: solution. Co-evolution uses the same schema several times:

solution track  : select → propose program → evaluate → admit
strategy track  : trigger → generate strategy → validate → activate → window-score → archive
guidance track  : trigger → generate ideas → activate/rotate guidance

Tracks are open strings, not scaffold enums. A future nested or co-evolutionary method can add a track without changing the ETIF models.

Search evaluation and final evaluation

Both are evaluation/evaluate events. details.evaluation_stage distinguishes search, final, verification, and strategy-window scoring; details.data_split can name train, held_out, private_test, or another task-defined split. The final evaluator always references the already selected frozen_winner—it does not pretend to create a new candidate.

Files written by a run

<run_dir>/
├── evolutionary_trajectory.jsonl
│                             # trajectory-event-v1: candidates and every behavior
├── tool_calling_trajectory.jsonl
│                             # tool/execute-only projection of the event stream
├── config.yaml                # latest secret-free effective config
├── run.json                   # terminal summary and artifact index
├── best/                      # canonical winning candidate
├── checkpoints/
│   └── checkpoint_<i>/
│       ├── state.json
│       ├── population.jsonl
│       └── tool_calling_trajectory.jsonl
│                             # cumulative tool calls through checkpoint i
└── etif.json                 # optional normalized ETIF-v1.1 export

evolutionary_trajectory.jsonl is the single authoritative raw stream. Its event rows record model calls, tools, evaluations, population changes, checkpoints, and other lifecycle behavior. Candidate creation events carry a streaming-only new_candidates list, so candidate content and genealogy do not require a second per-candidate file.

tool_calling_trajectory.jsonl retains the complete event envelope, including the global event id and sequence, but contains only tool/execute rows. It is written after event validation and deduplication, so failed calls and calls attached to rejected or no-diff proposals remain present without being repeated for multiple candidates. Whenever Galapagos saves checkpoint_<i>, it also writes a cumulative tool-only snapshot inside that checkpoint. On resume, the unified stream, root tool ledger, and surviving checkpoint snapshots are reconciled by event id before new calls append. This makes each checkpoint self-contained without maintaining a live file directly under checkpoints/.

The summary fields have one meaning across providers:

Field Meaning
enabled, available_tools whether tools were offered and their portable names
round_count, max_rounds tool-budget rounds consumed and the configured ceiling
llm_round_count all provider requests, including final synthesis and recovery
call_count, status_counts every attempted call and its terminal state
successful_call_count, failed_call_count convenient completed/failed aliases
llm_duration_seconds, tool_duration_seconds separate latency totals inside the logical call
error_count, errors bounded recovery diagnostics, including errors tied to failed calls

The raw event rows additionally carry schema_version and a streaming-only new_candidates list so each candidate survives a crash before export; the normalized etif.json moves those records into the root candidates[] ledger.

Older runs with evolution_events.jsonl remain directly readable. Runs that only have the legacy trajectory.jsonl candidate stream are still exportable: Galapagos projects their recorded genome steps into v1 events and marks extensions.legacy_projection: true. It cannot reconstruct an old strategy swap or tool action that was never recorded.

Producing ETIF

# --output-dir always writes both raw JSONL streams; --emit-etif adds etif.json
galapagos run --scaffold openevolve --task circle_packing \
    --output-dir runs/demo --emit-etif

# or later
galapagos export runs/demo

From Python:

from galapagos.trajectory import run_dir_to_etif

trajectory = run_dir_to_etif("runs/demo")
assert trajectory.schema_version == "ETIF-v1.1"
for event in trajectory.events:
    print(event.sequence, event.event_type, event.action, event.status)

Explicit run_dir_to_etif_v10 / convert_run_dir_v10 and run_dir_to_etif_v03 / convert_run_dir_v03 compatibility APIs remain available for consumers that need an older document.

What ETIF guarantees

The model validates that:

  1. event ids are unique and sequence is contiguous;
  2. every causal edge points backward;
  3. every candidate has exactly one creation event;
  4. every parent exists and predates its child;
  5. every candidate reference resolves.

See RFC 0006 for the inclusion rule, complete action vocabulary, runtime compatibility contract, and registered scaffold mapping.