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.
Causal links and model calls¶
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:
- event ids are unique and
sequenceis contiguous; - every causal edge points backward;
- every candidate has exactly one creation event;
- every parent exists and predates its child;
- every candidate reference resolves.
See RFC 0006 for the inclusion rule, complete action vocabulary, runtime compatibility contract, and registered scaffold mapping.