orizu 0.5.19 → 0.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/archive-artifacts-cli.d.ts +50 -0
- package/dist/archive-artifacts-cli.d.ts.map +1 -0
- package/dist/artifact-archive.d.ts +48 -0
- package/dist/artifact-archive.d.ts.map +1 -0
- package/dist/artifact-pull.d.ts +22 -0
- package/dist/artifact-pull.d.ts.map +1 -0
- package/dist/banner.d.ts +3 -0
- package/dist/banner.d.ts.map +1 -0
- package/dist/connectors-cli.d.ts +31 -0
- package/dist/connectors-cli.d.ts.map +1 -0
- package/dist/credentials.d.ts +46 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/csv.d.ts +49 -0
- package/dist/csv.d.ts.map +1 -0
- package/dist/dataset-download.d.ts +6 -0
- package/dist/dataset-download.d.ts.map +1 -0
- package/dist/dataset-edit-rows.d.ts +10 -0
- package/dist/dataset-edit-rows.d.ts.map +1 -0
- package/dist/dataset-upload-snapshot.d.ts +13 -0
- package/dist/dataset-upload-snapshot.d.ts.map +1 -0
- package/dist/daytona-slice-rehearsal.d.ts +89 -0
- package/dist/daytona-slice-rehearsal.d.ts.map +1 -0
- package/dist/daytona-slice.d.ts +106 -0
- package/dist/daytona-slice.d.ts.map +1 -0
- package/dist/deploy-key-merge-core.d.ts +121 -0
- package/dist/deploy-key-merge-core.d.ts.map +1 -0
- package/dist/diff-comments-cli.d.ts +12 -0
- package/dist/diff-comments-cli.d.ts.map +1 -0
- package/dist/diff-comments-cli.js +21 -2
- package/dist/egress-allowlist-cli.d.ts +23 -0
- package/dist/egress-allowlist-cli.d.ts.map +1 -0
- package/dist/egress-policy.d.ts +108 -0
- package/dist/egress-policy.d.ts.map +1 -0
- package/dist/error-response.d.ts +9 -0
- package/dist/error-response.d.ts.map +1 -0
- package/dist/file-parser.d.ts +5 -0
- package/dist/file-parser.d.ts.map +1 -0
- package/dist/gepa-engine-dispatch.d.ts +9 -0
- package/dist/gepa-engine-dispatch.d.ts.map +1 -0
- package/dist/gepa-engine-dispatch.js +307 -0
- package/dist/gepa-python-command.d.ts +6 -0
- package/dist/gepa-python-command.d.ts.map +1 -0
- package/dist/gepa-python-command.js +26 -0
- package/dist/gepa-python-paths.d.ts +6 -0
- package/dist/gepa-python-paths.d.ts.map +1 -0
- package/dist/gepa-python-paths.js +42 -0
- package/dist/git-credential.d.ts +69 -0
- package/dist/git-credential.d.ts.map +1 -0
- package/dist/git-recovery-environment.d.ts +31 -0
- package/dist/git-recovery-environment.d.ts.map +1 -0
- package/dist/github-setup.d.ts +122 -0
- package/dist/github-setup.d.ts.map +1 -0
- package/dist/global-flags.d.ts +12 -0
- package/dist/global-flags.d.ts.map +1 -0
- package/dist/help.d.ts +46 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +93 -25
- package/dist/hosted-boot.d.ts +273 -0
- package/dist/hosted-boot.d.ts.map +1 -0
- package/dist/hosted-bootstrap-rehearsal.d.ts +86 -0
- package/dist/hosted-bootstrap-rehearsal.d.ts.map +1 -0
- package/dist/hosted-bootstrap.d.ts +188 -0
- package/dist/hosted-bootstrap.d.ts.map +1 -0
- package/dist/hosted-broker-server.d.ts +50 -0
- package/dist/hosted-broker-server.d.ts.map +1 -0
- package/dist/hosted-git-http-server.d.ts +36 -0
- package/dist/hosted-git-http-server.d.ts.map +1 -0
- package/dist/hosted-harness-claude.d.ts +195 -0
- package/dist/hosted-harness-claude.d.ts.map +1 -0
- package/dist/hosted-harness-opencode.d.ts +227 -0
- package/dist/hosted-harness-opencode.d.ts.map +1 -0
- package/dist/hosted-harness.d.ts +118 -0
- package/dist/hosted-harness.d.ts.map +1 -0
- package/dist/hosted-harvest.d.ts +68 -0
- package/dist/hosted-harvest.d.ts.map +1 -0
- package/dist/hosted-headless.d.ts +72 -0
- package/dist/hosted-headless.d.ts.map +1 -0
- package/dist/hosted-loop-lifecycle.d.ts +206 -0
- package/dist/hosted-loop-lifecycle.d.ts.map +1 -0
- package/dist/hosted-loop.d.ts +170 -0
- package/dist/hosted-loop.d.ts.map +1 -0
- package/dist/hosted-run-event-sink.d.ts +222 -0
- package/dist/hosted-run-event-sink.d.ts.map +1 -0
- package/dist/hosted-runtime-assets.d.ts +186 -0
- package/dist/hosted-runtime-assets.d.ts.map +1 -0
- package/dist/hosted-session-cli.d.ts +258 -0
- package/dist/hosted-session-cli.d.ts.map +1 -0
- package/dist/hosted-skill-staging.d.ts +73 -0
- package/dist/hosted-skill-staging.d.ts.map +1 -0
- package/dist/http.d.ts +8 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +119 -129
- package/dist/instruction-set-gepa-launch.d.ts +3 -0
- package/dist/instruction-set-gepa-launch.d.ts.map +1 -0
- package/dist/instruction-set-gepa-launch.js +15 -0
- package/dist/instruction-set-loader.d.ts +6 -0
- package/dist/instruction-set-loader.d.ts.map +1 -0
- package/dist/instruction-set-loader.js +59 -0
- package/dist/instruction-set-manifest.d.ts +14 -0
- package/dist/instruction-set-manifest.d.ts.map +1 -0
- package/dist/instruction-set-manifest.js +162 -0
- package/dist/instruction-sets-cli.d.ts +40 -0
- package/dist/instruction-sets-cli.d.ts.map +1 -0
- package/dist/instruction-sets-cli.js +361 -0
- package/dist/json-response.d.ts +4 -0
- package/dist/json-response.d.ts.map +1 -0
- package/dist/jsonl-stream.d.ts +8 -0
- package/dist/jsonl-stream.d.ts.map +1 -0
- package/dist/kill-switch-cli.d.ts +30 -0
- package/dist/kill-switch-cli.d.ts.map +1 -0
- package/dist/loopback-serve.d.ts +25 -0
- package/dist/loopback-serve.d.ts.map +1 -0
- package/dist/manifests-cli.d.ts +22 -0
- package/dist/manifests-cli.d.ts.map +1 -0
- package/dist/markdown-report-input.d.ts +7 -0
- package/dist/markdown-report-input.d.ts.map +1 -0
- package/dist/markdown-report-input.js +15 -1
- package/dist/merge-job.d.ts +155 -0
- package/dist/merge-job.d.ts.map +1 -0
- package/dist/model-configs-cli.d.ts +19 -0
- package/dist/model-configs-cli.d.ts.map +1 -0
- package/dist/model-configs-cli.js +104 -0
- package/dist/optimization-export-cli.d.ts +19 -0
- package/dist/optimization-export-cli.d.ts.map +1 -0
- package/dist/optimizations-list-cli.d.ts +20 -0
- package/dist/optimizations-list-cli.d.ts.map +1 -0
- package/dist/optimizations-promote-cli.d.ts +8 -0
- package/dist/optimizations-promote-cli.d.ts.map +1 -0
- package/dist/optimizations-promote-cli.js +34 -0
- package/dist/option-validation.d.ts +15 -0
- package/dist/option-validation.d.ts.map +1 -0
- package/dist/preview-runtime.d.ts +63 -0
- package/dist/preview-runtime.d.ts.map +1 -0
- package/dist/prompt-draft-push.d.ts +60 -0
- package/dist/prompt-draft-push.d.ts.map +1 -0
- package/dist/prompt-label-cli.d.ts +11 -0
- package/dist/prompt-label-cli.d.ts.map +1 -0
- package/dist/prompt-label-cli.js +29 -0
- package/dist/prompt-length-wire.d.ts +28 -0
- package/dist/prompt-length-wire.d.ts.map +1 -0
- package/dist/prompt-length-wire.js +2 -2
- package/dist/prompt-report-cli.d.ts +10 -0
- package/dist/prompt-report-cli.d.ts.map +1 -0
- package/dist/prompt-report-cli.js +2 -2
- package/dist/prompt-summary-table.d.ts +15 -0
- package/dist/prompt-summary-table.d.ts.map +1 -0
- package/dist/prompt-summary-table.js +6 -0
- package/dist/registration-tag-warning.d.ts +32 -0
- package/dist/registration-tag-warning.d.ts.map +1 -0
- package/dist/report-comments-cli.d.ts +16 -0
- package/dist/report-comments-cli.d.ts.map +1 -0
- package/dist/report-markdown-blankness.d.ts +2 -0
- package/dist/report-markdown-blankness.d.ts.map +1 -0
- package/dist/report-markdown-blankness.js +6 -0
- package/dist/report-replacement-warning.d.ts +17 -0
- package/dist/report-replacement-warning.d.ts.map +1 -0
- package/dist/report-replacement-warning.js +31 -0
- package/dist/repository-backup-core.d.ts +225 -0
- package/dist/repository-backup-core.d.ts.map +1 -0
- package/dist/repository-recovery-manifest.d.ts +83 -0
- package/dist/repository-recovery-manifest.d.ts.map +1 -0
- package/dist/run-event-digest.d.ts +74 -0
- package/dist/run-event-digest.d.ts.map +1 -0
- package/dist/runner-dir-verify.d.ts +66 -0
- package/dist/runner-dir-verify.d.ts.map +1 -0
- package/dist/runner-instruction-set-contract.d.ts +24 -0
- package/dist/runner-instruction-set-contract.d.ts.map +1 -0
- package/dist/runner-instruction-set-contract.js +50 -0
- package/dist/sandbox-hygiene.d.ts +45 -0
- package/dist/sandbox-hygiene.d.ts.map +1 -0
- package/dist/sandbox-provider.d.ts +193 -0
- package/dist/sandbox-provider.d.ts.map +1 -0
- package/dist/scorer-draft-push.d.ts +50 -0
- package/dist/scorer-draft-push.d.ts.map +1 -0
- package/dist/scorers-cli.d.ts +20 -0
- package/dist/scorers-cli.d.ts.map +1 -0
- package/dist/scorers-cli.js +22 -0
- package/dist/scorers-verify-parity-bridge.d.ts +16 -0
- package/dist/scorers-verify-parity-bridge.d.ts.map +1 -0
- package/dist/scorers-verify-parity-bridge.js +90 -0
- package/dist/scorers-verify-parity.d.ts +81 -0
- package/dist/scorers-verify-parity.d.ts.map +1 -0
- package/dist/scorers-verify-parity.js +502 -0
- package/dist/scores-accept-cli.d.ts +21 -0
- package/dist/scores-accept-cli.d.ts.map +1 -0
- package/dist/secret-redaction.d.ts +47 -0
- package/dist/secret-redaction.d.ts.map +1 -0
- package/dist/session-timeline.d.ts +54 -0
- package/dist/session-timeline.d.ts.map +1 -0
- package/dist/setup-prompt.d.ts +5 -0
- package/dist/setup-prompt.d.ts.map +1 -0
- package/dist/skill-installer.d.ts +68 -0
- package/dist/skill-installer.d.ts.map +1 -0
- package/dist/skilled-proposer-launch.d.ts +7 -0
- package/dist/skilled-proposer-launch.d.ts.map +1 -0
- package/dist/skilled-proposer-launch.js +60 -0
- package/dist/smoke.d.ts +102 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/start-intent.d.ts +12 -0
- package/dist/start-intent.d.ts.map +1 -0
- package/dist/task-assignment-manifest.d.ts +15 -0
- package/dist/task-assignment-manifest.d.ts.map +1 -0
- package/dist/task-create-error.d.ts +18 -0
- package/dist/task-create-error.d.ts.map +1 -0
- package/dist/team-connectors-cli.d.ts +29 -0
- package/dist/team-connectors-cli.d.ts.map +1 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/vercel-sandbox-provider.d.ts +156 -0
- package/dist/vercel-sandbox-provider.d.ts.map +1 -0
- package/dist/workbench-cli.d.ts +233 -0
- package/dist/workbench-cli.d.ts.map +1 -0
- package/dist/workspace-sync.d.ts +207 -0
- package/dist/workspace-sync.d.ts.map +1 -0
- package/dist/workspace.d.ts +92 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/zip-draft-push.d.ts +55 -0
- package/dist/zip-draft-push.d.ts.map +1 -0
- package/package.json +15 -2
- package/requirements/README.md +138 -0
- package/requirements/skilled-proposer.in +66 -0
- package/requirements/skilled-proposer.lock +265 -0
- package/scripts/build-cli-dist.mjs +30 -0
- package/scripts/ensure-skilled-proposer-venv.mjs +5 -0
- package/scripts/orizu-source-checkout.d.mts +1 -0
- package/scripts/orizu-source-checkout.mjs +10 -0
- package/scripts/refresh-skilled-proposer-lock.mjs +256 -0
- package/scripts/vendor-gepa-python.mjs +187 -4
- package/src/diff-comments-cli.ts +41 -3
- package/src/gepa-engine-dispatch.ts +340 -0
- package/src/gepa-python-command.ts +31 -0
- package/src/gepa-python-paths.ts +56 -0
- package/src/help.ts +95 -25
- package/src/index.ts +131 -166
- package/src/instruction-set-gepa-launch.ts +28 -0
- package/src/instruction-set-loader.ts +37 -0
- package/src/instruction-set-manifest.ts +120 -0
- package/src/instruction-sets-cli.ts +306 -0
- package/src/markdown-report-input.ts +19 -1
- package/src/model-configs-cli.ts +119 -0
- package/src/optimizations-promote-cli.ts +41 -0
- package/src/prompt-label-cli.ts +55 -0
- package/src/prompt-length-wire.ts +1 -1
- package/src/prompt-report-cli.ts +2 -2
- package/src/prompt-summary-table.ts +12 -0
- package/src/report-markdown-blankness.ts +6 -0
- package/src/report-replacement-warning.ts +35 -0
- package/src/runner-instruction-set-contract.ts +64 -0
- package/src/scorers-cli.ts +40 -0
- package/src/scorers-verify-parity-bridge.ts +90 -0
- package/src/scorers-verify-parity.ts +564 -0
- package/src/skilled-proposer-launch.ts +89 -0
- package/src/skilled-proposer-venv-manager.mjs +757 -0
- package/tsconfig.json +2 -0
- package/vendor/gepa-python/LICENSE +7 -0
- package/vendor/gepa-python/manifest.json +107 -0
- package/vendor/gepa-python/src/gepa/__init__.py +22 -0
- package/vendor/gepa-python/src/gepa/adapters/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/__init__.py +1 -0
- package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
- package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/__init__.py +26 -0
- package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/confidence_adapter.py +618 -0
- package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/scoring.py +206 -0
- package/vendor/gepa-python/src/gepa/adapters/default_adapter/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/adapters/default_adapter/default_adapter.py +206 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/dspy_adapter.py +525 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/instruction_proposal.py +500 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/__init__.py +6 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
- package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/full_program_adapter.py +269 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/__init__.py +56 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
- package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
- package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/__init__.py +11 -0
- package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/langchain_adapter.py +261 -0
- package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/__init__.py +37 -0
- package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_adapter.py +693 -0
- package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_client.py +364 -0
- package/vendor/gepa-python/src/gepa/adapters/optimize_anything_adapter/optimize_anything_adapter.py +879 -0
- package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
- package/vendor/gepa-python/src/gepa/api.py +468 -0
- package/vendor/gepa-python/src/gepa/core/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/core/adapter.py +227 -0
- package/vendor/gepa-python/src/gepa/core/callbacks.py +551 -0
- package/vendor/gepa-python/src/gepa/core/data_loader.py +74 -0
- package/vendor/gepa-python/src/gepa/core/engine.py +1025 -0
- package/vendor/gepa-python/src/gepa/core/result.py +271 -0
- package/vendor/gepa-python/src/gepa/core/state.py +706 -0
- package/vendor/gepa-python/src/gepa/examples/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/examples/aime.py +24 -0
- package/vendor/gepa-python/src/gepa/examples/anymaths-bench/eval_default.py +111 -0
- package/vendor/gepa-python/src/gepa/examples/anymaths-bench/train_anymaths.py +177 -0
- package/vendor/gepa-python/src/gepa/examples/mcp_adapter/__init__.py +4 -0
- package/vendor/gepa-python/src/gepa/examples/mcp_adapter/mcp_optimization_example.py +456 -0
- package/vendor/gepa-python/src/gepa/examples/rag_adapter/__init__.py +9 -0
- package/vendor/gepa-python/src/gepa/examples/rag_adapter/rag_optimization.py +824 -0
- package/vendor/gepa-python/src/gepa/examples/terminal-bench/train_terminus.py +161 -0
- package/vendor/gepa-python/src/gepa/gepa_utils.py +117 -0
- package/vendor/gepa-python/src/gepa/gskill/__init__.py +1 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/cost_tracker.py +194 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code.py +1117 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code_skills.py +613 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/mini_swe_agent.py +440 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/experiment_logger.py +367 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/swe_fitness_fn.py +209 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/swe_harness.py +352 -0
- package/vendor/gepa-python/src/gepa/gskill/gskill/train_optimize_anything.py +830 -0
- package/vendor/gepa-python/src/gepa/image.py +104 -0
- package/vendor/gepa-python/src/gepa/lm.py +261 -0
- package/vendor/gepa-python/src/gepa/logging/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/logging/experiment_tracker.py +457 -0
- package/vendor/gepa-python/src/gepa/logging/logger.py +75 -0
- package/vendor/gepa-python/src/gepa/logging/utils.py +130 -0
- package/vendor/gepa-python/src/gepa/optimize_anything.py +1701 -0
- package/vendor/gepa-python/src/gepa/proposer/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/proposer/base.py +53 -0
- package/vendor/gepa-python/src/gepa/proposer/merge.py +403 -0
- package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/base.py +64 -0
- package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflection_lm.py +178 -0
- package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflective_mutation.py +641 -0
- package/vendor/gepa-python/src/gepa/py.typed +0 -0
- package/vendor/gepa-python/src/gepa/strategies/__init__.py +0 -0
- package/vendor/gepa-python/src/gepa/strategies/acceptance.py +66 -0
- package/vendor/gepa-python/src/gepa/strategies/batch_sampler.py +103 -0
- package/vendor/gepa-python/src/gepa/strategies/candidate_selector.py +82 -0
- package/vendor/gepa-python/src/gepa/strategies/component_selector.py +36 -0
- package/vendor/gepa-python/src/gepa/strategies/eval_policy.py +64 -0
- package/vendor/gepa-python/src/gepa/strategies/instruction_proposal.py +153 -0
- package/vendor/gepa-python/src/gepa/strategies/proposal_sampling.py +128 -0
- package/vendor/gepa-python/src/gepa/strategies/proposal_selection.py +84 -0
- package/vendor/gepa-python/src/gepa/utils/__init__.py +63 -0
- package/vendor/gepa-python/src/gepa/utils/code_execution.py +626 -0
- package/vendor/gepa-python/src/gepa/utils/stdio_capture.py +145 -0
- package/vendor/gepa-python/src/gepa/utils/stop_condition.py +230 -0
- package/vendor/gepa-python/src/gepa/visualization.py +359 -0
- package/vendor/orizu-gepa/pyproject.toml +18 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/__init__.py +9 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/__main__.py +5 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/adapter.py +138 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +666 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/cli.py +7 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +269 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +52 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/preflight.py +64 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +186 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +721 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +381 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/stop_conditions.py +55 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +203 -0
- package/vendor/orizu-gepa-python/pyproject.toml +1 -1
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +52 -7
- package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +52 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +10 -5
- package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +323 -68
- package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +207 -13
- package/vendor/skills/orizu-cli/SKILL.md +3 -1
- package/vendor/skills/orizu-cli/references/cli-reference.md +85 -1
- package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +303 -0
- package/vendor/skills/orizu-cli/references/optimization-reports.md +1 -1
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +26 -1
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +113 -10
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
# Copyright (c) 2025 Lakshya A Agrawal and the GEPA contributors
|
|
2
|
+
# https://github.com/gepa-ai/gepa
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import traceback
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import Any, Generic
|
|
8
|
+
|
|
9
|
+
from gepa.core.adapter import (
|
|
10
|
+
DataInst,
|
|
11
|
+
EvaluationBatch,
|
|
12
|
+
GEPAAdapter,
|
|
13
|
+
RolloutOutput,
|
|
14
|
+
Trajectory,
|
|
15
|
+
default_batch_evaluate,
|
|
16
|
+
)
|
|
17
|
+
from gepa.core.callbacks import (
|
|
18
|
+
BudgetUpdatedEvent,
|
|
19
|
+
CandidateAcceptedEvent,
|
|
20
|
+
CandidateRejectedEvent,
|
|
21
|
+
ErrorEvent,
|
|
22
|
+
GEPACallback,
|
|
23
|
+
IterationEndEvent,
|
|
24
|
+
IterationStartEvent,
|
|
25
|
+
MergeAcceptedEvent,
|
|
26
|
+
MergeAttemptedEvent,
|
|
27
|
+
MergeRejectedEvent,
|
|
28
|
+
OptimizationEndEvent,
|
|
29
|
+
OptimizationStartEvent,
|
|
30
|
+
ParetoFrontUpdatedEvent,
|
|
31
|
+
StateSavedEvent,
|
|
32
|
+
ValsetEvaluatedEvent,
|
|
33
|
+
notify_callbacks,
|
|
34
|
+
)
|
|
35
|
+
from gepa.core.data_loader import DataId, DataLoader, ensure_loader
|
|
36
|
+
from gepa.core.state import EvaluationCache, FrontierType, GEPAState, ValsetEvaluation, initialize_gepa_state
|
|
37
|
+
from gepa.logging.experiment_tracker import ExperimentTracker
|
|
38
|
+
from gepa.logging.logger import LoggerProtocol
|
|
39
|
+
from gepa.logging.utils import log_detailed_metrics_after_discovering_new_program
|
|
40
|
+
from gepa.proposer.base import CandidateProposal
|
|
41
|
+
from gepa.proposer.merge import MergeProposer
|
|
42
|
+
from gepa.proposer.reflective_mutation.reflective_mutation import ReflectiveMutationProposer
|
|
43
|
+
from gepa.strategies.acceptance import AcceptanceCriterion, ImprovementOrEqualAcceptance, StrictImprovementAcceptance
|
|
44
|
+
from gepa.strategies.eval_policy import EvaluationPolicy, FullEvaluationPolicy
|
|
45
|
+
from gepa.strategies.proposal_selection import AllImprovements, SelectionStrategy
|
|
46
|
+
from gepa.utils import StopperProtocol
|
|
47
|
+
|
|
48
|
+
# Import tqdm for progress bar functionality
|
|
49
|
+
try:
|
|
50
|
+
from tqdm import tqdm
|
|
51
|
+
except ImportError:
|
|
52
|
+
tqdm = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _MemoizedAcceptance:
|
|
56
|
+
"""Wraps an AcceptanceCriterion so each proposal is judged EXACTLY once per
|
|
57
|
+
engine batch, no matter how many times the selection strategy and the
|
|
58
|
+
engine's rejection classification consult it. Stateful or stochastic
|
|
59
|
+
criteria therefore see one call per proposal (the pre-#329 contract) and
|
|
60
|
+
verdicts stay consistent between selection and rejection reporting."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, criterion):
|
|
63
|
+
self._criterion = criterion
|
|
64
|
+
self._verdicts: dict[int, bool] = {}
|
|
65
|
+
|
|
66
|
+
def should_accept(self, proposal, state) -> bool:
|
|
67
|
+
key = id(proposal)
|
|
68
|
+
if key not in self._verdicts:
|
|
69
|
+
self._verdicts[key] = self._criterion.should_accept(proposal, state)
|
|
70
|
+
return self._verdicts[key]
|
|
71
|
+
|
|
72
|
+
def __getattr__(self, name):
|
|
73
|
+
return getattr(self._criterion, name)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class GEPAEngine(Generic[DataId, DataInst, Trajectory, RolloutOutput]):
|
|
77
|
+
"""Orchestrates the optimization loop using pluggable candidate proposers."""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
adapter: GEPAAdapter[DataInst, Trajectory, RolloutOutput],
|
|
82
|
+
run_dir: str | None,
|
|
83
|
+
valset: list[DataInst] | DataLoader[DataId, DataInst] | None,
|
|
84
|
+
seed_candidate: dict[str, str],
|
|
85
|
+
# Controls
|
|
86
|
+
perfect_score: float | None,
|
|
87
|
+
seed: int,
|
|
88
|
+
# Strategies and helpers
|
|
89
|
+
reflective_proposer: ReflectiveMutationProposer,
|
|
90
|
+
merge_proposer: MergeProposer | None,
|
|
91
|
+
frontier_type: FrontierType,
|
|
92
|
+
# Logging
|
|
93
|
+
logger: LoggerProtocol,
|
|
94
|
+
experiment_tracker: ExperimentTracker,
|
|
95
|
+
# Callbacks
|
|
96
|
+
callbacks: list[GEPACallback] | None = None,
|
|
97
|
+
# Optional parameters
|
|
98
|
+
track_best_outputs: bool = False,
|
|
99
|
+
display_progress_bar: bool = False,
|
|
100
|
+
raise_on_exception: bool = True,
|
|
101
|
+
use_cloudpickle: bool = False,
|
|
102
|
+
# Budget and Stop Condition
|
|
103
|
+
stop_callback: StopperProtocol | None = None,
|
|
104
|
+
val_evaluation_policy: EvaluationPolicy[DataId, DataInst] | None = None,
|
|
105
|
+
# Acceptance criterion + selection strategy for reflective mutation proposals
|
|
106
|
+
acceptance_criterion: AcceptanceCriterion | None = None,
|
|
107
|
+
selection_strategy: SelectionStrategy | None = None,
|
|
108
|
+
# Evaluation caching (stored in state, passed here for initialization)
|
|
109
|
+
evaluation_cache: EvaluationCache[RolloutOutput, DataId] | None = None,
|
|
110
|
+
):
|
|
111
|
+
self.logger = logger
|
|
112
|
+
self.run_dir = run_dir
|
|
113
|
+
self.callbacks = callbacks
|
|
114
|
+
|
|
115
|
+
# Graceful stopping mechanism
|
|
116
|
+
self._stop_requested = False
|
|
117
|
+
|
|
118
|
+
# Set up stopping mechanism
|
|
119
|
+
self.stop_callback = stop_callback
|
|
120
|
+
self.adapter = adapter
|
|
121
|
+
|
|
122
|
+
# Store cache reference for state initialization (actual cache lives in GEPAState)
|
|
123
|
+
self._initial_evaluation_cache = evaluation_cache
|
|
124
|
+
|
|
125
|
+
def evaluator(
|
|
126
|
+
batch: list[DataInst], program: dict[str, str]
|
|
127
|
+
) -> tuple[list[RolloutOutput], list[float], Sequence[dict[str, float]] | None]:
|
|
128
|
+
eval_result = adapter.evaluate(batch, program, capture_traces=False)
|
|
129
|
+
return eval_result.outputs, eval_result.scores, eval_result.objective_scores
|
|
130
|
+
|
|
131
|
+
self.evaluator = evaluator
|
|
132
|
+
|
|
133
|
+
self.valset = ensure_loader(valset) if valset is not None else None
|
|
134
|
+
self.seed_candidate = seed_candidate
|
|
135
|
+
|
|
136
|
+
self.perfect_score = perfect_score
|
|
137
|
+
self.seed = seed
|
|
138
|
+
self.experiment_tracker = experiment_tracker
|
|
139
|
+
|
|
140
|
+
self.reflective_proposer = reflective_proposer
|
|
141
|
+
self.merge_proposer = merge_proposer
|
|
142
|
+
self.frontier_type: FrontierType = frontier_type
|
|
143
|
+
|
|
144
|
+
# Merge scheduling flags (mirroring previous behavior)
|
|
145
|
+
if self.merge_proposer is not None:
|
|
146
|
+
self.merge_proposer.last_iter_found_new_program = False
|
|
147
|
+
|
|
148
|
+
self.acceptance_criterion: AcceptanceCriterion = acceptance_criterion or StrictImprovementAcceptance()
|
|
149
|
+
self.selection_strategy: SelectionStrategy = selection_strategy or AllImprovements()
|
|
150
|
+
self.track_best_outputs = track_best_outputs
|
|
151
|
+
self.display_progress_bar = display_progress_bar
|
|
152
|
+
self.use_cloudpickle = use_cloudpickle
|
|
153
|
+
|
|
154
|
+
self.raise_on_exception = raise_on_exception
|
|
155
|
+
self.val_evaluation_policy: EvaluationPolicy[DataId, DataInst] = (
|
|
156
|
+
val_evaluation_policy if val_evaluation_policy is not None else FullEvaluationPolicy()
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _sync_adapter_state_to_state(self, state: GEPAState) -> None:
|
|
160
|
+
"""Snapshot adapter state into GEPAState before saving.
|
|
161
|
+
|
|
162
|
+
No-op if the adapter does not implement ``get_adapter_state``.
|
|
163
|
+
Makes a shallow copy to avoid mutations between snapshot and save.
|
|
164
|
+
"""
|
|
165
|
+
getter = getattr(self.adapter, "get_adapter_state", None)
|
|
166
|
+
if getter is not None:
|
|
167
|
+
state.adapter_state = dict(getter())
|
|
168
|
+
|
|
169
|
+
def _sync_state_to_adapter(self, state: GEPAState) -> None:
|
|
170
|
+
"""Restore persisted adapter state into the adapter after loading.
|
|
171
|
+
|
|
172
|
+
No-op if the adapter does not implement ``set_adapter_state``.
|
|
173
|
+
"""
|
|
174
|
+
setter = getattr(self.adapter, "set_adapter_state", None)
|
|
175
|
+
if setter is not None:
|
|
176
|
+
setter(state.adapter_state)
|
|
177
|
+
|
|
178
|
+
def _evaluate_programs_on_valset(
|
|
179
|
+
self,
|
|
180
|
+
programs: list[dict[str, str]],
|
|
181
|
+
state: GEPAState[RolloutOutput, DataId],
|
|
182
|
+
) -> list[tuple[ValsetEvaluation[RolloutOutput, DataId], int]]:
|
|
183
|
+
"""Evaluate candidates on the valset, read-only, via ``adapter.batch_evaluate``.
|
|
184
|
+
|
|
185
|
+
Cache-miss examples across all candidates go out in one batched call, so
|
|
186
|
+
parallelism (if any) is the adapter's. Honors the eval cache and
|
|
187
|
+
``val_evaluation_policy``. Returns each evaluation with its metric-call
|
|
188
|
+
count (cache misses); the budget is incremented later in
|
|
189
|
+
:meth:`_add_evaluated_program`, not here.
|
|
190
|
+
"""
|
|
191
|
+
valset = self.valset
|
|
192
|
+
assert valset is not None
|
|
193
|
+
cache = state.evaluation_cache
|
|
194
|
+
|
|
195
|
+
# 1) Per program: split the requested val examples into cached vs. to-evaluate.
|
|
196
|
+
cached_per: list[dict[Any, Any]] = []
|
|
197
|
+
todo_per: list[list[Any]] = []
|
|
198
|
+
for program in programs:
|
|
199
|
+
val_ids = list(self.val_evaluation_policy.get_eval_batch(valset, state))
|
|
200
|
+
if cache is not None:
|
|
201
|
+
cached, uncached = cache.get_batch(program, val_ids)
|
|
202
|
+
else:
|
|
203
|
+
cached, uncached = {}, val_ids
|
|
204
|
+
cached_per.append(cached)
|
|
205
|
+
todo_per.append(uncached)
|
|
206
|
+
|
|
207
|
+
# 2) One adapter call over every (program, cache-miss examples) pair.
|
|
208
|
+
eval_idxs = [i for i, todo in enumerate(todo_per) if todo]
|
|
209
|
+
items = [(programs[i], valset.fetch(todo_per[i])) for i in eval_idxs]
|
|
210
|
+
fresh = self._batch_evaluate(items) if items else []
|
|
211
|
+
fresh_by_idx = dict(zip(eval_idxs, fresh, strict=True))
|
|
212
|
+
|
|
213
|
+
# 3) Merge cached + fresh per program, repopulating the cache.
|
|
214
|
+
results: list[tuple[ValsetEvaluation[RolloutOutput, DataId], int]] = []
|
|
215
|
+
for i, program in enumerate(programs):
|
|
216
|
+
outputs_by: dict[Any, Any] = {}
|
|
217
|
+
scores_by: dict[Any, float] = {}
|
|
218
|
+
objective_by: dict[Any, Any] | None = None
|
|
219
|
+
for eid, entry in cached_per[i].items():
|
|
220
|
+
outputs_by[eid] = entry.output
|
|
221
|
+
scores_by[eid] = entry.score
|
|
222
|
+
if entry.objective_scores is not None:
|
|
223
|
+
objective_by = objective_by or {}
|
|
224
|
+
objective_by[eid] = entry.objective_scores
|
|
225
|
+
|
|
226
|
+
uncached = todo_per[i]
|
|
227
|
+
if uncached:
|
|
228
|
+
eb = fresh_by_idx[i]
|
|
229
|
+
obj = list(eb.objective_scores) if eb.objective_scores else None
|
|
230
|
+
for j, eid in enumerate(uncached):
|
|
231
|
+
outputs_by[eid] = eb.outputs[j]
|
|
232
|
+
scores_by[eid] = eb.scores[j]
|
|
233
|
+
if obj is not None:
|
|
234
|
+
objective_by = objective_by or {}
|
|
235
|
+
objective_by[eid] = obj[j]
|
|
236
|
+
if cache is not None:
|
|
237
|
+
cache.put_batch(program, uncached, eb.outputs, eb.scores, obj)
|
|
238
|
+
|
|
239
|
+
results.append(
|
|
240
|
+
(
|
|
241
|
+
ValsetEvaluation(
|
|
242
|
+
outputs_by_val_id=outputs_by,
|
|
243
|
+
scores_by_val_id=scores_by,
|
|
244
|
+
objective_scores_by_val_id=objective_by,
|
|
245
|
+
),
|
|
246
|
+
len(uncached),
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
return results
|
|
250
|
+
|
|
251
|
+
def _batch_evaluate(self, items: list[tuple[dict[str, str], list]]) -> list[EvaluationBatch]:
|
|
252
|
+
"""Evaluate (candidate, batch) pairs via the adapter's batch_evaluate or fallback."""
|
|
253
|
+
batch_fn = getattr(self.adapter, "batch_evaluate", None)
|
|
254
|
+
if batch_fn is not None:
|
|
255
|
+
return batch_fn(items)
|
|
256
|
+
return default_batch_evaluate(self.adapter, items)
|
|
257
|
+
|
|
258
|
+
def _run_full_eval_and_add(
|
|
259
|
+
self,
|
|
260
|
+
new_program: dict[str, str],
|
|
261
|
+
state: GEPAState[RolloutOutput, DataId],
|
|
262
|
+
parent_program_idx: list[int],
|
|
263
|
+
) -> tuple[int, int]:
|
|
264
|
+
valset_evaluation, num_actual_evals = self._evaluate_programs_on_valset([new_program], state)[0]
|
|
265
|
+
return self._add_evaluated_program(new_program, state, parent_program_idx, valset_evaluation, num_actual_evals)
|
|
266
|
+
|
|
267
|
+
def _add_evaluated_program(
|
|
268
|
+
self,
|
|
269
|
+
new_program: dict[str, str],
|
|
270
|
+
state: GEPAState[RolloutOutput, DataId],
|
|
271
|
+
parent_program_idx: list[int],
|
|
272
|
+
valset_evaluation: ValsetEvaluation[RolloutOutput, DataId],
|
|
273
|
+
num_actual_evals: int,
|
|
274
|
+
) -> tuple[int, int]:
|
|
275
|
+
"""Add an already-evaluated candidate to the pool. Must run sequentially.
|
|
276
|
+
|
|
277
|
+
Snapshots the discovery budget before incrementing so candidates,
|
|
278
|
+
processed in order, record the same ``num_metric_calls_by_discovery``
|
|
279
|
+
as the serial path.
|
|
280
|
+
"""
|
|
281
|
+
num_metric_calls_by_discovery = state.total_num_evals
|
|
282
|
+
state.increment_evals(num_actual_evals)
|
|
283
|
+
state.num_full_ds_evals += 1
|
|
284
|
+
|
|
285
|
+
# Snapshot Pareto front before update
|
|
286
|
+
front_before = state.get_pareto_front_mapping()
|
|
287
|
+
candidates_before: set[int] = set()
|
|
288
|
+
for program_set in front_before.values():
|
|
289
|
+
candidates_before.update(program_set)
|
|
290
|
+
|
|
291
|
+
new_program_idx = state.update_state_with_new_program(
|
|
292
|
+
parent_program_idx=parent_program_idx,
|
|
293
|
+
new_program=new_program,
|
|
294
|
+
valset_evaluation=valset_evaluation,
|
|
295
|
+
run_dir=self.run_dir,
|
|
296
|
+
num_metric_calls_by_discovery_of_new_program=num_metric_calls_by_discovery,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Compute best program immediately after state update (before callbacks)
|
|
300
|
+
# to ensure is_best_program reflects the updated Pareto front
|
|
301
|
+
valset_score = self.val_evaluation_policy.get_valset_score(new_program_idx, state)
|
|
302
|
+
linear_pareto_front_program_idx = self.val_evaluation_policy.get_best_program(state)
|
|
303
|
+
is_best_program = new_program_idx == linear_pareto_front_program_idx
|
|
304
|
+
|
|
305
|
+
# Snapshot Pareto front after update and notify callback
|
|
306
|
+
front_after = state.get_pareto_front_mapping()
|
|
307
|
+
candidates_after: set[int] = set()
|
|
308
|
+
for program_set in front_after.values():
|
|
309
|
+
candidates_after.update(program_set)
|
|
310
|
+
|
|
311
|
+
new_front = sorted(candidates_after)
|
|
312
|
+
displaced_candidates = sorted(candidates_before - candidates_after)
|
|
313
|
+
|
|
314
|
+
notify_callbacks(
|
|
315
|
+
self.callbacks,
|
|
316
|
+
"on_pareto_front_updated",
|
|
317
|
+
ParetoFrontUpdatedEvent(
|
|
318
|
+
iteration=state.i + 1,
|
|
319
|
+
new_front=new_front,
|
|
320
|
+
displaced_candidates=displaced_candidates,
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
state.full_program_trace[-1]["new_program_idx"] = new_program_idx
|
|
325
|
+
state.full_program_trace[-1].setdefault("new_program_indices", []).append(new_program_idx)
|
|
326
|
+
state.full_program_trace[-1]["evaluated_val_indices"] = sorted(valset_evaluation.scores_by_val_id.keys())
|
|
327
|
+
|
|
328
|
+
if is_best_program:
|
|
329
|
+
self.logger.log(f"Iteration {state.i + 1}: Found a better program on the valset with score {valset_score}.")
|
|
330
|
+
|
|
331
|
+
valset = self.valset
|
|
332
|
+
assert valset is not None
|
|
333
|
+
|
|
334
|
+
notify_callbacks(
|
|
335
|
+
self.callbacks,
|
|
336
|
+
"on_valset_evaluated",
|
|
337
|
+
ValsetEvaluatedEvent(
|
|
338
|
+
iteration=state.i + 1,
|
|
339
|
+
candidate_idx=new_program_idx,
|
|
340
|
+
candidate=new_program,
|
|
341
|
+
scores_by_val_id=dict(valset_evaluation.scores_by_val_id),
|
|
342
|
+
average_score=valset_score,
|
|
343
|
+
num_examples_evaluated=len(valset_evaluation.scores_by_val_id),
|
|
344
|
+
total_valset_size=len(valset),
|
|
345
|
+
parent_ids=parent_program_idx,
|
|
346
|
+
is_best_program=is_best_program,
|
|
347
|
+
outputs_by_val_id=(
|
|
348
|
+
dict(valset_evaluation.outputs_by_val_id) if valset_evaluation.outputs_by_val_id else None
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
log_detailed_metrics_after_discovering_new_program(
|
|
354
|
+
logger=self.logger,
|
|
355
|
+
gepa_state=state,
|
|
356
|
+
new_program_idx=new_program_idx,
|
|
357
|
+
valset_evaluation=valset_evaluation,
|
|
358
|
+
objective_scores=state.prog_candidate_objective_scores[new_program_idx],
|
|
359
|
+
experiment_tracker=self.experiment_tracker,
|
|
360
|
+
linear_pareto_front_program_idx=linear_pareto_front_program_idx,
|
|
361
|
+
valset_size=len(valset),
|
|
362
|
+
val_evaluation_policy=self.val_evaluation_policy,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Log candidate table row with instructions and metadata
|
|
366
|
+
component_names = sorted(new_program.keys())
|
|
367
|
+
columns = ["iteration", "candidate_idx", "parent_ids", "valset_score", "is_best"] + [
|
|
368
|
+
f"text:{name}" for name in component_names
|
|
369
|
+
]
|
|
370
|
+
row = [
|
|
371
|
+
state.i + 1,
|
|
372
|
+
new_program_idx,
|
|
373
|
+
str(parent_program_idx),
|
|
374
|
+
valset_score,
|
|
375
|
+
is_best_program,
|
|
376
|
+
] + [new_program[name] for name in component_names]
|
|
377
|
+
self.experiment_tracker.log_table("candidates", columns=columns, data=[row])
|
|
378
|
+
|
|
379
|
+
# Update candidate tree visualization
|
|
380
|
+
self._log_candidate_tree(state)
|
|
381
|
+
|
|
382
|
+
return new_program_idx, linear_pareto_front_program_idx
|
|
383
|
+
|
|
384
|
+
# ------------------------------------------------------------------
|
|
385
|
+
# Reflective proposal acceptance (shared by single and parallel paths)
|
|
386
|
+
# ------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
def _report_rejected_proposal(
|
|
389
|
+
self,
|
|
390
|
+
proposal: CandidateProposal,
|
|
391
|
+
iteration: int,
|
|
392
|
+
state: GEPAState[RolloutOutput, DataId],
|
|
393
|
+
dropped_by_selection: bool = False,
|
|
394
|
+
reason_override: str | None = None,
|
|
395
|
+
) -> None:
|
|
396
|
+
"""Log and fire ``on_candidate_rejected`` for a non-selected proposal.
|
|
397
|
+
|
|
398
|
+
``dropped_by_selection=True`` means the proposal PASSED the acceptance
|
|
399
|
+
criterion but was not picked by the selection strategy (e.g.
|
|
400
|
+
BestImprovement/TopK) — the reason must say so rather than falsely
|
|
401
|
+
claiming a score regression.
|
|
402
|
+
"""
|
|
403
|
+
old_sum = sum(proposal.subsample_scores_before or [])
|
|
404
|
+
new_sum = sum(proposal.subsample_scores_after or [])
|
|
405
|
+
custom_reason = getattr(self.acceptance_criterion, "reject_reason", None)
|
|
406
|
+
if reason_override is not None:
|
|
407
|
+
reject_reason = reason_override
|
|
408
|
+
reject_msg = f"Iteration {iteration}: {reject_reason}, skipping"
|
|
409
|
+
elif dropped_by_selection:
|
|
410
|
+
reject_reason = (
|
|
411
|
+
f"Passed acceptance (score {old_sum} -> {new_sum}) but was not selected "
|
|
412
|
+
f"by the selection strategy ({type(self.selection_strategy).__name__})"
|
|
413
|
+
)
|
|
414
|
+
reject_msg = f"Iteration {iteration}: {reject_reason}, skipping"
|
|
415
|
+
elif custom_reason is not None:
|
|
416
|
+
reject_reason = custom_reason(proposal, state)
|
|
417
|
+
reject_msg = f"Iteration {iteration}: {reject_reason}"
|
|
418
|
+
elif isinstance(self.acceptance_criterion, StrictImprovementAcceptance | ImprovementOrEqualAcceptance):
|
|
419
|
+
reject_msg = (
|
|
420
|
+
f"Iteration {iteration}: New subsample score {new_sum} is not better than old score {old_sum}, skipping"
|
|
421
|
+
)
|
|
422
|
+
reject_reason = f"New subsample score {new_sum} not better than old score {old_sum}"
|
|
423
|
+
else:
|
|
424
|
+
reject_msg = f"Iteration {iteration}: Candidate rejected by acceptance criterion (old_sum={old_sum}, new_sum={new_sum}), skipping"
|
|
425
|
+
reject_reason = f"Candidate rejected by acceptance criterion (old_sum={old_sum}, new_sum={new_sum})"
|
|
426
|
+
self.logger.log(reject_msg)
|
|
427
|
+
self._log_proposal_lm_calls(iteration, proposal, candidate_idx=-1)
|
|
428
|
+
notify_callbacks(
|
|
429
|
+
self.callbacks,
|
|
430
|
+
"on_candidate_rejected",
|
|
431
|
+
CandidateRejectedEvent(
|
|
432
|
+
iteration=iteration,
|
|
433
|
+
old_score=old_sum,
|
|
434
|
+
new_score=new_sum,
|
|
435
|
+
reason=reject_reason,
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def _run_reflective_batch(
|
|
440
|
+
self,
|
|
441
|
+
proposals: list[CandidateProposal],
|
|
442
|
+
state: GEPAState[RolloutOutput, DataId],
|
|
443
|
+
) -> bool:
|
|
444
|
+
"""Gate, select, full-eval, and add a batch of reflective proposals.
|
|
445
|
+
|
|
446
|
+
The selection strategy is the authority over which proposals proceed:
|
|
447
|
+
it receives ALL evaluated proposals together with the acceptance
|
|
448
|
+
criterion. The built-in strategies apply the criterion; a custom
|
|
449
|
+
strategy may deliberately surface a proposal the criterion would
|
|
450
|
+
reject (e.g. exploration picks). Everything not selected fires
|
|
451
|
+
``on_candidate_rejected`` — including acceptance-passing proposals
|
|
452
|
+
dropped by Best/TopK selection, which previously vanished without an
|
|
453
|
+
event. Selected proposals are batch-evaluated on the valset (one
|
|
454
|
+
:meth:`_evaluate_programs_on_valset` call) and added to the pool in
|
|
455
|
+
order. Returns True if any proposal was accepted.
|
|
456
|
+
"""
|
|
457
|
+
iteration = state.i + 1
|
|
458
|
+
|
|
459
|
+
# 1) Selection is authoritative; it sees every evaluated proposal and
|
|
460
|
+
# the acceptance criterion (no engine pre-filter). The criterion is
|
|
461
|
+
# memoized so each proposal is judged exactly once per batch even
|
|
462
|
+
# though both the strategy and the rejection classification below
|
|
463
|
+
# consult it — stateful/stochastic criteria stay coherent.
|
|
464
|
+
criterion = _MemoizedAcceptance(self.acceptance_criterion)
|
|
465
|
+
selected = self.selection_strategy.select(list(proposals), state, criterion)
|
|
466
|
+
|
|
467
|
+
# Defensive: strategies must return proposals FROM the input list.
|
|
468
|
+
input_ids = {id(p) for p in proposals}
|
|
469
|
+
foreign = [p for p in selected if id(p) not in input_ids]
|
|
470
|
+
if foreign:
|
|
471
|
+
self.logger.log(
|
|
472
|
+
f"Iteration {iteration}: selection strategy returned {len(foreign)} object(s) not from "
|
|
473
|
+
"the proposal list; ignoring them (strategies must select from the given proposals)."
|
|
474
|
+
)
|
|
475
|
+
selected = [p for p in selected if id(p) in input_ids]
|
|
476
|
+
|
|
477
|
+
# Deduplicate: identical child candidates selected in the same
|
|
478
|
+
# iteration would each get a full valset eval and a pool entry.
|
|
479
|
+
deduped: list[CandidateProposal] = []
|
|
480
|
+
seen_candidates: set[tuple] = set()
|
|
481
|
+
seen_ids: set[int] = set()
|
|
482
|
+
duplicates: list[CandidateProposal] = []
|
|
483
|
+
for p in selected:
|
|
484
|
+
content_key = tuple(sorted(p.candidate.items()))
|
|
485
|
+
if id(p) in seen_ids or content_key in seen_candidates:
|
|
486
|
+
duplicates.append(p)
|
|
487
|
+
continue
|
|
488
|
+
seen_ids.add(id(p))
|
|
489
|
+
seen_candidates.add(content_key)
|
|
490
|
+
deduped.append(p)
|
|
491
|
+
selected = deduped
|
|
492
|
+
|
|
493
|
+
# 2) Report everything not selected as rejected, distinguishing
|
|
494
|
+
# acceptance failures from selection drops (memoized verdicts keep
|
|
495
|
+
# the reasons truthful at zero extra criterion calls).
|
|
496
|
+
selected_ids = {id(p) for p in selected}
|
|
497
|
+
for proposal in proposals:
|
|
498
|
+
if id(proposal) in selected_ids:
|
|
499
|
+
continue
|
|
500
|
+
if any(proposal is d for d in duplicates):
|
|
501
|
+
self._report_rejected_proposal(
|
|
502
|
+
proposal,
|
|
503
|
+
iteration,
|
|
504
|
+
state,
|
|
505
|
+
reason_override="Duplicate of another candidate selected this iteration",
|
|
506
|
+
)
|
|
507
|
+
continue
|
|
508
|
+
passed_acceptance = criterion.should_accept(proposal, state)
|
|
509
|
+
self._report_rejected_proposal(
|
|
510
|
+
proposal, iteration, state, dropped_by_selection=passed_acceptance
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if not selected:
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
# 3) Full-valset eval of the selected candidates (one batched, read-only call).
|
|
517
|
+
valset_evals = self._evaluate_programs_on_valset([p.candidate for p in selected], state)
|
|
518
|
+
|
|
519
|
+
# 4) Add each selected candidate to the pool, in order.
|
|
520
|
+
any_accepted = False
|
|
521
|
+
for proposal, (valset_evaluation, num_actual_evals) in zip(selected, valset_evals, strict=True):
|
|
522
|
+
new_sum = sum(proposal.subsample_scores_after or [])
|
|
523
|
+
self.logger.log(
|
|
524
|
+
f"Iteration {iteration}: Accepted candidate (subsample score "
|
|
525
|
+
f"{sum(proposal.subsample_scores_before or [])} -> {new_sum}); running full eval."
|
|
526
|
+
)
|
|
527
|
+
new_idx, _ = self._add_evaluated_program(
|
|
528
|
+
new_program=proposal.candidate,
|
|
529
|
+
state=state,
|
|
530
|
+
parent_program_idx=proposal.parent_program_ids,
|
|
531
|
+
valset_evaluation=valset_evaluation,
|
|
532
|
+
num_actual_evals=num_actual_evals,
|
|
533
|
+
)
|
|
534
|
+
self._log_proposal_lm_calls(iteration, proposal, candidate_idx=new_idx)
|
|
535
|
+
notify_callbacks(
|
|
536
|
+
self.callbacks,
|
|
537
|
+
"on_candidate_accepted",
|
|
538
|
+
CandidateAcceptedEvent(
|
|
539
|
+
iteration=iteration,
|
|
540
|
+
new_candidate_idx=new_idx,
|
|
541
|
+
new_score=new_sum,
|
|
542
|
+
parent_ids=proposal.parent_program_ids,
|
|
543
|
+
),
|
|
544
|
+
)
|
|
545
|
+
any_accepted = True
|
|
546
|
+
if self.merge_proposer is not None:
|
|
547
|
+
self.merge_proposer.last_iter_found_new_program = True
|
|
548
|
+
if self.merge_proposer.total_merges_tested < self.merge_proposer.max_merge_invocations:
|
|
549
|
+
self.merge_proposer.merges_due += 1
|
|
550
|
+
|
|
551
|
+
return any_accepted
|
|
552
|
+
|
|
553
|
+
# ------------------------------------------------------------------
|
|
554
|
+
# Main optimization loop
|
|
555
|
+
# ------------------------------------------------------------------
|
|
556
|
+
|
|
557
|
+
def run(self) -> GEPAState[RolloutOutput, DataId]:
|
|
558
|
+
# Check tqdm availability if progress bar is enabled
|
|
559
|
+
progress_bar = None
|
|
560
|
+
if self.display_progress_bar:
|
|
561
|
+
if tqdm is None:
|
|
562
|
+
raise ImportError("tqdm must be installed when display_progress_bar is enabled")
|
|
563
|
+
|
|
564
|
+
# Check if stop_callback contains MaxMetricCallsStopper
|
|
565
|
+
total_calls: int | None = None
|
|
566
|
+
stop_cb = self.stop_callback
|
|
567
|
+
if stop_cb is not None:
|
|
568
|
+
max_calls_attr = getattr(stop_cb, "max_metric_calls", None)
|
|
569
|
+
if isinstance(max_calls_attr, int):
|
|
570
|
+
# Direct MaxMetricCallsStopper
|
|
571
|
+
total_calls = max_calls_attr
|
|
572
|
+
else:
|
|
573
|
+
stoppers = getattr(stop_cb, "stoppers", None)
|
|
574
|
+
if stoppers is not None:
|
|
575
|
+
# CompositeStopper - iterate to find MaxMetricCallsStopper
|
|
576
|
+
for stopper in stoppers:
|
|
577
|
+
stopper_max = getattr(stopper, "max_metric_calls", None)
|
|
578
|
+
if isinstance(stopper_max, int):
|
|
579
|
+
total_calls = stopper_max
|
|
580
|
+
break
|
|
581
|
+
|
|
582
|
+
if total_calls is not None:
|
|
583
|
+
progress_bar = tqdm(total=total_calls, desc="GEPA Optimization", unit="rollouts")
|
|
584
|
+
else:
|
|
585
|
+
progress_bar = tqdm(desc="GEPA Optimization", unit="rollouts")
|
|
586
|
+
progress_bar.update(0)
|
|
587
|
+
|
|
588
|
+
# Prepare valset
|
|
589
|
+
valset = self.valset
|
|
590
|
+
if valset is None:
|
|
591
|
+
raise ValueError("valset must be provided to GEPAEngine.run()")
|
|
592
|
+
|
|
593
|
+
def valset_evaluator(
|
|
594
|
+
program: dict[str, str],
|
|
595
|
+
val_ids: list[Any],
|
|
596
|
+
) -> ValsetEvaluation[RolloutOutput, DataId]:
|
|
597
|
+
outputs, scores, objective_scores = self.evaluator(valset.fetch(val_ids), program)
|
|
598
|
+
outputs_dict = dict(zip(val_ids, outputs, strict=False))
|
|
599
|
+
scores_dict = dict(zip(val_ids, scores, strict=False))
|
|
600
|
+
objective_scores_dict = (
|
|
601
|
+
dict(zip(val_ids, objective_scores, strict=False)) if objective_scores is not None else None
|
|
602
|
+
)
|
|
603
|
+
return ValsetEvaluation(
|
|
604
|
+
outputs_by_val_id=outputs_dict,
|
|
605
|
+
scores_by_val_id=scores_dict,
|
|
606
|
+
objective_scores_by_val_id=objective_scores_dict,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# Notify callbacks of optimization start (before seed valset eval)
|
|
610
|
+
notify_callbacks(
|
|
611
|
+
self.callbacks,
|
|
612
|
+
"on_optimization_start",
|
|
613
|
+
OptimizationStartEvent(
|
|
614
|
+
seed_candidate=self.seed_candidate,
|
|
615
|
+
trainset_size=len(self.reflective_proposer.trainset),
|
|
616
|
+
valset_size=len(valset),
|
|
617
|
+
config={
|
|
618
|
+
"perfect_score": self.perfect_score,
|
|
619
|
+
"seed": self.seed,
|
|
620
|
+
"track_best_outputs": self.track_best_outputs,
|
|
621
|
+
},
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Evaluate seed candidate on valset (after on_optimization_start callback).
|
|
626
|
+
# Policies may narrow the seed evaluation via the optional get_seed_eval_batch
|
|
627
|
+
# hook; the state does not exist yet, so get_eval_batch cannot be used here.
|
|
628
|
+
seed_batch_fn = getattr(self.val_evaluation_policy, "get_seed_eval_batch", None)
|
|
629
|
+
seed_val_ids = list(seed_batch_fn(valset)) if seed_batch_fn is not None else list(valset.all_ids())
|
|
630
|
+
seed_valset_evaluation = valset_evaluator(self.seed_candidate, seed_val_ids)
|
|
631
|
+
|
|
632
|
+
# Initialize state with pre-computed seed evaluation
|
|
633
|
+
state = initialize_gepa_state(
|
|
634
|
+
run_dir=self.run_dir,
|
|
635
|
+
logger=self.logger,
|
|
636
|
+
seed_candidate=self.seed_candidate,
|
|
637
|
+
seed_valset_evaluation=seed_valset_evaluation,
|
|
638
|
+
track_best_outputs=self.track_best_outputs,
|
|
639
|
+
frontier_type=self.frontier_type,
|
|
640
|
+
evaluation_cache=self._initial_evaluation_cache,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
# Restore adapter state from persisted state (only has effect on resume)
|
|
644
|
+
self._sync_state_to_adapter(state)
|
|
645
|
+
|
|
646
|
+
# Log base program score
|
|
647
|
+
# Log run configuration
|
|
648
|
+
self.experiment_tracker.log_config(
|
|
649
|
+
{
|
|
650
|
+
"seed": self.seed,
|
|
651
|
+
"perfect_score": self.perfect_score,
|
|
652
|
+
"frontier_type": self.frontier_type,
|
|
653
|
+
"track_best_outputs": self.track_best_outputs,
|
|
654
|
+
"use_cloudpickle": self.use_cloudpickle,
|
|
655
|
+
"raise_on_exception": self.raise_on_exception,
|
|
656
|
+
"trainset_size": len(self.reflective_proposer.trainset),
|
|
657
|
+
"valset_size": len(valset),
|
|
658
|
+
"seed_candidate_components": sorted(self.seed_candidate.keys()),
|
|
659
|
+
"val_evaluation_policy": type(self.val_evaluation_policy).__name__,
|
|
660
|
+
"has_merge_proposer": self.merge_proposer is not None,
|
|
661
|
+
"run_dir": self.run_dir,
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Log base program score using the same metric names as subsequent iterations
|
|
666
|
+
# so they appear on the same charts in wandb/mlflow
|
|
667
|
+
base_val_avg, base_val_coverage = state.get_program_average_val_subset(0)
|
|
668
|
+
pareto_scores = list(state.pareto_front_valset.values())
|
|
669
|
+
base_pareto_avg = sum(pareto_scores) / len(pareto_scores) if pareto_scores else base_val_avg
|
|
670
|
+
self.experiment_tracker.log_metrics(
|
|
671
|
+
{
|
|
672
|
+
"val_program_average": base_val_avg,
|
|
673
|
+
"best_score_on_valset": base_val_avg,
|
|
674
|
+
"val_evaluated_count_new_program": base_val_coverage,
|
|
675
|
+
"val_total_count": len(valset),
|
|
676
|
+
"total_metric_calls": state.total_num_evals,
|
|
677
|
+
"valset_pareto_front_agg": base_pareto_avg,
|
|
678
|
+
"new_program_idx": 0,
|
|
679
|
+
"linear_pareto_front_program_idx": 0,
|
|
680
|
+
"best_program_as_per_agg_score_valset": 0,
|
|
681
|
+
},
|
|
682
|
+
step=state.i + 1,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
self.logger.log(
|
|
686
|
+
f"Iteration {state.i + 1}: Base program full valset score: {base_val_avg} "
|
|
687
|
+
f"over {base_val_coverage} / {len(valset)} examples"
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
# Notify callbacks of seed candidate's initial valset evaluation (iteration 0)
|
|
691
|
+
# This provides the baseline performance before any optimization
|
|
692
|
+
seed_scores = state.prog_candidate_val_subscores[0]
|
|
693
|
+
notify_callbacks(
|
|
694
|
+
self.callbacks,
|
|
695
|
+
"on_valset_evaluated",
|
|
696
|
+
ValsetEvaluatedEvent(
|
|
697
|
+
iteration=0,
|
|
698
|
+
candidate_idx=0,
|
|
699
|
+
candidate=self.seed_candidate,
|
|
700
|
+
scores_by_val_id=dict(seed_scores),
|
|
701
|
+
average_score=base_val_avg,
|
|
702
|
+
num_examples_evaluated=len(seed_scores),
|
|
703
|
+
total_valset_size=len(valset),
|
|
704
|
+
parent_ids=[],
|
|
705
|
+
is_best_program=True, # Seed is always best at iteration 0
|
|
706
|
+
outputs_by_val_id=None, # Outputs not tracked at initialization unless track_best_outputs=True
|
|
707
|
+
),
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
# Register budget hook to fire on_budget_updated callback in real-time
|
|
711
|
+
def budget_hook(new_total: int, delta: int) -> None:
|
|
712
|
+
notify_callbacks(
|
|
713
|
+
self.callbacks,
|
|
714
|
+
"on_budget_updated",
|
|
715
|
+
BudgetUpdatedEvent(
|
|
716
|
+
iteration=state.i + 1,
|
|
717
|
+
metric_calls_used=new_total,
|
|
718
|
+
metric_calls_delta=delta,
|
|
719
|
+
metric_calls_remaining=self._get_remaining_budget(state),
|
|
720
|
+
),
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
state.add_budget_hook(budget_hook)
|
|
724
|
+
|
|
725
|
+
# Merge scheduling
|
|
726
|
+
if self.merge_proposer is not None:
|
|
727
|
+
self.merge_proposer.last_iter_found_new_program = False
|
|
728
|
+
|
|
729
|
+
# Main loop
|
|
730
|
+
last_pbar_val = 0
|
|
731
|
+
while not self._should_stop(state):
|
|
732
|
+
if self.display_progress_bar and progress_bar is not None:
|
|
733
|
+
delta = state.total_num_evals - last_pbar_val
|
|
734
|
+
progress_bar.update(delta)
|
|
735
|
+
last_pbar_val = state.total_num_evals
|
|
736
|
+
|
|
737
|
+
assert state.is_consistent()
|
|
738
|
+
proposal_accepted = False
|
|
739
|
+
iteration_started = False
|
|
740
|
+
try:
|
|
741
|
+
self._sync_adapter_state_to_state(state)
|
|
742
|
+
state.save(self.run_dir, use_cloudpickle=self.use_cloudpickle)
|
|
743
|
+
notify_callbacks(
|
|
744
|
+
self.callbacks,
|
|
745
|
+
"on_state_saved",
|
|
746
|
+
StateSavedEvent(
|
|
747
|
+
iteration=state.i + 1,
|
|
748
|
+
run_dir=self.run_dir,
|
|
749
|
+
),
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
state.i += 1
|
|
753
|
+
state.full_program_trace.append({"i": state.i})
|
|
754
|
+
|
|
755
|
+
# Notify callbacks of iteration start
|
|
756
|
+
notify_callbacks(
|
|
757
|
+
self.callbacks,
|
|
758
|
+
"on_iteration_start",
|
|
759
|
+
IterationStartEvent(
|
|
760
|
+
iteration=state.i + 1,
|
|
761
|
+
state=state,
|
|
762
|
+
trainset_loader=self.reflective_proposer.trainset,
|
|
763
|
+
),
|
|
764
|
+
)
|
|
765
|
+
iteration_started = True
|
|
766
|
+
|
|
767
|
+
# 1) Attempt merge first if scheduled and last iter found new program
|
|
768
|
+
if self.merge_proposer is not None and self.merge_proposer.use_merge:
|
|
769
|
+
if self.merge_proposer.merges_due > 0 and self.merge_proposer.last_iter_found_new_program:
|
|
770
|
+
proposal = self.merge_proposer.propose(state)
|
|
771
|
+
self.merge_proposer.last_iter_found_new_program = False # old behavior
|
|
772
|
+
|
|
773
|
+
if proposal is not None and proposal.tag == "merge":
|
|
774
|
+
parent_sums = proposal.subsample_scores_before or [
|
|
775
|
+
float("-inf"),
|
|
776
|
+
float("-inf"),
|
|
777
|
+
]
|
|
778
|
+
new_sum = sum(proposal.subsample_scores_after or [])
|
|
779
|
+
|
|
780
|
+
# Notify merge attempted
|
|
781
|
+
notify_callbacks(
|
|
782
|
+
self.callbacks,
|
|
783
|
+
"on_merge_attempted",
|
|
784
|
+
MergeAttemptedEvent(
|
|
785
|
+
iteration=state.i + 1,
|
|
786
|
+
parent_ids=proposal.parent_program_ids,
|
|
787
|
+
merged_candidate=proposal.candidate,
|
|
788
|
+
),
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
if new_sum >= max(parent_sums):
|
|
792
|
+
# ACCEPTED: consume one merge attempt and record it
|
|
793
|
+
new_idx, _ = self._run_full_eval_and_add(
|
|
794
|
+
new_program=proposal.candidate,
|
|
795
|
+
state=state,
|
|
796
|
+
parent_program_idx=proposal.parent_program_ids,
|
|
797
|
+
)
|
|
798
|
+
self.merge_proposer.merges_due -= 1
|
|
799
|
+
self.merge_proposer.total_merges_tested += 1
|
|
800
|
+
proposal_accepted = True
|
|
801
|
+
|
|
802
|
+
# Notify merge accepted
|
|
803
|
+
notify_callbacks(
|
|
804
|
+
self.callbacks,
|
|
805
|
+
"on_merge_accepted",
|
|
806
|
+
MergeAcceptedEvent(
|
|
807
|
+
iteration=state.i + 1,
|
|
808
|
+
new_candidate_idx=new_idx,
|
|
809
|
+
parent_ids=proposal.parent_program_ids,
|
|
810
|
+
),
|
|
811
|
+
)
|
|
812
|
+
notify_callbacks(
|
|
813
|
+
self.callbacks,
|
|
814
|
+
"on_candidate_accepted",
|
|
815
|
+
CandidateAcceptedEvent(
|
|
816
|
+
iteration=state.i + 1,
|
|
817
|
+
new_candidate_idx=new_idx,
|
|
818
|
+
new_score=new_sum,
|
|
819
|
+
parent_ids=proposal.parent_program_ids,
|
|
820
|
+
),
|
|
821
|
+
)
|
|
822
|
+
continue # skip reflective this iteration
|
|
823
|
+
else:
|
|
824
|
+
# REJECTED: do NOT consume merges_due or total_merges_tested
|
|
825
|
+
self.logger.log(
|
|
826
|
+
f"Iteration {state.i + 1}: New program subsample score {new_sum} "
|
|
827
|
+
f"is worse than both parents {parent_sums}, skipping merge"
|
|
828
|
+
)
|
|
829
|
+
# Notify merge rejected
|
|
830
|
+
notify_callbacks(
|
|
831
|
+
self.callbacks,
|
|
832
|
+
"on_merge_rejected",
|
|
833
|
+
MergeRejectedEvent(
|
|
834
|
+
iteration=state.i + 1,
|
|
835
|
+
parent_ids=proposal.parent_program_ids,
|
|
836
|
+
reason=f"Merged score {new_sum} worse than both parents {parent_sums}",
|
|
837
|
+
),
|
|
838
|
+
)
|
|
839
|
+
# Skip reflective this iteration (old behavior)
|
|
840
|
+
continue
|
|
841
|
+
|
|
842
|
+
# Old behavior: regardless of whether we attempted, clear the flag before reflective
|
|
843
|
+
self.merge_proposer.last_iter_found_new_program = False
|
|
844
|
+
|
|
845
|
+
# 2) Reflective mutation proposer
|
|
846
|
+
proposals = self.reflective_proposer.propose(state)
|
|
847
|
+
if not proposals:
|
|
848
|
+
self.logger.log(f"Iteration {state.i + 1}: Reflective mutation did not propose a new candidate")
|
|
849
|
+
continue
|
|
850
|
+
|
|
851
|
+
proposal_accepted = self._run_reflective_batch(proposals, state)
|
|
852
|
+
|
|
853
|
+
except Exception as e:
|
|
854
|
+
self.logger.log(f"Iteration {state.i + 1}: Exception during optimization: {e}")
|
|
855
|
+
self.logger.log(traceback.format_exc())
|
|
856
|
+
# Notify error callback
|
|
857
|
+
notify_callbacks(
|
|
858
|
+
self.callbacks,
|
|
859
|
+
"on_error",
|
|
860
|
+
ErrorEvent(
|
|
861
|
+
iteration=state.i + 1,
|
|
862
|
+
exception=e,
|
|
863
|
+
will_continue=not self.raise_on_exception,
|
|
864
|
+
),
|
|
865
|
+
)
|
|
866
|
+
if self.raise_on_exception:
|
|
867
|
+
raise e
|
|
868
|
+
else:
|
|
869
|
+
continue
|
|
870
|
+
finally:
|
|
871
|
+
# Notify iteration end only if the iteration actually started
|
|
872
|
+
# (i.e., on_iteration_start was called successfully)
|
|
873
|
+
if iteration_started:
|
|
874
|
+
notify_callbacks(
|
|
875
|
+
self.callbacks,
|
|
876
|
+
"on_iteration_end",
|
|
877
|
+
IterationEndEvent(
|
|
878
|
+
iteration=state.i + 1,
|
|
879
|
+
state=state,
|
|
880
|
+
proposal_accepted=proposal_accepted,
|
|
881
|
+
),
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
# Close progress bar if it exists
|
|
885
|
+
if self.display_progress_bar and progress_bar is not None:
|
|
886
|
+
progress_bar.close()
|
|
887
|
+
|
|
888
|
+
self._sync_adapter_state_to_state(state)
|
|
889
|
+
state.save(self.run_dir, use_cloudpickle=self.use_cloudpickle)
|
|
890
|
+
|
|
891
|
+
# Notify optimization end
|
|
892
|
+
best_candidate_idx = self.val_evaluation_policy.get_best_program(state)
|
|
893
|
+
notify_callbacks(
|
|
894
|
+
self.callbacks,
|
|
895
|
+
"on_optimization_end",
|
|
896
|
+
OptimizationEndEvent(
|
|
897
|
+
best_candidate_idx=best_candidate_idx,
|
|
898
|
+
total_iterations=state.i,
|
|
899
|
+
total_metric_calls=state.total_num_evals,
|
|
900
|
+
final_state=state,
|
|
901
|
+
),
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
# Log final summary: seed candidate, best candidate, and all candidates table
|
|
905
|
+
best_candidate = state.program_candidates[best_candidate_idx]
|
|
906
|
+
best_score = self.val_evaluation_policy.get_valset_score(best_candidate_idx, state)
|
|
907
|
+
summary: dict[str, Any] = {
|
|
908
|
+
"best_candidate_idx": best_candidate_idx,
|
|
909
|
+
"best_valset_score": best_score,
|
|
910
|
+
"total_iterations": state.i,
|
|
911
|
+
"total_candidates": len(state.program_candidates),
|
|
912
|
+
}
|
|
913
|
+
for name in sorted(self.seed_candidate.keys()):
|
|
914
|
+
summary[f"seed/{name}"] = self.seed_candidate[name]
|
|
915
|
+
summary[f"best/{name}"] = best_candidate[name]
|
|
916
|
+
self.experiment_tracker.log_summary(summary)
|
|
917
|
+
|
|
918
|
+
return state
|
|
919
|
+
|
|
920
|
+
def _log_proposal_lm_calls(
|
|
921
|
+
self,
|
|
922
|
+
iteration: int,
|
|
923
|
+
proposal: Any,
|
|
924
|
+
candidate_idx: int,
|
|
925
|
+
) -> None:
|
|
926
|
+
"""Log per-component LM prompt / raw-output from a proposal to the experiment tracker.
|
|
927
|
+
|
|
928
|
+
Appends one row per component to the ``"proposals"`` table.
|
|
929
|
+
``candidate_idx`` is the assigned index for accepted proposals,
|
|
930
|
+
or ``-1`` for rejected ones — making it easy to join with the
|
|
931
|
+
``"candidates"`` table in WandB / MLflow.
|
|
932
|
+
"""
|
|
933
|
+
metadata = proposal.metadata or {}
|
|
934
|
+
components = {k.split(":", 1)[1] for k in metadata if k.startswith("prompt:") or k.startswith("raw_lm_output:")}
|
|
935
|
+
if not components:
|
|
936
|
+
return
|
|
937
|
+
|
|
938
|
+
status = "accepted" if candidate_idx >= 0 else "rejected"
|
|
939
|
+
subsample_before = sum(proposal.subsample_scores_before or [])
|
|
940
|
+
subsample_after = sum(proposal.subsample_scores_after or [])
|
|
941
|
+
parent_ids_str = str(proposal.parent_program_ids)
|
|
942
|
+
|
|
943
|
+
rows = []
|
|
944
|
+
for comp in sorted(components):
|
|
945
|
+
prompt = metadata.get(f"prompt:{comp}", "")
|
|
946
|
+
raw_output = metadata.get(f"raw_lm_output:{comp}", "")
|
|
947
|
+
proposed_text = proposal.candidate.get(comp, "")
|
|
948
|
+
rows.append(
|
|
949
|
+
[
|
|
950
|
+
iteration,
|
|
951
|
+
comp,
|
|
952
|
+
status,
|
|
953
|
+
candidate_idx,
|
|
954
|
+
parent_ids_str,
|
|
955
|
+
subsample_before,
|
|
956
|
+
subsample_after,
|
|
957
|
+
prompt if isinstance(prompt, str) else str(prompt),
|
|
958
|
+
raw_output,
|
|
959
|
+
proposed_text,
|
|
960
|
+
]
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
self.experiment_tracker.log_table(
|
|
964
|
+
"proposals",
|
|
965
|
+
columns=[
|
|
966
|
+
"iteration",
|
|
967
|
+
"component",
|
|
968
|
+
"status",
|
|
969
|
+
"candidate_idx",
|
|
970
|
+
"parent_ids",
|
|
971
|
+
"subsample_score_before",
|
|
972
|
+
"subsample_score_after",
|
|
973
|
+
"prompt",
|
|
974
|
+
"raw_lm_output",
|
|
975
|
+
"proposed_text",
|
|
976
|
+
],
|
|
977
|
+
data=rows,
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
def _log_candidate_tree(self, state: GEPAState[RolloutOutput, DataId]) -> None:
|
|
981
|
+
"""Generate and log the candidate tree visualization."""
|
|
982
|
+
try:
|
|
983
|
+
from gepa.visualization import candidate_tree_html
|
|
984
|
+
|
|
985
|
+
html_content = candidate_tree_html(state)
|
|
986
|
+
self.experiment_tracker.log_html(html_content, key="candidate_tree")
|
|
987
|
+
if self.run_dir is not None:
|
|
988
|
+
tree_path = os.path.join(self.run_dir, "candidate_tree.html")
|
|
989
|
+
with open(tree_path, "w") as f:
|
|
990
|
+
f.write(html_content)
|
|
991
|
+
except Exception as e:
|
|
992
|
+
self.logger.log(f"Warning: Failed to generate candidate tree visualization: {e}")
|
|
993
|
+
|
|
994
|
+
def _should_stop(self, state: GEPAState[RolloutOutput, DataId]) -> bool:
|
|
995
|
+
"""Check if the optimization should stop."""
|
|
996
|
+
if self._stop_requested:
|
|
997
|
+
return True
|
|
998
|
+
if self.stop_callback and self.stop_callback(state):
|
|
999
|
+
return True
|
|
1000
|
+
return False
|
|
1001
|
+
|
|
1002
|
+
def _get_remaining_budget(self, state: GEPAState[RolloutOutput, DataId]) -> int | None:
|
|
1003
|
+
"""Get remaining metric calls budget, or None if unlimited."""
|
|
1004
|
+
stop_cb = self.stop_callback
|
|
1005
|
+
if stop_cb is None:
|
|
1006
|
+
return None
|
|
1007
|
+
|
|
1008
|
+
max_calls = getattr(stop_cb, "max_metric_calls", None)
|
|
1009
|
+
if isinstance(max_calls, int):
|
|
1010
|
+
return max(0, max_calls - state.total_num_evals)
|
|
1011
|
+
|
|
1012
|
+
# Check for CompositeStopper
|
|
1013
|
+
stoppers = getattr(stop_cb, "stoppers", None)
|
|
1014
|
+
if stoppers is not None:
|
|
1015
|
+
for stopper in stoppers:
|
|
1016
|
+
stopper_max = getattr(stopper, "max_metric_calls", None)
|
|
1017
|
+
if isinstance(stopper_max, int):
|
|
1018
|
+
return max(0, stopper_max - state.total_num_evals)
|
|
1019
|
+
|
|
1020
|
+
return None
|
|
1021
|
+
|
|
1022
|
+
def request_stop(self) -> None:
|
|
1023
|
+
"""Manually request the optimization to stop gracefully."""
|
|
1024
|
+
self.logger.log("Stop requested manually. Initiating graceful shutdown...")
|
|
1025
|
+
self._stop_requested = True
|