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.
- agent_evolve/__init__.py +722 -0
- agent_evolve/agentic.py +2800 -0
- agent_evolve/api.py +767 -0
- agent_evolve/application/__init__.py +1580 -0
- agent_evolve/application/action_allocation.py +744 -0
- agent_evolve/application/action_allocation_frame.py +347 -0
- agent_evolve/application/action_allocation_frame_commit.py +185 -0
- agent_evolve/application/action_allocation_frame_commit_v3.py +184 -0
- agent_evolve/application/action_allocation_frame_v3.py +338 -0
- agent_evolve/application/action_archive_value.py +497 -0
- agent_evolve/application/action_evidence_consistency.py +455 -0
- agent_evolve/application/action_forecast_partitioning.py +1471 -0
- agent_evolve/application/action_metric_projection.py +211 -0
- agent_evolve/application/action_role_value.py +680 -0
- agent_evolve/application/action_score_authorities.py +363 -0
- agent_evolve/application/action_structural_signature.py +116 -0
- agent_evolve/application/action_target_realization.py +402 -0
- agent_evolve/application/agentic_evolution.py +7734 -0
- agent_evolve/application/agentic_portfolio_residual_expert.py +835 -0
- agent_evolve/application/anchor_residual_identification.py +463 -0
- agent_evolve/application/archive_conditioned_action_target.py +208 -0
- agent_evolve/application/artifact_journal.py +246 -0
- agent_evolve/application/artifact_replay.py +347 -0
- agent_evolve/application/budgeted_optimizer.py +1828 -0
- agent_evolve/application/calibrated_campaign.py +485 -0
- agent_evolve/application/calibrated_current_prefix_forecast_opportunity.py +322 -0
- agent_evolve/application/calibrated_positive_gain_opportunity.py +1581 -0
- agent_evolve/application/campaign_capacity_recourse.py +254 -0
- agent_evolve/application/campaign_contextual_outcomes.py +119 -0
- agent_evolve/application/campaign_diagnostic_blocks.py +930 -0
- agent_evolve/application/campaign_evidence_registry.py +262 -0
- agent_evolve/application/campaign_execution.py +2537 -0
- agent_evolve/application/campaign_generation_audit.py +942 -0
- agent_evolve/application/campaign_learning.py +1812 -0
- agent_evolve/application/campaign_learning_runtime.py +1977 -0
- agent_evolve/application/campaign_search_phase.py +227 -0
- agent_evolve/application/campaign_selector_context_extension.py +220 -0
- agent_evolve/application/campaign_variation_envelope.py +649 -0
- agent_evolve/application/campaign_variation_trace.py +451 -0
- agent_evolve/application/candidate_archive_consequence.py +128 -0
- agent_evolve/application/causal_opportunity_portfolio_gate.py +385 -0
- agent_evolve/application/composite_outcome_updater.py +145 -0
- agent_evolve/application/composition_portfolio_selection.py +363 -0
- agent_evolve/application/concurrent_stage.py +144 -0
- agent_evolve/application/contextual_action_allocation.py +181 -0
- agent_evolve/application/contextual_campaign_outcomes.py +267 -0
- agent_evolve/application/contextual_campaign_planning.py +1366 -0
- agent_evolve/application/contextual_delayed_credit.py +651 -0
- agent_evolve/application/contextual_search_controller.py +2374 -0
- agent_evolve/application/current_prefix_forecast_opportunity.py +714 -0
- agent_evolve/application/decision_metric_projection.py +112 -0
- agent_evolve/application/derived_action_semantics.py +129 -0
- agent_evolve/application/detailed_evaluation.py +449 -0
- agent_evolve/application/earned_lineage.py +1011 -0
- agent_evolve/application/effective_choice_audit.py +484 -0
- agent_evolve/application/empirical_consequence_calibration.py +908 -0
- agent_evolve/application/evaluation_accounting.py +325 -0
- agent_evolve/application/evaluation_cache.py +199 -0
- agent_evolve/application/evaluation_escrow.py +547 -0
- agent_evolve/application/evaluation_recourse.py +253 -0
- agent_evolve/application/event_recorder.py +151 -0
- agent_evolve/application/evolution_campaign.py +1840 -0
- agent_evolve/application/executable_hypothesis.py +323 -0
- agent_evolve/application/factorial_branch_pilot.py +772 -0
- agent_evolve/application/finite_acquisition_capacity_recourse.py +672 -0
- agent_evolve/application/finite_acquisition_residual_expert.py +373 -0
- agent_evolve/application/finite_acquisition_variation_envelope.py +802 -0
- agent_evolve/application/finite_action_hypothesis_semantics.py +446 -0
- agent_evolve/application/finite_action_selection.py +188 -0
- agent_evolve/application/finite_action_set.py +306 -0
- agent_evolve/application/finite_action_transition.py +537 -0
- agent_evolve/application/finite_variation_eligibility.py +296 -0
- agent_evolve/application/forecast_geometry_portfolio.py +799 -0
- agent_evolve/application/forecast_opportunity_shadow_calibration.py +316 -0
- agent_evolve/application/front_proximity_admission.py +311 -0
- agent_evolve/application/front_proximity_parent_basis.py +458 -0
- agent_evolve/application/frozen_hurdle_score.py +659 -0
- agent_evolve/application/g3_causal_screen.py +2257 -0
- agent_evolve/application/g3_causal_validation.py +1046 -0
- agent_evolve/application/g3_postseal_curation.py +818 -0
- agent_evolve/application/gated_agentic_generator.py +205 -0
- agent_evolve/application/generation_feedback.py +293 -0
- agent_evolve/application/generative_proposal_journal.py +185 -0
- agent_evolve/application/geometry_conditional_elasticity.py +453 -0
- agent_evolve/application/global_wave_action_allocation.py +1151 -0
- agent_evolve/application/head_mass_conditional_seat.py +268 -0
- agent_evolve/application/identifiable_reflection_evidence.py +1147 -0
- agent_evolve/application/identifiable_reflection_learning.py +395 -0
- agent_evolve/application/identifiable_reflection_request.py +364 -0
- agent_evolve/application/in_memory_residual_archive.py +341 -0
- agent_evolve/application/insight_memory.py +1804 -0
- agent_evolve/application/live_runtime_manifest.py +758 -0
- agent_evolve/application/llm_task_queue.py +769 -0
- agent_evolve/application/matched_finite_action_block.py +409 -0
- agent_evolve/application/materialized_action_broker.py +2328 -0
- agent_evolve/application/materialized_action_constraints.py +83 -0
- agent_evolve/application/materialized_variation.py +211 -0
- agent_evolve/application/multi_option_evolution.py +1536 -0
- agent_evolve/application/outcome_adaptive_action_racing.py +2827 -0
- agent_evolve/application/outcome_adaptive_residual_campaign_runtime.py +580 -0
- agent_evolve/application/outcome_adaptive_residual_portfolio_evolution.py +3671 -0
- agent_evolve/application/outcome_conditioned_portfolio_selection.py +1374 -0
- agent_evolve/application/outcome_relation.py +193 -0
- agent_evolve/application/paired_allocation_comparison.py +241 -0
- agent_evolve/application/paired_block_schedule.py +127 -0
- agent_evolve/application/parent_measurement.py +226 -0
- agent_evolve/application/pareto_archive.py +811 -0
- agent_evolve/application/portfolio_campaign_runtime.py +4739 -0
- agent_evolve/application/portfolio_evolution.py +2950 -0
- agent_evolve/application/portfolio_hypothesis_observations.py +814 -0
- agent_evolve/application/portfolio_memory_attribution.py +581 -0
- agent_evolve/application/portfolio_memory_dose.py +788 -0
- agent_evolve/application/portfolio_memory_matched_control.py +938 -0
- agent_evolve/application/portfolio_memory_transfer.py +297 -0
- agent_evolve/application/portfolio_optimization_memory.py +363 -0
- agent_evolve/application/portfolio_outcome_feedback.py +1613 -0
- agent_evolve/application/portfolio_projection.py +335 -0
- agent_evolve/application/portfolio_recombination.py +2032 -0
- agent_evolve/application/post_evolution_reflection.py +834 -0
- agent_evolve/application/postcommit_rank_authority.py +245 -0
- agent_evolve/application/precommitted_portfolio_racing.py +2762 -0
- agent_evolve/application/prequential_archive_opportunity_calibration.py +1154 -0
- agent_evolve/application/prequential_residual_exploration.py +343 -0
- agent_evolve/application/prequential_score_portfolio.py +954 -0
- agent_evolve/application/projections.py +292 -0
- agent_evolve/application/protected_action_committee.py +1027 -0
- agent_evolve/application/protected_branch_pilot.py +376 -0
- agent_evolve/application/protected_current_prefix_forecast_opportunity.py +552 -0
- agent_evolve/application/provider_replay.py +910 -0
- agent_evolve/application/rank_balanced_causal_pilot.py +1372 -0
- agent_evolve/application/recombination_residual_expert.py +403 -0
- agent_evolve/application/reflection_workflow.py +571 -0
- agent_evolve/application/region_conditional_credit.py +911 -0
- agent_evolve/application/residual_campaign_runtime.py +531 -0
- agent_evolve/application/residual_headroom_campaign_runtime.py +459 -0
- agent_evolve/application/residual_headroom_ledger.py +1544 -0
- agent_evolve/application/residual_learning_transaction.py +396 -0
- agent_evolve/application/residual_portfolio_evolution.py +1228 -0
- agent_evolve/application/residual_reachability.py +749 -0
- agent_evolve/application/residual_stage_credit.py +499 -0
- agent_evolve/application/same_prefix_paired_audit.py +1580 -0
- agent_evolve/application/semantic_coverage_score_portfolio.py +838 -0
- agent_evolve/application/sequential_lineage_allocation.py +1017 -0
- agent_evolve/application/sequential_market_replay.py +1395 -0
- agent_evolve/application/sequential_residual_campaign_runtime.py +305 -0
- agent_evolve/application/sequential_residual_portfolio_evolution.py +940 -0
- agent_evolve/application/single_score_action_allocation.py +299 -0
- agent_evolve/application/source_exposure_allocation.py +906 -0
- agent_evolve/application/staged_memory.py +210 -0
- agent_evolve/application/stratified_cold_start_allocation.py +732 -0
- agent_evolve/application/support_guarded_hurdle_score.py +549 -0
- agent_evolve/application/target_conditioned_action_forecast.py +595 -0
- agent_evolve/application/target_conditioned_campaign.py +566 -0
- agent_evolve/application/treatment_assignment.py +201 -0
- agent_evolve/application/trusted_objective_evidence.py +217 -0
- agent_evolve/application/two_stage_action_evolution.py +1131 -0
- agent_evolve/application/v8lite_allocation_policy.py +1083 -0
- agent_evolve/application/v9_candidate_policy.py +1303 -0
- agent_evolve/bootstrap.py +108 -0
- agent_evolve/campaign_presets.py +517 -0
- agent_evolve/campaign_profiles.py +452 -0
- agent_evolve/campaign_variation_topology.py +288 -0
- agent_evolve/campaign_workload.py +950 -0
- agent_evolve/cli.py +797 -0
- agent_evolve/contract.py +241 -0
- agent_evolve/core/__init__.py +91 -0
- agent_evolve/core/action_semantics.py +411 -0
- agent_evolve/core/authored.py +105 -0
- agent_evolve/core/formatting.py +286 -0
- agent_evolve/core/optimization_semantics.py +324 -0
- agent_evolve/core/problem.py +167 -0
- agent_evolve/core/results.py +323 -0
- agent_evolve/core/stats.py +70 -0
- agent_evolve/core/telemetry.py +100 -0
- agent_evolve/domain/__init__.py +89 -0
- agent_evolve/domain/artifact.py +162 -0
- agent_evolve/domain/durable_text.py +68 -0
- agent_evolve/domain/event.py +1454 -0
- agent_evolve/domain/finite_action_set.py +426 -0
- agent_evolve/domain/finite_variation.py +526 -0
- agent_evolve/domain/generative_emission.py +559 -0
- agent_evolve/domain/ids.py +163 -0
- agent_evolve/domain/inline_text.py +106 -0
- agent_evolve/domain/insight.py +27 -0
- agent_evolve/domain/lineage.py +737 -0
- agent_evolve/domain/llm_task_queue.py +960 -0
- agent_evolve/domain/outcome.py +96 -0
- agent_evolve/domain/patch.py +854 -0
- agent_evolve/domain/typed_json.py +542 -0
- agent_evolve/domain/variation_space.py +158 -0
- agent_evolve/driver.py +1014 -0
- agent_evolve/harness/__init__.py +29 -0
- agent_evolve/harness/base.py +242 -0
- agent_evolve/harness/directives.py +163 -0
- agent_evolve/harness/generative_seal.py +479 -0
- agent_evolve/harness/registry.py +41 -0
- agent_evolve/infrastructure/__init__.py +39 -0
- agent_evolve/infrastructure/artifacts/__init__.py +6 -0
- agent_evolve/infrastructure/artifacts/_verification.py +67 -0
- agent_evolve/infrastructure/artifacts/filesystem.py +343 -0
- agent_evolve/infrastructure/artifacts/in_memory.py +73 -0
- agent_evolve/infrastructure/asyncio_runtime.py +109 -0
- agent_evolve/infrastructure/authored_runtime.py +188 -0
- agent_evolve/infrastructure/authored_worker.py +171 -0
- agent_evolve/infrastructure/clock.py +53 -0
- agent_evolve/infrastructure/events/__init__.py +6 -0
- agent_evolve/infrastructure/events/_validation.py +89 -0
- agent_evolve/infrastructure/events/in_memory.py +56 -0
- agent_evolve/infrastructure/events/jsonl.py +193 -0
- agent_evolve/infrastructure/exception_provenance.py +215 -0
- agent_evolve/infrastructure/ids.py +118 -0
- agent_evolve/infrastructure/lineage_codec.py +1836 -0
- agent_evolve/infrastructure/outcome_adaptive_phase_journal.py +170 -0
- agent_evolve/infrastructure/residual_headroom_journal.py +221 -0
- agent_evolve/infrastructure/resource_lease.py +370 -0
- agent_evolve/infrastructure/sanitization/__init__.py +8 -0
- agent_evolve/infrastructure/sanitization/strict_json.py +484 -0
- agent_evolve/infrastructure/sequential_phase_journal.py +170 -0
- agent_evolve/infrastructure/stream_liveness.py +383 -0
- agent_evolve/infrastructure/subprocess_boundary.py +136 -0
- agent_evolve/integrations/__init__.py +1 -0
- agent_evolve/integrations/botorch/__init__.py +28 -0
- agent_evolve/integrations/botorch/finite_qlognehvi.py +190 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch.py +155 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch_identity.py +20 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch_worker.py +55 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_identity.py +22 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_worker.py +55 -0
- agent_evolve/integrations/botorch/subprocess_qlognehvi.py +261 -0
- agent_evolve/integrations/botorch/subprocess_qlognehvi_batch.py +273 -0
- agent_evolve/integrations/completion.py +242 -0
- agent_evolve/integrations/pydantic_ai/__init__.py +441 -0
- agent_evolve/integrations/pydantic_ai/action_forecast.py +1068 -0
- agent_evolve/integrations/pydantic_ai/agentic_generator.py +2308 -0
- agent_evolve/integrations/pydantic_ai/async_generator.py +1604 -0
- agent_evolve/integrations/pydantic_ai/boundary_codec.py +1526 -0
- agent_evolve/integrations/pydantic_ai/calibrated_portfolio_campaign.py +756 -0
- agent_evolve/integrations/pydantic_ai/calibrated_portfolio_selection.py +7537 -0
- agent_evolve/integrations/pydantic_ai/campaign_acquisition.py +609 -0
- agent_evolve/integrations/pydantic_ai/execution_binding.py +138 -0
- agent_evolve/integrations/pydantic_ai/forecast_geometry_action_committee.py +217 -0
- agent_evolve/integrations/pydantic_ai/harness.py +159 -0
- agent_evolve/integrations/pydantic_ai/heterogeneous_model_execution.py +306 -0
- agent_evolve/integrations/pydantic_ai/hierarchical_residual_adaptive_semantic_view.py +179 -0
- agent_evolve/integrations/pydantic_ai/json_schema_dialect.py +108 -0
- agent_evolve/integrations/pydantic_ai/materialized_hierarchical_residual_expert.py +952 -0
- agent_evolve/integrations/pydantic_ai/materialized_portfolio_judge.py +520 -0
- agent_evolve/integrations/pydantic_ai/model_execution_profile.py +659 -0
- agent_evolve/integrations/pydantic_ai/outbound_request_manifest.py +1170 -0
- agent_evolve/integrations/pydantic_ai/portable_residual_consequence_features.py +575 -0
- agent_evolve/integrations/pydantic_ai/portfolio_selection.py +422 -0
- agent_evolve/integrations/pydantic_ai/progress_aware_openrouter.py +416 -0
- agent_evolve/integrations/pydantic_ai/provider_attempt_join.py +1523 -0
- agent_evolve/integrations/pydantic_ai/provider_free_calibrated_runner.py +607 -0
- agent_evolve/integrations/pydantic_ai/queued_runner.py +2634 -0
- agent_evolve/integrations/pydantic_ai/reconciled_residual_reachability.py +1417 -0
- agent_evolve/integrations/pydantic_ai/residual_forecast_geometry.py +445 -0
- agent_evolve/integrations/pydantic_ai/residual_reachability.py +674 -0
- agent_evolve/integrations/pydantic_ai/residual_semantic_cells.py +239 -0
- agent_evolve/integrations/pydantic_ai/sealed_output_replay.py +1068 -0
- agent_evolve/integrations/pydantic_ai/semantic_coverage_residual_portfolio.py +770 -0
- agent_evolve/integrations/pydantic_ai/semantic_decision_replay.py +383 -0
- agent_evolve/integrations/pydantic_ai/support_adaptive_residual_portfolio.py +135 -0
- agent_evolve/integrations/pydantic_ai/trusted_residual_prompt_context.py +143 -0
- agent_evolve/integrations/pydantic_ai/validated_openrouter_model.py +107 -0
- agent_evolve/integrations/pymoo_adapter.py +242 -0
- agent_evolve/policies/__init__.py +17 -0
- agent_evolve/policies/check.py +469 -0
- agent_evolve/policies/emit_scaffold.py +451 -0
- agent_evolve/policies/feedback/__init__.py +37 -0
- agent_evolve/policies/feedback/held_out_asn.py +1325 -0
- agent_evolve/policies/genetic.py +607 -0
- agent_evolve/policies/llm_backoff.py +183 -0
- agent_evolve/policies/llm_chooser.py +226 -0
- agent_evolve/policies/llm_generator.py +1760 -0
- agent_evolve/policies/llm_init.py +267 -0
- agent_evolve/policies/llm_operator.py +109 -0
- agent_evolve/policies/llm_prior.py +194 -0
- agent_evolve/policies/llm_surrogate.py +334 -0
- agent_evolve/policies/measurement_evidence.py +704 -0
- agent_evolve/policies/memory/__init__.py +223 -0
- agent_evolve/policies/memory/balanced_subset_blocks.py +707 -0
- agent_evolve/policies/memory/compatibility_matching.py +593 -0
- agent_evolve/policies/memory/global_falsification.py +1841 -0
- agent_evolve/policies/memory/prompt_shape.py +503 -0
- agent_evolve/policies/memory/randomized_subset.py +714 -0
- agent_evolve/policies/memory/staged_causal.py +1270 -0
- agent_evolve/policies/memory/treatment_compliance.py +759 -0
- agent_evolve/policies/objective_resolution/__init__.py +17 -0
- agent_evolve/policies/objective_resolution/fixed_grid.py +364 -0
- agent_evolve/policies/operator_portfolio.py +407 -0
- agent_evolve/policies/reguidance.py +1133 -0
- agent_evolve/policies/reward/__init__.py +83 -0
- agent_evolve/policies/reward/affine_candidate_consequence.py +156 -0
- agent_evolve/policies/reward/affine_candidate_consequence_3d.py +159 -0
- agent_evolve/policies/reward/affine_hypervolume.py +490 -0
- agent_evolve/policies/reward/affine_hypervolume_3d.py +567 -0
- agent_evolve/policies/reward/contextual_marginal_utility.py +318 -0
- agent_evolve/policies/reward/frozen_archive.py +360 -0
- agent_evolve/policies/reward/frozen_wave_archive.py +368 -0
- agent_evolve/policies/search_state.py +208 -0
- agent_evolve/policies/selection/__init__.py +345 -0
- agent_evolve/policies/selection/acquisition_certified_slate.py +684 -0
- agent_evolve/policies/selection/affine_frontier_context.py +330 -0
- agent_evolve/policies/selection/affine_frontier_target.py +473 -0
- agent_evolve/policies/selection/archive_elite.py +1346 -0
- agent_evolve/policies/selection/calibrated_portfolio_binding.py +640 -0
- agent_evolve/policies/selection/calibrated_slate.py +1394 -0
- agent_evolve/policies/selection/calibrated_slate_codec.py +579 -0
- agent_evolve/policies/selection/common_candidate_pool.py +685 -0
- agent_evolve/policies/selection/diagnostic_sampling.py +319 -0
- agent_evolve/policies/selection/disjoint_pairs.py +479 -0
- agent_evolve/policies/selection/elite_explorer.py +719 -0
- agent_evolve/policies/selection/finite_action.py +187 -0
- agent_evolve/policies/selection/finite_option_prompt_projection.py +377 -0
- agent_evolve/policies/selection/finite_palette_evidence.py +247 -0
- agent_evolve/policies/selection/forecast_calibration.py +922 -0
- agent_evolve/policies/selection/frontier_probe_slate.py +814 -0
- agent_evolve/policies/selection/frozen_archive_pairs.py +762 -0
- agent_evolve/policies/selection/full_support_slate.py +91 -0
- agent_evolve/policies/selection/meaningful_direction.py +240 -0
- agent_evolve/policies/selection/memory_dose_feasibility.py +259 -0
- agent_evolve/policies/selection/model_anchored_slate.py +826 -0
- agent_evolve/policies/selection/phenotype_recourse.py +979 -0
- agent_evolve/policies/selection/proposal_support.py +368 -0
- agent_evolve/policies/selection/random_portfolio.py +254 -0
- agent_evolve/policies/selection/regret_bounded_slate.py +1084 -0
- agent_evolve/policies/selection/residual_frontier.py +463 -0
- agent_evolve/policies/selection/residual_frontier_target.py +605 -0
- agent_evolve/policies/selection/structural_posterior_slate.py +1571 -0
- agent_evolve/policies/selection/target_conditioned_allocator.py +648 -0
- agent_evolve/policies/selection/target_conditioned_features.py +812 -0
- agent_evolve/policies/selection/target_conditioned_prequential.py +1527 -0
- agent_evolve/policies/selection/task_keyed_palette.py +906 -0
- agent_evolve/policies/semantics.py +147 -0
- agent_evolve/policies/structure.py +362 -0
- agent_evolve/policies/structured_output_budget.py +62 -0
- agent_evolve/policies/surrogate.py +696 -0
- agent_evolve/policies/variation/__init__.py +1 -0
- agent_evolve/policies/variation/compositional_finite_catalog.py +426 -0
- agent_evolve/policies/variation/crossover_inheritance.py +575 -0
- agent_evolve/policies/variation/disjoint_recombination.py +611 -0
- agent_evolve/policies/variation/exact_composition_capacity.py +214 -0
- agent_evolve/policies/variation/exact_parent_crossover.py +950 -0
- agent_evolve/policies/variation/multiscale_restart_catalog.py +372 -0
- agent_evolve/policies/variation/source_union_finite_catalog.py +403 -0
- agent_evolve/policies/variation/typed_patch.py +1981 -0
- agent_evolve/policies/weighted_prior.py +394 -0
- agent_evolve/ports/__init__.py +383 -0
- agent_evolve/ports/action_allocation.py +733 -0
- agent_evolve/ports/action_allocation_frame.py +1153 -0
- agent_evolve/ports/action_allocation_frame_commit.py +294 -0
- agent_evolve/ports/action_allocation_frame_commit_v3.py +432 -0
- agent_evolve/ports/action_allocation_frame_v3.py +995 -0
- agent_evolve/ports/action_forecast.py +1568 -0
- agent_evolve/ports/action_metric_projection.py +165 -0
- agent_evolve/ports/agentic_generator.py +1561 -0
- agent_evolve/ports/archive_context.py +136 -0
- agent_evolve/ports/artifact_sanitizer.py +44 -0
- agent_evolve/ports/artifact_store.py +225 -0
- agent_evolve/ports/clock.py +13 -0
- agent_evolve/ports/contextual_search_allocation.py +827 -0
- agent_evolve/ports/decision_metric_projection.py +258 -0
- agent_evolve/ports/event_store.py +55 -0
- agent_evolve/ports/executable_hypothesis.py +557 -0
- agent_evolve/ports/finite_acquisition.py +377 -0
- agent_evolve/ports/finite_acquisition_batch.py +296 -0
- agent_evolve/ports/finite_acquisition_batch_json.py +164 -0
- agent_evolve/ports/finite_acquisition_json.py +247 -0
- agent_evolve/ports/finite_acquisition_space.py +168 -0
- agent_evolve/ports/finite_action_selection.py +348 -0
- agent_evolve/ports/finite_action_set.py +256 -0
- agent_evolve/ports/frontier_target.py +396 -0
- agent_evolve/ports/generation_failure.py +43 -0
- agent_evolve/ports/hard_feasibility.py +233 -0
- agent_evolve/ports/id_factory.py +34 -0
- agent_evolve/ports/llm_task_queue.py +93 -0
- agent_evolve/ports/objective_resolution.py +419 -0
- agent_evolve/ports/paired_allocation_comparison.py +401 -0
- agent_evolve/ports/paired_block_schedule.py +475 -0
- agent_evolve/ports/parent_measurement.py +336 -0
- agent_evolve/ports/portfolio_memory_dose.py +643 -0
- agent_evolve/ports/portfolio_selection.py +3169 -0
- agent_evolve/ports/postcommit_rank_authority.py +467 -0
- agent_evolve/ports/presented_action_evidence.py +794 -0
- agent_evolve/ports/resource_lease.py +162 -0
- agent_evolve/ports/structured_generator.py +734 -0
- agent_evolve/ports/structured_output_budget.py +120 -0
- agent_evolve/ports/subprocess_boundary.py +138 -0
- agent_evolve/ports/treatment_assignment.py +466 -0
- agent_evolve/ports/variation_catalog.py +76 -0
- agent_evolve/ports/variation_source.py +226 -0
- agent_evolve/proposal_mode.py +157 -0
- agent_evolve/proposers/__init__.py +10 -0
- agent_evolve/proposers/random_proposer.py +188 -0
- agent_evolve/provider_accounting.py +163 -0
- agent_evolve/py.typed +0 -0
- agent_evolve/reference_method.py +1570 -0
- agent_evolve/session/__init__.py +11 -0
- agent_evolve/session/authorship.py +864 -0
- agent_evolve/session/evaluate.py +236 -0
- agent_evolve/session/fidelity.py +237 -0
- agent_evolve/session/genetic_loop.py +742 -0
- agent_evolve/session/loop.py +803 -0
- agent_evolve/session/screening.py +671 -0
- agent_evolve/settings.py +376 -0
- agent_evolve/workload_kit.py +368 -0
- agent_evolve/workload_prompt.py +398 -0
- agentevolve_optimizer-0.5.0.dist-info/METADATA +599 -0
- agentevolve_optimizer-0.5.0.dist-info/RECORD +414 -0
- agentevolve_optimizer-0.5.0.dist-info/WHEEL +5 -0
- agentevolve_optimizer-0.5.0.dist-info/entry_points.txt +2 -0
- agentevolve_optimizer-0.5.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|