Summary
GEPA already exposes a lot of useful observability through callbacks, run_dir artifacts, and W&B/MLflow tables. I think one small missing artifact would make GEPA runs easier to audit and reproduce from local files alone:
a compact admission_manifest.jsonl record for each proposed candidate, accepted or rejected, keyed by candidate hash and parent hash.
This would be especially useful for prompt/code/agent-architecture optimization runs where the important question after the fact is not only "what was the best candidate?", but "why was this mutation admitted into the candidate pool, what evidence admitted it, and what evidence was only used for rejection/debugging?"
What I found in the current code/docs
- GEPA already has deterministic candidate hashing for evaluation-cache keys:
src/gepa/core/state.py.
run_dir already writes human-readable run_log.json and candidates.json: src/gepa/core/state.py.
- Accepted candidates are appended through
update_state_with_new_program(...): src/gepa/core/state.py.
- Reflective proposal acceptance/rejection is centralized in
_accept_reflective_proposal(...): src/gepa/core/engine.py.
- Rejected candidates currently emit
CandidateRejectedEvent with old_score, new_score, and reason, but not the proposed candidate, parent candidate hash, subsample ids, proposal kind, or candidate hash: src/gepa/core/engine.py.
- Accepted candidates emit
CandidateAcceptedEvent and then ValsetEvaluatedEvent, which includes validation scores and coverage: src/gepa/core/engine.py.
- The experiment tracking docs already document a
proposals table for accepted/rejected LM calls, including status, candidate_idx, parent_ids, minibatch scores, prompt, raw LM output, and proposed text: docs/docs/guides/experiment-tracking.md.
So the requested change is not "add logging from scratch." It is a narrower, local, machine-readable admission ledger that joins the candidate/proposal/validation/budget facts that are currently spread across callbacks, state, and tracker tables.
Why this is different from related open issues
I saw several related issues and tried to avoid duplicating them:
Proposed artifact
When run_dir is set, GEPA could append records like this to admission_manifest.jsonl:
{
"schema_version": 1,
"iteration": 12,
"proposal_kind": "reflective_mutation",
"decision": "accepted",
"decision_reason": "strict_improvement: new_sum > old_sum",
"candidate_idx": 5,
"candidate_hash": "sha256:...",
"parent_candidate_ids": [2],
"parent_candidate_hashes": ["sha256:..."],
"components_updated": ["instructions"],
"subsample_ids": ["train_17", "train_42", "train_81"],
"subsample_score_before": 1.2,
"subsample_score_after": 2.0,
"val_evaluation_policy": "FullEvaluationPolicy",
"evaluated_val_ids": ["val_0", "val_1", "val_2"],
"val_average_score": 0.73,
"is_best_program": true,
"metric_calls_before_decision": 120,
"metric_calls_after_decision": 146,
"metric_calls_charged": 26,
"cache_hits": null,
"cache_misses": null,
"tracker_row_ref": {
"table": "proposals",
"candidate_idx": 5
}
}
For rejected proposals:
{
"schema_version": 1,
"iteration": 13,
"proposal_kind": "reflective_mutation",
"decision": "rejected",
"decision_reason": "strict_improvement: new_sum <= old_sum",
"candidate_idx": null,
"candidate_hash": "sha256:...",
"parent_candidate_ids": [2],
"parent_candidate_hashes": ["sha256:..."],
"components_updated": ["instructions"],
"subsample_ids": ["train_5", "train_8", "train_13"],
"subsample_score_before": 2.0,
"subsample_score_after": 1.3,
"val_evaluation_policy": null,
"evaluated_val_ids": [],
"val_average_score": null,
"is_best_program": false,
"metric_calls_before_decision": 146,
"metric_calls_after_decision": 152,
"metric_calls_charged": 6
}
The full prompt/raw LM output can remain in the existing experiment tracker table. The manifest can store hashes or tracker references by default to avoid making a local audit file unexpectedly sensitive.
Possible implementation path
- Expose the existing candidate hash helper or add a small public/internal helper, rather than duplicating hash logic outside
state.py.
- Add a lightweight
AdmissionRecord helper or callback event around _accept_reflective_proposal(...).
- Write JSONL atomically under
run_dir when a proposal decision is made.
- Include merge decisions as the same schema with
proposal_kind: "merge" and two parent hashes.
- Keep this opt-in if there is concern about local artifacts, e.g.
EngineConfig(write_admission_manifest=True) or enabled only when run_dir is set.
Suggested tests
- A synthetic reflective proposal that is accepted writes one manifest line with non-null
candidate_idx, candidate hash, parent hash, minibatch ids, and evaluated val ids.
- A synthetic reflective proposal that is rejected writes one manifest line with
candidate_idx: null and is not present in candidates.json.
- A merge accept/reject path writes the same schema with two parents.
- A custom partial
EvaluationPolicy records partial evaluated_val_ids and total_valset_size/coverage.
- With
cache_evaluation=True, the manifest records actual charged metric calls separately from candidate/example pairs served from cache, if that accounting is available.
External reference
I have been using a small bounded verifier harness here: https://github.com/sunghunkwag/rsi-metaforge-core
The relevant pattern is not the whole harness, but the evidence discipline around hidden evals, train-fit rejection, rollback-sensitive admission, and explicit claim boundaries:
That repo is not a GEPA replacement and not an unrestricted self-improvement claim. It is just a useful reference for why an admission/rollback ledger becomes valuable once candidate-generating systems start optimizing prompts, code, or agent architectures under evaluator gates.
Summary
GEPA already exposes a lot of useful observability through callbacks,
run_dirartifacts, and W&B/MLflow tables. I think one small missing artifact would make GEPA runs easier to audit and reproduce from local files alone:This would be especially useful for prompt/code/agent-architecture optimization runs where the important question after the fact is not only "what was the best candidate?", but "why was this mutation admitted into the candidate pool, what evidence admitted it, and what evidence was only used for rejection/debugging?"
What I found in the current code/docs
src/gepa/core/state.py.run_diralready writes human-readablerun_log.jsonandcandidates.json:src/gepa/core/state.py.update_state_with_new_program(...):src/gepa/core/state.py._accept_reflective_proposal(...):src/gepa/core/engine.py.CandidateRejectedEventwithold_score,new_score, andreason, but not the proposed candidate, parent candidate hash, subsample ids, proposal kind, or candidate hash:src/gepa/core/engine.py.CandidateAcceptedEventand thenValsetEvaluatedEvent, which includes validation scores and coverage:src/gepa/core/engine.py.proposalstable for accepted/rejected LM calls, includingstatus,candidate_idx,parent_ids, minibatch scores, prompt, raw LM output, and proposed text:docs/docs/guides/experiment-tracking.md.So the requested change is not "add logging from scratch." It is a narrower, local, machine-readable admission ledger that joins the candidate/proposal/validation/budget facts that are currently spread across callbacks, state, and tracker tables.
Why this is different from related open issues
I saw several related issues and tried to avoid duplicating them:
opt_state#227 is about persistent adapter/opt state. This proposal does not require making rejected candidates part of optimizer state.EvaluationPolicyby recording which val ids were evaluated and which policy was used.Proposed artifact
When
run_diris set, GEPA could append records like this toadmission_manifest.jsonl:{ "schema_version": 1, "iteration": 12, "proposal_kind": "reflective_mutation", "decision": "accepted", "decision_reason": "strict_improvement: new_sum > old_sum", "candidate_idx": 5, "candidate_hash": "sha256:...", "parent_candidate_ids": [2], "parent_candidate_hashes": ["sha256:..."], "components_updated": ["instructions"], "subsample_ids": ["train_17", "train_42", "train_81"], "subsample_score_before": 1.2, "subsample_score_after": 2.0, "val_evaluation_policy": "FullEvaluationPolicy", "evaluated_val_ids": ["val_0", "val_1", "val_2"], "val_average_score": 0.73, "is_best_program": true, "metric_calls_before_decision": 120, "metric_calls_after_decision": 146, "metric_calls_charged": 26, "cache_hits": null, "cache_misses": null, "tracker_row_ref": { "table": "proposals", "candidate_idx": 5 } }For rejected proposals:
{ "schema_version": 1, "iteration": 13, "proposal_kind": "reflective_mutation", "decision": "rejected", "decision_reason": "strict_improvement: new_sum <= old_sum", "candidate_idx": null, "candidate_hash": "sha256:...", "parent_candidate_ids": [2], "parent_candidate_hashes": ["sha256:..."], "components_updated": ["instructions"], "subsample_ids": ["train_5", "train_8", "train_13"], "subsample_score_before": 2.0, "subsample_score_after": 1.3, "val_evaluation_policy": null, "evaluated_val_ids": [], "val_average_score": null, "is_best_program": false, "metric_calls_before_decision": 146, "metric_calls_after_decision": 152, "metric_calls_charged": 6 }The full prompt/raw LM output can remain in the existing experiment tracker table. The manifest can store hashes or tracker references by default to avoid making a local audit file unexpectedly sensitive.
Possible implementation path
state.py.AdmissionRecordhelper or callback event around_accept_reflective_proposal(...).run_dirwhen a proposal decision is made.proposal_kind: "merge"and two parent hashes.EngineConfig(write_admission_manifest=True)or enabled only whenrun_diris set.Suggested tests
candidate_idx, candidate hash, parent hash, minibatch ids, and evaluated val ids.candidate_idx: nulland is not present incandidates.json.EvaluationPolicyrecords partialevaluated_val_idsandtotal_valset_size/coverage.cache_evaluation=True, the manifest records actual charged metric calls separately from candidate/example pairs served from cache, if that accounting is available.External reference
I have been using a small bounded verifier harness here: https://github.com/sunghunkwag/rsi-metaforge-core
The relevant pattern is not the whole harness, but the evidence discipline around hidden evals, train-fit rejection, rollback-sensitive admission, and explicit claim boundaries:
That repo is not a GEPA replacement and not an unrestricted self-improvement claim. It is just a useful reference for why an admission/rollback ledger becomes valuable once candidate-generating systems start optimizing prompts, code, or agent architectures under evaluator gates.