Skip to content

Add a run_dir admission manifest for candidate acceptance/rejection audits #381

Description

@sunghunkwag

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

  1. Expose the existing candidate hash helper or add a small public/internal helper, rather than duplicating hash logic outside state.py.
  2. Add a lightweight AdmissionRecord helper or callback event around _accept_reflective_proposal(...).
  3. Write JSONL atomically under run_dir when a proposal decision is made.
  4. Include merge decisions as the same schema with proposal_kind: "merge" and two parent hashes.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions