agentevolve-optimizer 0.5.0__py3-none-any.whl

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 (414) hide show
  1. agent_evolve/__init__.py +722 -0
  2. agent_evolve/agentic.py +2800 -0
  3. agent_evolve/api.py +767 -0
  4. agent_evolve/application/__init__.py +1580 -0
  5. agent_evolve/application/action_allocation.py +744 -0
  6. agent_evolve/application/action_allocation_frame.py +347 -0
  7. agent_evolve/application/action_allocation_frame_commit.py +185 -0
  8. agent_evolve/application/action_allocation_frame_commit_v3.py +184 -0
  9. agent_evolve/application/action_allocation_frame_v3.py +338 -0
  10. agent_evolve/application/action_archive_value.py +497 -0
  11. agent_evolve/application/action_evidence_consistency.py +455 -0
  12. agent_evolve/application/action_forecast_partitioning.py +1471 -0
  13. agent_evolve/application/action_metric_projection.py +211 -0
  14. agent_evolve/application/action_role_value.py +680 -0
  15. agent_evolve/application/action_score_authorities.py +363 -0
  16. agent_evolve/application/action_structural_signature.py +116 -0
  17. agent_evolve/application/action_target_realization.py +402 -0
  18. agent_evolve/application/agentic_evolution.py +7734 -0
  19. agent_evolve/application/agentic_portfolio_residual_expert.py +835 -0
  20. agent_evolve/application/anchor_residual_identification.py +463 -0
  21. agent_evolve/application/archive_conditioned_action_target.py +208 -0
  22. agent_evolve/application/artifact_journal.py +246 -0
  23. agent_evolve/application/artifact_replay.py +347 -0
  24. agent_evolve/application/budgeted_optimizer.py +1828 -0
  25. agent_evolve/application/calibrated_campaign.py +485 -0
  26. agent_evolve/application/calibrated_current_prefix_forecast_opportunity.py +322 -0
  27. agent_evolve/application/calibrated_positive_gain_opportunity.py +1581 -0
  28. agent_evolve/application/campaign_capacity_recourse.py +254 -0
  29. agent_evolve/application/campaign_contextual_outcomes.py +119 -0
  30. agent_evolve/application/campaign_diagnostic_blocks.py +930 -0
  31. agent_evolve/application/campaign_evidence_registry.py +262 -0
  32. agent_evolve/application/campaign_execution.py +2537 -0
  33. agent_evolve/application/campaign_generation_audit.py +942 -0
  34. agent_evolve/application/campaign_learning.py +1812 -0
  35. agent_evolve/application/campaign_learning_runtime.py +1977 -0
  36. agent_evolve/application/campaign_search_phase.py +227 -0
  37. agent_evolve/application/campaign_selector_context_extension.py +220 -0
  38. agent_evolve/application/campaign_variation_envelope.py +649 -0
  39. agent_evolve/application/campaign_variation_trace.py +451 -0
  40. agent_evolve/application/candidate_archive_consequence.py +128 -0
  41. agent_evolve/application/causal_opportunity_portfolio_gate.py +385 -0
  42. agent_evolve/application/composite_outcome_updater.py +145 -0
  43. agent_evolve/application/composition_portfolio_selection.py +363 -0
  44. agent_evolve/application/concurrent_stage.py +144 -0
  45. agent_evolve/application/contextual_action_allocation.py +181 -0
  46. agent_evolve/application/contextual_campaign_outcomes.py +267 -0
  47. agent_evolve/application/contextual_campaign_planning.py +1366 -0
  48. agent_evolve/application/contextual_delayed_credit.py +651 -0
  49. agent_evolve/application/contextual_search_controller.py +2374 -0
  50. agent_evolve/application/current_prefix_forecast_opportunity.py +714 -0
  51. agent_evolve/application/decision_metric_projection.py +112 -0
  52. agent_evolve/application/derived_action_semantics.py +129 -0
  53. agent_evolve/application/detailed_evaluation.py +449 -0
  54. agent_evolve/application/earned_lineage.py +1011 -0
  55. agent_evolve/application/effective_choice_audit.py +484 -0
  56. agent_evolve/application/empirical_consequence_calibration.py +908 -0
  57. agent_evolve/application/evaluation_accounting.py +325 -0
  58. agent_evolve/application/evaluation_cache.py +199 -0
  59. agent_evolve/application/evaluation_escrow.py +547 -0
  60. agent_evolve/application/evaluation_recourse.py +253 -0
  61. agent_evolve/application/event_recorder.py +151 -0
  62. agent_evolve/application/evolution_campaign.py +1840 -0
  63. agent_evolve/application/executable_hypothesis.py +323 -0
  64. agent_evolve/application/factorial_branch_pilot.py +772 -0
  65. agent_evolve/application/finite_acquisition_capacity_recourse.py +672 -0
  66. agent_evolve/application/finite_acquisition_residual_expert.py +373 -0
  67. agent_evolve/application/finite_acquisition_variation_envelope.py +802 -0
  68. agent_evolve/application/finite_action_hypothesis_semantics.py +446 -0
  69. agent_evolve/application/finite_action_selection.py +188 -0
  70. agent_evolve/application/finite_action_set.py +306 -0
  71. agent_evolve/application/finite_action_transition.py +537 -0
  72. agent_evolve/application/finite_variation_eligibility.py +296 -0
  73. agent_evolve/application/forecast_geometry_portfolio.py +799 -0
  74. agent_evolve/application/forecast_opportunity_shadow_calibration.py +316 -0
  75. agent_evolve/application/front_proximity_admission.py +311 -0
  76. agent_evolve/application/front_proximity_parent_basis.py +458 -0
  77. agent_evolve/application/frozen_hurdle_score.py +659 -0
  78. agent_evolve/application/g3_causal_screen.py +2257 -0
  79. agent_evolve/application/g3_causal_validation.py +1046 -0
  80. agent_evolve/application/g3_postseal_curation.py +818 -0
  81. agent_evolve/application/gated_agentic_generator.py +205 -0
  82. agent_evolve/application/generation_feedback.py +293 -0
  83. agent_evolve/application/generative_proposal_journal.py +185 -0
  84. agent_evolve/application/geometry_conditional_elasticity.py +453 -0
  85. agent_evolve/application/global_wave_action_allocation.py +1151 -0
  86. agent_evolve/application/head_mass_conditional_seat.py +268 -0
  87. agent_evolve/application/identifiable_reflection_evidence.py +1147 -0
  88. agent_evolve/application/identifiable_reflection_learning.py +395 -0
  89. agent_evolve/application/identifiable_reflection_request.py +364 -0
  90. agent_evolve/application/in_memory_residual_archive.py +341 -0
  91. agent_evolve/application/insight_memory.py +1804 -0
  92. agent_evolve/application/live_runtime_manifest.py +758 -0
  93. agent_evolve/application/llm_task_queue.py +769 -0
  94. agent_evolve/application/matched_finite_action_block.py +409 -0
  95. agent_evolve/application/materialized_action_broker.py +2328 -0
  96. agent_evolve/application/materialized_action_constraints.py +83 -0
  97. agent_evolve/application/materialized_variation.py +211 -0
  98. agent_evolve/application/multi_option_evolution.py +1536 -0
  99. agent_evolve/application/outcome_adaptive_action_racing.py +2827 -0
  100. agent_evolve/application/outcome_adaptive_residual_campaign_runtime.py +580 -0
  101. agent_evolve/application/outcome_adaptive_residual_portfolio_evolution.py +3671 -0
  102. agent_evolve/application/outcome_conditioned_portfolio_selection.py +1374 -0
  103. agent_evolve/application/outcome_relation.py +193 -0
  104. agent_evolve/application/paired_allocation_comparison.py +241 -0
  105. agent_evolve/application/paired_block_schedule.py +127 -0
  106. agent_evolve/application/parent_measurement.py +226 -0
  107. agent_evolve/application/pareto_archive.py +811 -0
  108. agent_evolve/application/portfolio_campaign_runtime.py +4739 -0
  109. agent_evolve/application/portfolio_evolution.py +2950 -0
  110. agent_evolve/application/portfolio_hypothesis_observations.py +814 -0
  111. agent_evolve/application/portfolio_memory_attribution.py +581 -0
  112. agent_evolve/application/portfolio_memory_dose.py +788 -0
  113. agent_evolve/application/portfolio_memory_matched_control.py +938 -0
  114. agent_evolve/application/portfolio_memory_transfer.py +297 -0
  115. agent_evolve/application/portfolio_optimization_memory.py +363 -0
  116. agent_evolve/application/portfolio_outcome_feedback.py +1613 -0
  117. agent_evolve/application/portfolio_projection.py +335 -0
  118. agent_evolve/application/portfolio_recombination.py +2032 -0
  119. agent_evolve/application/post_evolution_reflection.py +834 -0
  120. agent_evolve/application/postcommit_rank_authority.py +245 -0
  121. agent_evolve/application/precommitted_portfolio_racing.py +2762 -0
  122. agent_evolve/application/prequential_archive_opportunity_calibration.py +1154 -0
  123. agent_evolve/application/prequential_residual_exploration.py +343 -0
  124. agent_evolve/application/prequential_score_portfolio.py +954 -0
  125. agent_evolve/application/projections.py +292 -0
  126. agent_evolve/application/protected_action_committee.py +1027 -0
  127. agent_evolve/application/protected_branch_pilot.py +376 -0
  128. agent_evolve/application/protected_current_prefix_forecast_opportunity.py +552 -0
  129. agent_evolve/application/provider_replay.py +910 -0
  130. agent_evolve/application/rank_balanced_causal_pilot.py +1372 -0
  131. agent_evolve/application/recombination_residual_expert.py +403 -0
  132. agent_evolve/application/reflection_workflow.py +571 -0
  133. agent_evolve/application/region_conditional_credit.py +911 -0
  134. agent_evolve/application/residual_campaign_runtime.py +531 -0
  135. agent_evolve/application/residual_headroom_campaign_runtime.py +459 -0
  136. agent_evolve/application/residual_headroom_ledger.py +1544 -0
  137. agent_evolve/application/residual_learning_transaction.py +396 -0
  138. agent_evolve/application/residual_portfolio_evolution.py +1228 -0
  139. agent_evolve/application/residual_reachability.py +749 -0
  140. agent_evolve/application/residual_stage_credit.py +499 -0
  141. agent_evolve/application/same_prefix_paired_audit.py +1580 -0
  142. agent_evolve/application/semantic_coverage_score_portfolio.py +838 -0
  143. agent_evolve/application/sequential_lineage_allocation.py +1017 -0
  144. agent_evolve/application/sequential_market_replay.py +1395 -0
  145. agent_evolve/application/sequential_residual_campaign_runtime.py +305 -0
  146. agent_evolve/application/sequential_residual_portfolio_evolution.py +940 -0
  147. agent_evolve/application/single_score_action_allocation.py +299 -0
  148. agent_evolve/application/source_exposure_allocation.py +906 -0
  149. agent_evolve/application/staged_memory.py +210 -0
  150. agent_evolve/application/stratified_cold_start_allocation.py +732 -0
  151. agent_evolve/application/support_guarded_hurdle_score.py +549 -0
  152. agent_evolve/application/target_conditioned_action_forecast.py +595 -0
  153. agent_evolve/application/target_conditioned_campaign.py +566 -0
  154. agent_evolve/application/treatment_assignment.py +201 -0
  155. agent_evolve/application/trusted_objective_evidence.py +217 -0
  156. agent_evolve/application/two_stage_action_evolution.py +1131 -0
  157. agent_evolve/application/v8lite_allocation_policy.py +1083 -0
  158. agent_evolve/application/v9_candidate_policy.py +1303 -0
  159. agent_evolve/bootstrap.py +108 -0
  160. agent_evolve/campaign_presets.py +517 -0
  161. agent_evolve/campaign_profiles.py +452 -0
  162. agent_evolve/campaign_variation_topology.py +288 -0
  163. agent_evolve/campaign_workload.py +950 -0
  164. agent_evolve/cli.py +797 -0
  165. agent_evolve/contract.py +241 -0
  166. agent_evolve/core/__init__.py +91 -0
  167. agent_evolve/core/action_semantics.py +411 -0
  168. agent_evolve/core/authored.py +105 -0
  169. agent_evolve/core/formatting.py +286 -0
  170. agent_evolve/core/optimization_semantics.py +324 -0
  171. agent_evolve/core/problem.py +167 -0
  172. agent_evolve/core/results.py +323 -0
  173. agent_evolve/core/stats.py +70 -0
  174. agent_evolve/core/telemetry.py +100 -0
  175. agent_evolve/domain/__init__.py +89 -0
  176. agent_evolve/domain/artifact.py +162 -0
  177. agent_evolve/domain/durable_text.py +68 -0
  178. agent_evolve/domain/event.py +1454 -0
  179. agent_evolve/domain/finite_action_set.py +426 -0
  180. agent_evolve/domain/finite_variation.py +526 -0
  181. agent_evolve/domain/generative_emission.py +559 -0
  182. agent_evolve/domain/ids.py +163 -0
  183. agent_evolve/domain/inline_text.py +106 -0
  184. agent_evolve/domain/insight.py +27 -0
  185. agent_evolve/domain/lineage.py +737 -0
  186. agent_evolve/domain/llm_task_queue.py +960 -0
  187. agent_evolve/domain/outcome.py +96 -0
  188. agent_evolve/domain/patch.py +854 -0
  189. agent_evolve/domain/typed_json.py +542 -0
  190. agent_evolve/domain/variation_space.py +158 -0
  191. agent_evolve/driver.py +1014 -0
  192. agent_evolve/harness/__init__.py +29 -0
  193. agent_evolve/harness/base.py +242 -0
  194. agent_evolve/harness/directives.py +163 -0
  195. agent_evolve/harness/generative_seal.py +479 -0
  196. agent_evolve/harness/registry.py +41 -0
  197. agent_evolve/infrastructure/__init__.py +39 -0
  198. agent_evolve/infrastructure/artifacts/__init__.py +6 -0
  199. agent_evolve/infrastructure/artifacts/_verification.py +67 -0
  200. agent_evolve/infrastructure/artifacts/filesystem.py +343 -0
  201. agent_evolve/infrastructure/artifacts/in_memory.py +73 -0
  202. agent_evolve/infrastructure/asyncio_runtime.py +109 -0
  203. agent_evolve/infrastructure/authored_runtime.py +188 -0
  204. agent_evolve/infrastructure/authored_worker.py +171 -0
  205. agent_evolve/infrastructure/clock.py +53 -0
  206. agent_evolve/infrastructure/events/__init__.py +6 -0
  207. agent_evolve/infrastructure/events/_validation.py +89 -0
  208. agent_evolve/infrastructure/events/in_memory.py +56 -0
  209. agent_evolve/infrastructure/events/jsonl.py +193 -0
  210. agent_evolve/infrastructure/exception_provenance.py +215 -0
  211. agent_evolve/infrastructure/ids.py +118 -0
  212. agent_evolve/infrastructure/lineage_codec.py +1836 -0
  213. agent_evolve/infrastructure/outcome_adaptive_phase_journal.py +170 -0
  214. agent_evolve/infrastructure/residual_headroom_journal.py +221 -0
  215. agent_evolve/infrastructure/resource_lease.py +370 -0
  216. agent_evolve/infrastructure/sanitization/__init__.py +8 -0
  217. agent_evolve/infrastructure/sanitization/strict_json.py +484 -0
  218. agent_evolve/infrastructure/sequential_phase_journal.py +170 -0
  219. agent_evolve/infrastructure/stream_liveness.py +383 -0
  220. agent_evolve/infrastructure/subprocess_boundary.py +136 -0
  221. agent_evolve/integrations/__init__.py +1 -0
  222. agent_evolve/integrations/botorch/__init__.py +28 -0
  223. agent_evolve/integrations/botorch/finite_qlognehvi.py +190 -0
  224. agent_evolve/integrations/botorch/finite_qlognehvi_batch.py +155 -0
  225. agent_evolve/integrations/botorch/finite_qlognehvi_batch_identity.py +20 -0
  226. agent_evolve/integrations/botorch/finite_qlognehvi_batch_worker.py +55 -0
  227. agent_evolve/integrations/botorch/finite_qlognehvi_identity.py +22 -0
  228. agent_evolve/integrations/botorch/finite_qlognehvi_worker.py +55 -0
  229. agent_evolve/integrations/botorch/subprocess_qlognehvi.py +261 -0
  230. agent_evolve/integrations/botorch/subprocess_qlognehvi_batch.py +273 -0
  231. agent_evolve/integrations/completion.py +242 -0
  232. agent_evolve/integrations/pydantic_ai/__init__.py +441 -0
  233. agent_evolve/integrations/pydantic_ai/action_forecast.py +1068 -0
  234. agent_evolve/integrations/pydantic_ai/agentic_generator.py +2308 -0
  235. agent_evolve/integrations/pydantic_ai/async_generator.py +1604 -0
  236. agent_evolve/integrations/pydantic_ai/boundary_codec.py +1526 -0
  237. agent_evolve/integrations/pydantic_ai/calibrated_portfolio_campaign.py +756 -0
  238. agent_evolve/integrations/pydantic_ai/calibrated_portfolio_selection.py +7537 -0
  239. agent_evolve/integrations/pydantic_ai/campaign_acquisition.py +609 -0
  240. agent_evolve/integrations/pydantic_ai/execution_binding.py +138 -0
  241. agent_evolve/integrations/pydantic_ai/forecast_geometry_action_committee.py +217 -0
  242. agent_evolve/integrations/pydantic_ai/harness.py +159 -0
  243. agent_evolve/integrations/pydantic_ai/heterogeneous_model_execution.py +306 -0
  244. agent_evolve/integrations/pydantic_ai/hierarchical_residual_adaptive_semantic_view.py +179 -0
  245. agent_evolve/integrations/pydantic_ai/json_schema_dialect.py +108 -0
  246. agent_evolve/integrations/pydantic_ai/materialized_hierarchical_residual_expert.py +952 -0
  247. agent_evolve/integrations/pydantic_ai/materialized_portfolio_judge.py +520 -0
  248. agent_evolve/integrations/pydantic_ai/model_execution_profile.py +659 -0
  249. agent_evolve/integrations/pydantic_ai/outbound_request_manifest.py +1170 -0
  250. agent_evolve/integrations/pydantic_ai/portable_residual_consequence_features.py +575 -0
  251. agent_evolve/integrations/pydantic_ai/portfolio_selection.py +422 -0
  252. agent_evolve/integrations/pydantic_ai/progress_aware_openrouter.py +416 -0
  253. agent_evolve/integrations/pydantic_ai/provider_attempt_join.py +1523 -0
  254. agent_evolve/integrations/pydantic_ai/provider_free_calibrated_runner.py +607 -0
  255. agent_evolve/integrations/pydantic_ai/queued_runner.py +2634 -0
  256. agent_evolve/integrations/pydantic_ai/reconciled_residual_reachability.py +1417 -0
  257. agent_evolve/integrations/pydantic_ai/residual_forecast_geometry.py +445 -0
  258. agent_evolve/integrations/pydantic_ai/residual_reachability.py +674 -0
  259. agent_evolve/integrations/pydantic_ai/residual_semantic_cells.py +239 -0
  260. agent_evolve/integrations/pydantic_ai/sealed_output_replay.py +1068 -0
  261. agent_evolve/integrations/pydantic_ai/semantic_coverage_residual_portfolio.py +770 -0
  262. agent_evolve/integrations/pydantic_ai/semantic_decision_replay.py +383 -0
  263. agent_evolve/integrations/pydantic_ai/support_adaptive_residual_portfolio.py +135 -0
  264. agent_evolve/integrations/pydantic_ai/trusted_residual_prompt_context.py +143 -0
  265. agent_evolve/integrations/pydantic_ai/validated_openrouter_model.py +107 -0
  266. agent_evolve/integrations/pymoo_adapter.py +242 -0
  267. agent_evolve/policies/__init__.py +17 -0
  268. agent_evolve/policies/check.py +469 -0
  269. agent_evolve/policies/emit_scaffold.py +451 -0
  270. agent_evolve/policies/feedback/__init__.py +37 -0
  271. agent_evolve/policies/feedback/held_out_asn.py +1325 -0
  272. agent_evolve/policies/genetic.py +607 -0
  273. agent_evolve/policies/llm_backoff.py +183 -0
  274. agent_evolve/policies/llm_chooser.py +226 -0
  275. agent_evolve/policies/llm_generator.py +1760 -0
  276. agent_evolve/policies/llm_init.py +267 -0
  277. agent_evolve/policies/llm_operator.py +109 -0
  278. agent_evolve/policies/llm_prior.py +194 -0
  279. agent_evolve/policies/llm_surrogate.py +334 -0
  280. agent_evolve/policies/measurement_evidence.py +704 -0
  281. agent_evolve/policies/memory/__init__.py +223 -0
  282. agent_evolve/policies/memory/balanced_subset_blocks.py +707 -0
  283. agent_evolve/policies/memory/compatibility_matching.py +593 -0
  284. agent_evolve/policies/memory/global_falsification.py +1841 -0
  285. agent_evolve/policies/memory/prompt_shape.py +503 -0
  286. agent_evolve/policies/memory/randomized_subset.py +714 -0
  287. agent_evolve/policies/memory/staged_causal.py +1270 -0
  288. agent_evolve/policies/memory/treatment_compliance.py +759 -0
  289. agent_evolve/policies/objective_resolution/__init__.py +17 -0
  290. agent_evolve/policies/objective_resolution/fixed_grid.py +364 -0
  291. agent_evolve/policies/operator_portfolio.py +407 -0
  292. agent_evolve/policies/reguidance.py +1133 -0
  293. agent_evolve/policies/reward/__init__.py +83 -0
  294. agent_evolve/policies/reward/affine_candidate_consequence.py +156 -0
  295. agent_evolve/policies/reward/affine_candidate_consequence_3d.py +159 -0
  296. agent_evolve/policies/reward/affine_hypervolume.py +490 -0
  297. agent_evolve/policies/reward/affine_hypervolume_3d.py +567 -0
  298. agent_evolve/policies/reward/contextual_marginal_utility.py +318 -0
  299. agent_evolve/policies/reward/frozen_archive.py +360 -0
  300. agent_evolve/policies/reward/frozen_wave_archive.py +368 -0
  301. agent_evolve/policies/search_state.py +208 -0
  302. agent_evolve/policies/selection/__init__.py +345 -0
  303. agent_evolve/policies/selection/acquisition_certified_slate.py +684 -0
  304. agent_evolve/policies/selection/affine_frontier_context.py +330 -0
  305. agent_evolve/policies/selection/affine_frontier_target.py +473 -0
  306. agent_evolve/policies/selection/archive_elite.py +1346 -0
  307. agent_evolve/policies/selection/calibrated_portfolio_binding.py +640 -0
  308. agent_evolve/policies/selection/calibrated_slate.py +1394 -0
  309. agent_evolve/policies/selection/calibrated_slate_codec.py +579 -0
  310. agent_evolve/policies/selection/common_candidate_pool.py +685 -0
  311. agent_evolve/policies/selection/diagnostic_sampling.py +319 -0
  312. agent_evolve/policies/selection/disjoint_pairs.py +479 -0
  313. agent_evolve/policies/selection/elite_explorer.py +719 -0
  314. agent_evolve/policies/selection/finite_action.py +187 -0
  315. agent_evolve/policies/selection/finite_option_prompt_projection.py +377 -0
  316. agent_evolve/policies/selection/finite_palette_evidence.py +247 -0
  317. agent_evolve/policies/selection/forecast_calibration.py +922 -0
  318. agent_evolve/policies/selection/frontier_probe_slate.py +814 -0
  319. agent_evolve/policies/selection/frozen_archive_pairs.py +762 -0
  320. agent_evolve/policies/selection/full_support_slate.py +91 -0
  321. agent_evolve/policies/selection/meaningful_direction.py +240 -0
  322. agent_evolve/policies/selection/memory_dose_feasibility.py +259 -0
  323. agent_evolve/policies/selection/model_anchored_slate.py +826 -0
  324. agent_evolve/policies/selection/phenotype_recourse.py +979 -0
  325. agent_evolve/policies/selection/proposal_support.py +368 -0
  326. agent_evolve/policies/selection/random_portfolio.py +254 -0
  327. agent_evolve/policies/selection/regret_bounded_slate.py +1084 -0
  328. agent_evolve/policies/selection/residual_frontier.py +463 -0
  329. agent_evolve/policies/selection/residual_frontier_target.py +605 -0
  330. agent_evolve/policies/selection/structural_posterior_slate.py +1571 -0
  331. agent_evolve/policies/selection/target_conditioned_allocator.py +648 -0
  332. agent_evolve/policies/selection/target_conditioned_features.py +812 -0
  333. agent_evolve/policies/selection/target_conditioned_prequential.py +1527 -0
  334. agent_evolve/policies/selection/task_keyed_palette.py +906 -0
  335. agent_evolve/policies/semantics.py +147 -0
  336. agent_evolve/policies/structure.py +362 -0
  337. agent_evolve/policies/structured_output_budget.py +62 -0
  338. agent_evolve/policies/surrogate.py +696 -0
  339. agent_evolve/policies/variation/__init__.py +1 -0
  340. agent_evolve/policies/variation/compositional_finite_catalog.py +426 -0
  341. agent_evolve/policies/variation/crossover_inheritance.py +575 -0
  342. agent_evolve/policies/variation/disjoint_recombination.py +611 -0
  343. agent_evolve/policies/variation/exact_composition_capacity.py +214 -0
  344. agent_evolve/policies/variation/exact_parent_crossover.py +950 -0
  345. agent_evolve/policies/variation/multiscale_restart_catalog.py +372 -0
  346. agent_evolve/policies/variation/source_union_finite_catalog.py +403 -0
  347. agent_evolve/policies/variation/typed_patch.py +1981 -0
  348. agent_evolve/policies/weighted_prior.py +394 -0
  349. agent_evolve/ports/__init__.py +383 -0
  350. agent_evolve/ports/action_allocation.py +733 -0
  351. agent_evolve/ports/action_allocation_frame.py +1153 -0
  352. agent_evolve/ports/action_allocation_frame_commit.py +294 -0
  353. agent_evolve/ports/action_allocation_frame_commit_v3.py +432 -0
  354. agent_evolve/ports/action_allocation_frame_v3.py +995 -0
  355. agent_evolve/ports/action_forecast.py +1568 -0
  356. agent_evolve/ports/action_metric_projection.py +165 -0
  357. agent_evolve/ports/agentic_generator.py +1561 -0
  358. agent_evolve/ports/archive_context.py +136 -0
  359. agent_evolve/ports/artifact_sanitizer.py +44 -0
  360. agent_evolve/ports/artifact_store.py +225 -0
  361. agent_evolve/ports/clock.py +13 -0
  362. agent_evolve/ports/contextual_search_allocation.py +827 -0
  363. agent_evolve/ports/decision_metric_projection.py +258 -0
  364. agent_evolve/ports/event_store.py +55 -0
  365. agent_evolve/ports/executable_hypothesis.py +557 -0
  366. agent_evolve/ports/finite_acquisition.py +377 -0
  367. agent_evolve/ports/finite_acquisition_batch.py +296 -0
  368. agent_evolve/ports/finite_acquisition_batch_json.py +164 -0
  369. agent_evolve/ports/finite_acquisition_json.py +247 -0
  370. agent_evolve/ports/finite_acquisition_space.py +168 -0
  371. agent_evolve/ports/finite_action_selection.py +348 -0
  372. agent_evolve/ports/finite_action_set.py +256 -0
  373. agent_evolve/ports/frontier_target.py +396 -0
  374. agent_evolve/ports/generation_failure.py +43 -0
  375. agent_evolve/ports/hard_feasibility.py +233 -0
  376. agent_evolve/ports/id_factory.py +34 -0
  377. agent_evolve/ports/llm_task_queue.py +93 -0
  378. agent_evolve/ports/objective_resolution.py +419 -0
  379. agent_evolve/ports/paired_allocation_comparison.py +401 -0
  380. agent_evolve/ports/paired_block_schedule.py +475 -0
  381. agent_evolve/ports/parent_measurement.py +336 -0
  382. agent_evolve/ports/portfolio_memory_dose.py +643 -0
  383. agent_evolve/ports/portfolio_selection.py +3169 -0
  384. agent_evolve/ports/postcommit_rank_authority.py +467 -0
  385. agent_evolve/ports/presented_action_evidence.py +794 -0
  386. agent_evolve/ports/resource_lease.py +162 -0
  387. agent_evolve/ports/structured_generator.py +734 -0
  388. agent_evolve/ports/structured_output_budget.py +120 -0
  389. agent_evolve/ports/subprocess_boundary.py +138 -0
  390. agent_evolve/ports/treatment_assignment.py +466 -0
  391. agent_evolve/ports/variation_catalog.py +76 -0
  392. agent_evolve/ports/variation_source.py +226 -0
  393. agent_evolve/proposal_mode.py +157 -0
  394. agent_evolve/proposers/__init__.py +10 -0
  395. agent_evolve/proposers/random_proposer.py +188 -0
  396. agent_evolve/provider_accounting.py +163 -0
  397. agent_evolve/py.typed +0 -0
  398. agent_evolve/reference_method.py +1570 -0
  399. agent_evolve/session/__init__.py +11 -0
  400. agent_evolve/session/authorship.py +864 -0
  401. agent_evolve/session/evaluate.py +236 -0
  402. agent_evolve/session/fidelity.py +237 -0
  403. agent_evolve/session/genetic_loop.py +742 -0
  404. agent_evolve/session/loop.py +803 -0
  405. agent_evolve/session/screening.py +671 -0
  406. agent_evolve/settings.py +376 -0
  407. agent_evolve/workload_kit.py +368 -0
  408. agent_evolve/workload_prompt.py +398 -0
  409. agentevolve_optimizer-0.5.0.dist-info/METADATA +599 -0
  410. agentevolve_optimizer-0.5.0.dist-info/RECORD +414 -0
  411. agentevolve_optimizer-0.5.0.dist-info/WHEEL +5 -0
  412. agentevolve_optimizer-0.5.0.dist-info/entry_points.txt +2 -0
  413. agentevolve_optimizer-0.5.0.dist-info/licenses/LICENSE +21 -0
  414. agentevolve_optimizer-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,607 @@
