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.
Files changed (378) hide show
  1. package/README.md +23 -0
  2. package/dist/archive-artifacts-cli.d.ts +50 -0
  3. package/dist/archive-artifacts-cli.d.ts.map +1 -0
  4. package/dist/artifact-archive.d.ts +48 -0
  5. package/dist/artifact-archive.d.ts.map +1 -0
  6. package/dist/artifact-pull.d.ts +22 -0
  7. package/dist/artifact-pull.d.ts.map +1 -0
  8. package/dist/banner.d.ts +3 -0
  9. package/dist/banner.d.ts.map +1 -0
  10. package/dist/connectors-cli.d.ts +31 -0
  11. package/dist/connectors-cli.d.ts.map +1 -0
  12. package/dist/credentials.d.ts +46 -0
  13. package/dist/credentials.d.ts.map +1 -0
  14. package/dist/csv.d.ts +49 -0
  15. package/dist/csv.d.ts.map +1 -0
  16. package/dist/dataset-download.d.ts +6 -0
  17. package/dist/dataset-download.d.ts.map +1 -0
  18. package/dist/dataset-edit-rows.d.ts +10 -0
  19. package/dist/dataset-edit-rows.d.ts.map +1 -0
  20. package/dist/dataset-upload-snapshot.d.ts +13 -0
  21. package/dist/dataset-upload-snapshot.d.ts.map +1 -0
  22. package/dist/daytona-slice-rehearsal.d.ts +89 -0
  23. package/dist/daytona-slice-rehearsal.d.ts.map +1 -0
  24. package/dist/daytona-slice.d.ts +106 -0
  25. package/dist/daytona-slice.d.ts.map +1 -0
  26. package/dist/deploy-key-merge-core.d.ts +121 -0
  27. package/dist/deploy-key-merge-core.d.ts.map +1 -0
  28. package/dist/diff-comments-cli.d.ts +12 -0
  29. package/dist/diff-comments-cli.d.ts.map +1 -0
  30. package/dist/diff-comments-cli.js +21 -2
  31. package/dist/egress-allowlist-cli.d.ts +23 -0
  32. package/dist/egress-allowlist-cli.d.ts.map +1 -0
  33. package/dist/egress-policy.d.ts +108 -0
  34. package/dist/egress-policy.d.ts.map +1 -0
  35. package/dist/error-response.d.ts +9 -0
  36. package/dist/error-response.d.ts.map +1 -0
  37. package/dist/file-parser.d.ts +5 -0
  38. package/dist/file-parser.d.ts.map +1 -0
  39. package/dist/gepa-engine-dispatch.d.ts +9 -0
  40. package/dist/gepa-engine-dispatch.d.ts.map +1 -0
  41. package/dist/gepa-engine-dispatch.js +307 -0
  42. package/dist/gepa-python-command.d.ts +6 -0
  43. package/dist/gepa-python-command.d.ts.map +1 -0
  44. package/dist/gepa-python-command.js +26 -0
  45. package/dist/gepa-python-paths.d.ts +6 -0
  46. package/dist/gepa-python-paths.d.ts.map +1 -0
  47. package/dist/gepa-python-paths.js +42 -0
  48. package/dist/git-credential.d.ts +69 -0
  49. package/dist/git-credential.d.ts.map +1 -0
  50. package/dist/git-recovery-environment.d.ts +31 -0
  51. package/dist/git-recovery-environment.d.ts.map +1 -0
  52. package/dist/github-setup.d.ts +122 -0
  53. package/dist/github-setup.d.ts.map +1 -0
  54. package/dist/global-flags.d.ts +12 -0
  55. package/dist/global-flags.d.ts.map +1 -0
  56. package/dist/help.d.ts +46 -0
  57. package/dist/help.d.ts.map +1 -0
  58. package/dist/help.js +93 -25
  59. package/dist/hosted-boot.d.ts +273 -0
  60. package/dist/hosted-boot.d.ts.map +1 -0
  61. package/dist/hosted-bootstrap-rehearsal.d.ts +86 -0
  62. package/dist/hosted-bootstrap-rehearsal.d.ts.map +1 -0
  63. package/dist/hosted-bootstrap.d.ts +188 -0
  64. package/dist/hosted-bootstrap.d.ts.map +1 -0
  65. package/dist/hosted-broker-server.d.ts +50 -0
  66. package/dist/hosted-broker-server.d.ts.map +1 -0
  67. package/dist/hosted-git-http-server.d.ts +36 -0
  68. package/dist/hosted-git-http-server.d.ts.map +1 -0
  69. package/dist/hosted-harness-claude.d.ts +195 -0
  70. package/dist/hosted-harness-claude.d.ts.map +1 -0
  71. package/dist/hosted-harness-opencode.d.ts +227 -0
  72. package/dist/hosted-harness-opencode.d.ts.map +1 -0
  73. package/dist/hosted-harness.d.ts +118 -0
  74. package/dist/hosted-harness.d.ts.map +1 -0
  75. package/dist/hosted-harvest.d.ts +68 -0
  76. package/dist/hosted-harvest.d.ts.map +1 -0
  77. package/dist/hosted-headless.d.ts +72 -0
  78. package/dist/hosted-headless.d.ts.map +1 -0
  79. package/dist/hosted-loop-lifecycle.d.ts +206 -0
  80. package/dist/hosted-loop-lifecycle.d.ts.map +1 -0
  81. package/dist/hosted-loop.d.ts +170 -0
  82. package/dist/hosted-loop.d.ts.map +1 -0
  83. package/dist/hosted-run-event-sink.d.ts +222 -0
  84. package/dist/hosted-run-event-sink.d.ts.map +1 -0
  85. package/dist/hosted-runtime-assets.d.ts +186 -0
  86. package/dist/hosted-runtime-assets.d.ts.map +1 -0
  87. package/dist/hosted-session-cli.d.ts +258 -0
  88. package/dist/hosted-session-cli.d.ts.map +1 -0
  89. package/dist/hosted-skill-staging.d.ts +73 -0
  90. package/dist/hosted-skill-staging.d.ts.map +1 -0
  91. package/dist/http.d.ts +8 -0
  92. package/dist/http.d.ts.map +1 -0
  93. package/dist/index.d.ts +22 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +119 -129
  96. package/dist/instruction-set-gepa-launch.d.ts +3 -0
  97. package/dist/instruction-set-gepa-launch.d.ts.map +1 -0
  98. package/dist/instruction-set-gepa-launch.js +15 -0
  99. package/dist/instruction-set-loader.d.ts +6 -0
  100. package/dist/instruction-set-loader.d.ts.map +1 -0
  101. package/dist/instruction-set-loader.js +59 -0
  102. package/dist/instruction-set-manifest.d.ts +14 -0
  103. package/dist/instruction-set-manifest.d.ts.map +1 -0
  104. package/dist/instruction-set-manifest.js +162 -0
  105. package/dist/instruction-sets-cli.d.ts +40 -0
  106. package/dist/instruction-sets-cli.d.ts.map +1 -0
  107. package/dist/instruction-sets-cli.js +361 -0
  108. package/dist/json-response.d.ts +4 -0
  109. package/dist/json-response.d.ts.map +1 -0
  110. package/dist/jsonl-stream.d.ts +8 -0
  111. package/dist/jsonl-stream.d.ts.map +1 -0
  112. package/dist/kill-switch-cli.d.ts +30 -0
  113. package/dist/kill-switch-cli.d.ts.map +1 -0
  114. package/dist/loopback-serve.d.ts +25 -0
  115. package/dist/loopback-serve.d.ts.map +1 -0
  116. package/dist/manifests-cli.d.ts +22 -0
  117. package/dist/manifests-cli.d.ts.map +1 -0
  118. package/dist/markdown-report-input.d.ts +7 -0
  119. package/dist/markdown-report-input.d.ts.map +1 -0
  120. package/dist/markdown-report-input.js +15 -1
  121. package/dist/merge-job.d.ts +155 -0
  122. package/dist/merge-job.d.ts.map +1 -0
  123. package/dist/model-configs-cli.d.ts +19 -0
  124. package/dist/model-configs-cli.d.ts.map +1 -0
  125. package/dist/model-configs-cli.js +104 -0
  126. package/dist/optimization-export-cli.d.ts +19 -0
  127. package/dist/optimization-export-cli.d.ts.map +1 -0
  128. package/dist/optimizations-list-cli.d.ts +20 -0
  129. package/dist/optimizations-list-cli.d.ts.map +1 -0
  130. package/dist/optimizations-promote-cli.d.ts +8 -0
  131. package/dist/optimizations-promote-cli.d.ts.map +1 -0
  132. package/dist/optimizations-promote-cli.js +34 -0
  133. package/dist/option-validation.d.ts +15 -0
  134. package/dist/option-validation.d.ts.map +1 -0
  135. package/dist/preview-runtime.d.ts +63 -0
  136. package/dist/preview-runtime.d.ts.map +1 -0
  137. package/dist/prompt-draft-push.d.ts +60 -0
  138. package/dist/prompt-draft-push.d.ts.map +1 -0
  139. package/dist/prompt-label-cli.d.ts +11 -0
  140. package/dist/prompt-label-cli.d.ts.map +1 -0
  141. package/dist/prompt-label-cli.js +29 -0
  142. package/dist/prompt-length-wire.d.ts +28 -0
  143. package/dist/prompt-length-wire.d.ts.map +1 -0
  144. package/dist/prompt-length-wire.js +2 -2
  145. package/dist/prompt-report-cli.d.ts +10 -0
  146. package/dist/prompt-report-cli.d.ts.map +1 -0
  147. package/dist/prompt-report-cli.js +2 -2
  148. package/dist/prompt-summary-table.d.ts +15 -0
  149. package/dist/prompt-summary-table.d.ts.map +1 -0
  150. package/dist/prompt-summary-table.js +6 -0
  151. package/dist/registration-tag-warning.d.ts +32 -0
  152. package/dist/registration-tag-warning.d.ts.map +1 -0
  153. package/dist/report-comments-cli.d.ts +16 -0
  154. package/dist/report-comments-cli.d.ts.map +1 -0
  155. package/dist/report-markdown-blankness.d.ts +2 -0
  156. package/dist/report-markdown-blankness.d.ts.map +1 -0
  157. package/dist/report-markdown-blankness.js +6 -0
  158. package/dist/report-replacement-warning.d.ts +17 -0
  159. package/dist/report-replacement-warning.d.ts.map +1 -0
  160. package/dist/report-replacement-warning.js +31 -0
  161. package/dist/repository-backup-core.d.ts +225 -0
  162. package/dist/repository-backup-core.d.ts.map +1 -0
  163. package/dist/repository-recovery-manifest.d.ts +83 -0
  164. package/dist/repository-recovery-manifest.d.ts.map +1 -0
  165. package/dist/run-event-digest.d.ts +74 -0
  166. package/dist/run-event-digest.d.ts.map +1 -0
  167. package/dist/runner-dir-verify.d.ts +66 -0
  168. package/dist/runner-dir-verify.d.ts.map +1 -0
  169. package/dist/runner-instruction-set-contract.d.ts +24 -0
  170. package/dist/runner-instruction-set-contract.d.ts.map +1 -0
  171. package/dist/runner-instruction-set-contract.js +50 -0
  172. package/dist/sandbox-hygiene.d.ts +45 -0
  173. package/dist/sandbox-hygiene.d.ts.map +1 -0
  174. package/dist/sandbox-provider.d.ts +193 -0
  175. package/dist/sandbox-provider.d.ts.map +1 -0
  176. package/dist/scorer-draft-push.d.ts +50 -0
  177. package/dist/scorer-draft-push.d.ts.map +1 -0
  178. package/dist/scorers-cli.d.ts +20 -0
  179. package/dist/scorers-cli.d.ts.map +1 -0
  180. package/dist/scorers-cli.js +22 -0
  181. package/dist/scorers-verify-parity-bridge.d.ts +16 -0
  182. package/dist/scorers-verify-parity-bridge.d.ts.map +1 -0
  183. package/dist/scorers-verify-parity-bridge.js +90 -0
  184. package/dist/scorers-verify-parity.d.ts +81 -0
  185. package/dist/scorers-verify-parity.d.ts.map +1 -0
  186. package/dist/scorers-verify-parity.js +502 -0
  187. package/dist/scores-accept-cli.d.ts +21 -0
  188. package/dist/scores-accept-cli.d.ts.map +1 -0
  189. package/dist/secret-redaction.d.ts +47 -0
  190. package/dist/secret-redaction.d.ts.map +1 -0
  191. package/dist/session-timeline.d.ts +54 -0
  192. package/dist/session-timeline.d.ts.map +1 -0
  193. package/dist/setup-prompt.d.ts +5 -0
  194. package/dist/setup-prompt.d.ts.map +1 -0
  195. package/dist/skill-installer.d.ts +68 -0
  196. package/dist/skill-installer.d.ts.map +1 -0
  197. package/dist/skilled-proposer-launch.d.ts +7 -0
  198. package/dist/skilled-proposer-launch.d.ts.map +1 -0
  199. package/dist/skilled-proposer-launch.js +60 -0
  200. package/dist/smoke.d.ts +102 -0
  201. package/dist/smoke.d.ts.map +1 -0
  202. package/dist/start-intent.d.ts +12 -0
  203. package/dist/start-intent.d.ts.map +1 -0
  204. package/dist/task-assignment-manifest.d.ts +15 -0
  205. package/dist/task-assignment-manifest.d.ts.map +1 -0
  206. package/dist/task-create-error.d.ts +18 -0
  207. package/dist/task-create-error.d.ts.map +1 -0
  208. package/dist/team-connectors-cli.d.ts +29 -0
  209. package/dist/team-connectors-cli.d.ts.map +1 -0
  210. package/dist/types.d.ts +39 -0
  211. package/dist/types.d.ts.map +1 -0
  212. package/dist/vercel-sandbox-provider.d.ts +156 -0
  213. package/dist/vercel-sandbox-provider.d.ts.map +1 -0
  214. package/dist/workbench-cli.d.ts +233 -0
  215. package/dist/workbench-cli.d.ts.map +1 -0
  216. package/dist/workspace-sync.d.ts +207 -0
  217. package/dist/workspace-sync.d.ts.map +1 -0
  218. package/dist/workspace.d.ts +92 -0
  219. package/dist/workspace.d.ts.map +1 -0
  220. package/dist/zip-draft-push.d.ts +55 -0
  221. package/dist/zip-draft-push.d.ts.map +1 -0
  222. package/package.json +15 -2
  223. package/requirements/README.md +138 -0
  224. package/requirements/skilled-proposer.in +66 -0
  225. package/requirements/skilled-proposer.lock +265 -0
  226. package/scripts/build-cli-dist.mjs +30 -0
  227. package/scripts/ensure-skilled-proposer-venv.mjs +5 -0
  228. package/scripts/orizu-source-checkout.d.mts +1 -0
  229. package/scripts/orizu-source-checkout.mjs +10 -0
  230. package/scripts/refresh-skilled-proposer-lock.mjs +256 -0
  231. package/scripts/vendor-gepa-python.mjs +187 -4
  232. package/src/diff-comments-cli.ts +41 -3
  233. package/src/gepa-engine-dispatch.ts +340 -0
  234. package/src/gepa-python-command.ts +31 -0
  235. package/src/gepa-python-paths.ts +56 -0
  236. package/src/help.ts +95 -25
  237. package/src/index.ts +131 -166
  238. package/src/instruction-set-gepa-launch.ts +28 -0
  239. package/src/instruction-set-loader.ts +37 -0
  240. package/src/instruction-set-manifest.ts +120 -0
  241. package/src/instruction-sets-cli.ts +306 -0
  242. package/src/markdown-report-input.ts +19 -1
  243. package/src/model-configs-cli.ts +119 -0
  244. package/src/optimizations-promote-cli.ts +41 -0
  245. package/src/prompt-label-cli.ts +55 -0
  246. package/src/prompt-length-wire.ts +1 -1
  247. package/src/prompt-report-cli.ts +2 -2
  248. package/src/prompt-summary-table.ts +12 -0
  249. package/src/report-markdown-blankness.ts +6 -0
  250. package/src/report-replacement-warning.ts +35 -0
  251. package/src/runner-instruction-set-contract.ts +64 -0
  252. package/src/scorers-cli.ts +40 -0
  253. package/src/scorers-verify-parity-bridge.ts +90 -0
  254. package/src/scorers-verify-parity.ts +564 -0
  255. package/src/skilled-proposer-launch.ts +89 -0
  256. package/src/skilled-proposer-venv-manager.mjs +757 -0
  257. package/tsconfig.json +2 -0
  258. package/vendor/gepa-python/LICENSE +7 -0
  259. package/vendor/gepa-python/manifest.json +107 -0
  260. package/vendor/gepa-python/src/gepa/__init__.py +22 -0
  261. package/vendor/gepa-python/src/gepa/adapters/__init__.py +0 -0
  262. package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/__init__.py +1 -0
  263. package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
  264. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/__init__.py +26 -0
  265. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/confidence_adapter.py +618 -0
  266. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/scoring.py +206 -0
  267. package/vendor/gepa-python/src/gepa/adapters/default_adapter/__init__.py +0 -0
  268. package/vendor/gepa-python/src/gepa/adapters/default_adapter/default_adapter.py +206 -0
  269. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/__init__.py +0 -0
  270. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/dspy_adapter.py +525 -0
  271. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/instruction_proposal.py +500 -0
  272. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/__init__.py +6 -0
  273. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
  274. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/full_program_adapter.py +269 -0
  275. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/__init__.py +56 -0
  276. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
  277. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
  278. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
  279. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
  280. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
  281. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
  282. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
  283. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
  284. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
  285. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
  286. package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/__init__.py +11 -0
  287. package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/langchain_adapter.py +261 -0
  288. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/__init__.py +37 -0
  289. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_adapter.py +693 -0
  290. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_client.py +364 -0
  291. package/vendor/gepa-python/src/gepa/adapters/optimize_anything_adapter/optimize_anything_adapter.py +879 -0
  292. package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/__init__.py +0 -0
  293. package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
  294. package/vendor/gepa-python/src/gepa/api.py +468 -0
  295. package/vendor/gepa-python/src/gepa/core/__init__.py +0 -0
  296. package/vendor/gepa-python/src/gepa/core/adapter.py +227 -0
  297. package/vendor/gepa-python/src/gepa/core/callbacks.py +551 -0
  298. package/vendor/gepa-python/src/gepa/core/data_loader.py +74 -0
  299. package/vendor/gepa-python/src/gepa/core/engine.py +1025 -0
  300. package/vendor/gepa-python/src/gepa/core/result.py +271 -0
  301. package/vendor/gepa-python/src/gepa/core/state.py +706 -0
  302. package/vendor/gepa-python/src/gepa/examples/__init__.py +0 -0
  303. package/vendor/gepa-python/src/gepa/examples/aime.py +24 -0
  304. package/vendor/gepa-python/src/gepa/examples/anymaths-bench/eval_default.py +111 -0
  305. package/vendor/gepa-python/src/gepa/examples/anymaths-bench/train_anymaths.py +177 -0
  306. package/vendor/gepa-python/src/gepa/examples/mcp_adapter/__init__.py +4 -0
  307. package/vendor/gepa-python/src/gepa/examples/mcp_adapter/mcp_optimization_example.py +456 -0
  308. package/vendor/gepa-python/src/gepa/examples/rag_adapter/__init__.py +9 -0
  309. package/vendor/gepa-python/src/gepa/examples/rag_adapter/rag_optimization.py +824 -0
  310. package/vendor/gepa-python/src/gepa/examples/terminal-bench/train_terminus.py +161 -0
  311. package/vendor/gepa-python/src/gepa/gepa_utils.py +117 -0
  312. package/vendor/gepa-python/src/gepa/gskill/__init__.py +1 -0
  313. package/vendor/gepa-python/src/gepa/gskill/gskill/__init__.py +0 -0
  314. package/vendor/gepa-python/src/gepa/gskill/gskill/cost_tracker.py +194 -0
  315. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/__init__.py +0 -0
  316. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code.py +1117 -0
  317. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code_skills.py +613 -0
  318. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/mini_swe_agent.py +440 -0
  319. package/vendor/gepa-python/src/gepa/gskill/gskill/experiment_logger.py +367 -0
  320. package/vendor/gepa-python/src/gepa/gskill/gskill/swe_fitness_fn.py +209 -0
  321. package/vendor/gepa-python/src/gepa/gskill/gskill/swe_harness.py +352 -0
  322. package/vendor/gepa-python/src/gepa/gskill/gskill/train_optimize_anything.py +830 -0
  323. package/vendor/gepa-python/src/gepa/image.py +104 -0
  324. package/vendor/gepa-python/src/gepa/lm.py +261 -0
  325. package/vendor/gepa-python/src/gepa/logging/__init__.py +0 -0
  326. package/vendor/gepa-python/src/gepa/logging/experiment_tracker.py +457 -0
  327. package/vendor/gepa-python/src/gepa/logging/logger.py +75 -0
  328. package/vendor/gepa-python/src/gepa/logging/utils.py +130 -0
  329. package/vendor/gepa-python/src/gepa/optimize_anything.py +1701 -0
  330. package/vendor/gepa-python/src/gepa/proposer/__init__.py +0 -0
  331. package/vendor/gepa-python/src/gepa/proposer/base.py +53 -0
  332. package/vendor/gepa-python/src/gepa/proposer/merge.py +403 -0
  333. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/__init__.py +0 -0
  334. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/base.py +64 -0
  335. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflection_lm.py +178 -0
  336. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflective_mutation.py +641 -0
  337. package/vendor/gepa-python/src/gepa/py.typed +0 -0
  338. package/vendor/gepa-python/src/gepa/strategies/__init__.py +0 -0
  339. package/vendor/gepa-python/src/gepa/strategies/acceptance.py +66 -0
  340. package/vendor/gepa-python/src/gepa/strategies/batch_sampler.py +103 -0
  341. package/vendor/gepa-python/src/gepa/strategies/candidate_selector.py +82 -0
  342. package/vendor/gepa-python/src/gepa/strategies/component_selector.py +36 -0
  343. package/vendor/gepa-python/src/gepa/strategies/eval_policy.py +64 -0
  344. package/vendor/gepa-python/src/gepa/strategies/instruction_proposal.py +153 -0
  345. package/vendor/gepa-python/src/gepa/strategies/proposal_sampling.py +128 -0
  346. package/vendor/gepa-python/src/gepa/strategies/proposal_selection.py +84 -0
  347. package/vendor/gepa-python/src/gepa/utils/__init__.py +63 -0
  348. package/vendor/gepa-python/src/gepa/utils/code_execution.py +626 -0
  349. package/vendor/gepa-python/src/gepa/utils/stdio_capture.py +145 -0
  350. package/vendor/gepa-python/src/gepa/utils/stop_condition.py +230 -0
  351. package/vendor/gepa-python/src/gepa/visualization.py +359 -0
  352. package/vendor/orizu-gepa/pyproject.toml +18 -0
  353. package/vendor/orizu-gepa/src/orizu_gepa_connector/__init__.py +9 -0
  354. package/vendor/orizu-gepa/src/orizu_gepa_connector/__main__.py +5 -0
  355. package/vendor/orizu-gepa/src/orizu_gepa_connector/adapter.py +138 -0
  356. package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +666 -0
  357. package/vendor/orizu-gepa/src/orizu_gepa_connector/cli.py +7 -0
  358. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +269 -0
  359. package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +52 -0
  360. package/vendor/orizu-gepa/src/orizu_gepa_connector/preflight.py +64 -0
  361. package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +186 -0
  362. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +721 -0
  363. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +381 -0
  364. package/vendor/orizu-gepa/src/orizu_gepa_connector/stop_conditions.py +55 -0
  365. package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +203 -0
  366. package/vendor/orizu-gepa-python/pyproject.toml +1 -1
  367. package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
  368. package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +52 -7
  369. package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +52 -0
  370. package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +10 -5
  371. package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +323 -68
  372. package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +207 -13
  373. package/vendor/skills/orizu-cli/SKILL.md +3 -1
  374. package/vendor/skills/orizu-cli/references/cli-reference.md +85 -1
  375. package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +303 -0
  376. package/vendor/skills/orizu-cli/references/optimization-reports.md +1 -1
  377. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +26 -1
  378. 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