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,1366 @@
1
+ """Campaign bridge for prior-only stage-global contextual allocations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from dataclasses import dataclass, field
8
+ from itertools import combinations
9
+ from typing import Protocol, runtime_checkable
10
+
11
+ from agent_evolve.application.contextual_search_controller import (
12
+ ContextualSearchLedger,
13
+ ContextualSearchQuery,
14
+ ContextualSearchStageAllocation,
15
+ PhaseAwareContextualSearchController,
16
+ slice_contextual_search_decision,
17
+ )
18
+ from agent_evolve.application.action_structural_signature import (
19
+ parent_relative_changed_json_paths_by_option,
20
+ parent_relative_path_sets_are_disjoint,
21
+ )
22
+ from agent_evolve.application.campaign_execution import CampaignStageRequest
23
+ from agent_evolve.application.evolution_campaign import (
24
+ ParentVariationBinding,
25
+ PreparedEvolutionCampaign,
26
+ )
27
+ from agent_evolve.domain.patch import require_sha256
28
+ from agent_evolve.domain.typed_json import thaw_json
29
+ from agent_evolve.policies.selection.affine_frontier_target import (
30
+ AuthenticatedAffineFrontierTargetAllocator,
31
+ )
32
+ from agent_evolve.ports.contextual_search_allocation import (
33
+ ContextualArmCountCapability,
34
+ ContextualArmCountCapabilityWitness,
35
+ ContextualJointCountVector,
36
+ ContextualLaneJointCountCapability,
37
+ ContextualPortfolioAllocationContract,
38
+ ContextualPortfolioAllocationRealization,
39
+ )
40
+ from agent_evolve.ports.frontier_target import (
41
+ CampaignPortfolioFrontierTarget,
42
+ CampaignPortfolioFrontierTargetAllocator,
43
+ objective_space_target_from_campaign_target,
44
+ )
45
+ from agent_evolve.ports.variation_source import (
46
+ PRIMARY_VARIATION_SOURCE_ID,
47
+ finite_variation_operator_id,
48
+ finite_variation_source_ids,
49
+ finite_variation_source_id,
50
+ finite_variation_source_minimum_counts,
51
+ )
52
+ from agent_evolve.ports.portfolio_selection import (
53
+ pairwise_disjoint_parent_patch_pairs,
54
+ )
55
+
56
+
57
+ _PLAN_DOMAIN = b"agent-evolve:contextual-campaign-search-plan:v1\x00"
58
+ _JOINT_CONSTRAINT_DOMAIN = b"agent-evolve:finite-contract-joint-count-constraint:v3\x00"
59
+
60
+
61
+ @runtime_checkable
62
+ class CampaignContextualPlanningContext(Protocol):
63
+ """Narrow structural view; avoids coupling the planner to one runtime."""
64
+
65
+ prepared: PreparedEvolutionCampaign
66
+ stage_request: CampaignStageRequest
67
+ parent_lane: object
68
+ variation: ParentVariationBinding
69
+
70
+ def __post_init__(self) -> None: ...
71
+
72
+
73
+ @runtime_checkable
74
+ class CampaignContextualJointCapabilityProjector(Protocol):
75
+ """Optional inverted API for current-wave finite structural capacity."""
76
+
77
+ def project(
78
+ self,
79
+ context: CampaignContextualPlanningContext,
80
+ *,
81
+ evaluation_slots: int,
82
+ source_arm_ids: tuple[str, ...],
83
+ operator_arm_ids: tuple[str, ...],
84
+ ) -> ContextualLaneJointCountCapability: ...
85
+
86
+
87
+ def _canonical_json(value: object) -> bytes:
88
+ return json.dumps(
89
+ value,
90
+ allow_nan=False,
91
+ ensure_ascii=True,
92
+ separators=(",", ":"),
93
+ sort_keys=True,
94
+ ).encode("ascii", errors="strict")
95
+
96
+
97
+ def _project_archive_front_size(archive_record: dict[str, object]) -> int:
98
+ """Resolve one generic frontier count from supported archive projections.
99
+
100
+ Campaign adapters may expose the count directly, inside the authenticated
101
+ Pareto summary, or implicitly through the serialized frontier. Treat all
102
+ present projections as redundant witnesses and fail closed on drift.
103
+ """
104
+
105
+ if type(archive_record) is not dict:
106
+ raise TypeError("archive_record must be an exact object")
107
+ witnesses: list[int] = []
108
+ direct = archive_record.get("front_size")
109
+ if direct is not None:
110
+ if type(direct) is not int or direct <= 0:
111
+ raise ValueError("campaign archive has an invalid direct front size")
112
+ witnesses.append(direct)
113
+ summary = archive_record.get("summary")
114
+ if summary is not None:
115
+ if type(summary) is not dict:
116
+ raise TypeError("campaign archive summary must be an object")
117
+ summarized = summary.get("front_size")
118
+ if summarized is not None:
119
+ if type(summarized) is not int or summarized <= 0:
120
+ raise ValueError("campaign archive has an invalid summary front size")
121
+ witnesses.append(summarized)
122
+ front_candidates = archive_record.get("front_candidates")
123
+ if front_candidates is not None:
124
+ if type(front_candidates) is not list or not front_candidates:
125
+ raise ValueError("campaign archive frontier must be a non-empty list")
126
+ witnesses.append(len(front_candidates))
127
+ if not witnesses:
128
+ raise ValueError("campaign archive omitted a positive frontier witness")
129
+ if len(set(witnesses)) != 1:
130
+ raise ValueError("campaign archive frontier witnesses disagree")
131
+ return witnesses[0]
132
+
133
+
134
+ @dataclass(frozen=True, slots=True)
135
+ class CampaignContextualSearchPlan:
136
+ """One stage allocation and its request-local signed contracts."""
137
+
138
+ campaign_generation: int
139
+ stage_allocation: ContextualSearchStageAllocation
140
+ contracts: tuple[ContextualPortfolioAllocationContract, ...]
141
+ frontier_targets: tuple[CampaignPortfolioFrontierTarget, ...]
142
+ plan_sha256: str = field(init=False)
143
+
144
+ def __post_init__(self) -> None:
145
+ if type(self.campaign_generation) is not int or self.campaign_generation <= 0:
146
+ raise ValueError("campaign_generation must be positive")
147
+ if type(self.stage_allocation) is not ContextualSearchStageAllocation:
148
+ raise TypeError("stage_allocation must be exact")
149
+ self.stage_allocation.__post_init__()
150
+ if (
151
+ type(self.contracts) is not tuple
152
+ or not self.contracts
153
+ or any(
154
+ type(value) is not ContextualPortfolioAllocationContract
155
+ for value in self.contracts
156
+ )
157
+ ):
158
+ raise ValueError("contracts must contain exact allocation contracts")
159
+ for value in self.contracts:
160
+ value.__post_init__()
161
+ if value.campaign_generation != self.campaign_generation:
162
+ raise ValueError("allocation contract differs from the stage")
163
+ if tuple(value.slice_id for value in self.contracts) != tuple(
164
+ value.slice_id for value in self.stage_allocation.slices
165
+ ):
166
+ raise ValueError("allocation contracts differ from stage slices")
167
+ if tuple(value.decision_sha256 for value in self.contracts) != tuple(
168
+ self.stage_allocation.decision.decision_sha256 for _ in self.contracts
169
+ ):
170
+ raise ValueError("allocation contracts differ from the decision")
171
+ if type(self.frontier_targets) is not tuple or any(
172
+ type(value) is not CampaignPortfolioFrontierTarget
173
+ for value in self.frontier_targets
174
+ ):
175
+ raise TypeError("frontier_targets must contain exact targets")
176
+ for value in self.frontier_targets:
177
+ value.__post_init__()
178
+ if tuple(value.lane_id for value in self.frontier_targets) != tuple(
179
+ value.slice_id for value in self.contracts
180
+ ):
181
+ raise ValueError("frontier targets must exactly cover allocation slices")
182
+ object.__setattr__(
183
+ self,
184
+ "plan_sha256",
185
+ hashlib.sha256(
186
+ _PLAN_DOMAIN + _canonical_json(self._unsigned_record())
187
+ ).hexdigest(),
188
+ )
189
+
190
+ def _unsigned_record(self) -> dict[str, object]:
191
+ return {
192
+ "schema_version": 2,
193
+ "campaign_generation": self.campaign_generation,
194
+ "stage_allocation_sha256": self.stage_allocation.allocation_sha256,
195
+ "contract_sha256s": [value.contract_sha256 for value in self.contracts],
196
+ "frontier_target_sha256s": [
197
+ value.target_sha256 for value in self.frontier_targets
198
+ ],
199
+ }
200
+
201
+ def to_record(self) -> dict[str, object]:
202
+ self.__post_init__()
203
+ return {
204
+ **self._unsigned_record(),
205
+ "stage_allocation": self.stage_allocation.to_record(),
206
+ "contracts": [value.to_record() for value in self.contracts],
207
+ "frontier_targets": [value.to_record() for value in self.frontier_targets],
208
+ "plan_sha256": self.plan_sha256,
209
+ }
210
+
211
+ def contract_for_slice(
212
+ self, slice_id: str
213
+ ) -> ContextualPortfolioAllocationContract:
214
+ try:
215
+ return next(value for value in self.contracts if value.slice_id == slice_id)
216
+ except StopIteration as error:
217
+ raise ValueError(
218
+ "allocation plan has no contract for this slice"
219
+ ) from error
220
+
221
+ def frontier_target_for_lane(
222
+ self,
223
+ lane_id: str,
224
+ ) -> CampaignPortfolioFrontierTarget:
225
+ try:
226
+ return next(
227
+ value for value in self.frontier_targets if value.lane_id == lane_id
228
+ )
229
+ except StopIteration as error:
230
+ raise ValueError("allocation plan has no target for this lane") from error
231
+
232
+
233
+ def _empirical_count_capability(
234
+ realizations: tuple[ContextualPortfolioAllocationRealization, ...],
235
+ *,
236
+ kind: str,
237
+ current_wave_index: int,
238
+ evaluation_slots: int,
239
+ arm_ids: tuple[str, ...],
240
+ ) -> ContextualArmCountCapability | None:
241
+ """Build a conservative prior-wave capability only after observed recourse."""
242
+
243
+ grouped: dict[int, list[ContextualPortfolioAllocationRealization]] = {}
244
+ for value in realizations:
245
+ value.__post_init__()
246
+ if value.controller_wave_index >= current_wave_index:
247
+ continue
248
+ grouped.setdefault(value.controller_wave_index, []).append(value)
249
+ witnesses: list[ContextualArmCountCapabilityWitness] = []
250
+ mismatch_observed = False
251
+ requested_name = f"requested_{kind}_target_counts"
252
+ realized_name = f"realized_{kind}_target_counts"
253
+ for wave_index, values in sorted(grouped.items()):
254
+ requested = {arm_id: 0 for arm_id in arm_ids}
255
+ realized = {arm_id: 0 for arm_id in arm_ids}
256
+ for value in values:
257
+ requested_counts = getattr(value, requested_name)
258
+ realized_counts = getattr(value, realized_name)
259
+ if tuple(arm_id for arm_id, _ in requested_counts) != arm_ids:
260
+ raise ValueError("allocation realization uses foreign capability arms")
261
+ for arm_id, count in requested_counts:
262
+ requested[arm_id] += count
263
+ for arm_id, count in realized_counts:
264
+ realized[arm_id] += count
265
+ if sum(requested.values()) != evaluation_slots:
266
+ # A different scale shape is evidence, but not a count vector in the
267
+ # current stage polytope. Keep it outside this bounded capability.
268
+ continue
269
+ mismatch_observed |= requested != realized
270
+ witnesses.append(
271
+ ContextualArmCountCapabilityWitness(
272
+ controller_wave_index=wave_index,
273
+ evaluation_slots=evaluation_slots,
274
+ realized_target_counts=tuple(sorted(realized.items())),
275
+ allocation_realization_sha256s=tuple(
276
+ sorted(value.realization_sha256 for value in values)
277
+ ),
278
+ )
279
+ )
280
+ if not mismatch_observed or not witnesses:
281
+ return None
282
+ return ContextualArmCountCapability(
283
+ kind=kind,
284
+ evaluation_slots=evaluation_slots,
285
+ arm_ids=arm_ids,
286
+ witnesses=tuple(sorted(witnesses, key=lambda value: value.witness_sha256)),
287
+ )
288
+
289
+
290
+ def _bounded_compositions(
291
+ total: int,
292
+ maxima: tuple[int, ...],
293
+ ) -> tuple[tuple[int, ...], ...]:
294
+ rows: list[tuple[int, ...]] = []
295
+
296
+ def visit(index: int, remaining: int, prefix: tuple[int, ...]) -> None:
297
+ if index == len(maxima) - 1:
298
+ if 0 <= remaining <= maxima[index]:
299
+ rows.append((*prefix, remaining))
300
+ return
301
+ lower = max(0, remaining - sum(maxima[index + 1 :]))
302
+ upper = min(maxima[index], remaining)
303
+ for value in range(lower, upper + 1):
304
+ visit(index + 1, remaining - value, (*prefix, value))
305
+
306
+ visit(0, total, ())
307
+ return tuple(rows)
308
+
309
+
310
+ def _contingency_tables(
311
+ row_counts: tuple[int, ...],
312
+ column_counts: tuple[int, ...],
313
+ ) -> tuple[tuple[tuple[int, ...], ...], ...]:
314
+ if sum(row_counts) != sum(column_counts):
315
+ raise ValueError("joint marginal counts must have equal capacity")
316
+ tables: list[tuple[tuple[int, ...], ...]] = []
317
+
318
+ def visit(
319
+ row_index: int,
320
+ remaining_columns: tuple[int, ...],
321
+ rows: tuple[tuple[int, ...], ...],
322
+ ) -> None:
323
+ if row_index == len(row_counts) - 1:
324
+ if sum(remaining_columns) == row_counts[row_index]:
325
+ tables.append((*rows, remaining_columns))
326
+ return
327
+ for row in _bounded_compositions(
328
+ row_counts[row_index],
329
+ remaining_columns,
330
+ ):
331
+ visit(
332
+ row_index + 1,
333
+ tuple(
334
+ remaining - used
335
+ for remaining, used in zip(
336
+ remaining_columns,
337
+ row,
338
+ strict=True,
339
+ )
340
+ ),
341
+ (*rows, row),
342
+ )
343
+
344
+ visit(0, column_counts, ())
345
+ return tuple(tables)
346
+
347
+
348
+ @dataclass(frozen=True, slots=True)
349
+ class FiniteContractContextualJointCapabilityProjector:
350
+ """Derive exact current-wave arm marginals from sealed finite structure.
351
+
352
+ This reusable projector knows only finite action metadata and structural
353
+ selection constraints. It does not receive objectives, model outputs,
354
+ posterior scores, workload identifiers, or provider state.
355
+ """
356
+
357
+ min_distinct_families: int | None = None
358
+ require_pairwise_disjoint_parent_patches: bool = False
359
+ family_exposure_bounds: tuple[tuple[str, int, int], ...] = ()
360
+ operator_exposure_bounds: tuple[tuple[str, int, int], ...] = ()
361
+ minimum_single_path_interventions: int = 0
362
+ protect_future_recombination_opportunities: bool = False
363
+ require_declared_source_floor_options: bool = True
364
+
365
+ def __post_init__(self) -> None:
366
+ if self.min_distinct_families is not None and (
367
+ type(self.min_distinct_families) is not int
368
+ or self.min_distinct_families <= 0
369
+ ):
370
+ raise ValueError("min_distinct_families must be positive or None")
371
+ if type(self.require_pairwise_disjoint_parent_patches) is not bool:
372
+ raise TypeError(
373
+ "require_pairwise_disjoint_parent_patches must be an exact bool"
374
+ )
375
+ if type(self.require_declared_source_floor_options) is not bool:
376
+ raise TypeError(
377
+ "require_declared_source_floor_options must be an exact bool"
378
+ )
379
+ if type(self.protect_future_recombination_opportunities) is not bool:
380
+ raise TypeError(
381
+ "protect_future_recombination_opportunities must be an exact bool"
382
+ )
383
+ if (
384
+ type(self.minimum_single_path_interventions) is not int
385
+ or self.minimum_single_path_interventions < 0
386
+ ):
387
+ raise ValueError(
388
+ "minimum_single_path_interventions must be non-negative"
389
+ )
390
+ if type(self.family_exposure_bounds) is not tuple:
391
+ raise TypeError("family_exposure_bounds must be an exact tuple")
392
+ families: list[str] = []
393
+ for value in self.family_exposure_bounds:
394
+ if type(value) is not tuple or len(value) != 3:
395
+ raise TypeError("family exposure bounds must be exact triples")
396
+ family, minimum, maximum = value
397
+ if type(family) is not str or not family:
398
+ raise ValueError("family exposure bound needs a family")
399
+ if (
400
+ type(minimum) is not int
401
+ or type(maximum) is not int
402
+ or minimum < 0
403
+ or maximum < minimum
404
+ ):
405
+ raise ValueError("family exposure bound is invalid")
406
+ families.append(family)
407
+ if families != sorted(set(families)):
408
+ raise ValueError("family exposure bounds must be unique and canonical")
409
+ if type(self.operator_exposure_bounds) is not tuple:
410
+ raise TypeError("operator_exposure_bounds must be an exact tuple")
411
+ operators: list[str] = []
412
+ for value in self.operator_exposure_bounds:
413
+ if type(value) is not tuple or len(value) != 3:
414
+ raise TypeError("operator exposure bounds must be exact triples")
415
+ operator, minimum, maximum = value
416
+ if type(operator) is not str or not operator:
417
+ raise ValueError("operator exposure bound needs an operator")
418
+ if (
419
+ type(minimum) is not int
420
+ or type(maximum) is not int
421
+ or minimum < 0
422
+ or maximum < minimum
423
+ ):
424
+ raise ValueError("operator exposure bound is invalid")
425
+ operators.append(operator)
426
+ if operators != sorted(set(operators)):
427
+ raise ValueError("operator exposure bounds must be unique and canonical")
428
+
429
+ @property
430
+ def structural_constraint_sha256(self) -> str:
431
+ self.__post_init__()
432
+ return hashlib.sha256(
433
+ _JOINT_CONSTRAINT_DOMAIN
434
+ + _canonical_json(
435
+ {
436
+ "schema_version": 2,
437
+ "witness_solver": {
438
+ "policy_id": "exact_structural_witness_search",
439
+ "policy_version": 3,
440
+ "pairwise_equivalence": (
441
+ "source_operator_family_exact_changed_path_set"
442
+ ),
443
+ "pairwise_representative": "lexicographically_first_option",
444
+ "pairwise_search_order": (
445
+ "descending_compatibility_then_arm_family_option"
446
+ ),
447
+ },
448
+ "min_distinct_families": self.min_distinct_families,
449
+ "require_pairwise_disjoint_parent_patches": (
450
+ self.require_pairwise_disjoint_parent_patches
451
+ ),
452
+ "family_exposure_bounds": [
453
+ list(value) for value in self.family_exposure_bounds
454
+ ],
455
+ "operator_exposure_bounds": [
456
+ list(value) for value in self.operator_exposure_bounds
457
+ ],
458
+ "minimum_single_path_interventions": (
459
+ self.minimum_single_path_interventions
460
+ ),
461
+ "protect_future_recombination_opportunities": (
462
+ self.protect_future_recombination_opportunities
463
+ ),
464
+ "offspring_opportunity_policy": (
465
+ "reserve_planned_disjoint_parent_patch_pairs_when_consumed"
466
+ ),
467
+ "intervention_axis": (
468
+ "exact_parent_relative_changed_json_path_count"
469
+ ),
470
+ "require_declared_source_floor_options": (
471
+ self.require_declared_source_floor_options
472
+ ),
473
+ "source_floor_semantics": (
474
+ "minimum_count_per_source_not_fixed_representative"
475
+ ),
476
+ "source_axis": "sealed_finite_variation_source",
477
+ "operator_axis": (
478
+ "declared_evaluation_operator_or_composition_metadata"
479
+ ),
480
+ "diversity_axis": "finite_option_family",
481
+ "objective_values_consulted": False,
482
+ "workload_identifiers_consulted": False,
483
+ }
484
+ )
485
+ ).hexdigest()
486
+
487
+ def project(
488
+ self,
489
+ context: CampaignContextualPlanningContext,
490
+ *,
491
+ evaluation_slots: int,
492
+ source_arm_ids: tuple[str, ...],
493
+ operator_arm_ids: tuple[str, ...],
494
+ ) -> ContextualLaneJointCountCapability:
495
+ self.__post_init__()
496
+ if not isinstance(context, CampaignContextualPlanningContext):
497
+ raise TypeError("context must implement the planning context port")
498
+ context.__post_init__()
499
+ if type(evaluation_slots) is not int or evaluation_slots <= 0:
500
+ raise ValueError("evaluation_slots must be positive")
501
+ if self.min_distinct_families is not None and (
502
+ self.min_distinct_families > evaluation_slots
503
+ ):
504
+ raise ValueError("minimum family diversity exceeds lane capacity")
505
+ if self.minimum_single_path_interventions > evaluation_slots:
506
+ raise ValueError("minimum single-path floor exceeds lane capacity")
507
+ future_recombination_widths = (
508
+ tuple(
509
+ step.offspring_per_parent
510
+ for step in context.prepared.schedule.steps
511
+ if step.source_portfolio_generation
512
+ == context.stage_request.step.generation
513
+ )
514
+ if self.protect_future_recombination_opportunities
515
+ else ()
516
+ )
517
+ if len(future_recombination_widths) > 1:
518
+ raise ValueError("portfolio generation has multiple recombination consumers")
519
+ minimum_disjoint_parent_patch_pairs = (
520
+ future_recombination_widths[0]
521
+ if self.protect_future_recombination_opportunities
522
+ and future_recombination_widths
523
+ else 0
524
+ )
525
+ maximum_selected_pairs = evaluation_slots * (evaluation_slots - 1) // 2
526
+ if minimum_disjoint_parent_patch_pairs > maximum_selected_pairs:
527
+ raise ValueError(
528
+ "planned recombination width exceeds the selected parent-pair universe"
529
+ )
530
+ contract = context.variation.contract
531
+ contract.__post_init__()
532
+ options = contract.options
533
+ source_by_option = {
534
+ value.option_id: finite_variation_source_id(value) for value in options
535
+ }
536
+ operator_by_option = {
537
+ value.option_id: finite_variation_operator_id(value) for value in options
538
+ }
539
+ if not set(source_by_option.values()).issubset(source_arm_ids):
540
+ raise ValueError("finite contract exposes a source outside planner arms")
541
+ if not set(operator_by_option.values()).issubset(operator_arm_ids):
542
+ raise ValueError("finite contract exposes an operator outside planner arms")
543
+ if not {
544
+ operator for operator, _, _ in self.operator_exposure_bounds
545
+ }.issubset(operator_arm_ids):
546
+ raise ValueError("operator exposure bound escapes planner arms")
547
+ option_by_id = {value.option_id: value for value in options}
548
+ changed_paths_by_option = parent_relative_changed_json_paths_by_option(
549
+ contract
550
+ )
551
+ changed_path_keys_by_option = {
552
+ option_id: tuple(path.schema_identity for path in paths)
553
+ for option_id, paths in changed_paths_by_option.items()
554
+ }
555
+ single_path_option_ids = {
556
+ option_id
557
+ for option_id, paths in changed_paths_by_option.items()
558
+ if len(paths) == 1
559
+ }
560
+ if len(single_path_option_ids) < self.minimum_single_path_interventions:
561
+ raise ValueError(
562
+ "finite contract cannot satisfy its single-path intervention floor"
563
+ )
564
+ source_minimum_counts = dict(
565
+ finite_variation_source_minimum_counts(contract)
566
+ if self.require_declared_source_floor_options
567
+ else ()
568
+ )
569
+ if not set(source_minimum_counts).issubset(source_arm_ids):
570
+ raise ValueError("declared source floor escapes planner arms")
571
+ if sum(source_minimum_counts.values()) > evaluation_slots:
572
+ raise ValueError("declared source floors exceed lane capacity")
573
+ # Strict patch-disjoint feasibility depends only on source, operator,
574
+ # family, and the exact parent-relative path set. Options sharing all
575
+ # four attributes are interchangeable to every constraint below and,
576
+ # because their non-empty path sets overlap, at most one can occur in
577
+ # a legal slate. Collapse that exact equivalence before constructing
578
+ # the compatibility graph. This is lossless but prevents a catalogue
579
+ # with many values per locus from turning a K-small witness query into
580
+ # an O(N**K) traversal over phenotype multiplicity.
581
+ pairwise_candidate_ids: tuple[str, ...] = ()
582
+ if self.require_pairwise_disjoint_parent_patches:
583
+ equivalence_groups: dict[
584
+ tuple[str, str, str, tuple[str, ...]], list[str]
585
+ ] = {}
586
+ for option in options:
587
+ option_id = option.option_id
588
+ signature = (
589
+ source_by_option[option_id],
590
+ operator_by_option[option_id],
591
+ option.family,
592
+ changed_path_keys_by_option[option_id],
593
+ )
594
+ equivalence_groups.setdefault(signature, []).append(option_id)
595
+ pairwise_candidate_ids = tuple(
596
+ sorted(
597
+ min(option_ids)
598
+ for option_ids in equivalence_groups.values()
599
+ )
600
+ )
601
+ allowed_pairs = (
602
+ None
603
+ if not self.require_pairwise_disjoint_parent_patches
604
+ else {
605
+ frozenset(value)
606
+ for value in pairwise_disjoint_parent_patch_pairs(
607
+ contract,
608
+ pairwise_candidate_ids,
609
+ )
610
+ }
611
+ )
612
+ disjoint_pair_cache: dict[frozenset[str], bool] = {}
613
+
614
+ def pair_is_disjoint(left: str, right: str) -> bool:
615
+ if left == right:
616
+ return False
617
+ key = frozenset((left, right))
618
+ cached = disjoint_pair_cache.get(key)
619
+ if cached is not None:
620
+ return cached
621
+ value = parent_relative_path_sets_are_disjoint(
622
+ changed_paths_by_option[left],
623
+ changed_paths_by_option[right],
624
+ )
625
+ disjoint_pair_cache[key] = value
626
+ return value
627
+
628
+ def disjoint_pair_count(selected: tuple[str, ...]) -> int:
629
+ return sum(
630
+ pair_is_disjoint(left, right)
631
+ for index, left in enumerate(selected)
632
+ for right in selected[index + 1 :]
633
+ )
634
+ family_bounds = {
635
+ family: (minimum, maximum)
636
+ for family, minimum, maximum in self.family_exposure_bounds
637
+ }
638
+ operator_bounds = {
639
+ operator: (minimum, maximum)
640
+ for operator, minimum, maximum in self.operator_exposure_bounds
641
+ }
642
+
643
+ def structurally_valid(
644
+ selected: tuple[str, ...],
645
+ *,
646
+ check_pairwise_patches: bool = True,
647
+ check_offspring_opportunity: bool = True,
648
+ ) -> bool:
649
+ if len(selected) != evaluation_slots:
650
+ return False
651
+ if any(
652
+ sum(source_by_option[value] == source_id for value in selected)
653
+ < minimum
654
+ for source_id, minimum in source_minimum_counts.items()
655
+ ):
656
+ return False
657
+ if any(
658
+ not minimum
659
+ <= sum(operator_by_option[value] == operator for value in selected)
660
+ <= maximum
661
+ for operator, (minimum, maximum) in operator_bounds.items()
662
+ ):
663
+ return False
664
+ if sum(
665
+ value in single_path_option_ids for value in selected
666
+ ) < self.minimum_single_path_interventions:
667
+ return False
668
+ if (
669
+ check_offspring_opportunity
670
+ and disjoint_pair_count(selected)
671
+ < minimum_disjoint_parent_patch_pairs
672
+ ):
673
+ return False
674
+ families = tuple(option_by_id[value].family for value in selected)
675
+ if self.min_distinct_families is not None and len(set(families)) < (
676
+ self.min_distinct_families
677
+ ):
678
+ return False
679
+ if any(
680
+ not minimum <= families.count(family) <= maximum
681
+ for family, (minimum, maximum) in family_bounds.items()
682
+ ):
683
+ return False
684
+ return (
685
+ not check_pairwise_patches
686
+ or allowed_pairs is None
687
+ or all(
688
+ frozenset((left, right)) in allowed_pairs
689
+ for index, left in enumerate(selected)
690
+ for right in selected[index + 1 :]
691
+ )
692
+ )
693
+
694
+ compatible_degree = (
695
+ {}
696
+ if allowed_pairs is None
697
+ else {
698
+ option_id: sum(
699
+ frozenset((option_id, other_id)) in allowed_pairs
700
+ for other_id in pairwise_candidate_ids
701
+ if other_id != option_id
702
+ )
703
+ for option_id in pairwise_candidate_ids
704
+ }
705
+ )
706
+ pairwise_base_candidates = (
707
+ ()
708
+ if allowed_pairs is None
709
+ else tuple(
710
+ sorted(
711
+ pairwise_candidate_ids,
712
+ key=lambda option_id: (
713
+ -compatible_degree[option_id],
714
+ source_by_option[option_id],
715
+ operator_by_option[option_id],
716
+ option_by_id[option_id].family,
717
+ option_id,
718
+ ),
719
+ )
720
+ )
721
+ )
722
+ options_by_arm_cell = {
723
+ (source_id, operator_id): tuple(
724
+ option.option_id
725
+ for option in options
726
+ if source_by_option[option.option_id] == source_id
727
+ and operator_by_option[option.option_id] == operator_id
728
+ )
729
+ for source_id in source_arm_ids
730
+ for operator_id in operator_arm_ids
731
+ }
732
+ base_options_by_arm_cell: dict[tuple[str, str], tuple[str, ...]] = {}
733
+ for arm_cell, group in options_by_arm_cell.items():
734
+ if allowed_pairs is not None:
735
+ base_options_by_arm_cell[arm_cell] = group
736
+ continue
737
+ # Family and exact intervention arity are the only option-level
738
+ # axes used by a base witness. Keep K representatives for each
739
+ # exact shape, interleaved across shapes so the first solution is
740
+ # diverse. Offspring opportunity is subsequently repaired over
741
+ # the complete changed-path equivalence domain.
742
+ by_shape: dict[tuple[str, bool], tuple[str, ...]] = {}
743
+ for family in sorted({option_by_id[value].family for value in group}):
744
+ for single_path in (True, False):
745
+ members = tuple(
746
+ value
747
+ for value in group
748
+ if option_by_id[value].family == family
749
+ and (value in single_path_option_ids) is single_path
750
+ )
751
+ if members:
752
+ by_shape[(family, single_path)] = members[:evaluation_slots]
753
+ reduced: list[str] = []
754
+ for rank in range(evaluation_slots):
755
+ reduced.extend(
756
+ members[rank]
757
+ for _, members in sorted(by_shape.items())
758
+ if rank < len(members)
759
+ )
760
+ base_options_by_arm_cell[arm_cell] = tuple(reduced)
761
+
762
+ # Within one source/operator/family/arity signature, only the exact
763
+ # changed-path pattern can affect offspring opportunity. Retain one
764
+ # representative per pattern (up to K identical-path duplicates) so
765
+ # the bounded repair is complete without exposing the full catalogue
766
+ # multiplicity to the base marginal solver.
767
+ groups: dict[tuple[str, str, str, bool], list[str]] = {}
768
+ signature_by_option: dict[str, tuple[str, str, str, bool]] = {}
769
+ for option in options:
770
+ signature = (
771
+ source_by_option[option.option_id],
772
+ operator_by_option[option.option_id],
773
+ option.family,
774
+ option.option_id in single_path_option_ids,
775
+ )
776
+ groups.setdefault(signature, []).append(option.option_id)
777
+ signature_by_option[option.option_id] = signature
778
+ opportunity_domain_by_signature: dict[
779
+ tuple[str, str, str, bool], tuple[str, ...]
780
+ ] = {}
781
+ if minimum_disjoint_parent_patch_pairs:
782
+ for signature, option_ids in groups.items():
783
+ by_paths: dict[tuple[str, ...], list[str]] = {}
784
+ for option_id in sorted(option_ids):
785
+ by_paths.setdefault(
786
+ changed_path_keys_by_option[option_id], []
787
+ ).append(option_id)
788
+ opportunity_domain_by_signature[signature] = tuple(
789
+ option_id
790
+ for paths in sorted(by_paths)
791
+ for option_id in by_paths[paths][:evaluation_slots]
792
+ )
793
+
794
+ def repair_offspring_opportunity(
795
+ selected: tuple[str, ...],
796
+ ) -> tuple[str, ...] | None:
797
+ if (
798
+ not minimum_disjoint_parent_patch_pairs
799
+ or disjoint_pair_count(selected)
800
+ >= minimum_disjoint_parent_patch_pairs
801
+ ):
802
+ return selected
803
+ position_signatures = tuple(
804
+ signature_by_option[value] for value in selected
805
+ )
806
+
807
+ def search(
808
+ position: int,
809
+ chosen: tuple[str, ...],
810
+ ) -> tuple[str, ...] | None:
811
+ remaining = len(position_signatures) - position
812
+ observed_pairs = disjoint_pair_count(chosen)
813
+ optimistic_pairs = (
814
+ observed_pairs
815
+ + len(chosen) * remaining
816
+ + remaining * (remaining - 1) // 2
817
+ )
818
+ if optimistic_pairs < minimum_disjoint_parent_patch_pairs:
819
+ return None
820
+ if position == len(position_signatures):
821
+ return chosen if structurally_valid(chosen) else None
822
+ domain = opportunity_domain_by_signature[
823
+ position_signatures[position]
824
+ ]
825
+ preferred = selected[position]
826
+ ordered = tuple(
827
+ sorted(
828
+ domain,
829
+ key=lambda option_id: (
830
+ -sum(
831
+ pair_is_disjoint(option_id, prior)
832
+ for prior in chosen
833
+ ),
834
+ option_id != preferred,
835
+ changed_path_keys_by_option[option_id],
836
+ option_id,
837
+ ),
838
+ )
839
+ )
840
+ for option_id in ordered:
841
+ if option_id in chosen:
842
+ continue
843
+ resolved = search(position + 1, (*chosen, option_id))
844
+ if resolved is not None:
845
+ return resolved
846
+ return None
847
+
848
+ return search(0, ())
849
+
850
+ def first_witness(
851
+ source_counts: tuple[int, ...],
852
+ operator_counts: tuple[int, ...],
853
+ ) -> tuple[str, ...] | None:
854
+ source_targets = dict(zip(source_arm_ids, source_counts, strict=True))
855
+ operator_targets = dict(zip(operator_arm_ids, operator_counts, strict=True))
856
+ required_source_counts = {value: 0 for value in source_arm_ids}
857
+ required_operator_counts = {value: 0 for value in operator_arm_ids}
858
+ if any(
859
+ required_source_counts[value] > source_targets[value]
860
+ for value in source_arm_ids
861
+ ) or any(
862
+ required_operator_counts[value] > operator_targets[value]
863
+ for value in operator_arm_ids
864
+ ):
865
+ return None
866
+ if allowed_pairs is not None:
867
+ initial: tuple[str, ...] = ()
868
+ remaining_source = {
869
+ value: source_targets[value] - required_source_counts[value]
870
+ for value in source_arm_ids
871
+ }
872
+ remaining_operator = {
873
+ value: operator_targets[value] - required_operator_counts[value]
874
+ for value in operator_arm_ids
875
+ }
876
+ candidates = pairwise_base_candidates
877
+
878
+ def pairwise_search(
879
+ selected: tuple[str, ...],
880
+ available: tuple[str, ...],
881
+ source_remaining: dict[str, int],
882
+ operator_remaining: dict[str, int],
883
+ ) -> tuple[str, ...] | None:
884
+ slots_remaining = evaluation_slots - len(selected)
885
+ if slots_remaining == 0:
886
+ if any(source_remaining.values()) or any(
887
+ operator_remaining.values()
888
+ ):
889
+ return None
890
+ return selected if structurally_valid(selected) else None
891
+ eligible = tuple(
892
+ value
893
+ for value in available
894
+ if source_remaining[source_by_option[value]] > 0
895
+ and operator_remaining[operator_by_option[value]] > 0
896
+ )
897
+ if len(eligible) < slots_remaining:
898
+ return None
899
+ if any(
900
+ sum(source_by_option[value] == arm_id for value in eligible)
901
+ < remaining
902
+ for arm_id, remaining in source_remaining.items()
903
+ ) or any(
904
+ sum(operator_by_option[value] == arm_id for value in eligible)
905
+ < remaining
906
+ for arm_id, remaining in operator_remaining.items()
907
+ ):
908
+ return None
909
+ selected_families = tuple(
910
+ option_by_id[value].family for value in selected
911
+ )
912
+ eligible_families = {
913
+ option_by_id[value].family for value in eligible
914
+ }
915
+ if self.min_distinct_families is not None and (
916
+ len(set(selected_families).union(eligible_families))
917
+ < self.min_distinct_families
918
+ ):
919
+ return None
920
+ if any(
921
+ selected_families.count(family)
922
+ + min(
923
+ slots_remaining,
924
+ sum(
925
+ option_by_id[value].family == family
926
+ for value in eligible
927
+ ),
928
+ )
929
+ < minimum
930
+ for family, (minimum, _) in family_bounds.items()
931
+ ):
932
+ return None
933
+ for index, option_id in enumerate(eligible):
934
+ family = option_by_id[option_id].family
935
+ maximum = family_bounds.get(
936
+ family,
937
+ (0, evaluation_slots),
938
+ )[1]
939
+ if selected_families.count(family) >= maximum:
940
+ continue
941
+ source_id = source_by_option[option_id]
942
+ operator_id = operator_by_option[option_id]
943
+ next_source = dict(source_remaining)
944
+ next_operator = dict(operator_remaining)
945
+ next_source[source_id] -= 1
946
+ next_operator[operator_id] -= 1
947
+ tail = tuple(
948
+ value
949
+ for value in eligible[index + 1 :]
950
+ if frozenset((option_id, value)) in allowed_pairs
951
+ )
952
+ resolved = pairwise_search(
953
+ (*selected, option_id),
954
+ tail,
955
+ next_source,
956
+ next_operator,
957
+ )
958
+ if resolved is not None:
959
+ return tuple(sorted(resolved))
960
+ return None
961
+
962
+ return pairwise_search(
963
+ initial,
964
+ candidates,
965
+ remaining_source,
966
+ remaining_operator,
967
+ )
968
+ for table in _contingency_tables(source_counts, operator_counts):
969
+ required_by_cell = {
970
+ (source_id, operator_id): sum(
971
+ source_by_option[value] == source_id
972
+ and operator_by_option[value] == operator_id
973
+ for value in ()
974
+ )
975
+ for source_id in source_arm_ids
976
+ for operator_id in operator_arm_ids
977
+ }
978
+ cell_rows: list[tuple[tuple[str, str], tuple[str, ...], int]] = []
979
+ table_valid = True
980
+ for source_index, source_id in enumerate(source_arm_ids):
981
+ for operator_index, operator_id in enumerate(operator_arm_ids):
982
+ target = table[source_index][operator_index]
983
+ required_count = required_by_cell[(source_id, operator_id)]
984
+ if required_count > target:
985
+ table_valid = False
986
+ break
987
+ group = base_options_by_arm_cell[(source_id, operator_id)]
988
+ needed = target - required_count
989
+ if needed > len(group):
990
+ table_valid = False
991
+ break
992
+ if needed:
993
+ cell_rows.append(((source_id, operator_id), group, needed))
994
+ if not table_valid:
995
+ break
996
+ if not table_valid:
997
+ continue
998
+ ordered_cells = tuple(
999
+ sorted(
1000
+ cell_rows,
1001
+ key=lambda value: (
1002
+ len(value[1]),
1003
+ value[0],
1004
+ ),
1005
+ )
1006
+ )
1007
+ initial: tuple[str, ...] = ()
1008
+
1009
+ def search(
1010
+ cell_index: int,
1011
+ selected: tuple[str, ...],
1012
+ ) -> tuple[str, ...] | None:
1013
+ if cell_index == len(ordered_cells):
1014
+ if not structurally_valid(
1015
+ selected,
1016
+ check_offspring_opportunity=False,
1017
+ ):
1018
+ return None
1019
+ return repair_offspring_opportunity(selected)
1020
+ _, group, needed = ordered_cells[cell_index]
1021
+ for choice in combinations(group, needed):
1022
+ combined = (*selected, *choice)
1023
+ if allowed_pairs is not None and any(
1024
+ frozenset((left, right)) not in allowed_pairs
1025
+ for index, left in enumerate(combined)
1026
+ for right in combined[index + 1 :]
1027
+ ):
1028
+ continue
1029
+ families = tuple(
1030
+ option_by_id[value].family for value in combined
1031
+ )
1032
+ if any(
1033
+ families.count(family) > maximum
1034
+ for family, (_, maximum) in family_bounds.items()
1035
+ ):
1036
+ continue
1037
+ if self.min_distinct_families is not None:
1038
+ remaining_slots = evaluation_slots - len(combined)
1039
+ remaining_families = {
1040
+ option_by_id[value].family
1041
+ for _, remaining_group, _ in ordered_cells[
1042
+ cell_index + 1 :
1043
+ ]
1044
+ for value in remaining_group
1045
+ }
1046
+ if (
1047
+ len(set(families))
1048
+ + min(
1049
+ remaining_slots,
1050
+ len(remaining_families.difference(families)),
1051
+ )
1052
+ < self.min_distinct_families
1053
+ ):
1054
+ continue
1055
+ resolved = search(cell_index + 1, combined)
1056
+ if resolved is not None:
1057
+ return resolved
1058
+ return None
1059
+
1060
+ witness = search(0, initial)
1061
+ if witness is not None:
1062
+ return tuple(sorted(witness))
1063
+ return None
1064
+
1065
+ # Eagerly enumerating every K-combination of structural signatures is
1066
+ # O(G**K) in the number of exposed families. Large finite catalogues
1067
+ # can have hundreds of such families even though the controller needs
1068
+ # only the small source/operator marginal polytope. Solve those
1069
+ # marginals directly below; the exact path-equivalence repair remains
1070
+ # responsible for the registered offspring opportunity floor.
1071
+
1072
+ vectors: list[ContextualJointCountVector] = []
1073
+ source_vectors = _bounded_compositions(
1074
+ evaluation_slots,
1075
+ tuple(evaluation_slots for _ in source_arm_ids),
1076
+ )
1077
+ operator_vectors = _bounded_compositions(
1078
+ evaluation_slots,
1079
+ tuple(evaluation_slots for _ in operator_arm_ids),
1080
+ )
1081
+ for source_counts in source_vectors:
1082
+ if any(
1083
+ source_counts[source_arm_ids.index(source_id)] < minimum
1084
+ for source_id, minimum in source_minimum_counts.items()
1085
+ ):
1086
+ continue
1087
+ for operator_counts in operator_vectors:
1088
+ witness = first_witness(source_counts, operator_counts)
1089
+ if witness is not None and allowed_pairs is None:
1090
+ witness = repair_offspring_opportunity(witness)
1091
+ if witness is None:
1092
+ continue
1093
+ vectors.append(
1094
+ ContextualJointCountVector(
1095
+ source_target_counts=tuple(
1096
+ zip(source_arm_ids, source_counts, strict=True)
1097
+ ),
1098
+ operator_target_counts=tuple(
1099
+ zip(operator_arm_ids, operator_counts, strict=True)
1100
+ ),
1101
+ feasibility_witness_option_identity_sha256s=tuple(
1102
+ sorted(
1103
+ option_by_id[value].identity_sha256 for value in witness
1104
+ )
1105
+ ),
1106
+ )
1107
+ )
1108
+ if not vectors:
1109
+ raise ValueError("finite contract has no jointly feasible lane vector")
1110
+ return ContextualLaneJointCountCapability(
1111
+ slice_id=context.parent_lane.lane_id,
1112
+ finite_contract_identity_sha256=contract.identity_sha256,
1113
+ structural_constraint_sha256=self.structural_constraint_sha256,
1114
+ evaluation_slots=evaluation_slots,
1115
+ source_arm_ids=source_arm_ids,
1116
+ operator_arm_ids=operator_arm_ids,
1117
+ feasible_vectors=tuple(
1118
+ sorted(vectors, key=lambda value: value.vector_sha256)
1119
+ ),
1120
+ minimum_single_path_interventions=(
1121
+ self.minimum_single_path_interventions
1122
+ ),
1123
+ minimum_disjoint_parent_patch_pairs=(
1124
+ minimum_disjoint_parent_patch_pairs
1125
+ ),
1126
+ )
1127
+
1128
+
1129
+ @dataclass(slots=True)
1130
+ class CampaignContextualSearchPlanner:
1131
+ """Build prior-only decisions from framework facts shared by all workloads."""
1132
+
1133
+ ledger: ContextualSearchLedger
1134
+ campaign_scope_sha256: str
1135
+ available_source_ids: tuple[str, ...] | None = None
1136
+ available_operator_ids: tuple[str, ...] = ("atomic", "composite")
1137
+ incumbent_source_id: str = PRIMARY_VARIATION_SOURCE_ID
1138
+ incumbent_operator_id: str = "atomic"
1139
+ composition_positive_atomic_threshold: int = 2
1140
+ require_objective_space_targets: bool = False
1141
+ joint_capability_projector: CampaignContextualJointCapabilityProjector | None = None
1142
+ controller: PhaseAwareContextualSearchController = field(
1143
+ default_factory=PhaseAwareContextualSearchController
1144
+ )
1145
+ frontier_target_allocator: CampaignPortfolioFrontierTargetAllocator = field(
1146
+ default_factory=AuthenticatedAffineFrontierTargetAllocator
1147
+ )
1148
+ plans: list[CampaignContextualSearchPlan] = field(
1149
+ init=False,
1150
+ default_factory=list,
1151
+ )
1152
+
1153
+ def __post_init__(self) -> None:
1154
+ if type(self.ledger) is not ContextualSearchLedger:
1155
+ raise TypeError("ledger must be exact ContextualSearchLedger")
1156
+ require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
1157
+ if type(self.controller) is not PhaseAwareContextualSearchController:
1158
+ raise TypeError("controller must be exact")
1159
+ if self.available_source_ids is not None and (
1160
+ type(self.available_source_ids) is not tuple
1161
+ or not self.available_source_ids
1162
+ or self.available_source_ids
1163
+ != tuple(sorted(set(self.available_source_ids)))
1164
+ ):
1165
+ raise ValueError(
1166
+ "available_source_ids must be canonical when explicitly supplied"
1167
+ )
1168
+ if not isinstance(
1169
+ self.frontier_target_allocator,
1170
+ CampaignPortfolioFrontierTargetAllocator,
1171
+ ):
1172
+ raise TypeError("frontier_target_allocator must satisfy its port")
1173
+ if self.joint_capability_projector is not None and not isinstance(
1174
+ self.joint_capability_projector,
1175
+ CampaignContextualJointCapabilityProjector,
1176
+ ):
1177
+ raise TypeError("joint_capability_projector must satisfy its inverted API")
1178
+ if (
1179
+ type(self.composition_positive_atomic_threshold) is not int
1180
+ or self.composition_positive_atomic_threshold <= 0
1181
+ ):
1182
+ raise ValueError("composition threshold must be positive")
1183
+ if type(self.require_objective_space_targets) is not bool:
1184
+ raise TypeError("require_objective_space_targets must be exact bool")
1185
+
1186
+ def plan(
1187
+ self,
1188
+ contexts: tuple[CampaignContextualPlanningContext, ...],
1189
+ ) -> CampaignContextualSearchPlan:
1190
+ self.__post_init__()
1191
+ if (
1192
+ type(contexts) is not tuple
1193
+ or not contexts
1194
+ or any(
1195
+ not isinstance(value, CampaignContextualPlanningContext)
1196
+ for value in contexts
1197
+ )
1198
+ ):
1199
+ raise ValueError("contexts must implement the planning context port")
1200
+ for value in contexts:
1201
+ value.__post_init__()
1202
+ preparations = {value.prepared.preparation_sha256 for value in contexts}
1203
+ generations = {value.stage_request.step.generation for value in contexts}
1204
+ if len(preparations) != 1 or len(generations) != 1:
1205
+ raise ValueError("contextual plan cannot mix campaigns or generations")
1206
+ generation = next(iter(generations))
1207
+ if generation % 2 != 1:
1208
+ raise ValueError("contextual planning requires a portfolio generation")
1209
+ ordered = tuple(sorted(contexts, key=lambda value: value.parent_lane.lane_id))
1210
+ slice_ids = tuple(value.parent_lane.lane_id for value in ordered)
1211
+ if slice_ids != tuple(sorted(set(slice_ids))):
1212
+ raise ValueError("parent lanes must be unique and canonical")
1213
+ prepared = ordered[0].prepared
1214
+ declared_source_ids = tuple(
1215
+ sorted(
1216
+ {
1217
+ source_id
1218
+ for value in ordered
1219
+ for source_id in finite_variation_source_ids(
1220
+ value.variation.contract
1221
+ )
1222
+ }
1223
+ )
1224
+ )
1225
+ available_source_ids = (
1226
+ declared_source_ids
1227
+ if self.available_source_ids is None
1228
+ else self.available_source_ids
1229
+ )
1230
+ if not set(declared_source_ids).issubset(available_source_ids):
1231
+ raise ValueError(
1232
+ "contextual source arms omit a finite-contract variation source"
1233
+ )
1234
+ if self.incumbent_source_id not in available_source_ids:
1235
+ raise ValueError("incumbent source is absent from current source arms")
1236
+ portfolio_width = prepared.protocol.portfolio_width
1237
+ evaluation_slots = tuple(portfolio_width for _ in ordered)
1238
+ joint_count_capabilities = (
1239
+ ()
1240
+ if self.joint_capability_projector is None
1241
+ else tuple(
1242
+ self.joint_capability_projector.project(
1243
+ value,
1244
+ evaluation_slots=portfolio_width,
1245
+ source_arm_ids=available_source_ids,
1246
+ operator_arm_ids=self.available_operator_ids,
1247
+ )
1248
+ for value in ordered
1249
+ )
1250
+ )
1251
+ if (
1252
+ joint_count_capabilities
1253
+ and tuple(value.slice_id for value in joint_count_capabilities) != slice_ids
1254
+ ):
1255
+ raise ValueError("joint capability projection changed campaign lanes")
1256
+ wave_index = (generation + 1) // 2
1257
+ total_portfolio_waves = len(prepared.schedule.portfolio_generations)
1258
+ archive_record = thaw_json(ordered[0].stage_request.archive_cutoff.archive)
1259
+ if type(archive_record) is not dict:
1260
+ raise TypeError("campaign archive cutoff must thaw to an object")
1261
+ archive_front_size = _project_archive_front_size(archive_record)
1262
+ prior = tuple(
1263
+ value
1264
+ for value in self.ledger.observations
1265
+ if value.campaign_scope_sha256 == self.campaign_scope_sha256
1266
+ and value.wave_index < wave_index
1267
+ )
1268
+ composition_evidence_available = (
1269
+ sum(
1270
+ value.operator_id == "atomic" and value.positive_marginal_utility
1271
+ for value in prior
1272
+ )
1273
+ >= self.composition_positive_atomic_threshold
1274
+ )
1275
+ prior_realizations = tuple(
1276
+ value
1277
+ for value in self.ledger.allocation_realizations
1278
+ if value.campaign_scope_sha256 == self.campaign_scope_sha256
1279
+ and value.controller_wave_index < wave_index
1280
+ )
1281
+ source_count_capability = _empirical_count_capability(
1282
+ prior_realizations,
1283
+ kind="source",
1284
+ current_wave_index=wave_index,
1285
+ evaluation_slots=sum(evaluation_slots),
1286
+ arm_ids=available_source_ids,
1287
+ )
1288
+ operator_count_capability = _empirical_count_capability(
1289
+ prior_realizations,
1290
+ kind="operator",
1291
+ current_wave_index=wave_index,
1292
+ evaluation_slots=sum(evaluation_slots),
1293
+ arm_ids=self.available_operator_ids,
1294
+ )
1295
+ query = ContextualSearchQuery(
1296
+ campaign_scope_sha256=self.campaign_scope_sha256,
1297
+ wave_index=wave_index,
1298
+ total_portfolio_waves=total_portfolio_waves,
1299
+ real_evaluation_slots=sum(evaluation_slots),
1300
+ available_source_ids=available_source_ids,
1301
+ available_operator_ids=self.available_operator_ids,
1302
+ incumbent_source_id=self.incumbent_source_id,
1303
+ incumbent_operator_id=self.incumbent_operator_id,
1304
+ archive_front_size=archive_front_size,
1305
+ # Individual fixed-reference marginals overlap and are not an
1306
+ # authenticated stage-level archive delta. Keep this channel empty
1307
+ # until the runtime publishes an exact pre/post utility receipt.
1308
+ recent_normalized_archive_gains=(),
1309
+ composition_evidence_available=composition_evidence_available,
1310
+ source_count_capability=source_count_capability,
1311
+ operator_count_capability=operator_count_capability,
1312
+ joint_count_capabilities=joint_count_capabilities,
1313
+ )
1314
+ snapshot = self.ledger.snapshot(
1315
+ campaign_scope_sha256=self.campaign_scope_sha256,
1316
+ cutoff_wave_index_exclusive=wave_index,
1317
+ available_source_ids=available_source_ids,
1318
+ available_operator_ids=self.available_operator_ids,
1319
+ )
1320
+ decision = self.controller.decide(query, snapshot)
1321
+ stage_allocation = slice_contextual_search_decision(
1322
+ decision,
1323
+ slice_ids=slice_ids,
1324
+ evaluation_slots=evaluation_slots,
1325
+ )
1326
+ frontier_targets = self.frontier_target_allocator.allocate(
1327
+ archive_utility=ordered[0].stage_request.archive_utility,
1328
+ lanes=tuple((value.parent_lane.lane_id, value.parent) for value in ordered),
1329
+ )
1330
+ if self.require_objective_space_targets and any(
1331
+ objective_space_target_from_campaign_target(value) is None
1332
+ for value in frontier_targets
1333
+ ):
1334
+ raise ValueError(
1335
+ "contextual frontier target omits the required raw objective-space "
1336
+ "representation"
1337
+ )
1338
+ plan = CampaignContextualSearchPlan(
1339
+ campaign_generation=generation,
1340
+ stage_allocation=stage_allocation,
1341
+ contracts=tuple(
1342
+ value.to_contract(campaign_generation=generation)
1343
+ for value in stage_allocation.slices
1344
+ ),
1345
+ frontier_targets=frontier_targets,
1346
+ )
1347
+ existing = next(
1348
+ (value for value in self.plans if value.campaign_generation == generation),
1349
+ None,
1350
+ )
1351
+ if existing is not None:
1352
+ if existing != plan:
1353
+ raise RuntimeError("contextual stage was replanned differently")
1354
+ return existing
1355
+ self.plans.append(plan)
1356
+ return plan
1357
+
1358
+
1359
+ __all__ = [
1360
+ "CampaignContextualJointCapabilityProjector",
1361
+ "CampaignContextualPlanningContext",
1362
+ "CampaignContextualSearchPlan",
1363
+ "CampaignContextualSearchPlanner",
1364
+ "FiniteContractContextualJointCapabilityProjector",
1365
+ "_empirical_count_capability",
1366
+ ]