1
+ """Generic genetic operators over a typed candidate model.
2
+
3
+ The loop this package shipped with had no genetic operators at all: it rendered
4
+ the Pareto front as text and asked the model to author whole configurations.
5
+ That is whole-artifact rewrite, and it is measurably worse than uniform random
6
+ sampling on every genome length we have tested (advantage_theory sweep,
7
+ 2026-08-03: greedy authoring -0.086 to -0.531 excess capture, population with
8
+ recombination +0.0042 to +0.1798).
9
+
10
+ Everything here is workload-agnostic. A locus is an addressable position inside a
11
+ candidate, derived from the problem's own ``candidate_model``; no module in this
12
+ package knows what a locus *means*. Sequence-valued fields expand element-wise so
13
+ that recombination is defined where the structure actually varies, which is the
14
+ whole reason a genome differs from a paragraph of text.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import math
20
+ import random
21
+ from dataclasses import dataclass, field
22
+ from typing import Any, Iterable, Mapping, Protocol, Sequence
23
+
24
+ __all__ = [
25
+ "Locus",
26
+ "loci_of",
27
+ "read_locus",
28
+ "write_locus",
29
+ "locus_domain",
30
+ "locus_is_projected",
31
+ "SamplingPrior",
32
+ "DomainRestriction",
33
+ "crossover",
34
+ "mutate",
35
+ "uniform_candidate",
36
+ "tournament",
37
+ "truncation_survival",
38
+ ]
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class Locus:
43
+ """An addressable position in a candidate.
44
+
45
+ ``field`` names a top-level key; ``index`` is set only for sequence-valued
46
+ fields, where each element is independently heritable.
47
+ """
48
+
49
+ field: str
50
+ index: int | None = None
51
+
52
+ def __str__(self) -> str: # pragma: no cover - display only
53
+ return self.field if self.index is None else f"{self.field}[{self.index}]"
54
+
55
+
56
+ def loci_of(config: Mapping[str, Any]) -> tuple[Locus, ...]:
57
+ """Every heritable position in *config*, derived from the value shapes.
58
+
59
+ Scalars contribute one locus. Sequences contribute one per element, because
60
+ a genome whose only locus is "the whole list" cannot be recombined -- which
61
+ is exactly the degenerate case that makes whole-artifact rewrite the only
62
+ available move.
63
+ """
64
+
65
+ out: list[Locus] = []
66
+ for field, value in config.items():
67
+ if isinstance(value, (list, tuple)) and not isinstance(value, (str, bytes)):
68
+ out.extend(Locus(field, i) for i in range(len(value)))
69
+ else:
70
+ out.append(Locus(field))
71
+ return tuple(out)
72
+
73
+
74
+ def read_locus(config: Mapping[str, Any], locus: Locus) -> Any:
75
+ value = config[locus.field]
76
+ return value if locus.index is None else value[locus.index]
77
+
78
+
79
+ def write_locus(config: Mapping[str, Any], locus: Locus, value: Any) -> dict[str, Any]:
80
+ """Return a copy of *config* with *locus* set to *value*. Never mutates."""
81
+
82
+ out = {k: (list(v) if isinstance(v, (list, tuple)) and not isinstance(v, (str, bytes)) else v)
83
+ for k, v in config.items()}
84
+ if locus.index is None:
85
+ out[locus.field] = value
86
+ else:
87
+ out[locus.field][locus.index] = value
88
+ return out
89
+
90
+
91
+ class SamplingPrior(Protocol):
92
+ """What a sampler needs from a prior over WHERE to sample.
93
+
94
+ ``narrow`` confines a declared domain -- always to a subset, never to
95
+ nothing (see :class:`DomainRestriction` for the enforced invariants).
96
+ ``weights_for`` grades the values a draw is about to choose among;
97
+ returning ``None`` means "uniform", and the sampler then draws through
98
+ exactly the call it used before graded priors existed, so a prior with
99
+ nothing to say costs nothing and changes nothing.
100
+ """
101
+
102
+ def narrow(self, locus: "Locus", domain: tuple[Any, ...]) -> tuple[Any, ...]:
103
+ ...
104
+
105
+ def weights_for(
106
+ self, locus: "Locus", values: Sequence[Any]
107
+ ) -> "tuple[float, ...] | None":
108
+ ...
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class DomainRestriction:
113
+ """A narrowing of declared locus domains -- a prior over where to sample.
114
+
115
+ Every operator that draws a value reads its domain through
116
+ :func:`locus_domain`, so restricting there restricts initialization and
117
+ mutation together without either operator knowing a restriction exists.
118
+
119
+ Two invariants, both enforced rather than documented. A restriction can only
120
+ ever *narrow*: the result is always a subset of what the problem declared,
121
+ because a caller that could widen a domain would be authoring values the
122
+ problem never allowed. And it can never empty a domain: an empty
123
+ intersection means the restriction is about a schema it does not match, so
124
+ the declared domain is returned unchanged and the miss is counted in
125
+ :attr:`misses` for the caller to act on. Silently sampling nothing, or
126
+ sampling outside the schema, are both worse than ignoring a bad prior.
127
+ """
128
+
129
+ allowed: Mapping[str, Sequence[Any]] = field(default_factory=dict)
130
+ misses: list[str] = field(default_factory=list, compare=False, repr=False)
131
+
132
+ def narrow(self, locus: "Locus", domain: tuple[Any, ...]) -> tuple[Any, ...]:
133
+ want = self.allowed.get(locus.field)
134
+ if want is None or not domain:
135
+ return domain
136
+ kept = tuple(v for v in domain if v in tuple(want))
137
+ if not kept:
138
+ self.misses.append(locus.field)
139
+ return domain
140
+ return kept
141
+
142
+ def weights_for(self, locus: "Locus", values: Sequence[Any]) -> None:
143
+ """Uniform over the narrowed support: the hard 0/1 special case."""
144
+
145
+ return None
146
+
147
+
148
+ def locus_domain(
149
+ candidate_model: Any,
150
+ locus: Locus,
151
+ *,
152
+ restriction: "SamplingPrior | None" = None,
153
+ ) -> tuple[Any, ...]:
154
+ """Allowed values for *locus*, read from the problem's own schema.
155
+
156
+ Returns ``()`` when the schema does not constrain the value to a finite set;
157
+ callers fall back to recombination-only for that locus rather than inventing
158
+ a domain the problem never declared.
159
+
160
+ *restriction* narrows the declared domain (see :class:`DomainRestriction`).
161
+ Omitted, the result is exactly what the schema declares.
162
+ """
163
+
164
+ domain = _declared_domain(candidate_model, locus)
165
+ if restriction is None:
166
+ return domain
167
+ return restriction.narrow(locus, domain)
168
+
169
+
170
+ #: Points a bounded numeric axis is projected onto when the schema says nothing
171
+ #: else. Integers get 64: an axis like ``ge=0, le=1000`` keeps a tenth of a
172
+ #: percent of resolution while its domain still renders in a prompt. Numbers get
173
+ #: 16, which is the grid ``from_pymoo`` has quantized continuous decision
174
+ #: variables onto since it shipped -- one resolution rule for the package, not
175
+ #: two. Both are overridable per field (see :func:`_grid_override`).
176
+ _INTEGER_GRID = 64
177
+ _NUMBER_GRID = 16
178
+
179
+ #: A per-field grid outside this range is not a search space: one point is a
180
+ #: constant, and a domain nobody can render or enumerate cheaply is not a
181
+ #: domain an operator can draw from.
182
+ _GRID_MIN, _GRID_MAX = 2, 256
183
+
184
+ #: Slack for reading multiples off a float bound, whose decimal literal in the
185
+ #: schema (0.3) is not the binary value the arithmetic produces.
186
+ _TOL = 1e-12
187
+
188
+
189
+ def _quantum(value: float) -> float:
190
+ """A grid float rendered the way every reader will see it.
191
+
192
+ Ten significant digits: enough that no two points of a 256-point grid
193
+ collide, few enough that ``repr`` is stable, so a value in a prompt, a value
194
+ in a card and a value compared against a domain are the same string and the
195
+ same float.
196
+ """
197
+
198
+ return float(f"{value:.10g}")
199
+
200
+
201
+ def _bound(node: Mapping[str, Any], key: str) -> float | None:
202
+ """A real numeric bound under *key*, or ``None``. ``True`` is not a bound."""
203
+
204
+ value = node.get(key)
205
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
206
+ return None
207
+ return float(value) if math.isfinite(float(value)) else None
208
+
209
+
210
+ def _grid_override(node: Mapping[str, Any]) -> int | None:
211
+ """The field's own point count, from ``{"agent_evolve": {"grid": N}}``.
212
+
213
+ Written by the problem author as
214
+ ``Field(json_schema_extra={"agent_evolve": {"grid": N}})``: the one place a
215
+ schema can say how finely *this* axis deserves to be searched.
216
+ """
217
+
218
+ extra = node.get("agent_evolve")
219
+ if isinstance(extra, Mapping):
220
+ wanted = extra.get("grid")
221
+ if isinstance(wanted, int) and not isinstance(wanted, bool):
222
+ return max(_GRID_MIN, min(_GRID_MAX, wanted))
223
+ return None
224
+
225
+
226
+ def _spread(count: int, points: int) -> tuple[int, ...]:
227
+ """*points* indices spread over ``range(count)``, both extremes included.
228
+
229
+ Fewer values than points means every value: a projection never invents
230
+ resolution the range does not have.
231
+ """
232
+
233
+ if count <= points:
234
+ return tuple(range(count))
235
+ return tuple(dict.fromkeys(
236
+ round(i * (count - 1) / (points - 1)) for i in range(points)
237
+ ))
238
+
239
+
240
+ def _multiple_indices(
241
+ lo: float, hi: float, step: float, *, open_lo: bool, open_hi: bool
242
+ ) -> tuple[int, int]:
243
+ """First and last multiple-of-*step* indices inside the interval."""
244
+
245
+ if isinstance(lo, int) and isinstance(hi, int) and isinstance(step, int):
246
+ first, last = -((-lo) // step), hi // step # exact for ints of any size
247
+ else:
248
+ first = math.ceil(lo / step - _TOL)
249
+ last = math.floor(hi / step + _TOL)
250
+ if open_lo and first * step <= lo:
251
+ first += 1
252
+ if open_hi and last * step >= hi:
253
+ last -= 1
254
+ return first, last
255
+
256
+
257
+ def _integer_domain(node: Mapping[str, Any], points: int) -> tuple[int, ...]:
258
+ """Every integer the bounds allow, or *points* of them evenly spaced.
259
+
260
+ ``exclusiveMinimum``/``exclusiveMaximum`` tighten by one, which is what they
261
+ mean on an integer axis. ``multipleOf`` restricts to multiples, so the
262
+ emitted values are ones the schema would actually accept.
263
+ """
264
+
265
+ lows: list[int] = []
266
+ highs: list[int] = []
267
+ closed_lo = _bound(node, "minimum")
268
+ if closed_lo is not None:
269
+ lows.append(math.ceil(closed_lo))
270
+ open_lo = _bound(node, "exclusiveMinimum")
271
+ if open_lo is not None:
272
+ lows.append(math.floor(open_lo) + 1)
273
+ closed_hi = _bound(node, "maximum")
274
+ if closed_hi is not None:
275
+ highs.append(math.floor(closed_hi))
276
+ open_hi = _bound(node, "exclusiveMaximum")
277
+ if open_hi is not None:
278
+ highs.append(math.ceil(open_hi) - 1)
279
+ if not lows or not highs:
280
+ return () # unbounded on one side: no finite reading
281
+ lo, hi = max(lows), min(highs)
282
+ step = 1
283
+ multiple = _bound(node, "multipleOf")
284
+ if multiple is not None:
285
+ if multiple <= 0 or not float(multiple).is_integer():
286
+ return () # no integer axis reading of this step
287
+ step = int(multiple)
288
+ first, last = _multiple_indices(lo, hi, step, open_lo=False, open_hi=False)
289
+ if first > last:
290
+ return () # the bounds admit nothing
291
+ return tuple((first + i) * step for i in _spread(last - first + 1, points))
292
+
293
+
294
+ def _number_domain(node: Mapping[str, Any], points: int) -> tuple[float, ...]:
295
+ """A *points*-point grid across a doubly bounded continuous axis.
296
+
297
+ Inclusive bounds put a point on each endpoint. An excluded endpoint is not
298
+ a legal value, so it becomes one more interval instead: an open interval of
299
+ ``points`` interior points sits on ``points + 1`` intervals.
300
+ """
301
+
302
+ lo, open_lo = _bound(node, "minimum"), False
303
+ tighter = _bound(node, "exclusiveMinimum")
304
+ if tighter is not None and (lo is None or tighter >= lo):
305
+ lo, open_lo = tighter, True
306
+ hi, open_hi = _bound(node, "maximum"), False
307
+ tighter = _bound(node, "exclusiveMaximum")
308
+ if tighter is not None and (hi is None or tighter <= hi):
309
+ hi, open_hi = tighter, True
310
+ if lo is None or hi is None or hi < lo:
311
+ return () # one-sided or empty: nothing finite
312
+ multiple = _bound(node, "multipleOf")
313
+ if multiple is not None:
314
+ if multiple <= 0:
315
+ return ()
316
+ first, last = _multiple_indices(lo, hi, multiple,
317
+ open_lo=open_lo, open_hi=open_hi)
318
+ if first > last:
319
+ return ()
320
+ return tuple(dict.fromkeys(
321
+ _quantum((first + i) * multiple)
322
+ for i in _spread(last - first + 1, points)
323
+ ))
324
+ if lo == hi:
325
+ return () if (open_lo or open_hi) else (_quantum(lo),)
326
+ edges = points + int(open_lo) + int(open_hi)
327
+ start = 1 if open_lo else 0
328
+ return tuple(dict.fromkeys(
329
+ _quantum(lo + (hi - lo) * i / (edges - 1))
330
+ for i in range(start, start + points)
331
+ ))
332
+
333
+
334
+ def _node_domain(
335
+ node: Mapping[str, Any], override: int | None
336
+ ) -> tuple[tuple[Any, ...], bool]:
337
+ """One schema node's finite domain, and whether it was projected.
338
+
339
+ Projected means "read off bounds": the schema declared a range, not a set of
340
+ values, and what comes back is a finite sample of that range. Enumerations,
341
+ booleans and constants are declared outright and are not projections.
342
+ """
343
+
344
+ if "enum" in node:
345
+ return tuple(node["enum"]), False
346
+ if "const" in node:
347
+ # A one-value domain is still a domain. Read as nothing, a Literal of
348
+ # length one freezes its locus and every reader calls it undeclared.
349
+ return (node["const"],), False
350
+ declared = node.get("type")
351
+ types = tuple(declared) if isinstance(declared, list) else (declared,)
352
+ if "boolean" in types:
353
+ # A boolean is a finite domain. Without this, a bool field has no
354
+ # declared values, mutate() leaves it alone, and a real design axis is
355
+ # silently frozen at whatever the seed happened to carry.
356
+ return (False, True), False
357
+ points = _grid_override(node) or override
358
+ if "integer" in types:
359
+ return _integer_domain(node, points or _INTEGER_GRID), True
360
+ if "number" in types:
361
+ return _number_domain(node, points or _NUMBER_GRID), True
362
+ return (), False
363
+
364
+
365
+ def _declared_domain_detail(
366
+ candidate_model: Any, locus: Locus
367
+ ) -> tuple[tuple[Any, ...], bool]:
368
+ """The schema's own domain for *locus*, and whether it is a projection."""
369
+
370
+ if candidate_model is None:
371
+ return (), False
372
+ try:
373
+ schema = candidate_model.model_json_schema()
374
+ except Exception: # not a pydantic model
375
+ return (), False
376
+ defs = schema.get("$defs", {}) or schema.get("definitions", {}) or {}
377
+ node = (schema.get("properties", {}) or {}).get(locus.field)
378
+ if node is None:
379
+ return (), False
380
+
381
+ def resolve(n: Any) -> Any:
382
+ seen = 0
383
+ while isinstance(n, dict) and "$ref" in n and seen < 10:
384
+ ref = n["$ref"].rsplit("/", 1)[-1]
385
+ n = defs.get(ref, {})
386
+ seen += 1
387
+ return n
388
+
389
+ node = resolve(node)
390
+ if locus.index is not None:
391
+ node = resolve(node.get("items", {}) if isinstance(node, dict) else {})
392
+ elif isinstance(node, dict) and node.get("type") == "array":
393
+ # A bare Locus("field") naming a sequence field means the field's
394
+ # shared per-element vocabulary -- what pooled attribution and
395
+ # field-keyed priors reason over. Indexed loci resolve above.
396
+ node = resolve(node.get("items", {}))
397
+ if not isinstance(node, dict):
398
+ return (), False
399
+ override = _grid_override(node) # a wrapper carries the field's own grid
400
+ domain, projected = _node_domain(node, override)
401
+ if domain:
402
+ return domain, projected
403
+ for key in ("anyOf", "oneOf"):
404
+ for branch in node.get(key, ()) or ():
405
+ branch = resolve(branch)
406
+ if isinstance(branch, dict) and "enum" in branch:
407
+ return tuple(branch["enum"]), False
408
+ for key in ("anyOf", "oneOf"):
409
+ for branch in node.get(key, ()) or ():
410
+ branch = resolve(branch)
411
+ # A "null" branch is Optional's absence marker, not a value to
412
+ # sample: writing None where the problem declared a range is how a
413
+ # sampler produces candidates the problem never allowed.
414
+ if not isinstance(branch, dict) or branch.get("type") == "null":
415
+ continue
416
+ domain, projected = _node_domain(branch, override)
417
+ if domain:
418
+ return domain, projected
419
+ return (), False
420
+
421
+
422
+ def _declared_domain(candidate_model: Any, locus: Locus) -> tuple[Any, ...]:
423
+ """The schema's own domain for *locus*, before any narrowing."""
424
+
425
+ return _declared_domain_detail(candidate_model, locus)[0]
426
+
427
+
428
+ def locus_is_projected(candidate_model: Any, locus: Locus) -> bool:
429
+ """True when *locus*'s domain is a finite projection of a declared range.
430
+
431
+ A projected domain is searchable but lossy: the optimizer moves on the grid,
432
+ not the continuum. Reports that name domain sizes say so, because "16
433
+ values" means something different when the schema declared 16 values than
434
+ when it declared an interval.
435
+ """
436
+
437
+ domain, projected = _declared_domain_detail(candidate_model, locus)
438
+ return bool(domain) and projected
439
+
440
+
441
+ def _draw(
442
+ r: random.Random,
443
+ values: tuple[Any, ...],
444
+ locus: Locus,
445
+ restriction: "SamplingPrior | None",
446
+ ) -> Any:
447
+ """Draw one of *values*, honouring the prior's weights when it has any.
448
+
449
+ The unweighted path is literally ``r.choice`` -- same call, same RNG
450
+ stream -- which is what keeps every run without a graded prior
451
+ byte-identical to the pre-weights seam (the fossil test holds this).
452
+ """
453
+
454
+ weights = None
455
+ if restriction is not None:
456
+ weigher = getattr(restriction, "weights_for", None)
457
+ if callable(weigher):
458
+ weights = weigher(locus, values)
459
+ if weights is None:
460
+ return r.choice(values)
461
+ return r.choices(values, weights=weights, k=1)[0]
462
+
463
+
464
+ def crossover(
465
+ parent_a: Mapping[str, Any],
466
+ parent_b: Mapping[str, Any],
467
+ *,
468
+ mask: Sequence[bool] | None = None,
469
+ rng: random.Random | None = None,
470
+ ) -> dict[str, Any]:
471
+ """Recombine two parents locus by locus.
472
+
473
+ ``mask[i]`` true takes locus *i* from *parent_b*. A caller-supplied mask is
474
+ how an external chooser -- a model, a heuristic -- expresses *where to cut*
475
+ without ever authoring a candidate. With no mask, each locus is drawn
476
+ independently at random, which is the unguided control.
477
+
478
+ The mask is indexed by *parent_a*'s loci, and its length must match them:
479
+ a mask of the wrong length is a caller bug and is refused by name rather
480
+ than silently reinterpreted.
481
+
482
+ **Ragged genomes.** When a field holds a sequence, each element is a locus,
483
+ so two candidates of the same problem may have different genome lengths.
484
+ A locus that *parent_b* does not have cannot be inherited from it, so the
485
+ child keeps *parent_a*'s value there. Recombination is therefore defined on
486
+ the loci the parents share, which reduces exactly to the fixed-length case
487
+ when the shapes match.
488
+ """
489
+
490
+ loci = loci_of(parent_a)
491
+ if mask is None:
492
+ r = rng or random.Random()
493
+ mask = [bool(r.getrandbits(1)) for _ in loci]
494
+ if len(mask) != len(loci):
495
+ raise ValueError(
496
+ f"mask has {len(mask)} bits but the candidate has {len(loci)} loci"
497
+ )
498
+ donor = frozenset(loci_of(parent_b))
499
+ child = dict(parent_a)
500
+ for locus, take_b in zip(loci, mask):
501
+ if take_b and locus in donor:
502
+ child = write_locus(child, locus, read_locus(parent_b, locus))
503
+ return child
504
+
505
+
506
+ def mutate(
507
+ config: Mapping[str, Any],
508
+ candidate_model: Any,
509
+ *,
510
+ rate: float | None = None,
511
+ loci: Iterable[Locus] | None = None,
512
+ rng: random.Random | None = None,
513
+ restriction: SamplingPrior | None = None,
514
+ ) -> dict[str, Any]:
515
+ """Resample loci from their declared domains.
516
+
517
+ *loci* names exactly which positions to change -- how an external chooser
518
+ expresses *which loci* without authoring a candidate. Otherwise every locus
519
+ is resampled independently with probability *rate*, defaulting to 1/n so a
520
+ candidate changes in about one place.
521
+ """
522
+
523
+ r = rng or random.Random()
524
+ all_loci = loci_of(config)
525
+ targets = tuple(loci) if loci is not None else tuple(
526
+ lc for lc in all_loci
527
+ if r.random() < (rate if rate is not None else 1.0 / max(1, len(all_loci)))
528
+ )
529
+ out = dict(config)
530
+ for locus in targets:
531
+ domain = locus_domain(candidate_model, locus, restriction=restriction)
532
+ if not domain:
533
+ continue # undeclared domain: leave it alone
534
+ current = read_locus(out, locus)
535
+ choices = tuple(v for v in domain if v != current) or domain
536
+ out = write_locus(out, locus, _draw(r, choices, locus, restriction))
537
+ return out
538
+
539
+
540
+ def uniform_candidate(
541
+ template: Mapping[str, Any],
542
+ candidate_model: Any,
543
+ *,
544
+ rng: random.Random | None = None,
545
+ restriction: SamplingPrior | None = None,
546
+ ) -> dict[str, Any]:
547
+ """A fresh draw over every locus with a declared domain.
548
+
549
+ Loci the schema does not constrain keep the template's value: inventing
550
+ values the problem never declared is not sampling, it is guessing. When no
551
+ locus declares a domain at all, fall back to :func:`mutate` so the caller
552
+ still gets diversity rather than a copy.
553
+
554
+ This exists because filling a population with *mutants of one seed* builds
555
+ an anchored cloud around that seed. Measured on a third-party optimizer,
556
+ correcting exactly this anchor moved its result from +0.095 (loses badly to
557
+ uniform) to +0.0066 (parity) -- and the standard seed on at least one of
558
+ our own workloads scores worse than a typical uniform draw.
559
+ """
560
+
561
+ r = rng or random.Random()
562
+ out = dict(template)
563
+ drew = False
564
+ for locus in loci_of(template):
565
+ domain = locus_domain(candidate_model, locus, restriction=restriction)
566
+ if domain:
567
+ out = write_locus(out, locus, _draw(r, domain, locus, restriction))
568
+ drew = True
569
+ if not drew:
570
+ return mutate(template, candidate_model, rng=r, restriction=restriction)
571
+ return out
572
+
573
+
574
+ def tournament(
575
+ population: Sequence[tuple[Mapping[str, Any], float]],
576
+ *,
577
+ size: int = 2,
578
+ rng: random.Random | None = None,
579
+ ) -> Mapping[str, Any]:
580
+ """Pick one parent by a *size*-way tournament on the ranking value (lower wins)."""
581
+
582
+ if not population:
583
+ raise ValueError("cannot select a parent from an empty population")
584
+ r = rng or random.Random()
585
+ contenders = r.sample(list(population), min(size, len(population)))
586
+ return min(contenders, key=lambda pair: pair[1])[0]
587
+
588
+
589
+ def truncation_survival(
590
+ population: Sequence[tuple[Mapping[str, Any], float]],
591
+ *,
592
+ keep: int,
593
+ key_of,
594
+ ) -> list[tuple[Mapping[str, Any], float]]:
595
+ """Deduplicate by candidate identity, then keep the *keep* best.
596
+
597
+ Deduplication is not cosmetic: without it a population collapses onto copies
598
+ of one genome and recombination stops producing anything new, which is the
599
+ failure mode a diversity-free loop reaches within a couple of generations.
600
+ """
601
+
602
+ best: dict[str, tuple[Mapping[str, Any], float]] = {}
603
+ for config, value in population:
604
+ k = key_of(config)
605
+ if k not in best or value < best[k][1]:
606
+ best[k] = (config, value)
607
+ return sorted(best.values(), key=lambda pair: pair[1])[:keep]