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,2950 @@
|
|
|
1
|
+
"""One-call ranked portfolio selection with concurrent exact evaluation.
|
|
2
|
+
|
|
3
|
+
The model selects only opaque IDs from a parent-bound finite contract. This
|
|
4
|
+
application service resolves every ranked ID, derives an exact parent-relative
|
|
5
|
+
patch, materializes every sealed child under engine authority, and submits the
|
|
6
|
+
whole wave through ``AgenticEvolutionEngine.run_materialized_invocations``.
|
|
7
|
+
Optional insight credit is recorded once for the complete wave, never once per
|
|
8
|
+
child.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import re
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from dataclasses import dataclass, field, replace
|
|
19
|
+
from decimal import Decimal
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import Protocol, runtime_checkable
|
|
22
|
+
|
|
23
|
+
from agent_evolve.application.agentic_evolution import (
|
|
24
|
+
EvolutionCandidate,
|
|
25
|
+
InvocationOutcome,
|
|
26
|
+
InvocationPlan,
|
|
27
|
+
MaterializedInvocation,
|
|
28
|
+
OperatorKind,
|
|
29
|
+
ProposalAuthority,
|
|
30
|
+
RewardPolicyBinding,
|
|
31
|
+
)
|
|
32
|
+
from agent_evolve.application.insight_memory import (
|
|
33
|
+
InsightLifecycleState,
|
|
34
|
+
InsightMemoryBank,
|
|
35
|
+
InsightOrigin,
|
|
36
|
+
QuarantineTestAdmissionReceipt,
|
|
37
|
+
)
|
|
38
|
+
from agent_evolve.application.outcome_relation import OutcomeRelation
|
|
39
|
+
from agent_evolve.application.portfolio_memory_matched_control import (
|
|
40
|
+
PortfolioMemoryMatchedArmAssignment,
|
|
41
|
+
PortfolioMemoryMatchedArmView,
|
|
42
|
+
PortfolioMemoryMatchedControlPlan,
|
|
43
|
+
)
|
|
44
|
+
from agent_evolve.domain.ids import (
|
|
45
|
+
ArtifactId,
|
|
46
|
+
CandidateId,
|
|
47
|
+
LLMCallId,
|
|
48
|
+
OperatorInvocationId,
|
|
49
|
+
)
|
|
50
|
+
from agent_evolve.domain.insight import InsightRef
|
|
51
|
+
from agent_evolve.domain.outcome import (
|
|
52
|
+
FailureCategory,
|
|
53
|
+
FailureCode,
|
|
54
|
+
FailureRecord,
|
|
55
|
+
validate_failure_pair,
|
|
56
|
+
)
|
|
57
|
+
from agent_evolve.domain.patch import ArrayIndex, JsonPath, ObjectKey, require_sha256
|
|
58
|
+
from agent_evolve.domain.typed_json import (
|
|
59
|
+
FrozenJsonObject,
|
|
60
|
+
freeze_json,
|
|
61
|
+
thaw_json,
|
|
62
|
+
typed_json_equal,
|
|
63
|
+
typed_json_sha256,
|
|
64
|
+
)
|
|
65
|
+
from agent_evolve.policies.memory.randomized_subset import (
|
|
66
|
+
InsightSelectionDecision,
|
|
67
|
+
InsightTrial,
|
|
68
|
+
)
|
|
69
|
+
from agent_evolve.policies.memory.staged_causal import (
|
|
70
|
+
MemoryScoreSnapshot,
|
|
71
|
+
ResolvedInsightAssignment,
|
|
72
|
+
insight_selection_decision_sha256,
|
|
73
|
+
)
|
|
74
|
+
from agent_evolve.policies.variation.typed_patch import derive_patch
|
|
75
|
+
from agent_evolve.ports.agentic_generator import (
|
|
76
|
+
AgenticCallTelemetry,
|
|
77
|
+
CandidateDraft,
|
|
78
|
+
MetricEffectPrediction,
|
|
79
|
+
SourceAttribution,
|
|
80
|
+
)
|
|
81
|
+
from agent_evolve.ports.id_factory import IdFactory
|
|
82
|
+
from agent_evolve.ports.portfolio_selection import (
|
|
83
|
+
PortfolioSelectionPolicy,
|
|
84
|
+
PortfolioSelectionRequest,
|
|
85
|
+
PortfolioSelectionResult,
|
|
86
|
+
PortfolioSelectionSupplementalAudit,
|
|
87
|
+
RankedPortfolioDecision,
|
|
88
|
+
RankedPortfolioMember,
|
|
89
|
+
validate_ranked_portfolio_decision,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
PORTFOLIO_MATERIALIZATION_POLICY_ID = "ranked_portfolio_exact_finite_option"
|
|
94
|
+
PORTFOLIO_MATERIALIZATION_POLICY_VERSION = 1
|
|
95
|
+
|
|
96
|
+
_TOKEN = re.compile(r"^[a-z][a-z0-9_.-]{0,95}$")
|
|
97
|
+
_TELEMETRY_DOMAIN = b"agent-evolve:portfolio-evolution-telemetry:v1\x00"
|
|
98
|
+
_MATERIALIZATION_DOMAIN = b"agent-evolve:portfolio-evolution-materialization:v1\x00"
|
|
99
|
+
_MEMBER_DOMAIN = b"agent-evolve:portfolio-evolution-member:v1\x00"
|
|
100
|
+
_CANDIDATE_FAILURE_DOMAIN = (
|
|
101
|
+
b"agent-evolve:portfolio-candidate-failure-evidence:v1\x00"
|
|
102
|
+
)
|
|
103
|
+
_ACTION_CARD_ATTRIBUTION_DOMAIN = (
|
|
104
|
+
b"agent-evolve:portfolio-action-card-attribution:v1\x00"
|
|
105
|
+
)
|
|
106
|
+
_ACTION_ATTRIBUTION_DOMAIN = b"agent-evolve:portfolio-action-attribution:v1\x00"
|
|
107
|
+
_MEMORY_CREDIT_DOMAIN = b"agent-evolve:portfolio-evolution-memory-credit:v1\x00"
|
|
108
|
+
_PENDING_MEMORY_CREDIT_DOMAIN = (
|
|
109
|
+
b"agent-evolve:portfolio-evolution-pending-memory-credit:v1\x00"
|
|
110
|
+
)
|
|
111
|
+
_MEMORY_CREDIT_BATCH_DOMAIN = (
|
|
112
|
+
b"agent-evolve:portfolio-evolution-memory-credit-batch:v1\x00"
|
|
113
|
+
)
|
|
114
|
+
_MEMORY_CREDIT_BATCH_PREPARATION_DOMAIN = (
|
|
115
|
+
b"agent-evolve:portfolio-evolution-memory-credit-batch-preparation:v1\x00"
|
|
116
|
+
)
|
|
117
|
+
_MEMORY_TREATMENT_BINDING_DOMAIN = (
|
|
118
|
+
b"agent-evolve:portfolio-memory-treatment-binding:v1\x00"
|
|
119
|
+
)
|
|
120
|
+
_MEMORY_CONTEXT_PROJECTION_DOMAIN = (
|
|
121
|
+
b"agent-evolve:portfolio-memory-context-projection:v1\x00"
|
|
122
|
+
)
|
|
123
|
+
_WAVE_DOMAIN = b"agent-evolve:portfolio-evolution-wave:v1\x00"
|
|
124
|
+
_PREPARED_WAVE_DOMAIN = b"agent-evolve:portfolio-evolution-prepared-wave:v1\x00"
|
|
125
|
+
_AGGREGATION_BINDING_DOMAIN = (
|
|
126
|
+
b"agent-evolve:portfolio-reward-aggregation-binding:v1\x00"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _canonical_json(record: dict[str, object]) -> bytes:
|
|
131
|
+
return json.dumps(
|
|
132
|
+
record,
|
|
133
|
+
ensure_ascii=True,
|
|
134
|
+
allow_nan=False,
|
|
135
|
+
separators=(",", ":"),
|
|
136
|
+
sort_keys=True,
|
|
137
|
+
).encode("ascii", errors="strict")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _hash_record(domain: bytes, record: dict[str, object]) -> str:
|
|
141
|
+
return hashlib.sha256(domain + _canonical_json(record)).hexdigest()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _require_token(value: str, name: str) -> None:
|
|
145
|
+
if type(value) is not str or _TOKEN.fullmatch(value) is None:
|
|
146
|
+
raise ValueError(f"{name} must use the closed lowercase token grammar")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _path_text(path: JsonPath) -> str:
|
|
150
|
+
"""Project a typed patch path into the engine's candidate-path vocabulary."""
|
|
151
|
+
|
|
152
|
+
parts = ["$"]
|
|
153
|
+
for segment in path.segments:
|
|
154
|
+
if type(segment) is ObjectKey:
|
|
155
|
+
parts.append(f".{segment.value}")
|
|
156
|
+
elif type(segment) is ArrayIndex:
|
|
157
|
+
parts.append(f"[{segment.value}]")
|
|
158
|
+
else: # pragma: no cover - JsonPath closes the segment union.
|
|
159
|
+
raise AssertionError("unsupported JSON-path segment")
|
|
160
|
+
return "".join(parts)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _telemetry_record(telemetry: AgenticCallTelemetry) -> dict[str, object]:
|
|
164
|
+
if type(telemetry) is not AgenticCallTelemetry:
|
|
165
|
+
raise TypeError("selection telemetry must be exact")
|
|
166
|
+
AgenticCallTelemetry.__post_init__(telemetry)
|
|
167
|
+
for name in ("provider_response_id", "finish_reason"):
|
|
168
|
+
value = getattr(telemetry, name)
|
|
169
|
+
if value is not None and type(value) is not str:
|
|
170
|
+
raise TypeError(f"telemetry {name} must be an exact string or None")
|
|
171
|
+
cost = telemetry.cost_usd
|
|
172
|
+
if cost is not None:
|
|
173
|
+
if type(cost) is not Decimal:
|
|
174
|
+
raise TypeError("telemetry cost_usd must be an exact Decimal or None")
|
|
175
|
+
if not cost.is_finite() or cost < 0:
|
|
176
|
+
raise ValueError("telemetry cost_usd must be finite and non-negative")
|
|
177
|
+
return {
|
|
178
|
+
"requested_model": telemetry.requested_model,
|
|
179
|
+
"resolved_model": telemetry.resolved_model,
|
|
180
|
+
"resolved_provider": telemetry.resolved_provider,
|
|
181
|
+
"provider_response_id": telemetry.provider_response_id,
|
|
182
|
+
"finish_reason": telemetry.finish_reason,
|
|
183
|
+
"input_tokens": telemetry.input_tokens,
|
|
184
|
+
"output_tokens": telemetry.output_tokens,
|
|
185
|
+
"reasoning_tokens": telemetry.reasoning_tokens,
|
|
186
|
+
"cache_read_tokens": telemetry.cache_read_tokens,
|
|
187
|
+
"cache_write_tokens": telemetry.cache_write_tokens,
|
|
188
|
+
"cost_usd": None if cost is None else str(cost),
|
|
189
|
+
"latency_ns": telemetry.latency_ns,
|
|
190
|
+
"attempt_count": telemetry.attempt_count,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def portfolio_selection_telemetry_sha256(
|
|
195
|
+
telemetry: AgenticCallTelemetry,
|
|
196
|
+
) -> str:
|
|
197
|
+
"""Return the exact identity of one ranked-selector call's telemetry."""
|
|
198
|
+
|
|
199
|
+
return _hash_record(_TELEMETRY_DOMAIN, _telemetry_record(telemetry))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
PortfolioRewardAggregator = Callable[[tuple[InvocationOutcome, ...]], float]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
EXACT_MEMORY_CONTEXT_PROJECTION_DEFINITION_SHA256 = hashlib.sha256(
|
|
206
|
+
b"agent-evolve:portfolio-memory-context-projection:exact-identity:v1"
|
|
207
|
+
).hexdigest()
|
|
208
|
+
MEMORY_ESTIMAND_CONTEXT_KEY = "memory_estimand_context"
|
|
209
|
+
MEMORY_ESTIMAND_SUBTREE_PROJECTION_DEFINITION_SHA256 = hashlib.sha256(
|
|
210
|
+
b"agent-evolve:portfolio-memory-context-projection:reserved-root-object-subtree:v1"
|
|
211
|
+
).hexdigest()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass(frozen=True, slots=True)
|
|
215
|
+
class PortfolioRewardAggregationBinding:
|
|
216
|
+
"""Identified aggregate endpoint for one multi-candidate credit unit."""
|
|
217
|
+
|
|
218
|
+
aggregate: PortfolioRewardAggregator
|
|
219
|
+
aggregation_id: str
|
|
220
|
+
aggregation_version: int
|
|
221
|
+
definition_sha256: str
|
|
222
|
+
|
|
223
|
+
def __post_init__(self) -> None:
|
|
224
|
+
if not callable(self.aggregate):
|
|
225
|
+
raise TypeError("aggregate must be callable")
|
|
226
|
+
_require_token(self.aggregation_id, "aggregation_id")
|
|
227
|
+
if type(self.aggregation_version) is not int or self.aggregation_version <= 0:
|
|
228
|
+
raise ValueError("aggregation_version must be a positive exact integer")
|
|
229
|
+
require_sha256(self.definition_sha256, "definition_sha256")
|
|
230
|
+
|
|
231
|
+
def to_record(self) -> dict[str, object]:
|
|
232
|
+
self.__post_init__()
|
|
233
|
+
return {
|
|
234
|
+
"aggregation_id": self.aggregation_id,
|
|
235
|
+
"aggregation_version": self.aggregation_version,
|
|
236
|
+
"definition_sha256": self.definition_sha256,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def binding_sha256(self) -> str:
|
|
241
|
+
return _hash_record(_AGGREGATION_BINDING_DOMAIN, self.to_record())
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True, slots=True)
|
|
245
|
+
class PortfolioMemoryContextProjectionBinding:
|
|
246
|
+
"""Replay one core-owned selector-context to estimand-context projection."""
|
|
247
|
+
|
|
248
|
+
estimand_context_sha256: str
|
|
249
|
+
selector_context_sha256: str
|
|
250
|
+
projection_key: str | None
|
|
251
|
+
|
|
252
|
+
def __post_init__(self) -> None:
|
|
253
|
+
require_sha256(self.estimand_context_sha256, "estimand_context_sha256")
|
|
254
|
+
require_sha256(self.selector_context_sha256, "selector_context_sha256")
|
|
255
|
+
if self.projection_key not in (None, MEMORY_ESTIMAND_CONTEXT_KEY):
|
|
256
|
+
raise ValueError("projection_key must use a core-owned projection")
|
|
257
|
+
if (
|
|
258
|
+
self.projection_key is None
|
|
259
|
+
and self.estimand_context_sha256 != self.selector_context_sha256
|
|
260
|
+
):
|
|
261
|
+
raise ValueError("exact context projection requires identical hashes")
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def projection_id(self) -> str:
|
|
265
|
+
return (
|
|
266
|
+
"exact_context_identity"
|
|
267
|
+
if self.projection_key is None
|
|
268
|
+
else "reserved_memory_estimand_subtree"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def projection_version(self) -> int:
|
|
273
|
+
return 1
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def definition_sha256(self) -> str:
|
|
277
|
+
return (
|
|
278
|
+
EXACT_MEMORY_CONTEXT_PROJECTION_DEFINITION_SHA256
|
|
279
|
+
if self.projection_key is None
|
|
280
|
+
else MEMORY_ESTIMAND_SUBTREE_PROJECTION_DEFINITION_SHA256
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def exact_identity(cls, context_sha256: str):
|
|
285
|
+
require_sha256(context_sha256, "context_sha256")
|
|
286
|
+
return cls(
|
|
287
|
+
estimand_context_sha256=context_sha256,
|
|
288
|
+
selector_context_sha256=context_sha256,
|
|
289
|
+
projection_key=None,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def from_selector_context(
|
|
294
|
+
cls,
|
|
295
|
+
selector_context: FrozenJsonObject,
|
|
296
|
+
) -> "PortfolioMemoryContextProjectionBinding":
|
|
297
|
+
projected = cls._project_reserved_subtree(selector_context)
|
|
298
|
+
return cls(
|
|
299
|
+
estimand_context_sha256=typed_json_sha256(projected),
|
|
300
|
+
selector_context_sha256=typed_json_sha256(selector_context),
|
|
301
|
+
projection_key=MEMORY_ESTIMAND_CONTEXT_KEY,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def _project_reserved_subtree(
|
|
306
|
+
selector_context: FrozenJsonObject,
|
|
307
|
+
) -> FrozenJsonObject:
|
|
308
|
+
if type(selector_context) is not FrozenJsonObject:
|
|
309
|
+
raise TypeError("selector_context must be an exact FrozenJsonObject")
|
|
310
|
+
values = dict(selector_context.items)
|
|
311
|
+
projected = values.get(MEMORY_ESTIMAND_CONTEXT_KEY)
|
|
312
|
+
if type(projected) is not FrozenJsonObject:
|
|
313
|
+
raise ValueError(
|
|
314
|
+
"selector context must contain the reserved memory estimand object"
|
|
315
|
+
)
|
|
316
|
+
return projected
|
|
317
|
+
|
|
318
|
+
def replay(self, selector_context: FrozenJsonObject) -> FrozenJsonObject:
|
|
319
|
+
self.__post_init__()
|
|
320
|
+
observed_selector_sha256 = typed_json_sha256(selector_context)
|
|
321
|
+
if observed_selector_sha256 != self.selector_context_sha256:
|
|
322
|
+
raise ValueError(
|
|
323
|
+
"context projection selector hash differs from selector context"
|
|
324
|
+
)
|
|
325
|
+
if self.projection_key is None:
|
|
326
|
+
projected = selector_context
|
|
327
|
+
else:
|
|
328
|
+
projected = self._project_reserved_subtree(selector_context)
|
|
329
|
+
if typed_json_sha256(projected) != self.estimand_context_sha256:
|
|
330
|
+
raise ValueError("context projection subtree differs from estimand context")
|
|
331
|
+
return projected
|
|
332
|
+
|
|
333
|
+
def to_record(self) -> dict[str, object]:
|
|
334
|
+
self.__post_init__()
|
|
335
|
+
return {
|
|
336
|
+
"schema_version": 1,
|
|
337
|
+
"estimand_context_sha256": self.estimand_context_sha256,
|
|
338
|
+
"selector_context_sha256": self.selector_context_sha256,
|
|
339
|
+
"projection_path": (
|
|
340
|
+
[] if self.projection_key is None else [self.projection_key]
|
|
341
|
+
),
|
|
342
|
+
"projection_id": self.projection_id,
|
|
343
|
+
"projection_version": self.projection_version,
|
|
344
|
+
"definition_sha256": self.definition_sha256,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def binding_sha256(self) -> str:
|
|
349
|
+
return _hash_record(_MEMORY_CONTEXT_PROJECTION_DOMAIN, self.to_record())
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@dataclass(frozen=True, slots=True)
|
|
353
|
+
class PortfolioMemoryCreditPlan:
|
|
354
|
+
"""One randomized card assignment credited once across the complete wave."""
|
|
355
|
+
|
|
356
|
+
decision: InsightSelectionDecision
|
|
357
|
+
credit_unit_id: OperatorInvocationId
|
|
358
|
+
aggregation: PortfolioRewardAggregationBinding
|
|
359
|
+
card_snapshot_sha256: str
|
|
360
|
+
score_snapshot: MemoryScoreSnapshot
|
|
361
|
+
assignment: ResolvedInsightAssignment
|
|
362
|
+
card_source_registry_sha256: str | None = None
|
|
363
|
+
quarantine_admission: QuarantineTestAdmissionReceipt | None = None
|
|
364
|
+
quarantine_admission_subset_authorization_sha256: str | None = None
|
|
365
|
+
context_projection: PortfolioMemoryContextProjectionBinding | None = None
|
|
366
|
+
|
|
367
|
+
def __post_init__(self) -> None:
|
|
368
|
+
if type(self.decision) is not InsightSelectionDecision:
|
|
369
|
+
raise TypeError("decision must be an exact InsightSelectionDecision")
|
|
370
|
+
InsightSelectionDecision.__post_init__(self.decision)
|
|
371
|
+
if not self.decision.credit_identifiable:
|
|
372
|
+
raise ValueError("portfolio memory credit requires identifiable overlap")
|
|
373
|
+
if type(self.credit_unit_id) is not OperatorInvocationId:
|
|
374
|
+
raise TypeError("credit_unit_id must be an exact OperatorInvocationId")
|
|
375
|
+
OperatorInvocationId.__post_init__(self.credit_unit_id)
|
|
376
|
+
if type(self.aggregation) is not PortfolioRewardAggregationBinding:
|
|
377
|
+
raise TypeError("aggregation must be an exact binding")
|
|
378
|
+
PortfolioRewardAggregationBinding.__post_init__(self.aggregation)
|
|
379
|
+
require_sha256(self.card_snapshot_sha256, "card_snapshot_sha256")
|
|
380
|
+
if type(self.score_snapshot) is not MemoryScoreSnapshot:
|
|
381
|
+
raise TypeError("score_snapshot must be an exact MemoryScoreSnapshot")
|
|
382
|
+
MemoryScoreSnapshot.__post_init__(self.score_snapshot)
|
|
383
|
+
if type(self.assignment) is not ResolvedInsightAssignment:
|
|
384
|
+
raise TypeError("assignment must be an exact ResolvedInsightAssignment")
|
|
385
|
+
ResolvedInsightAssignment.__post_init__(self.assignment)
|
|
386
|
+
self.assignment.validate_against_snapshot(self.score_snapshot)
|
|
387
|
+
if (
|
|
388
|
+
self.assignment.credit_unit_id != self.credit_unit_id
|
|
389
|
+
or self.assignment.selection_decision != self.decision
|
|
390
|
+
or self.assignment.prompt_shape_sha256 != self.card_snapshot_sha256
|
|
391
|
+
):
|
|
392
|
+
raise ValueError(
|
|
393
|
+
"resolved assignment differs from the credit unit, decision, or cards"
|
|
394
|
+
)
|
|
395
|
+
if self.card_source_registry_sha256 is not None:
|
|
396
|
+
require_sha256(
|
|
397
|
+
self.card_source_registry_sha256,
|
|
398
|
+
"card_source_registry_sha256",
|
|
399
|
+
)
|
|
400
|
+
if self.quarantine_admission is not None:
|
|
401
|
+
if type(self.quarantine_admission) is not QuarantineTestAdmissionReceipt:
|
|
402
|
+
raise TypeError("quarantine_admission must be an exact receipt or None")
|
|
403
|
+
QuarantineTestAdmissionReceipt.__post_init__(
|
|
404
|
+
self.quarantine_admission
|
|
405
|
+
)
|
|
406
|
+
subset_authorization = (
|
|
407
|
+
self.quarantine_admission_subset_authorization_sha256
|
|
408
|
+
)
|
|
409
|
+
if subset_authorization is not None:
|
|
410
|
+
if self.quarantine_admission is None:
|
|
411
|
+
raise ValueError(
|
|
412
|
+
"quarantine subset authorization requires an admission"
|
|
413
|
+
)
|
|
414
|
+
require_sha256(
|
|
415
|
+
subset_authorization,
|
|
416
|
+
"quarantine_admission_subset_authorization_sha256",
|
|
417
|
+
)
|
|
418
|
+
projection = self.context_projection
|
|
419
|
+
if projection is not None:
|
|
420
|
+
if type(projection) is not PortfolioMemoryContextProjectionBinding:
|
|
421
|
+
raise TypeError("context_projection must be an exact binding or None")
|
|
422
|
+
PortfolioMemoryContextProjectionBinding.__post_init__(projection)
|
|
423
|
+
if projection.estimand_context_sha256 != self.decision.context_hash:
|
|
424
|
+
raise ValueError(
|
|
425
|
+
"context projection estimand differs from memory decision context"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def treatment_binding_sha256(self) -> str:
|
|
430
|
+
self.__post_init__()
|
|
431
|
+
return _hash_record(
|
|
432
|
+
_MEMORY_TREATMENT_BINDING_DOMAIN,
|
|
433
|
+
{
|
|
434
|
+
"schema_version": 2,
|
|
435
|
+
"selection_decision_sha256": (
|
|
436
|
+
insight_selection_decision_sha256(self.decision)
|
|
437
|
+
),
|
|
438
|
+
"card_snapshot_sha256": self.card_snapshot_sha256,
|
|
439
|
+
"card_source_registry_sha256": (
|
|
440
|
+
self.card_source_registry_sha256
|
|
441
|
+
),
|
|
442
|
+
"assignment_receipt_sha256": self.assignment.assignment_sha256,
|
|
443
|
+
"score_snapshot_sha256": self.score_snapshot.snapshot_sha256,
|
|
444
|
+
"quarantine_admission_receipt_sha256": (
|
|
445
|
+
None
|
|
446
|
+
if self.quarantine_admission is None
|
|
447
|
+
else self.quarantine_admission.receipt_sha256
|
|
448
|
+
),
|
|
449
|
+
"quarantine_admission_subset_authorization_sha256": (
|
|
450
|
+
self.quarantine_admission_subset_authorization_sha256
|
|
451
|
+
),
|
|
452
|
+
},
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def resolve_context_projection(
|
|
456
|
+
self,
|
|
457
|
+
selector_context: FrozenJsonObject,
|
|
458
|
+
) -> PortfolioMemoryContextProjectionBinding:
|
|
459
|
+
self.__post_init__()
|
|
460
|
+
if type(selector_context) is not FrozenJsonObject:
|
|
461
|
+
raise TypeError("selector_context must be an exact FrozenJsonObject")
|
|
462
|
+
selector_context_sha256 = typed_json_sha256(selector_context)
|
|
463
|
+
projection = self.context_projection
|
|
464
|
+
if projection is None:
|
|
465
|
+
if self.decision.context_hash != selector_context_sha256:
|
|
466
|
+
raise ValueError(
|
|
467
|
+
"memory decision context differs from selector context; an "
|
|
468
|
+
"explicit authenticated context projection is required"
|
|
469
|
+
)
|
|
470
|
+
return PortfolioMemoryContextProjectionBinding.exact_identity(
|
|
471
|
+
selector_context_sha256
|
|
472
|
+
)
|
|
473
|
+
projection.replay(selector_context)
|
|
474
|
+
if projection.estimand_context_sha256 != self.decision.context_hash:
|
|
475
|
+
raise ValueError(
|
|
476
|
+
"context projection subtree differs from memory decision context"
|
|
477
|
+
)
|
|
478
|
+
return projection
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@dataclass(frozen=True, slots=True)
|
|
482
|
+
class PortfolioMemoryMatchedControlWavePlan:
|
|
483
|
+
"""One arm of a precommitted active-versus-neutral diagnostic pair."""
|
|
484
|
+
|
|
485
|
+
plan: PortfolioMemoryMatchedControlPlan
|
|
486
|
+
assignment: PortfolioMemoryMatchedArmAssignment
|
|
487
|
+
arm_view: PortfolioMemoryMatchedArmView
|
|
488
|
+
aggregation: PortfolioRewardAggregationBinding
|
|
489
|
+
context_projection: PortfolioMemoryContextProjectionBinding
|
|
490
|
+
|
|
491
|
+
def __post_init__(self) -> None:
|
|
492
|
+
if type(self.plan) is not PortfolioMemoryMatchedControlPlan:
|
|
493
|
+
raise TypeError("plan must be an exact matched-control plan")
|
|
494
|
+
self.plan.__post_init__()
|
|
495
|
+
if type(self.assignment) is not PortfolioMemoryMatchedArmAssignment:
|
|
496
|
+
raise TypeError("assignment must be an exact matched arm assignment")
|
|
497
|
+
self.assignment.__post_init__()
|
|
498
|
+
if self.assignment not in self.plan.assignments:
|
|
499
|
+
raise ValueError("matched arm assignment is outside its plan")
|
|
500
|
+
if type(self.arm_view) is not PortfolioMemoryMatchedArmView:
|
|
501
|
+
raise TypeError("arm_view must be an exact matched arm view")
|
|
502
|
+
self.arm_view.__post_init__()
|
|
503
|
+
if (
|
|
504
|
+
self.arm_view.plan_sha256 != self.plan.plan_sha256
|
|
505
|
+
or self.arm_view.assignment != self.assignment
|
|
506
|
+
):
|
|
507
|
+
raise ValueError("matched arm view differs from its plan assignment")
|
|
508
|
+
if type(self.aggregation) is not PortfolioRewardAggregationBinding:
|
|
509
|
+
raise TypeError("aggregation must be an exact reward binding")
|
|
510
|
+
self.aggregation.__post_init__()
|
|
511
|
+
if type(self.context_projection) is not PortfolioMemoryContextProjectionBinding:
|
|
512
|
+
raise TypeError("context_projection must be an exact binding")
|
|
513
|
+
self.context_projection.__post_init__()
|
|
514
|
+
if (
|
|
515
|
+
self.context_projection.estimand_context_sha256
|
|
516
|
+
!= self.plan.exact_context_sha256
|
|
517
|
+
):
|
|
518
|
+
raise ValueError("matched context projection differs from its plan")
|
|
519
|
+
|
|
520
|
+
def to_record(self) -> dict[str, object]:
|
|
521
|
+
self.__post_init__()
|
|
522
|
+
return {
|
|
523
|
+
"schema_version": 1,
|
|
524
|
+
"plan_sha256": self.plan.plan_sha256,
|
|
525
|
+
"assignment": self.assignment.to_record(),
|
|
526
|
+
"arm_view": self.arm_view.to_record(),
|
|
527
|
+
"aggregation": self.aggregation.to_record(),
|
|
528
|
+
"context_projection": self.context_projection.to_record(),
|
|
529
|
+
"card_vs_neutral_effect_identified": False,
|
|
530
|
+
"online_score_update_allowed": False,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@dataclass(frozen=True, slots=True)
|
|
535
|
+
class PortfolioVariationWaveRequest:
|
|
536
|
+
"""Parent, generation, selector request, and optional causal credit unit."""
|
|
537
|
+
|
|
538
|
+
selection_request: PortfolioSelectionRequest
|
|
539
|
+
parent: EvolutionCandidate
|
|
540
|
+
generation: int
|
|
541
|
+
label_prefix: str
|
|
542
|
+
phase: str = "portfolio_evolution"
|
|
543
|
+
memory_credit: PortfolioMemoryCreditPlan | None = None
|
|
544
|
+
matched_memory_control: PortfolioMemoryMatchedControlWavePlan | None = None
|
|
545
|
+
|
|
546
|
+
def __post_init__(self) -> None:
|
|
547
|
+
if type(self.selection_request) is not PortfolioSelectionRequest:
|
|
548
|
+
raise TypeError("selection_request must be exact")
|
|
549
|
+
PortfolioSelectionRequest.__post_init__(self.selection_request)
|
|
550
|
+
if type(self.parent) is not EvolutionCandidate:
|
|
551
|
+
raise TypeError("parent must be an exact EvolutionCandidate")
|
|
552
|
+
EvolutionCandidate.__post_init__(self.parent)
|
|
553
|
+
if not self.parent.valid:
|
|
554
|
+
raise ValueError("portfolio evolution requires a valid parent")
|
|
555
|
+
contract = self.selection_request.finite_variation_contract
|
|
556
|
+
if (
|
|
557
|
+
contract.parent_configuration_sha256
|
|
558
|
+
!= self.parent.occurrence.configuration_hash
|
|
559
|
+
or not typed_json_equal(
|
|
560
|
+
contract.parent_configuration,
|
|
561
|
+
self.parent.configuration,
|
|
562
|
+
)
|
|
563
|
+
):
|
|
564
|
+
raise ValueError("portfolio finite contract is bound to a different parent")
|
|
565
|
+
if (
|
|
566
|
+
type(self.generation) is not int
|
|
567
|
+
or self.generation <= self.parent.generation
|
|
568
|
+
):
|
|
569
|
+
raise ValueError("portfolio generation must follow the parent generation")
|
|
570
|
+
_require_token(self.label_prefix, "label_prefix")
|
|
571
|
+
_require_token(self.phase, "phase")
|
|
572
|
+
credit = self.memory_credit
|
|
573
|
+
matched = self.matched_memory_control
|
|
574
|
+
if credit is not None and matched is not None:
|
|
575
|
+
raise ValueError("one wave cannot carry legacy and matched memory credit")
|
|
576
|
+
if credit is not None:
|
|
577
|
+
if type(credit) is not PortfolioMemoryCreditPlan:
|
|
578
|
+
raise TypeError("memory_credit must be an exact plan or None")
|
|
579
|
+
PortfolioMemoryCreditPlan.__post_init__(credit)
|
|
580
|
+
credit.resolve_context_projection(self.selection_request.context)
|
|
581
|
+
if (
|
|
582
|
+
credit.card_snapshot_sha256
|
|
583
|
+
!= self.selection_request.card_snapshot_sha256
|
|
584
|
+
):
|
|
585
|
+
raise ValueError("memory credit is bound to a different card snapshot")
|
|
586
|
+
source_registry = self.selection_request.source_registry
|
|
587
|
+
observed_registry_sha256 = (
|
|
588
|
+
None if source_registry is None else source_registry.registry_sha256
|
|
589
|
+
)
|
|
590
|
+
if credit.card_source_registry_sha256 != observed_registry_sha256:
|
|
591
|
+
raise ValueError(
|
|
592
|
+
"memory credit is bound to a different card source registry"
|
|
593
|
+
)
|
|
594
|
+
card_references = tuple(
|
|
595
|
+
sorted(card.reference for card in self.selection_request.cards)
|
|
596
|
+
)
|
|
597
|
+
if card_references != credit.decision.selected:
|
|
598
|
+
raise ValueError(
|
|
599
|
+
"selector request card references must equal selected memory "
|
|
600
|
+
"references"
|
|
601
|
+
)
|
|
602
|
+
if matched is None:
|
|
603
|
+
return
|
|
604
|
+
if type(matched) is not PortfolioMemoryMatchedControlWavePlan:
|
|
605
|
+
raise TypeError("matched_memory_control must be an exact plan or None")
|
|
606
|
+
matched.__post_init__()
|
|
607
|
+
# Generation is a hard identity. The campaign runtime separately
|
|
608
|
+
# joins the stable lane through its decision slot; label text is not an
|
|
609
|
+
# authority and therefore cannot establish that join here.
|
|
610
|
+
if matched.assignment.unit.generation != self.generation:
|
|
611
|
+
raise ValueError("matched assignment generation differs from wave")
|
|
612
|
+
matched.context_projection.replay(self.selection_request.context)
|
|
613
|
+
view = matched.arm_view
|
|
614
|
+
if self.selection_request.cards != view.cards:
|
|
615
|
+
raise ValueError("matched selector cards differ from the arm view")
|
|
616
|
+
if self.selection_request.source_registry != view.source_registry:
|
|
617
|
+
raise ValueError("matched selector registry differs from the arm view")
|
|
618
|
+
if (
|
|
619
|
+
self.selection_request.experimental_view_receipt
|
|
620
|
+
!= view.experimental_view_receipt
|
|
621
|
+
):
|
|
622
|
+
raise ValueError("matched selector receipt differs from the arm view")
|
|
623
|
+
if (
|
|
624
|
+
self.selection_request.candidate_pool_required_option_ids
|
|
625
|
+
!= view.required_common_pool_option_ids
|
|
626
|
+
):
|
|
627
|
+
raise ValueError("matched selector required actions differ from arm view")
|
|
628
|
+
dose = self.selection_request.memory_dose_contract
|
|
629
|
+
if view.memory_dose_allowed:
|
|
630
|
+
if dose is None:
|
|
631
|
+
raise ValueError("matched M arm requires an administered memory dose")
|
|
632
|
+
elif dose is not None:
|
|
633
|
+
raise ValueError("matched N arm cannot carry a memory dose")
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
@dataclass(frozen=True, slots=True)
|
|
637
|
+
class PortfolioMemberMaterializationReceipt:
|
|
638
|
+
"""Replay identity for one exact finite option resolved by rank."""
|
|
639
|
+
|
|
640
|
+
request_sha256: str
|
|
641
|
+
decision_sha256: str
|
|
642
|
+
selection_telemetry_sha256: str
|
|
643
|
+
rank: int
|
|
644
|
+
option_id: str
|
|
645
|
+
option_identity_sha256: str
|
|
646
|
+
child_configuration_sha256: str
|
|
647
|
+
parent_candidate_id: CandidateId
|
|
648
|
+
parent_configuration_sha256: str
|
|
649
|
+
generation: int
|
|
650
|
+
candidate_id: CandidateId
|
|
651
|
+
patch_sha256: str
|
|
652
|
+
changed_paths: tuple[str, ...]
|
|
653
|
+
|
|
654
|
+
def __post_init__(self) -> None:
|
|
655
|
+
for name in (
|
|
656
|
+
"request_sha256",
|
|
657
|
+
"decision_sha256",
|
|
658
|
+
"selection_telemetry_sha256",
|
|
659
|
+
"option_identity_sha256",
|
|
660
|
+
"child_configuration_sha256",
|
|
661
|
+
"parent_configuration_sha256",
|
|
662
|
+
"patch_sha256",
|
|
663
|
+
):
|
|
664
|
+
require_sha256(getattr(self, name), name)
|
|
665
|
+
if type(self.rank) is not int or self.rank <= 0:
|
|
666
|
+
raise ValueError("rank must be a positive exact integer")
|
|
667
|
+
if type(self.option_id) is not str or not self.option_id:
|
|
668
|
+
raise ValueError("option_id must be non-empty")
|
|
669
|
+
if type(self.parent_candidate_id) is not CandidateId:
|
|
670
|
+
raise TypeError("parent_candidate_id must be exact")
|
|
671
|
+
if type(self.candidate_id) is not CandidateId:
|
|
672
|
+
raise TypeError("candidate_id must be exact")
|
|
673
|
+
CandidateId.__post_init__(self.parent_candidate_id)
|
|
674
|
+
CandidateId.__post_init__(self.candidate_id)
|
|
675
|
+
if self.parent_candidate_id == self.candidate_id:
|
|
676
|
+
raise ValueError("materialized child cannot reuse the parent occurrence")
|
|
677
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
678
|
+
raise ValueError("generation must be positive")
|
|
679
|
+
if type(self.changed_paths) is not tuple or any(
|
|
680
|
+
type(path) is not str or not path.startswith("$.")
|
|
681
|
+
for path in self.changed_paths
|
|
682
|
+
):
|
|
683
|
+
raise TypeError("changed_paths must be exact candidate paths")
|
|
684
|
+
if not self.changed_paths:
|
|
685
|
+
raise ValueError("finite option materialization must change a path")
|
|
686
|
+
if self.changed_paths != tuple(sorted(set(self.changed_paths))):
|
|
687
|
+
raise ValueError("changed_paths must be unique and canonical")
|
|
688
|
+
|
|
689
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
690
|
+
self.__post_init__()
|
|
691
|
+
return {
|
|
692
|
+
"schema_version": 1,
|
|
693
|
+
"selection_authority": "model_ranked_finite_option_ids",
|
|
694
|
+
"materialization_authority": "engine_exact_sealed_children",
|
|
695
|
+
"materialization_policy_id": PORTFOLIO_MATERIALIZATION_POLICY_ID,
|
|
696
|
+
"materialization_policy_version": (
|
|
697
|
+
PORTFOLIO_MATERIALIZATION_POLICY_VERSION
|
|
698
|
+
),
|
|
699
|
+
"request_sha256": self.request_sha256,
|
|
700
|
+
"decision_sha256": self.decision_sha256,
|
|
701
|
+
"selection_telemetry_sha256": self.selection_telemetry_sha256,
|
|
702
|
+
"rank": self.rank,
|
|
703
|
+
"option_id": self.option_id,
|
|
704
|
+
"option_identity_sha256": self.option_identity_sha256,
|
|
705
|
+
"child_configuration_sha256": self.child_configuration_sha256,
|
|
706
|
+
"parent_candidate_id": self.parent_candidate_id.value,
|
|
707
|
+
"parent_configuration_sha256": self.parent_configuration_sha256,
|
|
708
|
+
"generation": self.generation,
|
|
709
|
+
"candidate_id": self.candidate_id.value,
|
|
710
|
+
"patch_sha256": self.patch_sha256,
|
|
711
|
+
"changed_paths": list(self.changed_paths),
|
|
712
|
+
"source_attribution": [
|
|
713
|
+
{"path": path, "source": "mutation"} for path in self.changed_paths
|
|
714
|
+
],
|
|
715
|
+
"model_authored_configuration_fields": 0,
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
@property
|
|
719
|
+
def receipt_sha256(self) -> str:
|
|
720
|
+
return _hash_record(_MATERIALIZATION_DOMAIN, self._unsigned_record())
|
|
721
|
+
|
|
722
|
+
def to_record(self) -> dict[str, object]:
|
|
723
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
class PortfolioMemberDisposition(str, Enum):
|
|
727
|
+
"""Closed terminal meanings for a completely evaluated portfolio member."""
|
|
728
|
+
|
|
729
|
+
SCORED = "scored"
|
|
730
|
+
CANDIDATE_INFEASIBLE = "candidate_infeasible"
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
@dataclass(frozen=True, slots=True)
|
|
734
|
+
class PortfolioCandidateFailureEvidence:
|
|
735
|
+
"""Content-minimized proof of candidate-attributable infeasibility.
|
|
736
|
+
|
|
737
|
+
The detailed-evaluation digest authenticates the full evaluator record. The
|
|
738
|
+
compact projection makes the terminal category usable without copying a
|
|
739
|
+
potentially sensitive free-text diagnostic into every portfolio receipt.
|
|
740
|
+
"""
|
|
741
|
+
|
|
742
|
+
detailed_evaluation_sha256: str
|
|
743
|
+
failure_code: FailureCode
|
|
744
|
+
failure_message_sha256: str
|
|
745
|
+
retryable: bool
|
|
746
|
+
exception_type: str | None
|
|
747
|
+
diagnostics_artifact_id: ArtifactId | None
|
|
748
|
+
|
|
749
|
+
def __post_init__(self) -> None:
|
|
750
|
+
require_sha256(
|
|
751
|
+
self.detailed_evaluation_sha256,
|
|
752
|
+
"detailed_evaluation_sha256",
|
|
753
|
+
)
|
|
754
|
+
if type(self.failure_code) is not FailureCode:
|
|
755
|
+
raise TypeError("failure_code must be an exact FailureCode")
|
|
756
|
+
# Reuse the closed domain taxonomy rather than maintaining a second
|
|
757
|
+
# hand-written list of candidate-attributable codes here.
|
|
758
|
+
validate_failure_pair(FailureCategory.CANDIDATE, self.failure_code)
|
|
759
|
+
require_sha256(self.failure_message_sha256, "failure_message_sha256")
|
|
760
|
+
if type(self.retryable) is not bool:
|
|
761
|
+
raise TypeError("retryable must be an exact boolean")
|
|
762
|
+
if self.exception_type is not None and (
|
|
763
|
+
type(self.exception_type) is not str
|
|
764
|
+
or not self.exception_type.strip()
|
|
765
|
+
or self.exception_type != self.exception_type.strip()
|
|
766
|
+
):
|
|
767
|
+
raise ValueError(
|
|
768
|
+
"exception_type must be canonical non-empty text or None"
|
|
769
|
+
)
|
|
770
|
+
if self.diagnostics_artifact_id is not None:
|
|
771
|
+
if type(self.diagnostics_artifact_id) is not ArtifactId:
|
|
772
|
+
raise TypeError(
|
|
773
|
+
"diagnostics_artifact_id must be an exact ArtifactId or None"
|
|
774
|
+
)
|
|
775
|
+
ArtifactId.__post_init__(self.diagnostics_artifact_id)
|
|
776
|
+
|
|
777
|
+
@classmethod
|
|
778
|
+
def from_failure_record(
|
|
779
|
+
cls,
|
|
780
|
+
failure: FailureRecord,
|
|
781
|
+
*,
|
|
782
|
+
detailed_evaluation_sha256: str,
|
|
783
|
+
) -> PortfolioCandidateFailureEvidence:
|
|
784
|
+
"""Project one exact candidate-category evaluator failure."""
|
|
785
|
+
|
|
786
|
+
if type(failure) is not FailureRecord:
|
|
787
|
+
raise TypeError("failure must be an exact FailureRecord")
|
|
788
|
+
FailureRecord.__post_init__(failure)
|
|
789
|
+
if failure.category is not FailureCategory.CANDIDATE:
|
|
790
|
+
raise ValueError(
|
|
791
|
+
"portfolio infeasibility requires candidate-category evidence"
|
|
792
|
+
)
|
|
793
|
+
return cls(
|
|
794
|
+
detailed_evaluation_sha256=detailed_evaluation_sha256,
|
|
795
|
+
failure_code=failure.code,
|
|
796
|
+
failure_message_sha256=hashlib.sha256(
|
|
797
|
+
failure.message.encode("utf-8", errors="strict")
|
|
798
|
+
).hexdigest(),
|
|
799
|
+
retryable=failure.retryable,
|
|
800
|
+
exception_type=failure.exception_type,
|
|
801
|
+
diagnostics_artifact_id=failure.diagnostics_artifact_id,
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
805
|
+
self.__post_init__()
|
|
806
|
+
return {
|
|
807
|
+
"schema_version": 1,
|
|
808
|
+
"failure_category": FailureCategory.CANDIDATE.value,
|
|
809
|
+
"failure_code": self.failure_code.value,
|
|
810
|
+
"failure_message_sha256": self.failure_message_sha256,
|
|
811
|
+
"retryable": self.retryable,
|
|
812
|
+
"exception_type": self.exception_type,
|
|
813
|
+
"diagnostics_artifact_id": (
|
|
814
|
+
None
|
|
815
|
+
if self.diagnostics_artifact_id is None
|
|
816
|
+
else self.diagnostics_artifact_id.value
|
|
817
|
+
),
|
|
818
|
+
"detailed_evaluation_sha256": self.detailed_evaluation_sha256,
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
@property
|
|
822
|
+
def evidence_sha256(self) -> str:
|
|
823
|
+
return _hash_record(_CANDIDATE_FAILURE_DOMAIN, self._unsigned_record())
|
|
824
|
+
|
|
825
|
+
def to_record(self) -> dict[str, object]:
|
|
826
|
+
return {**self._unsigned_record(), "evidence_sha256": self.evidence_sha256}
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
@dataclass(frozen=True, slots=True)
|
|
830
|
+
class PortfolioVariationMemberReceipt:
|
|
831
|
+
"""Closed materialization-to-outcome join for one ranked member."""
|
|
832
|
+
|
|
833
|
+
materialization: PortfolioMemberMaterializationReceipt
|
|
834
|
+
operator_invocation_id: OperatorInvocationId
|
|
835
|
+
reward_definition_sha256: str
|
|
836
|
+
reward: float
|
|
837
|
+
parent_relations: tuple[OutcomeRelation, ...]
|
|
838
|
+
detailed_evaluation_sha256: str | None
|
|
839
|
+
dominates_any_parent: bool
|
|
840
|
+
better_than_any_parent: bool
|
|
841
|
+
disposition: PortfolioMemberDisposition = PortfolioMemberDisposition.SCORED
|
|
842
|
+
candidate_failure: PortfolioCandidateFailureEvidence | None = None
|
|
843
|
+
|
|
844
|
+
def __post_init__(self) -> None:
|
|
845
|
+
if type(self.materialization) is not PortfolioMemberMaterializationReceipt:
|
|
846
|
+
raise TypeError("materialization must be an exact receipt")
|
|
847
|
+
PortfolioMemberMaterializationReceipt.__post_init__(self.materialization)
|
|
848
|
+
if type(self.operator_invocation_id) is not OperatorInvocationId:
|
|
849
|
+
raise TypeError("operator_invocation_id must be exact")
|
|
850
|
+
OperatorInvocationId.__post_init__(self.operator_invocation_id)
|
|
851
|
+
require_sha256(self.reward_definition_sha256, "reward_definition_sha256")
|
|
852
|
+
if type(self.reward) is not float or not math.isfinite(self.reward):
|
|
853
|
+
raise TypeError("reward must be a finite canonical float")
|
|
854
|
+
if type(self.parent_relations) is not tuple or any(
|
|
855
|
+
type(relation) is not OutcomeRelation for relation in self.parent_relations
|
|
856
|
+
):
|
|
857
|
+
raise TypeError("parent_relations must contain exact values")
|
|
858
|
+
if self.detailed_evaluation_sha256 is not None:
|
|
859
|
+
require_sha256(
|
|
860
|
+
self.detailed_evaluation_sha256,
|
|
861
|
+
"detailed_evaluation_sha256",
|
|
862
|
+
)
|
|
863
|
+
if (
|
|
864
|
+
type(self.dominates_any_parent) is not bool
|
|
865
|
+
or type(self.better_than_any_parent) is not bool
|
|
866
|
+
):
|
|
867
|
+
raise TypeError("outcome comparison projections must be bool")
|
|
868
|
+
if type(self.disposition) is not PortfolioMemberDisposition:
|
|
869
|
+
raise TypeError("disposition must be a PortfolioMemberDisposition")
|
|
870
|
+
failure = self.candidate_failure
|
|
871
|
+
if self.disposition is PortfolioMemberDisposition.SCORED:
|
|
872
|
+
if len(self.parent_relations) != 1:
|
|
873
|
+
raise ValueError("scored portfolio member must compare to its parent")
|
|
874
|
+
if failure is not None:
|
|
875
|
+
raise ValueError("scored portfolio member cannot carry failure evidence")
|
|
876
|
+
else:
|
|
877
|
+
if type(failure) is not PortfolioCandidateFailureEvidence:
|
|
878
|
+
raise TypeError(
|
|
879
|
+
"candidate-infeasible member requires exact failure evidence"
|
|
880
|
+
)
|
|
881
|
+
PortfolioCandidateFailureEvidence.__post_init__(failure)
|
|
882
|
+
if self.detailed_evaluation_sha256 != failure.detailed_evaluation_sha256:
|
|
883
|
+
raise ValueError(
|
|
884
|
+
"candidate failure identifies another detailed evaluation"
|
|
885
|
+
)
|
|
886
|
+
if self.parent_relations:
|
|
887
|
+
raise ValueError(
|
|
888
|
+
"candidate-infeasible member cannot publish parent relations"
|
|
889
|
+
)
|
|
890
|
+
if self.dominates_any_parent or self.better_than_any_parent:
|
|
891
|
+
raise ValueError(
|
|
892
|
+
"candidate-infeasible member cannot publish improvement flags"
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
@property
|
|
896
|
+
def engine_reward(self) -> float:
|
|
897
|
+
"""Return the exact terminal reward published by the engine."""
|
|
898
|
+
|
|
899
|
+
return self.reward
|
|
900
|
+
|
|
901
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
902
|
+
self.__post_init__()
|
|
903
|
+
return {
|
|
904
|
+
"schema_version": 2,
|
|
905
|
+
"materialization": self.materialization.to_record(),
|
|
906
|
+
"operator_invocation_id": self.operator_invocation_id.value,
|
|
907
|
+
"candidate_id": self.materialization.candidate_id.value,
|
|
908
|
+
"candidate_configuration_sha256": (
|
|
909
|
+
self.materialization.child_configuration_sha256
|
|
910
|
+
),
|
|
911
|
+
"disposition": self.disposition.value,
|
|
912
|
+
"candidate_valid": (
|
|
913
|
+
self.disposition is PortfolioMemberDisposition.SCORED
|
|
914
|
+
),
|
|
915
|
+
"operator_compliant": True,
|
|
916
|
+
"evidence_compliant": True,
|
|
917
|
+
"reward_definition_sha256": self.reward_definition_sha256,
|
|
918
|
+
"engine_reward_hex": self.engine_reward.hex(),
|
|
919
|
+
"reward_hex": self.reward.hex(),
|
|
920
|
+
"parent_relations": [value.value for value in self.parent_relations],
|
|
921
|
+
"detailed_evaluation_sha256": self.detailed_evaluation_sha256,
|
|
922
|
+
"candidate_failure": (
|
|
923
|
+
None
|
|
924
|
+
if self.candidate_failure is None
|
|
925
|
+
else self.candidate_failure.to_record()
|
|
926
|
+
),
|
|
927
|
+
"dominates_any_parent": self.dominates_any_parent,
|
|
928
|
+
"better_than_any_parent": self.better_than_any_parent,
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
@property
|
|
932
|
+
def outcome_sha256(self) -> str:
|
|
933
|
+
return _hash_record(_MEMBER_DOMAIN, self._unsigned_record())
|
|
934
|
+
|
|
935
|
+
def to_record(self) -> dict[str, object]:
|
|
936
|
+
return {**self._unsigned_record(), "outcome_sha256": self.outcome_sha256}
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
@dataclass(frozen=True, slots=True)
|
|
940
|
+
class PortfolioActionCardAttribution:
|
|
941
|
+
"""Exact request-card identity cited for one selected finite action."""
|
|
942
|
+
|
|
943
|
+
card_key: str
|
|
944
|
+
reference: InsightRef
|
|
945
|
+
content_sha256: str
|
|
946
|
+
evidence_sha256: str
|
|
947
|
+
|
|
948
|
+
def __post_init__(self) -> None:
|
|
949
|
+
_require_token(self.card_key, "card_key")
|
|
950
|
+
if type(self.reference) is not InsightRef:
|
|
951
|
+
raise TypeError("reference must be an exact InsightRef")
|
|
952
|
+
InsightRef.__post_init__(self.reference)
|
|
953
|
+
require_sha256(self.content_sha256, "content_sha256")
|
|
954
|
+
require_sha256(self.evidence_sha256, "evidence_sha256")
|
|
955
|
+
|
|
956
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
957
|
+
self.__post_init__()
|
|
958
|
+
return {
|
|
959
|
+
"schema_version": 1,
|
|
960
|
+
"card_key": self.card_key,
|
|
961
|
+
"reference": {
|
|
962
|
+
"insight_id": self.reference.insight_id.value,
|
|
963
|
+
"version": self.reference.version,
|
|
964
|
+
},
|
|
965
|
+
"content_sha256": self.content_sha256,
|
|
966
|
+
"evidence_sha256": self.evidence_sha256,
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
@property
|
|
970
|
+
def binding_sha256(self) -> str:
|
|
971
|
+
return _hash_record(
|
|
972
|
+
_ACTION_CARD_ATTRIBUTION_DOMAIN,
|
|
973
|
+
self._unsigned_record(),
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
def to_record(self) -> dict[str, object]:
|
|
977
|
+
return {**self._unsigned_record(), "binding_sha256": self.binding_sha256}
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
@dataclass(frozen=True, slots=True)
|
|
981
|
+
class PortfolioActionAttributionReceipt:
|
|
982
|
+
"""Authenticated selector-card to materialized-outcome join for one action.
|
|
983
|
+
|
|
984
|
+
This is diagnostic attribution, not a causal credit unit. The randomized
|
|
985
|
+
memory treatment remains one whole-wave ITT trial in
|
|
986
|
+
:class:`PortfolioMemoryCreditReceipt`.
|
|
987
|
+
"""
|
|
988
|
+
|
|
989
|
+
request_sha256: str
|
|
990
|
+
decision_sha256: str
|
|
991
|
+
card_snapshot_sha256: str
|
|
992
|
+
rank: int
|
|
993
|
+
option_id: str
|
|
994
|
+
option_identity_sha256: str
|
|
995
|
+
child_configuration_sha256: str
|
|
996
|
+
family: str
|
|
997
|
+
supporting_cards: tuple[PortfolioActionCardAttribution, ...]
|
|
998
|
+
effect_predictions: tuple[MetricEffectPrediction, ...]
|
|
999
|
+
design_rationale_sha256: str
|
|
1000
|
+
materialization_receipt_sha256: str
|
|
1001
|
+
outcome_sha256: str
|
|
1002
|
+
operator_invocation_id: OperatorInvocationId
|
|
1003
|
+
candidate_id: CandidateId
|
|
1004
|
+
|
|
1005
|
+
def __post_init__(self) -> None:
|
|
1006
|
+
for name in (
|
|
1007
|
+
"request_sha256",
|
|
1008
|
+
"decision_sha256",
|
|
1009
|
+
"card_snapshot_sha256",
|
|
1010
|
+
"option_identity_sha256",
|
|
1011
|
+
"child_configuration_sha256",
|
|
1012
|
+
"design_rationale_sha256",
|
|
1013
|
+
"materialization_receipt_sha256",
|
|
1014
|
+
"outcome_sha256",
|
|
1015
|
+
):
|
|
1016
|
+
require_sha256(getattr(self, name), name)
|
|
1017
|
+
if type(self.rank) is not int or self.rank <= 0:
|
|
1018
|
+
raise ValueError("rank must be a positive exact integer")
|
|
1019
|
+
if type(self.option_id) is not str or not self.option_id:
|
|
1020
|
+
raise ValueError("option_id must be non-empty")
|
|
1021
|
+
_require_token(self.family, "family")
|
|
1022
|
+
if type(self.supporting_cards) is not tuple or any(
|
|
1023
|
+
type(value) is not PortfolioActionCardAttribution
|
|
1024
|
+
for value in self.supporting_cards
|
|
1025
|
+
):
|
|
1026
|
+
raise TypeError(
|
|
1027
|
+
"supporting_cards must contain exact card-attribution values"
|
|
1028
|
+
)
|
|
1029
|
+
for value in self.supporting_cards:
|
|
1030
|
+
PortfolioActionCardAttribution.__post_init__(value)
|
|
1031
|
+
card_keys = tuple(value.card_key for value in self.supporting_cards)
|
|
1032
|
+
if card_keys != tuple(sorted(set(card_keys))):
|
|
1033
|
+
raise ValueError("supporting cards must use canonical unique card keys")
|
|
1034
|
+
references = tuple(value.reference for value in self.supporting_cards)
|
|
1035
|
+
if len(set(references)) != len(references):
|
|
1036
|
+
raise ValueError("supporting cards cannot repeat an insight reference")
|
|
1037
|
+
if type(self.effect_predictions) is not tuple or any(
|
|
1038
|
+
type(value) is not MetricEffectPrediction
|
|
1039
|
+
for value in self.effect_predictions
|
|
1040
|
+
):
|
|
1041
|
+
raise TypeError(
|
|
1042
|
+
"effect_predictions must contain exact MetricEffectPrediction values"
|
|
1043
|
+
)
|
|
1044
|
+
for value in self.effect_predictions:
|
|
1045
|
+
MetricEffectPrediction.__post_init__(value)
|
|
1046
|
+
metric_ids = tuple(value.metric_id for value in self.effect_predictions)
|
|
1047
|
+
if metric_ids != tuple(sorted(set(metric_ids))):
|
|
1048
|
+
raise ValueError("effect predictions must use canonical metric order")
|
|
1049
|
+
if type(self.operator_invocation_id) is not OperatorInvocationId:
|
|
1050
|
+
raise TypeError("operator_invocation_id must be exact")
|
|
1051
|
+
OperatorInvocationId.__post_init__(self.operator_invocation_id)
|
|
1052
|
+
if type(self.candidate_id) is not CandidateId:
|
|
1053
|
+
raise TypeError("candidate_id must be exact")
|
|
1054
|
+
CandidateId.__post_init__(self.candidate_id)
|
|
1055
|
+
|
|
1056
|
+
@property
|
|
1057
|
+
def supporting_card_keys(self) -> tuple[str, ...]:
|
|
1058
|
+
self.__post_init__()
|
|
1059
|
+
return tuple(value.card_key for value in self.supporting_cards)
|
|
1060
|
+
|
|
1061
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1062
|
+
self.__post_init__()
|
|
1063
|
+
return {
|
|
1064
|
+
"schema_version": 1,
|
|
1065
|
+
"attribution_scope": "post_treatment_diagnostic_not_causal_credit",
|
|
1066
|
+
"request_sha256": self.request_sha256,
|
|
1067
|
+
"decision_sha256": self.decision_sha256,
|
|
1068
|
+
"card_snapshot_sha256": self.card_snapshot_sha256,
|
|
1069
|
+
"selected_member": {
|
|
1070
|
+
"rank": self.rank,
|
|
1071
|
+
"option_id": self.option_id,
|
|
1072
|
+
"option_identity_sha256": self.option_identity_sha256,
|
|
1073
|
+
"child_configuration_sha256": self.child_configuration_sha256,
|
|
1074
|
+
"family": self.family,
|
|
1075
|
+
"supporting_card_keys": list(self.supporting_card_keys),
|
|
1076
|
+
"effect_predictions": [
|
|
1077
|
+
{
|
|
1078
|
+
"metric_id": value.metric_id,
|
|
1079
|
+
"direction": value.direction.value,
|
|
1080
|
+
}
|
|
1081
|
+
for value in self.effect_predictions
|
|
1082
|
+
],
|
|
1083
|
+
"design_rationale_sha256": self.design_rationale_sha256,
|
|
1084
|
+
},
|
|
1085
|
+
"supporting_cards": [value.to_record() for value in self.supporting_cards],
|
|
1086
|
+
"materialization_receipt_sha256": (self.materialization_receipt_sha256),
|
|
1087
|
+
"outcome_sha256": self.outcome_sha256,
|
|
1088
|
+
"operator_invocation_id": self.operator_invocation_id.value,
|
|
1089
|
+
"candidate_id": self.candidate_id.value,
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
@property
|
|
1093
|
+
def receipt_sha256(self) -> str:
|
|
1094
|
+
return _hash_record(_ACTION_ATTRIBUTION_DOMAIN, self._unsigned_record())
|
|
1095
|
+
|
|
1096
|
+
def to_record(self) -> dict[str, object]:
|
|
1097
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
@dataclass(frozen=True, slots=True)
|
|
1101
|
+
class PortfolioPendingMemoryCredit:
|
|
1102
|
+
"""Complete credit evidence awaiting a generation publication barrier."""
|
|
1103
|
+
|
|
1104
|
+
credit_unit_id: OperatorInvocationId
|
|
1105
|
+
decision: InsightSelectionDecision
|
|
1106
|
+
candidate_ids: tuple[CandidateId, ...]
|
|
1107
|
+
aggregation: PortfolioRewardAggregationBinding
|
|
1108
|
+
context_projection: PortfolioMemoryContextProjectionBinding
|
|
1109
|
+
reward: float
|
|
1110
|
+
treatment_binding_sha256: str
|
|
1111
|
+
generation: int
|
|
1112
|
+
|
|
1113
|
+
def __post_init__(self) -> None:
|
|
1114
|
+
if type(self.credit_unit_id) is not OperatorInvocationId:
|
|
1115
|
+
raise TypeError("credit_unit_id must be exact")
|
|
1116
|
+
OperatorInvocationId.__post_init__(self.credit_unit_id)
|
|
1117
|
+
if type(self.decision) is not InsightSelectionDecision:
|
|
1118
|
+
raise TypeError("decision must be an exact InsightSelectionDecision")
|
|
1119
|
+
InsightSelectionDecision.__post_init__(self.decision)
|
|
1120
|
+
if type(self.candidate_ids) is not tuple or any(
|
|
1121
|
+
type(value) is not CandidateId for value in self.candidate_ids
|
|
1122
|
+
):
|
|
1123
|
+
raise TypeError("candidate_ids must contain exact CandidateId values")
|
|
1124
|
+
if not self.candidate_ids or len(set(self.candidate_ids)) != len(
|
|
1125
|
+
self.candidate_ids
|
|
1126
|
+
):
|
|
1127
|
+
raise ValueError("candidate_ids must be non-empty and unique")
|
|
1128
|
+
if type(self.aggregation) is not PortfolioRewardAggregationBinding:
|
|
1129
|
+
raise TypeError("aggregation must be an exact binding")
|
|
1130
|
+
PortfolioRewardAggregationBinding.__post_init__(self.aggregation)
|
|
1131
|
+
if type(self.context_projection) is not PortfolioMemoryContextProjectionBinding:
|
|
1132
|
+
raise TypeError("context_projection must be an exact binding")
|
|
1133
|
+
PortfolioMemoryContextProjectionBinding.__post_init__(self.context_projection)
|
|
1134
|
+
if (
|
|
1135
|
+
self.context_projection.estimand_context_sha256
|
|
1136
|
+
!= self.decision.context_hash
|
|
1137
|
+
):
|
|
1138
|
+
raise ValueError("context projection differs from decision context")
|
|
1139
|
+
if type(self.reward) is not float or not math.isfinite(self.reward):
|
|
1140
|
+
raise TypeError("reward must be a finite canonical float")
|
|
1141
|
+
require_sha256(
|
|
1142
|
+
self.treatment_binding_sha256,
|
|
1143
|
+
"treatment_binding_sha256",
|
|
1144
|
+
)
|
|
1145
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
1146
|
+
raise ValueError("generation must be a positive exact integer")
|
|
1147
|
+
|
|
1148
|
+
@property
|
|
1149
|
+
def selection_decision_sha256(self) -> str:
|
|
1150
|
+
return insight_selection_decision_sha256(self.decision)
|
|
1151
|
+
|
|
1152
|
+
def to_trial(self) -> InsightTrial:
|
|
1153
|
+
self.__post_init__()
|
|
1154
|
+
return InsightTrial(
|
|
1155
|
+
credit_unit_id=self.credit_unit_id,
|
|
1156
|
+
candidate_ids=self.candidate_ids,
|
|
1157
|
+
reward_definition_hash=self.aggregation.definition_sha256,
|
|
1158
|
+
decision=self.decision,
|
|
1159
|
+
reward=self.reward,
|
|
1160
|
+
treatment_binding_sha256=self.treatment_binding_sha256,
|
|
1161
|
+
generation=self.generation,
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
def to_committed_receipt(self) -> "PortfolioMemoryCreditReceipt":
|
|
1165
|
+
self.__post_init__()
|
|
1166
|
+
return PortfolioMemoryCreditReceipt(
|
|
1167
|
+
credit_unit_id=self.credit_unit_id,
|
|
1168
|
+
selection_decision_sha256=self.selection_decision_sha256,
|
|
1169
|
+
selection_decision_context_sha256=self.decision.context_hash,
|
|
1170
|
+
candidate_ids=self.candidate_ids,
|
|
1171
|
+
aggregation_id=self.aggregation.aggregation_id,
|
|
1172
|
+
aggregation_version=self.aggregation.aggregation_version,
|
|
1173
|
+
aggregation_definition_sha256=self.aggregation.definition_sha256,
|
|
1174
|
+
aggregation_binding_sha256=self.aggregation.binding_sha256,
|
|
1175
|
+
context_projection=self.context_projection,
|
|
1176
|
+
reward=self.reward,
|
|
1177
|
+
treatment_binding_sha256=self.treatment_binding_sha256,
|
|
1178
|
+
generation=self.generation,
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1182
|
+
self.__post_init__()
|
|
1183
|
+
return {
|
|
1184
|
+
"schema_version": 1,
|
|
1185
|
+
"status": "pending_generation_barrier",
|
|
1186
|
+
"credit_unit_id": self.credit_unit_id.value,
|
|
1187
|
+
"selection_decision_sha256": self.selection_decision_sha256,
|
|
1188
|
+
"selection_decision_context_sha256": self.decision.context_hash,
|
|
1189
|
+
"candidate_ids": [value.value for value in self.candidate_ids],
|
|
1190
|
+
"aggregation": {
|
|
1191
|
+
**self.aggregation.to_record(),
|
|
1192
|
+
"binding_sha256": self.aggregation.binding_sha256,
|
|
1193
|
+
},
|
|
1194
|
+
"context_projection": {
|
|
1195
|
+
**self.context_projection.to_record(),
|
|
1196
|
+
"binding_sha256": self.context_projection.binding_sha256,
|
|
1197
|
+
},
|
|
1198
|
+
"reward_hex": self.reward.hex(),
|
|
1199
|
+
"treatment_binding_sha256": self.treatment_binding_sha256,
|
|
1200
|
+
"generation": self.generation,
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
@property
|
|
1204
|
+
def pending_sha256(self) -> str:
|
|
1205
|
+
return _hash_record(_PENDING_MEMORY_CREDIT_DOMAIN, self._unsigned_record())
|
|
1206
|
+
|
|
1207
|
+
def to_record(self) -> dict[str, object]:
|
|
1208
|
+
return {**self._unsigned_record(), "pending_sha256": self.pending_sha256}
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
@dataclass(frozen=True, slots=True)
|
|
1212
|
+
class PortfolioMemoryCreditReceipt:
|
|
1213
|
+
"""Evidence that a complete portfolio became one memory trial."""
|
|
1214
|
+
|
|
1215
|
+
credit_unit_id: OperatorInvocationId
|
|
1216
|
+
selection_decision_sha256: str
|
|
1217
|
+
selection_decision_context_sha256: str
|
|
1218
|
+
candidate_ids: tuple[CandidateId, ...]
|
|
1219
|
+
aggregation_id: str
|
|
1220
|
+
aggregation_version: int
|
|
1221
|
+
aggregation_definition_sha256: str
|
|
1222
|
+
aggregation_binding_sha256: str
|
|
1223
|
+
context_projection: PortfolioMemoryContextProjectionBinding
|
|
1224
|
+
reward: float
|
|
1225
|
+
treatment_binding_sha256: str
|
|
1226
|
+
generation: int
|
|
1227
|
+
|
|
1228
|
+
def __post_init__(self) -> None:
|
|
1229
|
+
if type(self.credit_unit_id) is not OperatorInvocationId:
|
|
1230
|
+
raise TypeError("credit_unit_id must be exact")
|
|
1231
|
+
OperatorInvocationId.__post_init__(self.credit_unit_id)
|
|
1232
|
+
for name in (
|
|
1233
|
+
"selection_decision_sha256",
|
|
1234
|
+
"selection_decision_context_sha256",
|
|
1235
|
+
"aggregation_definition_sha256",
|
|
1236
|
+
"aggregation_binding_sha256",
|
|
1237
|
+
):
|
|
1238
|
+
require_sha256(getattr(self, name), name)
|
|
1239
|
+
if type(self.candidate_ids) is not tuple or any(
|
|
1240
|
+
type(value) is not CandidateId for value in self.candidate_ids
|
|
1241
|
+
):
|
|
1242
|
+
raise TypeError("candidate_ids must contain exact CandidateId values")
|
|
1243
|
+
if not self.candidate_ids or len(set(self.candidate_ids)) != len(
|
|
1244
|
+
self.candidate_ids
|
|
1245
|
+
):
|
|
1246
|
+
raise ValueError("candidate_ids must be non-empty and unique")
|
|
1247
|
+
_require_token(self.aggregation_id, "aggregation_id")
|
|
1248
|
+
if type(self.aggregation_version) is not int or self.aggregation_version <= 0:
|
|
1249
|
+
raise ValueError("aggregation_version must be positive")
|
|
1250
|
+
if type(self.reward) is not float or not math.isfinite(self.reward):
|
|
1251
|
+
raise TypeError("reward must be a finite canonical float")
|
|
1252
|
+
require_sha256(
|
|
1253
|
+
self.treatment_binding_sha256,
|
|
1254
|
+
"treatment_binding_sha256",
|
|
1255
|
+
)
|
|
1256
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
1257
|
+
raise ValueError("generation must be a positive exact integer")
|
|
1258
|
+
if type(self.context_projection) is not PortfolioMemoryContextProjectionBinding:
|
|
1259
|
+
raise TypeError("context_projection must be an exact binding")
|
|
1260
|
+
PortfolioMemoryContextProjectionBinding.__post_init__(self.context_projection)
|
|
1261
|
+
if (
|
|
1262
|
+
self.context_projection.estimand_context_sha256
|
|
1263
|
+
!= self.selection_decision_context_sha256
|
|
1264
|
+
):
|
|
1265
|
+
raise ValueError(
|
|
1266
|
+
"context projection estimand differs from decision context receipt"
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1270
|
+
self.__post_init__()
|
|
1271
|
+
return {
|
|
1272
|
+
"schema_version": 1,
|
|
1273
|
+
"credit_unit_id": self.credit_unit_id.value,
|
|
1274
|
+
"selection_decision_sha256": self.selection_decision_sha256,
|
|
1275
|
+
"selection_decision_context_sha256": (
|
|
1276
|
+
self.selection_decision_context_sha256
|
|
1277
|
+
),
|
|
1278
|
+
"candidate_ids": [value.value for value in self.candidate_ids],
|
|
1279
|
+
"aggregation": {
|
|
1280
|
+
"aggregation_id": self.aggregation_id,
|
|
1281
|
+
"aggregation_version": self.aggregation_version,
|
|
1282
|
+
"definition_sha256": self.aggregation_definition_sha256,
|
|
1283
|
+
"binding_sha256": self.aggregation_binding_sha256,
|
|
1284
|
+
},
|
|
1285
|
+
"context_projection": {
|
|
1286
|
+
**self.context_projection.to_record(),
|
|
1287
|
+
"binding_sha256": self.context_projection.binding_sha256,
|
|
1288
|
+
},
|
|
1289
|
+
"reward_hex": self.reward.hex(),
|
|
1290
|
+
"treatment_binding_sha256": self.treatment_binding_sha256,
|
|
1291
|
+
"generation": self.generation,
|
|
1292
|
+
"memory_trial_count": 1,
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
@property
|
|
1296
|
+
def receipt_sha256(self) -> str:
|
|
1297
|
+
return _hash_record(_MEMORY_CREDIT_DOMAIN, self._unsigned_record())
|
|
1298
|
+
|
|
1299
|
+
def to_record(self) -> dict[str, object]:
|
|
1300
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
@dataclass(frozen=True, slots=True)
|
|
1304
|
+
class PortfolioMemoryCreditBatchReceipt:
|
|
1305
|
+
"""Canonical publication receipt for one generation's pending credits."""
|
|
1306
|
+
|
|
1307
|
+
generation: int
|
|
1308
|
+
credits: tuple[PortfolioMemoryCreditReceipt, ...]
|
|
1309
|
+
memory_trial_count_before: int
|
|
1310
|
+
memory_trial_count_after: int
|
|
1311
|
+
|
|
1312
|
+
def __post_init__(self) -> None:
|
|
1313
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
1314
|
+
raise ValueError("generation must be a positive exact integer")
|
|
1315
|
+
if type(self.credits) is not tuple or not self.credits:
|
|
1316
|
+
raise ValueError("credits must be a non-empty exact tuple")
|
|
1317
|
+
if any(type(value) is not PortfolioMemoryCreditReceipt for value in self.credits):
|
|
1318
|
+
raise TypeError("credits must contain exact credit receipts")
|
|
1319
|
+
for value in self.credits:
|
|
1320
|
+
PortfolioMemoryCreditReceipt.__post_init__(value)
|
|
1321
|
+
if any(value.generation != self.generation for value in self.credits):
|
|
1322
|
+
raise ValueError("memory credits differ from the batch generation")
|
|
1323
|
+
credit_ids = tuple(value.credit_unit_id.value for value in self.credits)
|
|
1324
|
+
if credit_ids != tuple(sorted(set(credit_ids))):
|
|
1325
|
+
raise ValueError("credits must use canonical unique credit-unit order")
|
|
1326
|
+
for name in ("memory_trial_count_before", "memory_trial_count_after"):
|
|
1327
|
+
value = getattr(self, name)
|
|
1328
|
+
if type(value) is not int or value < 0:
|
|
1329
|
+
raise ValueError(f"{name} must be a non-negative exact integer")
|
|
1330
|
+
if self.memory_trial_count_after - self.memory_trial_count_before != len(
|
|
1331
|
+
self.credits
|
|
1332
|
+
):
|
|
1333
|
+
raise ValueError("memory trial counts differ from the committed batch")
|
|
1334
|
+
|
|
1335
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1336
|
+
self.__post_init__()
|
|
1337
|
+
return {
|
|
1338
|
+
"schema_version": 1,
|
|
1339
|
+
"publication_scope": "post_concurrent_generation_barrier",
|
|
1340
|
+
"generation": self.generation,
|
|
1341
|
+
"memory_trial_count_before": self.memory_trial_count_before,
|
|
1342
|
+
"memory_trial_count_after": self.memory_trial_count_after,
|
|
1343
|
+
"credit_count": len(self.credits),
|
|
1344
|
+
"credits": [value.to_record() for value in self.credits],
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
@property
|
|
1348
|
+
def receipt_sha256(self) -> str:
|
|
1349
|
+
return _hash_record(_MEMORY_CREDIT_BATCH_DOMAIN, self._unsigned_record())
|
|
1350
|
+
|
|
1351
|
+
def to_record(self) -> dict[str, object]:
|
|
1352
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
@dataclass(frozen=True, slots=True)
|
|
1356
|
+
class PortfolioVariationWaveReceipt:
|
|
1357
|
+
"""Closed one-selector-call to N-evaluation portfolio receipt."""
|
|
1358
|
+
|
|
1359
|
+
selection_call_id: LLMCallId
|
|
1360
|
+
request_sha256: str
|
|
1361
|
+
decision_sha256: str
|
|
1362
|
+
selection_policy_id: str
|
|
1363
|
+
selection_policy_version: int
|
|
1364
|
+
selection_policy_definition_sha256: str
|
|
1365
|
+
selection_telemetry: AgenticCallTelemetry
|
|
1366
|
+
selection_telemetry_sha256: str
|
|
1367
|
+
parent_candidate_id: CandidateId
|
|
1368
|
+
parent_configuration_sha256: str
|
|
1369
|
+
generation: int
|
|
1370
|
+
members: tuple[PortfolioVariationMemberReceipt, ...]
|
|
1371
|
+
memory_credit: PortfolioMemoryCreditReceipt | None = None
|
|
1372
|
+
action_attributions: tuple[PortfolioActionAttributionReceipt, ...] = ()
|
|
1373
|
+
|
|
1374
|
+
def __post_init__(self) -> None:
|
|
1375
|
+
if type(self.selection_call_id) is not LLMCallId:
|
|
1376
|
+
raise TypeError("selection_call_id must be exact")
|
|
1377
|
+
LLMCallId.__post_init__(self.selection_call_id)
|
|
1378
|
+
for name in (
|
|
1379
|
+
"request_sha256",
|
|
1380
|
+
"decision_sha256",
|
|
1381
|
+
"selection_policy_definition_sha256",
|
|
1382
|
+
"selection_telemetry_sha256",
|
|
1383
|
+
"parent_configuration_sha256",
|
|
1384
|
+
):
|
|
1385
|
+
require_sha256(getattr(self, name), name)
|
|
1386
|
+
_require_token(self.selection_policy_id, "selection_policy_id")
|
|
1387
|
+
if (
|
|
1388
|
+
type(self.selection_policy_version) is not int
|
|
1389
|
+
or self.selection_policy_version <= 0
|
|
1390
|
+
):
|
|
1391
|
+
raise ValueError("selection_policy_version must be positive")
|
|
1392
|
+
if type(self.selection_telemetry) is not AgenticCallTelemetry:
|
|
1393
|
+
raise TypeError("selection_telemetry must be exact")
|
|
1394
|
+
if self.selection_telemetry_sha256 != portfolio_selection_telemetry_sha256(
|
|
1395
|
+
self.selection_telemetry
|
|
1396
|
+
):
|
|
1397
|
+
raise ValueError("selection telemetry digest does not verify")
|
|
1398
|
+
if type(self.parent_candidate_id) is not CandidateId:
|
|
1399
|
+
raise TypeError("parent_candidate_id must be exact")
|
|
1400
|
+
CandidateId.__post_init__(self.parent_candidate_id)
|
|
1401
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
1402
|
+
raise ValueError("generation must be positive")
|
|
1403
|
+
if type(self.members) is not tuple or not self.members:
|
|
1404
|
+
raise ValueError("members must be a non-empty exact tuple")
|
|
1405
|
+
for member in self.members:
|
|
1406
|
+
if type(member) is not PortfolioVariationMemberReceipt:
|
|
1407
|
+
raise TypeError("members must contain exact receipts")
|
|
1408
|
+
PortfolioVariationMemberReceipt.__post_init__(member)
|
|
1409
|
+
if tuple(member.materialization.rank for member in self.members) != tuple(
|
|
1410
|
+
range(1, len(self.members) + 1)
|
|
1411
|
+
):
|
|
1412
|
+
raise ValueError("member receipts must use contiguous ranked order")
|
|
1413
|
+
for member in self.members:
|
|
1414
|
+
materialization = member.materialization
|
|
1415
|
+
if (
|
|
1416
|
+
materialization.request_sha256 != self.request_sha256
|
|
1417
|
+
or materialization.decision_sha256 != self.decision_sha256
|
|
1418
|
+
or materialization.selection_telemetry_sha256
|
|
1419
|
+
!= self.selection_telemetry_sha256
|
|
1420
|
+
or materialization.parent_candidate_id != self.parent_candidate_id
|
|
1421
|
+
or materialization.parent_configuration_sha256
|
|
1422
|
+
!= self.parent_configuration_sha256
|
|
1423
|
+
or materialization.generation != self.generation
|
|
1424
|
+
):
|
|
1425
|
+
raise ValueError("member receipt differs from its wave identity")
|
|
1426
|
+
for values, name in (
|
|
1427
|
+
(
|
|
1428
|
+
tuple(member.materialization.option_id for member in self.members),
|
|
1429
|
+
"option IDs",
|
|
1430
|
+
),
|
|
1431
|
+
(
|
|
1432
|
+
tuple(member.materialization.candidate_id for member in self.members),
|
|
1433
|
+
"candidate IDs",
|
|
1434
|
+
),
|
|
1435
|
+
(
|
|
1436
|
+
tuple(
|
|
1437
|
+
member.materialization.child_configuration_sha256
|
|
1438
|
+
for member in self.members
|
|
1439
|
+
),
|
|
1440
|
+
"child configurations",
|
|
1441
|
+
),
|
|
1442
|
+
(
|
|
1443
|
+
tuple(member.operator_invocation_id for member in self.members),
|
|
1444
|
+
"operator invocations",
|
|
1445
|
+
),
|
|
1446
|
+
(
|
|
1447
|
+
tuple(member.materialization.receipt_sha256 for member in self.members),
|
|
1448
|
+
"materialization receipts",
|
|
1449
|
+
),
|
|
1450
|
+
):
|
|
1451
|
+
if len(set(values)) != len(values):
|
|
1452
|
+
raise ValueError(f"portfolio wave contains colliding {name}")
|
|
1453
|
+
if type(self.action_attributions) is not tuple or any(
|
|
1454
|
+
type(value) is not PortfolioActionAttributionReceipt
|
|
1455
|
+
for value in self.action_attributions
|
|
1456
|
+
):
|
|
1457
|
+
raise TypeError(
|
|
1458
|
+
"action_attributions must contain exact attribution receipts"
|
|
1459
|
+
)
|
|
1460
|
+
for value in self.action_attributions:
|
|
1461
|
+
PortfolioActionAttributionReceipt.__post_init__(value)
|
|
1462
|
+
if self.action_attributions:
|
|
1463
|
+
if len(self.action_attributions) != len(self.members):
|
|
1464
|
+
raise ValueError(
|
|
1465
|
+
"action attributions must exactly cover the wave members"
|
|
1466
|
+
)
|
|
1467
|
+
if (
|
|
1468
|
+
len({value.card_snapshot_sha256 for value in self.action_attributions})
|
|
1469
|
+
!= 1
|
|
1470
|
+
):
|
|
1471
|
+
raise ValueError(
|
|
1472
|
+
"action attributions must share one request-card snapshot"
|
|
1473
|
+
)
|
|
1474
|
+
if tuple(value.rank for value in self.action_attributions) != tuple(
|
|
1475
|
+
range(1, len(self.members) + 1)
|
|
1476
|
+
):
|
|
1477
|
+
raise ValueError("action attributions must use contiguous ranked order")
|
|
1478
|
+
for attribution, member in zip(
|
|
1479
|
+
self.action_attributions,
|
|
1480
|
+
self.members,
|
|
1481
|
+
strict=True,
|
|
1482
|
+
):
|
|
1483
|
+
materialization = member.materialization
|
|
1484
|
+
if (
|
|
1485
|
+
attribution.request_sha256 != self.request_sha256
|
|
1486
|
+
or attribution.decision_sha256 != self.decision_sha256
|
|
1487
|
+
or attribution.rank != materialization.rank
|
|
1488
|
+
or attribution.option_id != materialization.option_id
|
|
1489
|
+
or attribution.option_identity_sha256
|
|
1490
|
+
!= materialization.option_identity_sha256
|
|
1491
|
+
or attribution.child_configuration_sha256
|
|
1492
|
+
!= materialization.child_configuration_sha256
|
|
1493
|
+
or attribution.materialization_receipt_sha256
|
|
1494
|
+
!= materialization.receipt_sha256
|
|
1495
|
+
or attribution.outcome_sha256 != member.outcome_sha256
|
|
1496
|
+
or attribution.operator_invocation_id
|
|
1497
|
+
!= member.operator_invocation_id
|
|
1498
|
+
or attribution.candidate_id != materialization.candidate_id
|
|
1499
|
+
):
|
|
1500
|
+
raise ValueError(
|
|
1501
|
+
"action attribution differs from its evaluated wave member"
|
|
1502
|
+
)
|
|
1503
|
+
if len({value.receipt_sha256 for value in self.action_attributions}) != len(
|
|
1504
|
+
self.action_attributions
|
|
1505
|
+
):
|
|
1506
|
+
raise ValueError("portfolio wave repeats an action attribution")
|
|
1507
|
+
if self.memory_credit is not None:
|
|
1508
|
+
if type(self.memory_credit) is not PortfolioMemoryCreditReceipt:
|
|
1509
|
+
raise TypeError("memory_credit must be an exact receipt or None")
|
|
1510
|
+
PortfolioMemoryCreditReceipt.__post_init__(self.memory_credit)
|
|
1511
|
+
if self.memory_credit.candidate_ids != tuple(
|
|
1512
|
+
member.materialization.candidate_id for member in self.members
|
|
1513
|
+
):
|
|
1514
|
+
raise ValueError("memory credit candidate IDs differ from the wave")
|
|
1515
|
+
if self.memory_credit.credit_unit_id in {
|
|
1516
|
+
member.operator_invocation_id for member in self.members
|
|
1517
|
+
}:
|
|
1518
|
+
raise ValueError("memory credit unit collides with a child invocation")
|
|
1519
|
+
|
|
1520
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1521
|
+
self.__post_init__()
|
|
1522
|
+
return {
|
|
1523
|
+
"schema_version": 1,
|
|
1524
|
+
"selection_call_count": 1,
|
|
1525
|
+
"selection_call_id": self.selection_call_id.value,
|
|
1526
|
+
"request_sha256": self.request_sha256,
|
|
1527
|
+
"decision_sha256": self.decision_sha256,
|
|
1528
|
+
"selection_policy": {
|
|
1529
|
+
"policy_id": self.selection_policy_id,
|
|
1530
|
+
"policy_version": self.selection_policy_version,
|
|
1531
|
+
"definition_sha256": self.selection_policy_definition_sha256,
|
|
1532
|
+
},
|
|
1533
|
+
"selection_telemetry": _telemetry_record(self.selection_telemetry),
|
|
1534
|
+
"selection_telemetry_sha256": self.selection_telemetry_sha256,
|
|
1535
|
+
"parent_candidate_id": self.parent_candidate_id.value,
|
|
1536
|
+
"parent_configuration_sha256": self.parent_configuration_sha256,
|
|
1537
|
+
"generation": self.generation,
|
|
1538
|
+
"member_count": len(self.members),
|
|
1539
|
+
"concurrent_materialized_evaluation_wave": True,
|
|
1540
|
+
"members": [member.to_record() for member in self.members],
|
|
1541
|
+
**(
|
|
1542
|
+
{}
|
|
1543
|
+
if not self.action_attributions
|
|
1544
|
+
else {
|
|
1545
|
+
"action_attributions": [
|
|
1546
|
+
value.to_record() for value in self.action_attributions
|
|
1547
|
+
]
|
|
1548
|
+
}
|
|
1549
|
+
),
|
|
1550
|
+
"memory_credit": (
|
|
1551
|
+
None if self.memory_credit is None else self.memory_credit.to_record()
|
|
1552
|
+
),
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
@property
|
|
1556
|
+
def receipt_sha256(self) -> str:
|
|
1557
|
+
return _hash_record(_WAVE_DOMAIN, self._unsigned_record())
|
|
1558
|
+
|
|
1559
|
+
def to_record(self) -> dict[str, object]:
|
|
1560
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
@dataclass(frozen=True, slots=True)
|
|
1564
|
+
class PortfolioVariationWaveResult:
|
|
1565
|
+
"""Planner-facing wave result with an exact receipt-to-outcome join.
|
|
1566
|
+
|
|
1567
|
+
The receipt remains the evidence-only serialization boundary. Outcomes
|
|
1568
|
+
remain typed runtime values so a generic planner can promote candidates to
|
|
1569
|
+
later generations or pass the complete wave to reflection without parsing
|
|
1570
|
+
evidence records back into domain objects.
|
|
1571
|
+
"""
|
|
1572
|
+
|
|
1573
|
+
receipt: PortfolioVariationWaveReceipt
|
|
1574
|
+
outcomes: tuple[InvocationOutcome, ...]
|
|
1575
|
+
selection_decision: RankedPortfolioDecision | None = None
|
|
1576
|
+
supplemental_selection_audit: PortfolioSelectionSupplementalAudit | None = None
|
|
1577
|
+
pending_memory_credit: PortfolioPendingMemoryCredit | None = None
|
|
1578
|
+
|
|
1579
|
+
def __post_init__(self) -> None:
|
|
1580
|
+
if type(self.receipt) is not PortfolioVariationWaveReceipt:
|
|
1581
|
+
raise TypeError("receipt must be an exact PortfolioVariationWaveReceipt")
|
|
1582
|
+
PortfolioVariationWaveReceipt.__post_init__(self.receipt)
|
|
1583
|
+
if type(self.outcomes) is not tuple or any(
|
|
1584
|
+
type(outcome) is not InvocationOutcome for outcome in self.outcomes
|
|
1585
|
+
):
|
|
1586
|
+
raise TypeError("outcomes must contain exact InvocationOutcome values")
|
|
1587
|
+
if len(self.outcomes) != len(self.receipt.members):
|
|
1588
|
+
raise ValueError("result outcomes differ from the receipt member count")
|
|
1589
|
+
pending = self.pending_memory_credit
|
|
1590
|
+
if pending is not None:
|
|
1591
|
+
if type(pending) is not PortfolioPendingMemoryCredit:
|
|
1592
|
+
raise TypeError(
|
|
1593
|
+
"pending_memory_credit must be an exact pending credit or None"
|
|
1594
|
+
)
|
|
1595
|
+
PortfolioPendingMemoryCredit.__post_init__(pending)
|
|
1596
|
+
if self.receipt.memory_credit is not None:
|
|
1597
|
+
raise ValueError(
|
|
1598
|
+
"a wave result cannot be both pending and memory-committed"
|
|
1599
|
+
)
|
|
1600
|
+
if pending.candidate_ids != tuple(
|
|
1601
|
+
member.materialization.candidate_id for member in self.receipt.members
|
|
1602
|
+
):
|
|
1603
|
+
raise ValueError("pending memory credit candidate IDs differ from wave")
|
|
1604
|
+
decision = self.selection_decision
|
|
1605
|
+
if decision is not None:
|
|
1606
|
+
if type(decision) is not RankedPortfolioDecision:
|
|
1607
|
+
raise TypeError(
|
|
1608
|
+
"selection_decision must be an exact RankedPortfolioDecision "
|
|
1609
|
+
"or None"
|
|
1610
|
+
)
|
|
1611
|
+
RankedPortfolioDecision.__post_init__(decision)
|
|
1612
|
+
if (
|
|
1613
|
+
decision.decision_sha256 != self.receipt.decision_sha256
|
|
1614
|
+
or decision.request_sha256 != self.receipt.request_sha256
|
|
1615
|
+
or decision.policy_id != self.receipt.selection_policy_id
|
|
1616
|
+
or decision.policy_version != self.receipt.selection_policy_version
|
|
1617
|
+
or decision.policy_definition_sha256
|
|
1618
|
+
!= self.receipt.selection_policy_definition_sha256
|
|
1619
|
+
):
|
|
1620
|
+
raise ValueError(
|
|
1621
|
+
"selection decision differs from the wave receipt identity"
|
|
1622
|
+
)
|
|
1623
|
+
if len(decision.members) != len(self.receipt.members):
|
|
1624
|
+
raise ValueError(
|
|
1625
|
+
"selection decision differs from the wave materialization count"
|
|
1626
|
+
)
|
|
1627
|
+
for selected, received in zip(
|
|
1628
|
+
decision.members,
|
|
1629
|
+
self.receipt.members,
|
|
1630
|
+
strict=True,
|
|
1631
|
+
):
|
|
1632
|
+
materialization = received.materialization
|
|
1633
|
+
if (
|
|
1634
|
+
selected.rank != materialization.rank
|
|
1635
|
+
or selected.option_id != materialization.option_id
|
|
1636
|
+
or selected.option_identity_sha256
|
|
1637
|
+
!= materialization.option_identity_sha256
|
|
1638
|
+
or selected.child_configuration_sha256
|
|
1639
|
+
!= materialization.child_configuration_sha256
|
|
1640
|
+
):
|
|
1641
|
+
raise ValueError(
|
|
1642
|
+
"selection decision differs from a wave materialization"
|
|
1643
|
+
)
|
|
1644
|
+
if self.receipt.action_attributions:
|
|
1645
|
+
for selected, attribution in zip(
|
|
1646
|
+
decision.members,
|
|
1647
|
+
self.receipt.action_attributions,
|
|
1648
|
+
strict=True,
|
|
1649
|
+
):
|
|
1650
|
+
rationale_sha256 = hashlib.sha256(
|
|
1651
|
+
selected.design_rationale.encode(
|
|
1652
|
+
"utf-8",
|
|
1653
|
+
errors="strict",
|
|
1654
|
+
)
|
|
1655
|
+
).hexdigest()
|
|
1656
|
+
if (
|
|
1657
|
+
attribution.decision_sha256 != decision.decision_sha256
|
|
1658
|
+
or attribution.card_snapshot_sha256
|
|
1659
|
+
!= decision.card_snapshot_sha256
|
|
1660
|
+
or attribution.rank != selected.rank
|
|
1661
|
+
or attribution.option_id != selected.option_id
|
|
1662
|
+
or attribution.option_identity_sha256
|
|
1663
|
+
!= selected.option_identity_sha256
|
|
1664
|
+
or attribution.child_configuration_sha256
|
|
1665
|
+
!= selected.child_configuration_sha256
|
|
1666
|
+
or attribution.family != selected.family
|
|
1667
|
+
or attribution.supporting_card_keys
|
|
1668
|
+
!= selected.supporting_card_keys
|
|
1669
|
+
or attribution.effect_predictions != selected.effect_predictions
|
|
1670
|
+
or attribution.design_rationale_sha256 != rationale_sha256
|
|
1671
|
+
):
|
|
1672
|
+
raise ValueError(
|
|
1673
|
+
"action attribution differs from the selection decision"
|
|
1674
|
+
)
|
|
1675
|
+
audit = self.supplemental_selection_audit
|
|
1676
|
+
if audit is not None:
|
|
1677
|
+
if type(audit) is not PortfolioSelectionSupplementalAudit:
|
|
1678
|
+
raise TypeError("supplemental_selection_audit must be exact or None")
|
|
1679
|
+
audit.__post_init__()
|
|
1680
|
+
if decision is None:
|
|
1681
|
+
raise ValueError("supplemental selection audit requires a decision")
|
|
1682
|
+
if (
|
|
1683
|
+
audit.request_sha256 != decision.request_sha256
|
|
1684
|
+
or audit.decision_sha256 != decision.decision_sha256
|
|
1685
|
+
):
|
|
1686
|
+
raise ValueError(
|
|
1687
|
+
"supplemental selection audit differs from the decision"
|
|
1688
|
+
)
|
|
1689
|
+
for member, outcome in zip(
|
|
1690
|
+
self.receipt.members,
|
|
1691
|
+
self.outcomes,
|
|
1692
|
+
strict=True,
|
|
1693
|
+
):
|
|
1694
|
+
InvocationOutcome.__post_init__(outcome)
|
|
1695
|
+
materialization = member.materialization
|
|
1696
|
+
prepared = outcome.prepared
|
|
1697
|
+
candidate = outcome.candidate
|
|
1698
|
+
if (
|
|
1699
|
+
outcome.failure_stage is not None
|
|
1700
|
+
or candidate is None
|
|
1701
|
+
or not candidate.operator_compliant
|
|
1702
|
+
or not candidate.evidence_compliant
|
|
1703
|
+
):
|
|
1704
|
+
raise ValueError("result contains a failed or partial outcome")
|
|
1705
|
+
detailed = candidate.detailed_evaluation
|
|
1706
|
+
detailed_sha256 = None if detailed is None else detailed.evidence_sha256
|
|
1707
|
+
observed_failure: PortfolioCandidateFailureEvidence | None = None
|
|
1708
|
+
if member.disposition is PortfolioMemberDisposition.SCORED:
|
|
1709
|
+
if not candidate.valid:
|
|
1710
|
+
raise ValueError(
|
|
1711
|
+
"scored result member contains an invalid candidate"
|
|
1712
|
+
)
|
|
1713
|
+
else:
|
|
1714
|
+
if candidate.valid or detailed is None or detailed.failure is None:
|
|
1715
|
+
raise ValueError(
|
|
1716
|
+
"candidate-infeasible result lacks detailed failure evidence"
|
|
1717
|
+
)
|
|
1718
|
+
observed_failure = (
|
|
1719
|
+
PortfolioCandidateFailureEvidence.from_failure_record(
|
|
1720
|
+
detailed.failure,
|
|
1721
|
+
detailed_evaluation_sha256=detailed.evidence_sha256,
|
|
1722
|
+
)
|
|
1723
|
+
)
|
|
1724
|
+
if (
|
|
1725
|
+
prepared.operator_invocation_id != member.operator_invocation_id
|
|
1726
|
+
or prepared.proposal_authority is not ProposalAuthority.ENGINE
|
|
1727
|
+
or prepared.call_id is not None
|
|
1728
|
+
or prepared.candidate_id != materialization.candidate_id
|
|
1729
|
+
or prepared.materialization_policy_id
|
|
1730
|
+
!= PORTFOLIO_MATERIALIZATION_POLICY_ID
|
|
1731
|
+
or prepared.materialization_policy_version
|
|
1732
|
+
!= PORTFOLIO_MATERIALIZATION_POLICY_VERSION
|
|
1733
|
+
or prepared.materialization_receipt_hash
|
|
1734
|
+
!= materialization.receipt_sha256
|
|
1735
|
+
or prepared.variation_case.reward_definition_hash
|
|
1736
|
+
!= member.reward_definition_sha256
|
|
1737
|
+
or candidate.candidate_id != materialization.candidate_id
|
|
1738
|
+
or candidate.occurrence.configuration_hash
|
|
1739
|
+
!= materialization.child_configuration_sha256
|
|
1740
|
+
or candidate.generation != materialization.generation
|
|
1741
|
+
or candidate.operator_kind is not OperatorKind.TYPED_MUTATION
|
|
1742
|
+
or candidate.parent_ids != (materialization.parent_candidate_id,)
|
|
1743
|
+
or candidate.parent_patch_hashes != (materialization.patch_sha256,)
|
|
1744
|
+
or candidate.call_telemetry is not None
|
|
1745
|
+
or tuple(
|
|
1746
|
+
(item.path, item.source) for item in candidate.source_attribution
|
|
1747
|
+
)
|
|
1748
|
+
!= tuple((path, "mutation") for path in materialization.changed_paths)
|
|
1749
|
+
or outcome.reward != member.reward
|
|
1750
|
+
or outcome.parent_relations != member.parent_relations
|
|
1751
|
+
or outcome.dominates_any_parent != member.dominates_any_parent
|
|
1752
|
+
or outcome.better_than_any_parent != member.better_than_any_parent
|
|
1753
|
+
or detailed_sha256 != member.detailed_evaluation_sha256
|
|
1754
|
+
or observed_failure != member.candidate_failure
|
|
1755
|
+
):
|
|
1756
|
+
raise ValueError("result outcome differs from its receipt member")
|
|
1757
|
+
|
|
1758
|
+
@property
|
|
1759
|
+
def action_attributions(self) -> tuple[PortfolioActionAttributionReceipt, ...]:
|
|
1760
|
+
"""Return exact per-action attribution receipts in ranked order."""
|
|
1761
|
+
|
|
1762
|
+
self.__post_init__()
|
|
1763
|
+
return self.receipt.action_attributions
|
|
1764
|
+
|
|
1765
|
+
@property
|
|
1766
|
+
def candidates(self) -> tuple[EvolutionCandidate, ...]:
|
|
1767
|
+
"""Return the full ranked intention-to-treat candidate population."""
|
|
1768
|
+
|
|
1769
|
+
candidates: list[EvolutionCandidate] = []
|
|
1770
|
+
for outcome in self.outcomes:
|
|
1771
|
+
candidate = outcome.candidate
|
|
1772
|
+
if candidate is None: # pragma: no cover - closed by __post_init__.
|
|
1773
|
+
raise AssertionError("validated portfolio outcome lost its candidate")
|
|
1774
|
+
candidates.append(candidate)
|
|
1775
|
+
return tuple(candidates)
|
|
1776
|
+
|
|
1777
|
+
@property
|
|
1778
|
+
def scored_candidates(self) -> tuple[EvolutionCandidate, ...]:
|
|
1779
|
+
"""Return only candidates with complete decision-objective vectors."""
|
|
1780
|
+
|
|
1781
|
+
return tuple(
|
|
1782
|
+
candidate
|
|
1783
|
+
for member, candidate in zip(
|
|
1784
|
+
self.receipt.members,
|
|
1785
|
+
self.candidates,
|
|
1786
|
+
strict=True,
|
|
1787
|
+
)
|
|
1788
|
+
if member.disposition is PortfolioMemberDisposition.SCORED
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1791
|
+
@property
|
|
1792
|
+
def infeasible_candidates(self) -> tuple[EvolutionCandidate, ...]:
|
|
1793
|
+
"""Return evaluated candidate-attributable infeasibilities in rank order."""
|
|
1794
|
+
|
|
1795
|
+
return tuple(
|
|
1796
|
+
candidate
|
|
1797
|
+
for member, candidate in zip(
|
|
1798
|
+
self.receipt.members,
|
|
1799
|
+
self.candidates,
|
|
1800
|
+
strict=True,
|
|
1801
|
+
)
|
|
1802
|
+
if member.disposition is PortfolioMemberDisposition.CANDIDATE_INFEASIBLE
|
|
1803
|
+
)
|
|
1804
|
+
|
|
1805
|
+
@property
|
|
1806
|
+
def selection_decision_audit_record(self) -> dict[str, object] | None:
|
|
1807
|
+
"""Return model reasoning for trace audit, or ``None`` for legacy results."""
|
|
1808
|
+
|
|
1809
|
+
if self.selection_decision is None:
|
|
1810
|
+
return None
|
|
1811
|
+
ranked = self.selection_decision.to_audit_record()
|
|
1812
|
+
if self.supplemental_selection_audit is None:
|
|
1813
|
+
return ranked
|
|
1814
|
+
return {
|
|
1815
|
+
"ranked_decision": ranked,
|
|
1816
|
+
"supplemental_selector_audit": (
|
|
1817
|
+
self.supplemental_selection_audit.to_record()
|
|
1818
|
+
),
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
@dataclass(frozen=True, slots=True)
|
|
1823
|
+
class PortfolioMemoryCreditBatchPreparation:
|
|
1824
|
+
"""Sealed, non-mutating preview of one generation memory publication.
|
|
1825
|
+
|
|
1826
|
+
``prepared_results`` contain the exact committed-receipt projection that
|
|
1827
|
+
downstream prepare hooks may inspect, while ``expected_trials`` remain
|
|
1828
|
+
absent from the live memory bank until :class:`PortfolioEvolution` commits
|
|
1829
|
+
this exact value. Retaining the prior immutable trial snapshot turns a
|
|
1830
|
+
concurrent or out-of-band bank change into a stale-preparation failure.
|
|
1831
|
+
"""
|
|
1832
|
+
|
|
1833
|
+
generation: int
|
|
1834
|
+
source_result_receipt_sha256s: tuple[str, ...]
|
|
1835
|
+
source_pending_credit_sha256s: tuple[str | None, ...]
|
|
1836
|
+
prepared_results: tuple[PortfolioVariationWaveResult, ...]
|
|
1837
|
+
expected_trials: tuple[InsightTrial, ...]
|
|
1838
|
+
prior_memory_trials: tuple[InsightTrial, ...]
|
|
1839
|
+
batch_receipt: PortfolioMemoryCreditBatchReceipt | None
|
|
1840
|
+
|
|
1841
|
+
def __post_init__(self) -> None:
|
|
1842
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
1843
|
+
raise ValueError("generation must be a positive exact integer")
|
|
1844
|
+
if (
|
|
1845
|
+
type(self.source_result_receipt_sha256s) is not tuple
|
|
1846
|
+
or not self.source_result_receipt_sha256s
|
|
1847
|
+
):
|
|
1848
|
+
raise ValueError("source result receipts must be a non-empty exact tuple")
|
|
1849
|
+
for value in self.source_result_receipt_sha256s:
|
|
1850
|
+
require_sha256(value, "source_result_receipt_sha256")
|
|
1851
|
+
if (
|
|
1852
|
+
type(self.source_pending_credit_sha256s) is not tuple
|
|
1853
|
+
or len(self.source_pending_credit_sha256s)
|
|
1854
|
+
!= len(self.source_result_receipt_sha256s)
|
|
1855
|
+
):
|
|
1856
|
+
raise ValueError("pending credit identities differ from source results")
|
|
1857
|
+
for value in self.source_pending_credit_sha256s:
|
|
1858
|
+
if value is not None:
|
|
1859
|
+
require_sha256(value, "source_pending_credit_sha256")
|
|
1860
|
+
if (
|
|
1861
|
+
type(self.prepared_results) is not tuple
|
|
1862
|
+
or len(self.prepared_results) != len(self.source_result_receipt_sha256s)
|
|
1863
|
+
or any(
|
|
1864
|
+
type(value) is not PortfolioVariationWaveResult
|
|
1865
|
+
for value in self.prepared_results
|
|
1866
|
+
)
|
|
1867
|
+
):
|
|
1868
|
+
raise ValueError("prepared_results must exactly cover source results")
|
|
1869
|
+
for result in self.prepared_results:
|
|
1870
|
+
PortfolioVariationWaveResult.__post_init__(result)
|
|
1871
|
+
if result.receipt.generation != self.generation:
|
|
1872
|
+
raise ValueError("prepared results differ from the generation")
|
|
1873
|
+
if result.pending_memory_credit is not None:
|
|
1874
|
+
raise ValueError("prepared results cannot retain pending memory credit")
|
|
1875
|
+
for name in ("expected_trials", "prior_memory_trials"):
|
|
1876
|
+
values = getattr(self, name)
|
|
1877
|
+
if type(values) is not tuple or any(
|
|
1878
|
+
type(value) is not InsightTrial for value in values
|
|
1879
|
+
):
|
|
1880
|
+
raise TypeError(f"{name} must contain exact InsightTrial values")
|
|
1881
|
+
for value in values:
|
|
1882
|
+
InsightTrial.__post_init__(value)
|
|
1883
|
+
if self.batch_receipt is None:
|
|
1884
|
+
if self.expected_trials or any(self.source_pending_credit_sha256s):
|
|
1885
|
+
raise ValueError("a no-credit preparation cannot contain credit state")
|
|
1886
|
+
if any(
|
|
1887
|
+
result.receipt.memory_credit is not None
|
|
1888
|
+
for result in self.prepared_results
|
|
1889
|
+
):
|
|
1890
|
+
raise ValueError("a no-credit preparation contains committed receipts")
|
|
1891
|
+
else:
|
|
1892
|
+
if type(self.batch_receipt) is not PortfolioMemoryCreditBatchReceipt:
|
|
1893
|
+
raise TypeError("batch_receipt must be exact or None")
|
|
1894
|
+
PortfolioMemoryCreditBatchReceipt.__post_init__(self.batch_receipt)
|
|
1895
|
+
if self.batch_receipt.generation != self.generation:
|
|
1896
|
+
raise ValueError("memory batch differs from the generation")
|
|
1897
|
+
if len(self.expected_trials) != len(self.batch_receipt.credits):
|
|
1898
|
+
raise ValueError("prospective trials differ from batch credits")
|
|
1899
|
+
if tuple(
|
|
1900
|
+
value.credit_unit_id for value in self.expected_trials
|
|
1901
|
+
) != tuple(
|
|
1902
|
+
value.credit_unit_id for value in self.batch_receipt.credits
|
|
1903
|
+
):
|
|
1904
|
+
raise ValueError("prospective trials differ from canonical credits")
|
|
1905
|
+
for trial, credit in zip(
|
|
1906
|
+
self.expected_trials,
|
|
1907
|
+
self.batch_receipt.credits,
|
|
1908
|
+
strict=True,
|
|
1909
|
+
):
|
|
1910
|
+
if (
|
|
1911
|
+
insight_selection_decision_sha256(trial.decision)
|
|
1912
|
+
!= credit.selection_decision_sha256
|
|
1913
|
+
or trial.decision.context_hash
|
|
1914
|
+
!= credit.selection_decision_context_sha256
|
|
1915
|
+
or trial.candidate_ids != credit.candidate_ids
|
|
1916
|
+
or trial.reward_definition_hash
|
|
1917
|
+
!= credit.aggregation_definition_sha256
|
|
1918
|
+
or trial.reward != credit.reward
|
|
1919
|
+
or trial.treatment_binding_sha256
|
|
1920
|
+
!= credit.treatment_binding_sha256
|
|
1921
|
+
or trial.generation != credit.generation
|
|
1922
|
+
):
|
|
1923
|
+
raise ValueError(
|
|
1924
|
+
"prospective trial differs from its committed credit receipt"
|
|
1925
|
+
)
|
|
1926
|
+
if self.batch_receipt.memory_trial_count_before != len(
|
|
1927
|
+
self.prior_memory_trials
|
|
1928
|
+
):
|
|
1929
|
+
raise ValueError("batch receipt differs from prior memory snapshot")
|
|
1930
|
+
|
|
1931
|
+
@staticmethod
|
|
1932
|
+
def _trial_record(value: InsightTrial) -> dict[str, object]:
|
|
1933
|
+
return {
|
|
1934
|
+
"credit_unit_id": value.credit_unit_id.value,
|
|
1935
|
+
"candidate_ids": [item.value for item in value.candidate_ids],
|
|
1936
|
+
"reward_definition_sha256": value.reward_definition_hash,
|
|
1937
|
+
"selection_decision_sha256": insight_selection_decision_sha256(
|
|
1938
|
+
value.decision
|
|
1939
|
+
),
|
|
1940
|
+
"reward_hex": value.reward.hex(),
|
|
1941
|
+
"treatment_binding_sha256": value.treatment_binding_sha256,
|
|
1942
|
+
"generation": value.generation,
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1946
|
+
self.__post_init__()
|
|
1947
|
+
return {
|
|
1948
|
+
"schema_version": 1,
|
|
1949
|
+
"generation": self.generation,
|
|
1950
|
+
"source_result_receipt_sha256s": list(
|
|
1951
|
+
self.source_result_receipt_sha256s
|
|
1952
|
+
),
|
|
1953
|
+
"source_pending_credit_sha256s": list(
|
|
1954
|
+
self.source_pending_credit_sha256s
|
|
1955
|
+
),
|
|
1956
|
+
"prepared_result_receipt_sha256s": [
|
|
1957
|
+
value.receipt.receipt_sha256 for value in self.prepared_results
|
|
1958
|
+
],
|
|
1959
|
+
"prior_memory_trials": [
|
|
1960
|
+
self._trial_record(value) for value in self.prior_memory_trials
|
|
1961
|
+
],
|
|
1962
|
+
"expected_trials": [
|
|
1963
|
+
self._trial_record(value) for value in self.expected_trials
|
|
1964
|
+
],
|
|
1965
|
+
"memory_credit_batch_receipt_sha256": (
|
|
1966
|
+
None
|
|
1967
|
+
if self.batch_receipt is None
|
|
1968
|
+
else self.batch_receipt.receipt_sha256
|
|
1969
|
+
),
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
@property
|
|
1973
|
+
def preparation_sha256(self) -> str:
|
|
1974
|
+
return _hash_record(
|
|
1975
|
+
_MEMORY_CREDIT_BATCH_PREPARATION_DOMAIN,
|
|
1976
|
+
self._unsigned_record(),
|
|
1977
|
+
)
|
|
1978
|
+
|
|
1979
|
+
def to_record(self) -> dict[str, object]:
|
|
1980
|
+
return {**self._unsigned_record(), "preparation_sha256": self.preparation_sha256}
|
|
1981
|
+
|
|
1982
|
+
|
|
1983
|
+
@runtime_checkable
|
|
1984
|
+
class MaterializedPortfolioEngine(Protocol):
|
|
1985
|
+
async def run_materialized_invocations(
|
|
1986
|
+
self,
|
|
1987
|
+
items: tuple[MaterializedInvocation, ...],
|
|
1988
|
+
*,
|
|
1989
|
+
reward_binding: RewardPolicyBinding | None = None,
|
|
1990
|
+
) -> tuple[InvocationOutcome, ...]: ...
|
|
1991
|
+
|
|
1992
|
+
|
|
1993
|
+
# A distinct, non-secret constant standing where a telemetry digest would go
|
|
1994
|
+
# for a selection that provably made no provider call. It is not a hash of any
|
|
1995
|
+
# telemetry record: there is no record, and inventing one would be the
|
|
1996
|
+
# fabrication this design exists to prevent.
|
|
1997
|
+
PROVIDER_FREE_SELECTION_TELEMETRY_SHA256 = hashlib.sha256(
|
|
1998
|
+
b"agent-evolve:provider-free-portfolio-selection:v1\x00"
|
|
1999
|
+
b"no-provider-call-observed-over-the-selection-window"
|
|
2000
|
+
).hexdigest()
|
|
2001
|
+
|
|
2002
|
+
|
|
2003
|
+
@runtime_checkable
|
|
2004
|
+
class ProviderTrafficWitness(Protocol):
|
|
2005
|
+
"""Counts provider calls actually observed, for verifying a claimed zero.
|
|
2006
|
+
|
|
2007
|
+
The runtime reads this immediately before and after a selection. A policy
|
|
2008
|
+
that asserts ``provider_free`` must leave the count unchanged; if it moved,
|
|
2009
|
+
the claim was false and the wave fails loudly rather than sealing a receipt
|
|
2010
|
+
that says no call happened while one did.
|
|
2011
|
+
|
|
2012
|
+
Implementations should count from the journals the run declared -- see
|
|
2013
|
+
``agent_evolve.provider_accounting.measure_provider_usage`` -- so that a
|
|
2014
|
+
missing sink is an instrumentation fault rather than a silent zero.
|
|
2015
|
+
"""
|
|
2016
|
+
|
|
2017
|
+
def observed_provider_calls(self) -> int: ...
|
|
2018
|
+
|
|
2019
|
+
@dataclass(frozen=True, slots=True)
|
|
2020
|
+
class PreparedPortfolioVariationWave:
|
|
2021
|
+
"""One selected and materialized portfolio before any real evaluation.
|
|
2022
|
+
|
|
2023
|
+
This is the public hand-off used by residual proposal markets. The LLM
|
|
2024
|
+
decision has already been resolved against the sealed finite contract and
|
|
2025
|
+
every target occurrence has been allocated, but the expensive evaluator
|
|
2026
|
+
has not run. A downstream broker can therefore compare these actions with
|
|
2027
|
+
acquisition, restart, or recombination proposals before spending budget.
|
|
2028
|
+
"""
|
|
2029
|
+
|
|
2030
|
+
wave: PortfolioVariationWaveRequest
|
|
2031
|
+
selection: PortfolioSelectionResult
|
|
2032
|
+
selection_telemetry_sha256: str
|
|
2033
|
+
invocations: tuple[MaterializedInvocation, ...]
|
|
2034
|
+
materializations: tuple[PortfolioMemberMaterializationReceipt, ...]
|
|
2035
|
+
preparation_sha256: str = field(init=False)
|
|
2036
|
+
|
|
2037
|
+
def __post_init__(self) -> None:
|
|
2038
|
+
if type(self.wave) is not PortfolioVariationWaveRequest:
|
|
2039
|
+
raise TypeError("wave must be an exact PortfolioVariationWaveRequest")
|
|
2040
|
+
PortfolioVariationWaveRequest.__post_init__(self.wave)
|
|
2041
|
+
if type(self.selection) is not PortfolioSelectionResult:
|
|
2042
|
+
raise TypeError("selection must be an exact PortfolioSelectionResult")
|
|
2043
|
+
PortfolioSelectionResult.__post_init__(self.selection)
|
|
2044
|
+
telemetry = self.selection.telemetry
|
|
2045
|
+
require_sha256(
|
|
2046
|
+
self.selection_telemetry_sha256,
|
|
2047
|
+
"selection_telemetry_sha256",
|
|
2048
|
+
)
|
|
2049
|
+
# A provider-free selection has no telemetry to digest, by construction:
|
|
2050
|
+
# there was no call. It carries the distinct sentinel instead, and the
|
|
2051
|
+
# zero it asserts was verified against a traffic witness at selection
|
|
2052
|
+
# time. Absence is therefore checked here, never inferred.
|
|
2053
|
+
if telemetry is None:
|
|
2054
|
+
if not self.selection.provider_free:
|
|
2055
|
+
raise ValueError("prepared portfolio selection requires telemetry")
|
|
2056
|
+
if self.selection_telemetry_sha256 != (
|
|
2057
|
+
PROVIDER_FREE_SELECTION_TELEMETRY_SHA256
|
|
2058
|
+
):
|
|
2059
|
+
raise ValueError(
|
|
2060
|
+
"a provider-free prepared selection must carry the "
|
|
2061
|
+
"provider-free telemetry sentinel"
|
|
2062
|
+
)
|
|
2063
|
+
elif (
|
|
2064
|
+
portfolio_selection_telemetry_sha256(telemetry)
|
|
2065
|
+
!= self.selection_telemetry_sha256
|
|
2066
|
+
):
|
|
2067
|
+
raise ValueError("prepared telemetry identity differs from selection")
|
|
2068
|
+
validate_ranked_portfolio_decision(
|
|
2069
|
+
self.wave.selection_request,
|
|
2070
|
+
self.selection.decision,
|
|
2071
|
+
)
|
|
2072
|
+
count = len(self.selection.decision.members)
|
|
2073
|
+
if (
|
|
2074
|
+
type(self.invocations) is not tuple
|
|
2075
|
+
or type(self.materializations) is not tuple
|
|
2076
|
+
or len(self.invocations) != count
|
|
2077
|
+
or len(self.materializations) != count
|
|
2078
|
+
):
|
|
2079
|
+
raise ValueError("prepared members must exactly cover the selection")
|
|
2080
|
+
for invocation in self.invocations:
|
|
2081
|
+
if type(invocation) is not MaterializedInvocation:
|
|
2082
|
+
raise TypeError("invocations must contain exact materialized values")
|
|
2083
|
+
MaterializedInvocation.__post_init__(invocation)
|
|
2084
|
+
for receipt in self.materializations:
|
|
2085
|
+
if type(receipt) is not PortfolioMemberMaterializationReceipt:
|
|
2086
|
+
raise TypeError("materializations must contain exact receipts")
|
|
2087
|
+
PortfolioMemberMaterializationReceipt.__post_init__(receipt)
|
|
2088
|
+
for selected, invocation, receipt in zip(
|
|
2089
|
+
self.selection.decision.members,
|
|
2090
|
+
self.invocations,
|
|
2091
|
+
self.materializations,
|
|
2092
|
+
strict=True,
|
|
2093
|
+
):
|
|
2094
|
+
if (
|
|
2095
|
+
selected.rank != receipt.rank
|
|
2096
|
+
or selected.option_id != receipt.option_id
|
|
2097
|
+
or selected.option_identity_sha256
|
|
2098
|
+
!= receipt.option_identity_sha256
|
|
2099
|
+
or selected.child_configuration_sha256
|
|
2100
|
+
!= receipt.child_configuration_sha256
|
|
2101
|
+
or invocation.candidate_id != receipt.candidate_id
|
|
2102
|
+
or invocation.materialization_receipt_hash
|
|
2103
|
+
!= receipt.receipt_sha256
|
|
2104
|
+
or invocation.plan.parents != (self.wave.parent,)
|
|
2105
|
+
or invocation.plan.generation != self.wave.generation
|
|
2106
|
+
or typed_json_sha256(freeze_json(invocation.draft.configuration))
|
|
2107
|
+
!= receipt.child_configuration_sha256
|
|
2108
|
+
):
|
|
2109
|
+
raise ValueError(
|
|
2110
|
+
"prepared invocation differs from its selected finite option"
|
|
2111
|
+
)
|
|
2112
|
+
candidate_ids = tuple(value.candidate_id for value in self.invocations)
|
|
2113
|
+
child_sha256s = tuple(
|
|
2114
|
+
value.child_configuration_sha256 for value in self.materializations
|
|
2115
|
+
)
|
|
2116
|
+
if (
|
|
2117
|
+
len(set(candidate_ids)) != count
|
|
2118
|
+
or len(set(child_sha256s)) != count
|
|
2119
|
+
):
|
|
2120
|
+
raise ValueError("prepared portfolio contains colliding members")
|
|
2121
|
+
object.__setattr__(
|
|
2122
|
+
self,
|
|
2123
|
+
"preparation_sha256",
|
|
2124
|
+
_hash_record(
|
|
2125
|
+
_PREPARED_WAVE_DOMAIN,
|
|
2126
|
+
{
|
|
2127
|
+
"schema_version": 1,
|
|
2128
|
+
"request_sha256": self.wave.selection_request.request_sha256,
|
|
2129
|
+
"decision_sha256": self.selection.decision.decision_sha256,
|
|
2130
|
+
"selection_telemetry_sha256": (
|
|
2131
|
+
self.selection_telemetry_sha256
|
|
2132
|
+
),
|
|
2133
|
+
"parent_candidate_id": self.wave.parent.candidate_id.value,
|
|
2134
|
+
"parent_configuration_sha256": (
|
|
2135
|
+
self.wave.parent.occurrence.configuration_hash
|
|
2136
|
+
),
|
|
2137
|
+
"generation": self.wave.generation,
|
|
2138
|
+
"materialization_receipt_sha256s": [
|
|
2139
|
+
value.receipt_sha256 for value in self.materializations
|
|
2140
|
+
],
|
|
2141
|
+
"evaluation_performed": False,
|
|
2142
|
+
},
|
|
2143
|
+
),
|
|
2144
|
+
)
|
|
2145
|
+
|
|
2146
|
+
def to_record(self) -> dict[str, object]:
|
|
2147
|
+
self.__post_init__()
|
|
2148
|
+
return {
|
|
2149
|
+
"schema_version": 1,
|
|
2150
|
+
"request_sha256": self.wave.selection_request.request_sha256,
|
|
2151
|
+
"decision_sha256": self.selection.decision.decision_sha256,
|
|
2152
|
+
"selection_telemetry_sha256": self.selection_telemetry_sha256,
|
|
2153
|
+
"parent_candidate_id": self.wave.parent.candidate_id.value,
|
|
2154
|
+
"parent_configuration_sha256": (
|
|
2155
|
+
self.wave.parent.occurrence.configuration_hash
|
|
2156
|
+
),
|
|
2157
|
+
"generation": self.wave.generation,
|
|
2158
|
+
"materializations": [
|
|
2159
|
+
value.to_record() for value in self.materializations
|
|
2160
|
+
],
|
|
2161
|
+
"preparation_sha256": self.preparation_sha256,
|
|
2162
|
+
"evaluation_performed": False,
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
|
|
2166
|
+
@dataclass(frozen=True, slots=True)
|
|
2167
|
+
class EvaluatedPreparedPortfolioMember:
|
|
2168
|
+
"""A broker-selected prepared member joined to one exact engine outcome."""
|
|
2169
|
+
|
|
2170
|
+
selected: RankedPortfolioMember
|
|
2171
|
+
invocation: MaterializedInvocation
|
|
2172
|
+
materialization: PortfolioMemberMaterializationReceipt
|
|
2173
|
+
outcome: InvocationOutcome
|
|
2174
|
+
receipt: PortfolioVariationMemberReceipt
|
|
2175
|
+
|
|
2176
|
+
def __post_init__(self) -> None:
|
|
2177
|
+
if type(self.selected) is not RankedPortfolioMember:
|
|
2178
|
+
raise TypeError("selected must be an exact ranked member")
|
|
2179
|
+
RankedPortfolioMember.__post_init__(self.selected)
|
|
2180
|
+
if type(self.invocation) is not MaterializedInvocation:
|
|
2181
|
+
raise TypeError("invocation must be exact")
|
|
2182
|
+
MaterializedInvocation.__post_init__(self.invocation)
|
|
2183
|
+
if type(self.materialization) is not PortfolioMemberMaterializationReceipt:
|
|
2184
|
+
raise TypeError("materialization must be exact")
|
|
2185
|
+
PortfolioMemberMaterializationReceipt.__post_init__(self.materialization)
|
|
2186
|
+
if type(self.outcome) is not InvocationOutcome:
|
|
2187
|
+
raise TypeError("outcome must be exact")
|
|
2188
|
+
InvocationOutcome.__post_init__(self.outcome)
|
|
2189
|
+
if type(self.receipt) is not PortfolioVariationMemberReceipt:
|
|
2190
|
+
raise TypeError("receipt must be an exact variation-member receipt")
|
|
2191
|
+
PortfolioVariationMemberReceipt.__post_init__(self.receipt)
|
|
2192
|
+
if (
|
|
2193
|
+
self.selected.rank != self.materialization.rank
|
|
2194
|
+
or self.invocation.candidate_id != self.materialization.candidate_id
|
|
2195
|
+
or self.receipt.materialization != self.materialization
|
|
2196
|
+
or self.outcome.candidate is None
|
|
2197
|
+
or self.outcome.candidate.candidate_id
|
|
2198
|
+
!= self.materialization.candidate_id
|
|
2199
|
+
):
|
|
2200
|
+
raise ValueError("evaluated prepared member has a broken exact join")
|
|
2201
|
+
|
|
2202
|
+
|
|
2203
|
+
@dataclass(slots=True)
|
|
2204
|
+
class PortfolioEvolution:
|
|
2205
|
+
"""Execute one ranked finite portfolio as a concurrent exact wave."""
|
|
2206
|
+
|
|
2207
|
+
engine: MaterializedPortfolioEngine
|
|
2208
|
+
selector: PortfolioSelectionPolicy
|
|
2209
|
+
ids: IdFactory
|
|
2210
|
+
memory: InsightMemoryBank | None = None
|
|
2211
|
+
provider_traffic_witness: ProviderTrafficWitness | None = None
|
|
2212
|
+
|
|
2213
|
+
def __post_init__(self) -> None:
|
|
2214
|
+
if not isinstance(self.engine, MaterializedPortfolioEngine):
|
|
2215
|
+
raise TypeError("engine must implement run_materialized_invocations")
|
|
2216
|
+
if not isinstance(self.selector, PortfolioSelectionPolicy):
|
|
2217
|
+
raise TypeError("selector must implement PortfolioSelectionPolicy")
|
|
2218
|
+
if not isinstance(self.ids, IdFactory):
|
|
2219
|
+
raise TypeError("ids must implement IdFactory")
|
|
2220
|
+
if self.memory is not None and type(self.memory) is not InsightMemoryBank:
|
|
2221
|
+
raise TypeError("memory must be an exact InsightMemoryBank or None")
|
|
2222
|
+
if self.provider_traffic_witness is not None and not isinstance(
|
|
2223
|
+
self.provider_traffic_witness, ProviderTrafficWitness
|
|
2224
|
+
):
|
|
2225
|
+
raise TypeError(
|
|
2226
|
+
"provider_traffic_witness must implement observed_provider_calls"
|
|
2227
|
+
)
|
|
2228
|
+
|
|
2229
|
+
def _materialize_member(
|
|
2230
|
+
self,
|
|
2231
|
+
wave: PortfolioVariationWaveRequest,
|
|
2232
|
+
decision: RankedPortfolioDecision,
|
|
2233
|
+
telemetry_sha256: str,
|
|
2234
|
+
member: RankedPortfolioMember,
|
|
2235
|
+
) -> tuple[MaterializedInvocation, PortfolioMemberMaterializationReceipt]:
|
|
2236
|
+
contract = wave.selection_request.finite_variation_contract
|
|
2237
|
+
option = contract.resolve(member.option_id)
|
|
2238
|
+
if (
|
|
2239
|
+
member.option_identity_sha256 != option.identity_sha256
|
|
2240
|
+
or member.child_configuration_sha256 != option.child_configuration_sha256
|
|
2241
|
+
):
|
|
2242
|
+
raise ValueError("ranked member drifted from its sealed finite option")
|
|
2243
|
+
candidate_id = self.ids.new_candidate_id()
|
|
2244
|
+
patch = derive_patch(
|
|
2245
|
+
wave.parent.configuration,
|
|
2246
|
+
option.child_configuration,
|
|
2247
|
+
base_candidate_id=wave.parent.candidate_id,
|
|
2248
|
+
target_candidate_id=candidate_id,
|
|
2249
|
+
)
|
|
2250
|
+
if not patch.operations:
|
|
2251
|
+
raise ValueError(
|
|
2252
|
+
"ranked finite option materialized no parent-relative patch"
|
|
2253
|
+
)
|
|
2254
|
+
top_level: set[str] = set()
|
|
2255
|
+
changed_paths: set[str] = set()
|
|
2256
|
+
for operation in patch.operations:
|
|
2257
|
+
if (
|
|
2258
|
+
not operation.path.segments
|
|
2259
|
+
or type(operation.path.segments[0]) is not ObjectKey
|
|
2260
|
+
):
|
|
2261
|
+
raise ValueError("ranked finite option changed the candidate root")
|
|
2262
|
+
top_level.add(operation.path.segments[0].value)
|
|
2263
|
+
changed_paths.add(_path_text(operation.path))
|
|
2264
|
+
paths = tuple(sorted(changed_paths))
|
|
2265
|
+
receipt = PortfolioMemberMaterializationReceipt(
|
|
2266
|
+
request_sha256=wave.selection_request.request_sha256,
|
|
2267
|
+
decision_sha256=decision.decision_sha256,
|
|
2268
|
+
selection_telemetry_sha256=telemetry_sha256,
|
|
2269
|
+
rank=member.rank,
|
|
2270
|
+
option_id=member.option_id,
|
|
2271
|
+
option_identity_sha256=member.option_identity_sha256,
|
|
2272
|
+
child_configuration_sha256=member.child_configuration_sha256,
|
|
2273
|
+
parent_candidate_id=wave.parent.candidate_id,
|
|
2274
|
+
parent_configuration_sha256=(wave.parent.occurrence.configuration_hash),
|
|
2275
|
+
generation=wave.generation,
|
|
2276
|
+
candidate_id=candidate_id,
|
|
2277
|
+
patch_sha256=patch.patch_hash,
|
|
2278
|
+
changed_paths=paths,
|
|
2279
|
+
)
|
|
2280
|
+
configuration = thaw_json(option.child_configuration)
|
|
2281
|
+
if type(configuration) is not dict:
|
|
2282
|
+
raise TypeError("finite option child must be an object")
|
|
2283
|
+
plan = InvocationPlan(
|
|
2284
|
+
operator_kind=OperatorKind.TYPED_MUTATION,
|
|
2285
|
+
parents=(wave.parent,),
|
|
2286
|
+
generation=wave.generation,
|
|
2287
|
+
label=f"{wave.label_prefix}.rank_{member.rank:04d}",
|
|
2288
|
+
allowed_top_level=tuple(sorted(top_level)),
|
|
2289
|
+
phase=wave.phase,
|
|
2290
|
+
)
|
|
2291
|
+
invocation = MaterializedInvocation(
|
|
2292
|
+
plan=plan,
|
|
2293
|
+
draft=CandidateDraft(
|
|
2294
|
+
configuration=configuration,
|
|
2295
|
+
design_rationale=(
|
|
2296
|
+
"Engine materialized one exact sealed finite option selected "
|
|
2297
|
+
"by ranked opaque identifier."
|
|
2298
|
+
),
|
|
2299
|
+
intended_changes=paths,
|
|
2300
|
+
source_attribution=tuple(
|
|
2301
|
+
SourceAttribution(path, "mutation") for path in paths
|
|
2302
|
+
),
|
|
2303
|
+
),
|
|
2304
|
+
candidate_id=candidate_id,
|
|
2305
|
+
materialization_policy_id=PORTFOLIO_MATERIALIZATION_POLICY_ID,
|
|
2306
|
+
materialization_policy_version=(PORTFOLIO_MATERIALIZATION_POLICY_VERSION),
|
|
2307
|
+
materialization_receipt_hash=receipt.receipt_sha256,
|
|
2308
|
+
)
|
|
2309
|
+
return invocation, receipt
|
|
2310
|
+
|
|
2311
|
+
async def prepare(
|
|
2312
|
+
self,
|
|
2313
|
+
wave: PortfolioVariationWaveRequest,
|
|
2314
|
+
) -> PreparedPortfolioVariationWave:
|
|
2315
|
+
"""Select once and materialize every member without evaluating it."""
|
|
2316
|
+
|
|
2317
|
+
if type(wave) is not PortfolioVariationWaveRequest:
|
|
2318
|
+
raise TypeError("wave must be an exact PortfolioVariationWaveRequest")
|
|
2319
|
+
PortfolioVariationWaveRequest.__post_init__(wave)
|
|
2320
|
+
request_sha256 = wave.selection_request.request_sha256
|
|
2321
|
+
contract_sha256 = (
|
|
2322
|
+
wave.selection_request.finite_variation_contract.identity_sha256
|
|
2323
|
+
)
|
|
2324
|
+
parent_sha256 = wave.parent.occurrence.configuration_hash
|
|
2325
|
+
observed_before = (
|
|
2326
|
+
self.provider_traffic_witness.observed_provider_calls()
|
|
2327
|
+
if self.provider_traffic_witness is not None
|
|
2328
|
+
else None
|
|
2329
|
+
)
|
|
2330
|
+
result = await self.selector.select(wave.selection_request)
|
|
2331
|
+
if type(result) is not PortfolioSelectionResult:
|
|
2332
|
+
raise TypeError("selector must return an exact PortfolioSelectionResult")
|
|
2333
|
+
PortfolioSelectionResult.__post_init__(result)
|
|
2334
|
+
if result.provider_free:
|
|
2335
|
+
# The assertion is not taken on trust. A provider-free selection is
|
|
2336
|
+
# a measured zero over the selection window, and a selector that
|
|
2337
|
+
# claims one while reaching the provider fails here rather than
|
|
2338
|
+
# sealing a receipt that contradicts the journals.
|
|
2339
|
+
if self.provider_traffic_witness is None:
|
|
2340
|
+
raise ValueError(
|
|
2341
|
+
"a provider-free selection requires a provider traffic "
|
|
2342
|
+
"witness; an unverified claim is not evidence"
|
|
2343
|
+
)
|
|
2344
|
+
observed_after = self.provider_traffic_witness.observed_provider_calls()
|
|
2345
|
+
if observed_after != observed_before:
|
|
2346
|
+
raise ValueError(
|
|
2347
|
+
"selector asserted provider_free but "
|
|
2348
|
+
f"{observed_after - observed_before} provider call(s) were "
|
|
2349
|
+
"observed during the selection window"
|
|
2350
|
+
)
|
|
2351
|
+
telemetry_sha256 = PROVIDER_FREE_SELECTION_TELEMETRY_SHA256
|
|
2352
|
+
else:
|
|
2353
|
+
if result.telemetry is None:
|
|
2354
|
+
raise ValueError("portfolio selection requires exact call telemetry")
|
|
2355
|
+
telemetry_sha256 = portfolio_selection_telemetry_sha256(result.telemetry)
|
|
2356
|
+
validate_ranked_portfolio_decision(wave.selection_request, result.decision)
|
|
2357
|
+
if (
|
|
2358
|
+
wave.selection_request.request_sha256 != request_sha256
|
|
2359
|
+
or wave.selection_request.finite_variation_contract.identity_sha256
|
|
2360
|
+
!= contract_sha256
|
|
2361
|
+
or wave.parent.occurrence.configuration_hash != parent_sha256
|
|
2362
|
+
or not typed_json_equal(
|
|
2363
|
+
wave.parent.configuration,
|
|
2364
|
+
wave.selection_request.finite_variation_contract.parent_configuration,
|
|
2365
|
+
)
|
|
2366
|
+
):
|
|
2367
|
+
raise ValueError(
|
|
2368
|
+
"portfolio parent or request contract drifted during selection"
|
|
2369
|
+
)
|
|
2370
|
+
materialized_pairs = tuple(
|
|
2371
|
+
self._materialize_member(
|
|
2372
|
+
wave,
|
|
2373
|
+
result.decision,
|
|
2374
|
+
telemetry_sha256,
|
|
2375
|
+
member,
|
|
2376
|
+
)
|
|
2377
|
+
for member in result.decision.members
|
|
2378
|
+
)
|
|
2379
|
+
return PreparedPortfolioVariationWave(
|
|
2380
|
+
wave=wave,
|
|
2381
|
+
selection=result,
|
|
2382
|
+
selection_telemetry_sha256=telemetry_sha256,
|
|
2383
|
+
invocations=tuple(value[0] for value in materialized_pairs),
|
|
2384
|
+
materializations=tuple(value[1] for value in materialized_pairs),
|
|
2385
|
+
)
|
|
2386
|
+
|
|
2387
|
+
async def evaluate_prepared_members(
|
|
2388
|
+
self,
|
|
2389
|
+
prepared: PreparedPortfolioVariationWave,
|
|
2390
|
+
candidate_ids: tuple[CandidateId, ...],
|
|
2391
|
+
*,
|
|
2392
|
+
reward_binding: RewardPolicyBinding | None = None,
|
|
2393
|
+
) -> tuple[EvaluatedPreparedPortfolioMember, ...]:
|
|
2394
|
+
"""Evaluate exactly the requested subset of one prepared portfolio."""
|
|
2395
|
+
|
|
2396
|
+
if type(prepared) is not PreparedPortfolioVariationWave:
|
|
2397
|
+
raise TypeError("prepared must be an exact prepared portfolio")
|
|
2398
|
+
PreparedPortfolioVariationWave.__post_init__(prepared)
|
|
2399
|
+
if (
|
|
2400
|
+
type(candidate_ids) is not tuple
|
|
2401
|
+
or not candidate_ids
|
|
2402
|
+
or any(type(value) is not CandidateId for value in candidate_ids)
|
|
2403
|
+
):
|
|
2404
|
+
raise TypeError("candidate_ids must be a non-empty exact tuple")
|
|
2405
|
+
for value in candidate_ids:
|
|
2406
|
+
CandidateId.__post_init__(value)
|
|
2407
|
+
if len(set(candidate_ids)) != len(candidate_ids):
|
|
2408
|
+
raise ValueError("candidate_ids must be unique")
|
|
2409
|
+
if reward_binding is not None:
|
|
2410
|
+
if type(reward_binding) is not RewardPolicyBinding:
|
|
2411
|
+
raise TypeError("reward_binding must be exact or None")
|
|
2412
|
+
RewardPolicyBinding.__post_init__(reward_binding)
|
|
2413
|
+
index = {
|
|
2414
|
+
invocation.candidate_id: (
|
|
2415
|
+
selected,
|
|
2416
|
+
invocation,
|
|
2417
|
+
materialization,
|
|
2418
|
+
)
|
|
2419
|
+
for selected, invocation, materialization in zip(
|
|
2420
|
+
prepared.selection.decision.members,
|
|
2421
|
+
prepared.invocations,
|
|
2422
|
+
prepared.materializations,
|
|
2423
|
+
strict=True,
|
|
2424
|
+
)
|
|
2425
|
+
}
|
|
2426
|
+
try:
|
|
2427
|
+
selected_rows = tuple(index[value] for value in candidate_ids)
|
|
2428
|
+
except KeyError as error:
|
|
2429
|
+
raise ValueError(
|
|
2430
|
+
"requested evaluation is outside the prepared portfolio"
|
|
2431
|
+
) from error
|
|
2432
|
+
outcomes = await self.engine.run_materialized_invocations(
|
|
2433
|
+
tuple(value[1] for value in selected_rows),
|
|
2434
|
+
reward_binding=reward_binding,
|
|
2435
|
+
)
|
|
2436
|
+
if type(outcomes) is not tuple or len(outcomes) != len(selected_rows):
|
|
2437
|
+
raise ValueError("engine returned a partial portfolio outcome wave")
|
|
2438
|
+
evaluated: list[EvaluatedPreparedPortfolioMember] = []
|
|
2439
|
+
for (selected, invocation, materialization), outcome in zip(
|
|
2440
|
+
selected_rows,
|
|
2441
|
+
outcomes,
|
|
2442
|
+
strict=True,
|
|
2443
|
+
):
|
|
2444
|
+
receipt = self._join_outcome(
|
|
2445
|
+
invocation,
|
|
2446
|
+
materialization,
|
|
2447
|
+
outcome,
|
|
2448
|
+
)
|
|
2449
|
+
evaluated.append(
|
|
2450
|
+
EvaluatedPreparedPortfolioMember(
|
|
2451
|
+
selected=selected,
|
|
2452
|
+
invocation=invocation,
|
|
2453
|
+
materialization=materialization,
|
|
2454
|
+
outcome=outcome,
|
|
2455
|
+
receipt=receipt,
|
|
2456
|
+
)
|
|
2457
|
+
)
|
|
2458
|
+
return tuple(evaluated)
|
|
2459
|
+
|
|
2460
|
+
@staticmethod
|
|
2461
|
+
def _join_outcome(
|
|
2462
|
+
invocation: MaterializedInvocation,
|
|
2463
|
+
materialization: PortfolioMemberMaterializationReceipt,
|
|
2464
|
+
outcome: InvocationOutcome,
|
|
2465
|
+
) -> PortfolioVariationMemberReceipt:
|
|
2466
|
+
if type(outcome) is not InvocationOutcome:
|
|
2467
|
+
raise TypeError("engine outcomes must be exact InvocationOutcome values")
|
|
2468
|
+
InvocationOutcome.__post_init__(outcome)
|
|
2469
|
+
prepared = outcome.prepared
|
|
2470
|
+
candidate = outcome.candidate
|
|
2471
|
+
if (
|
|
2472
|
+
prepared.plan != invocation.plan
|
|
2473
|
+
or prepared.proposal_authority is not ProposalAuthority.ENGINE
|
|
2474
|
+
or prepared.call_id is not None
|
|
2475
|
+
or prepared.candidate_id != materialization.candidate_id
|
|
2476
|
+
or prepared.materialization_policy_id != PORTFOLIO_MATERIALIZATION_POLICY_ID
|
|
2477
|
+
or prepared.materialization_policy_version
|
|
2478
|
+
!= PORTFOLIO_MATERIALIZATION_POLICY_VERSION
|
|
2479
|
+
or prepared.materialization_receipt_hash != materialization.receipt_sha256
|
|
2480
|
+
):
|
|
2481
|
+
raise ValueError("engine outcome differs from its materialized member")
|
|
2482
|
+
if (
|
|
2483
|
+
outcome.failure_stage is not None
|
|
2484
|
+
or candidate is None
|
|
2485
|
+
or not candidate.operator_compliant
|
|
2486
|
+
or not candidate.evidence_compliant
|
|
2487
|
+
):
|
|
2488
|
+
raise ValueError("portfolio wave contains a failed or partial member")
|
|
2489
|
+
if (
|
|
2490
|
+
candidate.candidate_id != materialization.candidate_id
|
|
2491
|
+
or candidate.occurrence.configuration_hash
|
|
2492
|
+
!= materialization.child_configuration_sha256
|
|
2493
|
+
or not typed_json_equal(
|
|
2494
|
+
candidate.configuration,
|
|
2495
|
+
freeze_json(invocation.draft.configuration),
|
|
2496
|
+
)
|
|
2497
|
+
or candidate.parent_ids != (materialization.parent_candidate_id,)
|
|
2498
|
+
or candidate.parent_patch_hashes != (materialization.patch_sha256,)
|
|
2499
|
+
or candidate.call_telemetry is not None
|
|
2500
|
+
or tuple((item.path, item.source) for item in candidate.source_attribution)
|
|
2501
|
+
!= tuple((path, "mutation") for path in materialization.changed_paths)
|
|
2502
|
+
):
|
|
2503
|
+
raise ValueError("portfolio candidate differs from exact materialization")
|
|
2504
|
+
detailed = candidate.detailed_evaluation
|
|
2505
|
+
disposition = PortfolioMemberDisposition.SCORED
|
|
2506
|
+
candidate_failure: PortfolioCandidateFailureEvidence | None = None
|
|
2507
|
+
if not candidate.valid:
|
|
2508
|
+
if detailed is None or detailed.failure is None:
|
|
2509
|
+
raise ValueError(
|
|
2510
|
+
"candidate infeasibility requires detailed evaluator evidence"
|
|
2511
|
+
)
|
|
2512
|
+
candidate_failure = PortfolioCandidateFailureEvidence.from_failure_record(
|
|
2513
|
+
detailed.failure,
|
|
2514
|
+
detailed_evaluation_sha256=detailed.evidence_sha256,
|
|
2515
|
+
)
|
|
2516
|
+
disposition = PortfolioMemberDisposition.CANDIDATE_INFEASIBLE
|
|
2517
|
+
if outcome.parent_relations:
|
|
2518
|
+
raise ValueError(
|
|
2519
|
+
"candidate infeasibility cannot publish parent relations"
|
|
2520
|
+
)
|
|
2521
|
+
if outcome.dominates_any_parent or outcome.better_than_any_parent:
|
|
2522
|
+
raise ValueError(
|
|
2523
|
+
"candidate infeasibility cannot publish improvement flags"
|
|
2524
|
+
)
|
|
2525
|
+
return PortfolioVariationMemberReceipt(
|
|
2526
|
+
materialization=materialization,
|
|
2527
|
+
operator_invocation_id=prepared.operator_invocation_id,
|
|
2528
|
+
reward_definition_sha256=(prepared.variation_case.reward_definition_hash),
|
|
2529
|
+
reward=outcome.reward,
|
|
2530
|
+
parent_relations=outcome.parent_relations,
|
|
2531
|
+
detailed_evaluation_sha256=(
|
|
2532
|
+
None if detailed is None else detailed.evidence_sha256
|
|
2533
|
+
),
|
|
2534
|
+
dominates_any_parent=outcome.dominates_any_parent,
|
|
2535
|
+
better_than_any_parent=outcome.better_than_any_parent,
|
|
2536
|
+
disposition=disposition,
|
|
2537
|
+
candidate_failure=candidate_failure,
|
|
2538
|
+
)
|
|
2539
|
+
|
|
2540
|
+
@staticmethod
|
|
2541
|
+
def _join_action_attribution(
|
|
2542
|
+
request: PortfolioSelectionRequest,
|
|
2543
|
+
decision: RankedPortfolioDecision,
|
|
2544
|
+
selected: RankedPortfolioMember,
|
|
2545
|
+
member: PortfolioVariationMemberReceipt,
|
|
2546
|
+
) -> PortfolioActionAttributionReceipt:
|
|
2547
|
+
"""Bind one selected member to its exact cards and evaluated outcome."""
|
|
2548
|
+
|
|
2549
|
+
if type(request) is not PortfolioSelectionRequest:
|
|
2550
|
+
raise TypeError("request must be an exact PortfolioSelectionRequest")
|
|
2551
|
+
PortfolioSelectionRequest.__post_init__(request)
|
|
2552
|
+
if type(decision) is not RankedPortfolioDecision:
|
|
2553
|
+
raise TypeError("decision must be an exact RankedPortfolioDecision")
|
|
2554
|
+
RankedPortfolioDecision.__post_init__(decision)
|
|
2555
|
+
if type(selected) is not RankedPortfolioMember:
|
|
2556
|
+
raise TypeError("selected must be an exact RankedPortfolioMember")
|
|
2557
|
+
RankedPortfolioMember.__post_init__(selected)
|
|
2558
|
+
if type(member) is not PortfolioVariationMemberReceipt:
|
|
2559
|
+
raise TypeError("member must be an exact portfolio member receipt")
|
|
2560
|
+
PortfolioVariationMemberReceipt.__post_init__(member)
|
|
2561
|
+
materialization = member.materialization
|
|
2562
|
+
if (
|
|
2563
|
+
decision.request_sha256 != request.request_sha256
|
|
2564
|
+
or decision.card_snapshot_sha256 != request.card_snapshot_sha256
|
|
2565
|
+
or selected.rank != materialization.rank
|
|
2566
|
+
or selected.option_id != materialization.option_id
|
|
2567
|
+
or selected.option_identity_sha256 != materialization.option_identity_sha256
|
|
2568
|
+
or selected.child_configuration_sha256
|
|
2569
|
+
!= materialization.child_configuration_sha256
|
|
2570
|
+
):
|
|
2571
|
+
raise ValueError(
|
|
2572
|
+
"selected member differs from its request or materialization"
|
|
2573
|
+
)
|
|
2574
|
+
card_by_key = {card.card_key: card for card in request.cards}
|
|
2575
|
+
supporting_cards = tuple(
|
|
2576
|
+
PortfolioActionCardAttribution(
|
|
2577
|
+
card_key=card_key,
|
|
2578
|
+
reference=card_by_key[card_key].reference,
|
|
2579
|
+
content_sha256=card_by_key[card_key].content_sha256,
|
|
2580
|
+
evidence_sha256=card_by_key[card_key].evidence_sha256,
|
|
2581
|
+
)
|
|
2582
|
+
for card_key in selected.supporting_card_keys
|
|
2583
|
+
)
|
|
2584
|
+
return PortfolioActionAttributionReceipt(
|
|
2585
|
+
request_sha256=request.request_sha256,
|
|
2586
|
+
decision_sha256=decision.decision_sha256,
|
|
2587
|
+
card_snapshot_sha256=request.card_snapshot_sha256,
|
|
2588
|
+
rank=selected.rank,
|
|
2589
|
+
option_id=selected.option_id,
|
|
2590
|
+
option_identity_sha256=selected.option_identity_sha256,
|
|
2591
|
+
child_configuration_sha256=selected.child_configuration_sha256,
|
|
2592
|
+
family=selected.family,
|
|
2593
|
+
supporting_cards=supporting_cards,
|
|
2594
|
+
effect_predictions=selected.effect_predictions,
|
|
2595
|
+
design_rationale_sha256=hashlib.sha256(
|
|
2596
|
+
selected.design_rationale.encode("utf-8", errors="strict")
|
|
2597
|
+
).hexdigest(),
|
|
2598
|
+
materialization_receipt_sha256=materialization.receipt_sha256,
|
|
2599
|
+
outcome_sha256=member.outcome_sha256,
|
|
2600
|
+
operator_invocation_id=member.operator_invocation_id,
|
|
2601
|
+
candidate_id=materialization.candidate_id,
|
|
2602
|
+
)
|
|
2603
|
+
|
|
2604
|
+
def prepare_pending_memory_credit_batch(
|
|
2605
|
+
self,
|
|
2606
|
+
results: tuple[PortfolioVariationWaveResult, ...],
|
|
2607
|
+
) -> PortfolioMemoryCreditBatchPreparation:
|
|
2608
|
+
"""Preview a complete concurrent stage without mutating memory.
|
|
2609
|
+
|
|
2610
|
+
Result order remains the caller's stable decision-slot order. Only the
|
|
2611
|
+
prospective memory mutation and batch receipt are canonicalized by
|
|
2612
|
+
preassigned credit-unit ID, so task completion order cannot affect the
|
|
2613
|
+
later commit. All estimator and ownership validation runs here.
|
|
2614
|
+
"""
|
|
2615
|
+
|
|
2616
|
+
if type(results) is not tuple:
|
|
2617
|
+
raise TypeError("results must be an exact tuple")
|
|
2618
|
+
if any(type(result) is not PortfolioVariationWaveResult for result in results):
|
|
2619
|
+
raise TypeError("results must contain exact portfolio wave results")
|
|
2620
|
+
for result in results:
|
|
2621
|
+
PortfolioVariationWaveResult.__post_init__(result)
|
|
2622
|
+
pending_by_result = tuple(
|
|
2623
|
+
result.pending_memory_credit for result in results
|
|
2624
|
+
)
|
|
2625
|
+
pending = tuple(value for value in pending_by_result if value is not None)
|
|
2626
|
+
generations = {result.receipt.generation for result in results}
|
|
2627
|
+
if len(generations) != 1:
|
|
2628
|
+
raise ValueError("one memory-credit batch cannot span generations")
|
|
2629
|
+
generation = next(iter(generations))
|
|
2630
|
+
source_receipts = tuple(result.receipt.receipt_sha256 for result in results)
|
|
2631
|
+
source_pending = tuple(
|
|
2632
|
+
None if value is None else value.pending_sha256
|
|
2633
|
+
for value in pending_by_result
|
|
2634
|
+
)
|
|
2635
|
+
if not pending:
|
|
2636
|
+
return PortfolioMemoryCreditBatchPreparation(
|
|
2637
|
+
generation=generation,
|
|
2638
|
+
source_result_receipt_sha256s=source_receipts,
|
|
2639
|
+
source_pending_credit_sha256s=source_pending,
|
|
2640
|
+
prepared_results=results,
|
|
2641
|
+
expected_trials=(),
|
|
2642
|
+
prior_memory_trials=(
|
|
2643
|
+
() if self.memory is None else self.memory.trials
|
|
2644
|
+
),
|
|
2645
|
+
batch_receipt=None,
|
|
2646
|
+
)
|
|
2647
|
+
if self.memory is None:
|
|
2648
|
+
raise ValueError("pending portfolio memory credit requires a memory bank")
|
|
2649
|
+
if any(result.receipt.memory_credit is not None for result in results):
|
|
2650
|
+
raise ValueError("cannot mix committed and pending stage memory credits")
|
|
2651
|
+
|
|
2652
|
+
canonical_pending = tuple(
|
|
2653
|
+
sorted(pending, key=lambda value: value.credit_unit_id.value)
|
|
2654
|
+
)
|
|
2655
|
+
canonical_ids = tuple(
|
|
2656
|
+
value.credit_unit_id.value for value in canonical_pending
|
|
2657
|
+
)
|
|
2658
|
+
if len(set(canonical_ids)) != len(canonical_ids):
|
|
2659
|
+
raise ValueError("pending stage repeats a memory credit unit")
|
|
2660
|
+
committed_receipts = tuple(
|
|
2661
|
+
value.to_committed_receipt() for value in canonical_pending
|
|
2662
|
+
)
|
|
2663
|
+
receipt_by_unit = {
|
|
2664
|
+
value.credit_unit_id: value for value in committed_receipts
|
|
2665
|
+
}
|
|
2666
|
+
committed_results = tuple(
|
|
2667
|
+
result
|
|
2668
|
+
if result.pending_memory_credit is None
|
|
2669
|
+
else replace(
|
|
2670
|
+
result,
|
|
2671
|
+
receipt=replace(
|
|
2672
|
+
result.receipt,
|
|
2673
|
+
memory_credit=receipt_by_unit[
|
|
2674
|
+
result.pending_memory_credit.credit_unit_id
|
|
2675
|
+
],
|
|
2676
|
+
),
|
|
2677
|
+
pending_memory_credit=None,
|
|
2678
|
+
)
|
|
2679
|
+
for result in results
|
|
2680
|
+
)
|
|
2681
|
+
prior_memory_trials = self.memory.trials
|
|
2682
|
+
trial_count_before = len(prior_memory_trials)
|
|
2683
|
+
batch_receipt = PortfolioMemoryCreditBatchReceipt(
|
|
2684
|
+
generation=generation,
|
|
2685
|
+
credits=committed_receipts,
|
|
2686
|
+
memory_trial_count_before=trial_count_before,
|
|
2687
|
+
memory_trial_count_after=trial_count_before + len(committed_receipts),
|
|
2688
|
+
)
|
|
2689
|
+
expected_trials = tuple(value.to_trial() for value in canonical_pending)
|
|
2690
|
+
# Exercise the complete bank estimator validation now. The bank method
|
|
2691
|
+
# is deliberately non-mutating; commit rechecks the retained exact prior
|
|
2692
|
+
# trial tuple before applying the same canonical batch.
|
|
2693
|
+
previewed_trials = self.memory.preview_trials_batch(expected_trials)
|
|
2694
|
+
if previewed_trials != expected_trials:
|
|
2695
|
+
raise RuntimeError("memory bank changed canonical preview order")
|
|
2696
|
+
return PortfolioMemoryCreditBatchPreparation(
|
|
2697
|
+
generation=generation,
|
|
2698
|
+
source_result_receipt_sha256s=source_receipts,
|
|
2699
|
+
source_pending_credit_sha256s=source_pending,
|
|
2700
|
+
prepared_results=committed_results,
|
|
2701
|
+
expected_trials=expected_trials,
|
|
2702
|
+
prior_memory_trials=prior_memory_trials,
|
|
2703
|
+
batch_receipt=batch_receipt,
|
|
2704
|
+
)
|
|
2705
|
+
|
|
2706
|
+
def commit_prepared_memory_credit_batch(
|
|
2707
|
+
self,
|
|
2708
|
+
preparation: PortfolioMemoryCreditBatchPreparation,
|
|
2709
|
+
) -> tuple[
|
|
2710
|
+
tuple[PortfolioVariationWaveResult, ...],
|
|
2711
|
+
PortfolioMemoryCreditBatchReceipt | None,
|
|
2712
|
+
]:
|
|
2713
|
+
"""Commit one exact prevalidated generation publication."""
|
|
2714
|
+
|
|
2715
|
+
if type(preparation) is not PortfolioMemoryCreditBatchPreparation:
|
|
2716
|
+
raise TypeError("preparation must be exact")
|
|
2717
|
+
PortfolioMemoryCreditBatchPreparation.__post_init__(preparation)
|
|
2718
|
+
batch_receipt = preparation.batch_receipt
|
|
2719
|
+
if batch_receipt is None:
|
|
2720
|
+
return preparation.prepared_results, None
|
|
2721
|
+
if self.memory is None:
|
|
2722
|
+
raise ValueError("prepared memory credit requires a memory bank")
|
|
2723
|
+
if self.memory.trials != preparation.prior_memory_trials:
|
|
2724
|
+
raise RuntimeError("memory bank changed after credit preparation")
|
|
2725
|
+
committed_trials = self.memory.record_trials_batch(
|
|
2726
|
+
preparation.expected_trials
|
|
2727
|
+
)
|
|
2728
|
+
if committed_trials != preparation.expected_trials:
|
|
2729
|
+
raise RuntimeError("memory bank changed canonical pending-credit order")
|
|
2730
|
+
if len(self.memory.trials) != batch_receipt.memory_trial_count_after:
|
|
2731
|
+
raise RuntimeError("memory bank trial count differs from batch receipt")
|
|
2732
|
+
return preparation.prepared_results, batch_receipt
|
|
2733
|
+
|
|
2734
|
+
def commit_pending_memory_credit_batch(
|
|
2735
|
+
self,
|
|
2736
|
+
results: tuple[PortfolioVariationWaveResult, ...],
|
|
2737
|
+
) -> tuple[
|
|
2738
|
+
tuple[PortfolioVariationWaveResult, ...],
|
|
2739
|
+
PortfolioMemoryCreditBatchReceipt | None,
|
|
2740
|
+
]:
|
|
2741
|
+
"""Backward-compatible prepare-and-commit convenience boundary."""
|
|
2742
|
+
|
|
2743
|
+
preparation = self.prepare_pending_memory_credit_batch(results)
|
|
2744
|
+
committed_results, batch_receipt = (
|
|
2745
|
+
self.commit_prepared_memory_credit_batch(preparation)
|
|
2746
|
+
)
|
|
2747
|
+
return committed_results, batch_receipt
|
|
2748
|
+
|
|
2749
|
+
async def run(
|
|
2750
|
+
self,
|
|
2751
|
+
wave: PortfolioVariationWaveRequest,
|
|
2752
|
+
*,
|
|
2753
|
+
reward_binding: RewardPolicyBinding | None = None,
|
|
2754
|
+
defer_memory_credit: bool = False,
|
|
2755
|
+
) -> PortfolioVariationWaveResult:
|
|
2756
|
+
"""Select once, materialize all members, and evaluate one concurrent wave."""
|
|
2757
|
+
|
|
2758
|
+
if type(wave) is not PortfolioVariationWaveRequest:
|
|
2759
|
+
raise TypeError("wave must be an exact PortfolioVariationWaveRequest")
|
|
2760
|
+
PortfolioVariationWaveRequest.__post_init__(wave)
|
|
2761
|
+
if reward_binding is not None:
|
|
2762
|
+
if type(reward_binding) is not RewardPolicyBinding:
|
|
2763
|
+
raise TypeError("reward_binding must be exact or None")
|
|
2764
|
+
RewardPolicyBinding.__post_init__(reward_binding)
|
|
2765
|
+
if type(defer_memory_credit) is not bool:
|
|
2766
|
+
raise TypeError("defer_memory_credit must be an exact boolean")
|
|
2767
|
+
credit = wave.memory_credit
|
|
2768
|
+
if credit is not None:
|
|
2769
|
+
if self.memory is None:
|
|
2770
|
+
raise ValueError("portfolio memory credit requires a memory bank")
|
|
2771
|
+
eligible_entries = self.memory.entries_for(credit.decision.eligible)
|
|
2772
|
+
entry_by_reference = {
|
|
2773
|
+
entry.reference: entry for entry in eligible_entries
|
|
2774
|
+
}
|
|
2775
|
+
for card in wave.selection_request.cards:
|
|
2776
|
+
entry = entry_by_reference[card.reference]
|
|
2777
|
+
if card.content_sha256 != entry.draft.content_sha256:
|
|
2778
|
+
raise ValueError(
|
|
2779
|
+
"memory credit card content differs from its memory entry"
|
|
2780
|
+
)
|
|
2781
|
+
if (
|
|
2782
|
+
entry.origin is InsightOrigin.REFLECTION
|
|
2783
|
+
and (
|
|
2784
|
+
card.source_binding is None
|
|
2785
|
+
or wave.selection_request.source_registry is None
|
|
2786
|
+
)
|
|
2787
|
+
):
|
|
2788
|
+
raise ValueError(
|
|
2789
|
+
"reflection memory credit requires source-admitted cards"
|
|
2790
|
+
)
|
|
2791
|
+
quarantine_entries = tuple(
|
|
2792
|
+
entry
|
|
2793
|
+
for entry in eligible_entries
|
|
2794
|
+
if entry.lifecycle_state is InsightLifecycleState.QUARANTINED
|
|
2795
|
+
)
|
|
2796
|
+
if quarantine_entries:
|
|
2797
|
+
admission = credit.quarantine_admission
|
|
2798
|
+
if admission is None:
|
|
2799
|
+
raise ValueError(
|
|
2800
|
+
"quarantine memory credit requires bank-issued admission"
|
|
2801
|
+
)
|
|
2802
|
+
self.memory.validate_quarantine_test_admission(
|
|
2803
|
+
admission,
|
|
2804
|
+
eligible_references=credit.decision.eligible,
|
|
2805
|
+
subset_authorization_sha256=(
|
|
2806
|
+
credit.quarantine_admission_subset_authorization_sha256
|
|
2807
|
+
),
|
|
2808
|
+
)
|
|
2809
|
+
elif credit.quarantine_admission is not None:
|
|
2810
|
+
raise ValueError(
|
|
2811
|
+
"normal memory credit cannot carry quarantine admission"
|
|
2812
|
+
)
|
|
2813
|
+
if any(
|
|
2814
|
+
entry.lifecycle_state is InsightLifecycleState.DEPRECATED
|
|
2815
|
+
for entry in eligible_entries
|
|
2816
|
+
):
|
|
2817
|
+
raise ValueError("memory credit eligible set contains deprecated insight")
|
|
2818
|
+
|
|
2819
|
+
request_sha256 = wave.selection_request.request_sha256
|
|
2820
|
+
parent_sha256 = wave.parent.occurrence.configuration_hash
|
|
2821
|
+
prepared = await self.prepare(wave)
|
|
2822
|
+
result = prepared.selection
|
|
2823
|
+
telemetry = result.telemetry
|
|
2824
|
+
telemetry_sha256 = prepared.selection_telemetry_sha256
|
|
2825
|
+
candidate_ids = tuple(
|
|
2826
|
+
value.candidate_id for value in prepared.invocations
|
|
2827
|
+
)
|
|
2828
|
+
evaluated = await self.evaluate_prepared_members(
|
|
2829
|
+
prepared,
|
|
2830
|
+
candidate_ids,
|
|
2831
|
+
reward_binding=reward_binding,
|
|
2832
|
+
)
|
|
2833
|
+
outcomes = tuple(value.outcome for value in evaluated)
|
|
2834
|
+
members = tuple(value.receipt for value in evaluated)
|
|
2835
|
+
if len({member.operator_invocation_id for member in members}) != len(members):
|
|
2836
|
+
raise ValueError("portfolio outcomes contain colliding invocations")
|
|
2837
|
+
action_attributions = tuple(
|
|
2838
|
+
self._join_action_attribution(
|
|
2839
|
+
wave.selection_request,
|
|
2840
|
+
result.decision,
|
|
2841
|
+
selected,
|
|
2842
|
+
member,
|
|
2843
|
+
)
|
|
2844
|
+
for selected, member in zip(
|
|
2845
|
+
result.decision.members,
|
|
2846
|
+
members,
|
|
2847
|
+
strict=True,
|
|
2848
|
+
)
|
|
2849
|
+
)
|
|
2850
|
+
|
|
2851
|
+
credit_receipt: PortfolioMemoryCreditReceipt | None = None
|
|
2852
|
+
pending_credit: PortfolioPendingMemoryCredit | None = None
|
|
2853
|
+
if credit is not None:
|
|
2854
|
+
assert self.memory is not None
|
|
2855
|
+
context_projection = credit.resolve_context_projection(
|
|
2856
|
+
wave.selection_request.context
|
|
2857
|
+
)
|
|
2858
|
+
if credit.credit_unit_id in {
|
|
2859
|
+
member.operator_invocation_id for member in members
|
|
2860
|
+
}:
|
|
2861
|
+
raise ValueError("memory credit unit collides with a child invocation")
|
|
2862
|
+
aggregate_reward = credit.aggregation.aggregate(outcomes)
|
|
2863
|
+
if type(aggregate_reward) is not float or not math.isfinite(
|
|
2864
|
+
aggregate_reward
|
|
2865
|
+
):
|
|
2866
|
+
raise TypeError("portfolio aggregate reward must be a finite float")
|
|
2867
|
+
pending_credit = PortfolioPendingMemoryCredit(
|
|
2868
|
+
credit_unit_id=credit.credit_unit_id,
|
|
2869
|
+
decision=credit.decision,
|
|
2870
|
+
candidate_ids=candidate_ids,
|
|
2871
|
+
aggregation=credit.aggregation,
|
|
2872
|
+
context_projection=context_projection,
|
|
2873
|
+
reward=aggregate_reward,
|
|
2874
|
+
treatment_binding_sha256=credit.treatment_binding_sha256,
|
|
2875
|
+
generation=wave.generation,
|
|
2876
|
+
)
|
|
2877
|
+
if not defer_memory_credit:
|
|
2878
|
+
before = len(self.memory.trials)
|
|
2879
|
+
trial = self.memory.record_trials_batch(
|
|
2880
|
+
(pending_credit.to_trial(),)
|
|
2881
|
+
)[0]
|
|
2882
|
+
if (
|
|
2883
|
+
len(self.memory.trials) != before + 1
|
|
2884
|
+
or self.memory.trials[-1] is not trial
|
|
2885
|
+
or trial.candidate_ids != candidate_ids
|
|
2886
|
+
or trial.reward != aggregate_reward
|
|
2887
|
+
):
|
|
2888
|
+
raise RuntimeError(
|
|
2889
|
+
"portfolio memory credit was not one exact trial"
|
|
2890
|
+
)
|
|
2891
|
+
credit_receipt = pending_credit.to_committed_receipt()
|
|
2892
|
+
pending_credit = None
|
|
2893
|
+
|
|
2894
|
+
receipt = PortfolioVariationWaveReceipt(
|
|
2895
|
+
selection_call_id=wave.selection_request.call_id,
|
|
2896
|
+
request_sha256=request_sha256,
|
|
2897
|
+
decision_sha256=result.decision.decision_sha256,
|
|
2898
|
+
selection_policy_id=result.decision.policy_id,
|
|
2899
|
+
selection_policy_version=result.decision.policy_version,
|
|
2900
|
+
selection_policy_definition_sha256=(
|
|
2901
|
+
result.decision.policy_definition_sha256
|
|
2902
|
+
),
|
|
2903
|
+
selection_telemetry=telemetry,
|
|
2904
|
+
selection_telemetry_sha256=telemetry_sha256,
|
|
2905
|
+
parent_candidate_id=wave.parent.candidate_id,
|
|
2906
|
+
parent_configuration_sha256=parent_sha256,
|
|
2907
|
+
generation=wave.generation,
|
|
2908
|
+
members=members,
|
|
2909
|
+
action_attributions=action_attributions,
|
|
2910
|
+
memory_credit=credit_receipt,
|
|
2911
|
+
)
|
|
2912
|
+
PortfolioVariationWaveReceipt.__post_init__(receipt)
|
|
2913
|
+
return PortfolioVariationWaveResult(
|
|
2914
|
+
receipt=receipt,
|
|
2915
|
+
outcomes=outcomes,
|
|
2916
|
+
selection_decision=result.decision,
|
|
2917
|
+
supplemental_selection_audit=result.supplemental_audit,
|
|
2918
|
+
pending_memory_credit=pending_credit,
|
|
2919
|
+
)
|
|
2920
|
+
|
|
2921
|
+
|
|
2922
|
+
__all__ = [
|
|
2923
|
+
"EXACT_MEMORY_CONTEXT_PROJECTION_DEFINITION_SHA256",
|
|
2924
|
+
"MEMORY_ESTIMAND_CONTEXT_KEY",
|
|
2925
|
+
"MEMORY_ESTIMAND_SUBTREE_PROJECTION_DEFINITION_SHA256",
|
|
2926
|
+
"PORTFOLIO_MATERIALIZATION_POLICY_ID",
|
|
2927
|
+
"PORTFOLIO_MATERIALIZATION_POLICY_VERSION",
|
|
2928
|
+
"EvaluatedPreparedPortfolioMember",
|
|
2929
|
+
"MaterializedPortfolioEngine",
|
|
2930
|
+
"PreparedPortfolioVariationWave",
|
|
2931
|
+
"PortfolioActionAttributionReceipt",
|
|
2932
|
+
"PortfolioActionCardAttribution",
|
|
2933
|
+
"PortfolioCandidateFailureEvidence",
|
|
2934
|
+
"PortfolioEvolution",
|
|
2935
|
+
"PortfolioMemberMaterializationReceipt",
|
|
2936
|
+
"PortfolioMemberDisposition",
|
|
2937
|
+
"PortfolioMemoryCreditBatchReceipt",
|
|
2938
|
+
"PortfolioMemoryCreditBatchPreparation",
|
|
2939
|
+
"PortfolioMemoryCreditPlan",
|
|
2940
|
+
"PortfolioMemoryCreditReceipt",
|
|
2941
|
+
"PortfolioMemoryContextProjectionBinding",
|
|
2942
|
+
"PortfolioMemoryMatchedControlWavePlan",
|
|
2943
|
+
"PortfolioPendingMemoryCredit",
|
|
2944
|
+
"PortfolioRewardAggregationBinding",
|
|
2945
|
+
"PortfolioVariationMemberReceipt",
|
|
2946
|
+
"PortfolioVariationWaveReceipt",
|
|
2947
|
+
"PortfolioVariationWaveResult",
|
|
2948
|
+
"PortfolioVariationWaveRequest",
|
|
2949
|
+
"portfolio_selection_telemetry_sha256",
|
|
2950
|
+
]
|