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,1701 @@
1
+ """
2
+ ``optimize_anything`` — a universal API for optimizing any text parameter.
3
+
4
+ This is the primary public API for GEPA. You declare **what** to optimize
5
+ (a text-representable artifact — code, prompts, agent architectures,
6
+ configurations, policies, SVG graphics) and **how** to measure it (an
7
+ evaluator); ``optimize_anything`` handles the **how**: prompt construction,
8
+ LLM reflection, candidate selection, and Pareto-efficient search.
9
+
10
+ The key insight is that a wide range of problems can be formulated as
11
+ optimizing a text artifact: speeding up a CUDA kernel, tuning a scheduling
12
+ policy, refining a prompt template, or redesigning an agent architecture.
13
+ If it can be serialized to a string and its quality measured, an LLM can
14
+ reason about it and propose improvements.
15
+
16
+ Core workflow::
17
+
18
+ seed_candidate → evaluate → reflect on ASI → propose → repeat
19
+ ↑ |
20
+ └──────────────────────────────────┘
21
+
22
+ Three optimization modes
23
+ ------------------------
24
+ ``optimize_anything`` unifies three optimization paradigms under one
25
+ declarative API, determined by whether you provide ``dataset`` and ``valset``:
26
+
27
+ 1. **Single-Task Search** (``dataset=None, valset=None``):
28
+ Solve one hard problem. The candidate *is* the solution.
29
+ Evaluator is called without ``example``.
30
+ *E.g. circle packing, blackbox mathematical optimization.*
31
+
32
+ 2. **Multi-Task Search** (``dataset=<list>, valset=None``):
33
+ Solve a batch of related problems with cross-task transfer. Insights
34
+ from solving one help solve the others. Evaluator is called per-example.
35
+ *E.g. CUDA kernel generation for multiple PyTorch operations,
36
+ multi-aspect SVG optimization.*
37
+
38
+ 3. **Generalization** (``dataset=<list>, valset=<list>``):
39
+ Build a skill that transfers to unseen problems. Evaluator is called
40
+ per-example; candidates must generalize to ``valset``.
41
+ *E.g. prompt optimization for AIME math, agent architecture evolution
42
+ for ARC-AGI, cloud scheduling policy discovery.*
43
+
44
+ Seedless mode
45
+ -------------
46
+ When you don't have a starting artifact, pass ``seed_candidate=None`` and
47
+ provide ``objective`` (and optionally ``background``). The reflection LM
48
+ bootstraps the first candidate from the description, then iterates as usual.
49
+ Useful for creative or exploratory tasks where the solution space is large.
50
+
51
+ Quick example::
52
+
53
+ import gepa.optimize_anything as oa
54
+ from gepa.optimize_anything import optimize_anything, GEPAConfig, EngineConfig
55
+
56
+ def evaluate(candidate: str) -> float:
57
+ score, diagnostic = run_candidate(candidate)
58
+ oa.log("Diagnostic:", diagnostic) # captured as ASI
59
+ return score
60
+
61
+ # Start from an existing artifact…
62
+ result = optimize_anything(
63
+ seed_candidate="<initial code>",
64
+ evaluator=evaluate,
65
+ objective="Maximize throughput.",
66
+ config=GEPAConfig(engine=EngineConfig(max_metric_calls=300)),
67
+ )
68
+
69
+ # … or just describe what you need (seedless mode).
70
+ result = optimize_anything(
71
+ evaluator=evaluate,
72
+ objective="Generate a Python function that reverses a string.",
73
+ )
74
+
75
+ print(result.best_candidate)
76
+
77
+ Key concepts:
78
+ - **Candidate**: ``dict[str, str]`` or plain ``str`` of optimizable text
79
+ parameters — prompts, code, configs, agent architectures, etc.
80
+ - **Evaluator**: Your scoring function. Returns ``(score, side_info)`` or
81
+ just ``score``. Higher scores are better.
82
+ - **ASI / SideInfo**: Actionable Side Information — diagnostic feedback
83
+ returned by the evaluator (or captured via ``oa.log()``). ASI is the
84
+ text-optimization analogue of the gradient: where gradients tell a
85
+ numerical optimizer which direction to move, ASI tells the LLM proposer
86
+ *why* a candidate failed and *how* to fix it. Can include error
87
+ messages, profiling traces, rendered images (via :class:`Image`), or
88
+ any structured data that would help an expert diagnose failures.
89
+ - **Pareto-efficient search**: Scores are tracked per-task and per-metric
90
+ individually; any candidate that is the best at *something* survives on
91
+ the frontier, enabling focused improvements that are preserved rather
92
+ than averaged away.
93
+ - **Objective / Background**: Natural-language strings that guide the
94
+ reflection LLM (what to optimize for, domain constraints).
95
+
96
+ Public API:
97
+ - :func:`optimize_anything` — main entry point
98
+ - :data:`SideInfo` — type alias for evaluation diagnostics (ASI)
99
+ - :class:`Evaluator` — evaluator protocol
100
+ - :class:`OptimizationState` — context injected into evaluators
101
+ - :class:`GEPAConfig`, :class:`EngineConfig`, :class:`ReflectionConfig`,
102
+ :class:`RefinerConfig`, :class:`MergeConfig`, :class:`TrackingConfig`
103
+ - :func:`log`, :func:`get_log_context`, :func:`set_log_context` — in-evaluator logging
104
+ - :func:`make_litellm_lm` — convert a model name string to a callable
105
+ - :class:`Image` — wrapper for including images in side_info (VLM reflection)
106
+ """
107
+
108
+ import inspect
109
+ import io
110
+ import os
111
+ import random
112
+ import threading
113
+ import warnings
114
+ from collections.abc import Callable
115
+ from dataclasses import asdict, dataclass, field
116
+ from typing import (
117
+ Any,
118
+ Literal,
119
+ Protocol,
120
+ Sequence,
121
+ TypeAlias,
122
+ )
123
+
124
+ from gepa.adapters.optimize_anything_adapter.optimize_anything_adapter import (
125
+ BatchEvaluatorWrapper,
126
+ OptimizeAnythingAdapter,
127
+ )
128
+ from gepa.core.adapter import DataInst, GEPAAdapter, ProposalFn
129
+ from gepa.core.callbacks import GEPACallback
130
+ from gepa.core.data_loader import ensure_loader
131
+ from gepa.core.engine import GEPAEngine
132
+ from gepa.core.result import GEPAResult
133
+ from gepa.core.state import EvaluationCache, FrontierType
134
+ from gepa.image import Image # noqa: F401 — re-exported for user convenience
135
+ from gepa.logging.experiment_tracker import create_experiment_tracker
136
+ from gepa.logging.logger import Logger, LoggerProtocol, StdOutLogger
137
+ from gepa.proposer.merge import MergeProposer
138
+ from gepa.proposer.reflective_mutation.base import CandidateSelector, LanguageModel, ReflectionComponentSelector
139
+ from gepa.proposer.reflective_mutation.reflection_lm import ReflectionLM
140
+ from gepa.proposer.reflective_mutation.reflective_mutation import ReflectiveMutationProposer
141
+ from gepa.strategies.acceptance import AcceptanceCriterion, ImprovementOrEqualAcceptance, StrictImprovementAcceptance
142
+ from gepa.strategies.batch_sampler import BatchSampler, EpochShuffledBatchSampler
143
+ from gepa.strategies.candidate_selector import (
144
+ CurrentBestCandidateSelector,
145
+ EpsilonGreedyCandidateSelector,
146
+ ParetoCandidateSelector,
147
+ TopKParetoCandidateSelector,
148
+ )
149
+ from gepa.strategies.component_selector import (
150
+ AllReflectionComponentSelector,
151
+ RoundRobinReflectionComponentSelector,
152
+ )
153
+ from gepa.strategies.eval_policy import EvaluationPolicy, FullEvaluationPolicy
154
+ from gepa.strategies.proposal_sampling import SamplingStrategy
155
+ from gepa.strategies.proposal_selection import SelectionStrategy
156
+ from gepa.utils import FileStopper, StopperProtocol
157
+ from gepa.utils.stdio_capture import ThreadLocalStreamCapture, stream_manager
158
+
159
+ OptimizableParam = str
160
+ Candidate = dict[str, OptimizableParam]
161
+
162
+ # Cache storage modes for evaluator caching (when cache_evaluation=True)
163
+ CacheEvaluationStorage = Literal["memory", "disk", "auto"]
164
+
165
+
166
+ # Sentinel object for single-instance mode
167
+ class _SingleInstanceSentinel:
168
+ """Sentinel object used to represent single-instance optimization mode."""
169
+
170
+ def __repr__(self) -> str:
171
+ return "<SingleInstanceSentinel>"
172
+
173
+
174
+ _SINGLE_INSTANCE_SENTINEL = _SingleInstanceSentinel()
175
+
176
+ # Internal key used to wrap a plain-str seed_candidate into a dict.
177
+ _STR_CANDIDATE_KEY = "current_candidate"
178
+
179
+ SideInfo: TypeAlias = dict[str, Any]
180
+ """Actionable Side Information (ASI) returned by the evaluator alongside each score.
181
+
182
+ ASI is the text-optimization analogue of the gradient. Where gradients tell
183
+ a numerical optimizer which direction to move, ASI tells an LLM proposer
184
+ *why* a candidate failed and *how* to fix it.
185
+
186
+ Traditional optimizers know *that* a candidate failed but not *why*. SideInfo
187
+ provides the *why* — error messages, expected vs actual output, profiling
188
+ traces, compiler diagnostics, rendered images — enabling the reflection LLM
189
+ to take targeted corrective action rather than random mutation.
190
+
191
+ **More informative SideInfo → better optimization.**
192
+
193
+ You can provide SideInfo in two ways:
194
+
195
+ 1. Return ``(score, side_info_dict)`` from your evaluator.
196
+ 2. Call ``oa.log(...)`` inside your evaluator (captured under ``"log"`` key).
197
+
198
+ Structure
199
+ ---------
200
+ 1. **``"scores"`` (optional)** — multi-objective metrics for Pareto tracking.
201
+ All values must follow "higher is better" convention.
202
+
203
+ ``{"scores": {"accuracy": 0.85, "latency_inv": 12.5}}``
204
+
205
+ 2. **Contextual fields** — any other keys. Common conventions:
206
+
207
+ - ``"Input"`` / ``"Output"`` / ``"Expected"`` — what went in and came out
208
+ - ``"Feedback"`` — qualitative assessment (human or machine)
209
+ - ``"Error"`` — error messages, tracebacks, compilation failures
210
+ - ``"Reasoning"`` — intermediate reasoning traces
211
+
212
+ 3. **Parameter-specific info** — ``"<param_name>_specific_info"`` dicts with
213
+ their own ``"scores"`` and contextual fields. During reflection on
214
+ parameter *X*, GEPA merges top-level fields with ``X_specific_info``.
215
+
216
+ 4. **Images** — use :class:`~gepa.Image` for visual feedback (rendered
217
+ SVGs, charts, screenshots). Requires a VLM as ``reflection_lm``.
218
+
219
+ Example::
220
+
221
+ {
222
+ "scores": {"accuracy": 0.73, "user_satisfaction": 4.2},
223
+ "Input": "Translate 'Hello world' to French",
224
+ "Output": "Salut monde",
225
+ "Expected": "Bonjour le monde",
226
+ "Feedback": "Translation is too informal for the context",
227
+ "system_prompt_specific_info": {
228
+ "scores": {"tone": 0.3},
229
+ "Analysis": "System prompt led to overly casual translation",
230
+ },
231
+ }
232
+
233
+ Best practices:
234
+ - Include error messages and failure reasons prominently
235
+ - Use consistent field names across evaluations
236
+ - Add context beyond raw numbers (explain *what* went wrong)
237
+ - Use ``"scores"`` only for "higher is better" metrics used in Pareto tracking
238
+ """
239
+
240
+
241
+ @dataclass
242
+ class OptimizationState:
243
+ """Accumulated optimization context injected into evaluators that declare an ``opt_state`` parameter.
244
+
245
+ Provides historical evaluation results so your evaluator can warm-start
246
+ from previous best solutions (e.g., pass the best-known circle packing to
247
+ a new optimization attempt).
248
+
249
+ To receive this, simply add ``opt_state: OptimizationState`` to your
250
+ evaluator signature — GEPA injects it automatically.
251
+
252
+ Example::
253
+
254
+ def evaluator(candidate, example, opt_state: OptimizationState):
255
+ prev_best = opt_state.best_example_evals[0]["side_info"] if opt_state.best_example_evals else None
256
+ # ... use prev_best to warm-start ...
257
+ """
258
+
259
+ best_example_evals: list[dict]
260
+ """Top-K best evaluations for the current example, sorted by score (descending).
261
+
262
+ Each entry: ``{"score": float, "side_info": dict}``. K is controlled by
263
+ ``EngineConfig.best_example_evals_k`` (default 30)."""
264
+
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # Evaluation log context — captures diagnostic output without polluting stdout
268
+ # ---------------------------------------------------------------------------
269
+
270
+
271
+ class LogContext:
272
+ """Thread-safe log buffer for a single evaluator invocation.
273
+
274
+ All ``oa.log()`` calls within the same evaluator call write to the same
275
+ buffer, even from child threads (when properly propagated via
276
+ :func:`get_log_context` / :func:`set_log_context`). Writes are
277
+ serialized with a lock so concurrent threads never interleave.
278
+ """
279
+
280
+ def __init__(self) -> None:
281
+ self._buffer = io.StringIO()
282
+ self._lock = threading.Lock()
283
+
284
+ def write(self, text: str) -> None:
285
+ with self._lock:
286
+ self._buffer.write(text)
287
+
288
+ def drain(self) -> str:
289
+ """Drain and return all accumulated text, leaving the buffer empty."""
290
+ with self._lock:
291
+ old = self._buffer
292
+ text = old.getvalue()
293
+ old.close()
294
+ self._buffer = io.StringIO()
295
+ return text
296
+
297
+
298
+ # Thread-local storage for the active LogContext on each thread.
299
+ _log_tls = threading.local()
300
+
301
+
302
+ def _get_log_context() -> "LogContext | None":
303
+ """Return the active log context for the current thread, or None."""
304
+ return getattr(_log_tls, "context", None)
305
+
306
+
307
+ def _set_log_context(ctx: "LogContext | None") -> None:
308
+ """Set (or clear) the active log context on the current thread."""
309
+ _log_tls.context = ctx
310
+
311
+
312
+ def get_log_context() -> LogContext:
313
+ """Return the active log context for the current evaluator call.
314
+
315
+ Use this to propagate ``oa.log()`` capture to child threads spawned
316
+ inside your evaluator::
317
+
318
+ import threading
319
+ import gepa.optimize_anything as oa
320
+
321
+ def my_evaluator(candidate):
322
+ ctx = oa.get_log_context()
323
+
324
+ def worker():
325
+ oa.set_log_context(ctx)
326
+ oa.log("from child thread")
327
+
328
+ t = threading.Thread(target=worker)
329
+ t.start()
330
+ t.join()
331
+ oa.log("from main evaluator thread")
332
+ return score
333
+
334
+ Raises:
335
+ RuntimeError: If called outside an evaluator invocation.
336
+ """
337
+ ctx = _get_log_context()
338
+ if ctx is None:
339
+ raise RuntimeError(
340
+ "No active log context. get_log_context() must be called inside an evaluator passed to optimize_anything()."
341
+ )
342
+ return ctx
343
+
344
+
345
+ def set_log_context(ctx: LogContext) -> None:
346
+ """Set the log context on the current thread.
347
+
348
+ Call this at the start of a child thread to route ``oa.log()`` output
349
+ into the parent evaluator's log buffer. See :func:`get_log_context`
350
+ for a complete usage example.
351
+ """
352
+ _set_log_context(ctx)
353
+
354
+
355
+ def log(*args: Any, sep: str = " ", end: str = "\n") -> None:
356
+ """Log diagnostic information during evaluation without printing to stdout.
357
+
358
+ Has the same calling convention as ``print()``. Output is captured
359
+ per-evaluator-call (thread-safe) and automatically included in
360
+ side_info under the ``"log"`` key.
361
+
362
+ Must only be called inside an evaluator function passed to
363
+ ``optimize_anything``. Calling it outside that context will emit a
364
+ warning and the output will be silently discarded.
365
+
366
+ For child threads spawned by your evaluator, propagate the log context
367
+ via :func:`get_log_context` / :func:`set_log_context`.
368
+
369
+ Usage::
370
+
371
+ import gepa.optimize_anything as oa
372
+ oa.log("Landing distance:", distance, "meters")
373
+ """
374
+ ctx = _get_log_context()
375
+ if ctx is None:
376
+ warnings.warn(
377
+ "oa.log() called outside of an evaluator function. "
378
+ "Output will be discarded. Only call oa.log() inside your evaluator, "
379
+ "or propagate the log context to child threads via "
380
+ "oa.get_log_context() / oa.set_log_context().",
381
+ stacklevel=2,
382
+ )
383
+ return
384
+ text = sep.join(str(a) for a in args) + end
385
+ ctx.write(text)
386
+
387
+
388
+ # Thread-safe stdout / stderr capture utilities are in gepa.utils.stdio_capture.
389
+ # The module-level ``stream_manager`` singleton and ``ThreadLocalStreamCapture``
390
+ # class are imported at the top of this file.
391
+
392
+
393
+ class Evaluator(Protocol):
394
+ def __call__(
395
+ self, candidate: str | Candidate, example: object | None = None, **kwargs: Any
396
+ ) -> tuple[float, SideInfo] | float:
397
+ """Score a candidate, returning a score and diagnostic side information (ASI).
398
+
399
+ This is the function you write. GEPA calls it repeatedly with
400
+ mutated candidates and collects the returned scores and diagnostics
401
+ to drive the optimization loop.
402
+
403
+ Args:
404
+ candidate: The text parameter(s) to evaluate. Type matches
405
+ ``seed_candidate``: plain ``str`` if you passed a string,
406
+ or ``dict[str, str]`` if you passed a dict.
407
+ example: One item from ``dataset``. **Not passed** in
408
+ single-task search mode (``dataset=None``).
409
+ opt_state: *(optional)* Declare ``opt_state: OptimizationState``
410
+ in your signature to receive historical best evaluations
411
+ for warm-starting. This is a **reserved parameter name**
412
+ — GEPA injects it automatically.
413
+
414
+ Returns:
415
+ Either ``(score, side_info)`` or just ``score``:
416
+
417
+ - **score** (float): Higher is better.
418
+ - **side_info** (:data:`SideInfo`): Diagnostic dict (ASI)
419
+ powering LLM reflection.
420
+
421
+ If you return score-only, use ``oa.log()`` or
422
+ ``capture_stdio=True`` to provide diagnostic context.
423
+
424
+ Evaluator signature per mode:
425
+
426
+ **Single-Task Search** (``dataset=None``) — called without ``example``::
427
+
428
+ import gepa.optimize_anything as oa
429
+
430
+ def evaluate(candidate):
431
+ result = run_code(candidate["code"])
432
+ oa.log(f"Output: {result}") # ASI via oa.log()
433
+ return compute_score(result)
434
+
435
+ **Multi-Task Search / Generalization** (``dataset`` provided) — called per example::
436
+
437
+ def evaluate(candidate, example):
438
+ pred = run(candidate["prompt"], example["input"])
439
+ score = 1.0 if pred == example["expected"] else 0.0
440
+ return score, {"Input": example["input"], "Output": pred}
441
+
442
+ Reserved side_info keys:
443
+ ``"log"``, ``"stdout"``, ``"stderr"`` are used by GEPA's
444
+ automatic capture. If you use them, GEPA stores captured
445
+ output under ``"_gepa_log"`` etc. to avoid collisions.
446
+ """
447
+ ...
448
+
449
+
450
+ # --- Component 1: Engine & Stopping Configuration ---
451
+ @dataclass
452
+ class EngineConfig:
453
+ """Controls the optimization run loop: budget, parallelism, caching, and stopping.
454
+
455
+ Most users only need to set ``max_metric_calls`` (evaluation budget).
456
+ Parallel evaluation is enabled by default with ``max_workers`` set to
457
+ ``os.cpu_count() or 32`` (CPU count when available, otherwise 32).
458
+
459
+ Set ``capture_stdio=True`` to automatically route any ``print()`` output
460
+ inside your evaluator into ASI (under ``"stdout"``/``"stderr"`` keys),
461
+ with no code changes needed. Useful for quick prototyping or wrapping
462
+ existing evaluation scripts that already have print statements.
463
+ """
464
+
465
+ run_dir: str | None = None
466
+ seed: int = 0
467
+ display_progress_bar: bool = False
468
+ raise_on_exception: bool = True
469
+ use_cloudpickle: bool = True
470
+ track_best_outputs: bool = True
471
+
472
+ # Simple stopping conditions
473
+ max_metric_calls: int | None = None
474
+ max_candidate_proposals: int | None = None
475
+ max_reflection_cost: float | None = None
476
+
477
+ # Strategy selection for the engine
478
+ val_evaluation_policy: EvaluationPolicy | Literal["full_eval"] = "full_eval"
479
+ candidate_selection_strategy: CandidateSelector | Literal[
480
+ "pareto", "current_best", "epsilon_greedy", "top_k_pareto"
481
+ ] = "pareto"
482
+ frontier_type: FrontierType = "hybrid"
483
+
484
+ # Acceptance criterion for reflective mutation proposals
485
+ acceptance_criterion: AcceptanceCriterion | Literal[
486
+ "strict_improvement", "improvement_or_equal"
487
+ ] = "strict_improvement"
488
+
489
+ # Parallelization settings for evaluation
490
+ parallel: bool = True
491
+ max_workers: int | None = field(default_factory=lambda: os.cpu_count() or 32)
492
+
493
+ # Evaluation caching
494
+ cache_evaluation: bool = False
495
+ cache_evaluation_storage: CacheEvaluationStorage = "auto"
496
+
497
+ # Track top-K best evaluations per example, passed to evaluator via OptimizationState
498
+ # Useful for warm-starting optimization from previous best solutions
499
+ best_example_evals_k: int = 30
500
+
501
+ # When True, automatically capture stdout/stderr during evaluation and
502
+ # include it in side_info as {"stdout": "...", "stderr": "..."}.
503
+ # Thread-safe via per-thread sys.stdout/stderr replacement.
504
+ #
505
+ # Captures all Python-level output: print(), sys.stdout.write(), and
506
+ # third-party library output — anything that goes through sys.stdout.
507
+ #
508
+ # Does NOT capture output that bypasses Python's sys.stdout:
509
+ # C extensions writing directly to fd 1/2, or subprocesses spawned
510
+ # internally by libraries. For those, use oa.log() or capture subprocess
511
+ # output manually and pass it to oa.log().
512
+ capture_stdio: bool = False
513
+
514
+ # Proposal strategies (default: 1 parent, 1 mutation per iteration).
515
+ # Swap in a different SamplingStrategy to propose multiple candidates.
516
+ sampling_strategy: SamplingStrategy | None = None
517
+ selection_strategy: SelectionStrategy | None = None
518
+
519
+
520
+ def _build_reflection_prompt_template(objective: str | None = None, background: str | None = None) -> str:
521
+ """
522
+ Build a reflection prompt template dynamically based on provided objective and background.
523
+
524
+ Only includes sections that have content, ensuring the prompt feels natural
525
+ regardless of which optional parameters are provided.
526
+
527
+ Args:
528
+ objective: High-level goal describing what the optimized component should achieve.
529
+ background: Domain knowledge, constraints, strategies, or implementation requirements.
530
+
531
+ Returns:
532
+ A reflection prompt template string with <curr_param> and <side_info> placeholders.
533
+ """
534
+ sections = []
535
+
536
+ # System context - always present
537
+ sections.append(
538
+ "You are an expert optimization assistant. Your task is to analyze evaluation "
539
+ "feedback and propose an improved version of a system component."
540
+ )
541
+
542
+ # Objective section
543
+ if objective:
544
+ sections.append(f"""
545
+ ## Optimization Goal
546
+
547
+ {objective}""")
548
+
549
+ # Background/context section
550
+ if background:
551
+ sections.append(f"""
552
+ ## Domain Context & Constraints
553
+
554
+ {background}""")
555
+
556
+ # Current component and evaluation data - always present
557
+ sections.append("""
558
+ ## Current Component
559
+
560
+ The component being optimized:
561
+
562
+ ```
563
+ <curr_param>
564
+ ```
565
+
566
+ ## Evaluation Results
567
+
568
+ Performance data from evaluating the current component across test cases:
569
+
570
+ ```
571
+ <side_info>
572
+ ```""")
573
+
574
+ # Analysis instructions - tailored based on what context is available
575
+ analysis_points = []
576
+ if objective:
577
+ analysis_points.append(
578
+ "- **Goal alignment**: How well does the current component achieve the stated optimization goal?"
579
+ )
580
+ analysis_points.extend(
581
+ [
582
+ "- **Failure patterns**: What specific errors, edge cases, or failure modes appear in the evaluation data?",
583
+ "- **Success patterns**: What behaviors or approaches worked well and should be preserved?",
584
+ "- **Root causes**: What underlying issues explain the observed failures?",
585
+ ]
586
+ )
587
+ if background:
588
+ analysis_points.append(
589
+ "- **Constraint compliance**: Does the component satisfy all requirements from the domain context?"
590
+ )
591
+
592
+ analysis_section = "\n".join(analysis_points)
593
+ constraint_line = "\n4. Adheres to all constraints and requirements from the domain context" if background else ""
594
+ sections.append(f"""
595
+ ## Your Task
596
+
597
+ Analyze the evaluation results systematically:
598
+
599
+ {analysis_section}
600
+
601
+ Based on your analysis, propose an improved version that:
602
+ 1. Addresses the identified failure patterns and root causes
603
+ 2. Preserves successful behaviors from the current version
604
+ 3. Makes meaningful improvements rather than superficial changes{constraint_line}""")
605
+
606
+ # Output format - always present
607
+ sections.append("""
608
+ ## Output Format
609
+
610
+ Provide ONLY the improved version within ``` blocks. The output must be a complete,
611
+ drop-in replacement for the current component (whether it's a prompt, configuration,
612
+ code, or any other parameter type).
613
+ Do not include explanations, commentary, or markdown outside the ``` blocks.""")
614
+
615
+ return "\n".join(sections)
616
+
617
+
618
+ def _build_seed_generation_prompt(
619
+ objective: str,
620
+ background: str | None = None,
621
+ dataset: list[DataInst] | None = None,
622
+ ) -> str:
623
+ """Build a prompt for the reflection LM to generate an initial seed candidate.
624
+
625
+ Used when ``seed_candidate=None`` — the LLM bootstraps the first candidate
626
+ from the objective, optional background, and optional dataset examples.
627
+ """
628
+ sections = []
629
+
630
+ sections.append(
631
+ "You are an expert assistant. Your task is to generate an initial candidate "
632
+ "that will be iteratively refined by an optimization system."
633
+ )
634
+
635
+ sections.append(f"\n## Goal\n\n{objective}")
636
+
637
+ if background:
638
+ sections.append(f"\n## Domain Context & Constraints\n\n{background}")
639
+
640
+ if dataset is not None:
641
+ examples = dataset[:3]
642
+ example_lines = [f"- Example {i}: {ex}" for i, ex in enumerate(examples, 1)]
643
+ sections.append(
644
+ "\n## Sample Inputs\n\n"
645
+ "The candidate will be evaluated on inputs like these:\n\n"
646
+ + "\n".join(example_lines)
647
+ )
648
+
649
+ sections.append(
650
+ "\n## Output Format\n\n"
651
+ "Generate a strong initial candidate based on the goal above.\n"
652
+ "Provide ONLY the candidate within ``` blocks. "
653
+ "Do not include explanations or commentary outside the ``` blocks."
654
+ )
655
+
656
+ return "\n".join(sections)
657
+
658
+
659
+ def _generate_seed_candidate(
660
+ lm: LanguageModel,
661
+ objective: str,
662
+ background: str | None = None,
663
+ dataset: list[DataInst] | None = None,
664
+ logger: LoggerProtocol | None = None,
665
+ ) -> Candidate:
666
+ """Call the reflection LM to generate an initial seed candidate.
667
+
668
+ Returns a single-key candidate dict ``{_STR_CANDIDATE_KEY: generated_text}``.
669
+ """
670
+ from gepa.strategies.instruction_proposal import InstructionProposalSignature
671
+
672
+ prompt = _build_seed_generation_prompt(
673
+ objective=objective,
674
+ background=background,
675
+ dataset=dataset,
676
+ )
677
+
678
+ if logger:
679
+ logger.log("Generating initial seed candidate via LLM...")
680
+
681
+ lm_output = lm(prompt)
682
+ extracted = InstructionProposalSignature.output_extractor(lm_output)
683
+ generated_text = extracted["new_instruction"]
684
+
685
+ if logger:
686
+ logger.log(f"Generated seed candidate ({len(generated_text)} chars)")
687
+
688
+ return {_STR_CANDIDATE_KEY: generated_text}
689
+
690
+
691
+ optimize_anything_reflection_prompt_template: str = """I am optimizing a parameter in my system. The current parameter value is:
692
+ ```
693
+ <curr_param>
694
+ ```
695
+
696
+ Below is evaluation data showing how this parameter value performed across multiple test cases. The data contains performance metrics, diagnostic information, and other relevant details from the evaluation:
697
+ ```
698
+ <side_info>
699
+ ```
700
+
701
+ Your task is to propose a new, improved parameter value that can be used as a drop-in replacement for the current one.
702
+
703
+ Carefully analyze all the evaluation data provided above. Look for patterns that indicate what works and what doesn't. Pay special attention to:
704
+ - Performance metrics and how they correlate with parameter behavior
705
+ - Recurring issues, errors, or failure patterns across multiple test cases
706
+ - Successful patterns or behaviors that should be preserved or enhanced
707
+ - Any domain-specific requirements, constraints, or factual information revealed in the evaluation data
708
+ - Specific technical details that are crucial for understanding the parameter's role
709
+
710
+ Based on your analysis, propose a new parameter value that addresses the identified issues while maintaining or improving upon what works well. Your proposal should be directly informed by the patterns and insights from the evaluation data.
711
+
712
+ Provide the new parameter value within ``` blocks."""
713
+
714
+
715
+ # --- Component 2: Proposer Configurations ---
716
+ @dataclass
717
+ class ReflectionConfig:
718
+ """Controls how the LLM proposes improved candidates each iteration.
719
+
720
+ The reflection LM sees evaluation feedback (side_info) for a minibatch of
721
+ examples and proposes an improved candidate. ``reflection_lm`` is the
722
+ model used for this step (defaults to ``openai/gpt-5.1``).
723
+
724
+ ``reflection_minibatch_size`` controls how many examples are shown per
725
+ reflection step (default: 1 for single-task, 3 otherwise). Showing a
726
+ small minibatch rather than all examples at once produces focused,
727
+ targeted improvements on that subset. Over iterations, all examples get
728
+ attention, and the Pareto frontier preserves specialized gains across
729
+ iterations rather than averaging them away.
730
+ """
731
+
732
+ skip_perfect_score: bool = False
733
+ perfect_score: float | None = None
734
+ # Advanced: a ReflectionLM implementation that owns how reflective mutation
735
+ # calls the reflection model (sessions, ComBEE-style aggregation, coding
736
+ # agents). Defaults to the stateless single-call reflector built from
737
+ # ``reflection_lm``. Mirror of gepa.optimize()'s reflection_strategy=.
738
+ reflection_strategy: "ReflectionLM | None" = None
739
+ batch_sampler: BatchSampler | Literal["epoch_shuffled"] = "epoch_shuffled"
740
+ reflection_minibatch_size: int | None = None # Default: 1 for single-instance mode, 3 otherwise
741
+ module_selector: ReflectionComponentSelector | Literal["round_robin", "all"] = "round_robin"
742
+ reflection_lm: LanguageModel | str | None = "openai/gpt-5.1"
743
+ reflection_lm_kwargs: dict[str, Any] | None = None
744
+ """Extra keyword arguments forwarded to ``litellm.completion`` when
745
+ ``reflection_lm`` is a model name string (e.g.
746
+ ``{"reasoning_effort": "high", "temperature": 0.7}``).
747
+ Ignored when ``reflection_lm`` is already a callable."""
748
+ reflection_prompt_template: str | dict[str, str] | None = optimize_anything_reflection_prompt_template
749
+ custom_candidate_proposer: ProposalFn | None = None
750
+
751
+
752
+ @dataclass
753
+ class MergeConfig:
754
+ """Enables cross-pollination between candidates on the Pareto frontier.
755
+
756
+ When set, GEPA periodically attempts to merge strengths of two candidates
757
+ that each excel on different subsets of the validation set.
758
+ """
759
+
760
+ max_merge_invocations: int = 5
761
+ merge_val_overlap_floor: int = 5
762
+
763
+
764
+ # --- Refiner Configuration ---
765
+
766
+ DEFAULT_REFINER_PROMPT = """You are a refinement agent improving candidates in an optimization loop.
767
+
768
+ ## What We're Optimizing For
769
+ The overall optimization objective is:
770
+ {objective}
771
+
772
+ This tells you what "better" means - use it to guide your improvements.
773
+
774
+ ## Domain Knowledge
775
+ {background}
776
+
777
+ ## Your Task
778
+ Given a candidate and its evaluation feedback:
779
+ 1. Understand why it scored the way it did
780
+ 2. Fix any errors (errors = zero score)
781
+ 3. Make improvements that move toward the objective
782
+ 4. Return the complete improved candidate
783
+ """
784
+
785
+
786
+ @dataclass
787
+ class RefinerConfig:
788
+ """Automatic per-evaluation candidate refinement via LLM.
789
+
790
+ When enabled, after each evaluation GEPA calls an LLM to propose a refined
791
+ version of the candidate based on the evaluation feedback. The refined
792
+ candidate is re-evaluated, and the better of (original, refined) is kept.
793
+
794
+ A ``refiner_prompt`` parameter is auto-injected into seed candidates and
795
+ co-evolved alongside the other parameters. All non-refiner params are
796
+ refined together as a JSON dict.
797
+
798
+ Set ``config.refiner = None`` to disable refinement.
799
+ """
800
+
801
+ # Language model for refinement (defaults to reflection_lm if not specified)
802
+ refiner_lm: LanguageModel | str | None = None
803
+
804
+ # Maximum refinement iterations per evaluation
805
+ max_refinements: int = 1
806
+
807
+
808
+ # --- Component 3: Experiment Tracking Configuration ---
809
+ @dataclass
810
+ class TrackingConfig:
811
+ """Experiment tracking and logging (W&B, MLflow, or custom logger)."""
812
+
813
+ logger: LoggerProtocol | None = None
814
+ use_wandb: bool = False
815
+ wandb_api_key: str | None = None
816
+ wandb_init_kwargs: dict[str, Any] | None = None
817
+ wandb_attach_existing: bool = False
818
+ """Attach to an already-active W&B run without managing its lifecycle.
819
+
820
+ When ``True``, GEPA logs metrics and tables into the run that is already
821
+ active in the process (``wandb.run``) — it will not call ``wandb.init()``
822
+ on entry or ``wandb.finish()`` on exit.
823
+ """
824
+ wandb_step_metric: str | None = None
825
+ """Custom x-axis metric name for wandb charts.
826
+
827
+ When set, GEPA uses ``wandb.define_metric`` to declare a custom x-axis
828
+ for all its metrics, decoupling them from wandb's global monotonic step
829
+ counter. The ``step`` value passed to ``log_metrics`` is injected as a
830
+ regular metric (under this name) instead of being passed as ``step=``.
831
+
832
+ **Required when embedding GEPA inside a host training loop** that manages
833
+ its own wandb step counter. Without this, GEPA's ``step=1, 2, 3, ...``
834
+ collides with the host's ``step=100, 101, ...``, causing wandb to drop
835
+ GEPA's data.
836
+
837
+ Example::
838
+
839
+ TrackingConfig(
840
+ use_wandb=True,
841
+ wandb_attach_existing=True,
842
+ wandb_step_metric="gepa/iteration",
843
+ )
844
+ """
845
+ use_mlflow: bool = False
846
+ mlflow_tracking_uri: str | None = None
847
+ mlflow_experiment_name: str | None = None
848
+ mlflow_attach_existing: bool = False
849
+ """Attach to an already-active MLflow run without managing its lifecycle.
850
+
851
+ When ``True``, GEPA logs into the run that is already active (via
852
+ ``mlflow.active_run()``) — it will not call ``mlflow.start_run()`` on
853
+ entry or ``mlflow.end_run()`` on exit.
854
+
855
+ Use this when embedding GEPA inside a training loop that manages its own
856
+ MLflow run::
857
+
858
+ import mlflow
859
+ with mlflow.start_run(): # caller owns this run
860
+ result = optimize_anything(
861
+ ...,
862
+ config=GEPAConfig(
863
+ tracking=TrackingConfig(
864
+ use_mlflow=True,
865
+ mlflow_attach_existing=True,
866
+ )
867
+ ),
868
+ )
869
+ mlflow.log_metric("train/loss", 0.1) # still works
870
+ """
871
+ key_prefix: str = ""
872
+ """String prepended to every key/name logged to wandb and MLflow.
873
+
874
+ Applies uniformly to metric keys, config keys, summary keys, table names,
875
+ and HTML artifact keys. Useful when running multiple GEPA optimizations in
876
+ the same wandb/MLflow run to keep their data namespaced::
877
+
878
+ TrackingConfig(
879
+ use_wandb=True,
880
+ wandb_attach_existing=True,
881
+ key_prefix="gepa/round2/", # metrics become e.g. gepa/round2/val_score
882
+ )
883
+ """
884
+
885
+
886
+ @dataclass
887
+ class GEPAConfig:
888
+ """Top-level configuration for :func:`optimize_anything`.
889
+
890
+ Groups all settings into nested component configs. Sensible defaults are
891
+ provided — most users only need to set ``engine.max_metric_calls`` and
892
+ optionally ``reflection.reflection_lm``.
893
+
894
+ Example::
895
+
896
+ config = GEPAConfig(
897
+ engine=EngineConfig(max_metric_calls=200),
898
+ reflection=ReflectionConfig(reflection_lm="openai/gpt-5.1"),
899
+ refiner=RefinerConfig(max_refinements=2),
900
+ )
901
+ """
902
+
903
+ # Component configurations
904
+ engine: EngineConfig = field(default_factory=EngineConfig)
905
+ reflection: ReflectionConfig = field(default_factory=ReflectionConfig)
906
+ tracking: TrackingConfig = field(default_factory=TrackingConfig)
907
+
908
+ # Use 'None' to disable these optional components
909
+ merge: MergeConfig | None = None
910
+ refiner: RefinerConfig | None = None
911
+
912
+ # Complex callbacks that aren't serializable
913
+ stop_callbacks: StopperProtocol | Sequence[StopperProtocol] | None = None
914
+ callbacks: "list[GEPACallback] | None" = None
915
+ """Observation callbacks for monitoring optimization progress.
916
+
917
+ Receive events like ``on_optimization_start``, ``on_iteration_end``,
918
+ ``on_candidate_accepted``, ``on_proposal_end``, etc. See
919
+ :class:`~gepa.core.callbacks.GEPACallback` for the full protocol.
920
+
921
+ Example::
922
+
923
+ class MyCallback:
924
+ def on_candidate_accepted(self, event):
925
+ print(f"New candidate {event['new_candidate_idx']} accepted")
926
+
927
+ config = GEPAConfig(
928
+ callbacks=[MyCallback()],
929
+ engine=EngineConfig(max_metric_calls=100),
930
+ )
931
+ """
932
+
933
+ def __post_init__(self):
934
+ """Handle dicts passed in (e.g., from a JSON/YAML file)."""
935
+ if isinstance(self.engine, dict):
936
+ self.engine = EngineConfig(**self.engine)
937
+ if isinstance(self.reflection, dict):
938
+ self.reflection = ReflectionConfig(**self.reflection)
939
+ if isinstance(self.tracking, dict):
940
+ self.tracking = TrackingConfig(**self.tracking)
941
+ if isinstance(self.merge, dict):
942
+ self.merge = MergeConfig(**self.merge)
943
+ if isinstance(self.refiner, dict):
944
+ self.refiner = RefinerConfig(**self.refiner)
945
+
946
+ def to_dict(self) -> dict[str, Any]:
947
+ """Convert config to dictionary representation."""
948
+ return asdict(self)
949
+
950
+ @staticmethod
951
+ def from_dict(d: dict[str, Any]) -> "GEPAConfig":
952
+ """Create config from dictionary representation."""
953
+ return GEPAConfig(**d)
954
+
955
+
956
+ def make_litellm_lm(model_name: str, **kwargs: Any) -> LanguageModel:
957
+ """Convert a LiteLLM model name string to a :class:`LanguageModel` callable.
958
+
959
+ The returned callable conforms to the ``LanguageModel`` protocol and
960
+ accepts a plain ``str`` prompt, a ``list[dict]`` chat-messages list, or
961
+ a multimodal messages list (with content arrays containing images).
962
+
963
+ Uses :class:`gepa.lm.LM` which handles reasoning model detection
964
+ (o1/o3/o4/gpt-5), retries with exponential backoff, truncation
965
+ warnings, and ``drop_params=True`` for cross-model compatibility.
966
+
967
+ Args:
968
+ model_name: LiteLLM model identifier (e.g. ``"openai/gpt-5"``).
969
+ **kwargs: Extra keyword arguments forwarded to ``litellm.completion``
970
+ (e.g. ``reasoning_effort="high"``, ``temperature=0.7``).
971
+ """
972
+ from gepa.lm import LM
973
+
974
+ return LM(model_name, **kwargs)
975
+
976
+
977
+ class EvaluatorWrapper:
978
+ """Internal wrapper that adapts a user's evaluator to GEPA's internal interface.
979
+
980
+ Handles: single-instance mode (omits ``example``), str-candidate unwrapping,
981
+ kwarg filtering (incl. ``opt_state`` injection), ``oa.log()`` capture,
982
+ optional stdout/stderr capture, and normalizing the return value to
983
+ ``(score, output, side_info)`` regardless of what the user returns.
984
+ """
985
+
986
+ def __init__(
987
+ self,
988
+ evaluator_fn: Callable[..., Any],
989
+ single_instance_mode: bool,
990
+ capture_stdio: bool = False,
991
+ str_candidate_mode: bool = False,
992
+ raise_on_exception: bool = True,
993
+ ) -> None:
994
+ # Inspect the evaluator's signature once to determine which kwargs it accepts.
995
+ sig = inspect.signature(evaluator_fn)
996
+ has_var_keyword = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values())
997
+ if has_var_keyword:
998
+ accepted_params = None # accept all
999
+ else:
1000
+ accepted_params = set(sig.parameters.keys())
1001
+
1002
+ def _filter_kwargs(kwargs: dict) -> dict:
1003
+ if accepted_params is None:
1004
+ return kwargs
1005
+ return {k: v for k, v in kwargs.items() if k in accepted_params}
1006
+
1007
+ def wrapped_evaluator(
1008
+ candidate: Candidate, example: object | None = None, **kwargs: Any
1009
+ ) -> tuple[float, Any, SideInfo]:
1010
+ # Create a fresh, shared log context for this evaluator call.
1011
+ # The same LogContext is accessible from child threads via
1012
+ # oa.get_log_context() / oa.set_log_context().
1013
+ log_ctx = LogContext()
1014
+ _set_log_context(log_ctx)
1015
+
1016
+ # Build full kwargs dict. In single-instance mode, don't forward
1017
+ # example to the evaluator at all.
1018
+ if single_instance_mode:
1019
+ all_kwargs = kwargs
1020
+ else:
1021
+ all_kwargs = {"example": example, **kwargs}
1022
+
1023
+ filtered = _filter_kwargs(all_kwargs)
1024
+
1025
+ # Unwrap candidate for str_candidate_mode
1026
+ eval_candidate: Candidate | str = candidate
1027
+ if str_candidate_mode:
1028
+ eval_candidate = candidate[_STR_CANDIDATE_KEY]
1029
+
1030
+ # Acquire per-thread stream capture from the shared manager per-call.
1031
+ # This scopes the sys.stdout/stderr replacement to only the duration
1032
+ # of evaluator execution, restoring the originals between calls.
1033
+ # Both acquire/start_capture and the evaluator call are inside the
1034
+ # same try/finally so that stream_manager.release() is always called
1035
+ # even if start_capture() raises (e.g. assertion on double-capture).
1036
+ stdout_capturer: ThreadLocalStreamCapture | None = None
1037
+ stderr_capturer: ThreadLocalStreamCapture | None = None
1038
+ try:
1039
+ if capture_stdio:
1040
+ stdout_capturer, stderr_capturer = stream_manager.acquire()
1041
+ stdout_capturer.start_capture()
1042
+ stderr_capturer.start_capture()
1043
+
1044
+ result = evaluator_fn(eval_candidate, **filtered)
1045
+ except Exception as e:
1046
+ result = e # Sentinel; handled below after cleanup
1047
+ finally:
1048
+ captured_stdout = stdout_capturer.stop_capture() if stdout_capturer else ""
1049
+ captured_stderr = stderr_capturer.stop_capture() if stderr_capturer else ""
1050
+ if capture_stdio and stdout_capturer is not None:
1051
+ stream_manager.release()
1052
+ log_output = log_ctx.drain()
1053
+ _set_log_context(None)
1054
+
1055
+ # If evaluator raised, preserve captured diagnostics
1056
+ if isinstance(result, Exception):
1057
+ if raise_on_exception:
1058
+ raise result
1059
+ fail_side_info: SideInfo = {"error": str(result)}
1060
+ if log_output:
1061
+ fail_side_info["log"] = log_output
1062
+ if captured_stdout:
1063
+ fail_side_info["stdout"] = captured_stdout
1064
+ if captured_stderr:
1065
+ fail_side_info["stderr"] = captured_stderr
1066
+ return 0.0, None, fail_side_info
1067
+
1068
+ # Detect return type and normalize to (score, output, side_info)
1069
+ if isinstance(result, tuple):
1070
+ score, side_info = result
1071
+ side_info = dict(side_info) if side_info is not None else {}
1072
+
1073
+ # Inject captured output, renaming on collision with a warning
1074
+ injected: dict[str, str] = {}
1075
+ if log_output:
1076
+ injected["log"] = log_output
1077
+ if captured_stdout:
1078
+ injected["stdout"] = captured_stdout
1079
+ if captured_stderr:
1080
+ injected["stderr"] = captured_stderr
1081
+
1082
+ for key in list(injected):
1083
+ if key in side_info:
1084
+ prefixed = f"_gepa_{key}"
1085
+ warnings.warn(
1086
+ f"Your evaluator returned side_info with key '{key}' that conflicts "
1087
+ f"with GEPA's captured output key. The captured output will be stored "
1088
+ f"under '{prefixed}' instead.",
1089
+ stacklevel=2,
1090
+ )
1091
+ injected[prefixed] = injected.pop(key)
1092
+
1093
+ side_info.update(injected)
1094
+ return score, None, side_info
1095
+ else:
1096
+ score = result
1097
+ auto_side_info: SideInfo = {}
1098
+ if captured_stdout:
1099
+ auto_side_info["stdout"] = captured_stdout
1100
+ if captured_stderr:
1101
+ auto_side_info["stderr"] = captured_stderr
1102
+ if log_output:
1103
+ auto_side_info["log"] = log_output
1104
+ return score, None, auto_side_info
1105
+
1106
+ self._wrapped = wrapped_evaluator
1107
+
1108
+ def __call__(
1109
+ self, candidate: Candidate, example: object | None = None, **kwargs: Any
1110
+ ) -> tuple[float, Any, SideInfo]:
1111
+ return self._wrapped(candidate, example=example, **kwargs)
1112
+
1113
+
1114
+ def optimize_anything(
1115
+ seed_candidate: str | Candidate | None = None,
1116
+ *,
1117
+ evaluator: Callable[..., Any] | None = None,
1118
+ batch_evaluator: Callable[..., Any] | None = None,
1119
+ dataset: list[DataInst] | None = None,
1120
+ valset: list[DataInst] | None = None,
1121
+ objective: str | None = None,
1122
+ background: str | None = None,
1123
+ config: GEPAConfig | None = None,
1124
+ ) -> GEPAResult:
1125
+ """Optimize any text artifact using LLM-guided search.
1126
+
1127
+ This is the main entry point for GEPA. You declare the **what** — your
1128
+ artifact, your evaluator, and any domain knowledge — and
1129
+ ``optimize_anything`` handles the **how**: prompt construction, reflection,
1130
+ candidate selection, and Pareto-efficient search.
1131
+
1132
+ **Three optimization modes** (determined by ``dataset`` / ``valset``):
1133
+
1134
+ 1. **Single-Task Search** (``dataset=None, valset=None``):
1135
+ Solve one hard problem. The candidate *is* the solution.
1136
+ Evaluator called without ``example``.
1137
+ *E.g. circle packing, blackbox mathematical optimization.*
1138
+
1139
+ 2. **Multi-Task Search** (``dataset=<list>, valset=None``):
1140
+ Solve a batch of related problems with cross-task transfer.
1141
+ Insights from solving one help solve the others.
1142
+ ``valset`` defaults to ``dataset``.
1143
+ *E.g. CUDA kernel generation, multi-aspect SVG optimization.*
1144
+
1145
+ 3. **Generalization** (``dataset=<list>, valset=<list>``):
1146
+ Build a skill that transfers to unseen problems.
1147
+ *E.g. prompt optimization for AIME math, agent architecture evolution
1148
+ for ARC-AGI, cloud scheduling policy discovery.*
1149
+
1150
+ Args:
1151
+ seed_candidate: Starting point for optimization.
1152
+
1153
+ - ``str`` — single text parameter (evaluator receives ``str``).
1154
+ - ``dict[str, str]`` — named parameters (evaluator receives the dict).
1155
+ - ``None`` — **seedless mode**: the reflection LLM generates the
1156
+ initial candidate from ``objective`` (and optionally ``background``
1157
+ / ``dataset``). Requires ``objective``. Useful for creative or
1158
+ exploratory tasks where you know *what good looks like* but not
1159
+ where to begin.
1160
+
1161
+ evaluator: Scoring function. Returns ``(score, side_info)`` or ``score``.
1162
+ Optional when ``batch_evaluator`` is provided (at least one is
1163
+ required): without it, every evaluation — including refiner steps,
1164
+ as singleton batches — routes through ``batch_evaluator``.
1165
+ See :class:`Evaluator`. Diagnostic output via ``oa.log()`` is
1166
+ automatically captured as Actionable Side Information (ASI).
1167
+ For richer diagnostics, return a ``(score, dict)`` tuple with
1168
+ structured feedback, error messages, or even rendered images
1169
+ (via :class:`~gepa.Image`).
1170
+ batch_evaluator: Optional low-level batch evaluation hook. When set,
1171
+ GEPA hands you ALL pending ``(candidate, example)`` pairs in ONE
1172
+ call — e.g. to submit a provider batch job or fan out over your
1173
+ own infrastructure — instead of calling ``evaluator`` per pair.
1174
+ Return one result per pair, each ``score`` or
1175
+ ``(score, side_info)``. Parity with the standard path: candidates
1176
+ are unwrapped in str-candidate mode; if your function accepts an
1177
+ ``opt_states`` keyword it receives per-pair
1178
+ :class:`OptimizationState` objects (warm-starting; note they are
1179
+ snapshotted when the grouped call is issued, so pairs within one
1180
+ call do not see each other's fresh results); a raised call
1181
+ is converted to per-pair ``(0.0, {"error": ...})`` results when
1182
+ ``raise_on_exception=False``; best-evals history and output
1183
+ packaging match the sequential path exactly. NOT available on this
1184
+ path (structurally per-call features): ``oa.log()`` capture and
1185
+ stdout/stderr capture — put diagnostics in each returned
1186
+ ``side_info`` instead. When both are provided,
1187
+ multi-pair evaluations (minibatch stages, valset/seed/merge evals)
1188
+ prefer ``batch_evaluator`` while single-pair resolutions (refiner
1189
+ steps) use ``evaluator``; with only ``batch_evaluator``, singles
1190
+ route through it as singleton batches. Caching (``cache_evaluation``)
1191
+ applies identically on both paths, with a shared store: a pair
1192
+ cached by either path is a hit for the other. Note: with
1193
+ ``refiner_config`` + ``parallel=True`` and no ``evaluator``, your
1194
+ batch function may be called concurrently with singleton batches
1195
+ from the refinement thread pool — make it thread-safe.
1196
+ dataset: Examples for multi-task or generalization modes.
1197
+ ``None`` = single-task search mode.
1198
+ valset: Held-out validation set for generalization mode.
1199
+ ``None`` = defaults to ``dataset`` (multi-task search).
1200
+ objective: Natural-language goal for the reflection LLM (e.g.
1201
+ ``"Generate prompts that solve competition math problems."``).
1202
+ background: Domain knowledge, constraints, or strategies for the
1203
+ reflection LLM.
1204
+ config: Full configuration. See :class:`GEPAConfig`.
1205
+
1206
+ Returns:
1207
+ :class:`~gepa.core.result.GEPAResult` — access ``result.best_candidate``
1208
+ for the optimized parameter(s) and the full optimization history.
1209
+
1210
+ Examples:
1211
+
1212
+ Single-task search (circle packing)::
1213
+
1214
+ import gepa.optimize_anything as oa
1215
+
1216
+ def evaluate(candidate: str) -> float:
1217
+ result = run_code(candidate)
1218
+ oa.log(f"Score: {result.score}, Overlaps: {result.overlaps}")
1219
+ return result.score
1220
+
1221
+ result = optimize_anything(
1222
+ seed_candidate="def pack_circles(): ...",
1223
+ evaluator=evaluate,
1224
+ objective="Maximize the sum of radii for n circles in a unit square.",
1225
+ config=GEPAConfig(engine=EngineConfig(max_metric_calls=500)),
1226
+ )
1227
+
1228
+ Multi-task search (CUDA kernels)::
1229
+
1230
+ result = optimize_anything(
1231
+ seed_candidate={"prompt": "Write an optimized CUDA kernel."},
1232
+ evaluator=kernel_evaluator,
1233
+ dataset=kernel_problems, # batch of related problems
1234
+ objective="Generate prompts that produce fast, correct CUDA kernels.",
1235
+ config=GEPAConfig(engine=EngineConfig(max_metric_calls=300)),
1236
+ )
1237
+
1238
+ Generalization (prompt optimization for math)::
1239
+
1240
+ result = optimize_anything(
1241
+ seed_candidate={"prompt": "Solve this math problem step by step:"},
1242
+ evaluator=math_evaluator,
1243
+ dataset=train_problems, # train on these
1244
+ valset=val_problems, # must generalize to these
1245
+ objective="Generate system prompts that improve math reasoning.",
1246
+ config=GEPAConfig(engine=EngineConfig(max_metric_calls=200)),
1247
+ )
1248
+
1249
+ Seedless mode (no starting artifact)::
1250
+
1251
+ result = optimize_anything(
1252
+ seed_candidate=None, # LLM writes the first draft
1253
+ evaluator=evaluate_3d_render,
1254
+ dataset=visual_aspects,
1255
+ objective="Optimize a Python program to generate a 3D unicorn.",
1256
+ background="Use build123d for CSG geometry, export to STL, render with pyrender.",
1257
+ )
1258
+ """
1259
+ # Use default config if not provided
1260
+ if config is None:
1261
+ config = GEPAConfig()
1262
+
1263
+ # Detect seed generation mode: when seed_candidate is None, the LLM
1264
+ # will generate the initial candidate from the objective.
1265
+ needs_seed_generation = False
1266
+ if seed_candidate is None:
1267
+ needs_seed_generation = True
1268
+ str_candidate_mode = True
1269
+ if not objective or not objective.strip():
1270
+ raise ValueError(
1271
+ "'objective' is required when seed_candidate is None. "
1272
+ "The reflection LLM needs the objective to generate an initial candidate."
1273
+ )
1274
+ seed_candidate = {_STR_CANDIDATE_KEY: ""} # placeholder until LLM generates it
1275
+ else:
1276
+ # Normalize seed_candidate: str -> {_STR_CANDIDATE_KEY: str}
1277
+ str_candidate_mode = isinstance(seed_candidate, str)
1278
+ if isinstance(seed_candidate, str):
1279
+ seed_candidate = {_STR_CANDIDATE_KEY: seed_candidate}
1280
+
1281
+ # Detect single-instance mode: when both dataset=None and valset=None
1282
+ single_instance_mode = dataset is None and valset is None
1283
+
1284
+ # Set reflection_minibatch_size default based on mode (if not explicitly set)
1285
+ if config.reflection.reflection_minibatch_size is None:
1286
+ config.reflection.reflection_minibatch_size = 1 if single_instance_mode else 3
1287
+
1288
+ # Handle single-instance mode: when both dataset=None and valset=None, create a
1289
+ # dataset with a single sentinel element. The evaluator will be called
1290
+ # without the example parameter.
1291
+ if single_instance_mode:
1292
+ effective_dataset: list[DataInst] = [_SINGLE_INSTANCE_SENTINEL] # type: ignore[list-item]
1293
+ else:
1294
+ effective_dataset = dataset if dataset is not None else [None] # type: ignore[list-item]
1295
+
1296
+ # Wrap the evaluator to handle signature normalization, log/stdout capture, etc.
1297
+ if evaluator is None and batch_evaluator is None:
1298
+ raise ValueError(
1299
+ "Provide evaluator=, batch_evaluator=, or both. "
1300
+ "batch_evaluator alone is sufficient: single-pair resolutions "
1301
+ "(e.g. refiner steps) are routed through it as singleton batches."
1302
+ )
1303
+ if config.engine.capture_stdio and evaluator is None:
1304
+ warnings.warn(
1305
+ "capture_stdio=True has no effect without a single-pair evaluator: the batch_evaluator "
1306
+ "path cannot scope stdio capture to individual evaluations. Return diagnostics in "
1307
+ "side_info instead.",
1308
+ stacklevel=2,
1309
+ )
1310
+ wrapped_evaluator = None
1311
+ if evaluator is not None:
1312
+ wrapped_evaluator = EvaluatorWrapper(
1313
+ evaluator,
1314
+ single_instance_mode,
1315
+ capture_stdio=config.engine.capture_stdio,
1316
+ str_candidate_mode=str_candidate_mode,
1317
+ raise_on_exception=config.engine.raise_on_exception,
1318
+ )
1319
+
1320
+ # Resolve cache mode: cache_evaluation controls on/off, cache_evaluation_storage controls where
1321
+ if not config.engine.cache_evaluation:
1322
+ resolved_cache_mode = "off"
1323
+ if config.engine.cache_evaluation_storage != "auto":
1324
+ warnings.warn(
1325
+ f"cache_evaluation_storage={config.engine.cache_evaluation_storage!r} is set but "
1326
+ f"cache_evaluation=False, so caching is disabled. Set cache_evaluation=True to "
1327
+ f"enable caching with the specified storage mode.",
1328
+ stacklevel=2,
1329
+ )
1330
+ elif config.engine.cache_evaluation_storage == "auto":
1331
+ resolved_cache_mode = "disk" if config.engine.run_dir else "memory"
1332
+ else:
1333
+ resolved_cache_mode = config.engine.cache_evaluation_storage
1334
+
1335
+ # Validate disk mode requires run_dir
1336
+ if resolved_cache_mode == "disk" and not config.engine.run_dir:
1337
+ raise ValueError("cache_evaluation_storage='disk' requires run_dir in EngineConfig")
1338
+
1339
+ # Configure cloudpickle for code execution subprocess serialization
1340
+ from gepa.utils.code_execution import set_use_cloudpickle
1341
+
1342
+ set_use_cloudpickle(config.engine.use_cloudpickle)
1343
+
1344
+ active_adapter: GEPAAdapter = OptimizeAnythingAdapter(
1345
+ evaluator=wrapped_evaluator,
1346
+ parallel=config.engine.parallel,
1347
+ max_workers=config.engine.max_workers,
1348
+ refiner_config=config.refiner,
1349
+ best_example_evals_k=config.engine.best_example_evals_k,
1350
+ objective=objective,
1351
+ background=background,
1352
+ cache_mode=resolved_cache_mode,
1353
+ cache_dir=config.engine.run_dir,
1354
+ batch_evaluator=(
1355
+ BatchEvaluatorWrapper(
1356
+ batch_evaluator,
1357
+ str_candidate_key=_STR_CANDIDATE_KEY if str_candidate_mode else None,
1358
+ raise_on_exception=config.engine.raise_on_exception,
1359
+ single_instance_mode=single_instance_mode,
1360
+ )
1361
+ if batch_evaluator is not None
1362
+ else None
1363
+ ),
1364
+ )
1365
+
1366
+ # Normalize datasets to DataLoader instances
1367
+ train_loader = ensure_loader(effective_dataset)
1368
+ val_loader = ensure_loader(valset) if valset is not None else train_loader
1369
+
1370
+ # --- 1. Validate and setup reflection LM ---
1371
+ if needs_seed_generation and config.reflection.reflection_lm is None:
1372
+ raise ValueError(
1373
+ "reflection_lm is required when seed_candidate is None. "
1374
+ "Set config.reflection.reflection_lm to a model name or callable."
1375
+ )
1376
+ if not hasattr(active_adapter, "propose_new_texts"):
1377
+ assert config.reflection.reflection_lm is not None, (
1378
+ f"reflection_lm was not provided. The adapter '{active_adapter!s}' does not provide a propose_new_texts method, "
1379
+ + "and hence, GEPA will use the default proposer, which requires a reflection_lm to be specified."
1380
+ )
1381
+
1382
+ # Default refiner_lm to reflection_lm name BEFORE converting reflection_lm to callable
1383
+ if config.refiner is not None and config.refiner.refiner_lm is None:
1384
+ config.refiner.refiner_lm = config.reflection.reflection_lm
1385
+
1386
+ # Convert reflection_lm string to callable
1387
+ if isinstance(config.reflection.reflection_lm, str):
1388
+ config.reflection.reflection_lm = make_litellm_lm(
1389
+ config.reflection.reflection_lm, **(config.reflection.reflection_lm_kwargs or {})
1390
+ )
1391
+ elif config.reflection.reflection_lm is not None and not hasattr(config.reflection.reflection_lm, "total_cost"):
1392
+ from gepa.lm import TrackingLM
1393
+
1394
+ config.reflection.reflection_lm = TrackingLM(config.reflection.reflection_lm)
1395
+
1396
+ # --- 2. Build stoppers (all in one place, after LM conversion) ---
1397
+ stop_callbacks_list: list[StopperProtocol] = []
1398
+
1399
+ if config.stop_callbacks is not None:
1400
+ if isinstance(config.stop_callbacks, Sequence):
1401
+ stop_callbacks_list.extend(config.stop_callbacks)
1402
+ else:
1403
+ stop_callbacks_list.append(config.stop_callbacks)
1404
+
1405
+ if config.engine.run_dir is not None:
1406
+ stop_callbacks_list.append(FileStopper(os.path.join(config.engine.run_dir, "gepa.stop")))
1407
+
1408
+ if config.engine.max_metric_calls is not None:
1409
+ from gepa.utils import MaxMetricCallsStopper
1410
+
1411
+ stop_callbacks_list.append(MaxMetricCallsStopper(config.engine.max_metric_calls))
1412
+
1413
+ if config.engine.max_candidate_proposals is not None:
1414
+ from gepa.utils import MaxCandidateProposalsStopper
1415
+
1416
+ stop_callbacks_list.append(MaxCandidateProposalsStopper(config.engine.max_candidate_proposals))
1417
+
1418
+ if config.engine.max_reflection_cost is not None and config.reflection.reflection_strategy is not None:
1419
+ strategy = config.reflection.reflection_strategy
1420
+ if not hasattr(strategy, "total_cost"):
1421
+ raise ValueError(
1422
+ "max_reflection_cost is set but reflection_strategy does not expose total_cost — "
1423
+ "the cost stopper would silently never fire (unbounded reflection spend). Expose a "
1424
+ "total_cost property on the strategy, or remove max_reflection_cost."
1425
+ )
1426
+ if config.engine.max_reflection_cost is not None:
1427
+ from gepa.utils import MaxReflectionCostStopper
1428
+
1429
+ stop_callbacks_list.append(
1430
+ MaxReflectionCostStopper(config.engine.max_reflection_cost, reflection_lm=config.reflection.reflection_lm)
1431
+ )
1432
+
1433
+ if not stop_callbacks_list:
1434
+ raise ValueError(
1435
+ "At least one stopping condition must be provided via config.engine.max_metric_calls or config.stop_callbacks."
1436
+ )
1437
+
1438
+ stop_callback: StopperProtocol
1439
+ if len(stop_callbacks_list) == 1:
1440
+ stop_callback = stop_callbacks_list[0]
1441
+ else:
1442
+ from gepa.utils import CompositeStopper
1443
+
1444
+ stop_callback = CompositeStopper(*stop_callbacks_list)
1445
+
1446
+ # Convert refiner_lm string to LiteLLM callable (if refiner is enabled)
1447
+ if config.refiner is not None:
1448
+ if isinstance(config.refiner.refiner_lm, str):
1449
+ config.refiner.refiner_lm = make_litellm_lm(config.refiner.refiner_lm)
1450
+
1451
+ # Generate seed candidate via LLM if seed_candidate was None
1452
+ if needs_seed_generation:
1453
+ assert config.reflection.reflection_lm is not None and not isinstance(config.reflection.reflection_lm, str)
1454
+ assert objective is not None # validated earlier in needs_seed_generation block
1455
+ seed_candidate = _generate_seed_candidate(
1456
+ lm=config.reflection.reflection_lm,
1457
+ objective=objective,
1458
+ background=background,
1459
+ dataset=dataset,
1460
+ logger=config.tracking.logger or StdOutLogger(),
1461
+ )
1462
+
1463
+ # Auto-inject refiner_prompt into seed_candidate if refiner is enabled
1464
+ if config.refiner is not None:
1465
+ formatted_refiner_prompt = DEFAULT_REFINER_PROMPT.format(
1466
+ objective=objective or "Maximize the score",
1467
+ background=background or "No additional background provided.",
1468
+ )
1469
+ if "refiner_prompt" not in seed_candidate:
1470
+ seed_candidate["refiner_prompt"] = formatted_refiner_prompt
1471
+ # If user provides their own refiner_prompt, use it (allows custom refiner prompts)
1472
+
1473
+ # Setup default logger if not provided
1474
+ if config.tracking.logger is None:
1475
+ if config.engine.run_dir is not None:
1476
+ os.makedirs(config.engine.run_dir, exist_ok=True)
1477
+ config.tracking.logger = Logger(os.path.join(config.engine.run_dir, "run_log.txt"))
1478
+ else:
1479
+ config.tracking.logger = StdOutLogger()
1480
+
1481
+ # --- 3. Setup random number generator ---
1482
+ rng = random.Random(config.engine.seed)
1483
+
1484
+ # --- 4. Build candidate selector from EngineConfig ---
1485
+ candidate_selector: CandidateSelector
1486
+ if isinstance(config.engine.candidate_selection_strategy, str):
1487
+ factories = {
1488
+ "pareto": lambda: ParetoCandidateSelector(rng=rng),
1489
+ "current_best": lambda: CurrentBestCandidateSelector(),
1490
+ "epsilon_greedy": lambda: EpsilonGreedyCandidateSelector(epsilon=0.1, rng=rng),
1491
+ "top_k_pareto": lambda: TopKParetoCandidateSelector(k=5, rng=rng),
1492
+ }
1493
+
1494
+ try:
1495
+ candidate_selector = factories[config.engine.candidate_selection_strategy]()
1496
+ except KeyError as exc:
1497
+ raise ValueError(
1498
+ f"Unknown candidate_selector strategy: {config.engine.candidate_selection_strategy}. "
1499
+ "Supported strategies: 'pareto', 'current_best', 'epsilon_greedy', 'top_k_pareto'"
1500
+ ) from exc
1501
+ elif isinstance(config.engine.candidate_selection_strategy, CandidateSelector):
1502
+ candidate_selector = config.engine.candidate_selection_strategy
1503
+ else:
1504
+ raise TypeError(
1505
+ "candidate_selection_strategy must be a supported string strategy or an instance of CandidateSelector."
1506
+ )
1507
+
1508
+ # --- 5. Build evaluation policy from EngineConfig ---
1509
+ if config.engine.val_evaluation_policy is None or config.engine.val_evaluation_policy == "full_eval":
1510
+ config.engine.val_evaluation_policy = FullEvaluationPolicy()
1511
+ elif not isinstance(config.engine.val_evaluation_policy, EvaluationPolicy):
1512
+ raise ValueError(
1513
+ f"val_evaluation_policy should be 'full_eval' or an EvaluationPolicy instance, but got {type(config.engine.val_evaluation_policy)}"
1514
+ )
1515
+
1516
+ # --- 5b. Build acceptance criterion from EngineConfig ---
1517
+ acceptance_criterion_instance: AcceptanceCriterion
1518
+ if isinstance(config.engine.acceptance_criterion, str):
1519
+ acceptance_factories: dict[str, type[AcceptanceCriterion]] = {
1520
+ "strict_improvement": StrictImprovementAcceptance,
1521
+ "improvement_or_equal": ImprovementOrEqualAcceptance,
1522
+ }
1523
+ try:
1524
+ acceptance_criterion_instance = acceptance_factories[config.engine.acceptance_criterion]()
1525
+ except KeyError as exc:
1526
+ raise ValueError(
1527
+ f"Unknown acceptance_criterion: {config.engine.acceptance_criterion}. "
1528
+ "Supported strategies: 'strict_improvement', 'improvement_or_equal'"
1529
+ ) from exc
1530
+ elif isinstance(config.engine.acceptance_criterion, AcceptanceCriterion):
1531
+ acceptance_criterion_instance = config.engine.acceptance_criterion
1532
+ else:
1533
+ raise TypeError(
1534
+ "acceptance_criterion must be a supported string strategy or an instance of AcceptanceCriterion."
1535
+ )
1536
+
1537
+ # --- 6. Build module selector from ReflectionConfig ---
1538
+ if isinstance(config.reflection.module_selector, str):
1539
+ module_selector_cls = {
1540
+ "round_robin": RoundRobinReflectionComponentSelector,
1541
+ "all": AllReflectionComponentSelector,
1542
+ }.get(config.reflection.module_selector)
1543
+
1544
+ assert module_selector_cls is not None, (
1545
+ f"Unknown module_selector strategy: {config.reflection.module_selector}. "
1546
+ "Supported strategies: 'round_robin', 'all'"
1547
+ )
1548
+
1549
+ module_selector_instance: ReflectionComponentSelector = module_selector_cls()
1550
+ else:
1551
+ module_selector_instance = config.reflection.module_selector
1552
+
1553
+ # --- 7. Build batch sampler from ReflectionConfig ---
1554
+ if config.reflection.batch_sampler == "epoch_shuffled":
1555
+ config.reflection.batch_sampler = EpochShuffledBatchSampler(
1556
+ minibatch_size=config.reflection.reflection_minibatch_size, rng=rng
1557
+ )
1558
+
1559
+ # --- 8. Build experiment tracker from TrackingConfig ---
1560
+ experiment_tracker = create_experiment_tracker(
1561
+ use_wandb=config.tracking.use_wandb,
1562
+ wandb_api_key=config.tracking.wandb_api_key,
1563
+ wandb_init_kwargs=config.tracking.wandb_init_kwargs,
1564
+ wandb_attach_existing=config.tracking.wandb_attach_existing,
1565
+ wandb_step_metric=config.tracking.wandb_step_metric,
1566
+ use_mlflow=config.tracking.use_mlflow,
1567
+ mlflow_tracking_uri=config.tracking.mlflow_tracking_uri,
1568
+ mlflow_experiment_name=config.tracking.mlflow_experiment_name,
1569
+ mlflow_attach_existing=config.tracking.mlflow_attach_existing,
1570
+ key_prefix=config.tracking.key_prefix,
1571
+ )
1572
+
1573
+ # --- 9. Build reflection prompt template from objective/background if provided ---
1574
+ # Check for conflicting configuration: user cannot provide both objective/background
1575
+ # AND a custom reflection_prompt_template (these are mutually exclusive approaches)
1576
+ user_provided_custom_template = (
1577
+ config.reflection.reflection_prompt_template is not None
1578
+ and config.reflection.reflection_prompt_template != optimize_anything_reflection_prompt_template
1579
+ )
1580
+ # Treat empty strings as "not provided" - only non-empty strings count
1581
+ user_provided_objective_or_background = bool(objective) or bool(background)
1582
+
1583
+ if user_provided_custom_template and user_provided_objective_or_background:
1584
+ raise ValueError(
1585
+ "Cannot specify both 'objective'/'background' parameters and a custom "
1586
+ "'config.reflection.reflection_prompt_template'. These are mutually exclusive options. "
1587
+ "Either use objective/background to auto-generate a reflection prompt, or provide "
1588
+ "your own custom template via config.reflection.reflection_prompt_template."
1589
+ )
1590
+
1591
+ # If objective or background are provided, build a custom reflection prompt template
1592
+ # with those values filled in, creating a template with <curr_param> and <side_info> placeholders
1593
+ if user_provided_objective_or_background:
1594
+ config.reflection.reflection_prompt_template = _build_reflection_prompt_template(
1595
+ objective=objective, background=background
1596
+ )
1597
+
1598
+ # --- 10. Validate reflection prompt template ---
1599
+ if config.reflection.reflection_prompt_template is not None:
1600
+ assert not (active_adapter is not None and getattr(active_adapter, "propose_new_texts", None) is not None), (
1601
+ f"Adapter {active_adapter!s} provides its own propose_new_texts method; "
1602
+ "reflection_prompt_template will be ignored. Set reflection_prompt_template to None."
1603
+ )
1604
+
1605
+ # Validate template(s) - can be a single string or dict of templates
1606
+ from gepa.strategies.instruction_proposal import InstructionProposalSignature
1607
+
1608
+ if isinstance(config.reflection.reflection_prompt_template, dict):
1609
+ for param_name, template in config.reflection.reflection_prompt_template.items():
1610
+ try:
1611
+ InstructionProposalSignature.validate_prompt_template(template)
1612
+ except ValueError as e:
1613
+ raise ValueError(f"Invalid reflection_prompt_template for parameter '{param_name}': {e}") from e
1614
+ else:
1615
+ InstructionProposalSignature.validate_prompt_template(config.reflection.reflection_prompt_template)
1616
+
1617
+ # --- 11. Build reflective proposer from ReflectionConfig ---
1618
+ reflective_proposer = ReflectiveMutationProposer(
1619
+ logger=config.tracking.logger,
1620
+ trainset=train_loader,
1621
+ adapter=active_adapter,
1622
+ candidate_selector=candidate_selector,
1623
+ module_selector=module_selector_instance,
1624
+ batch_sampler=config.reflection.batch_sampler,
1625
+ perfect_score=config.reflection.perfect_score,
1626
+ skip_perfect_score=config.reflection.skip_perfect_score,
1627
+ experiment_tracker=experiment_tracker,
1628
+ reflection_lm=config.reflection.reflection_lm,
1629
+ reflection_prompt_template=config.reflection.reflection_prompt_template,
1630
+ custom_candidate_proposer=config.reflection.custom_candidate_proposer,
1631
+ callbacks=config.callbacks,
1632
+ sampling_strategy=config.engine.sampling_strategy,
1633
+ reflection_strategy=config.reflection.reflection_strategy,
1634
+ )
1635
+
1636
+ # Define evaluator function for merge proposer
1637
+ def merge_evaluator(
1638
+ inputs: list[DataInst], prog: Candidate
1639
+ ) -> tuple[list[object], list[float], list[dict[str, float]] | None]:
1640
+ eval_out = active_adapter.evaluate(inputs, prog, capture_traces=False)
1641
+ return eval_out.outputs, eval_out.scores, eval_out.objective_scores
1642
+
1643
+ # --- 12. Build merge proposer from MergeConfig (if provided) ---
1644
+ merge_proposer: MergeProposer | None = None
1645
+ if config.merge is not None:
1646
+ merge_proposer = MergeProposer(
1647
+ logger=config.tracking.logger,
1648
+ valset=val_loader,
1649
+ evaluator=merge_evaluator,
1650
+ use_merge=True,
1651
+ max_merge_invocations=config.merge.max_merge_invocations,
1652
+ rng=rng,
1653
+ val_overlap_floor=config.merge.merge_val_overlap_floor,
1654
+ callbacks=config.callbacks,
1655
+ )
1656
+
1657
+ # --- 13. Create evaluation cache if enabled ---
1658
+ evaluation_cache: EvaluationCache[Any, Any] | None = None
1659
+ if config.engine.cache_evaluation:
1660
+ evaluation_cache = EvaluationCache[Any, Any]()
1661
+
1662
+ # --- 14. Build the main engine from EngineConfig ---
1663
+ engine = GEPAEngine(
1664
+ adapter=active_adapter,
1665
+ run_dir=config.engine.run_dir,
1666
+ valset=val_loader,
1667
+ seed_candidate=seed_candidate,
1668
+ perfect_score=config.reflection.perfect_score,
1669
+ seed=config.engine.seed,
1670
+ reflective_proposer=reflective_proposer,
1671
+ merge_proposer=merge_proposer,
1672
+ frontier_type=config.engine.frontier_type,
1673
+ logger=config.tracking.logger,
1674
+ experiment_tracker=experiment_tracker,
1675
+ callbacks=config.callbacks,
1676
+ track_best_outputs=config.engine.track_best_outputs,
1677
+ display_progress_bar=config.engine.display_progress_bar,
1678
+ raise_on_exception=config.engine.raise_on_exception,
1679
+ stop_callback=stop_callback,
1680
+ val_evaluation_policy=config.engine.val_evaluation_policy,
1681
+ acceptance_criterion=acceptance_criterion_instance,
1682
+ selection_strategy=config.engine.selection_strategy,
1683
+ use_cloudpickle=config.engine.use_cloudpickle,
1684
+ evaluation_cache=evaluation_cache,
1685
+ )
1686
+
1687
+ # --- 16. Run optimization ---
1688
+ logger = config.tracking.logger
1689
+ with experiment_tracker:
1690
+ if isinstance(logger, Logger):
1691
+ with logger:
1692
+ state = engine.run()
1693
+ else:
1694
+ state = engine.run()
1695
+
1696
+ return GEPAResult.from_state(
1697
+ state,
1698
+ run_dir=config.engine.run_dir,
1699
+ seed=config.engine.seed,
1700
+ str_candidate_key=_STR_CANDIDATE_KEY if str_candidate_mode else None,
1701
+ )