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,2328 @@
|
|
|
1
|
+
"""Workload-blind brokerage over already-materialized evolutionary actions.
|
|
2
|
+
|
|
3
|
+
The broker is deliberately downstream of proposal generation. Workload
|
|
4
|
+
adapters and proposal experts own legality and materialization; this module
|
|
5
|
+
sees only authenticated configurations, generic state cells, lineage, and
|
|
6
|
+
strictly prior outcomes. Mutation, restart, acquisition, and recombination
|
|
7
|
+
therefore compete for the same expensive evaluation slots without exposing a
|
|
8
|
+
workload or model identifier to the policy.
|
|
9
|
+
|
|
10
|
+
Outcome channels remain orthogonal diagnostics. Selection itself uses one
|
|
11
|
+
currency: normalized archive return resolved over an authenticated lineage
|
|
12
|
+
horizon. Feasibility and realization gate that return, forecast error governs
|
|
13
|
+
the authority of the consequence model, and uncertainty is used only for
|
|
14
|
+
nonterminal exploration. Sparse state cells shrink toward an arm-level
|
|
15
|
+
posterior before a joint-slate optimizer adds only residual complementarity.
|
|
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
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from itertools import combinations
|
|
27
|
+
from typing import Protocol, runtime_checkable
|
|
28
|
+
|
|
29
|
+
from agent_evolve.application.contextual_search_controller import SearchPhase
|
|
30
|
+
from agent_evolve.application.outcome_adaptive_action_racing import (
|
|
31
|
+
AdaptiveActionAllocationDirective,
|
|
32
|
+
)
|
|
33
|
+
from agent_evolve.domain.ids import CandidateId
|
|
34
|
+
from agent_evolve.domain.patch import require_sha256
|
|
35
|
+
from agent_evolve.domain.typed_json import (
|
|
36
|
+
FrozenJsonObject,
|
|
37
|
+
freeze_json,
|
|
38
|
+
thaw_json,
|
|
39
|
+
typed_json_sha256,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
MATERIALIZED_ACTION_BROKER_ID = "regret_brokered_expert_evolution"
|
|
44
|
+
MATERIALIZED_ACTION_BROKER_VERSION = 8
|
|
45
|
+
EMPIRICAL_RETURN_ESTIMATOR_DEFINITION_SHA256 = hashlib.sha256(
|
|
46
|
+
b"agent-evolve:materialized-action-empirical-return:v1"
|
|
47
|
+
).hexdigest()
|
|
48
|
+
MATERIALIZED_ACTION_BROKER_DEFINITION_SHA256 = hashlib.sha256(
|
|
49
|
+
b"agent-evolve:regret-brokered-expert-evolution:v8;"
|
|
50
|
+
b"decision-unit=authenticated-materialized-action;"
|
|
51
|
+
b"axes=expert,native-rank,parent-arity,operator,target,role;"
|
|
52
|
+
b"state=residual-cell,parent-cell,archive-cell,structural-signature,"
|
|
53
|
+
b"patch-compatibility,phase,horizon,calibration,source-distance,memory-dose;"
|
|
54
|
+
b"forbidden-inputs=workload-id,model-id,provider-id,objective-name;"
|
|
55
|
+
b"selection-currency=normalized-resolved-lineage-return;"
|
|
56
|
+
b"diagnostic-channels=gain,positive,stage-survival,terminal-persistence,"
|
|
57
|
+
b"descendant,feasibility,forecast-error,realization;"
|
|
58
|
+
b"return-estimator=injected-port-or-zero-baseline-bounded-empirical-mean-with-arm-hierarchical-shrinkage;"
|
|
59
|
+
b"cold-start-tie=within-expert-native-rank-then-expert-diversity;"
|
|
60
|
+
b"uncertainty=distribution-free-maximum-standard-error;"
|
|
61
|
+
b"selection=additive-return-plus-residual-complementarity;"
|
|
62
|
+
b"allocation=optional-authenticated-static-requirement-or-outcome-"
|
|
63
|
+
b"adaptive-directive;"
|
|
64
|
+
b"adaptive-allocation=prior-decision-and-observed-outcome-bound;"
|
|
65
|
+
b"exploration=authenticated-prequential-required-set-plus-at-most-one-"
|
|
66
|
+
b"posterior-nonterminal-arm;"
|
|
67
|
+
b"terminal-information-bonus=zero;"
|
|
68
|
+
b"reference=bounded-multislot-conservative-escrow;"
|
|
69
|
+
b"duplicate-constraint=one-evaluation-per-phenotype;"
|
|
70
|
+
b"bounded-beam-prefix-gate=canonical-suffix-unique-phenotype-"
|
|
71
|
+
b"completion-witness;deterministic=true"
|
|
72
|
+
).hexdigest()
|
|
73
|
+
|
|
74
|
+
_TOKEN = re.compile(r"^[a-z][a-z0-9_.:-]{0,255}$")
|
|
75
|
+
_CONTEXT_DOMAIN = b"agent-evolve:materialized-action-context:v1\x00"
|
|
76
|
+
_ACTION_DOMAIN = b"agent-evolve:materialized-action-descriptor:v1\x00"
|
|
77
|
+
_OUTCOME_DOMAIN = b"agent-evolve:materialized-action-outcome:v1\x00"
|
|
78
|
+
_CREDIT_DOMAIN = b"agent-evolve:materialized-action-delayed-credit:v1\x00"
|
|
79
|
+
_RESOLVED_RETURN_DOMAIN = b"agent-evolve:materialized-action-resolved-return:v1\x00"
|
|
80
|
+
_RETURN_PRIOR_PREDICTION_DOMAIN = (
|
|
81
|
+
b"agent-evolve:materialized-action-return-prior-prediction:v1\x00"
|
|
82
|
+
)
|
|
83
|
+
_EMPIRICAL_BAYES_RETURN_VALUE_DOMAIN = (
|
|
84
|
+
b"agent-evolve:empirical-bayes-materialized-action-return-value:v1\x00"
|
|
85
|
+
)
|
|
86
|
+
_ACTION_OPPORTUNITY_EVIDENCE_DOMAIN = (
|
|
87
|
+
b"agent-evolve:materialized-action-opportunity-evidence:v1\x00"
|
|
88
|
+
)
|
|
89
|
+
_OPPORTUNITY_CONDITIONED_RETURN_VALUE_DOMAIN = (
|
|
90
|
+
b"agent-evolve:opportunity-conditioned-materialized-action-return-value:v1\x00"
|
|
91
|
+
)
|
|
92
|
+
_EXPLORATION_REQUIREMENT_DOMAIN = (
|
|
93
|
+
b"agent-evolve:materialized-action-exploration-requirement:v1\x00"
|
|
94
|
+
)
|
|
95
|
+
_ALLOCATION_REQUIREMENT_DOMAIN = (
|
|
96
|
+
b"agent-evolve:materialized-action-allocation-requirement:v1\x00"
|
|
97
|
+
)
|
|
98
|
+
_DECISION_DOMAIN = b"agent-evolve:materialized-action-broker-decision:v1\x00"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _canonical_json(value: object) -> bytes:
|
|
102
|
+
return json.dumps(
|
|
103
|
+
value,
|
|
104
|
+
allow_nan=False,
|
|
105
|
+
ensure_ascii=True,
|
|
106
|
+
separators=(",", ":"),
|
|
107
|
+
sort_keys=True,
|
|
108
|
+
).encode("ascii", errors="strict")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _hash(domain: bytes, value: object) -> str:
|
|
112
|
+
return hashlib.sha256(domain + _canonical_json(value)).hexdigest()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _require_token(value: str, *, name: str) -> None:
|
|
116
|
+
if type(value) is not str or _TOKEN.fullmatch(value) is None:
|
|
117
|
+
raise ValueError(f"{name} must use the closed token grammar")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _require_probability(value: float, *, name: str) -> None:
|
|
121
|
+
if type(value) is not float or not math.isfinite(value):
|
|
122
|
+
raise TypeError(f"{name} must be a finite exact float")
|
|
123
|
+
if not 0.0 <= value <= 1.0:
|
|
124
|
+
raise ValueError(f"{name} must lie in [0, 1]")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _require_nonnegative_finite(value: float, *, name: str) -> None:
|
|
128
|
+
if type(value) is not float or not math.isfinite(value):
|
|
129
|
+
raise TypeError(f"{name} must be a finite exact float")
|
|
130
|
+
if value < 0.0:
|
|
131
|
+
raise ValueError(f"{name} must be non-negative")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _candidate_record(value: CandidateId) -> str:
|
|
135
|
+
if type(value) is not CandidateId:
|
|
136
|
+
raise TypeError("parent and target IDs must be exact CandidateId values")
|
|
137
|
+
CandidateId.__post_init__(value)
|
|
138
|
+
return value.value
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True, slots=True)
|
|
142
|
+
class MaterializedActionContext:
|
|
143
|
+
"""Bounded generic state visible to the action broker."""
|
|
144
|
+
|
|
145
|
+
campaign_scope_sha256: str
|
|
146
|
+
decision_index: int
|
|
147
|
+
phase: SearchPhase
|
|
148
|
+
remaining_decisions: int
|
|
149
|
+
remaining_evaluations: int
|
|
150
|
+
residual_frontier_cell: str
|
|
151
|
+
parent_position_cell: str
|
|
152
|
+
archive_relation_cell: str
|
|
153
|
+
structural_signature_sha256: str
|
|
154
|
+
patch_compatibility_cell: str
|
|
155
|
+
forecast_calibration_cell: str
|
|
156
|
+
source_distance_bin: int
|
|
157
|
+
memory_dose_bin: int
|
|
158
|
+
state_signature_sha256: str = field(init=False)
|
|
159
|
+
context_sha256: str = field(init=False)
|
|
160
|
+
|
|
161
|
+
def __post_init__(self) -> None:
|
|
162
|
+
require_sha256(self.campaign_scope_sha256, "campaign_scope_sha256")
|
|
163
|
+
require_sha256(
|
|
164
|
+
self.structural_signature_sha256,
|
|
165
|
+
"structural_signature_sha256",
|
|
166
|
+
)
|
|
167
|
+
if type(self.decision_index) is not int or self.decision_index <= 0:
|
|
168
|
+
raise ValueError("decision_index must be a positive exact integer")
|
|
169
|
+
if type(self.phase) is not SearchPhase:
|
|
170
|
+
raise TypeError("phase must be an exact SearchPhase")
|
|
171
|
+
for name in ("remaining_decisions", "remaining_evaluations"):
|
|
172
|
+
value = getattr(self, name)
|
|
173
|
+
if type(value) is not int or value <= 0:
|
|
174
|
+
raise ValueError(f"{name} must be a positive exact integer")
|
|
175
|
+
for name in (
|
|
176
|
+
"residual_frontier_cell",
|
|
177
|
+
"parent_position_cell",
|
|
178
|
+
"archive_relation_cell",
|
|
179
|
+
"patch_compatibility_cell",
|
|
180
|
+
"forecast_calibration_cell",
|
|
181
|
+
):
|
|
182
|
+
_require_token(getattr(self, name), name=name)
|
|
183
|
+
for name in ("source_distance_bin", "memory_dose_bin"):
|
|
184
|
+
value = getattr(self, name)
|
|
185
|
+
if type(value) is not int or not 0 <= value <= 15:
|
|
186
|
+
raise ValueError(f"{name} must lie in [0, 15]")
|
|
187
|
+
state = self._state_record()
|
|
188
|
+
object.__setattr__(
|
|
189
|
+
self,
|
|
190
|
+
"state_signature_sha256",
|
|
191
|
+
_hash(_CONTEXT_DOMAIN, state),
|
|
192
|
+
)
|
|
193
|
+
object.__setattr__(
|
|
194
|
+
self,
|
|
195
|
+
"context_sha256",
|
|
196
|
+
_hash(
|
|
197
|
+
_CONTEXT_DOMAIN,
|
|
198
|
+
{
|
|
199
|
+
**state,
|
|
200
|
+
"campaign_scope_sha256": self.campaign_scope_sha256,
|
|
201
|
+
"decision_index": self.decision_index,
|
|
202
|
+
},
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _state_record(self) -> dict[str, object]:
|
|
207
|
+
return {
|
|
208
|
+
"schema_version": 1,
|
|
209
|
+
"phase": self.phase.value,
|
|
210
|
+
"remaining_decisions": self.remaining_decisions,
|
|
211
|
+
"remaining_evaluations": self.remaining_evaluations,
|
|
212
|
+
"residual_frontier_cell": self.residual_frontier_cell,
|
|
213
|
+
"parent_position_cell": self.parent_position_cell,
|
|
214
|
+
"archive_relation_cell": self.archive_relation_cell,
|
|
215
|
+
"structural_signature_sha256": self.structural_signature_sha256,
|
|
216
|
+
"patch_compatibility_cell": self.patch_compatibility_cell,
|
|
217
|
+
"forecast_calibration_cell": self.forecast_calibration_cell,
|
|
218
|
+
"source_distance_bin": self.source_distance_bin,
|
|
219
|
+
"memory_dose_bin": self.memory_dose_bin,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def to_record(self) -> dict[str, object]:
|
|
223
|
+
self.__post_init__()
|
|
224
|
+
return {
|
|
225
|
+
**self._state_record(),
|
|
226
|
+
"campaign_scope_sha256": self.campaign_scope_sha256,
|
|
227
|
+
"decision_index": self.decision_index,
|
|
228
|
+
"state_signature_sha256": self.state_signature_sha256,
|
|
229
|
+
"context_sha256": self.context_sha256,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(frozen=True, slots=True)
|
|
234
|
+
class MaterializedActionDescriptor:
|
|
235
|
+
"""One legal route to one fully materialized candidate configuration."""
|
|
236
|
+
|
|
237
|
+
context: MaterializedActionContext
|
|
238
|
+
configuration: FrozenJsonObject
|
|
239
|
+
phenotype_identity_sha256: str
|
|
240
|
+
expert_id: str
|
|
241
|
+
native_rank: int
|
|
242
|
+
parent_ids: tuple[CandidateId, ...]
|
|
243
|
+
operator_id: str
|
|
244
|
+
target_candidate_id: CandidateId
|
|
245
|
+
role_id: str
|
|
246
|
+
normalized_evaluation_cost: float
|
|
247
|
+
reference_action: bool = False
|
|
248
|
+
action_sha256: str = field(init=False)
|
|
249
|
+
|
|
250
|
+
def __post_init__(self) -> None:
|
|
251
|
+
if type(self.context) is not MaterializedActionContext:
|
|
252
|
+
raise TypeError("context must be an exact MaterializedActionContext")
|
|
253
|
+
MaterializedActionContext.__post_init__(self.context)
|
|
254
|
+
if type(self.configuration) is not FrozenJsonObject:
|
|
255
|
+
raise TypeError("configuration must be an exact FrozenJsonObject")
|
|
256
|
+
require_sha256(self.phenotype_identity_sha256, "phenotype_identity_sha256")
|
|
257
|
+
_require_token(self.expert_id, name="expert_id")
|
|
258
|
+
_require_token(self.operator_id, name="operator_id")
|
|
259
|
+
_require_token(self.role_id, name="role_id")
|
|
260
|
+
if type(self.native_rank) is not int or self.native_rank <= 0:
|
|
261
|
+
raise ValueError("native_rank must be a positive exact integer")
|
|
262
|
+
if type(self.parent_ids) is not tuple or len(self.parent_ids) > 8:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
"parent_ids must be an exact tuple with arity at most eight"
|
|
265
|
+
)
|
|
266
|
+
parent_values = tuple(_candidate_record(value) for value in self.parent_ids)
|
|
267
|
+
if len(parent_values) != len(set(parent_values)):
|
|
268
|
+
raise ValueError("parent_ids must be unique")
|
|
269
|
+
_candidate_record(self.target_candidate_id)
|
|
270
|
+
if self.target_candidate_id in self.parent_ids:
|
|
271
|
+
raise ValueError("target_candidate_id cannot be one of its parents")
|
|
272
|
+
_require_probability(
|
|
273
|
+
self.normalized_evaluation_cost,
|
|
274
|
+
name="normalized_evaluation_cost",
|
|
275
|
+
)
|
|
276
|
+
if type(self.reference_action) is not bool:
|
|
277
|
+
raise TypeError("reference_action must be an exact bool")
|
|
278
|
+
object.__setattr__(
|
|
279
|
+
self,
|
|
280
|
+
"action_sha256",
|
|
281
|
+
_hash(_ACTION_DOMAIN, self._unsigned_record()),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def parent_arity(self) -> int:
|
|
286
|
+
return len(self.parent_ids)
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def configuration_sha256(self) -> str:
|
|
290
|
+
return typed_json_sha256(self.configuration)
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def arm_key(self) -> tuple[str, str, int]:
|
|
294
|
+
return (self.expert_id, self.operator_id, self.parent_arity)
|
|
295
|
+
|
|
296
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
297
|
+
return {
|
|
298
|
+
"schema_version": 1,
|
|
299
|
+
"context_sha256": self.context.context_sha256,
|
|
300
|
+
"state_signature_sha256": self.context.state_signature_sha256,
|
|
301
|
+
"configuration_sha256": self.configuration_sha256,
|
|
302
|
+
"phenotype_identity_sha256": self.phenotype_identity_sha256,
|
|
303
|
+
"expert_id": self.expert_id,
|
|
304
|
+
"native_rank": self.native_rank,
|
|
305
|
+
"parent_ids": [_candidate_record(value) for value in self.parent_ids],
|
|
306
|
+
"parent_arity": self.parent_arity,
|
|
307
|
+
"operator_id": self.operator_id,
|
|
308
|
+
"target_candidate_id": _candidate_record(self.target_candidate_id),
|
|
309
|
+
"role_id": self.role_id,
|
|
310
|
+
"normalized_evaluation_cost_hex": (self.normalized_evaluation_cost.hex()),
|
|
311
|
+
"reference_action": self.reference_action,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
def to_record(self, *, include_configuration: bool = False) -> dict[str, object]:
|
|
315
|
+
self.__post_init__()
|
|
316
|
+
record = {**self._unsigned_record(), "action_sha256": self.action_sha256}
|
|
317
|
+
if include_configuration:
|
|
318
|
+
record["configuration"] = self.configuration
|
|
319
|
+
return record
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass(frozen=True, slots=True)
|
|
323
|
+
class MaterializedActionOutcome:
|
|
324
|
+
"""Immediate, append-only evidence for one requested broker action."""
|
|
325
|
+
|
|
326
|
+
action: MaterializedActionDescriptor
|
|
327
|
+
realized: bool
|
|
328
|
+
feasible: bool | None
|
|
329
|
+
normalized_archive_gain: float | None
|
|
330
|
+
positive_marginal_utility: bool | None
|
|
331
|
+
normalized_forecast_error: float | None = None
|
|
332
|
+
outcome_sha256: str = field(init=False)
|
|
333
|
+
|
|
334
|
+
def __post_init__(self) -> None:
|
|
335
|
+
if type(self.action) is not MaterializedActionDescriptor:
|
|
336
|
+
raise TypeError("action must be an exact MaterializedActionDescriptor")
|
|
337
|
+
MaterializedActionDescriptor.__post_init__(self.action)
|
|
338
|
+
if type(self.realized) is not bool:
|
|
339
|
+
raise TypeError("realized must be an exact bool")
|
|
340
|
+
if not self.realized:
|
|
341
|
+
if (
|
|
342
|
+
self.feasible is not None
|
|
343
|
+
or self.normalized_archive_gain is not None
|
|
344
|
+
or self.positive_marginal_utility is not None
|
|
345
|
+
or self.normalized_forecast_error is not None
|
|
346
|
+
):
|
|
347
|
+
raise ValueError("unrealized actions cannot carry evaluator evidence")
|
|
348
|
+
else:
|
|
349
|
+
if type(self.feasible) is not bool:
|
|
350
|
+
raise TypeError("realized actions require an exact feasibility verdict")
|
|
351
|
+
if self.normalized_archive_gain is None:
|
|
352
|
+
raise ValueError("realized actions require normalized archive gain")
|
|
353
|
+
_require_probability(
|
|
354
|
+
self.normalized_archive_gain,
|
|
355
|
+
name="normalized_archive_gain",
|
|
356
|
+
)
|
|
357
|
+
if type(self.positive_marginal_utility) is not bool:
|
|
358
|
+
raise TypeError("realized actions require an exact positive verdict")
|
|
359
|
+
if self.positive_marginal_utility != (self.normalized_archive_gain > 0.0):
|
|
360
|
+
raise ValueError("positive verdict differs from normalized gain")
|
|
361
|
+
if not self.feasible and (
|
|
362
|
+
self.normalized_archive_gain != 0.0 or self.positive_marginal_utility
|
|
363
|
+
):
|
|
364
|
+
raise ValueError("infeasible actions cannot carry positive gain")
|
|
365
|
+
if self.normalized_forecast_error is not None:
|
|
366
|
+
_require_probability(
|
|
367
|
+
self.normalized_forecast_error,
|
|
368
|
+
name="normalized_forecast_error",
|
|
369
|
+
)
|
|
370
|
+
object.__setattr__(
|
|
371
|
+
self,
|
|
372
|
+
"outcome_sha256",
|
|
373
|
+
_hash(_OUTCOME_DOMAIN, self._unsigned_record()),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
377
|
+
return {
|
|
378
|
+
"schema_version": 1,
|
|
379
|
+
"action_sha256": self.action.action_sha256,
|
|
380
|
+
"realized": self.realized,
|
|
381
|
+
"feasible": self.feasible,
|
|
382
|
+
"normalized_archive_gain_hex": (
|
|
383
|
+
None
|
|
384
|
+
if self.normalized_archive_gain is None
|
|
385
|
+
else self.normalized_archive_gain.hex()
|
|
386
|
+
),
|
|
387
|
+
"positive_marginal_utility": self.positive_marginal_utility,
|
|
388
|
+
"normalized_forecast_error_hex": (
|
|
389
|
+
None
|
|
390
|
+
if self.normalized_forecast_error is None
|
|
391
|
+
else self.normalized_forecast_error.hex()
|
|
392
|
+
),
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
def to_record(self) -> dict[str, object]:
|
|
396
|
+
self.__post_init__()
|
|
397
|
+
return {**self._unsigned_record(), "outcome_sha256": self.outcome_sha256}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@dataclass(frozen=True, slots=True)
|
|
401
|
+
class MaterializedActionDelayedCredit:
|
|
402
|
+
"""Later survival or descendant evidence joined to an immediate outcome."""
|
|
403
|
+
|
|
404
|
+
outcome: MaterializedActionOutcome
|
|
405
|
+
available_at_decision_index: int
|
|
406
|
+
stage_front_survived: bool | None = None
|
|
407
|
+
terminal_front_persisted: bool | None = None
|
|
408
|
+
useful_descendant_observed: bool | None = None
|
|
409
|
+
credit_sha256: str = field(init=False)
|
|
410
|
+
|
|
411
|
+
def __post_init__(self) -> None:
|
|
412
|
+
if type(self.outcome) is not MaterializedActionOutcome:
|
|
413
|
+
raise TypeError("outcome must be an exact MaterializedActionOutcome")
|
|
414
|
+
MaterializedActionOutcome.__post_init__(self.outcome)
|
|
415
|
+
if not self.outcome.realized or self.outcome.feasible is not True:
|
|
416
|
+
raise ValueError("delayed credit requires a realized feasible action")
|
|
417
|
+
if (
|
|
418
|
+
type(self.available_at_decision_index) is not int
|
|
419
|
+
or self.available_at_decision_index
|
|
420
|
+
< self.outcome.action.context.decision_index
|
|
421
|
+
):
|
|
422
|
+
raise ValueError("delayed credit cannot precede its source action")
|
|
423
|
+
values = (
|
|
424
|
+
self.stage_front_survived,
|
|
425
|
+
self.terminal_front_persisted,
|
|
426
|
+
self.useful_descendant_observed,
|
|
427
|
+
)
|
|
428
|
+
if all(value is None for value in values):
|
|
429
|
+
raise ValueError("delayed credit must adjudicate at least one channel")
|
|
430
|
+
if any(value is not None and type(value) is not bool for value in values):
|
|
431
|
+
raise TypeError("delayed credit channels must be exact bools or None")
|
|
432
|
+
object.__setattr__(
|
|
433
|
+
self,
|
|
434
|
+
"credit_sha256",
|
|
435
|
+
_hash(_CREDIT_DOMAIN, self._unsigned_record()),
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
439
|
+
return {
|
|
440
|
+
"schema_version": 1,
|
|
441
|
+
"outcome_sha256": self.outcome.outcome_sha256,
|
|
442
|
+
"available_at_decision_index": self.available_at_decision_index,
|
|
443
|
+
"stage_front_survived": self.stage_front_survived,
|
|
444
|
+
"terminal_front_persisted": self.terminal_front_persisted,
|
|
445
|
+
"useful_descendant_observed": self.useful_descendant_observed,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
def to_record(self) -> dict[str, object]:
|
|
449
|
+
self.__post_init__()
|
|
450
|
+
return {**self._unsigned_record(), "credit_sha256": self.credit_sha256}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@dataclass(frozen=True, slots=True)
|
|
454
|
+
class MaterializedActionResolvedReturn:
|
|
455
|
+
"""One append-only resolution of an action's finite-horizon return.
|
|
456
|
+
|
|
457
|
+
A resolution may be refined as descendants become observable. The ledger
|
|
458
|
+
uses only the newest resolution available strictly before the decision it
|
|
459
|
+
is scoring. Components are expressed in the same normalized archive-
|
|
460
|
+
utility currency and must close exactly to ``normalized_horizon_return``.
|
|
461
|
+
The resolver, not the broker, owns lineage attribution and discounting.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
outcome: MaterializedActionOutcome
|
|
465
|
+
available_at_decision_index: int
|
|
466
|
+
horizon_end_decision_index: int
|
|
467
|
+
normalized_immediate_return: float
|
|
468
|
+
normalized_descendant_return: float
|
|
469
|
+
normalized_horizon_return: float
|
|
470
|
+
fully_resolved: bool
|
|
471
|
+
attribution_definition_sha256: str
|
|
472
|
+
return_sha256: str = field(init=False)
|
|
473
|
+
|
|
474
|
+
def __post_init__(self) -> None:
|
|
475
|
+
if type(self.outcome) is not MaterializedActionOutcome:
|
|
476
|
+
raise TypeError("outcome must be an exact MaterializedActionOutcome")
|
|
477
|
+
MaterializedActionOutcome.__post_init__(self.outcome)
|
|
478
|
+
source_index = self.outcome.action.context.decision_index
|
|
479
|
+
if (
|
|
480
|
+
type(self.available_at_decision_index) is not int
|
|
481
|
+
or self.available_at_decision_index <= source_index
|
|
482
|
+
):
|
|
483
|
+
raise ValueError("resolved return must become available after its action")
|
|
484
|
+
if (
|
|
485
|
+
type(self.horizon_end_decision_index) is not int
|
|
486
|
+
or self.horizon_end_decision_index < source_index
|
|
487
|
+
or self.horizon_end_decision_index >= self.available_at_decision_index
|
|
488
|
+
):
|
|
489
|
+
raise ValueError(
|
|
490
|
+
"resolved return horizon is inconsistent with availability"
|
|
491
|
+
)
|
|
492
|
+
for name in (
|
|
493
|
+
"normalized_immediate_return",
|
|
494
|
+
"normalized_descendant_return",
|
|
495
|
+
"normalized_horizon_return",
|
|
496
|
+
):
|
|
497
|
+
_require_probability(getattr(self, name), name=name)
|
|
498
|
+
if not math.isclose(
|
|
499
|
+
self.normalized_immediate_return + self.normalized_descendant_return,
|
|
500
|
+
self.normalized_horizon_return,
|
|
501
|
+
rel_tol=0.0,
|
|
502
|
+
abs_tol=1e-12,
|
|
503
|
+
):
|
|
504
|
+
raise ValueError("resolved return components do not close")
|
|
505
|
+
expected_immediate = (
|
|
506
|
+
0.0 if not self.outcome.realized else self.outcome.normalized_archive_gain
|
|
507
|
+
)
|
|
508
|
+
assert expected_immediate is not None
|
|
509
|
+
if not math.isclose(
|
|
510
|
+
self.normalized_immediate_return,
|
|
511
|
+
expected_immediate,
|
|
512
|
+
rel_tol=0.0,
|
|
513
|
+
abs_tol=1e-12,
|
|
514
|
+
):
|
|
515
|
+
raise ValueError("resolved immediate return differs from its outcome")
|
|
516
|
+
if type(self.fully_resolved) is not bool:
|
|
517
|
+
raise TypeError("fully_resolved must be an exact bool")
|
|
518
|
+
require_sha256(
|
|
519
|
+
self.attribution_definition_sha256,
|
|
520
|
+
"attribution_definition_sha256",
|
|
521
|
+
)
|
|
522
|
+
object.__setattr__(
|
|
523
|
+
self,
|
|
524
|
+
"return_sha256",
|
|
525
|
+
_hash(_RESOLVED_RETURN_DOMAIN, self._unsigned_record()),
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
529
|
+
return {
|
|
530
|
+
"schema_version": 1,
|
|
531
|
+
"outcome_sha256": self.outcome.outcome_sha256,
|
|
532
|
+
"available_at_decision_index": self.available_at_decision_index,
|
|
533
|
+
"horizon_end_decision_index": self.horizon_end_decision_index,
|
|
534
|
+
"normalized_immediate_return_hex": (self.normalized_immediate_return.hex()),
|
|
535
|
+
"normalized_descendant_return_hex": (
|
|
536
|
+
self.normalized_descendant_return.hex()
|
|
537
|
+
),
|
|
538
|
+
"normalized_horizon_return_hex": self.normalized_horizon_return.hex(),
|
|
539
|
+
"fully_resolved": self.fully_resolved,
|
|
540
|
+
"attribution_definition_sha256": self.attribution_definition_sha256,
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
def to_record(self) -> dict[str, object]:
|
|
544
|
+
self.__post_init__()
|
|
545
|
+
return {**self._unsigned_record(), "return_sha256": self.return_sha256}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class BrokerEvidenceChannel(str, Enum):
|
|
549
|
+
GAIN = "gain"
|
|
550
|
+
POSITIVE = "positive"
|
|
551
|
+
STAGE_SURVIVAL = "stage_survival"
|
|
552
|
+
TERMINAL_PERSISTENCE = "terminal_persistence"
|
|
553
|
+
DESCENDANT = "descendant"
|
|
554
|
+
FEASIBILITY = "feasibility"
|
|
555
|
+
FORECAST_ERROR = "forecast_error"
|
|
556
|
+
REALIZATION = "realization"
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@dataclass(frozen=True, slots=True)
|
|
560
|
+
class BrokerChannelEstimate:
|
|
561
|
+
channel: BrokerEvidenceChannel
|
|
562
|
+
mean: float
|
|
563
|
+
standard_deviation: float
|
|
564
|
+
local_count: int
|
|
565
|
+
global_count: int
|
|
566
|
+
local_mean: float
|
|
567
|
+
global_mean: float
|
|
568
|
+
shrinkage_weight: float
|
|
569
|
+
|
|
570
|
+
def __post_init__(self) -> None:
|
|
571
|
+
if type(self.channel) is not BrokerEvidenceChannel:
|
|
572
|
+
raise TypeError("channel must be an exact BrokerEvidenceChannel")
|
|
573
|
+
for name in (
|
|
574
|
+
"mean",
|
|
575
|
+
"standard_deviation",
|
|
576
|
+
"local_mean",
|
|
577
|
+
"global_mean",
|
|
578
|
+
"shrinkage_weight",
|
|
579
|
+
):
|
|
580
|
+
_require_probability(getattr(self, name), name=name)
|
|
581
|
+
for name in ("local_count", "global_count"):
|
|
582
|
+
value = getattr(self, name)
|
|
583
|
+
if type(value) is not int or value < 0:
|
|
584
|
+
raise ValueError(f"{name} must be a non-negative exact integer")
|
|
585
|
+
if self.local_count > self.global_count:
|
|
586
|
+
raise ValueError("local evidence cannot exceed its arm-global evidence")
|
|
587
|
+
|
|
588
|
+
def to_record(self) -> dict[str, object]:
|
|
589
|
+
self.__post_init__()
|
|
590
|
+
return {
|
|
591
|
+
"channel": self.channel.value,
|
|
592
|
+
"mean_hex": self.mean.hex(),
|
|
593
|
+
"standard_deviation_hex": self.standard_deviation.hex(),
|
|
594
|
+
"local_count": self.local_count,
|
|
595
|
+
"global_count": self.global_count,
|
|
596
|
+
"local_mean_hex": self.local_mean.hex(),
|
|
597
|
+
"global_mean_hex": self.global_mean.hex(),
|
|
598
|
+
"shrinkage_weight_hex": self.shrinkage_weight.hex(),
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _bounded_empirical(values: tuple[float, ...]) -> tuple[float, float, float]:
|
|
603
|
+
"""Preserve return scale while retaining a distribution-free error bound."""
|
|
604
|
+
|
|
605
|
+
if not values:
|
|
606
|
+
# Archive return is a non-negative *gain*, not a Bernoulli success
|
|
607
|
+
# probability. A Beta(1, 1)-style mean of 1/2 overwhelms the small
|
|
608
|
+
# gains seen in real campaigns and causes every multi-action slate to
|
|
609
|
+
# saturate. Zero is the only scale-free lower-bound mean; uncertainty
|
|
610
|
+
# remains maximal and may buy one explicit nonterminal probe below.
|
|
611
|
+
return 0.0, 0.5, 0.0
|
|
612
|
+
mean = math.fsum(values) / len(values)
|
|
613
|
+
# Popoviciu's bound gives sigma <= 1/2 for observations in [0, 1].
|
|
614
|
+
standard_error = 0.5 / math.sqrt(len(values))
|
|
615
|
+
return float(mean), float(standard_error), float(len(values))
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@dataclass(frozen=True, slots=True)
|
|
619
|
+
class BrokerReturnEstimate:
|
|
620
|
+
"""Hierarchically shrunk posterior in the sole selection currency."""
|
|
621
|
+
|
|
622
|
+
mean: float
|
|
623
|
+
standard_deviation: float
|
|
624
|
+
local_count: int
|
|
625
|
+
global_count: int
|
|
626
|
+
resolved_count: int
|
|
627
|
+
provisional_count: int
|
|
628
|
+
local_mean: float
|
|
629
|
+
global_mean: float
|
|
630
|
+
shrinkage_weight: float
|
|
631
|
+
|
|
632
|
+
def __post_init__(self) -> None:
|
|
633
|
+
for name in (
|
|
634
|
+
"mean",
|
|
635
|
+
"standard_deviation",
|
|
636
|
+
"local_mean",
|
|
637
|
+
"global_mean",
|
|
638
|
+
"shrinkage_weight",
|
|
639
|
+
):
|
|
640
|
+
_require_probability(getattr(self, name), name=name)
|
|
641
|
+
for name in (
|
|
642
|
+
"local_count",
|
|
643
|
+
"global_count",
|
|
644
|
+
"resolved_count",
|
|
645
|
+
"provisional_count",
|
|
646
|
+
):
|
|
647
|
+
value = getattr(self, name)
|
|
648
|
+
if type(value) is not int or value < 0:
|
|
649
|
+
raise ValueError(f"{name} must be a non-negative exact integer")
|
|
650
|
+
if self.local_count > self.global_count:
|
|
651
|
+
raise ValueError("local return evidence exceeds arm-global evidence")
|
|
652
|
+
if self.resolved_count + self.provisional_count != self.global_count:
|
|
653
|
+
raise ValueError("return resolution counts do not close")
|
|
654
|
+
|
|
655
|
+
def to_record(self) -> dict[str, object]:
|
|
656
|
+
self.__post_init__()
|
|
657
|
+
return {
|
|
658
|
+
"mean_hex": self.mean.hex(),
|
|
659
|
+
"standard_deviation_hex": self.standard_deviation.hex(),
|
|
660
|
+
"local_count": self.local_count,
|
|
661
|
+
"global_count": self.global_count,
|
|
662
|
+
"resolved_count": self.resolved_count,
|
|
663
|
+
"provisional_count": self.provisional_count,
|
|
664
|
+
"local_mean_hex": self.local_mean.hex(),
|
|
665
|
+
"global_mean_hex": self.global_mean.hex(),
|
|
666
|
+
"shrinkage_weight_hex": self.shrinkage_weight.hex(),
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
@runtime_checkable
|
|
671
|
+
class MaterializedActionReturnValuePort(Protocol):
|
|
672
|
+
"""Predict the common evaluator-grounded return of one unseen action.
|
|
673
|
+
|
|
674
|
+
Implementations may wrap a frozen cross-run meta-prior plus branch-local
|
|
675
|
+
prequential updates. The descriptor deliberately contains no workload,
|
|
676
|
+
model, provider, prompt, or objective-name feature, so the orchestration
|
|
677
|
+
core cannot branch on those identities. Returning the same typed estimate
|
|
678
|
+
as the empirical fallback keeps one selection currency and one uncertainty
|
|
679
|
+
contract.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
definition_sha256: str
|
|
683
|
+
|
|
684
|
+
def estimate(
|
|
685
|
+
self, action: MaterializedActionDescriptor
|
|
686
|
+
) -> BrokerReturnEstimate: ...
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
@dataclass(frozen=True, slots=True)
|
|
690
|
+
class MaterializedActionReturnPriorPrediction:
|
|
691
|
+
"""Authenticated portable prior for one not-yet-evaluated action."""
|
|
692
|
+
|
|
693
|
+
action_sha256: str
|
|
694
|
+
mean: float
|
|
695
|
+
standard_deviation: float
|
|
696
|
+
effective_sample_size: float
|
|
697
|
+
evidence_sha256: str
|
|
698
|
+
prediction_sha256: str = field(init=False)
|
|
699
|
+
|
|
700
|
+
def __post_init__(self) -> None:
|
|
701
|
+
require_sha256(self.action_sha256, "action_sha256")
|
|
702
|
+
_require_probability(self.mean, name="mean")
|
|
703
|
+
_require_probability(
|
|
704
|
+
self.standard_deviation,
|
|
705
|
+
name="standard_deviation",
|
|
706
|
+
)
|
|
707
|
+
if (
|
|
708
|
+
type(self.effective_sample_size) is not float
|
|
709
|
+
or not math.isfinite(self.effective_sample_size)
|
|
710
|
+
or self.effective_sample_size <= 0.0
|
|
711
|
+
):
|
|
712
|
+
raise ValueError(
|
|
713
|
+
"effective_sample_size must be a positive finite exact float"
|
|
714
|
+
)
|
|
715
|
+
require_sha256(self.evidence_sha256, "evidence_sha256")
|
|
716
|
+
object.__setattr__(
|
|
717
|
+
self,
|
|
718
|
+
"prediction_sha256",
|
|
719
|
+
_hash(_RETURN_PRIOR_PREDICTION_DOMAIN, self._unsigned_record()),
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
723
|
+
return {
|
|
724
|
+
"schema_version": 1,
|
|
725
|
+
"action_sha256": self.action_sha256,
|
|
726
|
+
"mean_hex": self.mean.hex(),
|
|
727
|
+
"standard_deviation_hex": self.standard_deviation.hex(),
|
|
728
|
+
"effective_sample_size_hex": self.effective_sample_size.hex(),
|
|
729
|
+
"evidence_sha256": self.evidence_sha256,
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
def to_record(self) -> dict[str, object]:
|
|
733
|
+
self.__post_init__()
|
|
734
|
+
return {
|
|
735
|
+
**self._unsigned_record(),
|
|
736
|
+
"prediction_sha256": self.prediction_sha256,
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@runtime_checkable
|
|
741
|
+
class MaterializedActionReturnPriorPort(Protocol):
|
|
742
|
+
"""Project an action into a frozen cross-run return prior.
|
|
743
|
+
|
|
744
|
+
Workload adapters may compute generic numerical features or consult a
|
|
745
|
+
content-addressed feature panel, but the application core receives only a
|
|
746
|
+
common normalized-return distribution. The predictor definition must
|
|
747
|
+
authenticate its feature schema, fit, training cutoff, and evidence panel.
|
|
748
|
+
"""
|
|
749
|
+
|
|
750
|
+
definition_sha256: str
|
|
751
|
+
|
|
752
|
+
def predict(
|
|
753
|
+
self,
|
|
754
|
+
action: MaterializedActionDescriptor,
|
|
755
|
+
) -> MaterializedActionReturnPriorPrediction | None: ...
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
@dataclass(frozen=True, slots=True)
|
|
759
|
+
class MaterializedActionOpportunityEvidence:
|
|
760
|
+
"""Authenticated current-archive opportunity for an action's lineage.
|
|
761
|
+
|
|
762
|
+
``source_opportunity`` and ``archive_opportunity_scale`` use the same
|
|
763
|
+
normalized archive-utility currency as realized return. The former is
|
|
764
|
+
normally the source parent's leave-one-out contribution; the latter is a
|
|
765
|
+
strictly-prior, action-independent scale such as the maximum contribution
|
|
766
|
+
on the current front. Workload adapters own the projection, while the
|
|
767
|
+
broker sees neither objective names nor workload fields.
|
|
768
|
+
"""
|
|
769
|
+
|
|
770
|
+
action_sha256: str
|
|
771
|
+
source_opportunity: float
|
|
772
|
+
archive_opportunity_scale: float
|
|
773
|
+
evidence_sha256: str
|
|
774
|
+
opportunity_sha256: str = field(init=False)
|
|
775
|
+
|
|
776
|
+
def __post_init__(self) -> None:
|
|
777
|
+
require_sha256(self.action_sha256, "action_sha256")
|
|
778
|
+
_require_probability(
|
|
779
|
+
self.source_opportunity,
|
|
780
|
+
name="source_opportunity",
|
|
781
|
+
)
|
|
782
|
+
_require_probability(
|
|
783
|
+
self.archive_opportunity_scale,
|
|
784
|
+
name="archive_opportunity_scale",
|
|
785
|
+
)
|
|
786
|
+
if self.archive_opportunity_scale <= 0.0:
|
|
787
|
+
raise ValueError("archive_opportunity_scale must be positive")
|
|
788
|
+
if self.source_opportunity > self.archive_opportunity_scale:
|
|
789
|
+
raise ValueError(
|
|
790
|
+
"source opportunity cannot exceed the archive opportunity scale"
|
|
791
|
+
)
|
|
792
|
+
require_sha256(self.evidence_sha256, "evidence_sha256")
|
|
793
|
+
object.__setattr__(
|
|
794
|
+
self,
|
|
795
|
+
"opportunity_sha256",
|
|
796
|
+
_hash(
|
|
797
|
+
_ACTION_OPPORTUNITY_EVIDENCE_DOMAIN,
|
|
798
|
+
self._unsigned_record(),
|
|
799
|
+
),
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
@property
|
|
803
|
+
def relative_source_opportunity(self) -> float:
|
|
804
|
+
return self.source_opportunity / self.archive_opportunity_scale
|
|
805
|
+
|
|
806
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
807
|
+
return {
|
|
808
|
+
"schema_version": 1,
|
|
809
|
+
"action_sha256": self.action_sha256,
|
|
810
|
+
"source_opportunity_hex": self.source_opportunity.hex(),
|
|
811
|
+
"archive_opportunity_scale_hex": (self.archive_opportunity_scale.hex()),
|
|
812
|
+
"evidence_sha256": self.evidence_sha256,
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
def to_record(self) -> dict[str, object]:
|
|
816
|
+
self.__post_init__()
|
|
817
|
+
return {
|
|
818
|
+
**self._unsigned_record(),
|
|
819
|
+
"relative_source_opportunity_hex": (self.relative_source_opportunity.hex()),
|
|
820
|
+
"opportunity_sha256": self.opportunity_sha256,
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
@runtime_checkable
|
|
825
|
+
class MaterializedActionOpportunityPort(Protocol):
|
|
826
|
+
"""Project strictly-prior archive geometry into one portable scalar."""
|
|
827
|
+
|
|
828
|
+
definition_sha256: str
|
|
829
|
+
|
|
830
|
+
def estimate(
|
|
831
|
+
self,
|
|
832
|
+
action: MaterializedActionDescriptor,
|
|
833
|
+
) -> MaterializedActionOpportunityEvidence | None: ...
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
@dataclass(frozen=True, slots=True)
|
|
837
|
+
class OpportunityConditionedMaterializedActionReturnValue:
|
|
838
|
+
"""Condition return on source opportunity and cap uncertainty in its units.
|
|
839
|
+
|
|
840
|
+
Parent opportunity is bounded evidence about the size of the source basin,
|
|
841
|
+
not outcome credit. It can multiply the predicted mean by at most
|
|
842
|
+
``maximum_parent_multiplier``. The same current-archive scale caps the
|
|
843
|
+
standard deviation that otherwise defaults to a unit-interval worst case,
|
|
844
|
+
preventing an unobserved arm from receiving orders-of-magnitude more
|
|
845
|
+
information value solely because its prior is absent.
|
|
846
|
+
"""
|
|
847
|
+
|
|
848
|
+
base: MaterializedActionReturnValuePort = field(
|
|
849
|
+
repr=False,
|
|
850
|
+
compare=False,
|
|
851
|
+
)
|
|
852
|
+
opportunity: MaterializedActionOpportunityPort = field(
|
|
853
|
+
repr=False,
|
|
854
|
+
compare=False,
|
|
855
|
+
)
|
|
856
|
+
maximum_parent_multiplier: float = 2.0
|
|
857
|
+
definition_sha256: str = field(init=False)
|
|
858
|
+
|
|
859
|
+
def __post_init__(self) -> None:
|
|
860
|
+
if not isinstance(self.base, MaterializedActionReturnValuePort):
|
|
861
|
+
raise TypeError("base must implement MaterializedActionReturnValuePort")
|
|
862
|
+
if not isinstance(
|
|
863
|
+
self.opportunity,
|
|
864
|
+
MaterializedActionOpportunityPort,
|
|
865
|
+
):
|
|
866
|
+
raise TypeError(
|
|
867
|
+
"opportunity must implement MaterializedActionOpportunityPort"
|
|
868
|
+
)
|
|
869
|
+
require_sha256(self.base.definition_sha256, "base definition_sha256")
|
|
870
|
+
require_sha256(
|
|
871
|
+
self.opportunity.definition_sha256,
|
|
872
|
+
"opportunity definition_sha256",
|
|
873
|
+
)
|
|
874
|
+
if (
|
|
875
|
+
type(self.maximum_parent_multiplier) is not float
|
|
876
|
+
or not math.isfinite(self.maximum_parent_multiplier)
|
|
877
|
+
or not 1.0 <= self.maximum_parent_multiplier <= 2.0
|
|
878
|
+
):
|
|
879
|
+
raise ValueError(
|
|
880
|
+
"maximum_parent_multiplier must be a finite float in [1, 2]"
|
|
881
|
+
)
|
|
882
|
+
object.__setattr__(
|
|
883
|
+
self,
|
|
884
|
+
"definition_sha256",
|
|
885
|
+
_hash(
|
|
886
|
+
_OPPORTUNITY_CONDITIONED_RETURN_VALUE_DOMAIN,
|
|
887
|
+
{
|
|
888
|
+
"schema_version": 1,
|
|
889
|
+
"base_definition_sha256": self.base.definition_sha256,
|
|
890
|
+
"opportunity_definition_sha256": (
|
|
891
|
+
self.opportunity.definition_sha256
|
|
892
|
+
),
|
|
893
|
+
"maximum_parent_multiplier_hex": (
|
|
894
|
+
self.maximum_parent_multiplier.hex()
|
|
895
|
+
),
|
|
896
|
+
"mean": (
|
|
897
|
+
"base_mean_times_one_plus_bounded_relative_source_opportunity"
|
|
898
|
+
),
|
|
899
|
+
"uncertainty": (
|
|
900
|
+
"minimum_of_base_standard_deviation_and_maximum_of_"
|
|
901
|
+
"conditioned_mean_and_archive_opportunity_scale"
|
|
902
|
+
),
|
|
903
|
+
"strictly_prior_only": True,
|
|
904
|
+
"workload_model_provider_branches": False,
|
|
905
|
+
},
|
|
906
|
+
),
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
def estimate(
|
|
910
|
+
self,
|
|
911
|
+
action: MaterializedActionDescriptor,
|
|
912
|
+
) -> BrokerReturnEstimate:
|
|
913
|
+
if type(action) is not MaterializedActionDescriptor:
|
|
914
|
+
raise TypeError("action must be an exact MaterializedActionDescriptor")
|
|
915
|
+
base = self.base.estimate(action)
|
|
916
|
+
if type(base) is not BrokerReturnEstimate:
|
|
917
|
+
raise TypeError("base return value produced a foreign estimate")
|
|
918
|
+
base.__post_init__()
|
|
919
|
+
opportunity = self.opportunity.estimate(action)
|
|
920
|
+
if opportunity is None:
|
|
921
|
+
return base
|
|
922
|
+
if type(opportunity) is not MaterializedActionOpportunityEvidence:
|
|
923
|
+
raise TypeError("opportunity port produced foreign evidence")
|
|
924
|
+
opportunity.__post_init__()
|
|
925
|
+
if opportunity.action_sha256 != action.action_sha256:
|
|
926
|
+
raise ValueError("opportunity evidence identifies another action")
|
|
927
|
+
multiplier = (
|
|
928
|
+
1.0
|
|
929
|
+
+ (self.maximum_parent_multiplier - 1.0)
|
|
930
|
+
* opportunity.relative_source_opportunity
|
|
931
|
+
)
|
|
932
|
+
mean = min(1.0, base.mean * multiplier)
|
|
933
|
+
uncertainty_cap = max(
|
|
934
|
+
opportunity.archive_opportunity_scale,
|
|
935
|
+
mean,
|
|
936
|
+
)
|
|
937
|
+
return BrokerReturnEstimate(
|
|
938
|
+
mean=float(mean),
|
|
939
|
+
standard_deviation=float(min(base.standard_deviation, uncertainty_cap)),
|
|
940
|
+
local_count=base.local_count,
|
|
941
|
+
global_count=base.global_count,
|
|
942
|
+
resolved_count=base.resolved_count,
|
|
943
|
+
provisional_count=base.provisional_count,
|
|
944
|
+
local_mean=base.local_mean,
|
|
945
|
+
global_mean=base.global_mean,
|
|
946
|
+
shrinkage_weight=base.shrinkage_weight,
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
@dataclass(frozen=True, slots=True)
|
|
951
|
+
class EmpiricalBayesMaterializedActionReturnValue:
|
|
952
|
+
"""Fuse a frozen portable meta-prior with strictly prior live outcomes."""
|
|
953
|
+
|
|
954
|
+
ledger: "MaterializedActionEvidenceLedger" = field(
|
|
955
|
+
repr=False,
|
|
956
|
+
compare=False,
|
|
957
|
+
)
|
|
958
|
+
prior: MaterializedActionReturnPriorPort = field(
|
|
959
|
+
repr=False,
|
|
960
|
+
compare=False,
|
|
961
|
+
)
|
|
962
|
+
hierarchical_kappa: float = 4.0
|
|
963
|
+
definition_sha256: str = field(init=False)
|
|
964
|
+
|
|
965
|
+
def __post_init__(self) -> None:
|
|
966
|
+
if type(self.ledger) is not MaterializedActionEvidenceLedger:
|
|
967
|
+
raise TypeError("ledger must be an exact MaterializedActionEvidenceLedger")
|
|
968
|
+
if not isinstance(self.prior, MaterializedActionReturnPriorPort):
|
|
969
|
+
raise TypeError("prior must implement MaterializedActionReturnPriorPort")
|
|
970
|
+
require_sha256(self.prior.definition_sha256, "prior definition_sha256")
|
|
971
|
+
if (
|
|
972
|
+
type(self.hierarchical_kappa) is not float
|
|
973
|
+
or not math.isfinite(self.hierarchical_kappa)
|
|
974
|
+
or self.hierarchical_kappa <= 0.0
|
|
975
|
+
):
|
|
976
|
+
raise ValueError("hierarchical_kappa must be a positive finite float")
|
|
977
|
+
object.__setattr__(
|
|
978
|
+
self,
|
|
979
|
+
"definition_sha256",
|
|
980
|
+
_hash(
|
|
981
|
+
_EMPIRICAL_BAYES_RETURN_VALUE_DOMAIN,
|
|
982
|
+
{
|
|
983
|
+
"schema_version": 1,
|
|
984
|
+
"prior_definition_sha256": self.prior.definition_sha256,
|
|
985
|
+
"hierarchical_kappa_hex": self.hierarchical_kappa.hex(),
|
|
986
|
+
"fusion": (
|
|
987
|
+
"effective-sample-size-weighted-mean-and-total-variance"
|
|
988
|
+
),
|
|
989
|
+
"live_evidence_cutoff": "strictly_prior_decision_index",
|
|
990
|
+
"workload_model_provider_branches": False,
|
|
991
|
+
},
|
|
992
|
+
),
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
def estimate(
|
|
996
|
+
self,
|
|
997
|
+
action: MaterializedActionDescriptor,
|
|
998
|
+
) -> BrokerReturnEstimate:
|
|
999
|
+
if type(action) is not MaterializedActionDescriptor:
|
|
1000
|
+
raise TypeError("action must be an exact MaterializedActionDescriptor")
|
|
1001
|
+
empirical = self.ledger.estimate_return(
|
|
1002
|
+
action,
|
|
1003
|
+
kappa=self.hierarchical_kappa,
|
|
1004
|
+
)
|
|
1005
|
+
prediction = self.prior.predict(action)
|
|
1006
|
+
if prediction is None:
|
|
1007
|
+
return empirical
|
|
1008
|
+
if type(prediction) is not MaterializedActionReturnPriorPrediction:
|
|
1009
|
+
raise TypeError("return prior produced a foreign prediction")
|
|
1010
|
+
prediction.__post_init__()
|
|
1011
|
+
if prediction.action_sha256 != action.action_sha256:
|
|
1012
|
+
raise ValueError("return prior prediction identifies another action")
|
|
1013
|
+
|
|
1014
|
+
empirical_weight = empirical.global_count / (
|
|
1015
|
+
empirical.global_count + prediction.effective_sample_size
|
|
1016
|
+
)
|
|
1017
|
+
prior_weight = 1.0 - empirical_weight
|
|
1018
|
+
mean = prior_weight * prediction.mean + empirical_weight * empirical.mean
|
|
1019
|
+
variance = prior_weight * (
|
|
1020
|
+
prediction.standard_deviation**2 + (prediction.mean - mean) ** 2
|
|
1021
|
+
) + empirical_weight * (
|
|
1022
|
+
empirical.standard_deviation**2 + (empirical.mean - mean) ** 2
|
|
1023
|
+
)
|
|
1024
|
+
return BrokerReturnEstimate(
|
|
1025
|
+
mean=float(min(1.0, max(0.0, mean))),
|
|
1026
|
+
standard_deviation=float(min(1.0, math.sqrt(max(0.0, variance)))),
|
|
1027
|
+
local_count=empirical.local_count,
|
|
1028
|
+
global_count=empirical.global_count,
|
|
1029
|
+
resolved_count=empirical.resolved_count,
|
|
1030
|
+
provisional_count=empirical.provisional_count,
|
|
1031
|
+
local_mean=empirical.local_mean,
|
|
1032
|
+
global_mean=empirical.global_mean,
|
|
1033
|
+
shrinkage_weight=empirical.shrinkage_weight,
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
@dataclass(slots=True)
|
|
1038
|
+
class MaterializedActionEvidenceLedger:
|
|
1039
|
+
"""Append-only immediate and delayed evidence used by the broker."""
|
|
1040
|
+
|
|
1041
|
+
outcomes: list[MaterializedActionOutcome] = field(default_factory=list)
|
|
1042
|
+
delayed_credits: list[MaterializedActionDelayedCredit] = field(default_factory=list)
|
|
1043
|
+
resolved_returns: list[MaterializedActionResolvedReturn] = field(
|
|
1044
|
+
default_factory=list
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
def append_outcome(self, value: MaterializedActionOutcome) -> None:
|
|
1048
|
+
if type(value) is not MaterializedActionOutcome:
|
|
1049
|
+
raise TypeError("value must be an exact MaterializedActionOutcome")
|
|
1050
|
+
value.__post_init__()
|
|
1051
|
+
if any(
|
|
1052
|
+
item.action.action_sha256 == value.action.action_sha256
|
|
1053
|
+
for item in self.outcomes
|
|
1054
|
+
):
|
|
1055
|
+
raise ValueError(
|
|
1056
|
+
"one materialized action can have only one immediate outcome"
|
|
1057
|
+
)
|
|
1058
|
+
self.outcomes.append(value)
|
|
1059
|
+
|
|
1060
|
+
def append_delayed_credit(self, value: MaterializedActionDelayedCredit) -> None:
|
|
1061
|
+
if type(value) is not MaterializedActionDelayedCredit:
|
|
1062
|
+
raise TypeError("value must be an exact MaterializedActionDelayedCredit")
|
|
1063
|
+
value.__post_init__()
|
|
1064
|
+
if not any(
|
|
1065
|
+
item.outcome_sha256 == value.outcome.outcome_sha256
|
|
1066
|
+
for item in self.outcomes
|
|
1067
|
+
):
|
|
1068
|
+
raise ValueError(
|
|
1069
|
+
"delayed credit source is absent from the immediate ledger"
|
|
1070
|
+
)
|
|
1071
|
+
for existing in self.delayed_credits:
|
|
1072
|
+
if existing.outcome.outcome_sha256 != value.outcome.outcome_sha256:
|
|
1073
|
+
continue
|
|
1074
|
+
for name in (
|
|
1075
|
+
"stage_front_survived",
|
|
1076
|
+
"terminal_front_persisted",
|
|
1077
|
+
"useful_descendant_observed",
|
|
1078
|
+
):
|
|
1079
|
+
if (
|
|
1080
|
+
getattr(existing, name) is not None
|
|
1081
|
+
and getattr(value, name) is not None
|
|
1082
|
+
):
|
|
1083
|
+
raise ValueError("a delayed channel can be adjudicated only once")
|
|
1084
|
+
self.delayed_credits.append(value)
|
|
1085
|
+
|
|
1086
|
+
def append_resolved_return(self, value: MaterializedActionResolvedReturn) -> None:
|
|
1087
|
+
if type(value) is not MaterializedActionResolvedReturn:
|
|
1088
|
+
raise TypeError("value must be an exact MaterializedActionResolvedReturn")
|
|
1089
|
+
value.__post_init__()
|
|
1090
|
+
if not any(
|
|
1091
|
+
item.outcome_sha256 == value.outcome.outcome_sha256
|
|
1092
|
+
for item in self.outcomes
|
|
1093
|
+
):
|
|
1094
|
+
raise ValueError("resolved return source is absent from the outcome ledger")
|
|
1095
|
+
prior = tuple(
|
|
1096
|
+
item
|
|
1097
|
+
for item in self.resolved_returns
|
|
1098
|
+
if item.outcome.outcome_sha256 == value.outcome.outcome_sha256
|
|
1099
|
+
)
|
|
1100
|
+
if prior:
|
|
1101
|
+
latest = max(prior, key=lambda item: item.available_at_decision_index)
|
|
1102
|
+
if value.available_at_decision_index <= latest.available_at_decision_index:
|
|
1103
|
+
raise ValueError("return resolutions must advance their availability")
|
|
1104
|
+
if value.horizon_end_decision_index < latest.horizon_end_decision_index:
|
|
1105
|
+
raise ValueError("return resolution horizon cannot move backward")
|
|
1106
|
+
if latest.fully_resolved:
|
|
1107
|
+
raise ValueError("a fully resolved return cannot be revised")
|
|
1108
|
+
self.resolved_returns.append(value)
|
|
1109
|
+
|
|
1110
|
+
@staticmethod
|
|
1111
|
+
def _arm_matches(
|
|
1112
|
+
action: MaterializedActionDescriptor,
|
|
1113
|
+
target: MaterializedActionDescriptor,
|
|
1114
|
+
) -> bool:
|
|
1115
|
+
return action.arm_key == target.arm_key
|
|
1116
|
+
|
|
1117
|
+
def _values(
|
|
1118
|
+
self,
|
|
1119
|
+
action: MaterializedActionDescriptor,
|
|
1120
|
+
channel: BrokerEvidenceChannel,
|
|
1121
|
+
*,
|
|
1122
|
+
local: bool,
|
|
1123
|
+
) -> tuple[float, ...]:
|
|
1124
|
+
def eligible(source: MaterializedActionDescriptor) -> bool:
|
|
1125
|
+
return self._arm_matches(source, action) and (
|
|
1126
|
+
not local
|
|
1127
|
+
or source.context.state_signature_sha256
|
|
1128
|
+
== action.context.state_signature_sha256
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
values: list[float] = []
|
|
1132
|
+
if channel is BrokerEvidenceChannel.REALIZATION:
|
|
1133
|
+
return tuple(
|
|
1134
|
+
1.0 if outcome.realized else 0.0
|
|
1135
|
+
for outcome in self.outcomes
|
|
1136
|
+
if eligible(outcome.action)
|
|
1137
|
+
)
|
|
1138
|
+
if channel in {
|
|
1139
|
+
BrokerEvidenceChannel.FEASIBILITY,
|
|
1140
|
+
BrokerEvidenceChannel.GAIN,
|
|
1141
|
+
BrokerEvidenceChannel.POSITIVE,
|
|
1142
|
+
BrokerEvidenceChannel.FORECAST_ERROR,
|
|
1143
|
+
}:
|
|
1144
|
+
for outcome in self.outcomes:
|
|
1145
|
+
if not eligible(outcome.action) or not outcome.realized:
|
|
1146
|
+
continue
|
|
1147
|
+
if channel is BrokerEvidenceChannel.FEASIBILITY:
|
|
1148
|
+
assert outcome.feasible is not None
|
|
1149
|
+
values.append(1.0 if outcome.feasible else 0.0)
|
|
1150
|
+
elif channel is BrokerEvidenceChannel.GAIN:
|
|
1151
|
+
assert outcome.normalized_archive_gain is not None
|
|
1152
|
+
values.append(outcome.normalized_archive_gain)
|
|
1153
|
+
elif channel is BrokerEvidenceChannel.POSITIVE:
|
|
1154
|
+
assert outcome.positive_marginal_utility is not None
|
|
1155
|
+
values.append(1.0 if outcome.positive_marginal_utility else 0.0)
|
|
1156
|
+
elif outcome.normalized_forecast_error is not None:
|
|
1157
|
+
values.append(outcome.normalized_forecast_error)
|
|
1158
|
+
return tuple(values)
|
|
1159
|
+
field_name = {
|
|
1160
|
+
BrokerEvidenceChannel.STAGE_SURVIVAL: "stage_front_survived",
|
|
1161
|
+
BrokerEvidenceChannel.TERMINAL_PERSISTENCE: "terminal_front_persisted",
|
|
1162
|
+
BrokerEvidenceChannel.DESCENDANT: "useful_descendant_observed",
|
|
1163
|
+
}[channel]
|
|
1164
|
+
for credit in self.delayed_credits:
|
|
1165
|
+
if not eligible(credit.outcome.action):
|
|
1166
|
+
continue
|
|
1167
|
+
verdict = getattr(credit, field_name)
|
|
1168
|
+
if verdict is not None:
|
|
1169
|
+
values.append(1.0 if verdict else 0.0)
|
|
1170
|
+
return tuple(values)
|
|
1171
|
+
|
|
1172
|
+
def estimate(
|
|
1173
|
+
self,
|
|
1174
|
+
action: MaterializedActionDescriptor,
|
|
1175
|
+
channel: BrokerEvidenceChannel,
|
|
1176
|
+
*,
|
|
1177
|
+
kappa: float,
|
|
1178
|
+
) -> BrokerChannelEstimate:
|
|
1179
|
+
if type(action) is not MaterializedActionDescriptor:
|
|
1180
|
+
raise TypeError("action must be an exact MaterializedActionDescriptor")
|
|
1181
|
+
if type(channel) is not BrokerEvidenceChannel:
|
|
1182
|
+
raise TypeError("channel must be an exact BrokerEvidenceChannel")
|
|
1183
|
+
if type(kappa) is not float or not math.isfinite(kappa) or kappa <= 0.0:
|
|
1184
|
+
raise ValueError("kappa must be a positive finite exact float")
|
|
1185
|
+
local_values = self._values(action, channel, local=True)
|
|
1186
|
+
global_values = self._values(action, channel, local=False)
|
|
1187
|
+
local_mean, local_sd, local_count_float = _bounded_empirical(local_values)
|
|
1188
|
+
global_mean, global_sd, global_count_float = _bounded_empirical(global_values)
|
|
1189
|
+
local_count = int(local_count_float)
|
|
1190
|
+
global_count = int(global_count_float)
|
|
1191
|
+
weight = local_count / (local_count + kappa)
|
|
1192
|
+
mean = weight * local_mean + (1.0 - weight) * global_mean
|
|
1193
|
+
sd = math.sqrt(weight * local_sd**2 + (1.0 - weight) * global_sd**2)
|
|
1194
|
+
return BrokerChannelEstimate(
|
|
1195
|
+
channel=channel,
|
|
1196
|
+
mean=float(mean),
|
|
1197
|
+
standard_deviation=float(min(1.0, sd)),
|
|
1198
|
+
local_count=local_count,
|
|
1199
|
+
global_count=global_count,
|
|
1200
|
+
local_mean=float(local_mean),
|
|
1201
|
+
global_mean=float(global_mean),
|
|
1202
|
+
shrinkage_weight=float(weight),
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
def _return_values(
|
|
1206
|
+
self,
|
|
1207
|
+
action: MaterializedActionDescriptor,
|
|
1208
|
+
*,
|
|
1209
|
+
local: bool,
|
|
1210
|
+
) -> tuple[tuple[float, bool], ...]:
|
|
1211
|
+
"""Return prior-only values and whether each is lineage-resolved."""
|
|
1212
|
+
|
|
1213
|
+
cutoff = action.context.decision_index
|
|
1214
|
+
newest: dict[str, MaterializedActionResolvedReturn] = {}
|
|
1215
|
+
for value in self.resolved_returns:
|
|
1216
|
+
if value.available_at_decision_index >= cutoff:
|
|
1217
|
+
continue
|
|
1218
|
+
key = value.outcome.outcome_sha256
|
|
1219
|
+
prior = newest.get(key)
|
|
1220
|
+
if prior is None or (
|
|
1221
|
+
value.available_at_decision_index > prior.available_at_decision_index
|
|
1222
|
+
):
|
|
1223
|
+
newest[key] = value
|
|
1224
|
+
values: list[tuple[float, bool]] = []
|
|
1225
|
+
for outcome in self.outcomes:
|
|
1226
|
+
source = outcome.action
|
|
1227
|
+
if source.context.decision_index >= cutoff:
|
|
1228
|
+
continue
|
|
1229
|
+
if not self._arm_matches(source, action):
|
|
1230
|
+
continue
|
|
1231
|
+
if local and (
|
|
1232
|
+
source.context.state_signature_sha256
|
|
1233
|
+
!= action.context.state_signature_sha256
|
|
1234
|
+
):
|
|
1235
|
+
continue
|
|
1236
|
+
resolved = newest.get(outcome.outcome_sha256)
|
|
1237
|
+
if resolved is not None:
|
|
1238
|
+
values.append((resolved.normalized_horizon_return, True))
|
|
1239
|
+
continue
|
|
1240
|
+
# Immediate real archive gain is a censored lower-bound observation
|
|
1241
|
+
# until a lineage resolver publishes a strictly later resolution.
|
|
1242
|
+
provisional = (
|
|
1243
|
+
0.0 if not outcome.realized else outcome.normalized_archive_gain
|
|
1244
|
+
)
|
|
1245
|
+
assert provisional is not None
|
|
1246
|
+
values.append((provisional, False))
|
|
1247
|
+
return tuple(values)
|
|
1248
|
+
|
|
1249
|
+
def estimate_return(
|
|
1250
|
+
self,
|
|
1251
|
+
action: MaterializedActionDescriptor,
|
|
1252
|
+
*,
|
|
1253
|
+
kappa: float,
|
|
1254
|
+
) -> BrokerReturnEstimate:
|
|
1255
|
+
if type(action) is not MaterializedActionDescriptor:
|
|
1256
|
+
raise TypeError("action must be an exact MaterializedActionDescriptor")
|
|
1257
|
+
if type(kappa) is not float or not math.isfinite(kappa) or kappa <= 0.0:
|
|
1258
|
+
raise ValueError("kappa must be a positive finite exact float")
|
|
1259
|
+
local_rows = self._return_values(action, local=True)
|
|
1260
|
+
global_rows = self._return_values(action, local=False)
|
|
1261
|
+
local_values = tuple(value for value, _resolved in local_rows)
|
|
1262
|
+
global_values = tuple(value for value, _resolved in global_rows)
|
|
1263
|
+
local_mean, local_sd, local_count_float = _bounded_empirical(local_values)
|
|
1264
|
+
global_mean, global_sd, global_count_float = _bounded_empirical(global_values)
|
|
1265
|
+
local_count = int(local_count_float)
|
|
1266
|
+
global_count = int(global_count_float)
|
|
1267
|
+
weight = local_count / (local_count + kappa)
|
|
1268
|
+
mean = weight * local_mean + (1.0 - weight) * global_mean
|
|
1269
|
+
sd = math.sqrt(weight * local_sd**2 + (1.0 - weight) * global_sd**2)
|
|
1270
|
+
resolved_count = sum(resolved for _value, resolved in global_rows)
|
|
1271
|
+
return BrokerReturnEstimate(
|
|
1272
|
+
mean=float(mean),
|
|
1273
|
+
standard_deviation=float(min(1.0, sd)),
|
|
1274
|
+
local_count=local_count,
|
|
1275
|
+
global_count=global_count,
|
|
1276
|
+
resolved_count=resolved_count,
|
|
1277
|
+
provisional_count=global_count - resolved_count,
|
|
1278
|
+
local_mean=float(local_mean),
|
|
1279
|
+
global_mean=float(global_mean),
|
|
1280
|
+
shrinkage_weight=float(weight),
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
@dataclass(frozen=True, slots=True)
|
|
1285
|
+
class BrokerActionScore:
|
|
1286
|
+
action_sha256: str
|
|
1287
|
+
value: float
|
|
1288
|
+
lower_confidence_bound: float
|
|
1289
|
+
upper_confidence_bound: float
|
|
1290
|
+
selection_index: float
|
|
1291
|
+
return_estimator_definition_sha256: str
|
|
1292
|
+
return_estimate: BrokerReturnEstimate
|
|
1293
|
+
estimates: tuple[BrokerChannelEstimate, ...]
|
|
1294
|
+
|
|
1295
|
+
def __post_init__(self) -> None:
|
|
1296
|
+
require_sha256(self.action_sha256, "action_sha256")
|
|
1297
|
+
for name in (
|
|
1298
|
+
"value",
|
|
1299
|
+
"lower_confidence_bound",
|
|
1300
|
+
"upper_confidence_bound",
|
|
1301
|
+
"selection_index",
|
|
1302
|
+
):
|
|
1303
|
+
value = getattr(self, name)
|
|
1304
|
+
if type(value) is not float or not math.isfinite(value):
|
|
1305
|
+
raise TypeError(f"{name} must be a finite exact float")
|
|
1306
|
+
if self.lower_confidence_bound > self.value:
|
|
1307
|
+
raise ValueError("lower confidence bound exceeds value")
|
|
1308
|
+
if self.upper_confidence_bound < self.value:
|
|
1309
|
+
raise ValueError("upper confidence bound is below value")
|
|
1310
|
+
if not 0.0 <= self.selection_index <= 1.0:
|
|
1311
|
+
raise ValueError("selection_index must lie in [0, 1]")
|
|
1312
|
+
require_sha256(
|
|
1313
|
+
self.return_estimator_definition_sha256,
|
|
1314
|
+
"return_estimator_definition_sha256",
|
|
1315
|
+
)
|
|
1316
|
+
if type(self.return_estimate) is not BrokerReturnEstimate:
|
|
1317
|
+
raise TypeError("return_estimate must be exact")
|
|
1318
|
+
self.return_estimate.__post_init__()
|
|
1319
|
+
if type(self.estimates) is not tuple or tuple(
|
|
1320
|
+
item.channel for item in self.estimates
|
|
1321
|
+
) != tuple(BrokerEvidenceChannel):
|
|
1322
|
+
raise ValueError("estimates must cover every channel in canonical order")
|
|
1323
|
+
|
|
1324
|
+
def to_record(self) -> dict[str, object]:
|
|
1325
|
+
self.__post_init__()
|
|
1326
|
+
return {
|
|
1327
|
+
"action_sha256": self.action_sha256,
|
|
1328
|
+
"value_hex": self.value.hex(),
|
|
1329
|
+
"lower_confidence_bound_hex": self.lower_confidence_bound.hex(),
|
|
1330
|
+
"upper_confidence_bound_hex": self.upper_confidence_bound.hex(),
|
|
1331
|
+
"selection_index_hex": self.selection_index.hex(),
|
|
1332
|
+
"return_estimator_definition_sha256": (
|
|
1333
|
+
self.return_estimator_definition_sha256
|
|
1334
|
+
),
|
|
1335
|
+
"return_estimate": self.return_estimate.to_record(),
|
|
1336
|
+
"estimates": [value.to_record() for value in self.estimates],
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
@runtime_checkable
|
|
1341
|
+
class MaterializedSlateValuePort(Protocol):
|
|
1342
|
+
"""Predict residual complementarity not explained by member returns.
|
|
1343
|
+
|
|
1344
|
+
The value must lie in ``[0, 1]``. Zero means that the member-level return
|
|
1345
|
+
posterior completely explains the slate. One means that the slate is
|
|
1346
|
+
expected to capture all return headroom remaining after additive member
|
|
1347
|
+
credit. This closed meaning composes with coalition-efficient action
|
|
1348
|
+
returns without a hand-authored joint/individual mixing weight.
|
|
1349
|
+
"""
|
|
1350
|
+
|
|
1351
|
+
definition_sha256: str
|
|
1352
|
+
|
|
1353
|
+
def value(self, actions: tuple[MaterializedActionDescriptor, ...]) -> float: ...
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
@runtime_checkable
|
|
1357
|
+
class MaterializedSlateFeasibilityPort(Protocol):
|
|
1358
|
+
"""Apply exact generic materialization constraints to one slate."""
|
|
1359
|
+
|
|
1360
|
+
definition_sha256: str
|
|
1361
|
+
|
|
1362
|
+
def permits(self, actions: tuple[MaterializedActionDescriptor, ...]) -> bool: ...
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
@dataclass(frozen=True, slots=True)
|
|
1366
|
+
class MaterializedActionExplorationRequirement:
|
|
1367
|
+
"""Authenticated actions reserved by a strictly prequential policy."""
|
|
1368
|
+
|
|
1369
|
+
policy_id: str
|
|
1370
|
+
policy_version: int
|
|
1371
|
+
policy_definition_sha256: str
|
|
1372
|
+
required_action_sha256s: tuple[str, ...]
|
|
1373
|
+
prior_outcome_count: int
|
|
1374
|
+
cold_start: bool
|
|
1375
|
+
evidence: FrozenJsonObject
|
|
1376
|
+
requirement_sha256: str = field(init=False)
|
|
1377
|
+
|
|
1378
|
+
def __post_init__(self) -> None:
|
|
1379
|
+
_require_token(self.policy_id, name="policy_id")
|
|
1380
|
+
if type(self.policy_version) is not int or self.policy_version <= 0:
|
|
1381
|
+
raise ValueError("policy_version must be a positive exact integer")
|
|
1382
|
+
require_sha256(
|
|
1383
|
+
self.policy_definition_sha256,
|
|
1384
|
+
"policy_definition_sha256",
|
|
1385
|
+
)
|
|
1386
|
+
if type(self.required_action_sha256s) is not tuple:
|
|
1387
|
+
raise TypeError("required action hashes must be an exact tuple")
|
|
1388
|
+
if self.required_action_sha256s != tuple(
|
|
1389
|
+
sorted(set(self.required_action_sha256s))
|
|
1390
|
+
):
|
|
1391
|
+
raise ValueError("required action hashes must be unique and canonical")
|
|
1392
|
+
for value in self.required_action_sha256s:
|
|
1393
|
+
require_sha256(value, "required action sha256")
|
|
1394
|
+
if type(self.prior_outcome_count) is not int or self.prior_outcome_count < 0:
|
|
1395
|
+
raise ValueError("prior_outcome_count must be non-negative")
|
|
1396
|
+
if type(self.cold_start) is not bool:
|
|
1397
|
+
raise TypeError("cold_start must be an exact bool")
|
|
1398
|
+
if self.cold_start != (self.prior_outcome_count == 0):
|
|
1399
|
+
raise ValueError("cold_start must exactly reflect prior outcome count")
|
|
1400
|
+
if (
|
|
1401
|
+
type(self.evidence) is not FrozenJsonObject
|
|
1402
|
+
or freeze_json(self.evidence) is not self.evidence
|
|
1403
|
+
):
|
|
1404
|
+
raise TypeError("exploration evidence must be an exact frozen object")
|
|
1405
|
+
object.__setattr__(
|
|
1406
|
+
self,
|
|
1407
|
+
"requirement_sha256",
|
|
1408
|
+
_hash(
|
|
1409
|
+
_EXPLORATION_REQUIREMENT_DOMAIN,
|
|
1410
|
+
self._unsigned_record(),
|
|
1411
|
+
),
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1415
|
+
return {
|
|
1416
|
+
"schema_version": 1,
|
|
1417
|
+
"policy": {
|
|
1418
|
+
"policy_id": self.policy_id,
|
|
1419
|
+
"policy_version": self.policy_version,
|
|
1420
|
+
"definition_sha256": self.policy_definition_sha256,
|
|
1421
|
+
},
|
|
1422
|
+
"required_action_sha256s": list(self.required_action_sha256s),
|
|
1423
|
+
"prior_outcome_count": self.prior_outcome_count,
|
|
1424
|
+
"cold_start": self.cold_start,
|
|
1425
|
+
"evidence_sha256": typed_json_sha256(self.evidence),
|
|
1426
|
+
"strictly_prior_outcomes_only": True,
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
def to_record(self, *, include_evidence: bool = False) -> dict[str, object]:
|
|
1430
|
+
self.__post_init__()
|
|
1431
|
+
record = {
|
|
1432
|
+
**self._unsigned_record(),
|
|
1433
|
+
"requirement_sha256": self.requirement_sha256,
|
|
1434
|
+
}
|
|
1435
|
+
if include_evidence:
|
|
1436
|
+
record["evidence"] = thaw_json(self.evidence)
|
|
1437
|
+
return record
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
@dataclass(frozen=True, slots=True)
|
|
1441
|
+
class MaterializedActionAllocationRequirement:
|
|
1442
|
+
"""Authenticated outcome-blind constraint produced after proposal sealing.
|
|
1443
|
+
|
|
1444
|
+
The application core treats the policy and its evidence as opaque. The
|
|
1445
|
+
hashes bind the requirement to one residual request and one exact proposal
|
|
1446
|
+
universe, while ``candidate_outcomes_observed`` makes the information
|
|
1447
|
+
boundary explicit and mechanically rejects post-hoc selectors.
|
|
1448
|
+
"""
|
|
1449
|
+
|
|
1450
|
+
policy_id: str
|
|
1451
|
+
policy_version: int
|
|
1452
|
+
policy_definition_sha256: str
|
|
1453
|
+
residual_request_sha256: str
|
|
1454
|
+
proposal_sha256s: tuple[str, ...]
|
|
1455
|
+
required_action_sha256s: tuple[str, ...]
|
|
1456
|
+
candidate_outcomes_observed: bool
|
|
1457
|
+
evidence: FrozenJsonObject
|
|
1458
|
+
requirement_sha256: str = field(init=False)
|
|
1459
|
+
|
|
1460
|
+
def __post_init__(self) -> None:
|
|
1461
|
+
_require_token(self.policy_id, name="policy_id")
|
|
1462
|
+
if type(self.policy_version) is not int or self.policy_version <= 0:
|
|
1463
|
+
raise ValueError("policy_version must be a positive exact integer")
|
|
1464
|
+
require_sha256(
|
|
1465
|
+
self.policy_definition_sha256,
|
|
1466
|
+
"policy_definition_sha256",
|
|
1467
|
+
)
|
|
1468
|
+
require_sha256(
|
|
1469
|
+
self.residual_request_sha256,
|
|
1470
|
+
"residual_request_sha256",
|
|
1471
|
+
)
|
|
1472
|
+
if type(self.proposal_sha256s) is not tuple or not self.proposal_sha256s:
|
|
1473
|
+
raise ValueError("proposal hashes must be a non-empty exact tuple")
|
|
1474
|
+
if self.proposal_sha256s != tuple(sorted(set(self.proposal_sha256s))):
|
|
1475
|
+
raise ValueError("proposal hashes must be unique and canonical")
|
|
1476
|
+
for value in self.proposal_sha256s:
|
|
1477
|
+
require_sha256(value, "proposal sha256")
|
|
1478
|
+
if type(self.required_action_sha256s) is not tuple:
|
|
1479
|
+
raise TypeError("required action hashes must be an exact tuple")
|
|
1480
|
+
if self.required_action_sha256s != tuple(
|
|
1481
|
+
sorted(set(self.required_action_sha256s))
|
|
1482
|
+
):
|
|
1483
|
+
raise ValueError("required action hashes must be unique and canonical")
|
|
1484
|
+
for value in self.required_action_sha256s:
|
|
1485
|
+
require_sha256(value, "required action sha256")
|
|
1486
|
+
if type(self.candidate_outcomes_observed) is not bool:
|
|
1487
|
+
raise TypeError("candidate_outcomes_observed must be an exact bool")
|
|
1488
|
+
if self.candidate_outcomes_observed:
|
|
1489
|
+
raise ValueError(
|
|
1490
|
+
"allocation requirements cannot observe candidate outcomes"
|
|
1491
|
+
)
|
|
1492
|
+
if (
|
|
1493
|
+
type(self.evidence) is not FrozenJsonObject
|
|
1494
|
+
or freeze_json(self.evidence) is not self.evidence
|
|
1495
|
+
):
|
|
1496
|
+
raise TypeError("allocation evidence must be an exact frozen object")
|
|
1497
|
+
object.__setattr__(
|
|
1498
|
+
self,
|
|
1499
|
+
"requirement_sha256",
|
|
1500
|
+
_hash(
|
|
1501
|
+
_ALLOCATION_REQUIREMENT_DOMAIN,
|
|
1502
|
+
self._unsigned_record(),
|
|
1503
|
+
),
|
|
1504
|
+
)
|
|
1505
|
+
|
|
1506
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1507
|
+
return {
|
|
1508
|
+
"schema_version": 1,
|
|
1509
|
+
"policy": {
|
|
1510
|
+
"policy_id": self.policy_id,
|
|
1511
|
+
"policy_version": self.policy_version,
|
|
1512
|
+
"definition_sha256": self.policy_definition_sha256,
|
|
1513
|
+
},
|
|
1514
|
+
"residual_request_sha256": self.residual_request_sha256,
|
|
1515
|
+
"proposal_sha256s": list(self.proposal_sha256s),
|
|
1516
|
+
"required_action_sha256s": list(self.required_action_sha256s),
|
|
1517
|
+
"candidate_outcomes_observed": self.candidate_outcomes_observed,
|
|
1518
|
+
"evidence_sha256": typed_json_sha256(self.evidence),
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
def to_record(self, *, include_evidence: bool = False) -> dict[str, object]:
|
|
1522
|
+
self.__post_init__()
|
|
1523
|
+
record = {
|
|
1524
|
+
**self._unsigned_record(),
|
|
1525
|
+
"requirement_sha256": self.requirement_sha256,
|
|
1526
|
+
}
|
|
1527
|
+
if include_evidence:
|
|
1528
|
+
record["evidence"] = thaw_json(self.evidence)
|
|
1529
|
+
return record
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
MaterializedActionAllocationConstraint = (
|
|
1533
|
+
MaterializedActionAllocationRequirement | AdaptiveActionAllocationDirective
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
def _validate_allocation_constraint(
|
|
1538
|
+
value: MaterializedActionAllocationConstraint,
|
|
1539
|
+
) -> None:
|
|
1540
|
+
"""Accept only the two authenticated allocation information boundaries."""
|
|
1541
|
+
|
|
1542
|
+
if type(value) not in (
|
|
1543
|
+
MaterializedActionAllocationRequirement,
|
|
1544
|
+
AdaptiveActionAllocationDirective,
|
|
1545
|
+
):
|
|
1546
|
+
raise TypeError(
|
|
1547
|
+
"allocation_requirement must be an exact static requirement, "
|
|
1548
|
+
"an exact adaptive directive, or None"
|
|
1549
|
+
)
|
|
1550
|
+
value.__post_init__()
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
@runtime_checkable
|
|
1554
|
+
class MaterializedActionExplorationPort(Protocol):
|
|
1555
|
+
"""Reserve a bounded action subset using only current proposals and priors."""
|
|
1556
|
+
|
|
1557
|
+
policy_id: str
|
|
1558
|
+
policy_version: int
|
|
1559
|
+
definition_sha256: str
|
|
1560
|
+
|
|
1561
|
+
def require(
|
|
1562
|
+
self,
|
|
1563
|
+
request: "MaterializedActionBrokerRequest",
|
|
1564
|
+
ledger: MaterializedActionEvidenceLedger,
|
|
1565
|
+
required_reference_action_sha256s: tuple[str, ...],
|
|
1566
|
+
) -> MaterializedActionExplorationRequirement: ...
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
@dataclass(frozen=True, slots=True)
|
|
1570
|
+
class MaterializedActionBrokerRequest:
|
|
1571
|
+
actions: tuple[MaterializedActionDescriptor, ...]
|
|
1572
|
+
evaluation_slots: int
|
|
1573
|
+
slate_value: MaterializedSlateValuePort
|
|
1574
|
+
slate_feasibility: MaterializedSlateFeasibilityPort
|
|
1575
|
+
reference_escrow_slots: int = 1
|
|
1576
|
+
allocation_requirement: MaterializedActionAllocationConstraint | None = None
|
|
1577
|
+
|
|
1578
|
+
def __post_init__(self) -> None:
|
|
1579
|
+
if type(self.actions) is not tuple or not self.actions:
|
|
1580
|
+
raise ValueError("actions must be a non-empty exact tuple")
|
|
1581
|
+
for action in self.actions:
|
|
1582
|
+
if type(action) is not MaterializedActionDescriptor:
|
|
1583
|
+
raise TypeError("actions must contain exact descriptors")
|
|
1584
|
+
action.__post_init__()
|
|
1585
|
+
if len({value.action_sha256 for value in self.actions}) != len(self.actions):
|
|
1586
|
+
raise ValueError("action identities must be unique")
|
|
1587
|
+
contexts = {
|
|
1588
|
+
(value.context.campaign_scope_sha256, value.context.decision_index)
|
|
1589
|
+
for value in self.actions
|
|
1590
|
+
}
|
|
1591
|
+
if len(contexts) != 1:
|
|
1592
|
+
raise ValueError("one broker request cannot mix decision cutoffs")
|
|
1593
|
+
if type(
|
|
1594
|
+
self.evaluation_slots
|
|
1595
|
+
) is not int or not 1 <= self.evaluation_slots <= len(self.actions):
|
|
1596
|
+
raise ValueError("evaluation_slots must fit the supplied action universe")
|
|
1597
|
+
if not isinstance(self.slate_value, MaterializedSlateValuePort):
|
|
1598
|
+
raise TypeError("slate_value must implement MaterializedSlateValuePort")
|
|
1599
|
+
if not isinstance(self.slate_feasibility, MaterializedSlateFeasibilityPort):
|
|
1600
|
+
raise TypeError(
|
|
1601
|
+
"slate_feasibility must implement MaterializedSlateFeasibilityPort"
|
|
1602
|
+
)
|
|
1603
|
+
require_sha256(self.slate_value.definition_sha256, "slate value definition")
|
|
1604
|
+
require_sha256(
|
|
1605
|
+
self.slate_feasibility.definition_sha256,
|
|
1606
|
+
"slate feasibility definition",
|
|
1607
|
+
)
|
|
1608
|
+
if (
|
|
1609
|
+
type(self.reference_escrow_slots) is not int
|
|
1610
|
+
or not 0 <= self.reference_escrow_slots <= self.evaluation_slots
|
|
1611
|
+
):
|
|
1612
|
+
raise ValueError("reference_escrow_slots must fit the evaluation capacity")
|
|
1613
|
+
if self.allocation_requirement is not None:
|
|
1614
|
+
_validate_allocation_constraint(self.allocation_requirement)
|
|
1615
|
+
action_sha256s = {value.action_sha256 for value in self.actions}
|
|
1616
|
+
if not set(self.allocation_requirement.required_action_sha256s).issubset(
|
|
1617
|
+
action_sha256s
|
|
1618
|
+
):
|
|
1619
|
+
raise ValueError(
|
|
1620
|
+
"allocation policy required an action outside the request"
|
|
1621
|
+
)
|
|
1622
|
+
if (
|
|
1623
|
+
len(self.allocation_requirement.required_action_sha256s)
|
|
1624
|
+
> self.evaluation_slots
|
|
1625
|
+
):
|
|
1626
|
+
raise ValueError("allocation requirement exceeds evaluation capacity")
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
@dataclass(frozen=True, slots=True)
|
|
1630
|
+
class MaterializedActionBrokerDecision:
|
|
1631
|
+
selected_actions: tuple[MaterializedActionDescriptor, ...]
|
|
1632
|
+
scores: tuple[BrokerActionScore, ...]
|
|
1633
|
+
required_reference_action_sha256s: tuple[str, ...]
|
|
1634
|
+
exploration_requirement: MaterializedActionExplorationRequirement | None
|
|
1635
|
+
allocation_requirement: MaterializedActionAllocationConstraint | None
|
|
1636
|
+
reference_displaced_count: int
|
|
1637
|
+
search_mode: str
|
|
1638
|
+
complete_slate_count_considered: int
|
|
1639
|
+
residual_complementarity_value: float
|
|
1640
|
+
exploration_action_sha256: str | None
|
|
1641
|
+
broker_definition_sha256: str = MATERIALIZED_ACTION_BROKER_DEFINITION_SHA256
|
|
1642
|
+
decision_sha256: str = field(init=False)
|
|
1643
|
+
|
|
1644
|
+
def __post_init__(self) -> None:
|
|
1645
|
+
if type(self.selected_actions) is not tuple or not self.selected_actions:
|
|
1646
|
+
raise ValueError("selected_actions must be a non-empty exact tuple")
|
|
1647
|
+
if len(
|
|
1648
|
+
{value.phenotype_identity_sha256 for value in self.selected_actions}
|
|
1649
|
+
) != len(self.selected_actions):
|
|
1650
|
+
raise ValueError("selected actions must have unique phenotypes")
|
|
1651
|
+
if tuple(
|
|
1652
|
+
sorted(value.action_sha256 for value in self.selected_actions)
|
|
1653
|
+
) != tuple(value.action_sha256 for value in self.selected_actions):
|
|
1654
|
+
raise ValueError("selected actions must use canonical action order")
|
|
1655
|
+
if type(self.scores) is not tuple or not self.scores:
|
|
1656
|
+
raise ValueError("scores must be a non-empty exact tuple")
|
|
1657
|
+
if type(self.required_reference_action_sha256s) is not tuple:
|
|
1658
|
+
raise TypeError("required reference identities must be an exact tuple")
|
|
1659
|
+
if tuple(sorted(set(self.required_reference_action_sha256s))) != (
|
|
1660
|
+
self.required_reference_action_sha256s
|
|
1661
|
+
):
|
|
1662
|
+
raise ValueError("required reference identities must be unique/canonical")
|
|
1663
|
+
selected_sha256s = {value.action_sha256 for value in self.selected_actions}
|
|
1664
|
+
for value in self.required_reference_action_sha256s:
|
|
1665
|
+
require_sha256(value, "required reference action sha256")
|
|
1666
|
+
if value not in selected_sha256s:
|
|
1667
|
+
raise ValueError("required reference action is absent from the slate")
|
|
1668
|
+
if self.exploration_requirement is not None:
|
|
1669
|
+
if (
|
|
1670
|
+
type(self.exploration_requirement)
|
|
1671
|
+
is not MaterializedActionExplorationRequirement
|
|
1672
|
+
):
|
|
1673
|
+
raise TypeError("exploration_requirement must be exact or None")
|
|
1674
|
+
self.exploration_requirement.__post_init__()
|
|
1675
|
+
if not set(self.exploration_requirement.required_action_sha256s).issubset(
|
|
1676
|
+
selected_sha256s
|
|
1677
|
+
):
|
|
1678
|
+
raise ValueError("required exploration action is absent from the slate")
|
|
1679
|
+
if self.allocation_requirement is not None:
|
|
1680
|
+
_validate_allocation_constraint(self.allocation_requirement)
|
|
1681
|
+
if not set(self.allocation_requirement.required_action_sha256s).issubset(
|
|
1682
|
+
selected_sha256s
|
|
1683
|
+
):
|
|
1684
|
+
raise ValueError("required allocation action is absent from the slate")
|
|
1685
|
+
if (
|
|
1686
|
+
type(self.reference_displaced_count) is not int
|
|
1687
|
+
or self.reference_displaced_count < 0
|
|
1688
|
+
):
|
|
1689
|
+
raise ValueError("reference_displaced_count must be non-negative")
|
|
1690
|
+
_require_token(self.search_mode, name="search_mode")
|
|
1691
|
+
if (
|
|
1692
|
+
type(self.complete_slate_count_considered) is not int
|
|
1693
|
+
or self.complete_slate_count_considered <= 0
|
|
1694
|
+
):
|
|
1695
|
+
raise ValueError("at least one complete slate must be considered")
|
|
1696
|
+
_require_probability(
|
|
1697
|
+
self.residual_complementarity_value,
|
|
1698
|
+
name="residual_complementarity_value",
|
|
1699
|
+
)
|
|
1700
|
+
if self.exploration_action_sha256 is not None:
|
|
1701
|
+
require_sha256(
|
|
1702
|
+
self.exploration_action_sha256,
|
|
1703
|
+
"exploration_action_sha256",
|
|
1704
|
+
)
|
|
1705
|
+
selected = {value.action_sha256: value for value in self.selected_actions}
|
|
1706
|
+
action = selected.get(self.exploration_action_sha256)
|
|
1707
|
+
if action is None:
|
|
1708
|
+
raise ValueError("exploration action is absent from selected slate")
|
|
1709
|
+
if action.context.phase is SearchPhase.TERMINAL_CONVERSION:
|
|
1710
|
+
raise ValueError("terminal decisions cannot purchase information")
|
|
1711
|
+
require_sha256(self.broker_definition_sha256, "broker_definition_sha256")
|
|
1712
|
+
object.__setattr__(
|
|
1713
|
+
self,
|
|
1714
|
+
"decision_sha256",
|
|
1715
|
+
_hash(_DECISION_DOMAIN, self._unsigned_record()),
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1719
|
+
return {
|
|
1720
|
+
"schema_version": 1,
|
|
1721
|
+
"broker_id": MATERIALIZED_ACTION_BROKER_ID,
|
|
1722
|
+
"broker_version": MATERIALIZED_ACTION_BROKER_VERSION,
|
|
1723
|
+
"broker_definition_sha256": self.broker_definition_sha256,
|
|
1724
|
+
"selected_action_sha256s": [
|
|
1725
|
+
value.action_sha256 for value in self.selected_actions
|
|
1726
|
+
],
|
|
1727
|
+
"scores": [value.to_record() for value in self.scores],
|
|
1728
|
+
"required_reference_action_sha256s": list(
|
|
1729
|
+
self.required_reference_action_sha256s
|
|
1730
|
+
),
|
|
1731
|
+
"exploration_requirement": (
|
|
1732
|
+
None
|
|
1733
|
+
if self.exploration_requirement is None
|
|
1734
|
+
else self.exploration_requirement.to_record()
|
|
1735
|
+
),
|
|
1736
|
+
"allocation_requirement": (
|
|
1737
|
+
None
|
|
1738
|
+
if self.allocation_requirement is None
|
|
1739
|
+
else self.allocation_requirement.to_record()
|
|
1740
|
+
),
|
|
1741
|
+
"reference_displaced_count": self.reference_displaced_count,
|
|
1742
|
+
"search_mode": self.search_mode,
|
|
1743
|
+
"complete_slate_count_considered": self.complete_slate_count_considered,
|
|
1744
|
+
"residual_complementarity_value_hex": (
|
|
1745
|
+
self.residual_complementarity_value.hex()
|
|
1746
|
+
),
|
|
1747
|
+
"exploration_action_sha256": self.exploration_action_sha256,
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
@property
|
|
1751
|
+
def required_reference_action_sha256(self) -> str | None:
|
|
1752
|
+
"""Compatibility view for callers that escrow exactly one reference."""
|
|
1753
|
+
|
|
1754
|
+
if not self.required_reference_action_sha256s:
|
|
1755
|
+
return None
|
|
1756
|
+
return self.required_reference_action_sha256s[0]
|
|
1757
|
+
|
|
1758
|
+
@property
|
|
1759
|
+
def reference_displaced(self) -> bool:
|
|
1760
|
+
return self.reference_displaced_count > 0
|
|
1761
|
+
|
|
1762
|
+
def to_record(
|
|
1763
|
+
self,
|
|
1764
|
+
*,
|
|
1765
|
+
include_allocation_evidence: bool = False,
|
|
1766
|
+
) -> dict[str, object]:
|
|
1767
|
+
self.__post_init__()
|
|
1768
|
+
record = {
|
|
1769
|
+
**self._unsigned_record(),
|
|
1770
|
+
"decision_sha256": self.decision_sha256,
|
|
1771
|
+
}
|
|
1772
|
+
if include_allocation_evidence and self.allocation_requirement is not None:
|
|
1773
|
+
record["allocation_requirement"] = self.allocation_requirement.to_record(
|
|
1774
|
+
include_evidence=True
|
|
1775
|
+
)
|
|
1776
|
+
return record
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
@dataclass(frozen=True, slots=True)
|
|
1780
|
+
class RegretBrokeredMaterializedActionPolicy:
|
|
1781
|
+
"""Broker normalized lineage return without cross-channel score weights."""
|
|
1782
|
+
|
|
1783
|
+
ledger: MaterializedActionEvidenceLedger
|
|
1784
|
+
return_value: MaterializedActionReturnValuePort | None = None
|
|
1785
|
+
exploration_policy: MaterializedActionExplorationPort | None = None
|
|
1786
|
+
hierarchical_kappa: float = 4.0
|
|
1787
|
+
confidence_width: float = 1.0
|
|
1788
|
+
exact_combination_limit: int = 250_000
|
|
1789
|
+
beam_width: int = 512
|
|
1790
|
+
|
|
1791
|
+
def __post_init__(self) -> None:
|
|
1792
|
+
if type(self.ledger) is not MaterializedActionEvidenceLedger:
|
|
1793
|
+
raise TypeError("ledger must be an exact MaterializedActionEvidenceLedger")
|
|
1794
|
+
if self.return_value is not None:
|
|
1795
|
+
if not isinstance(self.return_value, MaterializedActionReturnValuePort):
|
|
1796
|
+
raise TypeError(
|
|
1797
|
+
"return_value must implement MaterializedActionReturnValuePort"
|
|
1798
|
+
)
|
|
1799
|
+
require_sha256(
|
|
1800
|
+
self.return_value.definition_sha256,
|
|
1801
|
+
"return value definition_sha256",
|
|
1802
|
+
)
|
|
1803
|
+
if self.exploration_policy is not None:
|
|
1804
|
+
if not isinstance(
|
|
1805
|
+
self.exploration_policy,
|
|
1806
|
+
MaterializedActionExplorationPort,
|
|
1807
|
+
):
|
|
1808
|
+
raise TypeError(
|
|
1809
|
+
"exploration_policy must implement "
|
|
1810
|
+
"MaterializedActionExplorationPort"
|
|
1811
|
+
)
|
|
1812
|
+
_require_token(
|
|
1813
|
+
self.exploration_policy.policy_id,
|
|
1814
|
+
name="exploration policy_id",
|
|
1815
|
+
)
|
|
1816
|
+
if (
|
|
1817
|
+
type(self.exploration_policy.policy_version) is not int
|
|
1818
|
+
or self.exploration_policy.policy_version <= 0
|
|
1819
|
+
):
|
|
1820
|
+
raise ValueError("exploration policy_version must be positive")
|
|
1821
|
+
require_sha256(
|
|
1822
|
+
self.exploration_policy.definition_sha256,
|
|
1823
|
+
"exploration policy definition_sha256",
|
|
1824
|
+
)
|
|
1825
|
+
for name in ("hierarchical_kappa", "confidence_width"):
|
|
1826
|
+
value = getattr(self, name)
|
|
1827
|
+
if type(value) is not float or not math.isfinite(value) or value <= 0.0:
|
|
1828
|
+
raise ValueError(f"{name} must be a positive finite exact float")
|
|
1829
|
+
for name in ("exact_combination_limit", "beam_width"):
|
|
1830
|
+
value = getattr(self, name)
|
|
1831
|
+
if type(value) is not int or value <= 0:
|
|
1832
|
+
raise ValueError(f"{name} must be a positive exact integer")
|
|
1833
|
+
|
|
1834
|
+
def score(self, action: MaterializedActionDescriptor) -> BrokerActionScore:
|
|
1835
|
+
if self.return_value is None:
|
|
1836
|
+
return_estimate = self.ledger.estimate_return(
|
|
1837
|
+
action,
|
|
1838
|
+
kappa=self.hierarchical_kappa,
|
|
1839
|
+
)
|
|
1840
|
+
return_estimator_definition_sha256 = (
|
|
1841
|
+
EMPIRICAL_RETURN_ESTIMATOR_DEFINITION_SHA256
|
|
1842
|
+
)
|
|
1843
|
+
else:
|
|
1844
|
+
return_estimate = self.return_value.estimate(action)
|
|
1845
|
+
if type(return_estimate) is not BrokerReturnEstimate:
|
|
1846
|
+
raise TypeError("return value port returned a foreign estimate")
|
|
1847
|
+
return_estimate.__post_init__()
|
|
1848
|
+
return_estimator_definition_sha256 = self.return_value.definition_sha256
|
|
1849
|
+
estimates = tuple(
|
|
1850
|
+
self.ledger.estimate(
|
|
1851
|
+
action,
|
|
1852
|
+
channel,
|
|
1853
|
+
kappa=self.hierarchical_kappa,
|
|
1854
|
+
)
|
|
1855
|
+
for channel in BrokerEvidenceChannel
|
|
1856
|
+
)
|
|
1857
|
+
value = return_estimate.mean
|
|
1858
|
+
radius = self.confidence_width * return_estimate.standard_deviation
|
|
1859
|
+
lower = max(0.0, value - radius)
|
|
1860
|
+
upper = min(1.0, value + radius)
|
|
1861
|
+
# Information has no endpoint value when there is no future decision
|
|
1862
|
+
# that can consume it. Earlier waves use the posterior upper bound as
|
|
1863
|
+
# the action index; the terminal wave uses posterior mean exactly.
|
|
1864
|
+
selection_index = (
|
|
1865
|
+
value if action.context.phase is SearchPhase.TERMINAL_CONVERSION else upper
|
|
1866
|
+
)
|
|
1867
|
+
return BrokerActionScore(
|
|
1868
|
+
action_sha256=action.action_sha256,
|
|
1869
|
+
value=float(value),
|
|
1870
|
+
lower_confidence_bound=float(lower),
|
|
1871
|
+
upper_confidence_bound=float(upper),
|
|
1872
|
+
selection_index=float(selection_index),
|
|
1873
|
+
return_estimator_definition_sha256=(return_estimator_definition_sha256),
|
|
1874
|
+
return_estimate=return_estimate,
|
|
1875
|
+
estimates=estimates,
|
|
1876
|
+
)
|
|
1877
|
+
|
|
1878
|
+
@staticmethod
|
|
1879
|
+
def _canonical_slate(
|
|
1880
|
+
actions: tuple[MaterializedActionDescriptor, ...],
|
|
1881
|
+
) -> tuple[MaterializedActionDescriptor, ...]:
|
|
1882
|
+
return tuple(sorted(actions, key=lambda value: value.action_sha256))
|
|
1883
|
+
|
|
1884
|
+
@staticmethod
|
|
1885
|
+
def _cold_start_tie_key(
|
|
1886
|
+
actions: tuple[MaterializedActionDescriptor, ...],
|
|
1887
|
+
) -> tuple[tuple[int, ...], int, tuple[str, ...]]:
|
|
1888
|
+
"""Respect native expert order without comparing heterogeneous scores."""
|
|
1889
|
+
|
|
1890
|
+
return (
|
|
1891
|
+
tuple(sorted(value.native_rank for value in actions)),
|
|
1892
|
+
-len({value.expert_id for value in actions}),
|
|
1893
|
+
tuple(value.action_sha256 for value in actions),
|
|
1894
|
+
)
|
|
1895
|
+
|
|
1896
|
+
@staticmethod
|
|
1897
|
+
def _unique_phenotypes(
|
|
1898
|
+
actions: tuple[MaterializedActionDescriptor, ...],
|
|
1899
|
+
) -> bool:
|
|
1900
|
+
return len({value.phenotype_identity_sha256 for value in actions}) == len(
|
|
1901
|
+
actions
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
def _slate_score(
|
|
1905
|
+
self,
|
|
1906
|
+
actions: tuple[MaterializedActionDescriptor, ...],
|
|
1907
|
+
scores: dict[str, BrokerActionScore],
|
|
1908
|
+
value_port: MaterializedSlateValuePort,
|
|
1909
|
+
cache: dict[tuple[str, ...], tuple[float, float, str | None]],
|
|
1910
|
+
) -> tuple[float, float, str | None]:
|
|
1911
|
+
identity = tuple(value.action_sha256 for value in actions)
|
|
1912
|
+
cached = cache.get(identity)
|
|
1913
|
+
if cached is not None:
|
|
1914
|
+
return cached
|
|
1915
|
+
residual_complementarity = value_port.value(actions)
|
|
1916
|
+
_require_probability(
|
|
1917
|
+
residual_complementarity,
|
|
1918
|
+
name="residual complementarity",
|
|
1919
|
+
)
|
|
1920
|
+
# Shapley-attributed archive returns are additive contributions, not
|
|
1921
|
+
# independent success probabilities. Sum their empirical means and
|
|
1922
|
+
# cap only at the normalized archive-return boundary.
|
|
1923
|
+
member_return = min(
|
|
1924
|
+
1.0,
|
|
1925
|
+
math.fsum(scores[value.action_sha256].value for value in actions),
|
|
1926
|
+
)
|
|
1927
|
+
|
|
1928
|
+
# Information is a scarce action, not a bonus silently attached to
|
|
1929
|
+
# every member. At most one nonterminal member receives its optimistic
|
|
1930
|
+
# increment; all remaining slots are selected by empirical return.
|
|
1931
|
+
exploration_action_sha256: str | None = None
|
|
1932
|
+
if actions[0].context.phase is not SearchPhase.TERMINAL_CONVERSION:
|
|
1933
|
+
exploratory = max(
|
|
1934
|
+
actions,
|
|
1935
|
+
key=lambda value: (
|
|
1936
|
+
scores[value.action_sha256].selection_index
|
|
1937
|
+
- scores[value.action_sha256].value,
|
|
1938
|
+
scores[value.action_sha256].selection_index,
|
|
1939
|
+
value.action_sha256,
|
|
1940
|
+
),
|
|
1941
|
+
)
|
|
1942
|
+
exploratory_score = scores[exploratory.action_sha256]
|
|
1943
|
+
exploration_increment = min(
|
|
1944
|
+
1.0 - member_return,
|
|
1945
|
+
exploratory_score.selection_index - exploratory_score.value,
|
|
1946
|
+
)
|
|
1947
|
+
if exploration_increment > 0.0:
|
|
1948
|
+
member_return += exploration_increment
|
|
1949
|
+
exploration_action_sha256 = exploratory.action_sha256
|
|
1950
|
+
|
|
1951
|
+
slate_return = member_return + (1.0 - member_return) * residual_complementarity
|
|
1952
|
+
result = (
|
|
1953
|
+
float(slate_return),
|
|
1954
|
+
residual_complementarity,
|
|
1955
|
+
exploration_action_sha256,
|
|
1956
|
+
)
|
|
1957
|
+
cache[identity] = result
|
|
1958
|
+
return result
|
|
1959
|
+
|
|
1960
|
+
def _required_references(
|
|
1961
|
+
self,
|
|
1962
|
+
request: MaterializedActionBrokerRequest,
|
|
1963
|
+
scores: dict[str, BrokerActionScore],
|
|
1964
|
+
) -> tuple[tuple[str, ...], int]:
|
|
1965
|
+
if request.reference_escrow_slots == 0:
|
|
1966
|
+
return (), 0
|
|
1967
|
+
references = tuple(value for value in request.actions if value.reference_action)
|
|
1968
|
+
nonreferences = tuple(
|
|
1969
|
+
value for value in request.actions if not value.reference_action
|
|
1970
|
+
)
|
|
1971
|
+
if not references:
|
|
1972
|
+
return (), 0
|
|
1973
|
+
ordered_references = sorted(
|
|
1974
|
+
references,
|
|
1975
|
+
key=lambda value: (
|
|
1976
|
+
value.native_rank,
|
|
1977
|
+
-scores[value.action_sha256].value,
|
|
1978
|
+
value.action_sha256,
|
|
1979
|
+
),
|
|
1980
|
+
)
|
|
1981
|
+
protected = ordered_references[: request.reference_escrow_slots]
|
|
1982
|
+
if not nonreferences or not protected:
|
|
1983
|
+
return tuple(sorted(value.action_sha256 for value in protected)), 0
|
|
1984
|
+
challengers = sorted(
|
|
1985
|
+
nonreferences,
|
|
1986
|
+
key=lambda value: (
|
|
1987
|
+
-scores[value.action_sha256].lower_confidence_bound,
|
|
1988
|
+
-scores[value.action_sha256].value,
|
|
1989
|
+
value.action_sha256,
|
|
1990
|
+
),
|
|
1991
|
+
)
|
|
1992
|
+
retained = list(protected)
|
|
1993
|
+
displaced_count = 0
|
|
1994
|
+
# Challenge the weakest protected reference first. A challenger only
|
|
1995
|
+
# removes escrow authority when its lower bound is strictly above that
|
|
1996
|
+
# reference's upper bound; the joint optimizer still decides whether
|
|
1997
|
+
# the challenger belongs in the final slate.
|
|
1998
|
+
for challenger, reference in zip(
|
|
1999
|
+
challengers,
|
|
2000
|
+
reversed(protected),
|
|
2001
|
+
strict=False,
|
|
2002
|
+
):
|
|
2003
|
+
if (
|
|
2004
|
+
scores[challenger.action_sha256].lower_confidence_bound
|
|
2005
|
+
> scores[reference.action_sha256].upper_confidence_bound
|
|
2006
|
+
):
|
|
2007
|
+
retained.remove(reference)
|
|
2008
|
+
displaced_count += 1
|
|
2009
|
+
return (
|
|
2010
|
+
tuple(sorted(value.action_sha256 for value in retained)),
|
|
2011
|
+
displaced_count,
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
def _admissible(
|
|
2015
|
+
self,
|
|
2016
|
+
slate: tuple[MaterializedActionDescriptor, ...],
|
|
2017
|
+
request: MaterializedActionBrokerRequest,
|
|
2018
|
+
required_references: tuple[str, ...],
|
|
2019
|
+
cache: dict[tuple[tuple[str, ...], tuple[str, ...]], bool],
|
|
2020
|
+
) -> bool:
|
|
2021
|
+
identity = tuple(value.action_sha256 for value in slate)
|
|
2022
|
+
cache_key = (identity, required_references)
|
|
2023
|
+
cached = cache.get(cache_key)
|
|
2024
|
+
if cached is not None:
|
|
2025
|
+
return cached
|
|
2026
|
+
result = (
|
|
2027
|
+
self._unique_phenotypes(slate)
|
|
2028
|
+
and (
|
|
2029
|
+
not required_references
|
|
2030
|
+
or set(required_references).issubset(
|
|
2031
|
+
{value.action_sha256 for value in slate}
|
|
2032
|
+
)
|
|
2033
|
+
)
|
|
2034
|
+
and request.slate_feasibility.permits(slate)
|
|
2035
|
+
)
|
|
2036
|
+
cache[cache_key] = result
|
|
2037
|
+
return result
|
|
2038
|
+
|
|
2039
|
+
def _exact_search(
|
|
2040
|
+
self,
|
|
2041
|
+
request: MaterializedActionBrokerRequest,
|
|
2042
|
+
scores: dict[str, BrokerActionScore],
|
|
2043
|
+
required_references: tuple[str, ...],
|
|
2044
|
+
slate_score_cache: dict[tuple[str, ...], tuple[float, float, str | None]],
|
|
2045
|
+
admissibility_cache: dict[tuple[tuple[str, ...], tuple[str, ...]], bool],
|
|
2046
|
+
) -> tuple[tuple[MaterializedActionDescriptor, ...], float, str | None, int]:
|
|
2047
|
+
action_by_sha256 = {value.action_sha256: value for value in request.actions}
|
|
2048
|
+
if not set(required_references).issubset(action_by_sha256):
|
|
2049
|
+
raise ValueError("required action is absent from the broker request")
|
|
2050
|
+
required = tuple(action_by_sha256[value] for value in required_references)
|
|
2051
|
+
optional = tuple(
|
|
2052
|
+
value
|
|
2053
|
+
for value in request.actions
|
|
2054
|
+
if value.action_sha256 not in required_references
|
|
2055
|
+
)
|
|
2056
|
+
remaining_slots = request.evaluation_slots - len(required)
|
|
2057
|
+
if remaining_slots < 0:
|
|
2058
|
+
raise ValueError("required actions exceed evaluation capacity")
|
|
2059
|
+
best: tuple[MaterializedActionDescriptor, ...] | None = None
|
|
2060
|
+
best_score = -math.inf
|
|
2061
|
+
best_joint = 0.0
|
|
2062
|
+
best_exploration: str | None = None
|
|
2063
|
+
count = 0
|
|
2064
|
+
for raw in combinations(optional, remaining_slots):
|
|
2065
|
+
slate = self._canonical_slate((*required, *raw))
|
|
2066
|
+
if not self._admissible(
|
|
2067
|
+
slate,
|
|
2068
|
+
request,
|
|
2069
|
+
required_references,
|
|
2070
|
+
admissibility_cache,
|
|
2071
|
+
):
|
|
2072
|
+
continue
|
|
2073
|
+
count += 1
|
|
2074
|
+
score, joint, exploration = self._slate_score(
|
|
2075
|
+
slate,
|
|
2076
|
+
scores,
|
|
2077
|
+
request.slate_value,
|
|
2078
|
+
slate_score_cache,
|
|
2079
|
+
)
|
|
2080
|
+
tie_key = self._cold_start_tie_key(slate)
|
|
2081
|
+
best_tie_key = None if best is None else self._cold_start_tie_key(best)
|
|
2082
|
+
if score > best_score or (
|
|
2083
|
+
score == best_score and (best_tie_key is None or tie_key < best_tie_key)
|
|
2084
|
+
):
|
|
2085
|
+
best, best_score, best_joint, best_exploration = (
|
|
2086
|
+
slate,
|
|
2087
|
+
score,
|
|
2088
|
+
joint,
|
|
2089
|
+
exploration,
|
|
2090
|
+
)
|
|
2091
|
+
if best is None:
|
|
2092
|
+
raise ValueError("no feasible complete materialized-action slate exists")
|
|
2093
|
+
return best, best_joint, best_exploration, count
|
|
2094
|
+
|
|
2095
|
+
def _beam_search(
|
|
2096
|
+
self,
|
|
2097
|
+
request: MaterializedActionBrokerRequest,
|
|
2098
|
+
scores: dict[str, BrokerActionScore],
|
|
2099
|
+
required_references: tuple[str, ...],
|
|
2100
|
+
slate_score_cache: dict[tuple[str, ...], tuple[float, float, str | None]],
|
|
2101
|
+
admissibility_cache: dict[tuple[tuple[str, ...], tuple[str, ...]], bool],
|
|
2102
|
+
) -> tuple[tuple[MaterializedActionDescriptor, ...], float, str | None, int]:
|
|
2103
|
+
action_by_sha256 = {value.action_sha256: value for value in request.actions}
|
|
2104
|
+
if not set(required_references).issubset(action_by_sha256):
|
|
2105
|
+
raise ValueError("required action is absent from the broker request")
|
|
2106
|
+
required = tuple(action_by_sha256[value] for value in required_references)
|
|
2107
|
+
ordered = tuple(
|
|
2108
|
+
sorted(
|
|
2109
|
+
(
|
|
2110
|
+
value
|
|
2111
|
+
for value in request.actions
|
|
2112
|
+
if value.action_sha256 not in required_references
|
|
2113
|
+
),
|
|
2114
|
+
key=lambda value: value.action_sha256,
|
|
2115
|
+
)
|
|
2116
|
+
)
|
|
2117
|
+
remaining_slots = request.evaluation_slots - len(required)
|
|
2118
|
+
if remaining_slots < 0:
|
|
2119
|
+
raise ValueError("required actions exceed evaluation capacity")
|
|
2120
|
+
if not self._unique_phenotypes(required):
|
|
2121
|
+
raise ValueError("required actions repeat a materialized phenotype")
|
|
2122
|
+
beam: tuple[tuple[MaterializedActionDescriptor, ...], ...] = ((),)
|
|
2123
|
+
complete: dict[
|
|
2124
|
+
tuple[str, ...],
|
|
2125
|
+
tuple[
|
|
2126
|
+
tuple[MaterializedActionDescriptor, ...],
|
|
2127
|
+
float,
|
|
2128
|
+
float,
|
|
2129
|
+
str | None,
|
|
2130
|
+
],
|
|
2131
|
+
] = {}
|
|
2132
|
+
for _depth in range(remaining_slots):
|
|
2133
|
+
expanded: dict[
|
|
2134
|
+
tuple[str, ...], tuple[MaterializedActionDescriptor, ...]
|
|
2135
|
+
] = {}
|
|
2136
|
+
for partial in beam:
|
|
2137
|
+
# ``partial`` is already canonical. Extending only with a
|
|
2138
|
+
# larger identity enumerates each unordered slate once instead
|
|
2139
|
+
# of revisiting every permutation before dictionary dedup.
|
|
2140
|
+
lower_bound = "" if not partial else partial[-1].action_sha256
|
|
2141
|
+
for action in ordered:
|
|
2142
|
+
if action.action_sha256 <= lower_bound:
|
|
2143
|
+
continue
|
|
2144
|
+
candidate = (*partial, action)
|
|
2145
|
+
candidate_slate = self._canonical_slate((*required, *candidate))
|
|
2146
|
+
if not self._unique_phenotypes(candidate_slate):
|
|
2147
|
+
continue
|
|
2148
|
+
remaining_after_candidate = remaining_slots - len(candidate)
|
|
2149
|
+
if remaining_after_candidate:
|
|
2150
|
+
used_phenotypes = {
|
|
2151
|
+
value.phenotype_identity_sha256 for value in candidate_slate
|
|
2152
|
+
}
|
|
2153
|
+
available_suffix_phenotypes = {
|
|
2154
|
+
value.phenotype_identity_sha256
|
|
2155
|
+
for value in ordered
|
|
2156
|
+
if (
|
|
2157
|
+
value.action_sha256 > action.action_sha256
|
|
2158
|
+
and value.phenotype_identity_sha256
|
|
2159
|
+
not in used_phenotypes
|
|
2160
|
+
)
|
|
2161
|
+
}
|
|
2162
|
+
if len(available_suffix_phenotypes) < remaining_after_candidate:
|
|
2163
|
+
# The canonical identity ordering is an
|
|
2164
|
+
# enumeration device, not a quality signal. A
|
|
2165
|
+
# bounded beam must not retain a high-scoring
|
|
2166
|
+
# late-identity partial that can no longer be
|
|
2167
|
+
# completed while pruning every feasible prefix.
|
|
2168
|
+
continue
|
|
2169
|
+
if not self._admissible(
|
|
2170
|
+
candidate_slate,
|
|
2171
|
+
request,
|
|
2172
|
+
required_references=(),
|
|
2173
|
+
cache=admissibility_cache,
|
|
2174
|
+
):
|
|
2175
|
+
continue
|
|
2176
|
+
identity = tuple(value.action_sha256 for value in candidate)
|
|
2177
|
+
expanded[identity] = candidate
|
|
2178
|
+
ranked = sorted(
|
|
2179
|
+
expanded.values(),
|
|
2180
|
+
key=lambda partial: (
|
|
2181
|
+
-self._slate_score(
|
|
2182
|
+
self._canonical_slate((*required, *partial)),
|
|
2183
|
+
scores,
|
|
2184
|
+
request.slate_value,
|
|
2185
|
+
slate_score_cache,
|
|
2186
|
+
)[0],
|
|
2187
|
+
self._cold_start_tie_key(
|
|
2188
|
+
self._canonical_slate((*required, *partial))
|
|
2189
|
+
),
|
|
2190
|
+
),
|
|
2191
|
+
)
|
|
2192
|
+
beam = tuple(ranked[: self.beam_width])
|
|
2193
|
+
if not beam:
|
|
2194
|
+
break
|
|
2195
|
+
for partial in beam:
|
|
2196
|
+
slate = self._canonical_slate((*required, *partial))
|
|
2197
|
+
if len(slate) != request.evaluation_slots or not self._admissible(
|
|
2198
|
+
slate,
|
|
2199
|
+
request,
|
|
2200
|
+
required_references,
|
|
2201
|
+
admissibility_cache,
|
|
2202
|
+
):
|
|
2203
|
+
continue
|
|
2204
|
+
score, joint, exploration = self._slate_score(
|
|
2205
|
+
slate,
|
|
2206
|
+
scores,
|
|
2207
|
+
request.slate_value,
|
|
2208
|
+
slate_score_cache,
|
|
2209
|
+
)
|
|
2210
|
+
identity = tuple(value.action_sha256 for value in slate)
|
|
2211
|
+
complete[identity] = (slate, score, joint, exploration)
|
|
2212
|
+
if not complete:
|
|
2213
|
+
raise ValueError(
|
|
2214
|
+
"beam search found no feasible complete materialized slate"
|
|
2215
|
+
)
|
|
2216
|
+
best = min(
|
|
2217
|
+
complete.values(),
|
|
2218
|
+
key=lambda value: (
|
|
2219
|
+
-value[1],
|
|
2220
|
+
self._cold_start_tie_key(value[0]),
|
|
2221
|
+
),
|
|
2222
|
+
)
|
|
2223
|
+
return best[0], best[2], best[3], len(complete)
|
|
2224
|
+
|
|
2225
|
+
def select(
|
|
2226
|
+
self,
|
|
2227
|
+
request: MaterializedActionBrokerRequest,
|
|
2228
|
+
) -> MaterializedActionBrokerDecision:
|
|
2229
|
+
if type(request) is not MaterializedActionBrokerRequest:
|
|
2230
|
+
raise TypeError("request must be an exact MaterializedActionBrokerRequest")
|
|
2231
|
+
request.__post_init__()
|
|
2232
|
+
scores = {value.action_sha256: self.score(value) for value in request.actions}
|
|
2233
|
+
required_references, displaced_count = self._required_references(
|
|
2234
|
+
request,
|
|
2235
|
+
scores,
|
|
2236
|
+
)
|
|
2237
|
+
exploration_requirement = (
|
|
2238
|
+
None
|
|
2239
|
+
if self.exploration_policy is None
|
|
2240
|
+
else self.exploration_policy.require(
|
|
2241
|
+
request,
|
|
2242
|
+
self.ledger,
|
|
2243
|
+
required_references,
|
|
2244
|
+
)
|
|
2245
|
+
)
|
|
2246
|
+
if exploration_requirement is not None:
|
|
2247
|
+
if (
|
|
2248
|
+
type(exploration_requirement)
|
|
2249
|
+
is not MaterializedActionExplorationRequirement
|
|
2250
|
+
):
|
|
2251
|
+
raise TypeError("exploration policy returned a foreign requirement")
|
|
2252
|
+
exploration_requirement.__post_init__()
|
|
2253
|
+
if (
|
|
2254
|
+
exploration_requirement.policy_id != self.exploration_policy.policy_id
|
|
2255
|
+
or exploration_requirement.policy_version
|
|
2256
|
+
!= self.exploration_policy.policy_version
|
|
2257
|
+
or exploration_requirement.policy_definition_sha256
|
|
2258
|
+
!= self.exploration_policy.definition_sha256
|
|
2259
|
+
):
|
|
2260
|
+
raise ValueError("exploration requirement differs from its policy")
|
|
2261
|
+
action_sha256s = {value.action_sha256 for value in request.actions}
|
|
2262
|
+
if not set(exploration_requirement.required_action_sha256s).issubset(
|
|
2263
|
+
action_sha256s
|
|
2264
|
+
):
|
|
2265
|
+
raise ValueError(
|
|
2266
|
+
"exploration policy required an action outside the request"
|
|
2267
|
+
)
|
|
2268
|
+
required_exploration = (
|
|
2269
|
+
()
|
|
2270
|
+
if exploration_requirement is None
|
|
2271
|
+
else exploration_requirement.required_action_sha256s
|
|
2272
|
+
)
|
|
2273
|
+
required_allocation = (
|
|
2274
|
+
()
|
|
2275
|
+
if request.allocation_requirement is None
|
|
2276
|
+
else request.allocation_requirement.required_action_sha256s
|
|
2277
|
+
)
|
|
2278
|
+
required_actions = tuple(
|
|
2279
|
+
sorted(
|
|
2280
|
+
set(required_references)
|
|
2281
|
+
| set(required_exploration)
|
|
2282
|
+
| set(required_allocation)
|
|
2283
|
+
)
|
|
2284
|
+
)
|
|
2285
|
+
if len(required_actions) > request.evaluation_slots:
|
|
2286
|
+
raise ValueError("reference and exploration requirements exceed capacity")
|
|
2287
|
+
slate_score_cache: dict[tuple[str, ...], tuple[float, float, str | None]] = {}
|
|
2288
|
+
admissibility_cache: dict[tuple[tuple[str, ...], tuple[str, ...]], bool] = {}
|
|
2289
|
+
optional_action_count = len(request.actions) - len(required_actions)
|
|
2290
|
+
remaining_slot_count = request.evaluation_slots - len(required_actions)
|
|
2291
|
+
combination_count = math.comb(
|
|
2292
|
+
optional_action_count,
|
|
2293
|
+
remaining_slot_count,
|
|
2294
|
+
)
|
|
2295
|
+
if combination_count <= self.exact_combination_limit:
|
|
2296
|
+
selected, joint, exploration, considered = self._exact_search(
|
|
2297
|
+
request,
|
|
2298
|
+
scores,
|
|
2299
|
+
required_actions,
|
|
2300
|
+
slate_score_cache,
|
|
2301
|
+
admissibility_cache,
|
|
2302
|
+
)
|
|
2303
|
+
mode = "exact_joint"
|
|
2304
|
+
else:
|
|
2305
|
+
selected, joint, exploration, considered = self._beam_search(
|
|
2306
|
+
request,
|
|
2307
|
+
scores,
|
|
2308
|
+
required_actions,
|
|
2309
|
+
slate_score_cache,
|
|
2310
|
+
admissibility_cache,
|
|
2311
|
+
)
|
|
2312
|
+
mode = "bounded_joint_beam"
|
|
2313
|
+
ordered_scores = tuple(
|
|
2314
|
+
scores[action.action_sha256]
|
|
2315
|
+
for action in sorted(request.actions, key=lambda value: value.action_sha256)
|
|
2316
|
+
)
|
|
2317
|
+
return MaterializedActionBrokerDecision(
|
|
2318
|
+
selected_actions=selected,
|
|
2319
|
+
scores=ordered_scores,
|
|
2320
|
+
required_reference_action_sha256s=required_references,
|
|
2321
|
+
exploration_requirement=exploration_requirement,
|
|
2322
|
+
allocation_requirement=request.allocation_requirement,
|
|
2323
|
+
reference_displaced_count=displaced_count,
|
|
2324
|
+
search_mode=mode,
|
|
2325
|
+
complete_slate_count_considered=considered,
|
|
2326
|
+
residual_complementarity_value=joint,
|
|
2327
|
+
exploration_action_sha256=exploration,
|
|
2328
|
+
)
|