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,2374 @@
|
|
|
1
|
+
"""Authenticated phase-aware credit and allocation for generic search sources.
|
|
2
|
+
|
|
3
|
+
The controller operates only on workload-neutral, normalized observations. It
|
|
4
|
+
does not inspect candidate fields, objective names, model prose, or provider
|
|
5
|
+
metadata. Sources (for example model, restart, global coverage, or memory) and
|
|
6
|
+
operators (for example atomic or recombination families) share the same typed
|
|
7
|
+
posterior machinery.
|
|
8
|
+
|
|
9
|
+
The controller is intentionally deterministic. It uses the empirical mean of
|
|
10
|
+
bounded normalized archive return and one distribution-free uncertainty slot
|
|
11
|
+
instead of stochastic Thompson sampling, which makes every decision exactly
|
|
12
|
+
replayable while preserving the scale of small but real archive improvements.
|
|
13
|
+
Feasibility, persistence, descendant yield, and allocation realizability remain
|
|
14
|
+
separate diagnostic channels, but are not added to return with arbitrary
|
|
15
|
+
weights.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import math
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass, field, replace
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from itertools import product
|
|
27
|
+
|
|
28
|
+
from agent_evolve.domain.ids import CandidateId
|
|
29
|
+
from agent_evolve.domain.patch import require_sha256
|
|
30
|
+
from agent_evolve.ports.contextual_search_allocation import (
|
|
31
|
+
ContextualArmCountCapability,
|
|
32
|
+
ContextualLaneJointCountCapability,
|
|
33
|
+
ContextualPortfolioAllocationContract,
|
|
34
|
+
ContextualPortfolioAllocationRealization,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_TOKEN = re.compile(r"^[a-z][a-z0-9_.:-]{0,255}$")
|
|
39
|
+
CONTEXTUAL_SEARCH_CONTROLLER_ID = "phase_aware_contextual_source_operator"
|
|
40
|
+
CONTEXTUAL_SEARCH_CONTROLLER_VERSION = 9
|
|
41
|
+
CONTEXTUAL_SEARCH_CONTROLLER_DEFINITION_SHA256 = hashlib.sha256(
|
|
42
|
+
b"agent-evolve:phase-aware-contextual-source-operator:v9;"
|
|
43
|
+
b"evidence=authenticated-prior-only-normalized-outcomes;"
|
|
44
|
+
b"selection-currency=normalized-archive-return;"
|
|
45
|
+
b"diagnostics=positive-marginal,utility-share,feasibility,"
|
|
46
|
+
b"stage-front-survival,final-persistence,descendant,allocation-realizability;"
|
|
47
|
+
b"delayed-credit=append-only-source-joined-prior-cutoff;"
|
|
48
|
+
b"allocation-realization=objective-workload-blind-request-overlap;"
|
|
49
|
+
b"return-estimator=bounded-empirical-mean;"
|
|
50
|
+
b"uncertainty=distribution-free-maximum-standard-error;deterministic=true;"
|
|
51
|
+
b"phases=basin-acquisition,basin-expansion,composition,terminal-conversion;"
|
|
52
|
+
b"terminal-information-bonus=zero;one-nonterminal-exploration-slot=true;"
|
|
53
|
+
b"allocation=posterior-mean-proportional-plus-one-uncertainty-slot;"
|
|
54
|
+
b"cold-start=prior-proportional-plus-one-exploration;"
|
|
55
|
+
b"source-axis=sealed-finite-variation-source-not-reconciliation-origin;"
|
|
56
|
+
b"source-incumbent-prior-mass=0.50;operator-incumbent-prior-mass=0.75;"
|
|
57
|
+
b"empirical-capability=prior-realized-stage-count-witnesses;"
|
|
58
|
+
b"capability-projection=minimum-l1-then-maximum-posterior-score;"
|
|
59
|
+
b"capability-is-witness-not-current-guarantee=true;"
|
|
60
|
+
b"prospective-joint-capability=finite-contract-lane-product;"
|
|
61
|
+
b"joint-projection=maximum-feasible-exploration-retention-then-minimum-"
|
|
62
|
+
b"source-operator-l1-then-posterior-score;"
|
|
63
|
+
b"joint-exploration-recourse=deterministic-realizable-challenger;"
|
|
64
|
+
b"joint-slicing=exact-witnessed-lane-vector;"
|
|
65
|
+
b"durable-evidence=expanded-authenticated-query-and-prior-snapshot;"
|
|
66
|
+
b"workload-model-provider-identifiers=false"
|
|
67
|
+
).hexdigest()
|
|
68
|
+
_OBSERVATION_DOMAIN = b"agent-evolve:contextual-search-observation:v2\x00"
|
|
69
|
+
_DELAYED_CREDIT_DOMAIN = b"agent-evolve:contextual-search-delayed-credit:v2\x00"
|
|
70
|
+
_SNAPSHOT_DOMAIN = b"agent-evolve:contextual-search-snapshot:v2\x00"
|
|
71
|
+
_QUERY_DOMAIN = b"agent-evolve:contextual-search-query:v3\x00"
|
|
72
|
+
_DECISION_DOMAIN = b"agent-evolve:contextual-search-decision:v3\x00"
|
|
73
|
+
_ALLOCATION_SLICE_DOMAIN = b"agent-evolve:contextual-search-allocation-slice:v1\x00"
|
|
74
|
+
_STAGE_ALLOCATION_DOMAIN = b"agent-evolve:contextual-search-stage-allocation:v1\x00"
|
|
75
|
+
_COMPLETION_AUDIT_DOMAIN = b"agent-evolve:contextual-search-completion-audit:v1\x00"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _canonical_json(value: object) -> bytes:
|
|
79
|
+
return json.dumps(
|
|
80
|
+
value,
|
|
81
|
+
allow_nan=False,
|
|
82
|
+
ensure_ascii=True,
|
|
83
|
+
separators=(",", ":"),
|
|
84
|
+
sort_keys=True,
|
|
85
|
+
).encode("ascii", errors="strict")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _hash(domain: bytes, value: object) -> str:
|
|
89
|
+
return hashlib.sha256(domain + _canonical_json(value)).hexdigest()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _require_token(value: str, *, name: str) -> None:
|
|
93
|
+
if type(value) is not str or _TOKEN.fullmatch(value) is None:
|
|
94
|
+
raise ValueError(f"{name} must use the closed token grammar")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _require_probability(value: float, *, name: str) -> None:
|
|
98
|
+
if type(value) is not float or not math.isfinite(value):
|
|
99
|
+
raise TypeError(f"{name} must be a finite canonical float")
|
|
100
|
+
if not 0.0 <= value <= 1.0:
|
|
101
|
+
raise ValueError(f"{name} must lie in [0, 1]")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _canonical_tokens(values: tuple[str, ...], *, name: str) -> None:
|
|
105
|
+
if type(values) is not tuple or not values:
|
|
106
|
+
raise ValueError(f"{name} must be a non-empty exact tuple")
|
|
107
|
+
for value in values:
|
|
108
|
+
_require_token(value, name=name)
|
|
109
|
+
if values != tuple(sorted(set(values))):
|
|
110
|
+
raise ValueError(f"{name} must be unique and canonical")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SearchPhase(str, Enum):
|
|
114
|
+
BASIN_ACQUISITION = "basin_acquisition"
|
|
115
|
+
BASIN_EXPANSION = "basin_expansion"
|
|
116
|
+
COMPOSITION = "composition"
|
|
117
|
+
TERMINAL_CONVERSION = "terminal_conversion"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class SearchArmKind(str, Enum):
|
|
121
|
+
SOURCE = "source"
|
|
122
|
+
OPERATOR = "operator"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True, slots=True)
|
|
126
|
+
class ContextualSearchObservation:
|
|
127
|
+
"""One evaluated action with normalized, separately typed credit channels."""
|
|
128
|
+
|
|
129
|
+
campaign_scope_sha256: str
|
|
130
|
+
wave_index: int
|
|
131
|
+
source_id: str
|
|
132
|
+
operator_id: str
|
|
133
|
+
option_identity_sha256: str
|
|
134
|
+
parent_context_sha256: str
|
|
135
|
+
feasible: bool
|
|
136
|
+
positive_marginal_utility: bool
|
|
137
|
+
normalized_marginal_utility: float
|
|
138
|
+
marginal_utility_share: float
|
|
139
|
+
stage_front_persisted: bool | None = None
|
|
140
|
+
final_front_persisted: bool | None = None
|
|
141
|
+
useful_descendant_observed: bool | None = None
|
|
142
|
+
source_distance: float = 0.0
|
|
143
|
+
candidate_id: CandidateId | None = None
|
|
144
|
+
observation_sha256: str = field(init=False)
|
|
145
|
+
|
|
146
|
+
def __post_init__(self) -> None:
|
|
147
|
+
for name in (
|
|
148
|
+
"campaign_scope_sha256",
|
|
149
|
+
"option_identity_sha256",
|
|
150
|
+
"parent_context_sha256",
|
|
151
|
+
):
|
|
152
|
+
require_sha256(getattr(self, name), name)
|
|
153
|
+
if type(self.wave_index) is not int or self.wave_index <= 0:
|
|
154
|
+
raise ValueError("wave_index must be a positive exact integer")
|
|
155
|
+
_require_token(self.source_id, name="source_id")
|
|
156
|
+
_require_token(self.operator_id, name="operator_id")
|
|
157
|
+
for name in ("feasible", "positive_marginal_utility"):
|
|
158
|
+
if type(getattr(self, name)) is not bool:
|
|
159
|
+
raise TypeError(f"{name} must be an exact bool")
|
|
160
|
+
for name in (
|
|
161
|
+
"stage_front_persisted",
|
|
162
|
+
"final_front_persisted",
|
|
163
|
+
"useful_descendant_observed",
|
|
164
|
+
):
|
|
165
|
+
value = getattr(self, name)
|
|
166
|
+
if value is not None and type(value) is not bool:
|
|
167
|
+
raise TypeError(f"{name} must be an exact bool or None")
|
|
168
|
+
_require_probability(
|
|
169
|
+
self.normalized_marginal_utility,
|
|
170
|
+
name="normalized_marginal_utility",
|
|
171
|
+
)
|
|
172
|
+
_require_probability(
|
|
173
|
+
self.marginal_utility_share,
|
|
174
|
+
name="marginal_utility_share",
|
|
175
|
+
)
|
|
176
|
+
_require_probability(self.source_distance, name="source_distance")
|
|
177
|
+
if self.candidate_id is not None:
|
|
178
|
+
if type(self.candidate_id) is not CandidateId:
|
|
179
|
+
raise TypeError("candidate_id must be exact CandidateId or None")
|
|
180
|
+
CandidateId.__post_init__(self.candidate_id)
|
|
181
|
+
if not self.feasible and (
|
|
182
|
+
self.positive_marginal_utility
|
|
183
|
+
or self.normalized_marginal_utility != 0.0
|
|
184
|
+
or self.marginal_utility_share != 0.0
|
|
185
|
+
or self.stage_front_persisted is True
|
|
186
|
+
or self.final_front_persisted is True
|
|
187
|
+
or self.useful_descendant_observed is True
|
|
188
|
+
):
|
|
189
|
+
raise ValueError("infeasible observations cannot carry positive yield")
|
|
190
|
+
if self.positive_marginal_utility != (
|
|
191
|
+
self.normalized_marginal_utility > 0.0 and self.marginal_utility_share > 0.0
|
|
192
|
+
):
|
|
193
|
+
raise ValueError("positive verdict differs from its utility channels")
|
|
194
|
+
object.__setattr__(
|
|
195
|
+
self,
|
|
196
|
+
"observation_sha256",
|
|
197
|
+
_hash(_OBSERVATION_DOMAIN, self._unsigned_record()),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
201
|
+
return {
|
|
202
|
+
"schema_version": 2,
|
|
203
|
+
"campaign_scope_sha256": self.campaign_scope_sha256,
|
|
204
|
+
"wave_index": self.wave_index,
|
|
205
|
+
"source_id": self.source_id,
|
|
206
|
+
"operator_id": self.operator_id,
|
|
207
|
+
"option_identity_sha256": self.option_identity_sha256,
|
|
208
|
+
"parent_context_sha256": self.parent_context_sha256,
|
|
209
|
+
"feasible": self.feasible,
|
|
210
|
+
"positive_marginal_utility": self.positive_marginal_utility,
|
|
211
|
+
"normalized_marginal_utility_hex": (self.normalized_marginal_utility.hex()),
|
|
212
|
+
"marginal_utility_share_hex": self.marginal_utility_share.hex(),
|
|
213
|
+
"stage_front_persisted": self.stage_front_persisted,
|
|
214
|
+
"final_front_persisted": self.final_front_persisted,
|
|
215
|
+
"useful_descendant_observed": self.useful_descendant_observed,
|
|
216
|
+
"source_distance_hex": self.source_distance.hex(),
|
|
217
|
+
"candidate_id": (
|
|
218
|
+
None if self.candidate_id is None else self.candidate_id.value
|
|
219
|
+
),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def to_record(self) -> dict[str, object]:
|
|
223
|
+
self.__post_init__()
|
|
224
|
+
return {
|
|
225
|
+
**self._unsigned_record(),
|
|
226
|
+
"observation_sha256": self.observation_sha256,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass(frozen=True, slots=True)
|
|
231
|
+
class ContextualSearchDelayedCredit:
|
|
232
|
+
"""A later outcome joined to one immutable evaluated-action observation."""
|
|
233
|
+
|
|
234
|
+
campaign_scope_sha256: str
|
|
235
|
+
source_observation_sha256: str
|
|
236
|
+
available_at_wave_index: int
|
|
237
|
+
stage_front_persisted: bool | None = None
|
|
238
|
+
final_front_persisted: bool | None = None
|
|
239
|
+
useful_descendant_observed: bool | None = None
|
|
240
|
+
credit_sha256: str = field(init=False)
|
|
241
|
+
|
|
242
|
+
def __post_init__(self) -> None:
|
|
243
|
+
require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
|
|
244
|
+
require_sha256(
|
|
245
|
+
self.source_observation_sha256,
|
|
246
|
+
"source_observation_sha256",
|
|
247
|
+
)
|
|
248
|
+
if (
|
|
249
|
+
type(self.available_at_wave_index) is not int
|
|
250
|
+
or self.available_at_wave_index <= 0
|
|
251
|
+
):
|
|
252
|
+
raise ValueError("available_at_wave_index must be positive")
|
|
253
|
+
for name in (
|
|
254
|
+
"stage_front_persisted",
|
|
255
|
+
"final_front_persisted",
|
|
256
|
+
"useful_descendant_observed",
|
|
257
|
+
):
|
|
258
|
+
value = getattr(self, name)
|
|
259
|
+
if value is not None and type(value) is not bool:
|
|
260
|
+
raise TypeError(f"{name} must be an exact bool or None")
|
|
261
|
+
if (
|
|
262
|
+
self.stage_front_persisted is None
|
|
263
|
+
and self.final_front_persisted is None
|
|
264
|
+
and self.useful_descendant_observed is None
|
|
265
|
+
):
|
|
266
|
+
raise ValueError("delayed credit must adjudicate at least one channel")
|
|
267
|
+
object.__setattr__(
|
|
268
|
+
self,
|
|
269
|
+
"credit_sha256",
|
|
270
|
+
_hash(_DELAYED_CREDIT_DOMAIN, self._unsigned_record()),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
274
|
+
return {
|
|
275
|
+
"schema_version": 2,
|
|
276
|
+
"campaign_scope_sha256": self.campaign_scope_sha256,
|
|
277
|
+
"source_observation_sha256": self.source_observation_sha256,
|
|
278
|
+
"available_at_wave_index": self.available_at_wave_index,
|
|
279
|
+
"stage_front_persisted": self.stage_front_persisted,
|
|
280
|
+
"final_front_persisted": self.final_front_persisted,
|
|
281
|
+
"useful_descendant_observed": self.useful_descendant_observed,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
def to_record(self) -> dict[str, object]:
|
|
285
|
+
self.__post_init__()
|
|
286
|
+
return {**self._unsigned_record(), "credit_sha256": self.credit_sha256}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass(frozen=True, slots=True)
|
|
290
|
+
class ContextualArmPosterior:
|
|
291
|
+
"""Independent evidence channels for one source or operator arm."""
|
|
292
|
+
|
|
293
|
+
kind: SearchArmKind
|
|
294
|
+
arm_id: str
|
|
295
|
+
observation_count: int
|
|
296
|
+
positive_count: int
|
|
297
|
+
feasible_count: int
|
|
298
|
+
stage_persistence_observation_count: int
|
|
299
|
+
stage_persistence_positive_count: int
|
|
300
|
+
persistence_observation_count: int
|
|
301
|
+
persistence_positive_count: int
|
|
302
|
+
descendant_observation_count: int
|
|
303
|
+
descendant_positive_count: int
|
|
304
|
+
allocation_requested_slot_count: int
|
|
305
|
+
allocation_realized_overlap_count: int
|
|
306
|
+
allocation_projection_count: int
|
|
307
|
+
normalized_marginal_utility_sum: float
|
|
308
|
+
marginal_utility_share_sum: float
|
|
309
|
+
mean_source_distance: float
|
|
310
|
+
|
|
311
|
+
def __post_init__(self) -> None:
|
|
312
|
+
if type(self.kind) is not SearchArmKind:
|
|
313
|
+
raise TypeError("kind must be exact SearchArmKind")
|
|
314
|
+
_require_token(self.arm_id, name="arm_id")
|
|
315
|
+
for name in (
|
|
316
|
+
"observation_count",
|
|
317
|
+
"positive_count",
|
|
318
|
+
"feasible_count",
|
|
319
|
+
"stage_persistence_observation_count",
|
|
320
|
+
"stage_persistence_positive_count",
|
|
321
|
+
"persistence_observation_count",
|
|
322
|
+
"persistence_positive_count",
|
|
323
|
+
"descendant_observation_count",
|
|
324
|
+
"descendant_positive_count",
|
|
325
|
+
"allocation_requested_slot_count",
|
|
326
|
+
"allocation_realized_overlap_count",
|
|
327
|
+
"allocation_projection_count",
|
|
328
|
+
):
|
|
329
|
+
value = getattr(self, name)
|
|
330
|
+
if type(value) is not int or value < 0:
|
|
331
|
+
raise ValueError(f"{name} must be a non-negative exact integer")
|
|
332
|
+
if self.positive_count > self.observation_count:
|
|
333
|
+
raise ValueError("positive count exceeds observations")
|
|
334
|
+
if self.feasible_count > self.observation_count:
|
|
335
|
+
raise ValueError("feasible count exceeds observations")
|
|
336
|
+
if self.stage_persistence_positive_count > (
|
|
337
|
+
self.stage_persistence_observation_count
|
|
338
|
+
):
|
|
339
|
+
raise ValueError("stage persistence positives exceed observations")
|
|
340
|
+
if self.persistence_positive_count > self.persistence_observation_count:
|
|
341
|
+
raise ValueError("persistence positives exceed observations")
|
|
342
|
+
if self.descendant_positive_count > self.descendant_observation_count:
|
|
343
|
+
raise ValueError("descendant positives exceed observations")
|
|
344
|
+
if self.allocation_realized_overlap_count > (
|
|
345
|
+
self.allocation_requested_slot_count
|
|
346
|
+
):
|
|
347
|
+
raise ValueError("allocation overlap exceeds requested slots")
|
|
348
|
+
if (
|
|
349
|
+
self.allocation_requested_slot_count == 0
|
|
350
|
+
and self.allocation_projection_count != 0
|
|
351
|
+
):
|
|
352
|
+
raise ValueError("allocation projections require requested slots")
|
|
353
|
+
for name in (
|
|
354
|
+
"normalized_marginal_utility_sum",
|
|
355
|
+
"marginal_utility_share_sum",
|
|
356
|
+
):
|
|
357
|
+
value = getattr(self, name)
|
|
358
|
+
if (
|
|
359
|
+
type(value) is not float
|
|
360
|
+
or not math.isfinite(value)
|
|
361
|
+
or not 0.0 <= value <= float(self.observation_count)
|
|
362
|
+
):
|
|
363
|
+
raise ValueError(f"{name} is invalid")
|
|
364
|
+
_require_probability(self.mean_source_distance, name="mean_source_distance")
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def _beta_mean(successes: int, observations: int) -> float:
|
|
368
|
+
return (1.0 + successes) / (2.0 + observations)
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def _beta_variance(successes: int, observations: int) -> float:
|
|
372
|
+
alpha = 1.0 + successes
|
|
373
|
+
beta = 1.0 + observations - successes
|
|
374
|
+
total = alpha + beta
|
|
375
|
+
return (alpha * beta) / (total * total * (total + 1.0))
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def positive_probability(self) -> float:
|
|
379
|
+
return self._beta_mean(self.positive_count, self.observation_count)
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def positive_uncertainty(self) -> float:
|
|
383
|
+
return math.sqrt(
|
|
384
|
+
self._beta_variance(self.positive_count, self.observation_count)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def return_probability(self) -> float:
|
|
389
|
+
"""Empirical mean of bounded normalized archive return.
|
|
390
|
+
|
|
391
|
+
Infeasible evaluated actions already contribute zero utility, so
|
|
392
|
+
feasibility is a return gate rather than an independently rewarded
|
|
393
|
+
feature. An unseen arm receives the neutral cold-start value only
|
|
394
|
+
until its first real observation; no unit pseudo-count is allowed to
|
|
395
|
+
dwarf the small archive gains common in expensive optimization.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
if self.observation_count == 0:
|
|
399
|
+
return 0.5
|
|
400
|
+
return self.normalized_marginal_utility_sum / self.observation_count
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def return_uncertainty(self) -> float:
|
|
404
|
+
# A variable bounded in [0, 1] has standard deviation at most 1/2.
|
|
405
|
+
# This conservative standard-error bound is scale free and, because
|
|
406
|
+
# only one explicit exploration slot consumes it, cannot inflate the
|
|
407
|
+
# entire exploitation allocation.
|
|
408
|
+
return 0.5 / math.sqrt(max(1, self.observation_count))
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def feasibility_probability(self) -> float:
|
|
412
|
+
return self._beta_mean(self.feasible_count, self.observation_count)
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def persistence_probability(self) -> float:
|
|
416
|
+
return self._beta_mean(
|
|
417
|
+
self.persistence_positive_count,
|
|
418
|
+
self.persistence_observation_count,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def stage_persistence_probability(self) -> float:
|
|
423
|
+
return self._beta_mean(
|
|
424
|
+
self.stage_persistence_positive_count,
|
|
425
|
+
self.stage_persistence_observation_count,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def descendant_probability(self) -> float:
|
|
430
|
+
return self._beta_mean(
|
|
431
|
+
self.descendant_positive_count,
|
|
432
|
+
self.descendant_observation_count,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def allocation_realizability_probability(self) -> float:
|
|
437
|
+
return self._beta_mean(
|
|
438
|
+
self.allocation_realized_overlap_count,
|
|
439
|
+
self.allocation_requested_slot_count,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def mean_marginal_utility_share(self) -> float:
|
|
444
|
+
if self.observation_count == 0:
|
|
445
|
+
return 0.0
|
|
446
|
+
return self.marginal_utility_share_sum / self.observation_count
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def mean_normalized_marginal_utility(self) -> float:
|
|
450
|
+
if self.observation_count == 0:
|
|
451
|
+
return 0.0
|
|
452
|
+
return self.normalized_marginal_utility_sum / self.observation_count
|
|
453
|
+
|
|
454
|
+
def to_record(self) -> dict[str, object]:
|
|
455
|
+
self.__post_init__()
|
|
456
|
+
return {
|
|
457
|
+
"kind": self.kind.value,
|
|
458
|
+
"arm_id": self.arm_id,
|
|
459
|
+
"observation_count": self.observation_count,
|
|
460
|
+
"positive_count": self.positive_count,
|
|
461
|
+
"feasible_count": self.feasible_count,
|
|
462
|
+
"stage_persistence_observation_count": (
|
|
463
|
+
self.stage_persistence_observation_count
|
|
464
|
+
),
|
|
465
|
+
"stage_persistence_positive_count": (self.stage_persistence_positive_count),
|
|
466
|
+
"persistence_observation_count": self.persistence_observation_count,
|
|
467
|
+
"persistence_positive_count": self.persistence_positive_count,
|
|
468
|
+
"descendant_observation_count": self.descendant_observation_count,
|
|
469
|
+
"descendant_positive_count": self.descendant_positive_count,
|
|
470
|
+
"allocation_requested_slot_count": (self.allocation_requested_slot_count),
|
|
471
|
+
"allocation_realized_overlap_count": (
|
|
472
|
+
self.allocation_realized_overlap_count
|
|
473
|
+
),
|
|
474
|
+
"allocation_projection_count": self.allocation_projection_count,
|
|
475
|
+
"normalized_marginal_utility_sum_hex": (
|
|
476
|
+
self.normalized_marginal_utility_sum.hex()
|
|
477
|
+
),
|
|
478
|
+
"marginal_utility_share_sum_hex": (self.marginal_utility_share_sum.hex()),
|
|
479
|
+
"mean_source_distance_hex": self.mean_source_distance.hex(),
|
|
480
|
+
"posterior": {
|
|
481
|
+
"positive_probability_hex": self.positive_probability.hex(),
|
|
482
|
+
"positive_uncertainty_hex": self.positive_uncertainty.hex(),
|
|
483
|
+
"feasibility_probability_hex": self.feasibility_probability.hex(),
|
|
484
|
+
"stage_persistence_probability_hex": (
|
|
485
|
+
self.stage_persistence_probability.hex()
|
|
486
|
+
),
|
|
487
|
+
"persistence_probability_hex": self.persistence_probability.hex(),
|
|
488
|
+
"descendant_probability_hex": self.descendant_probability.hex(),
|
|
489
|
+
"allocation_realizability_probability_hex": (
|
|
490
|
+
self.allocation_realizability_probability.hex()
|
|
491
|
+
),
|
|
492
|
+
"mean_normalized_marginal_utility_hex": (
|
|
493
|
+
self.mean_normalized_marginal_utility.hex()
|
|
494
|
+
),
|
|
495
|
+
"return_probability_hex": self.return_probability.hex(),
|
|
496
|
+
"return_uncertainty_hex": self.return_uncertainty.hex(),
|
|
497
|
+
"mean_marginal_utility_share_hex": (
|
|
498
|
+
self.mean_marginal_utility_share.hex()
|
|
499
|
+
),
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _posterior(
|
|
505
|
+
observations: tuple[ContextualSearchObservation, ...],
|
|
506
|
+
*,
|
|
507
|
+
kind: SearchArmKind,
|
|
508
|
+
arm_id: str,
|
|
509
|
+
delayed_credits: tuple[ContextualSearchDelayedCredit, ...] = (),
|
|
510
|
+
allocation_realizations: tuple[ContextualPortfolioAllocationRealization, ...] = (),
|
|
511
|
+
) -> ContextualArmPosterior:
|
|
512
|
+
selected = tuple(
|
|
513
|
+
value
|
|
514
|
+
for value in observations
|
|
515
|
+
if (value.source_id if kind is SearchArmKind.SOURCE else value.operator_id)
|
|
516
|
+
== arm_id
|
|
517
|
+
)
|
|
518
|
+
delayed_by_channel = {
|
|
519
|
+
(value.source_observation_sha256, name): getattr(value, name)
|
|
520
|
+
for value in delayed_credits
|
|
521
|
+
for name in (
|
|
522
|
+
"stage_front_persisted",
|
|
523
|
+
"final_front_persisted",
|
|
524
|
+
"useful_descendant_observed",
|
|
525
|
+
)
|
|
526
|
+
if getattr(value, name) is not None
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
def delayed_value(
|
|
530
|
+
value: ContextualSearchObservation,
|
|
531
|
+
name: str,
|
|
532
|
+
) -> bool | None:
|
|
533
|
+
immediate = getattr(value, name)
|
|
534
|
+
if immediate is not None:
|
|
535
|
+
return immediate
|
|
536
|
+
return delayed_by_channel.get((value.observation_sha256, name))
|
|
537
|
+
|
|
538
|
+
stage_persistence = tuple(
|
|
539
|
+
delayed_value(value, "stage_front_persisted") for value in selected
|
|
540
|
+
)
|
|
541
|
+
stage_persistence = tuple(value for value in stage_persistence if value is not None)
|
|
542
|
+
persistence = tuple(
|
|
543
|
+
delayed_value(value, "final_front_persisted") for value in selected
|
|
544
|
+
)
|
|
545
|
+
persistence = tuple(value for value in persistence if value is not None)
|
|
546
|
+
descendants = tuple(
|
|
547
|
+
delayed_value(value, "useful_descendant_observed") for value in selected
|
|
548
|
+
)
|
|
549
|
+
descendants = tuple(value for value in descendants if value is not None)
|
|
550
|
+
allocation_rows = tuple(
|
|
551
|
+
value
|
|
552
|
+
for value in allocation_realizations
|
|
553
|
+
if dict(
|
|
554
|
+
value.requested_source_target_counts
|
|
555
|
+
if kind is SearchArmKind.SOURCE
|
|
556
|
+
else value.requested_operator_target_counts
|
|
557
|
+
).get(arm_id, 0)
|
|
558
|
+
> 0
|
|
559
|
+
)
|
|
560
|
+
requested_slots = sum(
|
|
561
|
+
dict(
|
|
562
|
+
value.requested_source_target_counts
|
|
563
|
+
if kind is SearchArmKind.SOURCE
|
|
564
|
+
else value.requested_operator_target_counts
|
|
565
|
+
)[arm_id]
|
|
566
|
+
for value in allocation_rows
|
|
567
|
+
)
|
|
568
|
+
realized_overlap = sum(
|
|
569
|
+
min(
|
|
570
|
+
dict(
|
|
571
|
+
value.requested_source_target_counts
|
|
572
|
+
if kind is SearchArmKind.SOURCE
|
|
573
|
+
else value.requested_operator_target_counts
|
|
574
|
+
)[arm_id],
|
|
575
|
+
dict(
|
|
576
|
+
value.realized_source_target_counts
|
|
577
|
+
if kind is SearchArmKind.SOURCE
|
|
578
|
+
else value.realized_operator_target_counts
|
|
579
|
+
)[arm_id],
|
|
580
|
+
)
|
|
581
|
+
for value in allocation_rows
|
|
582
|
+
)
|
|
583
|
+
return ContextualArmPosterior(
|
|
584
|
+
kind=kind,
|
|
585
|
+
arm_id=arm_id,
|
|
586
|
+
observation_count=len(selected),
|
|
587
|
+
positive_count=sum(value.positive_marginal_utility for value in selected),
|
|
588
|
+
feasible_count=sum(value.feasible for value in selected),
|
|
589
|
+
stage_persistence_observation_count=len(stage_persistence),
|
|
590
|
+
stage_persistence_positive_count=sum(
|
|
591
|
+
value is True for value in stage_persistence
|
|
592
|
+
),
|
|
593
|
+
persistence_observation_count=len(persistence),
|
|
594
|
+
persistence_positive_count=sum(value is True for value in persistence),
|
|
595
|
+
descendant_observation_count=len(descendants),
|
|
596
|
+
descendant_positive_count=sum(value is True for value in descendants),
|
|
597
|
+
allocation_requested_slot_count=requested_slots,
|
|
598
|
+
allocation_realized_overlap_count=realized_overlap,
|
|
599
|
+
allocation_projection_count=len(allocation_rows),
|
|
600
|
+
normalized_marginal_utility_sum=float(
|
|
601
|
+
sum(value.normalized_marginal_utility for value in selected)
|
|
602
|
+
),
|
|
603
|
+
marginal_utility_share_sum=float(
|
|
604
|
+
sum(value.marginal_utility_share for value in selected)
|
|
605
|
+
),
|
|
606
|
+
mean_source_distance=(
|
|
607
|
+
0.0
|
|
608
|
+
if not selected
|
|
609
|
+
else float(sum(value.source_distance for value in selected) / len(selected))
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@dataclass(frozen=True, slots=True)
|
|
615
|
+
class ContextualSearchSnapshot:
|
|
616
|
+
campaign_scope_sha256: str
|
|
617
|
+
cutoff_wave_index_exclusive: int
|
|
618
|
+
observation_sha256s: tuple[str, ...]
|
|
619
|
+
delayed_credit_sha256s: tuple[str, ...]
|
|
620
|
+
allocation_realization_sha256s: tuple[str, ...]
|
|
621
|
+
source_posteriors: tuple[ContextualArmPosterior, ...]
|
|
622
|
+
operator_posteriors: tuple[ContextualArmPosterior, ...]
|
|
623
|
+
snapshot_sha256: str = field(init=False)
|
|
624
|
+
|
|
625
|
+
def __post_init__(self) -> None:
|
|
626
|
+
require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
|
|
627
|
+
if (
|
|
628
|
+
type(self.cutoff_wave_index_exclusive) is not int
|
|
629
|
+
or self.cutoff_wave_index_exclusive <= 0
|
|
630
|
+
):
|
|
631
|
+
raise ValueError("cutoff_wave_index_exclusive must be positive")
|
|
632
|
+
if self.observation_sha256s != tuple(sorted(set(self.observation_sha256s))):
|
|
633
|
+
raise ValueError("observation hashes must be unique and canonical")
|
|
634
|
+
for value in self.observation_sha256s:
|
|
635
|
+
require_sha256(value, "observation_sha256")
|
|
636
|
+
if self.delayed_credit_sha256s != tuple(
|
|
637
|
+
sorted(set(self.delayed_credit_sha256s))
|
|
638
|
+
):
|
|
639
|
+
raise ValueError("delayed credit hashes must be unique and canonical")
|
|
640
|
+
for value in self.delayed_credit_sha256s:
|
|
641
|
+
require_sha256(value, "delayed_credit_sha256")
|
|
642
|
+
if self.allocation_realization_sha256s != tuple(
|
|
643
|
+
sorted(set(self.allocation_realization_sha256s))
|
|
644
|
+
):
|
|
645
|
+
raise ValueError(
|
|
646
|
+
"allocation realization hashes must be unique and canonical"
|
|
647
|
+
)
|
|
648
|
+
for value in self.allocation_realization_sha256s:
|
|
649
|
+
require_sha256(value, "allocation_realization_sha256")
|
|
650
|
+
for name, kind in (
|
|
651
|
+
("source_posteriors", SearchArmKind.SOURCE),
|
|
652
|
+
("operator_posteriors", SearchArmKind.OPERATOR),
|
|
653
|
+
):
|
|
654
|
+
values = getattr(self, name)
|
|
655
|
+
if type(values) is not tuple or any(
|
|
656
|
+
type(value) is not ContextualArmPosterior for value in values
|
|
657
|
+
):
|
|
658
|
+
raise TypeError(f"{name} must contain exact posteriors")
|
|
659
|
+
for value in values:
|
|
660
|
+
value.__post_init__()
|
|
661
|
+
if value.kind is not kind:
|
|
662
|
+
raise ValueError(f"{name} contains a foreign arm kind")
|
|
663
|
+
if tuple(value.arm_id for value in values) != tuple(
|
|
664
|
+
sorted({value.arm_id for value in values})
|
|
665
|
+
):
|
|
666
|
+
raise ValueError(f"{name} must use canonical unique arm IDs")
|
|
667
|
+
object.__setattr__(
|
|
668
|
+
self,
|
|
669
|
+
"snapshot_sha256",
|
|
670
|
+
_hash(_SNAPSHOT_DOMAIN, self._unsigned_record()),
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
674
|
+
return {
|
|
675
|
+
"schema_version": 2,
|
|
676
|
+
"campaign_scope_sha256": self.campaign_scope_sha256,
|
|
677
|
+
"cutoff_wave_index_exclusive": self.cutoff_wave_index_exclusive,
|
|
678
|
+
"observation_sha256s": list(self.observation_sha256s),
|
|
679
|
+
"delayed_credit_sha256s": list(self.delayed_credit_sha256s),
|
|
680
|
+
"allocation_realization_sha256s": list(self.allocation_realization_sha256s),
|
|
681
|
+
"source_posteriors": [
|
|
682
|
+
value.to_record() for value in self.source_posteriors
|
|
683
|
+
],
|
|
684
|
+
"operator_posteriors": [
|
|
685
|
+
value.to_record() for value in self.operator_posteriors
|
|
686
|
+
],
|
|
687
|
+
"policy": {
|
|
688
|
+
"policy_id": CONTEXTUAL_SEARCH_CONTROLLER_ID,
|
|
689
|
+
"policy_version": CONTEXTUAL_SEARCH_CONTROLLER_VERSION,
|
|
690
|
+
"definition_sha256": CONTEXTUAL_SEARCH_CONTROLLER_DEFINITION_SHA256,
|
|
691
|
+
},
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
def to_record(self) -> dict[str, object]:
|
|
695
|
+
self.__post_init__()
|
|
696
|
+
return {**self._unsigned_record(), "snapshot_sha256": self.snapshot_sha256}
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@dataclass(slots=True)
|
|
700
|
+
class ContextualSearchLedger:
|
|
701
|
+
"""Append-only observation ledger with strict prior-wave snapshots."""
|
|
702
|
+
|
|
703
|
+
observations: list[ContextualSearchObservation] = field(default_factory=list)
|
|
704
|
+
delayed_credits: list[ContextualSearchDelayedCredit] = field(default_factory=list)
|
|
705
|
+
allocation_realizations: list[ContextualPortfolioAllocationRealization] = field(
|
|
706
|
+
default_factory=list
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
def append_batch(
|
|
710
|
+
self,
|
|
711
|
+
observations: tuple[ContextualSearchObservation, ...],
|
|
712
|
+
) -> None:
|
|
713
|
+
if (
|
|
714
|
+
type(observations) is not tuple
|
|
715
|
+
or not observations
|
|
716
|
+
or any(
|
|
717
|
+
type(value) is not ContextualSearchObservation for value in observations
|
|
718
|
+
)
|
|
719
|
+
):
|
|
720
|
+
raise ValueError("observations must contain exact controller observations")
|
|
721
|
+
for value in observations:
|
|
722
|
+
value.__post_init__()
|
|
723
|
+
combined = (*self.observations, *observations)
|
|
724
|
+
identities = tuple(value.observation_sha256 for value in combined)
|
|
725
|
+
if len(set(identities)) != len(identities):
|
|
726
|
+
raise ValueError("controller ledger cannot repeat an observation")
|
|
727
|
+
if tuple(
|
|
728
|
+
(value.wave_index, value.observation_sha256) for value in combined
|
|
729
|
+
) != tuple(
|
|
730
|
+
sorted((value.wave_index, value.observation_sha256) for value in combined)
|
|
731
|
+
):
|
|
732
|
+
raise ValueError("controller observations must be appended canonically")
|
|
733
|
+
scopes = {value.campaign_scope_sha256 for value in combined}
|
|
734
|
+
if len(scopes) != 1:
|
|
735
|
+
raise ValueError("controller ledger cannot mix campaign scopes")
|
|
736
|
+
self.observations.extend(observations)
|
|
737
|
+
|
|
738
|
+
def append_delayed_credit_batch(
|
|
739
|
+
self,
|
|
740
|
+
credits: tuple[ContextualSearchDelayedCredit, ...],
|
|
741
|
+
) -> None:
|
|
742
|
+
if (
|
|
743
|
+
type(credits) is not tuple
|
|
744
|
+
or not credits
|
|
745
|
+
or any(
|
|
746
|
+
type(value) is not ContextualSearchDelayedCredit for value in credits
|
|
747
|
+
)
|
|
748
|
+
):
|
|
749
|
+
raise ValueError("credits must contain exact delayed-credit events")
|
|
750
|
+
observations = {value.observation_sha256: value for value in self.observations}
|
|
751
|
+
combined = (*self.delayed_credits, *credits)
|
|
752
|
+
if len({value.credit_sha256 for value in combined}) != len(combined):
|
|
753
|
+
raise ValueError("controller ledger cannot repeat delayed credit")
|
|
754
|
+
if tuple(
|
|
755
|
+
(value.available_at_wave_index, value.credit_sha256) for value in combined
|
|
756
|
+
) != tuple(
|
|
757
|
+
sorted(
|
|
758
|
+
(value.available_at_wave_index, value.credit_sha256)
|
|
759
|
+
for value in combined
|
|
760
|
+
)
|
|
761
|
+
):
|
|
762
|
+
raise ValueError("delayed credits must be appended canonically")
|
|
763
|
+
occupied: set[tuple[str, str]] = set()
|
|
764
|
+
for value in combined:
|
|
765
|
+
value.__post_init__()
|
|
766
|
+
source = observations.get(value.source_observation_sha256)
|
|
767
|
+
if source is None:
|
|
768
|
+
raise ValueError("delayed credit cites an unknown observation")
|
|
769
|
+
if source.campaign_scope_sha256 != value.campaign_scope_sha256:
|
|
770
|
+
raise ValueError("delayed credit crosses campaign scopes")
|
|
771
|
+
if value.available_at_wave_index <= source.wave_index:
|
|
772
|
+
raise ValueError("delayed credit predates its source observation")
|
|
773
|
+
for name in (
|
|
774
|
+
"stage_front_persisted",
|
|
775
|
+
"final_front_persisted",
|
|
776
|
+
"useful_descendant_observed",
|
|
777
|
+
):
|
|
778
|
+
delayed = getattr(value, name)
|
|
779
|
+
if delayed is None:
|
|
780
|
+
continue
|
|
781
|
+
if getattr(source, name) is not None:
|
|
782
|
+
raise ValueError("delayed credit overwrites immediate evidence")
|
|
783
|
+
key = (source.observation_sha256, name)
|
|
784
|
+
if key in occupied:
|
|
785
|
+
raise ValueError("delayed credit repeats one evidence channel")
|
|
786
|
+
occupied.add(key)
|
|
787
|
+
self.delayed_credits.extend(credits)
|
|
788
|
+
|
|
789
|
+
def append_allocation_realization_batch(
|
|
790
|
+
self,
|
|
791
|
+
realizations: tuple[ContextualPortfolioAllocationRealization, ...],
|
|
792
|
+
) -> None:
|
|
793
|
+
if (
|
|
794
|
+
type(realizations) is not tuple
|
|
795
|
+
or not realizations
|
|
796
|
+
or any(
|
|
797
|
+
type(value) is not ContextualPortfolioAllocationRealization
|
|
798
|
+
for value in realizations
|
|
799
|
+
)
|
|
800
|
+
):
|
|
801
|
+
raise ValueError("realizations must contain exact allocation evidence")
|
|
802
|
+
for value in realizations:
|
|
803
|
+
value.__post_init__()
|
|
804
|
+
combined = (*self.allocation_realizations, *realizations)
|
|
805
|
+
identities = tuple(value.realization_sha256 for value in combined)
|
|
806
|
+
if len(set(identities)) != len(identities):
|
|
807
|
+
raise ValueError(
|
|
808
|
+
"controller ledger cannot repeat an allocation realization"
|
|
809
|
+
)
|
|
810
|
+
if tuple(
|
|
811
|
+
(value.controller_wave_index, value.slice_id, value.realization_sha256)
|
|
812
|
+
for value in combined
|
|
813
|
+
) != tuple(
|
|
814
|
+
sorted(
|
|
815
|
+
(
|
|
816
|
+
value.controller_wave_index,
|
|
817
|
+
value.slice_id,
|
|
818
|
+
value.realization_sha256,
|
|
819
|
+
)
|
|
820
|
+
for value in combined
|
|
821
|
+
)
|
|
822
|
+
):
|
|
823
|
+
raise ValueError("allocation realizations must be appended canonically")
|
|
824
|
+
scopes = {value.campaign_scope_sha256 for value in combined}
|
|
825
|
+
if len(scopes) != 1:
|
|
826
|
+
raise ValueError("controller ledger cannot mix realization scopes")
|
|
827
|
+
self.allocation_realizations.extend(realizations)
|
|
828
|
+
|
|
829
|
+
def snapshot(
|
|
830
|
+
self,
|
|
831
|
+
*,
|
|
832
|
+
campaign_scope_sha256: str,
|
|
833
|
+
cutoff_wave_index_exclusive: int,
|
|
834
|
+
available_source_ids: tuple[str, ...],
|
|
835
|
+
available_operator_ids: tuple[str, ...],
|
|
836
|
+
) -> ContextualSearchSnapshot:
|
|
837
|
+
require_sha256(campaign_scope_sha256, "campaign_scope_sha256")
|
|
838
|
+
if (
|
|
839
|
+
type(cutoff_wave_index_exclusive) is not int
|
|
840
|
+
or cutoff_wave_index_exclusive <= 0
|
|
841
|
+
):
|
|
842
|
+
raise ValueError("cutoff_wave_index_exclusive must be positive")
|
|
843
|
+
_canonical_tokens(available_source_ids, name="available_source_ids")
|
|
844
|
+
_canonical_tokens(available_operator_ids, name="available_operator_ids")
|
|
845
|
+
selected = tuple(
|
|
846
|
+
value
|
|
847
|
+
for value in self.observations
|
|
848
|
+
if value.campaign_scope_sha256 == campaign_scope_sha256
|
|
849
|
+
and value.wave_index < cutoff_wave_index_exclusive
|
|
850
|
+
)
|
|
851
|
+
selected_hashes = {value.observation_sha256 for value in selected}
|
|
852
|
+
delayed = tuple(
|
|
853
|
+
value
|
|
854
|
+
for value in self.delayed_credits
|
|
855
|
+
if value.campaign_scope_sha256 == campaign_scope_sha256
|
|
856
|
+
and value.available_at_wave_index <= cutoff_wave_index_exclusive
|
|
857
|
+
and value.source_observation_sha256 in selected_hashes
|
|
858
|
+
)
|
|
859
|
+
realizations = tuple(
|
|
860
|
+
value
|
|
861
|
+
for value in self.allocation_realizations
|
|
862
|
+
if value.campaign_scope_sha256 == campaign_scope_sha256
|
|
863
|
+
and value.controller_wave_index < cutoff_wave_index_exclusive
|
|
864
|
+
)
|
|
865
|
+
return ContextualSearchSnapshot(
|
|
866
|
+
campaign_scope_sha256=campaign_scope_sha256,
|
|
867
|
+
cutoff_wave_index_exclusive=cutoff_wave_index_exclusive,
|
|
868
|
+
observation_sha256s=tuple(
|
|
869
|
+
sorted(value.observation_sha256 for value in selected)
|
|
870
|
+
),
|
|
871
|
+
delayed_credit_sha256s=tuple(
|
|
872
|
+
sorted(value.credit_sha256 for value in delayed)
|
|
873
|
+
),
|
|
874
|
+
allocation_realization_sha256s=tuple(
|
|
875
|
+
sorted(value.realization_sha256 for value in realizations)
|
|
876
|
+
),
|
|
877
|
+
source_posteriors=tuple(
|
|
878
|
+
_posterior(
|
|
879
|
+
selected,
|
|
880
|
+
kind=SearchArmKind.SOURCE,
|
|
881
|
+
arm_id=value,
|
|
882
|
+
delayed_credits=delayed,
|
|
883
|
+
allocation_realizations=realizations,
|
|
884
|
+
)
|
|
885
|
+
for value in available_source_ids
|
|
886
|
+
),
|
|
887
|
+
operator_posteriors=tuple(
|
|
888
|
+
_posterior(
|
|
889
|
+
selected,
|
|
890
|
+
kind=SearchArmKind.OPERATOR,
|
|
891
|
+
arm_id=value,
|
|
892
|
+
delayed_credits=delayed,
|
|
893
|
+
allocation_realizations=realizations,
|
|
894
|
+
)
|
|
895
|
+
for value in available_operator_ids
|
|
896
|
+
),
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
@dataclass(frozen=True, slots=True)
|
|
901
|
+
class ContextualSearchCompletionAudit:
|
|
902
|
+
"""Generic post-campaign integrity audit for the adaptive loop."""
|
|
903
|
+
|
|
904
|
+
campaign_scope_sha256: str
|
|
905
|
+
expected_wave_count: int
|
|
906
|
+
expected_post_recombination_wave_indices: tuple[int, ...]
|
|
907
|
+
expected_observation_count: int
|
|
908
|
+
expected_stage_credit_count: int
|
|
909
|
+
expected_allocation_realization_count: int
|
|
910
|
+
observation_count: int
|
|
911
|
+
allocation_realization_count: int
|
|
912
|
+
delayed_credit_count: int
|
|
913
|
+
candidate_bound_observation_count: int
|
|
914
|
+
unique_candidate_count: int
|
|
915
|
+
observation_wave_indices: tuple[int, ...]
|
|
916
|
+
allocation_wave_indices: tuple[int, ...]
|
|
917
|
+
stage_credit_wave_indices: tuple[int, ...]
|
|
918
|
+
stage_credit_source_sha256s: tuple[str, ...]
|
|
919
|
+
final_credit_source_sha256s: tuple[str, ...]
|
|
920
|
+
descendant_credit_source_sha256s: tuple[str, ...]
|
|
921
|
+
descendant_credit_wave_indices: tuple[int, ...]
|
|
922
|
+
audit_sha256: str = field(init=False)
|
|
923
|
+
|
|
924
|
+
def __post_init__(self) -> None:
|
|
925
|
+
require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
|
|
926
|
+
for name in (
|
|
927
|
+
"expected_wave_count",
|
|
928
|
+
"expected_observation_count",
|
|
929
|
+
"expected_allocation_realization_count",
|
|
930
|
+
):
|
|
931
|
+
if type(getattr(self, name)) is not int or getattr(self, name) <= 0:
|
|
932
|
+
raise ValueError(f"{name} must be positive")
|
|
933
|
+
for name in (
|
|
934
|
+
"observation_count",
|
|
935
|
+
"expected_stage_credit_count",
|
|
936
|
+
"allocation_realization_count",
|
|
937
|
+
"delayed_credit_count",
|
|
938
|
+
"candidate_bound_observation_count",
|
|
939
|
+
"unique_candidate_count",
|
|
940
|
+
):
|
|
941
|
+
if type(getattr(self, name)) is not int or getattr(self, name) < 0:
|
|
942
|
+
raise ValueError(f"{name} must be non-negative")
|
|
943
|
+
for name in (
|
|
944
|
+
"observation_wave_indices",
|
|
945
|
+
"allocation_wave_indices",
|
|
946
|
+
"expected_post_recombination_wave_indices",
|
|
947
|
+
"stage_credit_wave_indices",
|
|
948
|
+
"descendant_credit_wave_indices",
|
|
949
|
+
):
|
|
950
|
+
values = getattr(self, name)
|
|
951
|
+
if type(values) is not tuple or any(
|
|
952
|
+
type(value) is not int or value <= 0 for value in values
|
|
953
|
+
):
|
|
954
|
+
raise ValueError(f"{name} must contain positive exact integers")
|
|
955
|
+
if values != tuple(sorted(set(values))):
|
|
956
|
+
raise ValueError(f"{name} must be unique and canonical")
|
|
957
|
+
if not set(self.expected_post_recombination_wave_indices).issubset(
|
|
958
|
+
self.expected_wave_indices
|
|
959
|
+
):
|
|
960
|
+
raise ValueError(
|
|
961
|
+
"post-recombination wave indices escape the expected horizon"
|
|
962
|
+
)
|
|
963
|
+
for name in (
|
|
964
|
+
"stage_credit_source_sha256s",
|
|
965
|
+
"final_credit_source_sha256s",
|
|
966
|
+
"descendant_credit_source_sha256s",
|
|
967
|
+
):
|
|
968
|
+
values = getattr(self, name)
|
|
969
|
+
if type(values) is not tuple or values != tuple(sorted(set(values))):
|
|
970
|
+
raise ValueError(f"{name} must be a canonical exact tuple")
|
|
971
|
+
for value in values:
|
|
972
|
+
require_sha256(value, name)
|
|
973
|
+
object.__setattr__(
|
|
974
|
+
self,
|
|
975
|
+
"audit_sha256",
|
|
976
|
+
_hash(_COMPLETION_AUDIT_DOMAIN, self._unsigned_record()),
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
@property
|
|
980
|
+
def expected_wave_indices(self) -> tuple[int, ...]:
|
|
981
|
+
return tuple(range(1, self.expected_wave_count + 1))
|
|
982
|
+
|
|
983
|
+
@property
|
|
984
|
+
def healthy(self) -> bool:
|
|
985
|
+
return (
|
|
986
|
+
self.observation_count == self.expected_observation_count
|
|
987
|
+
and self.allocation_realization_count
|
|
988
|
+
== self.expected_allocation_realization_count
|
|
989
|
+
and self.delayed_credit_count
|
|
990
|
+
== self.expected_stage_credit_count + self.expected_observation_count
|
|
991
|
+
and self.candidate_bound_observation_count
|
|
992
|
+
== self.expected_observation_count
|
|
993
|
+
and self.unique_candidate_count == self.expected_observation_count
|
|
994
|
+
and self.observation_wave_indices == self.expected_wave_indices
|
|
995
|
+
and self.allocation_wave_indices == self.expected_wave_indices
|
|
996
|
+
and self.stage_credit_wave_indices
|
|
997
|
+
== self.expected_post_recombination_wave_indices
|
|
998
|
+
and len(self.stage_credit_source_sha256s)
|
|
999
|
+
== self.expected_stage_credit_count
|
|
1000
|
+
and len(self.final_credit_source_sha256s) == self.expected_observation_count
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1004
|
+
return {
|
|
1005
|
+
"schema_version": 1,
|
|
1006
|
+
"campaign_scope_sha256": self.campaign_scope_sha256,
|
|
1007
|
+
"expected_wave_count": self.expected_wave_count,
|
|
1008
|
+
"expected_post_recombination_wave_indices": list(
|
|
1009
|
+
self.expected_post_recombination_wave_indices
|
|
1010
|
+
),
|
|
1011
|
+
"expected_observation_count": self.expected_observation_count,
|
|
1012
|
+
"expected_stage_credit_count": self.expected_stage_credit_count,
|
|
1013
|
+
"expected_allocation_realization_count": (
|
|
1014
|
+
self.expected_allocation_realization_count
|
|
1015
|
+
),
|
|
1016
|
+
"observation_count": self.observation_count,
|
|
1017
|
+
"allocation_realization_count": self.allocation_realization_count,
|
|
1018
|
+
"delayed_credit_count": self.delayed_credit_count,
|
|
1019
|
+
"candidate_bound_observation_count": (
|
|
1020
|
+
self.candidate_bound_observation_count
|
|
1021
|
+
),
|
|
1022
|
+
"unique_candidate_count": self.unique_candidate_count,
|
|
1023
|
+
"observation_wave_indices": list(self.observation_wave_indices),
|
|
1024
|
+
"allocation_wave_indices": list(self.allocation_wave_indices),
|
|
1025
|
+
"stage_credit_wave_indices": list(self.stage_credit_wave_indices),
|
|
1026
|
+
"stage_credit_source_sha256s": list(self.stage_credit_source_sha256s),
|
|
1027
|
+
"final_credit_source_sha256s": list(self.final_credit_source_sha256s),
|
|
1028
|
+
"descendant_credit_source_sha256s": list(
|
|
1029
|
+
self.descendant_credit_source_sha256s
|
|
1030
|
+
),
|
|
1031
|
+
"descendant_credit_wave_indices": list(self.descendant_credit_wave_indices),
|
|
1032
|
+
"healthy": self.healthy,
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
def to_record(self) -> dict[str, object]:
|
|
1036
|
+
self.__post_init__()
|
|
1037
|
+
return {**self._unsigned_record(), "audit_sha256": self.audit_sha256}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def audit_completed_contextual_search_ledger(
|
|
1041
|
+
ledger: ContextualSearchLedger,
|
|
1042
|
+
*,
|
|
1043
|
+
campaign_scope_sha256: str,
|
|
1044
|
+
expected_wave_count: int,
|
|
1045
|
+
expected_post_recombination_wave_indices: tuple[int, ...],
|
|
1046
|
+
expected_observation_count: int,
|
|
1047
|
+
expected_allocation_realization_count: int,
|
|
1048
|
+
) -> ContextualSearchCompletionAudit:
|
|
1049
|
+
"""Verify candidate binding and both delayed horizons after finalization."""
|
|
1050
|
+
|
|
1051
|
+
if type(ledger) is not ContextualSearchLedger:
|
|
1052
|
+
raise TypeError("ledger must be exact ContextualSearchLedger")
|
|
1053
|
+
require_sha256(campaign_scope_sha256, "campaign_scope_sha256")
|
|
1054
|
+
observations = tuple(
|
|
1055
|
+
value
|
|
1056
|
+
for value in ledger.observations
|
|
1057
|
+
if value.campaign_scope_sha256 == campaign_scope_sha256
|
|
1058
|
+
)
|
|
1059
|
+
realizations = tuple(
|
|
1060
|
+
value
|
|
1061
|
+
for value in ledger.allocation_realizations
|
|
1062
|
+
if value.campaign_scope_sha256 == campaign_scope_sha256
|
|
1063
|
+
)
|
|
1064
|
+
credits = tuple(
|
|
1065
|
+
value
|
|
1066
|
+
for value in ledger.delayed_credits
|
|
1067
|
+
if value.campaign_scope_sha256 == campaign_scope_sha256
|
|
1068
|
+
)
|
|
1069
|
+
observation_by_sha256 = {value.observation_sha256: value for value in observations}
|
|
1070
|
+
|
|
1071
|
+
def sources(name: str) -> tuple[str, ...]:
|
|
1072
|
+
return tuple(
|
|
1073
|
+
sorted(
|
|
1074
|
+
value.source_observation_sha256
|
|
1075
|
+
for value in credits
|
|
1076
|
+
if getattr(value, name) is not None
|
|
1077
|
+
)
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
stage_sources = sources("stage_front_persisted")
|
|
1081
|
+
descendant_sources = sources("useful_descendant_observed")
|
|
1082
|
+
candidate_ids = tuple(
|
|
1083
|
+
value.candidate_id for value in observations if value.candidate_id is not None
|
|
1084
|
+
)
|
|
1085
|
+
return ContextualSearchCompletionAudit(
|
|
1086
|
+
campaign_scope_sha256=campaign_scope_sha256,
|
|
1087
|
+
expected_wave_count=expected_wave_count,
|
|
1088
|
+
expected_post_recombination_wave_indices=(
|
|
1089
|
+
expected_post_recombination_wave_indices
|
|
1090
|
+
),
|
|
1091
|
+
expected_observation_count=expected_observation_count,
|
|
1092
|
+
expected_stage_credit_count=sum(
|
|
1093
|
+
value.wave_index in expected_post_recombination_wave_indices
|
|
1094
|
+
for value in observations
|
|
1095
|
+
),
|
|
1096
|
+
expected_allocation_realization_count=(expected_allocation_realization_count),
|
|
1097
|
+
observation_count=len(observations),
|
|
1098
|
+
allocation_realization_count=len(realizations),
|
|
1099
|
+
delayed_credit_count=len(credits),
|
|
1100
|
+
candidate_bound_observation_count=len(candidate_ids),
|
|
1101
|
+
unique_candidate_count=len(set(candidate_ids)),
|
|
1102
|
+
observation_wave_indices=tuple(
|
|
1103
|
+
sorted({value.wave_index for value in observations})
|
|
1104
|
+
),
|
|
1105
|
+
allocation_wave_indices=tuple(
|
|
1106
|
+
sorted({value.controller_wave_index for value in realizations})
|
|
1107
|
+
),
|
|
1108
|
+
stage_credit_source_sha256s=stage_sources,
|
|
1109
|
+
final_credit_source_sha256s=sources("final_front_persisted"),
|
|
1110
|
+
descendant_credit_source_sha256s=descendant_sources,
|
|
1111
|
+
stage_credit_wave_indices=tuple(
|
|
1112
|
+
sorted(
|
|
1113
|
+
{
|
|
1114
|
+
observation_by_sha256[value].wave_index
|
|
1115
|
+
for value in stage_sources
|
|
1116
|
+
if value in observation_by_sha256
|
|
1117
|
+
}
|
|
1118
|
+
)
|
|
1119
|
+
),
|
|
1120
|
+
descendant_credit_wave_indices=tuple(
|
|
1121
|
+
sorted(
|
|
1122
|
+
{
|
|
1123
|
+
observation_by_sha256[value].wave_index
|
|
1124
|
+
for value in descendant_sources
|
|
1125
|
+
if value in observation_by_sha256
|
|
1126
|
+
}
|
|
1127
|
+
)
|
|
1128
|
+
),
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
@dataclass(frozen=True, slots=True)
|
|
1133
|
+
class ContextualSearchQuery:
|
|
1134
|
+
campaign_scope_sha256: str
|
|
1135
|
+
wave_index: int
|
|
1136
|
+
total_portfolio_waves: int
|
|
1137
|
+
real_evaluation_slots: int
|
|
1138
|
+
available_source_ids: tuple[str, ...]
|
|
1139
|
+
available_operator_ids: tuple[str, ...]
|
|
1140
|
+
incumbent_source_id: str
|
|
1141
|
+
incumbent_operator_id: str
|
|
1142
|
+
archive_front_size: int
|
|
1143
|
+
recent_normalized_archive_gains: tuple[float, ...] = ()
|
|
1144
|
+
composition_evidence_available: bool = False
|
|
1145
|
+
source_count_capability: ContextualArmCountCapability | None = None
|
|
1146
|
+
operator_count_capability: ContextualArmCountCapability | None = None
|
|
1147
|
+
joint_count_capabilities: tuple[ContextualLaneJointCountCapability, ...] = ()
|
|
1148
|
+
query_sha256: str = field(init=False)
|
|
1149
|
+
|
|
1150
|
+
def __post_init__(self) -> None:
|
|
1151
|
+
require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
|
|
1152
|
+
if type(self.wave_index) is not int or self.wave_index <= 0:
|
|
1153
|
+
raise ValueError("wave_index must be positive")
|
|
1154
|
+
if (
|
|
1155
|
+
type(self.total_portfolio_waves) is not int
|
|
1156
|
+
or self.total_portfolio_waves <= 0
|
|
1157
|
+
or self.wave_index > self.total_portfolio_waves
|
|
1158
|
+
):
|
|
1159
|
+
raise ValueError("total_portfolio_waves must cover the current wave")
|
|
1160
|
+
if (
|
|
1161
|
+
type(self.real_evaluation_slots) is not int
|
|
1162
|
+
or self.real_evaluation_slots <= 0
|
|
1163
|
+
):
|
|
1164
|
+
raise ValueError("real_evaluation_slots must be positive")
|
|
1165
|
+
_canonical_tokens(self.available_source_ids, name="available_source_ids")
|
|
1166
|
+
_canonical_tokens(self.available_operator_ids, name="available_operator_ids")
|
|
1167
|
+
_require_token(self.incumbent_source_id, name="incumbent_source_id")
|
|
1168
|
+
if self.incumbent_source_id not in self.available_source_ids:
|
|
1169
|
+
raise ValueError("incumbent source must be available")
|
|
1170
|
+
_require_token(self.incumbent_operator_id, name="incumbent_operator_id")
|
|
1171
|
+
if self.incumbent_operator_id not in self.available_operator_ids:
|
|
1172
|
+
raise ValueError("incumbent operator must be available")
|
|
1173
|
+
if type(self.archive_front_size) is not int or self.archive_front_size <= 0:
|
|
1174
|
+
raise ValueError("archive_front_size must be positive")
|
|
1175
|
+
if type(self.recent_normalized_archive_gains) is not tuple:
|
|
1176
|
+
raise TypeError("recent_normalized_archive_gains must be an exact tuple")
|
|
1177
|
+
for value in self.recent_normalized_archive_gains:
|
|
1178
|
+
_require_probability(value, name="recent_normalized_archive_gain")
|
|
1179
|
+
if len(self.recent_normalized_archive_gains) > 4:
|
|
1180
|
+
raise ValueError("recent gain history is bounded to four stages")
|
|
1181
|
+
if type(self.composition_evidence_available) is not bool:
|
|
1182
|
+
raise TypeError("composition_evidence_available must be an exact bool")
|
|
1183
|
+
for name, kind, arm_ids in (
|
|
1184
|
+
(
|
|
1185
|
+
"source_count_capability",
|
|
1186
|
+
"source",
|
|
1187
|
+
self.available_source_ids,
|
|
1188
|
+
),
|
|
1189
|
+
(
|
|
1190
|
+
"operator_count_capability",
|
|
1191
|
+
"operator",
|
|
1192
|
+
self.available_operator_ids,
|
|
1193
|
+
),
|
|
1194
|
+
):
|
|
1195
|
+
capability = getattr(self, name)
|
|
1196
|
+
if capability is None:
|
|
1197
|
+
continue
|
|
1198
|
+
if type(capability) is not ContextualArmCountCapability:
|
|
1199
|
+
raise TypeError(f"{name} must be an exact capability or None")
|
|
1200
|
+
capability.__post_init__()
|
|
1201
|
+
if (
|
|
1202
|
+
capability.kind != kind
|
|
1203
|
+
or capability.arm_ids != arm_ids
|
|
1204
|
+
or capability.evaluation_slots != self.real_evaluation_slots
|
|
1205
|
+
):
|
|
1206
|
+
raise ValueError(f"{name} differs from the controller query")
|
|
1207
|
+
if any(
|
|
1208
|
+
witness.controller_wave_index >= self.wave_index
|
|
1209
|
+
for witness in capability.witnesses
|
|
1210
|
+
):
|
|
1211
|
+
raise ValueError(f"{name} contains current/future-wave evidence")
|
|
1212
|
+
capabilities = self.joint_count_capabilities
|
|
1213
|
+
if type(capabilities) is not tuple or any(
|
|
1214
|
+
type(value) is not ContextualLaneJointCountCapability
|
|
1215
|
+
for value in capabilities
|
|
1216
|
+
):
|
|
1217
|
+
raise TypeError(
|
|
1218
|
+
"joint_count_capabilities must contain exact lane capabilities"
|
|
1219
|
+
)
|
|
1220
|
+
for value in capabilities:
|
|
1221
|
+
value.__post_init__()
|
|
1222
|
+
if (
|
|
1223
|
+
value.source_arm_ids != self.available_source_ids
|
|
1224
|
+
or value.operator_arm_ids != self.available_operator_ids
|
|
1225
|
+
):
|
|
1226
|
+
raise ValueError("joint capability differs from controller arms")
|
|
1227
|
+
if capabilities:
|
|
1228
|
+
if tuple(value.slice_id for value in capabilities) != tuple(
|
|
1229
|
+
sorted({value.slice_id for value in capabilities})
|
|
1230
|
+
):
|
|
1231
|
+
raise ValueError(
|
|
1232
|
+
"joint lane capabilities must be unique and canonical"
|
|
1233
|
+
)
|
|
1234
|
+
if sum(value.evaluation_slots for value in capabilities) != (
|
|
1235
|
+
self.real_evaluation_slots
|
|
1236
|
+
):
|
|
1237
|
+
raise ValueError(
|
|
1238
|
+
"joint lane capabilities must cover every evaluation slot"
|
|
1239
|
+
)
|
|
1240
|
+
object.__setattr__(
|
|
1241
|
+
self,
|
|
1242
|
+
"query_sha256",
|
|
1243
|
+
_hash(_QUERY_DOMAIN, self._unsigned_record()),
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
@property
|
|
1247
|
+
def remaining_portfolio_waves(self) -> int:
|
|
1248
|
+
return self.total_portfolio_waves - self.wave_index + 1
|
|
1249
|
+
|
|
1250
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1251
|
+
return {
|
|
1252
|
+
"schema_version": 3,
|
|
1253
|
+
"campaign_scope_sha256": self.campaign_scope_sha256,
|
|
1254
|
+
"wave_index": self.wave_index,
|
|
1255
|
+
"total_portfolio_waves": self.total_portfolio_waves,
|
|
1256
|
+
"remaining_portfolio_waves": self.remaining_portfolio_waves,
|
|
1257
|
+
"real_evaluation_slots": self.real_evaluation_slots,
|
|
1258
|
+
"available_source_ids": list(self.available_source_ids),
|
|
1259
|
+
"available_operator_ids": list(self.available_operator_ids),
|
|
1260
|
+
"incumbent_source_id": self.incumbent_source_id,
|
|
1261
|
+
"incumbent_operator_id": self.incumbent_operator_id,
|
|
1262
|
+
"archive_front_size": self.archive_front_size,
|
|
1263
|
+
"recent_normalized_archive_gain_hex": [
|
|
1264
|
+
value.hex() for value in self.recent_normalized_archive_gains
|
|
1265
|
+
],
|
|
1266
|
+
"composition_evidence_available": self.composition_evidence_available,
|
|
1267
|
+
"source_count_capability": (
|
|
1268
|
+
None
|
|
1269
|
+
if self.source_count_capability is None
|
|
1270
|
+
else self.source_count_capability.to_record()
|
|
1271
|
+
),
|
|
1272
|
+
"operator_count_capability": (
|
|
1273
|
+
None
|
|
1274
|
+
if self.operator_count_capability is None
|
|
1275
|
+
else self.operator_count_capability.to_record()
|
|
1276
|
+
),
|
|
1277
|
+
"joint_count_capabilities": [
|
|
1278
|
+
value.to_record() for value in self.joint_count_capabilities
|
|
1279
|
+
],
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
def to_record(self) -> dict[str, object]:
|
|
1283
|
+
self.__post_init__()
|
|
1284
|
+
return {**self._unsigned_record(), "query_sha256": self.query_sha256}
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
@dataclass(frozen=True, slots=True)
|
|
1288
|
+
class ContextualArmAllocation:
|
|
1289
|
+
kind: SearchArmKind
|
|
1290
|
+
arm_id: str
|
|
1291
|
+
target_slots: int
|
|
1292
|
+
score: float
|
|
1293
|
+
allocation_probability: float
|
|
1294
|
+
exploration_slot: bool
|
|
1295
|
+
unconstrained_target_slots: int
|
|
1296
|
+
empirical_capability_projected: bool
|
|
1297
|
+
prospective_joint_capability_projected: bool = False
|
|
1298
|
+
prospective_joint_exploration_projected: bool = False
|
|
1299
|
+
|
|
1300
|
+
def __post_init__(self) -> None:
|
|
1301
|
+
if type(self.kind) is not SearchArmKind:
|
|
1302
|
+
raise TypeError("kind must be exact SearchArmKind")
|
|
1303
|
+
_require_token(self.arm_id, name="arm_id")
|
|
1304
|
+
if type(self.target_slots) is not int or self.target_slots < 0:
|
|
1305
|
+
raise ValueError("target_slots must be a non-negative exact integer")
|
|
1306
|
+
if type(self.score) is not float or not math.isfinite(self.score):
|
|
1307
|
+
raise TypeError("score must be a finite canonical float")
|
|
1308
|
+
_require_probability(
|
|
1309
|
+
self.allocation_probability,
|
|
1310
|
+
name="allocation_probability",
|
|
1311
|
+
)
|
|
1312
|
+
if type(self.exploration_slot) is not bool:
|
|
1313
|
+
raise TypeError("exploration_slot must be an exact bool")
|
|
1314
|
+
if self.exploration_slot and self.target_slots == 0:
|
|
1315
|
+
raise ValueError("exploration marker requires a target slot")
|
|
1316
|
+
if (
|
|
1317
|
+
type(self.unconstrained_target_slots) is not int
|
|
1318
|
+
or self.unconstrained_target_slots < 0
|
|
1319
|
+
):
|
|
1320
|
+
raise ValueError(
|
|
1321
|
+
"unconstrained_target_slots must be a non-negative exact integer"
|
|
1322
|
+
)
|
|
1323
|
+
if type(self.empirical_capability_projected) is not bool:
|
|
1324
|
+
raise TypeError("empirical_capability_projected must be an exact bool")
|
|
1325
|
+
if type(self.prospective_joint_capability_projected) is not bool:
|
|
1326
|
+
raise TypeError(
|
|
1327
|
+
"prospective_joint_capability_projected must be an exact bool"
|
|
1328
|
+
)
|
|
1329
|
+
if type(self.prospective_joint_exploration_projected) is not bool:
|
|
1330
|
+
raise TypeError(
|
|
1331
|
+
"prospective_joint_exploration_projected must be an exact bool"
|
|
1332
|
+
)
|
|
1333
|
+
if (
|
|
1334
|
+
self.prospective_joint_exploration_projected
|
|
1335
|
+
and not self.prospective_joint_capability_projected
|
|
1336
|
+
):
|
|
1337
|
+
raise ValueError(
|
|
1338
|
+
"exploration projection requires joint-capability projection"
|
|
1339
|
+
)
|
|
1340
|
+
if not (
|
|
1341
|
+
self.empirical_capability_projected
|
|
1342
|
+
or self.prospective_joint_capability_projected
|
|
1343
|
+
) and (
|
|
1344
|
+
self.target_slots != self.unconstrained_target_slots
|
|
1345
|
+
):
|
|
1346
|
+
raise ValueError("unprojected allocation changed its unconstrained target")
|
|
1347
|
+
|
|
1348
|
+
def to_record(self) -> dict[str, object]:
|
|
1349
|
+
self.__post_init__()
|
|
1350
|
+
return {
|
|
1351
|
+
"kind": self.kind.value,
|
|
1352
|
+
"arm_id": self.arm_id,
|
|
1353
|
+
"target_slots": self.target_slots,
|
|
1354
|
+
"score_hex": self.score.hex(),
|
|
1355
|
+
"allocation_probability_hex": self.allocation_probability.hex(),
|
|
1356
|
+
"exploration_slot": self.exploration_slot,
|
|
1357
|
+
"unconstrained_target_slots": self.unconstrained_target_slots,
|
|
1358
|
+
"empirical_capability_projected": self.empirical_capability_projected,
|
|
1359
|
+
"prospective_joint_capability_projected": (
|
|
1360
|
+
self.prospective_joint_capability_projected
|
|
1361
|
+
),
|
|
1362
|
+
"prospective_joint_exploration_projected": (
|
|
1363
|
+
self.prospective_joint_exploration_projected
|
|
1364
|
+
),
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
@dataclass(frozen=True, slots=True)
|
|
1369
|
+
class ContextualSearchDecision:
|
|
1370
|
+
query: ContextualSearchQuery
|
|
1371
|
+
snapshot: ContextualSearchSnapshot
|
|
1372
|
+
phase: SearchPhase
|
|
1373
|
+
source_allocations: tuple[ContextualArmAllocation, ...]
|
|
1374
|
+
operator_allocations: tuple[ContextualArmAllocation, ...]
|
|
1375
|
+
joint_capability_selection: tuple[tuple[str, str], ...] = ()
|
|
1376
|
+
decision_sha256: str = field(init=False)
|
|
1377
|
+
|
|
1378
|
+
def __post_init__(self) -> None:
|
|
1379
|
+
if type(self.query) is not ContextualSearchQuery:
|
|
1380
|
+
raise TypeError("query must be exact ContextualSearchQuery")
|
|
1381
|
+
self.query.__post_init__()
|
|
1382
|
+
if type(self.snapshot) is not ContextualSearchSnapshot:
|
|
1383
|
+
raise TypeError("snapshot must be exact ContextualSearchSnapshot")
|
|
1384
|
+
self.snapshot.__post_init__()
|
|
1385
|
+
if (
|
|
1386
|
+
self.snapshot.campaign_scope_sha256 != self.query.campaign_scope_sha256
|
|
1387
|
+
or self.snapshot.cutoff_wave_index_exclusive != self.query.wave_index
|
|
1388
|
+
):
|
|
1389
|
+
raise ValueError("controller snapshot differs from its query cutoff")
|
|
1390
|
+
snapshot_realizations = set(self.snapshot.allocation_realization_sha256s)
|
|
1391
|
+
for capability in (
|
|
1392
|
+
self.query.source_count_capability,
|
|
1393
|
+
self.query.operator_count_capability,
|
|
1394
|
+
):
|
|
1395
|
+
if capability is not None and not set(
|
|
1396
|
+
capability.allocation_realization_sha256s
|
|
1397
|
+
).issubset(snapshot_realizations):
|
|
1398
|
+
raise ValueError(
|
|
1399
|
+
"controller capability cites evidence outside its snapshot"
|
|
1400
|
+
)
|
|
1401
|
+
if type(self.phase) is not SearchPhase:
|
|
1402
|
+
raise TypeError("phase must be exact SearchPhase")
|
|
1403
|
+
for name, kind, available in (
|
|
1404
|
+
(
|
|
1405
|
+
"source_allocations",
|
|
1406
|
+
SearchArmKind.SOURCE,
|
|
1407
|
+
self.query.available_source_ids,
|
|
1408
|
+
),
|
|
1409
|
+
(
|
|
1410
|
+
"operator_allocations",
|
|
1411
|
+
SearchArmKind.OPERATOR,
|
|
1412
|
+
self.query.available_operator_ids,
|
|
1413
|
+
),
|
|
1414
|
+
):
|
|
1415
|
+
values = getattr(self, name)
|
|
1416
|
+
if type(values) is not tuple or any(
|
|
1417
|
+
type(value) is not ContextualArmAllocation for value in values
|
|
1418
|
+
):
|
|
1419
|
+
raise TypeError(f"{name} must contain exact allocations")
|
|
1420
|
+
for value in values:
|
|
1421
|
+
value.__post_init__()
|
|
1422
|
+
if value.kind is not kind:
|
|
1423
|
+
raise ValueError(f"{name} contains a foreign arm kind")
|
|
1424
|
+
if tuple(value.arm_id for value in values) != available:
|
|
1425
|
+
raise ValueError(f"{name} must cover available arms canonically")
|
|
1426
|
+
if sum(value.target_slots for value in values) != (
|
|
1427
|
+
self.query.real_evaluation_slots
|
|
1428
|
+
):
|
|
1429
|
+
raise ValueError(f"{name} must allocate every real evaluation slot")
|
|
1430
|
+
if not math.isclose(
|
|
1431
|
+
sum(value.allocation_probability for value in values),
|
|
1432
|
+
1.0,
|
|
1433
|
+
rel_tol=0.0,
|
|
1434
|
+
abs_tol=1e-12,
|
|
1435
|
+
):
|
|
1436
|
+
raise ValueError(f"{name} allocation probabilities must sum to one")
|
|
1437
|
+
expected_exploration = (
|
|
1438
|
+
0
|
|
1439
|
+
if self.phase is SearchPhase.TERMINAL_CONVERSION
|
|
1440
|
+
else min(1, len(values) - 1)
|
|
1441
|
+
)
|
|
1442
|
+
if sum(value.exploration_slot for value in values) != expected_exploration:
|
|
1443
|
+
raise ValueError(f"{name} has an invalid exploration-slot count")
|
|
1444
|
+
selection = self.joint_capability_selection
|
|
1445
|
+
if type(selection) is not tuple or any(
|
|
1446
|
+
type(value) is not tuple
|
|
1447
|
+
or len(value) != 2
|
|
1448
|
+
or type(value[0]) is not str
|
|
1449
|
+
or type(value[1]) is not str
|
|
1450
|
+
for value in selection
|
|
1451
|
+
):
|
|
1452
|
+
raise TypeError(
|
|
1453
|
+
"joint_capability_selection must contain exact slice/hash pairs"
|
|
1454
|
+
)
|
|
1455
|
+
capabilities = self.query.joint_count_capabilities
|
|
1456
|
+
if not capabilities:
|
|
1457
|
+
if selection:
|
|
1458
|
+
raise ValueError(
|
|
1459
|
+
"decision selected a joint vector without query capabilities"
|
|
1460
|
+
)
|
|
1461
|
+
else:
|
|
1462
|
+
if tuple(slice_id for slice_id, _ in selection) != tuple(
|
|
1463
|
+
value.slice_id for value in capabilities
|
|
1464
|
+
):
|
|
1465
|
+
raise ValueError(
|
|
1466
|
+
"joint capability selection must exactly cover query lanes"
|
|
1467
|
+
)
|
|
1468
|
+
selected_vectors = tuple(
|
|
1469
|
+
capability.resolve_vector(vector_sha256)
|
|
1470
|
+
for capability, (slice_id, vector_sha256) in zip(
|
|
1471
|
+
capabilities,
|
|
1472
|
+
selection,
|
|
1473
|
+
strict=True,
|
|
1474
|
+
)
|
|
1475
|
+
)
|
|
1476
|
+
if any(
|
|
1477
|
+
capability.slice_id != slice_id
|
|
1478
|
+
for capability, (slice_id, _) in zip(
|
|
1479
|
+
capabilities,
|
|
1480
|
+
selection,
|
|
1481
|
+
strict=True,
|
|
1482
|
+
)
|
|
1483
|
+
):
|
|
1484
|
+
raise ValueError("joint selection lane differs from its capability")
|
|
1485
|
+
for count_name, allocations in (
|
|
1486
|
+
("source_target_counts", self.source_allocations),
|
|
1487
|
+
("operator_target_counts", self.operator_allocations),
|
|
1488
|
+
):
|
|
1489
|
+
aggregate = {
|
|
1490
|
+
allocation.arm_id: sum(
|
|
1491
|
+
dict(getattr(vector, count_name))[allocation.arm_id]
|
|
1492
|
+
for vector in selected_vectors
|
|
1493
|
+
)
|
|
1494
|
+
for allocation in allocations
|
|
1495
|
+
}
|
|
1496
|
+
expected = {
|
|
1497
|
+
allocation.arm_id: allocation.target_slots
|
|
1498
|
+
for allocation in allocations
|
|
1499
|
+
}
|
|
1500
|
+
if aggregate != expected:
|
|
1501
|
+
raise ValueError(
|
|
1502
|
+
"joint capability selection differs from decision targets"
|
|
1503
|
+
)
|
|
1504
|
+
object.__setattr__(
|
|
1505
|
+
self,
|
|
1506
|
+
"decision_sha256",
|
|
1507
|
+
_hash(_DECISION_DOMAIN, self._unsigned_record()),
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1510
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1511
|
+
return {
|
|
1512
|
+
"schema_version": 3,
|
|
1513
|
+
"query_sha256": self.query.query_sha256,
|
|
1514
|
+
"snapshot_sha256": self.snapshot.snapshot_sha256,
|
|
1515
|
+
"phase": self.phase.value,
|
|
1516
|
+
"source_allocations": [
|
|
1517
|
+
value.to_record() for value in self.source_allocations
|
|
1518
|
+
],
|
|
1519
|
+
"operator_allocations": [
|
|
1520
|
+
value.to_record() for value in self.operator_allocations
|
|
1521
|
+
],
|
|
1522
|
+
"joint_capability_selection": [
|
|
1523
|
+
{"slice_id": slice_id, "vector_sha256": vector_sha256}
|
|
1524
|
+
for slice_id, vector_sha256 in self.joint_capability_selection
|
|
1525
|
+
],
|
|
1526
|
+
"terminal_information_bonus": (
|
|
1527
|
+
0.0 if self.phase is SearchPhase.TERMINAL_CONVERSION else None
|
|
1528
|
+
),
|
|
1529
|
+
"policy": {
|
|
1530
|
+
"policy_id": CONTEXTUAL_SEARCH_CONTROLLER_ID,
|
|
1531
|
+
"policy_version": CONTEXTUAL_SEARCH_CONTROLLER_VERSION,
|
|
1532
|
+
"definition_sha256": CONTEXTUAL_SEARCH_CONTROLLER_DEFINITION_SHA256,
|
|
1533
|
+
},
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
def to_record(self) -> dict[str, object]:
|
|
1537
|
+
self.__post_init__()
|
|
1538
|
+
return {
|
|
1539
|
+
**self._unsigned_record(),
|
|
1540
|
+
"query": self.query.to_record(),
|
|
1541
|
+
"snapshot": self.snapshot.to_record(),
|
|
1542
|
+
"decision_sha256": self.decision_sha256,
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
@dataclass(frozen=True, slots=True)
|
|
1547
|
+
class ContextualPortfolioAllocationSlice:
|
|
1548
|
+
"""One exact request-local slice of a stage-global controller decision."""
|
|
1549
|
+
|
|
1550
|
+
campaign_scope_sha256: str
|
|
1551
|
+
query_sha256: str
|
|
1552
|
+
decision_sha256: str
|
|
1553
|
+
wave_index: int
|
|
1554
|
+
phase: SearchPhase
|
|
1555
|
+
slice_id: str
|
|
1556
|
+
evaluation_slots: int
|
|
1557
|
+
source_target_counts: tuple[tuple[str, int], ...]
|
|
1558
|
+
operator_target_counts: tuple[tuple[str, int], ...]
|
|
1559
|
+
minimum_single_path_interventions: int = 0
|
|
1560
|
+
minimum_disjoint_parent_patch_pairs: int = 0
|
|
1561
|
+
feasibility_witness_option_identity_sha256s: tuple[str, ...] = ()
|
|
1562
|
+
slice_sha256: str = field(init=False)
|
|
1563
|
+
|
|
1564
|
+
def __post_init__(self) -> None:
|
|
1565
|
+
for name in (
|
|
1566
|
+
"campaign_scope_sha256",
|
|
1567
|
+
"query_sha256",
|
|
1568
|
+
"decision_sha256",
|
|
1569
|
+
):
|
|
1570
|
+
require_sha256(getattr(self, name), name)
|
|
1571
|
+
if type(self.wave_index) is not int or self.wave_index <= 0:
|
|
1572
|
+
raise ValueError("wave_index must be positive")
|
|
1573
|
+
if type(self.phase) is not SearchPhase:
|
|
1574
|
+
raise TypeError("phase must be exact SearchPhase")
|
|
1575
|
+
_require_token(self.slice_id, name="slice_id")
|
|
1576
|
+
if type(self.evaluation_slots) is not int or self.evaluation_slots <= 0:
|
|
1577
|
+
raise ValueError("evaluation_slots must be positive")
|
|
1578
|
+
if (
|
|
1579
|
+
type(self.minimum_single_path_interventions) is not int
|
|
1580
|
+
or not 0
|
|
1581
|
+
<= self.minimum_single_path_interventions
|
|
1582
|
+
<= self.evaluation_slots
|
|
1583
|
+
):
|
|
1584
|
+
raise ValueError(
|
|
1585
|
+
"minimum_single_path_interventions must lie in slice capacity"
|
|
1586
|
+
)
|
|
1587
|
+
maximum_pairs = self.evaluation_slots * (self.evaluation_slots - 1) // 2
|
|
1588
|
+
if (
|
|
1589
|
+
type(self.minimum_disjoint_parent_patch_pairs) is not int
|
|
1590
|
+
or not 0
|
|
1591
|
+
<= self.minimum_disjoint_parent_patch_pairs
|
|
1592
|
+
<= maximum_pairs
|
|
1593
|
+
):
|
|
1594
|
+
raise ValueError(
|
|
1595
|
+
"minimum_disjoint_parent_patch_pairs must lie in slice pair capacity"
|
|
1596
|
+
)
|
|
1597
|
+
for name in ("source_target_counts", "operator_target_counts"):
|
|
1598
|
+
values = getattr(self, name)
|
|
1599
|
+
if type(values) is not tuple or not values:
|
|
1600
|
+
raise ValueError(f"{name} must be a non-empty exact tuple")
|
|
1601
|
+
for arm_id, count in values:
|
|
1602
|
+
_require_token(arm_id, name=f"{name}.arm_id")
|
|
1603
|
+
if type(count) is not int or count < 0:
|
|
1604
|
+
raise ValueError(f"{name} counts must be non-negative")
|
|
1605
|
+
if values != tuple(sorted(values)) or len(
|
|
1606
|
+
{arm_id for arm_id, _ in values}
|
|
1607
|
+
) != len(values):
|
|
1608
|
+
raise ValueError(f"{name} must use canonical unique arms")
|
|
1609
|
+
if sum(count for _, count in values) != self.evaluation_slots:
|
|
1610
|
+
raise ValueError(f"{name} must allocate every slice slot")
|
|
1611
|
+
witnesses = self.feasibility_witness_option_identity_sha256s
|
|
1612
|
+
if type(witnesses) is not tuple:
|
|
1613
|
+
raise TypeError(
|
|
1614
|
+
"feasibility_witness_option_identity_sha256s must be an exact tuple"
|
|
1615
|
+
)
|
|
1616
|
+
if witnesses:
|
|
1617
|
+
if len(witnesses) != self.evaluation_slots or witnesses != tuple(
|
|
1618
|
+
sorted(set(witnesses))
|
|
1619
|
+
):
|
|
1620
|
+
raise ValueError(
|
|
1621
|
+
"allocation-slice feasibility witness must contain one canonical "
|
|
1622
|
+
"unique option identity per evaluation slot"
|
|
1623
|
+
)
|
|
1624
|
+
for value in witnesses:
|
|
1625
|
+
require_sha256(value, "feasibility_witness_option_identity_sha256")
|
|
1626
|
+
object.__setattr__(
|
|
1627
|
+
self,
|
|
1628
|
+
"slice_sha256",
|
|
1629
|
+
_hash(_ALLOCATION_SLICE_DOMAIN, self._unsigned_record()),
|
|
1630
|
+
)
|
|
1631
|
+
|
|
1632
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1633
|
+
record: dict[str, object] = {
|
|
1634
|
+
"schema_version": (
|
|
1635
|
+
4
|
|
1636
|
+
if self.feasibility_witness_option_identity_sha256s
|
|
1637
|
+
else 3
|
|
1638
|
+
if self.minimum_disjoint_parent_patch_pairs
|
|
1639
|
+
else 2
|
|
1640
|
+
if self.minimum_single_path_interventions
|
|
1641
|
+
else 1
|
|
1642
|
+
),
|
|
1643
|
+
"campaign_scope_sha256": self.campaign_scope_sha256,
|
|
1644
|
+
"query_sha256": self.query_sha256,
|
|
1645
|
+
"decision_sha256": self.decision_sha256,
|
|
1646
|
+
"wave_index": self.wave_index,
|
|
1647
|
+
"phase": self.phase.value,
|
|
1648
|
+
"slice_id": self.slice_id,
|
|
1649
|
+
"evaluation_slots": self.evaluation_slots,
|
|
1650
|
+
"source_target_counts": [
|
|
1651
|
+
list(value) for value in self.source_target_counts
|
|
1652
|
+
],
|
|
1653
|
+
"operator_target_counts": [
|
|
1654
|
+
list(value) for value in self.operator_target_counts
|
|
1655
|
+
],
|
|
1656
|
+
}
|
|
1657
|
+
if self.minimum_single_path_interventions:
|
|
1658
|
+
record["minimum_single_path_interventions"] = (
|
|
1659
|
+
self.minimum_single_path_interventions
|
|
1660
|
+
)
|
|
1661
|
+
record["intervention_axis"] = (
|
|
1662
|
+
"exact_parent_relative_changed_json_path_count"
|
|
1663
|
+
)
|
|
1664
|
+
if self.minimum_disjoint_parent_patch_pairs:
|
|
1665
|
+
record["minimum_disjoint_parent_patch_pairs"] = (
|
|
1666
|
+
self.minimum_disjoint_parent_patch_pairs
|
|
1667
|
+
)
|
|
1668
|
+
record["offspring_opportunity_axis"] = (
|
|
1669
|
+
"pairwise_disjoint_parent_relative_patch_pairs"
|
|
1670
|
+
)
|
|
1671
|
+
if self.feasibility_witness_option_identity_sha256s:
|
|
1672
|
+
record["feasibility_witness_option_identity_sha256s"] = list(
|
|
1673
|
+
self.feasibility_witness_option_identity_sha256s
|
|
1674
|
+
)
|
|
1675
|
+
record["feasibility_witness_semantics"] = (
|
|
1676
|
+
"current_finite_contract_exact_joint_count_and_structural_witness"
|
|
1677
|
+
)
|
|
1678
|
+
return record
|
|
1679
|
+
|
|
1680
|
+
def to_record(self) -> dict[str, object]:
|
|
1681
|
+
self.__post_init__()
|
|
1682
|
+
return {**self._unsigned_record(), "slice_sha256": self.slice_sha256}
|
|
1683
|
+
|
|
1684
|
+
def target_count(self, kind: SearchArmKind, arm_id: str) -> int:
|
|
1685
|
+
if type(kind) is not SearchArmKind:
|
|
1686
|
+
raise TypeError("kind must be exact SearchArmKind")
|
|
1687
|
+
_require_token(arm_id, name="arm_id")
|
|
1688
|
+
values = (
|
|
1689
|
+
self.source_target_counts
|
|
1690
|
+
if kind is SearchArmKind.SOURCE
|
|
1691
|
+
else self.operator_target_counts
|
|
1692
|
+
)
|
|
1693
|
+
try:
|
|
1694
|
+
return dict(values)[arm_id]
|
|
1695
|
+
except KeyError as error:
|
|
1696
|
+
raise ValueError("arm is absent from this allocation slice") from error
|
|
1697
|
+
|
|
1698
|
+
def to_contract(
|
|
1699
|
+
self,
|
|
1700
|
+
*,
|
|
1701
|
+
campaign_generation: int,
|
|
1702
|
+
) -> ContextualPortfolioAllocationContract:
|
|
1703
|
+
if type(campaign_generation) is not int or campaign_generation <= 0:
|
|
1704
|
+
raise ValueError("campaign_generation must be positive")
|
|
1705
|
+
return ContextualPortfolioAllocationContract(
|
|
1706
|
+
campaign_scope_sha256=self.campaign_scope_sha256,
|
|
1707
|
+
query_sha256=self.query_sha256,
|
|
1708
|
+
decision_sha256=self.decision_sha256,
|
|
1709
|
+
campaign_generation=campaign_generation,
|
|
1710
|
+
controller_wave_index=self.wave_index,
|
|
1711
|
+
phase_id=self.phase.value,
|
|
1712
|
+
slice_id=self.slice_id,
|
|
1713
|
+
evaluation_slots=self.evaluation_slots,
|
|
1714
|
+
source_target_counts=self.source_target_counts,
|
|
1715
|
+
operator_target_counts=self.operator_target_counts,
|
|
1716
|
+
minimum_single_path_interventions=(
|
|
1717
|
+
self.minimum_single_path_interventions
|
|
1718
|
+
),
|
|
1719
|
+
minimum_disjoint_parent_patch_pairs=(
|
|
1720
|
+
self.minimum_disjoint_parent_patch_pairs
|
|
1721
|
+
),
|
|
1722
|
+
feasibility_witness_option_identity_sha256s=(
|
|
1723
|
+
self.feasibility_witness_option_identity_sha256s
|
|
1724
|
+
),
|
|
1725
|
+
)
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
@dataclass(frozen=True, slots=True)
|
|
1729
|
+
class ContextualSearchStageAllocation:
|
|
1730
|
+
"""Authenticated decomposition of one decision across concurrent parents."""
|
|
1731
|
+
|
|
1732
|
+
decision: ContextualSearchDecision
|
|
1733
|
+
slices: tuple[ContextualPortfolioAllocationSlice, ...]
|
|
1734
|
+
allocation_sha256: str = field(init=False)
|
|
1735
|
+
|
|
1736
|
+
def __post_init__(self) -> None:
|
|
1737
|
+
if type(self.decision) is not ContextualSearchDecision:
|
|
1738
|
+
raise TypeError("decision must be exact ContextualSearchDecision")
|
|
1739
|
+
self.decision.__post_init__()
|
|
1740
|
+
if (
|
|
1741
|
+
type(self.slices) is not tuple
|
|
1742
|
+
or not self.slices
|
|
1743
|
+
or any(
|
|
1744
|
+
type(value) is not ContextualPortfolioAllocationSlice
|
|
1745
|
+
for value in self.slices
|
|
1746
|
+
)
|
|
1747
|
+
):
|
|
1748
|
+
raise ValueError("slices must contain exact allocation slices")
|
|
1749
|
+
for value in self.slices:
|
|
1750
|
+
value.__post_init__()
|
|
1751
|
+
if (
|
|
1752
|
+
value.campaign_scope_sha256 != self.decision.query.campaign_scope_sha256
|
|
1753
|
+
or value.query_sha256 != self.decision.query.query_sha256
|
|
1754
|
+
or value.decision_sha256 != self.decision.decision_sha256
|
|
1755
|
+
or value.wave_index != self.decision.query.wave_index
|
|
1756
|
+
or value.phase is not self.decision.phase
|
|
1757
|
+
):
|
|
1758
|
+
raise ValueError("allocation slice differs from its decision")
|
|
1759
|
+
if tuple(value.slice_id for value in self.slices) != tuple(
|
|
1760
|
+
sorted({value.slice_id for value in self.slices})
|
|
1761
|
+
):
|
|
1762
|
+
raise ValueError("allocation slices must be unique and canonical")
|
|
1763
|
+
if self.decision.joint_capability_selection:
|
|
1764
|
+
selected_vectors = tuple(
|
|
1765
|
+
capability.resolve_vector(vector_sha256)
|
|
1766
|
+
for capability, (slice_id, vector_sha256) in zip(
|
|
1767
|
+
self.decision.query.joint_count_capabilities,
|
|
1768
|
+
self.decision.joint_capability_selection,
|
|
1769
|
+
strict=True,
|
|
1770
|
+
)
|
|
1771
|
+
if capability.slice_id == slice_id
|
|
1772
|
+
)
|
|
1773
|
+
if len(selected_vectors) != len(self.slices) or any(
|
|
1774
|
+
allocation.feasibility_witness_option_identity_sha256s
|
|
1775
|
+
!= vector.feasibility_witness_option_identity_sha256s
|
|
1776
|
+
for allocation, vector in zip(
|
|
1777
|
+
self.slices,
|
|
1778
|
+
selected_vectors,
|
|
1779
|
+
strict=True,
|
|
1780
|
+
)
|
|
1781
|
+
):
|
|
1782
|
+
raise ValueError(
|
|
1783
|
+
"allocation slices differ from their selected joint witnesses"
|
|
1784
|
+
)
|
|
1785
|
+
elif any(
|
|
1786
|
+
value.feasibility_witness_option_identity_sha256s
|
|
1787
|
+
for value in self.slices
|
|
1788
|
+
):
|
|
1789
|
+
raise ValueError(
|
|
1790
|
+
"allocation slice carries a witness without joint capability selection"
|
|
1791
|
+
)
|
|
1792
|
+
if sum(value.evaluation_slots for value in self.slices) != (
|
|
1793
|
+
self.decision.query.real_evaluation_slots
|
|
1794
|
+
):
|
|
1795
|
+
raise ValueError("allocation slices do not cover the stage")
|
|
1796
|
+
for name, allocations in (
|
|
1797
|
+
("source_target_counts", self.decision.source_allocations),
|
|
1798
|
+
("operator_target_counts", self.decision.operator_allocations),
|
|
1799
|
+
):
|
|
1800
|
+
aggregate = {
|
|
1801
|
+
allocation.arm_id: sum(
|
|
1802
|
+
dict(getattr(value, name))[allocation.arm_id]
|
|
1803
|
+
for value in self.slices
|
|
1804
|
+
)
|
|
1805
|
+
for allocation in allocations
|
|
1806
|
+
}
|
|
1807
|
+
expected = {
|
|
1808
|
+
allocation.arm_id: allocation.target_slots for allocation in allocations
|
|
1809
|
+
}
|
|
1810
|
+
if aggregate != expected:
|
|
1811
|
+
raise ValueError("allocation slices differ from stage targets")
|
|
1812
|
+
object.__setattr__(
|
|
1813
|
+
self,
|
|
1814
|
+
"allocation_sha256",
|
|
1815
|
+
_hash(_STAGE_ALLOCATION_DOMAIN, self._unsigned_record()),
|
|
1816
|
+
)
|
|
1817
|
+
|
|
1818
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1819
|
+
return {
|
|
1820
|
+
"schema_version": 1,
|
|
1821
|
+
"decision_sha256": self.decision.decision_sha256,
|
|
1822
|
+
"slice_sha256s": [value.slice_sha256 for value in self.slices],
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
def to_record(self) -> dict[str, object]:
|
|
1826
|
+
self.__post_init__()
|
|
1827
|
+
return {
|
|
1828
|
+
**self._unsigned_record(),
|
|
1829
|
+
"decision": self.decision.to_record(),
|
|
1830
|
+
"slices": [value.to_record() for value in self.slices],
|
|
1831
|
+
"allocation_sha256": self.allocation_sha256,
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
def _slice_arm_counts(
|
|
1836
|
+
allocations: tuple[ContextualArmAllocation, ...],
|
|
1837
|
+
slice_slots: tuple[int, ...],
|
|
1838
|
+
) -> tuple[tuple[tuple[str, int], ...], ...]:
|
|
1839
|
+
remaining = {value.arm_id: value.target_slots for value in allocations}
|
|
1840
|
+
remaining_slots = sum(slice_slots)
|
|
1841
|
+
rows: list[tuple[tuple[str, int], ...]] = []
|
|
1842
|
+
for index, slots in enumerate(slice_slots):
|
|
1843
|
+
if index == len(slice_slots) - 1:
|
|
1844
|
+
counts = dict(remaining)
|
|
1845
|
+
else:
|
|
1846
|
+
desired = {
|
|
1847
|
+
arm_id: count * slots / remaining_slots
|
|
1848
|
+
for arm_id, count in remaining.items()
|
|
1849
|
+
}
|
|
1850
|
+
counts = {arm_id: math.floor(value) for arm_id, value in desired.items()}
|
|
1851
|
+
for _ in range(slots - sum(counts.values())):
|
|
1852
|
+
arm_id = sorted(
|
|
1853
|
+
remaining,
|
|
1854
|
+
key=lambda value: (
|
|
1855
|
+
-(desired[value] - counts[value]),
|
|
1856
|
+
value,
|
|
1857
|
+
),
|
|
1858
|
+
)[0]
|
|
1859
|
+
counts[arm_id] += 1
|
|
1860
|
+
if sum(counts.values()) != slots or any(
|
|
1861
|
+
counts[arm_id] > remaining[arm_id] for arm_id in remaining
|
|
1862
|
+
):
|
|
1863
|
+
raise AssertionError("deficit rounding produced an invalid slice")
|
|
1864
|
+
rows.append(tuple(sorted(counts.items())))
|
|
1865
|
+
remaining = {arm_id: remaining[arm_id] - counts[arm_id] for arm_id in remaining}
|
|
1866
|
+
remaining_slots -= slots
|
|
1867
|
+
if remaining_slots != 0 or any(remaining.values()):
|
|
1868
|
+
raise AssertionError("allocation slicing left an unassigned target")
|
|
1869
|
+
return tuple(rows)
|
|
1870
|
+
|
|
1871
|
+
|
|
1872
|
+
def slice_contextual_search_decision(
|
|
1873
|
+
decision: ContextualSearchDecision,
|
|
1874
|
+
*,
|
|
1875
|
+
slice_ids: tuple[str, ...],
|
|
1876
|
+
evaluation_slots: tuple[int, ...],
|
|
1877
|
+
) -> ContextualSearchStageAllocation:
|
|
1878
|
+
"""Split stage-global marginals without workload or provider knowledge."""
|
|
1879
|
+
|
|
1880
|
+
if type(decision) is not ContextualSearchDecision:
|
|
1881
|
+
raise TypeError("decision must be exact ContextualSearchDecision")
|
|
1882
|
+
decision.__post_init__()
|
|
1883
|
+
_canonical_tokens(slice_ids, name="slice_ids")
|
|
1884
|
+
if (
|
|
1885
|
+
type(evaluation_slots) is not tuple
|
|
1886
|
+
or len(evaluation_slots) != len(slice_ids)
|
|
1887
|
+
or any(type(value) is not int or value <= 0 for value in evaluation_slots)
|
|
1888
|
+
):
|
|
1889
|
+
raise ValueError("evaluation_slots must positively cover every slice")
|
|
1890
|
+
if sum(evaluation_slots) != decision.query.real_evaluation_slots:
|
|
1891
|
+
raise ValueError("slice capacity differs from the controller query")
|
|
1892
|
+
if decision.joint_capability_selection:
|
|
1893
|
+
capabilities = decision.query.joint_count_capabilities
|
|
1894
|
+
if tuple(value.slice_id for value in capabilities) != slice_ids:
|
|
1895
|
+
raise ValueError("requested slices differ from joint capabilities")
|
|
1896
|
+
if tuple(value.evaluation_slots for value in capabilities) != evaluation_slots:
|
|
1897
|
+
raise ValueError("requested slice widths differ from joint capabilities")
|
|
1898
|
+
vectors = tuple(
|
|
1899
|
+
capability.resolve_vector(vector_sha256)
|
|
1900
|
+
for capability, (selected_slice_id, vector_sha256) in zip(
|
|
1901
|
+
capabilities,
|
|
1902
|
+
decision.joint_capability_selection,
|
|
1903
|
+
strict=True,
|
|
1904
|
+
)
|
|
1905
|
+
if capability.slice_id == selected_slice_id
|
|
1906
|
+
)
|
|
1907
|
+
if len(vectors) != len(capabilities):
|
|
1908
|
+
raise ValueError("joint capability selection has a foreign slice")
|
|
1909
|
+
source_rows = tuple(value.source_target_counts for value in vectors)
|
|
1910
|
+
operator_rows = tuple(value.operator_target_counts for value in vectors)
|
|
1911
|
+
minimum_single_path_rows = tuple(
|
|
1912
|
+
value.minimum_single_path_interventions for value in capabilities
|
|
1913
|
+
)
|
|
1914
|
+
minimum_disjoint_pair_rows = tuple(
|
|
1915
|
+
value.minimum_disjoint_parent_patch_pairs for value in capabilities
|
|
1916
|
+
)
|
|
1917
|
+
feasibility_witness_rows = tuple(
|
|
1918
|
+
value.feasibility_witness_option_identity_sha256s for value in vectors
|
|
1919
|
+
)
|
|
1920
|
+
else:
|
|
1921
|
+
source_rows = _slice_arm_counts(decision.source_allocations, evaluation_slots)
|
|
1922
|
+
operator_rows = _slice_arm_counts(
|
|
1923
|
+
decision.operator_allocations,
|
|
1924
|
+
evaluation_slots,
|
|
1925
|
+
)
|
|
1926
|
+
minimum_single_path_rows = tuple(0 for _ in slice_ids)
|
|
1927
|
+
minimum_disjoint_pair_rows = tuple(0 for _ in slice_ids)
|
|
1928
|
+
feasibility_witness_rows = tuple(() for _ in slice_ids)
|
|
1929
|
+
return ContextualSearchStageAllocation(
|
|
1930
|
+
decision=decision,
|
|
1931
|
+
slices=tuple(
|
|
1932
|
+
ContextualPortfolioAllocationSlice(
|
|
1933
|
+
campaign_scope_sha256=decision.query.campaign_scope_sha256,
|
|
1934
|
+
query_sha256=decision.query.query_sha256,
|
|
1935
|
+
decision_sha256=decision.decision_sha256,
|
|
1936
|
+
wave_index=decision.query.wave_index,
|
|
1937
|
+
phase=decision.phase,
|
|
1938
|
+
slice_id=slice_id,
|
|
1939
|
+
evaluation_slots=slots,
|
|
1940
|
+
source_target_counts=source_counts,
|
|
1941
|
+
operator_target_counts=operator_counts,
|
|
1942
|
+
minimum_single_path_interventions=(
|
|
1943
|
+
minimum_single_path_interventions
|
|
1944
|
+
),
|
|
1945
|
+
minimum_disjoint_parent_patch_pairs=(
|
|
1946
|
+
minimum_disjoint_parent_patch_pairs
|
|
1947
|
+
),
|
|
1948
|
+
feasibility_witness_option_identity_sha256s=(
|
|
1949
|
+
feasibility_witness_option_identity_sha256s
|
|
1950
|
+
),
|
|
1951
|
+
)
|
|
1952
|
+
for (
|
|
1953
|
+
slice_id,
|
|
1954
|
+
slots,
|
|
1955
|
+
source_counts,
|
|
1956
|
+
operator_counts,
|
|
1957
|
+
minimum_single_path_interventions,
|
|
1958
|
+
minimum_disjoint_parent_patch_pairs,
|
|
1959
|
+
feasibility_witness_option_identity_sha256s,
|
|
1960
|
+
) in zip(
|
|
1961
|
+
slice_ids,
|
|
1962
|
+
evaluation_slots,
|
|
1963
|
+
source_rows,
|
|
1964
|
+
operator_rows,
|
|
1965
|
+
minimum_single_path_rows,
|
|
1966
|
+
minimum_disjoint_pair_rows,
|
|
1967
|
+
feasibility_witness_rows,
|
|
1968
|
+
strict=True,
|
|
1969
|
+
)
|
|
1970
|
+
),
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
def _phase(
|
|
1975
|
+
query: ContextualSearchQuery,
|
|
1976
|
+
snapshot: ContextualSearchSnapshot,
|
|
1977
|
+
) -> SearchPhase:
|
|
1978
|
+
if query.remaining_portfolio_waves == 1:
|
|
1979
|
+
return SearchPhase.TERMINAL_CONVERSION
|
|
1980
|
+
if not snapshot.observation_sha256s:
|
|
1981
|
+
return SearchPhase.BASIN_ACQUISITION
|
|
1982
|
+
if query.composition_evidence_available:
|
|
1983
|
+
return SearchPhase.COMPOSITION
|
|
1984
|
+
return SearchPhase.BASIN_EXPANSION
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
def _score(posterior: ContextualArmPosterior, phase: SearchPhase) -> float:
|
|
1988
|
+
"""Return the exploitation value in the single archive-return currency.
|
|
1989
|
+
|
|
1990
|
+
Every evaluated infeasible or zero-yield action is already a zero in the
|
|
1991
|
+
fractional-Beta return posterior. Persistence, descendant incidence,
|
|
1992
|
+
source distance, and allocation overlap remain available for diagnosis and
|
|
1993
|
+
future resolved-return construction; rewarding them separately would count
|
|
1994
|
+
the same causal outcome more than once. Nonterminal uncertainty receives
|
|
1995
|
+
exactly one separately marked exploration slot in ``_allocate``. Folding
|
|
1996
|
+
it into every proportional target as well would pay for information twice.
|
|
1997
|
+
"""
|
|
1998
|
+
|
|
1999
|
+
del phase
|
|
2000
|
+
return posterior.return_probability
|
|
2001
|
+
|
|
2002
|
+
|
|
2003
|
+
def _allocate(
|
|
2004
|
+
posteriors: tuple[ContextualArmPosterior, ...],
|
|
2005
|
+
*,
|
|
2006
|
+
slots: int,
|
|
2007
|
+
phase: SearchPhase,
|
|
2008
|
+
incumbent_arm_id: str | None,
|
|
2009
|
+
incumbent_prior_mass: float,
|
|
2010
|
+
empirical_capability: ContextualArmCountCapability | None,
|
|
2011
|
+
) -> tuple[ContextualArmAllocation, ...]:
|
|
2012
|
+
if not posteriors:
|
|
2013
|
+
raise ValueError("controller allocation requires arm posteriors")
|
|
2014
|
+
if type(slots) is not int or slots <= 0:
|
|
2015
|
+
raise ValueError("slots must be positive")
|
|
2016
|
+
_require_probability(incumbent_prior_mass, name="incumbent_prior_mass")
|
|
2017
|
+
if len(posteriors) > 1 and not 0.0 < incumbent_prior_mass < 1.0:
|
|
2018
|
+
raise ValueError(
|
|
2019
|
+
"multi-arm incumbent prior mass must lie strictly inside (0, 1)"
|
|
2020
|
+
)
|
|
2021
|
+
scores = {value.arm_id: _score(value, phase) for value in posteriors}
|
|
2022
|
+
counts = {value.arm_id: 0 for value in posteriors}
|
|
2023
|
+
if incumbent_arm_id is not None and incumbent_arm_id not in {
|
|
2024
|
+
value.arm_id for value in posteriors
|
|
2025
|
+
}:
|
|
2026
|
+
raise ValueError("incumbent arm is absent from controller posteriors")
|
|
2027
|
+
incumbent_prior = {
|
|
2028
|
+
value.arm_id: (
|
|
2029
|
+
1.0 / len(posteriors)
|
|
2030
|
+
if incumbent_arm_id is None
|
|
2031
|
+
else 1.0
|
|
2032
|
+
if len(posteriors) == 1
|
|
2033
|
+
else incumbent_prior_mass
|
|
2034
|
+
if value.arm_id == incumbent_arm_id
|
|
2035
|
+
else (1.0 - incumbent_prior_mass) / (len(posteriors) - 1)
|
|
2036
|
+
)
|
|
2037
|
+
for value in posteriors
|
|
2038
|
+
}
|
|
2039
|
+
if phase is SearchPhase.BASIN_ACQUISITION and incumbent_arm_id is not None:
|
|
2040
|
+
leader_id = incumbent_arm_id
|
|
2041
|
+
else:
|
|
2042
|
+
leader_id = sorted(
|
|
2043
|
+
posteriors,
|
|
2044
|
+
key=lambda value: (
|
|
2045
|
+
-scores[value.arm_id],
|
|
2046
|
+
value.observation_count,
|
|
2047
|
+
value.arm_id,
|
|
2048
|
+
),
|
|
2049
|
+
)[0].arm_id
|
|
2050
|
+
exploration_id: str | None = None
|
|
2051
|
+
if phase is not SearchPhase.TERMINAL_CONVERSION and len(posteriors) > 1:
|
|
2052
|
+
exploration_id = sorted(
|
|
2053
|
+
(value for value in posteriors if value.arm_id != leader_id),
|
|
2054
|
+
key=lambda value: (
|
|
2055
|
+
-value.return_uncertainty,
|
|
2056
|
+
value.observation_count,
|
|
2057
|
+
value.arm_id,
|
|
2058
|
+
),
|
|
2059
|
+
)[0].arm_id
|
|
2060
|
+
counts[exploration_id] += 1
|
|
2061
|
+
|
|
2062
|
+
remaining = slots - sum(counts.values())
|
|
2063
|
+
cold_start = phase is SearchPhase.BASIN_ACQUISITION and all(
|
|
2064
|
+
value.observation_count == 0 for value in posteriors
|
|
2065
|
+
)
|
|
2066
|
+
if cold_start:
|
|
2067
|
+
probabilities = incumbent_prior
|
|
2068
|
+
else:
|
|
2069
|
+
# Beta(1,1) is already the explicit cold-start prior. A second
|
|
2070
|
+
# temperature or hand-sized prior mixture would count prior mass twice.
|
|
2071
|
+
total_score = sum(scores.values())
|
|
2072
|
+
if total_score <= 0.0: # Defensive only: fractional-Beta means are > 0.
|
|
2073
|
+
probabilities = incumbent_prior
|
|
2074
|
+
else:
|
|
2075
|
+
probabilities = {
|
|
2076
|
+
value.arm_id: scores[value.arm_id] / total_score
|
|
2077
|
+
for value in posteriors
|
|
2078
|
+
}
|
|
2079
|
+
desired = {
|
|
2080
|
+
value.arm_id: slots * probabilities[value.arm_id] for value in posteriors
|
|
2081
|
+
}
|
|
2082
|
+
# Fill against the full-generation target after reserving exploration;
|
|
2083
|
+
# otherwise the forced slot would be counted twice for uncertain arms.
|
|
2084
|
+
for _ in range(remaining):
|
|
2085
|
+
chosen = sorted(
|
|
2086
|
+
posteriors,
|
|
2087
|
+
key=lambda value: (
|
|
2088
|
+
-(desired[value.arm_id] - counts[value.arm_id]),
|
|
2089
|
+
-scores[value.arm_id],
|
|
2090
|
+
value.arm_id,
|
|
2091
|
+
),
|
|
2092
|
+
)[0]
|
|
2093
|
+
counts[chosen.arm_id] += 1
|
|
2094
|
+
unconstrained_counts = dict(counts)
|
|
2095
|
+
capability_projected = False
|
|
2096
|
+
if empirical_capability is not None:
|
|
2097
|
+
empirical_capability.__post_init__()
|
|
2098
|
+
arm_ids = tuple(value.arm_id for value in posteriors)
|
|
2099
|
+
if (
|
|
2100
|
+
empirical_capability.arm_ids != arm_ids
|
|
2101
|
+
or empirical_capability.evaluation_slots != slots
|
|
2102
|
+
):
|
|
2103
|
+
raise ValueError("empirical capability differs from allocation arms")
|
|
2104
|
+
candidates = []
|
|
2105
|
+
for vector in empirical_capability.feasible_count_vectors:
|
|
2106
|
+
candidate = dict(vector)
|
|
2107
|
+
if exploration_id is not None and candidate[exploration_id] == 0:
|
|
2108
|
+
continue
|
|
2109
|
+
candidates.append(candidate)
|
|
2110
|
+
if candidates:
|
|
2111
|
+
counts = min(
|
|
2112
|
+
candidates,
|
|
2113
|
+
key=lambda candidate: (
|
|
2114
|
+
sum(
|
|
2115
|
+
abs(candidate[arm_id] - unconstrained_counts[arm_id])
|
|
2116
|
+
for arm_id in arm_ids
|
|
2117
|
+
),
|
|
2118
|
+
-sum(scores[arm_id] * candidate[arm_id] for arm_id in arm_ids),
|
|
2119
|
+
tuple(candidate[arm_id] for arm_id in arm_ids),
|
|
2120
|
+
),
|
|
2121
|
+
)
|
|
2122
|
+
capability_projected = counts != unconstrained_counts
|
|
2123
|
+
return tuple(
|
|
2124
|
+
ContextualArmAllocation(
|
|
2125
|
+
kind=value.kind,
|
|
2126
|
+
arm_id=value.arm_id,
|
|
2127
|
+
target_slots=counts[value.arm_id],
|
|
2128
|
+
score=float(scores[value.arm_id]),
|
|
2129
|
+
allocation_probability=float(probabilities[value.arm_id]),
|
|
2130
|
+
exploration_slot=value.arm_id == exploration_id,
|
|
2131
|
+
unconstrained_target_slots=unconstrained_counts[value.arm_id],
|
|
2132
|
+
empirical_capability_projected=capability_projected,
|
|
2133
|
+
)
|
|
2134
|
+
for value in posteriors
|
|
2135
|
+
)
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
def _project_joint_capability_product(
|
|
2139
|
+
source_allocations: tuple[ContextualArmAllocation, ...],
|
|
2140
|
+
operator_allocations: tuple[ContextualArmAllocation, ...],
|
|
2141
|
+
capabilities: tuple[ContextualLaneJointCountCapability, ...],
|
|
2142
|
+
) -> tuple[
|
|
2143
|
+
tuple[ContextualArmAllocation, ...],
|
|
2144
|
+
tuple[ContextualArmAllocation, ...],
|
|
2145
|
+
tuple[tuple[str, str], ...],
|
|
2146
|
+
]:
|
|
2147
|
+
"""Project independent marginals onto exact current lane capabilities."""
|
|
2148
|
+
|
|
2149
|
+
if not capabilities:
|
|
2150
|
+
return source_allocations, operator_allocations, ()
|
|
2151
|
+
for value in capabilities:
|
|
2152
|
+
value.__post_init__()
|
|
2153
|
+
source_ids = tuple(value.arm_id for value in source_allocations)
|
|
2154
|
+
operator_ids = tuple(value.arm_id for value in operator_allocations)
|
|
2155
|
+
if any(
|
|
2156
|
+
value.source_arm_ids != source_ids or value.operator_arm_ids != operator_ids
|
|
2157
|
+
for value in capabilities
|
|
2158
|
+
):
|
|
2159
|
+
raise ValueError("joint capabilities differ from controller allocations")
|
|
2160
|
+
preferred_source = {
|
|
2161
|
+
value.arm_id: value.target_slots for value in source_allocations
|
|
2162
|
+
}
|
|
2163
|
+
preferred_operator = {
|
|
2164
|
+
value.arm_id: value.target_slots for value in operator_allocations
|
|
2165
|
+
}
|
|
2166
|
+
source_scores = {value.arm_id: value.score for value in source_allocations}
|
|
2167
|
+
operator_scores = {value.arm_id: value.score for value in operator_allocations}
|
|
2168
|
+
source_exploration = {
|
|
2169
|
+
value.arm_id for value in source_allocations if value.exploration_slot
|
|
2170
|
+
}
|
|
2171
|
+
operator_exploration = {
|
|
2172
|
+
value.arm_id for value in operator_allocations if value.exploration_slot
|
|
2173
|
+
}
|
|
2174
|
+
candidates: list[
|
|
2175
|
+
tuple[
|
|
2176
|
+
tuple[object, ...],
|
|
2177
|
+
tuple[object, ...],
|
|
2178
|
+
dict[str, int],
|
|
2179
|
+
dict[str, int],
|
|
2180
|
+
]
|
|
2181
|
+
] = []
|
|
2182
|
+
for vectors in product(*(value.feasible_vectors for value in capabilities)):
|
|
2183
|
+
source_counts = {
|
|
2184
|
+
arm_id: sum(
|
|
2185
|
+
dict(vector.source_target_counts)[arm_id] for vector in vectors
|
|
2186
|
+
)
|
|
2187
|
+
for arm_id in source_ids
|
|
2188
|
+
}
|
|
2189
|
+
operator_counts = {
|
|
2190
|
+
arm_id: sum(
|
|
2191
|
+
dict(vector.operator_target_counts)[arm_id] for vector in vectors
|
|
2192
|
+
)
|
|
2193
|
+
for arm_id in operator_ids
|
|
2194
|
+
}
|
|
2195
|
+
source_exploration_loss = sum(
|
|
2196
|
+
source_counts[value] == 0 for value in source_exploration
|
|
2197
|
+
)
|
|
2198
|
+
operator_exploration_loss = sum(
|
|
2199
|
+
operator_counts[value] == 0 for value in operator_exploration
|
|
2200
|
+
)
|
|
2201
|
+
source_l1 = sum(
|
|
2202
|
+
abs(source_counts[value] - preferred_source[value]) for value in source_ids
|
|
2203
|
+
)
|
|
2204
|
+
operator_l1 = sum(
|
|
2205
|
+
abs(operator_counts[value] - preferred_operator[value])
|
|
2206
|
+
for value in operator_ids
|
|
2207
|
+
)
|
|
2208
|
+
posterior_score = sum(
|
|
2209
|
+
source_scores[value] * source_counts[value] for value in source_ids
|
|
2210
|
+
) + sum(
|
|
2211
|
+
operator_scores[value] * operator_counts[value] for value in operator_ids
|
|
2212
|
+
)
|
|
2213
|
+
key: tuple[object, ...] = (
|
|
2214
|
+
source_exploration_loss + operator_exploration_loss,
|
|
2215
|
+
source_exploration_loss,
|
|
2216
|
+
operator_exploration_loss,
|
|
2217
|
+
source_l1 + operator_l1,
|
|
2218
|
+
source_l1,
|
|
2219
|
+
operator_l1,
|
|
2220
|
+
-posterior_score,
|
|
2221
|
+
tuple(value.vector_sha256 for value in vectors),
|
|
2222
|
+
)
|
|
2223
|
+
candidates.append((key, vectors, source_counts, operator_counts))
|
|
2224
|
+
if not candidates: # pragma: no cover - lane capability constructors close this.
|
|
2225
|
+
raise ValueError("joint lane capabilities have no feasible product")
|
|
2226
|
+
_, vectors, selected_source, selected_operator = min(
|
|
2227
|
+
candidates,
|
|
2228
|
+
key=lambda value: value[0],
|
|
2229
|
+
)
|
|
2230
|
+
|
|
2231
|
+
def projected(
|
|
2232
|
+
allocations: tuple[ContextualArmAllocation, ...],
|
|
2233
|
+
selected: dict[str, int],
|
|
2234
|
+
) -> tuple[ContextualArmAllocation, ...]:
|
|
2235
|
+
requested_exploration = tuple(
|
|
2236
|
+
value.arm_id for value in allocations if value.exploration_slot
|
|
2237
|
+
)
|
|
2238
|
+
realized_exploration: str | None = None
|
|
2239
|
+
if requested_exploration:
|
|
2240
|
+
requested = requested_exploration[0]
|
|
2241
|
+
if selected[requested] > 0:
|
|
2242
|
+
realized_exploration = requested
|
|
2243
|
+
else:
|
|
2244
|
+
eligible = tuple(
|
|
2245
|
+
value for value in allocations if selected[value.arm_id] > 0
|
|
2246
|
+
)
|
|
2247
|
+
if not eligible: # pragma: no cover - selected counts sum positive.
|
|
2248
|
+
raise AssertionError("joint projection selected no realized arm")
|
|
2249
|
+
# Infer the exploitation leader from the unconstrained dose, then
|
|
2250
|
+
# move the information marker to the least-entitled realizable
|
|
2251
|
+
# challenger. This is deterministic and consumes no workload,
|
|
2252
|
+
# model, provider, or outcome identifier.
|
|
2253
|
+
leader = sorted(
|
|
2254
|
+
allocations,
|
|
2255
|
+
key=lambda value: (
|
|
2256
|
+
-value.unconstrained_target_slots,
|
|
2257
|
+
-value.allocation_probability,
|
|
2258
|
+
-value.score,
|
|
2259
|
+
value.arm_id,
|
|
2260
|
+
),
|
|
2261
|
+
)[0]
|
|
2262
|
+
challengers = tuple(
|
|
2263
|
+
value for value in eligible if value.arm_id != leader.arm_id
|
|
2264
|
+
)
|
|
2265
|
+
realized_exploration = sorted(
|
|
2266
|
+
challengers if challengers else eligible,
|
|
2267
|
+
key=lambda value: (
|
|
2268
|
+
value.unconstrained_target_slots,
|
|
2269
|
+
value.allocation_probability,
|
|
2270
|
+
value.score,
|
|
2271
|
+
value.arm_id,
|
|
2272
|
+
),
|
|
2273
|
+
)[0].arm_id
|
|
2274
|
+
return tuple(
|
|
2275
|
+
replace(
|
|
2276
|
+
value,
|
|
2277
|
+
target_slots=selected[value.arm_id],
|
|
2278
|
+
exploration_slot=value.arm_id == realized_exploration,
|
|
2279
|
+
prospective_joint_capability_projected=(
|
|
2280
|
+
value.prospective_joint_capability_projected
|
|
2281
|
+
or selected[value.arm_id] != value.target_slots
|
|
2282
|
+
or value.exploration_slot
|
|
2283
|
+
!= (value.arm_id == realized_exploration)
|
|
2284
|
+
),
|
|
2285
|
+
prospective_joint_exploration_projected=(
|
|
2286
|
+
value.exploration_slot
|
|
2287
|
+
!= (value.arm_id == realized_exploration)
|
|
2288
|
+
),
|
|
2289
|
+
)
|
|
2290
|
+
for value in allocations
|
|
2291
|
+
)
|
|
2292
|
+
|
|
2293
|
+
return (
|
|
2294
|
+
projected(source_allocations, selected_source),
|
|
2295
|
+
projected(operator_allocations, selected_operator),
|
|
2296
|
+
tuple(
|
|
2297
|
+
(capability.slice_id, vector.vector_sha256)
|
|
2298
|
+
for capability, vector in zip(capabilities, vectors, strict=True)
|
|
2299
|
+
),
|
|
2300
|
+
)
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
@dataclass(frozen=True, slots=True)
|
|
2304
|
+
class PhaseAwareContextualSearchController:
|
|
2305
|
+
"""Compute one prior-only, phase-aware source/operator decision."""
|
|
2306
|
+
|
|
2307
|
+
def decide(
|
|
2308
|
+
self,
|
|
2309
|
+
query: ContextualSearchQuery,
|
|
2310
|
+
snapshot: ContextualSearchSnapshot,
|
|
2311
|
+
) -> ContextualSearchDecision:
|
|
2312
|
+
if type(query) is not ContextualSearchQuery:
|
|
2313
|
+
raise TypeError("query must be exact ContextualSearchQuery")
|
|
2314
|
+
if type(snapshot) is not ContextualSearchSnapshot:
|
|
2315
|
+
raise TypeError("snapshot must be exact ContextualSearchSnapshot")
|
|
2316
|
+
query.__post_init__()
|
|
2317
|
+
snapshot.__post_init__()
|
|
2318
|
+
phase = _phase(query, snapshot)
|
|
2319
|
+
source_allocations = _allocate(
|
|
2320
|
+
snapshot.source_posteriors,
|
|
2321
|
+
slots=query.real_evaluation_slots,
|
|
2322
|
+
phase=phase,
|
|
2323
|
+
incumbent_arm_id=query.incumbent_source_id,
|
|
2324
|
+
incumbent_prior_mass=0.50,
|
|
2325
|
+
empirical_capability=query.source_count_capability,
|
|
2326
|
+
)
|
|
2327
|
+
operator_allocations = _allocate(
|
|
2328
|
+
snapshot.operator_posteriors,
|
|
2329
|
+
slots=query.real_evaluation_slots,
|
|
2330
|
+
phase=phase,
|
|
2331
|
+
incumbent_arm_id=query.incumbent_operator_id,
|
|
2332
|
+
incumbent_prior_mass=0.75,
|
|
2333
|
+
empirical_capability=query.operator_count_capability,
|
|
2334
|
+
)
|
|
2335
|
+
(
|
|
2336
|
+
source_allocations,
|
|
2337
|
+
operator_allocations,
|
|
2338
|
+
joint_capability_selection,
|
|
2339
|
+
) = _project_joint_capability_product(
|
|
2340
|
+
source_allocations,
|
|
2341
|
+
operator_allocations,
|
|
2342
|
+
query.joint_count_capabilities,
|
|
2343
|
+
)
|
|
2344
|
+
return ContextualSearchDecision(
|
|
2345
|
+
query=query,
|
|
2346
|
+
snapshot=snapshot,
|
|
2347
|
+
phase=phase,
|
|
2348
|
+
source_allocations=source_allocations,
|
|
2349
|
+
operator_allocations=operator_allocations,
|
|
2350
|
+
joint_capability_selection=joint_capability_selection,
|
|
2351
|
+
)
|
|
2352
|
+
|
|
2353
|
+
|
|
2354
|
+
__all__ = [
|
|
2355
|
+
"CONTEXTUAL_SEARCH_CONTROLLER_DEFINITION_SHA256",
|
|
2356
|
+
"CONTEXTUAL_SEARCH_CONTROLLER_ID",
|
|
2357
|
+
"CONTEXTUAL_SEARCH_CONTROLLER_VERSION",
|
|
2358
|
+
"ContextualArmAllocation",
|
|
2359
|
+
"ContextualArmPosterior",
|
|
2360
|
+
"ContextualPortfolioAllocationSlice",
|
|
2361
|
+
"ContextualSearchCompletionAudit",
|
|
2362
|
+
"ContextualSearchDecision",
|
|
2363
|
+
"ContextualSearchDelayedCredit",
|
|
2364
|
+
"ContextualSearchLedger",
|
|
2365
|
+
"ContextualSearchObservation",
|
|
2366
|
+
"ContextualSearchQuery",
|
|
2367
|
+
"ContextualSearchSnapshot",
|
|
2368
|
+
"ContextualSearchStageAllocation",
|
|
2369
|
+
"PhaseAwareContextualSearchController",
|
|
2370
|
+
"SearchArmKind",
|
|
2371
|
+
"SearchPhase",
|
|
2372
|
+
"audit_completed_contextual_search_ledger",
|
|
2373
|
+
"slice_contextual_search_decision",
|
|
2374
|
+
]
|