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,2537 @@
|
|
|
1
|
+
"""Live, workload-neutral execution ledger for prepared evolution campaigns.
|
|
2
|
+
|
|
3
|
+
Preparation and execution intentionally remain separate. This module consumes
|
|
4
|
+
an authenticated :class:`PreparedEvolutionCampaign`, but delegates benchmark
|
|
5
|
+
state, portfolio/recombination work, reflection, and lifecycle effects to
|
|
6
|
+
small async ports. The application layer owns only chronology, joins, budget
|
|
7
|
+
accounting, archive-utility cutoffs, reflection visibility, durable events,
|
|
8
|
+
and cleanup/finalization.
|
|
9
|
+
|
|
10
|
+
No provider call, model setting, objective, or workload action appears here.
|
|
11
|
+
Existing ``PortfolioEvolution`` and ``PortfolioRecombination`` services can be
|
|
12
|
+
bridged by one stateful ``CampaignStageRuntimePort`` implementation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import base64
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import zlib
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from typing import Protocol, runtime_checkable
|
|
26
|
+
|
|
27
|
+
from agent_evolve.application.evolution_campaign import (
|
|
28
|
+
ArchiveUtilitySnapshot,
|
|
29
|
+
CampaignGenerationKind,
|
|
30
|
+
CampaignGenerationStep,
|
|
31
|
+
CampaignPolicies,
|
|
32
|
+
CampaignPromotionBarrier,
|
|
33
|
+
CampaignReflectionWave,
|
|
34
|
+
CampaignReflectionSupervisionPolicy,
|
|
35
|
+
PreparedEvolutionCampaign,
|
|
36
|
+
ReflectionFailureMode,
|
|
37
|
+
ReflectionVisibility,
|
|
38
|
+
freeze_archive_utility,
|
|
39
|
+
)
|
|
40
|
+
from agent_evolve.domain.patch import require_sha256
|
|
41
|
+
from agent_evolve.domain.typed_json import (
|
|
42
|
+
FrozenJsonObject,
|
|
43
|
+
freeze_json,
|
|
44
|
+
thaw_json,
|
|
45
|
+
typed_json_sha256,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_TOKEN = re.compile(r"^[a-z][a-z0-9_.:-]{0,127}$")
|
|
50
|
+
_STEP_DOMAIN = b"agent-evolve:campaign-execution-step:v1\x00"
|
|
51
|
+
_AUDIT_DOMAIN = b"agent-evolve:campaign-selector-audit:v1\x00"
|
|
52
|
+
_AUDIT_SET_DOMAIN = b"agent-evolve:campaign-selector-audit-set:v1\x00"
|
|
53
|
+
_START_DOMAIN = b"agent-evolve:campaign-execution-start:v1\x00"
|
|
54
|
+
_ARCHIVE_REQUEST_DOMAIN = b"agent-evolve:campaign-archive-cutoff-request:v1\x00"
|
|
55
|
+
_ARCHIVE_RECEIPT_DOMAIN = b"agent-evolve:campaign-archive-cutoff-receipt:v1\x00"
|
|
56
|
+
_STAGE_REQUEST_DOMAIN = b"agent-evolve:campaign-stage-request:v1\x00"
|
|
57
|
+
_STAGE_RECEIPT_DOMAIN = b"agent-evolve:campaign-stage-receipt:v1\x00"
|
|
58
|
+
_REFLECTION_REQUEST_DOMAIN = b"agent-evolve:campaign-reflection-request:v1\x00"
|
|
59
|
+
_REFLECTION_RECEIPT_DOMAIN = b"agent-evolve:campaign-reflection-receipt:v1\x00"
|
|
60
|
+
_REFLECTION_CANCELLATION_RECEIPT_DOMAIN = (
|
|
61
|
+
b"agent-evolve:campaign-reflection-cancellation-receipt:v1\x00"
|
|
62
|
+
)
|
|
63
|
+
_TEST_ADMISSION_REQUEST_DOMAIN = (
|
|
64
|
+
b"agent-evolve:campaign-reflection-test-admission-request:v1\x00"
|
|
65
|
+
)
|
|
66
|
+
_TEST_ADMISSION_RECEIPT_DOMAIN = (
|
|
67
|
+
b"agent-evolve:campaign-reflection-test-admission-receipt:v1\x00"
|
|
68
|
+
)
|
|
69
|
+
_TAIL_DRAIN_DOMAIN = b"agent-evolve:campaign-tail-drain:v1\x00"
|
|
70
|
+
_COUNTERS_DOMAIN = b"agent-evolve:campaign-execution-counters:v1\x00"
|
|
71
|
+
_FINALIZATION_REQUEST_DOMAIN = (
|
|
72
|
+
b"agent-evolve:campaign-execution-finalization-request:v1\x00"
|
|
73
|
+
)
|
|
74
|
+
_FINALIZATION_RECEIPT_DOMAIN = (
|
|
75
|
+
b"agent-evolve:campaign-execution-finalization-receipt:v1\x00"
|
|
76
|
+
)
|
|
77
|
+
_CLEANUP_REQUEST_DOMAIN = b"agent-evolve:campaign-runtime-cleanup-request:v1\x00"
|
|
78
|
+
_CLEANUP_RECEIPT_DOMAIN = b"agent-evolve:campaign-runtime-cleanup-receipt:v1\x00"
|
|
79
|
+
_EVENT_DOMAIN = b"agent-evolve:campaign-execution-event:v1\x00"
|
|
80
|
+
_RESULT_DOMAIN = b"agent-evolve:campaign-execution-result:v1\x00"
|
|
81
|
+
_AUDIT_TEXT_INLINE_UTF8_BYTES = 1_000_000
|
|
82
|
+
_AUDIT_TEXT_BASE64_CHUNK_CHARACTERS = 500_000
|
|
83
|
+
_AUDIT_TEXT_MAX_DECOMPRESSED_UTF8_BYTES = 64 * 1024 * 1024
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CampaignExecutionContractError(ValueError):
|
|
87
|
+
"""A runtime receipt violated the prepared campaign contract."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _canonical_json(value: object) -> bytes:
|
|
91
|
+
return json.dumps(
|
|
92
|
+
value,
|
|
93
|
+
ensure_ascii=True,
|
|
94
|
+
allow_nan=False,
|
|
95
|
+
separators=(",", ":"),
|
|
96
|
+
sort_keys=True,
|
|
97
|
+
).encode("ascii", errors="strict")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _hash(domain: bytes, record: object) -> str:
|
|
101
|
+
return hashlib.sha256(domain + _canonical_json(record)).hexdigest()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def encode_selector_audit_text(name: str, value: str) -> dict[str, object]:
|
|
105
|
+
"""Represent exact audit text without violating typed-JSON scalar limits.
|
|
106
|
+
|
|
107
|
+
Ordinary traces retain the original single-string representation byte for
|
|
108
|
+
byte. Large structured decisions are losslessly compressed and split into
|
|
109
|
+
bounded ASCII chunks; the receipt authenticates both compressed and
|
|
110
|
+
decompressed bytes. This is representation-only and has no optimizer or
|
|
111
|
+
model authority.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
if name not in {"request_text", "response_text"}:
|
|
115
|
+
raise ValueError("selector audit text name is unsupported")
|
|
116
|
+
if type(value) is not str or not value:
|
|
117
|
+
raise ValueError("selector audit text must be a non-empty exact string")
|
|
118
|
+
raw = value.encode("utf-8", errors="strict")
|
|
119
|
+
if len(raw) <= _AUDIT_TEXT_INLINE_UTF8_BYTES:
|
|
120
|
+
return {name: value}
|
|
121
|
+
if len(raw) > _AUDIT_TEXT_MAX_DECOMPRESSED_UTF8_BYTES:
|
|
122
|
+
raise ValueError("selector audit text exceeds the lossless audit ceiling")
|
|
123
|
+
compressed = zlib.compress(raw, level=9)
|
|
124
|
+
encoded = base64.b64encode(compressed).decode("ascii")
|
|
125
|
+
chunks = [
|
|
126
|
+
encoded[offset : offset + _AUDIT_TEXT_BASE64_CHUNK_CHARACTERS]
|
|
127
|
+
for offset in range(0, len(encoded), _AUDIT_TEXT_BASE64_CHUNK_CHARACTERS)
|
|
128
|
+
]
|
|
129
|
+
return {
|
|
130
|
+
f"{name}_encoding": "zlib_base64_chunks_v1",
|
|
131
|
+
f"{name}_base64_chunks": chunks,
|
|
132
|
+
f"{name}_utf8_bytes": len(raw),
|
|
133
|
+
f"{name}_sha256": hashlib.sha256(raw).hexdigest(),
|
|
134
|
+
f"{name}_compressed_bytes": len(compressed),
|
|
135
|
+
f"{name}_compressed_sha256": hashlib.sha256(compressed).hexdigest(),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def decode_selector_audit_text(audit: dict[str, object], name: str) -> str:
|
|
140
|
+
"""Validate and recover one inline or losslessly chunked audit field."""
|
|
141
|
+
|
|
142
|
+
if type(audit) is not dict:
|
|
143
|
+
raise TypeError("selector audit must be an exact object")
|
|
144
|
+
if name not in {"request_text", "response_text"}:
|
|
145
|
+
raise ValueError("selector audit text name is unsupported")
|
|
146
|
+
inline = audit.get(name)
|
|
147
|
+
if inline is not None:
|
|
148
|
+
if type(inline) is not str or not inline:
|
|
149
|
+
raise ValueError(f"plaintext audit requires non-empty {name}")
|
|
150
|
+
encoded_keys = {
|
|
151
|
+
f"{name}_encoding",
|
|
152
|
+
f"{name}_base64_chunks",
|
|
153
|
+
f"{name}_utf8_bytes",
|
|
154
|
+
f"{name}_sha256",
|
|
155
|
+
f"{name}_compressed_bytes",
|
|
156
|
+
f"{name}_compressed_sha256",
|
|
157
|
+
}
|
|
158
|
+
if any(key in audit for key in encoded_keys):
|
|
159
|
+
raise ValueError("inline selector audit text cannot also be encoded")
|
|
160
|
+
return inline
|
|
161
|
+
if audit.get(f"{name}_encoding") != "zlib_base64_chunks_v1":
|
|
162
|
+
raise ValueError(f"plaintext audit requires non-empty {name}")
|
|
163
|
+
chunks = audit.get(f"{name}_base64_chunks")
|
|
164
|
+
if (
|
|
165
|
+
type(chunks) is not list
|
|
166
|
+
or not chunks
|
|
167
|
+
or any(type(value) is not str or not value for value in chunks)
|
|
168
|
+
):
|
|
169
|
+
raise ValueError("selector audit chunks must be non-empty exact strings")
|
|
170
|
+
expected_utf8_bytes = audit.get(f"{name}_utf8_bytes")
|
|
171
|
+
expected_compressed_bytes = audit.get(f"{name}_compressed_bytes")
|
|
172
|
+
if (
|
|
173
|
+
type(expected_utf8_bytes) is not int
|
|
174
|
+
or not _AUDIT_TEXT_INLINE_UTF8_BYTES
|
|
175
|
+
< expected_utf8_bytes
|
|
176
|
+
<= _AUDIT_TEXT_MAX_DECOMPRESSED_UTF8_BYTES
|
|
177
|
+
or type(expected_compressed_bytes) is not int
|
|
178
|
+
or expected_compressed_bytes <= 0
|
|
179
|
+
):
|
|
180
|
+
raise ValueError("selector audit encoded byte counts are invalid")
|
|
181
|
+
try:
|
|
182
|
+
compressed = base64.b64decode("".join(chunks), validate=True)
|
|
183
|
+
except (ValueError, UnicodeEncodeError) as error:
|
|
184
|
+
raise ValueError("selector audit chunks are not canonical base64") from error
|
|
185
|
+
if len(compressed) != expected_compressed_bytes:
|
|
186
|
+
raise ValueError("selector audit compressed byte count disagrees")
|
|
187
|
+
if hashlib.sha256(compressed).hexdigest() != audit.get(
|
|
188
|
+
f"{name}_compressed_sha256"
|
|
189
|
+
):
|
|
190
|
+
raise ValueError("selector audit compressed digest disagrees")
|
|
191
|
+
decompressor = zlib.decompressobj()
|
|
192
|
+
try:
|
|
193
|
+
raw = decompressor.decompress(compressed, expected_utf8_bytes + 1)
|
|
194
|
+
raw += decompressor.flush()
|
|
195
|
+
except zlib.error as error:
|
|
196
|
+
raise ValueError("selector audit compressed text is invalid") from error
|
|
197
|
+
if (
|
|
198
|
+
not decompressor.eof
|
|
199
|
+
or decompressor.unused_data
|
|
200
|
+
or decompressor.unconsumed_tail
|
|
201
|
+
or len(raw) != expected_utf8_bytes
|
|
202
|
+
):
|
|
203
|
+
raise ValueError("selector audit decompressed byte count disagrees")
|
|
204
|
+
if hashlib.sha256(raw).hexdigest() != audit.get(f"{name}_sha256"):
|
|
205
|
+
raise ValueError("selector audit decompressed digest disagrees")
|
|
206
|
+
try:
|
|
207
|
+
value = raw.decode("utf-8", errors="strict")
|
|
208
|
+
except UnicodeDecodeError as error:
|
|
209
|
+
raise ValueError("selector audit text is not valid UTF-8") from error
|
|
210
|
+
if not value:
|
|
211
|
+
raise ValueError(f"plaintext audit requires non-empty {name}")
|
|
212
|
+
return value
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _token(value: str, *, name: str) -> None:
|
|
216
|
+
if type(value) is not str or _TOKEN.fullmatch(value) is None:
|
|
217
|
+
raise ValueError(f"{name} must use the closed execution-token grammar")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _frozen(value: object, *, name: str) -> FrozenJsonObject:
|
|
221
|
+
if type(value) is not FrozenJsonObject:
|
|
222
|
+
raise TypeError(f"{name} must be an exact FrozenJsonObject")
|
|
223
|
+
if freeze_json(value) is not value:
|
|
224
|
+
raise TypeError(f"{name} must already be frozen typed JSON")
|
|
225
|
+
return value
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _freeze_record(value: dict[str, object]) -> FrozenJsonObject:
|
|
229
|
+
frozen = freeze_json(value)
|
|
230
|
+
if type(frozen) is not FrozenJsonObject: # pragma: no cover - closed input.
|
|
231
|
+
raise AssertionError("record did not freeze to an object")
|
|
232
|
+
return frozen
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def campaign_step_sha256(step: CampaignGenerationStep) -> str:
|
|
236
|
+
if type(step) is not CampaignGenerationStep:
|
|
237
|
+
raise TypeError("step must be an exact CampaignGenerationStep")
|
|
238
|
+
CampaignGenerationStep.__post_init__(step)
|
|
239
|
+
return _hash(_STEP_DOMAIN, step.to_record())
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class SelectorAuditExecutionMode(str, Enum):
|
|
243
|
+
FRESH = "fresh"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass(frozen=True, slots=True)
|
|
247
|
+
class CampaignSelectorAuditReceipt:
|
|
248
|
+
"""Fresh selector-call trace required for each portfolio parent slot."""
|
|
249
|
+
|
|
250
|
+
generation: int
|
|
251
|
+
parent_slot: int
|
|
252
|
+
selector_call_id: str
|
|
253
|
+
request_sha256: str
|
|
254
|
+
decision_sha256: str
|
|
255
|
+
trace_receipt_sha256: str
|
|
256
|
+
plaintext_audit: FrozenJsonObject
|
|
257
|
+
prior_audit_set_sha256: str
|
|
258
|
+
execution_mode: SelectorAuditExecutionMode
|
|
259
|
+
|
|
260
|
+
def __post_init__(self) -> None:
|
|
261
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
262
|
+
raise ValueError("selector audit generation must be positive")
|
|
263
|
+
if type(self.parent_slot) is not int or self.parent_slot < 0:
|
|
264
|
+
raise ValueError("selector audit parent_slot must be non-negative")
|
|
265
|
+
_token(self.selector_call_id, name="selector_call_id")
|
|
266
|
+
for name in (
|
|
267
|
+
"request_sha256",
|
|
268
|
+
"decision_sha256",
|
|
269
|
+
"trace_receipt_sha256",
|
|
270
|
+
"prior_audit_set_sha256",
|
|
271
|
+
):
|
|
272
|
+
require_sha256(getattr(self, name), name)
|
|
273
|
+
_frozen(self.plaintext_audit, name="plaintext_audit")
|
|
274
|
+
if typed_json_sha256(self.plaintext_audit) != self.trace_receipt_sha256:
|
|
275
|
+
raise ValueError(
|
|
276
|
+
"trace_receipt_sha256 does not authenticate plaintext audit"
|
|
277
|
+
)
|
|
278
|
+
audit = thaw_json(self.plaintext_audit)
|
|
279
|
+
if type(audit) is not dict: # pragma: no cover - exact object above.
|
|
280
|
+
raise AssertionError("plaintext audit thawed to a non-object")
|
|
281
|
+
required = {
|
|
282
|
+
"selector_call_id": self.selector_call_id,
|
|
283
|
+
"request_sha256": self.request_sha256,
|
|
284
|
+
"decision_sha256": self.decision_sha256,
|
|
285
|
+
}
|
|
286
|
+
if any(audit.get(name) != value for name, value in required.items()):
|
|
287
|
+
raise ValueError(
|
|
288
|
+
"plaintext audit request/decision/call join is inconsistent"
|
|
289
|
+
)
|
|
290
|
+
for name in ("request_text", "response_text"):
|
|
291
|
+
decode_selector_audit_text(audit, name)
|
|
292
|
+
if self.execution_mode is not SelectorAuditExecutionMode.FRESH:
|
|
293
|
+
raise ValueError("portfolio selector audit must record fresh execution")
|
|
294
|
+
|
|
295
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
296
|
+
self.__post_init__()
|
|
297
|
+
return {
|
|
298
|
+
"generation": self.generation,
|
|
299
|
+
"parent_slot": self.parent_slot,
|
|
300
|
+
"selector_call_id": self.selector_call_id,
|
|
301
|
+
"request_sha256": self.request_sha256,
|
|
302
|
+
"decision_sha256": self.decision_sha256,
|
|
303
|
+
"trace_receipt_sha256": self.trace_receipt_sha256,
|
|
304
|
+
"plaintext_audit": thaw_json(self.plaintext_audit),
|
|
305
|
+
"prior_audit_set_sha256": self.prior_audit_set_sha256,
|
|
306
|
+
"execution_mode": self.execution_mode.value,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def audit_sha256(self) -> str:
|
|
311
|
+
return _hash(_AUDIT_DOMAIN, self._unsigned_record())
|
|
312
|
+
|
|
313
|
+
def to_record(self) -> dict[str, object]:
|
|
314
|
+
return {**self._unsigned_record(), "audit_sha256": self.audit_sha256}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def selector_audit_set_sha256(
|
|
318
|
+
audits: tuple[CampaignSelectorAuditReceipt, ...],
|
|
319
|
+
) -> str:
|
|
320
|
+
if type(audits) is not tuple or any(
|
|
321
|
+
type(value) is not CampaignSelectorAuditReceipt for value in audits
|
|
322
|
+
):
|
|
323
|
+
raise TypeError("audits must contain exact selector audit receipts")
|
|
324
|
+
for value in audits:
|
|
325
|
+
CampaignSelectorAuditReceipt.__post_init__(value)
|
|
326
|
+
return _hash(
|
|
327
|
+
_AUDIT_SET_DOMAIN,
|
|
328
|
+
[value.audit_sha256 for value in audits],
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@dataclass(frozen=True, slots=True)
|
|
333
|
+
class CampaignSeedExecutionReceipt:
|
|
334
|
+
"""Exact seed admission/evaluation accounting produced during start."""
|
|
335
|
+
|
|
336
|
+
seed_id: str
|
|
337
|
+
configuration_sha256: str
|
|
338
|
+
evaluated: bool
|
|
339
|
+
unique_evaluation: bool
|
|
340
|
+
valid: bool
|
|
341
|
+
failure_type: str | None
|
|
342
|
+
evidence: FrozenJsonObject
|
|
343
|
+
|
|
344
|
+
def __post_init__(self) -> None:
|
|
345
|
+
_token(self.seed_id, name="seed_id")
|
|
346
|
+
require_sha256(self.configuration_sha256, "configuration_sha256")
|
|
347
|
+
for name in ("evaluated", "unique_evaluation", "valid"):
|
|
348
|
+
if type(getattr(self, name)) is not bool:
|
|
349
|
+
raise TypeError(f"{name} must be an exact bool")
|
|
350
|
+
if not self.evaluated and self.unique_evaluation:
|
|
351
|
+
raise ValueError("an unevaluated seed cannot be a unique evaluation")
|
|
352
|
+
if not self.evaluated and self.valid:
|
|
353
|
+
raise ValueError("an unevaluated seed cannot be valid")
|
|
354
|
+
if self.valid:
|
|
355
|
+
if not self.evaluated or self.failure_type is not None:
|
|
356
|
+
raise ValueError("valid seed accounting cannot carry a failure")
|
|
357
|
+
elif type(self.failure_type) is not str or not self.failure_type:
|
|
358
|
+
raise ValueError("failed or invalid seed accounting requires failure_type")
|
|
359
|
+
_frozen(self.evidence, name="evidence")
|
|
360
|
+
|
|
361
|
+
def to_record(self) -> dict[str, object]:
|
|
362
|
+
self.__post_init__()
|
|
363
|
+
return {
|
|
364
|
+
"seed_id": self.seed_id,
|
|
365
|
+
"configuration_sha256": self.configuration_sha256,
|
|
366
|
+
"evaluated": self.evaluated,
|
|
367
|
+
"unique_evaluation": self.unique_evaluation,
|
|
368
|
+
"valid": self.valid,
|
|
369
|
+
"failure_type": self.failure_type,
|
|
370
|
+
"evidence": thaw_json(self.evidence),
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@dataclass(frozen=True, slots=True)
|
|
375
|
+
class CampaignExecutionStartReceipt:
|
|
376
|
+
preparation_sha256: str
|
|
377
|
+
runtime_preflight_receipt_sha256: str
|
|
378
|
+
runtime_session_id: str
|
|
379
|
+
seed_batch_sha256: str
|
|
380
|
+
seed_receipts: tuple[CampaignSeedExecutionReceipt, ...]
|
|
381
|
+
evidence: FrozenJsonObject
|
|
382
|
+
|
|
383
|
+
def __post_init__(self) -> None:
|
|
384
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
385
|
+
require_sha256(
|
|
386
|
+
self.runtime_preflight_receipt_sha256,
|
|
387
|
+
"runtime_preflight_receipt_sha256",
|
|
388
|
+
)
|
|
389
|
+
_token(self.runtime_session_id, name="runtime_session_id")
|
|
390
|
+
require_sha256(self.seed_batch_sha256, "seed_batch_sha256")
|
|
391
|
+
if type(self.seed_receipts) is not tuple or not self.seed_receipts:
|
|
392
|
+
raise ValueError("runtime start requires exact seed receipts")
|
|
393
|
+
if any(
|
|
394
|
+
type(value) is not CampaignSeedExecutionReceipt
|
|
395
|
+
for value in self.seed_receipts
|
|
396
|
+
):
|
|
397
|
+
raise TypeError("seed_receipts must contain exact receipts")
|
|
398
|
+
for value in self.seed_receipts:
|
|
399
|
+
CampaignSeedExecutionReceipt.__post_init__(value)
|
|
400
|
+
if len({value.seed_id for value in self.seed_receipts}) != len(
|
|
401
|
+
self.seed_receipts
|
|
402
|
+
):
|
|
403
|
+
raise ValueError("runtime start seed IDs must be unique")
|
|
404
|
+
_frozen(self.evidence, name="evidence")
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def seed_occurrence_count(self) -> int:
|
|
408
|
+
return len(self.seed_receipts)
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def seed_unique_evaluation_count(self) -> int:
|
|
412
|
+
return sum(value.unique_evaluation for value in self.seed_receipts)
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def seed_failure_count(self) -> int:
|
|
416
|
+
return sum(not value.valid for value in self.seed_receipts)
|
|
417
|
+
|
|
418
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
419
|
+
self.__post_init__()
|
|
420
|
+
return {
|
|
421
|
+
"preparation_sha256": self.preparation_sha256,
|
|
422
|
+
"runtime_preflight_receipt_sha256": (self.runtime_preflight_receipt_sha256),
|
|
423
|
+
"runtime_session_id": self.runtime_session_id,
|
|
424
|
+
"seed_batch_sha256": self.seed_batch_sha256,
|
|
425
|
+
"seed_receipts": [value.to_record() for value in self.seed_receipts],
|
|
426
|
+
"seed_accounting": {
|
|
427
|
+
"occurrences": self.seed_occurrence_count,
|
|
428
|
+
"unique_evaluations": self.seed_unique_evaluation_count,
|
|
429
|
+
"failures": self.seed_failure_count,
|
|
430
|
+
},
|
|
431
|
+
"evidence": thaw_json(self.evidence),
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def receipt_sha256(self) -> str:
|
|
436
|
+
return _hash(_START_DOMAIN, self._unsigned_record())
|
|
437
|
+
|
|
438
|
+
def to_record(self) -> dict[str, object]:
|
|
439
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@dataclass(frozen=True, slots=True)
|
|
443
|
+
class CampaignArchiveCutoffRequest:
|
|
444
|
+
preparation_sha256: str
|
|
445
|
+
runtime_start_receipt_sha256: str
|
|
446
|
+
generation: int
|
|
447
|
+
kind: CampaignGenerationKind
|
|
448
|
+
step_sha256: str
|
|
449
|
+
prior_stage_receipt_sha256: str | None
|
|
450
|
+
|
|
451
|
+
def __post_init__(self) -> None:
|
|
452
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
453
|
+
require_sha256(
|
|
454
|
+
self.runtime_start_receipt_sha256,
|
|
455
|
+
"runtime_start_receipt_sha256",
|
|
456
|
+
)
|
|
457
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
458
|
+
raise ValueError("archive cutoff generation must be positive")
|
|
459
|
+
if type(self.kind) is not CampaignGenerationKind:
|
|
460
|
+
raise TypeError("archive cutoff kind must be CampaignGenerationKind")
|
|
461
|
+
require_sha256(self.step_sha256, "step_sha256")
|
|
462
|
+
if self.prior_stage_receipt_sha256 is not None:
|
|
463
|
+
require_sha256(
|
|
464
|
+
self.prior_stage_receipt_sha256,
|
|
465
|
+
"prior_stage_receipt_sha256",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def to_record(self) -> dict[str, object]:
|
|
469
|
+
self.__post_init__()
|
|
470
|
+
return {
|
|
471
|
+
"preparation_sha256": self.preparation_sha256,
|
|
472
|
+
"runtime_start_receipt_sha256": self.runtime_start_receipt_sha256,
|
|
473
|
+
"generation": self.generation,
|
|
474
|
+
"kind": self.kind.value,
|
|
475
|
+
"step_sha256": self.step_sha256,
|
|
476
|
+
"prior_stage_receipt_sha256": self.prior_stage_receipt_sha256,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def request_sha256(self) -> str:
|
|
481
|
+
return _hash(_ARCHIVE_REQUEST_DOMAIN, self.to_record())
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@dataclass(frozen=True, slots=True)
|
|
485
|
+
class CampaignArchiveCutoffReceipt:
|
|
486
|
+
request_sha256: str
|
|
487
|
+
preparation_sha256: str
|
|
488
|
+
generation: int
|
|
489
|
+
archive: FrozenJsonObject
|
|
490
|
+
evidence: FrozenJsonObject
|
|
491
|
+
|
|
492
|
+
def __post_init__(self) -> None:
|
|
493
|
+
require_sha256(self.request_sha256, "request_sha256")
|
|
494
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
495
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
496
|
+
raise ValueError("archive cutoff generation must be positive")
|
|
497
|
+
_frozen(self.archive, name="archive")
|
|
498
|
+
_frozen(self.evidence, name="evidence")
|
|
499
|
+
|
|
500
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
501
|
+
self.__post_init__()
|
|
502
|
+
return {
|
|
503
|
+
"request_sha256": self.request_sha256,
|
|
504
|
+
"preparation_sha256": self.preparation_sha256,
|
|
505
|
+
"generation": self.generation,
|
|
506
|
+
"archive": thaw_json(self.archive),
|
|
507
|
+
"archive_sha256": typed_json_sha256(self.archive),
|
|
508
|
+
"evidence": thaw_json(self.evidence),
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
@property
|
|
512
|
+
def receipt_sha256(self) -> str:
|
|
513
|
+
return _hash(_ARCHIVE_RECEIPT_DOMAIN, self._unsigned_record())
|
|
514
|
+
|
|
515
|
+
def to_record(self) -> dict[str, object]:
|
|
516
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@dataclass(frozen=True, slots=True)
|
|
520
|
+
class CampaignStageReceipt:
|
|
521
|
+
"""Opaque stage result plus exact accounting and selector trace evidence."""
|
|
522
|
+
|
|
523
|
+
request_sha256: str
|
|
524
|
+
preparation_sha256: str
|
|
525
|
+
generation: int
|
|
526
|
+
kind: CampaignGenerationKind
|
|
527
|
+
candidate_occurrence_count: int
|
|
528
|
+
unique_evaluation_count: int
|
|
529
|
+
selector_audits: tuple[CampaignSelectorAuditReceipt, ...]
|
|
530
|
+
result: FrozenJsonObject
|
|
531
|
+
|
|
532
|
+
def __post_init__(self) -> None:
|
|
533
|
+
require_sha256(self.request_sha256, "request_sha256")
|
|
534
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
535
|
+
if type(self.generation) is not int or self.generation <= 0:
|
|
536
|
+
raise ValueError("stage receipt generation must be positive")
|
|
537
|
+
if type(self.kind) is not CampaignGenerationKind:
|
|
538
|
+
raise TypeError("stage receipt kind must be CampaignGenerationKind")
|
|
539
|
+
if (
|
|
540
|
+
type(self.candidate_occurrence_count) is not int
|
|
541
|
+
or self.candidate_occurrence_count < 0
|
|
542
|
+
):
|
|
543
|
+
raise ValueError("candidate_occurrence_count must be non-negative")
|
|
544
|
+
if (
|
|
545
|
+
type(self.unique_evaluation_count) is not int
|
|
546
|
+
or not 0 <= self.unique_evaluation_count <= self.candidate_occurrence_count
|
|
547
|
+
):
|
|
548
|
+
raise ValueError(
|
|
549
|
+
"unique_evaluation_count must lie within candidate occurrences"
|
|
550
|
+
)
|
|
551
|
+
if type(self.selector_audits) is not tuple or any(
|
|
552
|
+
type(value) is not CampaignSelectorAuditReceipt
|
|
553
|
+
for value in self.selector_audits
|
|
554
|
+
):
|
|
555
|
+
raise TypeError("selector_audits must contain exact audit receipts")
|
|
556
|
+
for value in self.selector_audits:
|
|
557
|
+
CampaignSelectorAuditReceipt.__post_init__(value)
|
|
558
|
+
_frozen(self.result, name="result")
|
|
559
|
+
|
|
560
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
561
|
+
self.__post_init__()
|
|
562
|
+
return {
|
|
563
|
+
"request_sha256": self.request_sha256,
|
|
564
|
+
"preparation_sha256": self.preparation_sha256,
|
|
565
|
+
"generation": self.generation,
|
|
566
|
+
"kind": self.kind.value,
|
|
567
|
+
"candidate_occurrence_count": self.candidate_occurrence_count,
|
|
568
|
+
"unique_evaluation_count": self.unique_evaluation_count,
|
|
569
|
+
"selector_audits": [value.to_record() for value in self.selector_audits],
|
|
570
|
+
"result": thaw_json(self.result),
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def receipt_sha256(self) -> str:
|
|
575
|
+
return _hash(_STAGE_RECEIPT_DOMAIN, self._unsigned_record())
|
|
576
|
+
|
|
577
|
+
def to_record(self) -> dict[str, object]:
|
|
578
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@dataclass(frozen=True, slots=True)
|
|
582
|
+
class CampaignStageRequest:
|
|
583
|
+
preparation_sha256: str
|
|
584
|
+
runtime_start_receipt_sha256: str
|
|
585
|
+
step: CampaignGenerationStep
|
|
586
|
+
archive_cutoff: CampaignArchiveCutoffReceipt
|
|
587
|
+
archive_utility: ArchiveUtilitySnapshot
|
|
588
|
+
source_portfolio: CampaignStageReceipt | None
|
|
589
|
+
test_eligible_reflection_receipt_sha256s: tuple[str, ...]
|
|
590
|
+
prior_selector_audit_set_sha256: str
|
|
591
|
+
|
|
592
|
+
def __post_init__(self) -> None:
|
|
593
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
594
|
+
require_sha256(
|
|
595
|
+
self.runtime_start_receipt_sha256,
|
|
596
|
+
"runtime_start_receipt_sha256",
|
|
597
|
+
)
|
|
598
|
+
if type(self.step) is not CampaignGenerationStep:
|
|
599
|
+
raise TypeError("step must be an exact CampaignGenerationStep")
|
|
600
|
+
CampaignGenerationStep.__post_init__(self.step)
|
|
601
|
+
if type(self.archive_cutoff) is not CampaignArchiveCutoffReceipt:
|
|
602
|
+
raise TypeError("archive_cutoff must be exact")
|
|
603
|
+
CampaignArchiveCutoffReceipt.__post_init__(self.archive_cutoff)
|
|
604
|
+
if type(self.archive_utility) is not ArchiveUtilitySnapshot:
|
|
605
|
+
raise TypeError("archive_utility must be exact")
|
|
606
|
+
ArchiveUtilitySnapshot.__post_init__(self.archive_utility)
|
|
607
|
+
if self.archive_cutoff.generation != self.step.generation:
|
|
608
|
+
raise ValueError("archive cutoff generation differs from stage")
|
|
609
|
+
if (
|
|
610
|
+
self.archive_utility.generation != self.step.generation
|
|
611
|
+
or self.archive_utility.archive_sha256
|
|
612
|
+
!= typed_json_sha256(self.archive_cutoff.archive)
|
|
613
|
+
):
|
|
614
|
+
raise ValueError("archive utility differs from the pre-stage cutoff")
|
|
615
|
+
if self.step.kind is CampaignGenerationKind.PORTFOLIO:
|
|
616
|
+
if self.source_portfolio is not None:
|
|
617
|
+
raise ValueError("portfolio stage cannot carry a source portfolio")
|
|
618
|
+
else:
|
|
619
|
+
if type(self.source_portfolio) is not CampaignStageReceipt:
|
|
620
|
+
raise ValueError("recombination requires its portfolio receipt")
|
|
621
|
+
CampaignStageReceipt.__post_init__(self.source_portfolio)
|
|
622
|
+
if (
|
|
623
|
+
self.source_portfolio.kind is not CampaignGenerationKind.PORTFOLIO
|
|
624
|
+
or self.source_portfolio.generation
|
|
625
|
+
!= self.step.source_portfolio_generation
|
|
626
|
+
or self.source_portfolio.preparation_sha256 != self.preparation_sha256
|
|
627
|
+
):
|
|
628
|
+
raise ValueError("recombination source portfolio is foreign")
|
|
629
|
+
if type(self.test_eligible_reflection_receipt_sha256s) is not tuple or any(
|
|
630
|
+
type(value) is not str
|
|
631
|
+
for value in self.test_eligible_reflection_receipt_sha256s
|
|
632
|
+
):
|
|
633
|
+
raise TypeError(
|
|
634
|
+
"test-eligible reflection hashes must be an exact string tuple"
|
|
635
|
+
)
|
|
636
|
+
for value in self.test_eligible_reflection_receipt_sha256s:
|
|
637
|
+
require_sha256(value, "test_eligible_reflection_receipt_sha256")
|
|
638
|
+
if self.test_eligible_reflection_receipt_sha256s != tuple(
|
|
639
|
+
sorted(set(self.test_eligible_reflection_receipt_sha256s))
|
|
640
|
+
):
|
|
641
|
+
raise ValueError(
|
|
642
|
+
"test-eligible reflection hashes must be unique and canonical"
|
|
643
|
+
)
|
|
644
|
+
require_sha256(
|
|
645
|
+
self.prior_selector_audit_set_sha256,
|
|
646
|
+
"prior_selector_audit_set_sha256",
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
def to_record(self) -> dict[str, object]:
|
|
650
|
+
self.__post_init__()
|
|
651
|
+
return {
|
|
652
|
+
"preparation_sha256": self.preparation_sha256,
|
|
653
|
+
"runtime_start_receipt_sha256": self.runtime_start_receipt_sha256,
|
|
654
|
+
"step": self.step.to_record(),
|
|
655
|
+
"step_sha256": campaign_step_sha256(self.step),
|
|
656
|
+
"archive_cutoff_receipt_sha256": self.archive_cutoff.receipt_sha256,
|
|
657
|
+
"archive_utility_snapshot_sha256": self.archive_utility.snapshot_sha256,
|
|
658
|
+
"source_portfolio_receipt_sha256": (
|
|
659
|
+
None
|
|
660
|
+
if self.source_portfolio is None
|
|
661
|
+
else self.source_portfolio.receipt_sha256
|
|
662
|
+
),
|
|
663
|
+
"test_eligible_reflection_receipt_sha256s": list(
|
|
664
|
+
self.test_eligible_reflection_receipt_sha256s
|
|
665
|
+
),
|
|
666
|
+
"prior_selector_audit_set_sha256": (self.prior_selector_audit_set_sha256),
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
@property
|
|
670
|
+
def request_sha256(self) -> str:
|
|
671
|
+
return _hash(_STAGE_REQUEST_DOMAIN, self.to_record())
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@dataclass(frozen=True, slots=True)
|
|
675
|
+
class CampaignReflectionRequest:
|
|
676
|
+
"""Reflection over one exact, already-sealed recombination cutoff.
|
|
677
|
+
|
|
678
|
+
``source_stage`` is the complete evidence boundary exposed by this
|
|
679
|
+
application port. A delayed admission barrier may occur later, but no
|
|
680
|
+
later stage receipt is present in this request or passed to ``reflect``.
|
|
681
|
+
"""
|
|
682
|
+
|
|
683
|
+
preparation_sha256: str
|
|
684
|
+
runtime_start_receipt_sha256: str
|
|
685
|
+
wave: CampaignReflectionWave
|
|
686
|
+
source_stage: CampaignStageReceipt
|
|
687
|
+
|
|
688
|
+
def __post_init__(self) -> None:
|
|
689
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
690
|
+
require_sha256(
|
|
691
|
+
self.runtime_start_receipt_sha256,
|
|
692
|
+
"runtime_start_receipt_sha256",
|
|
693
|
+
)
|
|
694
|
+
if type(self.wave) is not CampaignReflectionWave:
|
|
695
|
+
raise TypeError("wave must be an exact CampaignReflectionWave")
|
|
696
|
+
CampaignReflectionWave.__post_init__(self.wave)
|
|
697
|
+
if type(self.source_stage) is not CampaignStageReceipt:
|
|
698
|
+
raise TypeError("source_stage must be exact")
|
|
699
|
+
CampaignStageReceipt.__post_init__(self.source_stage)
|
|
700
|
+
if (
|
|
701
|
+
self.source_stage.preparation_sha256 != self.preparation_sha256
|
|
702
|
+
or self.source_stage.generation != self.wave.source_generation
|
|
703
|
+
or self.source_stage.kind is not CampaignGenerationKind.RECOMBINATION
|
|
704
|
+
):
|
|
705
|
+
raise ValueError("reflection source stage is foreign")
|
|
706
|
+
|
|
707
|
+
def to_record(self) -> dict[str, object]:
|
|
708
|
+
self.__post_init__()
|
|
709
|
+
record: dict[str, object] = {
|
|
710
|
+
"preparation_sha256": self.preparation_sha256,
|
|
711
|
+
"runtime_start_receipt_sha256": self.runtime_start_receipt_sha256,
|
|
712
|
+
"wave": self.wave.to_record(),
|
|
713
|
+
"source_stage_receipt_sha256": self.source_stage.receipt_sha256,
|
|
714
|
+
}
|
|
715
|
+
barrier = self.wave.promotion_barrier_generation
|
|
716
|
+
if barrier is not None and barrier > self.wave.source_generation:
|
|
717
|
+
record["sealed_evidence_cutoff"] = {
|
|
718
|
+
"generation": self.source_stage.generation,
|
|
719
|
+
"stage_receipt_sha256": self.source_stage.receipt_sha256,
|
|
720
|
+
}
|
|
721
|
+
record["future_stage_evidence_permitted"] = False
|
|
722
|
+
return record
|
|
723
|
+
|
|
724
|
+
@property
|
|
725
|
+
def request_sha256(self) -> str:
|
|
726
|
+
return _hash(_REFLECTION_REQUEST_DOMAIN, self.to_record())
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
class CampaignReflectionStatus(str, Enum):
|
|
730
|
+
COMPLETED = "completed"
|
|
731
|
+
ABSTAINED = "abstained"
|
|
732
|
+
FAILED = "failed"
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
@dataclass(frozen=True, slots=True)
|
|
736
|
+
class CampaignReflectionReceipt:
|
|
737
|
+
request_sha256: str
|
|
738
|
+
preparation_sha256: str
|
|
739
|
+
source_generation: int
|
|
740
|
+
source_stage_receipt_sha256: str
|
|
741
|
+
logical_agent_calls: int
|
|
742
|
+
visibility: ReflectionVisibility
|
|
743
|
+
status: CampaignReflectionStatus
|
|
744
|
+
failure_type: str | None
|
|
745
|
+
quarantined_result: FrozenJsonObject
|
|
746
|
+
|
|
747
|
+
def __post_init__(self) -> None:
|
|
748
|
+
require_sha256(self.request_sha256, "request_sha256")
|
|
749
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
750
|
+
if type(self.source_generation) is not int or self.source_generation <= 0:
|
|
751
|
+
raise ValueError("reflection source_generation must be positive")
|
|
752
|
+
require_sha256(
|
|
753
|
+
self.source_stage_receipt_sha256,
|
|
754
|
+
"source_stage_receipt_sha256",
|
|
755
|
+
)
|
|
756
|
+
if type(self.logical_agent_calls) is not int or self.logical_agent_calls <= 0:
|
|
757
|
+
raise ValueError("logical_agent_calls must be positive")
|
|
758
|
+
if self.visibility is not ReflectionVisibility.QUARANTINED_UNTIL_BLOCK_CLOSE:
|
|
759
|
+
raise ValueError("reflection result must remain quarantined")
|
|
760
|
+
if type(self.status) is not CampaignReflectionStatus:
|
|
761
|
+
raise TypeError("status must be CampaignReflectionStatus")
|
|
762
|
+
if self.status is CampaignReflectionStatus.COMPLETED:
|
|
763
|
+
if self.failure_type is not None:
|
|
764
|
+
raise ValueError("completed reflection cannot carry failure_type")
|
|
765
|
+
elif self.status is CampaignReflectionStatus.ABSTAINED:
|
|
766
|
+
if self.failure_type is not None:
|
|
767
|
+
raise ValueError("abstained reflection cannot carry failure_type")
|
|
768
|
+
abstention = thaw_json(self.quarantined_result)
|
|
769
|
+
if (
|
|
770
|
+
abstention.get("status")
|
|
771
|
+
!= "abstained_no_identifiable_mutation_evidence"
|
|
772
|
+
or abstention.get("evidence_tier") != "e0"
|
|
773
|
+
or abstention.get("provider_calls") != 0
|
|
774
|
+
or abstention.get("publishable_reflection_content") is not False
|
|
775
|
+
):
|
|
776
|
+
raise ValueError("abstained reflection requires typed E0 evidence")
|
|
777
|
+
elif type(self.failure_type) is not str or not self.failure_type:
|
|
778
|
+
raise ValueError("failed reflection requires failure_type")
|
|
779
|
+
_frozen(self.quarantined_result, name="quarantined_result")
|
|
780
|
+
|
|
781
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
782
|
+
self.__post_init__()
|
|
783
|
+
return {
|
|
784
|
+
"request_sha256": self.request_sha256,
|
|
785
|
+
"preparation_sha256": self.preparation_sha256,
|
|
786
|
+
"source_generation": self.source_generation,
|
|
787
|
+
"source_stage_receipt_sha256": self.source_stage_receipt_sha256,
|
|
788
|
+
"logical_agent_calls": self.logical_agent_calls,
|
|
789
|
+
"visibility": self.visibility.value,
|
|
790
|
+
"status": self.status.value,
|
|
791
|
+
"failure_type": self.failure_type,
|
|
792
|
+
"quarantined_result": thaw_json(self.quarantined_result),
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
@property
|
|
796
|
+
def receipt_sha256(self) -> str:
|
|
797
|
+
return _hash(_REFLECTION_RECEIPT_DOMAIN, self._unsigned_record())
|
|
798
|
+
|
|
799
|
+
def to_record(self) -> dict[str, object]:
|
|
800
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
@dataclass(frozen=True, slots=True)
|
|
804
|
+
class CampaignReflectionCancellationReceipt:
|
|
805
|
+
"""Durable accounting for one scheduled reflection canceled on abort."""
|
|
806
|
+
|
|
807
|
+
request_sha256: str
|
|
808
|
+
preparation_sha256: str
|
|
809
|
+
source_generation: int
|
|
810
|
+
source_stage_receipt_sha256: str
|
|
811
|
+
logical_agent_calls_reserved: int
|
|
812
|
+
dispatched_to_runtime: bool
|
|
813
|
+
cancellation_reason: str
|
|
814
|
+
|
|
815
|
+
def __post_init__(self) -> None:
|
|
816
|
+
require_sha256(self.request_sha256, "request_sha256")
|
|
817
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
818
|
+
if type(self.source_generation) is not int or self.source_generation <= 0:
|
|
819
|
+
raise ValueError("source_generation must be a positive exact integer")
|
|
820
|
+
require_sha256(
|
|
821
|
+
self.source_stage_receipt_sha256,
|
|
822
|
+
"source_stage_receipt_sha256",
|
|
823
|
+
)
|
|
824
|
+
if (
|
|
825
|
+
type(self.logical_agent_calls_reserved) is not int
|
|
826
|
+
or self.logical_agent_calls_reserved <= 0
|
|
827
|
+
):
|
|
828
|
+
raise ValueError("logical_agent_calls_reserved must be positive")
|
|
829
|
+
if type(self.dispatched_to_runtime) is not bool:
|
|
830
|
+
raise TypeError("dispatched_to_runtime must be an exact bool")
|
|
831
|
+
if (
|
|
832
|
+
type(self.cancellation_reason) is not str
|
|
833
|
+
or _TOKEN.fullmatch(self.cancellation_reason) is None
|
|
834
|
+
):
|
|
835
|
+
raise ValueError("cancellation_reason must use the campaign token grammar")
|
|
836
|
+
|
|
837
|
+
@property
|
|
838
|
+
def logical_agent_calls_dispatched_to_runtime(self) -> int:
|
|
839
|
+
self.__post_init__()
|
|
840
|
+
return self.logical_agent_calls_reserved if self.dispatched_to_runtime else 0
|
|
841
|
+
|
|
842
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
843
|
+
self.__post_init__()
|
|
844
|
+
return {
|
|
845
|
+
"request_sha256": self.request_sha256,
|
|
846
|
+
"preparation_sha256": self.preparation_sha256,
|
|
847
|
+
"source_generation": self.source_generation,
|
|
848
|
+
"source_stage_receipt_sha256": self.source_stage_receipt_sha256,
|
|
849
|
+
"logical_agent_calls_reserved": self.logical_agent_calls_reserved,
|
|
850
|
+
"logical_agent_calls_dispatched_to_runtime": (
|
|
851
|
+
self.logical_agent_calls_dispatched_to_runtime
|
|
852
|
+
),
|
|
853
|
+
"dispatched_to_runtime": self.dispatched_to_runtime,
|
|
854
|
+
"cancellation_reason": self.cancellation_reason,
|
|
855
|
+
"publishable_reflection_content": False,
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
@property
|
|
859
|
+
def receipt_sha256(self) -> str:
|
|
860
|
+
return _hash(
|
|
861
|
+
_REFLECTION_CANCELLATION_RECEIPT_DOMAIN,
|
|
862
|
+
self._unsigned_record(),
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
def to_record(self) -> dict[str, object]:
|
|
866
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
@dataclass(frozen=True, slots=True)
|
|
870
|
+
class CampaignReflectionTestAdmissionRequest:
|
|
871
|
+
"""Admit quarantined evidence only to later controlled testing.
|
|
872
|
+
|
|
873
|
+
For a delayed barrier, ``admission_cutoff_stage`` authenticates the later
|
|
874
|
+
stage seal available to an admission-time shadow screen. It is deliberately
|
|
875
|
+
absent from :class:`CampaignReflectionRequest`, so it cannot alter or leak
|
|
876
|
+
into the already-running reflection.
|
|
877
|
+
"""
|
|
878
|
+
|
|
879
|
+
preparation_sha256: str
|
|
880
|
+
runtime_start_receipt_sha256: str
|
|
881
|
+
barrier: CampaignPromotionBarrier
|
|
882
|
+
reflections: tuple[CampaignReflectionReceipt, ...]
|
|
883
|
+
previously_test_eligible_reflection_receipt_sha256s: tuple[str, ...]
|
|
884
|
+
admission_cutoff_stage: CampaignStageReceipt | None = None
|
|
885
|
+
|
|
886
|
+
def __post_init__(self) -> None:
|
|
887
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
888
|
+
require_sha256(
|
|
889
|
+
self.runtime_start_receipt_sha256,
|
|
890
|
+
"runtime_start_receipt_sha256",
|
|
891
|
+
)
|
|
892
|
+
if type(self.barrier) is not CampaignPromotionBarrier:
|
|
893
|
+
raise TypeError("barrier must be an exact CampaignPromotionBarrier")
|
|
894
|
+
CampaignPromotionBarrier.__post_init__(self.barrier)
|
|
895
|
+
if type(self.reflections) is not tuple or any(
|
|
896
|
+
type(value) is not CampaignReflectionReceipt for value in self.reflections
|
|
897
|
+
):
|
|
898
|
+
raise TypeError("reflections must contain exact receipts")
|
|
899
|
+
for value in self.reflections:
|
|
900
|
+
CampaignReflectionReceipt.__post_init__(value)
|
|
901
|
+
if tuple(value.source_generation for value in self.reflections) != (
|
|
902
|
+
self.barrier.reflection_source_generations
|
|
903
|
+
):
|
|
904
|
+
raise ValueError("test-admission reflections differ from barrier sources")
|
|
905
|
+
if any(
|
|
906
|
+
value.preparation_sha256 != self.preparation_sha256
|
|
907
|
+
or value.status is not CampaignReflectionStatus.COMPLETED
|
|
908
|
+
for value in self.reflections
|
|
909
|
+
):
|
|
910
|
+
raise ValueError("test admission requires completed reflections")
|
|
911
|
+
values = self.previously_test_eligible_reflection_receipt_sha256s
|
|
912
|
+
if type(values) is not tuple or any(type(value) is not str for value in values):
|
|
913
|
+
raise TypeError(
|
|
914
|
+
"previously test-eligible hashes must be an exact string tuple"
|
|
915
|
+
)
|
|
916
|
+
for value in values:
|
|
917
|
+
require_sha256(value, "previous_test_eligible_reflection_sha256")
|
|
918
|
+
if values != tuple(sorted(set(values))):
|
|
919
|
+
raise ValueError(
|
|
920
|
+
"previously test-eligible hashes must be unique and canonical"
|
|
921
|
+
)
|
|
922
|
+
delayed = (
|
|
923
|
+
self.barrier.generation
|
|
924
|
+
> self.barrier.reflection_source_generations[-1]
|
|
925
|
+
)
|
|
926
|
+
if delayed:
|
|
927
|
+
if type(self.admission_cutoff_stage) is not CampaignStageReceipt:
|
|
928
|
+
raise ValueError(
|
|
929
|
+
"delayed admission requires its exact barrier-stage cutoff"
|
|
930
|
+
)
|
|
931
|
+
CampaignStageReceipt.__post_init__(self.admission_cutoff_stage)
|
|
932
|
+
if (
|
|
933
|
+
self.admission_cutoff_stage.preparation_sha256
|
|
934
|
+
!= self.preparation_sha256
|
|
935
|
+
or self.admission_cutoff_stage.generation
|
|
936
|
+
!= self.barrier.generation
|
|
937
|
+
or self.admission_cutoff_stage.kind
|
|
938
|
+
is not CampaignGenerationKind.RECOMBINATION
|
|
939
|
+
):
|
|
940
|
+
raise ValueError("delayed admission cutoff stage is foreign")
|
|
941
|
+
elif self.admission_cutoff_stage is not None:
|
|
942
|
+
raise ValueError(
|
|
943
|
+
"source-closing admission cannot carry a later-stage cutoff"
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
def to_record(self) -> dict[str, object]:
|
|
947
|
+
self.__post_init__()
|
|
948
|
+
record: dict[str, object] = {
|
|
949
|
+
"preparation_sha256": self.preparation_sha256,
|
|
950
|
+
"runtime_start_receipt_sha256": self.runtime_start_receipt_sha256,
|
|
951
|
+
"barrier": self.barrier.to_record(),
|
|
952
|
+
"reflection_receipt_sha256s": [
|
|
953
|
+
value.receipt_sha256 for value in self.reflections
|
|
954
|
+
],
|
|
955
|
+
"previously_test_eligible_reflection_receipt_sha256s": list(
|
|
956
|
+
self.previously_test_eligible_reflection_receipt_sha256s
|
|
957
|
+
),
|
|
958
|
+
"admission_scope": "controlled_future_testing_only",
|
|
959
|
+
"lifecycle_promotion": False,
|
|
960
|
+
}
|
|
961
|
+
if self.admission_cutoff_stage is not None:
|
|
962
|
+
record["admission_cutoff_stage_receipt_sha256"] = (
|
|
963
|
+
self.admission_cutoff_stage.receipt_sha256
|
|
964
|
+
)
|
|
965
|
+
record["reflection_cutoff_precedes_admission_cutoff"] = True
|
|
966
|
+
record["future_evidence_visible_to_reflection"] = False
|
|
967
|
+
return record
|
|
968
|
+
|
|
969
|
+
@property
|
|
970
|
+
def request_sha256(self) -> str:
|
|
971
|
+
return _hash(_TEST_ADMISSION_REQUEST_DOMAIN, self.to_record())
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@dataclass(frozen=True, slots=True)
|
|
975
|
+
class CampaignReflectionTestAdmissionReceipt:
|
|
976
|
+
request_sha256: str
|
|
977
|
+
preparation_sha256: str
|
|
978
|
+
barrier_generation: int
|
|
979
|
+
admitted_reflection_receipt_sha256s: tuple[str, ...]
|
|
980
|
+
test_eligible_reflection_receipt_sha256s: tuple[str, ...]
|
|
981
|
+
lifecycle_promoted: bool
|
|
982
|
+
evidence: FrozenJsonObject
|
|
983
|
+
|
|
984
|
+
def __post_init__(self) -> None:
|
|
985
|
+
require_sha256(self.request_sha256, "request_sha256")
|
|
986
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
987
|
+
if type(self.barrier_generation) is not int or self.barrier_generation <= 0:
|
|
988
|
+
raise ValueError("barrier_generation must be positive")
|
|
989
|
+
for name in (
|
|
990
|
+
"admitted_reflection_receipt_sha256s",
|
|
991
|
+
"test_eligible_reflection_receipt_sha256s",
|
|
992
|
+
):
|
|
993
|
+
values = getattr(self, name)
|
|
994
|
+
if type(values) is not tuple or any(
|
|
995
|
+
type(value) is not str for value in values
|
|
996
|
+
):
|
|
997
|
+
raise TypeError(f"{name} must be an exact string tuple")
|
|
998
|
+
for value in values:
|
|
999
|
+
require_sha256(value, name[:-1])
|
|
1000
|
+
if values != tuple(sorted(set(values))):
|
|
1001
|
+
raise ValueError(f"{name} must be unique and canonical")
|
|
1002
|
+
if self.lifecycle_promoted is not False:
|
|
1003
|
+
raise ValueError(
|
|
1004
|
+
"reflection barrier cannot lifecycle-promote untested insights"
|
|
1005
|
+
)
|
|
1006
|
+
_frozen(self.evidence, name="evidence")
|
|
1007
|
+
|
|
1008
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1009
|
+
self.__post_init__()
|
|
1010
|
+
return {
|
|
1011
|
+
"request_sha256": self.request_sha256,
|
|
1012
|
+
"preparation_sha256": self.preparation_sha256,
|
|
1013
|
+
"barrier_generation": self.barrier_generation,
|
|
1014
|
+
"admitted_reflection_receipt_sha256s": list(
|
|
1015
|
+
self.admitted_reflection_receipt_sha256s
|
|
1016
|
+
),
|
|
1017
|
+
"test_eligible_reflection_receipt_sha256s": list(
|
|
1018
|
+
self.test_eligible_reflection_receipt_sha256s
|
|
1019
|
+
),
|
|
1020
|
+
"lifecycle_promoted": False,
|
|
1021
|
+
"evidence": thaw_json(self.evidence),
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
@property
|
|
1025
|
+
def receipt_sha256(self) -> str:
|
|
1026
|
+
return _hash(_TEST_ADMISSION_RECEIPT_DOMAIN, self._unsigned_record())
|
|
1027
|
+
|
|
1028
|
+
def to_record(self) -> dict[str, object]:
|
|
1029
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
@dataclass(frozen=True, slots=True)
|
|
1033
|
+
class CampaignTailDrainReceipt:
|
|
1034
|
+
preparation_sha256: str
|
|
1035
|
+
drained_reflections: tuple[tuple[int, str], ...]
|
|
1036
|
+
admitted_for_testing: bool = False
|
|
1037
|
+
lifecycle_promoted: bool = False
|
|
1038
|
+
|
|
1039
|
+
def __post_init__(self) -> None:
|
|
1040
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
1041
|
+
if type(self.drained_reflections) is not tuple or not self.drained_reflections:
|
|
1042
|
+
raise ValueError("tail drain requires reflection receipts")
|
|
1043
|
+
generations: list[int] = []
|
|
1044
|
+
for item in self.drained_reflections:
|
|
1045
|
+
if (
|
|
1046
|
+
type(item) is not tuple
|
|
1047
|
+
or len(item) != 2
|
|
1048
|
+
or type(item[0]) is not int
|
|
1049
|
+
or item[0] <= 0
|
|
1050
|
+
or type(item[1]) is not str
|
|
1051
|
+
):
|
|
1052
|
+
raise TypeError(
|
|
1053
|
+
"drained_reflections must contain generation/hash pairs"
|
|
1054
|
+
)
|
|
1055
|
+
require_sha256(item[1], "reflection_receipt_sha256")
|
|
1056
|
+
generations.append(item[0])
|
|
1057
|
+
if generations != sorted(set(generations)):
|
|
1058
|
+
raise ValueError("tail drain generations must be unique and canonical")
|
|
1059
|
+
if (
|
|
1060
|
+
self.admitted_for_testing is not False
|
|
1061
|
+
or self.lifecycle_promoted is not False
|
|
1062
|
+
):
|
|
1063
|
+
raise ValueError(
|
|
1064
|
+
"incomplete-tail reflections remain quarantined and unadmitted"
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1068
|
+
self.__post_init__()
|
|
1069
|
+
return {
|
|
1070
|
+
"preparation_sha256": self.preparation_sha256,
|
|
1071
|
+
"drained_reflections": [
|
|
1072
|
+
{"source_generation": generation, "receipt_sha256": receipt}
|
|
1073
|
+
for generation, receipt in self.drained_reflections
|
|
1074
|
+
],
|
|
1075
|
+
"admitted_for_testing": False,
|
|
1076
|
+
"lifecycle_promoted": False,
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
@property
|
|
1080
|
+
def receipt_sha256(self) -> str:
|
|
1081
|
+
return _hash(_TAIL_DRAIN_DOMAIN, self._unsigned_record())
|
|
1082
|
+
|
|
1083
|
+
def to_record(self) -> dict[str, object]:
|
|
1084
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
@dataclass(frozen=True, slots=True)
|
|
1088
|
+
class CampaignExecutionCounters:
|
|
1089
|
+
generations_completed: int
|
|
1090
|
+
candidate_occurrences: int
|
|
1091
|
+
unique_evaluations: int
|
|
1092
|
+
logical_agent_calls: int
|
|
1093
|
+
logical_agent_calls_dispatched_to_runtime: int = 0
|
|
1094
|
+
logical_agent_calls_succeeded: int = 0
|
|
1095
|
+
logical_agent_calls_abstained: int = 0
|
|
1096
|
+
logical_agent_calls_failed: int = 0
|
|
1097
|
+
logical_agent_calls_cancelled_before_dispatch: int = 0
|
|
1098
|
+
logical_agent_calls_cancelled_after_dispatch: int = 0
|
|
1099
|
+
|
|
1100
|
+
def __post_init__(self) -> None:
|
|
1101
|
+
for name in (
|
|
1102
|
+
"generations_completed",
|
|
1103
|
+
"candidate_occurrences",
|
|
1104
|
+
"unique_evaluations",
|
|
1105
|
+
"logical_agent_calls",
|
|
1106
|
+
"logical_agent_calls_dispatched_to_runtime",
|
|
1107
|
+
"logical_agent_calls_succeeded",
|
|
1108
|
+
"logical_agent_calls_abstained",
|
|
1109
|
+
"logical_agent_calls_failed",
|
|
1110
|
+
"logical_agent_calls_cancelled_before_dispatch",
|
|
1111
|
+
"logical_agent_calls_cancelled_after_dispatch",
|
|
1112
|
+
):
|
|
1113
|
+
value = getattr(self, name)
|
|
1114
|
+
if type(value) is not int or value < 0:
|
|
1115
|
+
raise ValueError(f"{name} must be a non-negative exact integer")
|
|
1116
|
+
if self.logical_agent_calls_dispatched_to_runtime > self.logical_agent_calls:
|
|
1117
|
+
raise ValueError("dispatched logical calls exceed reserved calls")
|
|
1118
|
+
if (
|
|
1119
|
+
self.logical_agent_calls_dispatched_to_runtime
|
|
1120
|
+
+ self.logical_agent_calls_cancelled_before_dispatch
|
|
1121
|
+
> self.logical_agent_calls
|
|
1122
|
+
):
|
|
1123
|
+
raise ValueError(
|
|
1124
|
+
"dispatched and pre-dispatch-cancelled calls exceed reserve"
|
|
1125
|
+
)
|
|
1126
|
+
if (
|
|
1127
|
+
self.logical_agent_calls_succeeded
|
|
1128
|
+
+ self.logical_agent_calls_abstained
|
|
1129
|
+
+ self.logical_agent_calls_failed
|
|
1130
|
+
+ self.logical_agent_calls_cancelled_after_dispatch
|
|
1131
|
+
> self.logical_agent_calls_dispatched_to_runtime
|
|
1132
|
+
):
|
|
1133
|
+
raise ValueError("settled logical calls exceed dispatched calls")
|
|
1134
|
+
|
|
1135
|
+
def to_record(self) -> dict[str, int]:
|
|
1136
|
+
self.__post_init__()
|
|
1137
|
+
return {
|
|
1138
|
+
"generations_completed": self.generations_completed,
|
|
1139
|
+
"candidate_occurrences": self.candidate_occurrences,
|
|
1140
|
+
"unique_evaluations": self.unique_evaluations,
|
|
1141
|
+
"logical_agent_calls": self.logical_agent_calls,
|
|
1142
|
+
"logical_agent_calls_reserved": self.logical_agent_calls,
|
|
1143
|
+
"logical_agent_calls_dispatched_to_runtime": (
|
|
1144
|
+
self.logical_agent_calls_dispatched_to_runtime
|
|
1145
|
+
),
|
|
1146
|
+
"logical_agent_calls_succeeded": self.logical_agent_calls_succeeded,
|
|
1147
|
+
"logical_agent_calls_abstained": self.logical_agent_calls_abstained,
|
|
1148
|
+
"logical_agent_calls_failed": self.logical_agent_calls_failed,
|
|
1149
|
+
"logical_agent_calls_cancelled_before_dispatch": (
|
|
1150
|
+
self.logical_agent_calls_cancelled_before_dispatch
|
|
1151
|
+
),
|
|
1152
|
+
"logical_agent_calls_cancelled_after_dispatch": (
|
|
1153
|
+
self.logical_agent_calls_cancelled_after_dispatch
|
|
1154
|
+
),
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
@property
|
|
1158
|
+
def counters_sha256(self) -> str:
|
|
1159
|
+
return _hash(_COUNTERS_DOMAIN, self.to_record())
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
class CampaignExecutionStatus(str, Enum):
|
|
1163
|
+
COMPLETED = "completed"
|
|
1164
|
+
DEGRADED = "degraded"
|
|
1165
|
+
FAILED = "failed"
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
@dataclass(frozen=True, slots=True)
|
|
1169
|
+
class CampaignFinalizationRequest:
|
|
1170
|
+
preparation_sha256: str
|
|
1171
|
+
runtime_start_receipt_sha256: str
|
|
1172
|
+
status: CampaignExecutionStatus
|
|
1173
|
+
counters: CampaignExecutionCounters
|
|
1174
|
+
stage_receipt_sha256s: tuple[str, ...]
|
|
1175
|
+
reflection_receipt_sha256s: tuple[str, ...]
|
|
1176
|
+
reflection_cancellation_receipt_sha256s: tuple[str, ...]
|
|
1177
|
+
test_admission_receipt_sha256s: tuple[str, ...]
|
|
1178
|
+
tail_drain_receipt_sha256: str | None
|
|
1179
|
+
last_durable_event_sha256: str | None
|
|
1180
|
+
failure_type: str | None
|
|
1181
|
+
|
|
1182
|
+
def __post_init__(self) -> None:
|
|
1183
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
1184
|
+
require_sha256(
|
|
1185
|
+
self.runtime_start_receipt_sha256,
|
|
1186
|
+
"runtime_start_receipt_sha256",
|
|
1187
|
+
)
|
|
1188
|
+
if type(self.status) is not CampaignExecutionStatus:
|
|
1189
|
+
raise TypeError("status must be CampaignExecutionStatus")
|
|
1190
|
+
if type(self.counters) is not CampaignExecutionCounters:
|
|
1191
|
+
raise TypeError("counters must be exact")
|
|
1192
|
+
CampaignExecutionCounters.__post_init__(self.counters)
|
|
1193
|
+
for name in (
|
|
1194
|
+
"stage_receipt_sha256s",
|
|
1195
|
+
"reflection_receipt_sha256s",
|
|
1196
|
+
"reflection_cancellation_receipt_sha256s",
|
|
1197
|
+
"test_admission_receipt_sha256s",
|
|
1198
|
+
):
|
|
1199
|
+
values = getattr(self, name)
|
|
1200
|
+
if type(values) is not tuple or any(
|
|
1201
|
+
type(value) is not str for value in values
|
|
1202
|
+
):
|
|
1203
|
+
raise TypeError(f"{name} must be an exact string tuple")
|
|
1204
|
+
for value in values:
|
|
1205
|
+
require_sha256(value, name[:-1])
|
|
1206
|
+
for name in (
|
|
1207
|
+
"tail_drain_receipt_sha256",
|
|
1208
|
+
"last_durable_event_sha256",
|
|
1209
|
+
):
|
|
1210
|
+
value = getattr(self, name)
|
|
1211
|
+
if value is not None:
|
|
1212
|
+
require_sha256(value, name)
|
|
1213
|
+
if self.status is CampaignExecutionStatus.COMPLETED:
|
|
1214
|
+
if self.failure_type is not None:
|
|
1215
|
+
raise ValueError("completed finalization cannot carry failure_type")
|
|
1216
|
+
elif type(self.failure_type) is not str or not self.failure_type:
|
|
1217
|
+
raise ValueError("failed finalization requires failure_type")
|
|
1218
|
+
|
|
1219
|
+
def to_record(self) -> dict[str, object]:
|
|
1220
|
+
self.__post_init__()
|
|
1221
|
+
return {
|
|
1222
|
+
"preparation_sha256": self.preparation_sha256,
|
|
1223
|
+
"runtime_start_receipt_sha256": self.runtime_start_receipt_sha256,
|
|
1224
|
+
"status": self.status.value,
|
|
1225
|
+
"counters": self.counters.to_record(),
|
|
1226
|
+
"stage_receipt_sha256s": list(self.stage_receipt_sha256s),
|
|
1227
|
+
"reflection_receipt_sha256s": list(self.reflection_receipt_sha256s),
|
|
1228
|
+
"reflection_cancellation_receipt_sha256s": list(
|
|
1229
|
+
self.reflection_cancellation_receipt_sha256s
|
|
1230
|
+
),
|
|
1231
|
+
"test_admission_receipt_sha256s": list(self.test_admission_receipt_sha256s),
|
|
1232
|
+
"tail_drain_receipt_sha256": self.tail_drain_receipt_sha256,
|
|
1233
|
+
"last_durable_event_sha256": self.last_durable_event_sha256,
|
|
1234
|
+
"failure_type": self.failure_type,
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
@property
|
|
1238
|
+
def request_sha256(self) -> str:
|
|
1239
|
+
return _hash(_FINALIZATION_REQUEST_DOMAIN, self.to_record())
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
@dataclass(frozen=True, slots=True)
|
|
1243
|
+
class CampaignFinalizationReceipt:
|
|
1244
|
+
request_sha256: str
|
|
1245
|
+
preparation_sha256: str
|
|
1246
|
+
status: CampaignExecutionStatus
|
|
1247
|
+
evidence: FrozenJsonObject
|
|
1248
|
+
|
|
1249
|
+
def __post_init__(self) -> None:
|
|
1250
|
+
require_sha256(self.request_sha256, "request_sha256")
|
|
1251
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
1252
|
+
if type(self.status) is not CampaignExecutionStatus:
|
|
1253
|
+
raise TypeError("status must be CampaignExecutionStatus")
|
|
1254
|
+
_frozen(self.evidence, name="evidence")
|
|
1255
|
+
|
|
1256
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1257
|
+
self.__post_init__()
|
|
1258
|
+
return {
|
|
1259
|
+
"request_sha256": self.request_sha256,
|
|
1260
|
+
"preparation_sha256": self.preparation_sha256,
|
|
1261
|
+
"status": self.status.value,
|
|
1262
|
+
"evidence": thaw_json(self.evidence),
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
@property
|
|
1266
|
+
def receipt_sha256(self) -> str:
|
|
1267
|
+
return _hash(_FINALIZATION_RECEIPT_DOMAIN, self._unsigned_record())
|
|
1268
|
+
|
|
1269
|
+
def to_record(self) -> dict[str, object]:
|
|
1270
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
@dataclass(frozen=True, slots=True)
|
|
1274
|
+
class CampaignCleanupRequest:
|
|
1275
|
+
preparation_sha256: str
|
|
1276
|
+
runtime_start_receipt_sha256: str
|
|
1277
|
+
status: CampaignExecutionStatus
|
|
1278
|
+
finalization_receipt_sha256: str | None
|
|
1279
|
+
failure_type: str | None
|
|
1280
|
+
|
|
1281
|
+
def __post_init__(self) -> None:
|
|
1282
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
1283
|
+
require_sha256(
|
|
1284
|
+
self.runtime_start_receipt_sha256,
|
|
1285
|
+
"runtime_start_receipt_sha256",
|
|
1286
|
+
)
|
|
1287
|
+
if type(self.status) is not CampaignExecutionStatus:
|
|
1288
|
+
raise TypeError("status must be CampaignExecutionStatus")
|
|
1289
|
+
if self.finalization_receipt_sha256 is not None:
|
|
1290
|
+
require_sha256(
|
|
1291
|
+
self.finalization_receipt_sha256,
|
|
1292
|
+
"finalization_receipt_sha256",
|
|
1293
|
+
)
|
|
1294
|
+
if self.status is CampaignExecutionStatus.COMPLETED:
|
|
1295
|
+
if self.failure_type is not None:
|
|
1296
|
+
raise ValueError("completed cleanup cannot carry failure_type")
|
|
1297
|
+
elif type(self.failure_type) is not str or not self.failure_type:
|
|
1298
|
+
raise ValueError("failed cleanup requires failure_type")
|
|
1299
|
+
|
|
1300
|
+
def to_record(self) -> dict[str, object]:
|
|
1301
|
+
self.__post_init__()
|
|
1302
|
+
return {
|
|
1303
|
+
"preparation_sha256": self.preparation_sha256,
|
|
1304
|
+
"runtime_start_receipt_sha256": self.runtime_start_receipt_sha256,
|
|
1305
|
+
"status": self.status.value,
|
|
1306
|
+
"finalization_receipt_sha256": self.finalization_receipt_sha256,
|
|
1307
|
+
"failure_type": self.failure_type,
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
@property
|
|
1311
|
+
def request_sha256(self) -> str:
|
|
1312
|
+
return _hash(_CLEANUP_REQUEST_DOMAIN, self.to_record())
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
@dataclass(frozen=True, slots=True)
|
|
1316
|
+
class CampaignCleanupReceipt:
|
|
1317
|
+
request_sha256: str
|
|
1318
|
+
preparation_sha256: str
|
|
1319
|
+
released: bool
|
|
1320
|
+
evidence: FrozenJsonObject
|
|
1321
|
+
|
|
1322
|
+
def __post_init__(self) -> None:
|
|
1323
|
+
require_sha256(self.request_sha256, "request_sha256")
|
|
1324
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
1325
|
+
if type(self.released) is not bool:
|
|
1326
|
+
raise TypeError("released must be an exact bool")
|
|
1327
|
+
_frozen(self.evidence, name="evidence")
|
|
1328
|
+
|
|
1329
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1330
|
+
self.__post_init__()
|
|
1331
|
+
return {
|
|
1332
|
+
"request_sha256": self.request_sha256,
|
|
1333
|
+
"preparation_sha256": self.preparation_sha256,
|
|
1334
|
+
"released": self.released,
|
|
1335
|
+
"evidence": thaw_json(self.evidence),
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
@property
|
|
1339
|
+
def receipt_sha256(self) -> str:
|
|
1340
|
+
return _hash(_CLEANUP_RECEIPT_DOMAIN, self._unsigned_record())
|
|
1341
|
+
|
|
1342
|
+
def to_record(self) -> dict[str, object]:
|
|
1343
|
+
return {**self._unsigned_record(), "receipt_sha256": self.receipt_sha256}
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
class CampaignExecutionEventKind(str, Enum):
|
|
1347
|
+
EXECUTION_STARTED = "execution_started"
|
|
1348
|
+
ARCHIVE_UTILITY_FROZEN = "archive_utility_frozen"
|
|
1349
|
+
STAGE_SEALED = "stage_sealed"
|
|
1350
|
+
REFLECTION_LAUNCHED = "reflection_launched"
|
|
1351
|
+
REFLECTION_COMPLETED = "reflection_completed"
|
|
1352
|
+
REFLECTION_ABSTAINED = "reflection_abstained"
|
|
1353
|
+
REFLECTION_FAILED = "reflection_failed"
|
|
1354
|
+
REFLECTION_CANCELLED = "reflection_cancelled"
|
|
1355
|
+
REFLECTION_ADMITTED_FOR_TESTING = "reflection_admitted_for_testing"
|
|
1356
|
+
REFLECTION_TAIL_DRAINED = "reflection_tail_drained"
|
|
1357
|
+
EXECUTION_DEGRADED = "execution_degraded"
|
|
1358
|
+
EXECUTION_FAILED = "execution_failed"
|
|
1359
|
+
EXECUTION_FINALIZED = "execution_finalized"
|
|
1360
|
+
RUNTIME_CLEANED = "runtime_cleaned"
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
@dataclass(frozen=True, slots=True)
|
|
1364
|
+
class CampaignExecutionEvent:
|
|
1365
|
+
preparation_sha256: str
|
|
1366
|
+
sequence: int
|
|
1367
|
+
kind: CampaignExecutionEventKind
|
|
1368
|
+
previous_event_sha256: str | None
|
|
1369
|
+
payload: FrozenJsonObject
|
|
1370
|
+
|
|
1371
|
+
def __post_init__(self) -> None:
|
|
1372
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
1373
|
+
if type(self.sequence) is not int or self.sequence <= 0:
|
|
1374
|
+
raise ValueError("event sequence must be positive")
|
|
1375
|
+
if type(self.kind) is not CampaignExecutionEventKind:
|
|
1376
|
+
raise TypeError("event kind must be CampaignExecutionEventKind")
|
|
1377
|
+
if self.previous_event_sha256 is not None:
|
|
1378
|
+
require_sha256(self.previous_event_sha256, "previous_event_sha256")
|
|
1379
|
+
if self.sequence == 1 and self.previous_event_sha256 is not None:
|
|
1380
|
+
raise ValueError("first event cannot name a predecessor")
|
|
1381
|
+
if self.sequence > 1 and self.previous_event_sha256 is None:
|
|
1382
|
+
raise ValueError("later event requires a predecessor")
|
|
1383
|
+
_frozen(self.payload, name="payload")
|
|
1384
|
+
|
|
1385
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1386
|
+
self.__post_init__()
|
|
1387
|
+
return {
|
|
1388
|
+
"preparation_sha256": self.preparation_sha256,
|
|
1389
|
+
"sequence": self.sequence,
|
|
1390
|
+
"kind": self.kind.value,
|
|
1391
|
+
"previous_event_sha256": self.previous_event_sha256,
|
|
1392
|
+
"payload": thaw_json(self.payload),
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
@property
|
|
1396
|
+
def event_sha256(self) -> str:
|
|
1397
|
+
return _hash(_EVENT_DOMAIN, self._unsigned_record())
|
|
1398
|
+
|
|
1399
|
+
def to_record(self) -> dict[str, object]:
|
|
1400
|
+
return {**self._unsigned_record(), "event_sha256": self.event_sha256}
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
@dataclass(frozen=True, slots=True)
|
|
1404
|
+
class CampaignJournalAck:
|
|
1405
|
+
event_sha256: str
|
|
1406
|
+
durable: bool
|
|
1407
|
+
|
|
1408
|
+
def __post_init__(self) -> None:
|
|
1409
|
+
require_sha256(self.event_sha256, "event_sha256")
|
|
1410
|
+
if self.durable is not True:
|
|
1411
|
+
raise ValueError("campaign journal acknowledgement must be durable")
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
@runtime_checkable
|
|
1415
|
+
class CampaignStageRuntimePort(Protocol):
|
|
1416
|
+
async def snapshot_archive(
|
|
1417
|
+
self,
|
|
1418
|
+
request: CampaignArchiveCutoffRequest,
|
|
1419
|
+
) -> CampaignArchiveCutoffReceipt: ...
|
|
1420
|
+
|
|
1421
|
+
async def execute_stage(
|
|
1422
|
+
self,
|
|
1423
|
+
request: CampaignStageRequest,
|
|
1424
|
+
) -> CampaignStageReceipt: ...
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
@runtime_checkable
|
|
1428
|
+
class CampaignReflectionRuntimePort(Protocol):
|
|
1429
|
+
async def reflect(
|
|
1430
|
+
self,
|
|
1431
|
+
request: CampaignReflectionRequest,
|
|
1432
|
+
) -> CampaignReflectionReceipt: ...
|
|
1433
|
+
|
|
1434
|
+
async def admit_for_testing(
|
|
1435
|
+
self,
|
|
1436
|
+
request: CampaignReflectionTestAdmissionRequest,
|
|
1437
|
+
) -> CampaignReflectionTestAdmissionReceipt: ...
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
@runtime_checkable
|
|
1441
|
+
class CampaignExecutionLifecyclePort(Protocol):
|
|
1442
|
+
"""Own runtime resources around one scheduler execution.
|
|
1443
|
+
|
|
1444
|
+
``start`` is transaction-like: if it raises before returning a valid start
|
|
1445
|
+
receipt, it must release anything it acquired. After a valid receipt is
|
|
1446
|
+
returned, the scheduler calls ``finalize`` and ``cleanup`` on every success,
|
|
1447
|
+
stage failure, receipt rejection, cancellation, or durable-journal failure.
|
|
1448
|
+
``cleanup`` must tolerate a failed ``finalize`` request.
|
|
1449
|
+
"""
|
|
1450
|
+
|
|
1451
|
+
async def start(
|
|
1452
|
+
self,
|
|
1453
|
+
prepared: PreparedEvolutionCampaign,
|
|
1454
|
+
) -> CampaignExecutionStartReceipt: ...
|
|
1455
|
+
|
|
1456
|
+
async def finalize(
|
|
1457
|
+
self,
|
|
1458
|
+
request: CampaignFinalizationRequest,
|
|
1459
|
+
) -> CampaignFinalizationReceipt: ...
|
|
1460
|
+
|
|
1461
|
+
async def cleanup(
|
|
1462
|
+
self,
|
|
1463
|
+
request: CampaignCleanupRequest,
|
|
1464
|
+
) -> CampaignCleanupReceipt: ...
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
@runtime_checkable
|
|
1468
|
+
class CampaignExecutionJournalPort(Protocol):
|
|
1469
|
+
async def append(self, event: CampaignExecutionEvent) -> CampaignJournalAck: ...
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
@dataclass(frozen=True, slots=True)
|
|
1473
|
+
class CampaignExecutionResult:
|
|
1474
|
+
preparation_sha256: str
|
|
1475
|
+
runtime_start_receipt_sha256: str
|
|
1476
|
+
stage_receipts: tuple[CampaignStageReceipt, ...]
|
|
1477
|
+
reflection_receipts: tuple[CampaignReflectionReceipt, ...]
|
|
1478
|
+
reflection_cancellation_receipts: tuple[CampaignReflectionCancellationReceipt, ...]
|
|
1479
|
+
test_admission_receipts: tuple[CampaignReflectionTestAdmissionReceipt, ...]
|
|
1480
|
+
tail_drain_receipt: CampaignTailDrainReceipt | None
|
|
1481
|
+
counters: CampaignExecutionCounters
|
|
1482
|
+
finalization_receipt: CampaignFinalizationReceipt
|
|
1483
|
+
cleanup_receipt: CampaignCleanupReceipt
|
|
1484
|
+
durable_event_sha256s: tuple[str, ...]
|
|
1485
|
+
|
|
1486
|
+
def __post_init__(self) -> None:
|
|
1487
|
+
require_sha256(self.preparation_sha256, "preparation_sha256")
|
|
1488
|
+
require_sha256(
|
|
1489
|
+
self.runtime_start_receipt_sha256,
|
|
1490
|
+
"runtime_start_receipt_sha256",
|
|
1491
|
+
)
|
|
1492
|
+
if type(self.stage_receipts) is not tuple or not self.stage_receipts:
|
|
1493
|
+
raise ValueError("execution result requires stage receipts")
|
|
1494
|
+
if any(
|
|
1495
|
+
type(value) is not CampaignStageReceipt for value in self.stage_receipts
|
|
1496
|
+
):
|
|
1497
|
+
raise TypeError("stage_receipts must contain exact receipts")
|
|
1498
|
+
if type(self.reflection_receipts) is not tuple or any(
|
|
1499
|
+
type(value) is not CampaignReflectionReceipt
|
|
1500
|
+
for value in self.reflection_receipts
|
|
1501
|
+
):
|
|
1502
|
+
raise TypeError("reflection_receipts must contain exact receipts")
|
|
1503
|
+
if type(self.reflection_cancellation_receipts) is not tuple or any(
|
|
1504
|
+
type(value) is not CampaignReflectionCancellationReceipt
|
|
1505
|
+
for value in self.reflection_cancellation_receipts
|
|
1506
|
+
):
|
|
1507
|
+
raise TypeError(
|
|
1508
|
+
"reflection_cancellation_receipts must contain exact receipts"
|
|
1509
|
+
)
|
|
1510
|
+
if type(self.test_admission_receipts) is not tuple or any(
|
|
1511
|
+
type(value) is not CampaignReflectionTestAdmissionReceipt
|
|
1512
|
+
for value in self.test_admission_receipts
|
|
1513
|
+
):
|
|
1514
|
+
raise TypeError("test_admission_receipts must contain exact receipts")
|
|
1515
|
+
if (
|
|
1516
|
+
self.tail_drain_receipt is not None
|
|
1517
|
+
and type(self.tail_drain_receipt) is not CampaignTailDrainReceipt
|
|
1518
|
+
):
|
|
1519
|
+
raise TypeError("tail_drain_receipt must be exact or None")
|
|
1520
|
+
if type(self.counters) is not CampaignExecutionCounters:
|
|
1521
|
+
raise TypeError("counters must be exact")
|
|
1522
|
+
CampaignExecutionCounters.__post_init__(self.counters)
|
|
1523
|
+
if type(self.finalization_receipt) is not CampaignFinalizationReceipt:
|
|
1524
|
+
raise TypeError("finalization_receipt must be exact")
|
|
1525
|
+
if type(self.cleanup_receipt) is not CampaignCleanupReceipt:
|
|
1526
|
+
raise TypeError("cleanup_receipt must be exact")
|
|
1527
|
+
if not self.cleanup_receipt.released:
|
|
1528
|
+
raise ValueError("successful execution requires released runtime resources")
|
|
1529
|
+
if type(self.durable_event_sha256s) is not tuple or any(
|
|
1530
|
+
type(value) is not str for value in self.durable_event_sha256s
|
|
1531
|
+
):
|
|
1532
|
+
raise TypeError("durable event hashes must be an exact string tuple")
|
|
1533
|
+
for value in self.durable_event_sha256s:
|
|
1534
|
+
require_sha256(value, "durable_event_sha256")
|
|
1535
|
+
|
|
1536
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
1537
|
+
self.__post_init__()
|
|
1538
|
+
return {
|
|
1539
|
+
"preparation_sha256": self.preparation_sha256,
|
|
1540
|
+
"runtime_start_receipt_sha256": self.runtime_start_receipt_sha256,
|
|
1541
|
+
"stage_receipt_sha256s": [
|
|
1542
|
+
value.receipt_sha256 for value in self.stage_receipts
|
|
1543
|
+
],
|
|
1544
|
+
"reflection_receipt_sha256s": [
|
|
1545
|
+
value.receipt_sha256 for value in self.reflection_receipts
|
|
1546
|
+
],
|
|
1547
|
+
"reflection_cancellation_receipt_sha256s": [
|
|
1548
|
+
value.receipt_sha256 for value in self.reflection_cancellation_receipts
|
|
1549
|
+
],
|
|
1550
|
+
"test_admission_receipt_sha256s": [
|
|
1551
|
+
value.receipt_sha256 for value in self.test_admission_receipts
|
|
1552
|
+
],
|
|
1553
|
+
"tail_drain_receipt_sha256": (
|
|
1554
|
+
None
|
|
1555
|
+
if self.tail_drain_receipt is None
|
|
1556
|
+
else self.tail_drain_receipt.receipt_sha256
|
|
1557
|
+
),
|
|
1558
|
+
"counters": self.counters.to_record(),
|
|
1559
|
+
"finalization_receipt_sha256": self.finalization_receipt.receipt_sha256,
|
|
1560
|
+
"cleanup_receipt_sha256": self.cleanup_receipt.receipt_sha256,
|
|
1561
|
+
"durable_event_sha256s": list(self.durable_event_sha256s),
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
@property
|
|
1565
|
+
def result_sha256(self) -> str:
|
|
1566
|
+
return _hash(_RESULT_DOMAIN, self._unsigned_record())
|
|
1567
|
+
|
|
1568
|
+
def to_record(self) -> dict[str, object]:
|
|
1569
|
+
return {**self._unsigned_record(), "result_sha256": self.result_sha256}
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
@dataclass(slots=True)
|
|
1573
|
+
class EvolutionCampaignScheduler:
|
|
1574
|
+
"""Execute a prepared schedule while owning its scientific ledger."""
|
|
1575
|
+
|
|
1576
|
+
prepared: PreparedEvolutionCampaign
|
|
1577
|
+
policies: CampaignPolicies
|
|
1578
|
+
stages: CampaignStageRuntimePort
|
|
1579
|
+
reflections: CampaignReflectionRuntimePort
|
|
1580
|
+
lifecycle: CampaignExecutionLifecyclePort
|
|
1581
|
+
journal: CampaignExecutionJournalPort
|
|
1582
|
+
_has_run: bool = field(init=False, default=False)
|
|
1583
|
+
|
|
1584
|
+
def __post_init__(self) -> None:
|
|
1585
|
+
if type(self.prepared) is not PreparedEvolutionCampaign:
|
|
1586
|
+
raise TypeError("prepared must be an exact PreparedEvolutionCampaign")
|
|
1587
|
+
PreparedEvolutionCampaign.__post_init__(self.prepared)
|
|
1588
|
+
if type(self.policies) is not CampaignPolicies:
|
|
1589
|
+
raise TypeError("policies must be an exact CampaignPolicies")
|
|
1590
|
+
CampaignPolicies.__post_init__(self.policies)
|
|
1591
|
+
if self.policies.policies_sha256 != self.prepared.policies_sha256:
|
|
1592
|
+
raise ValueError("execution policies differ from prepared policies")
|
|
1593
|
+
if not isinstance(self.stages, CampaignStageRuntimePort):
|
|
1594
|
+
raise TypeError("stages must implement CampaignStageRuntimePort")
|
|
1595
|
+
if not isinstance(self.reflections, CampaignReflectionRuntimePort):
|
|
1596
|
+
raise TypeError("reflections must implement CampaignReflectionRuntimePort")
|
|
1597
|
+
if not isinstance(self.lifecycle, CampaignExecutionLifecyclePort):
|
|
1598
|
+
raise TypeError("lifecycle must implement CampaignExecutionLifecyclePort")
|
|
1599
|
+
if not isinstance(self.journal, CampaignExecutionJournalPort):
|
|
1600
|
+
raise TypeError("journal must implement CampaignExecutionJournalPort")
|
|
1601
|
+
|
|
1602
|
+
async def run(self) -> CampaignExecutionResult:
|
|
1603
|
+
"""Run every prepared generation and always finalize and clean up."""
|
|
1604
|
+
|
|
1605
|
+
self.__post_init__()
|
|
1606
|
+
if self._has_run:
|
|
1607
|
+
raise RuntimeError("campaign scheduler instances are one-shot")
|
|
1608
|
+
self._has_run = True
|
|
1609
|
+
preparation_sha256 = self.prepared.preparation_sha256
|
|
1610
|
+
start = await self.lifecycle.start(self.prepared)
|
|
1611
|
+
if type(start) is not CampaignExecutionStartReceipt:
|
|
1612
|
+
raise TypeError("lifecycle start must return CampaignExecutionStartReceipt")
|
|
1613
|
+
CampaignExecutionStartReceipt.__post_init__(start)
|
|
1614
|
+
|
|
1615
|
+
event_sequence = 0
|
|
1616
|
+
last_event_sha256: str | None = None
|
|
1617
|
+
durable_event_sha256s: list[str] = []
|
|
1618
|
+
|
|
1619
|
+
async def append_event(
|
|
1620
|
+
kind: CampaignExecutionEventKind,
|
|
1621
|
+
payload: dict[str, object],
|
|
1622
|
+
) -> None:
|
|
1623
|
+
nonlocal event_sequence, last_event_sha256
|
|
1624
|
+
event = CampaignExecutionEvent(
|
|
1625
|
+
preparation_sha256=preparation_sha256,
|
|
1626
|
+
sequence=event_sequence + 1,
|
|
1627
|
+
kind=kind,
|
|
1628
|
+
previous_event_sha256=last_event_sha256,
|
|
1629
|
+
payload=_freeze_record(payload),
|
|
1630
|
+
)
|
|
1631
|
+
ack = await self.journal.append(event)
|
|
1632
|
+
if type(ack) is not CampaignJournalAck:
|
|
1633
|
+
raise TypeError("journal must return CampaignJournalAck")
|
|
1634
|
+
CampaignJournalAck.__post_init__(ack)
|
|
1635
|
+
if ack.event_sha256 != event.event_sha256:
|
|
1636
|
+
raise CampaignExecutionContractError(
|
|
1637
|
+
"journal acknowledged a foreign event"
|
|
1638
|
+
)
|
|
1639
|
+
event_sequence = event.sequence
|
|
1640
|
+
last_event_sha256 = event.event_sha256
|
|
1641
|
+
durable_event_sha256s.append(event.event_sha256)
|
|
1642
|
+
|
|
1643
|
+
counters = CampaignExecutionCounters(
|
|
1644
|
+
generations_completed=0,
|
|
1645
|
+
candidate_occurrences=0,
|
|
1646
|
+
unique_evaluations=0,
|
|
1647
|
+
logical_agent_calls=0,
|
|
1648
|
+
)
|
|
1649
|
+
stage_receipts: list[CampaignStageReceipt] = []
|
|
1650
|
+
reflection_receipts: dict[int, CampaignReflectionReceipt] = {}
|
|
1651
|
+
reflection_cancellations: list[CampaignReflectionCancellationReceipt] = []
|
|
1652
|
+
test_admission_receipts: list[CampaignReflectionTestAdmissionReceipt] = []
|
|
1653
|
+
tail_drain: CampaignTailDrainReceipt | None = None
|
|
1654
|
+
selector_audits: list[CampaignSelectorAuditReceipt] = []
|
|
1655
|
+
test_eligible_reflections: tuple[str, ...] = ()
|
|
1656
|
+
reflection_tasks: dict[int, asyncio.Task[CampaignReflectionReceipt]] = {}
|
|
1657
|
+
reflection_requests: dict[int, CampaignReflectionRequest] = {}
|
|
1658
|
+
reflection_runtime_dispatched: set[int] = set()
|
|
1659
|
+
degraded_reflection_sources: set[int] = set()
|
|
1660
|
+
abstained_reflection_sources: set[int] = set()
|
|
1661
|
+
finalization: CampaignFinalizationReceipt | None = None
|
|
1662
|
+
cleanup: CampaignCleanupReceipt | None = None
|
|
1663
|
+
primary_error: BaseException | None = None
|
|
1664
|
+
|
|
1665
|
+
supervision = self.policies.reflection_supervision
|
|
1666
|
+
if type(supervision) is not CampaignReflectionSupervisionPolicy:
|
|
1667
|
+
raise TypeError("campaign has a foreign reflection supervision policy")
|
|
1668
|
+
CampaignReflectionSupervisionPolicy.__post_init__(supervision)
|
|
1669
|
+
|
|
1670
|
+
def update_call_counters(
|
|
1671
|
+
*,
|
|
1672
|
+
reserved: int = 0,
|
|
1673
|
+
dispatched: int = 0,
|
|
1674
|
+
succeeded: int = 0,
|
|
1675
|
+
abstained: int = 0,
|
|
1676
|
+
failed: int = 0,
|
|
1677
|
+
cancelled_before_dispatch: int = 0,
|
|
1678
|
+
cancelled_after_dispatch: int = 0,
|
|
1679
|
+
) -> None:
|
|
1680
|
+
nonlocal counters
|
|
1681
|
+
counters = CampaignExecutionCounters(
|
|
1682
|
+
generations_completed=counters.generations_completed,
|
|
1683
|
+
candidate_occurrences=counters.candidate_occurrences,
|
|
1684
|
+
unique_evaluations=counters.unique_evaluations,
|
|
1685
|
+
logical_agent_calls=counters.logical_agent_calls + reserved,
|
|
1686
|
+
logical_agent_calls_dispatched_to_runtime=(
|
|
1687
|
+
counters.logical_agent_calls_dispatched_to_runtime + dispatched
|
|
1688
|
+
),
|
|
1689
|
+
logical_agent_calls_succeeded=(
|
|
1690
|
+
counters.logical_agent_calls_succeeded + succeeded
|
|
1691
|
+
),
|
|
1692
|
+
logical_agent_calls_abstained=(
|
|
1693
|
+
counters.logical_agent_calls_abstained + abstained
|
|
1694
|
+
),
|
|
1695
|
+
logical_agent_calls_failed=(
|
|
1696
|
+
counters.logical_agent_calls_failed + failed
|
|
1697
|
+
),
|
|
1698
|
+
logical_agent_calls_cancelled_before_dispatch=(
|
|
1699
|
+
counters.logical_agent_calls_cancelled_before_dispatch
|
|
1700
|
+
+ cancelled_before_dispatch
|
|
1701
|
+
),
|
|
1702
|
+
logical_agent_calls_cancelled_after_dispatch=(
|
|
1703
|
+
counters.logical_agent_calls_cancelled_after_dispatch
|
|
1704
|
+
+ cancelled_after_dispatch
|
|
1705
|
+
),
|
|
1706
|
+
)
|
|
1707
|
+
|
|
1708
|
+
def failed_reflection_receipt(
|
|
1709
|
+
source_generation: int,
|
|
1710
|
+
error: BaseException,
|
|
1711
|
+
) -> CampaignReflectionReceipt:
|
|
1712
|
+
request = reflection_requests[source_generation]
|
|
1713
|
+
failure_type = type(error).__name__
|
|
1714
|
+
failure_digest = hashlib.sha256(
|
|
1715
|
+
type(error).__qualname__.encode("utf-8", errors="strict")
|
|
1716
|
+
+ b"\x00"
|
|
1717
|
+
+ str(error).encode("utf-8", errors="replace")
|
|
1718
|
+
).hexdigest()
|
|
1719
|
+
return CampaignReflectionReceipt(
|
|
1720
|
+
request_sha256=request.request_sha256,
|
|
1721
|
+
preparation_sha256=preparation_sha256,
|
|
1722
|
+
source_generation=source_generation,
|
|
1723
|
+
source_stage_receipt_sha256=(request.source_stage.receipt_sha256),
|
|
1724
|
+
logical_agent_calls=request.wave.call_count,
|
|
1725
|
+
visibility=request.wave.visibility,
|
|
1726
|
+
status=CampaignReflectionStatus.FAILED,
|
|
1727
|
+
failure_type=failure_type,
|
|
1728
|
+
quarantined_result=_freeze_record(
|
|
1729
|
+
{
|
|
1730
|
+
"schema_version": 1,
|
|
1731
|
+
"status": "failed",
|
|
1732
|
+
"failure_type": failure_type,
|
|
1733
|
+
"failure_digest_sha256": failure_digest,
|
|
1734
|
+
"approved_summary": (
|
|
1735
|
+
"reflection execution failed without publishable "
|
|
1736
|
+
"insight content"
|
|
1737
|
+
),
|
|
1738
|
+
"publishable_reflection_content": False,
|
|
1739
|
+
}
|
|
1740
|
+
),
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
async def run_reflection(
|
|
1744
|
+
request: CampaignReflectionRequest,
|
|
1745
|
+
started: asyncio.Event,
|
|
1746
|
+
) -> CampaignReflectionReceipt:
|
|
1747
|
+
source_generation = request.wave.source_generation
|
|
1748
|
+
reflection_runtime_dispatched.add(source_generation)
|
|
1749
|
+
started.set()
|
|
1750
|
+
try:
|
|
1751
|
+
return await self.reflections.reflect(request)
|
|
1752
|
+
except asyncio.CancelledError:
|
|
1753
|
+
raise
|
|
1754
|
+
except Exception as error:
|
|
1755
|
+
return failed_reflection_receipt(source_generation, error)
|
|
1756
|
+
|
|
1757
|
+
async def settle_reflection(
|
|
1758
|
+
source_generation: int,
|
|
1759
|
+
receipt: CampaignReflectionReceipt,
|
|
1760
|
+
) -> CampaignReflectionReceipt:
|
|
1761
|
+
if source_generation in reflection_receipts:
|
|
1762
|
+
if reflection_receipts[source_generation] != receipt:
|
|
1763
|
+
raise CampaignExecutionContractError(
|
|
1764
|
+
"reflection source settled with two different receipts"
|
|
1765
|
+
)
|
|
1766
|
+
return receipt
|
|
1767
|
+
request = reflection_requests[source_generation]
|
|
1768
|
+
if type(receipt) is not CampaignReflectionReceipt:
|
|
1769
|
+
raise TypeError("reflection runtime must return exact receipt")
|
|
1770
|
+
CampaignReflectionReceipt.__post_init__(receipt)
|
|
1771
|
+
if (
|
|
1772
|
+
receipt.request_sha256 != request.request_sha256
|
|
1773
|
+
or receipt.preparation_sha256 != preparation_sha256
|
|
1774
|
+
or receipt.source_generation != source_generation
|
|
1775
|
+
or receipt.source_stage_receipt_sha256
|
|
1776
|
+
!= request.source_stage.receipt_sha256
|
|
1777
|
+
or receipt.logical_agent_calls != request.wave.call_count
|
|
1778
|
+
):
|
|
1779
|
+
raise CampaignExecutionContractError(
|
|
1780
|
+
"reflection receipt differs from its launch request"
|
|
1781
|
+
)
|
|
1782
|
+
reflection_receipts[source_generation] = receipt
|
|
1783
|
+
reflection_tasks.pop(source_generation, None)
|
|
1784
|
+
if receipt.status is CampaignReflectionStatus.COMPLETED:
|
|
1785
|
+
update_call_counters(succeeded=receipt.logical_agent_calls)
|
|
1786
|
+
kind = CampaignExecutionEventKind.REFLECTION_COMPLETED
|
|
1787
|
+
elif receipt.status is CampaignReflectionStatus.ABSTAINED:
|
|
1788
|
+
update_call_counters(abstained=receipt.logical_agent_calls)
|
|
1789
|
+
abstained_reflection_sources.add(source_generation)
|
|
1790
|
+
kind = CampaignExecutionEventKind.REFLECTION_ABSTAINED
|
|
1791
|
+
else:
|
|
1792
|
+
update_call_counters(failed=receipt.logical_agent_calls)
|
|
1793
|
+
kind = CampaignExecutionEventKind.REFLECTION_FAILED
|
|
1794
|
+
await append_event(
|
|
1795
|
+
kind,
|
|
1796
|
+
{"reflection_receipt": receipt.to_record()},
|
|
1797
|
+
)
|
|
1798
|
+
return receipt
|
|
1799
|
+
|
|
1800
|
+
async def record_reflection_cancellation(
|
|
1801
|
+
source_generation: int,
|
|
1802
|
+
*,
|
|
1803
|
+
reason: str,
|
|
1804
|
+
) -> CampaignReflectionCancellationReceipt:
|
|
1805
|
+
request = reflection_requests[source_generation]
|
|
1806
|
+
dispatched = source_generation in reflection_runtime_dispatched
|
|
1807
|
+
receipt = CampaignReflectionCancellationReceipt(
|
|
1808
|
+
request_sha256=request.request_sha256,
|
|
1809
|
+
preparation_sha256=preparation_sha256,
|
|
1810
|
+
source_generation=source_generation,
|
|
1811
|
+
source_stage_receipt_sha256=request.source_stage.receipt_sha256,
|
|
1812
|
+
logical_agent_calls_reserved=request.wave.call_count,
|
|
1813
|
+
dispatched_to_runtime=dispatched,
|
|
1814
|
+
cancellation_reason=reason,
|
|
1815
|
+
)
|
|
1816
|
+
reflection_cancellations.append(receipt)
|
|
1817
|
+
if dispatched:
|
|
1818
|
+
update_call_counters(cancelled_after_dispatch=request.wave.call_count)
|
|
1819
|
+
else:
|
|
1820
|
+
update_call_counters(cancelled_before_dispatch=request.wave.call_count)
|
|
1821
|
+
await append_event(
|
|
1822
|
+
CampaignExecutionEventKind.REFLECTION_CANCELLED,
|
|
1823
|
+
{"reflection_cancellation_receipt": receipt.to_record()},
|
|
1824
|
+
)
|
|
1825
|
+
return receipt
|
|
1826
|
+
|
|
1827
|
+
async def settle_sources(
|
|
1828
|
+
sources: tuple[int, ...],
|
|
1829
|
+
*,
|
|
1830
|
+
wait_for_all: bool,
|
|
1831
|
+
) -> tuple[CampaignReflectionReceipt, ...]:
|
|
1832
|
+
pending_sources = tuple(
|
|
1833
|
+
source
|
|
1834
|
+
for source in sources
|
|
1835
|
+
if source not in reflection_receipts
|
|
1836
|
+
and source in reflection_tasks
|
|
1837
|
+
and (wait_for_all or reflection_tasks[source].done())
|
|
1838
|
+
)
|
|
1839
|
+
if pending_sources:
|
|
1840
|
+
raw = await asyncio.gather(
|
|
1841
|
+
*(reflection_tasks[source] for source in pending_sources),
|
|
1842
|
+
return_exceptions=True,
|
|
1843
|
+
)
|
|
1844
|
+
for source, value in zip(pending_sources, raw, strict=True):
|
|
1845
|
+
if isinstance(value, asyncio.CancelledError):
|
|
1846
|
+
await record_reflection_cancellation(
|
|
1847
|
+
source,
|
|
1848
|
+
reason="scheduler_task_cancelled",
|
|
1849
|
+
)
|
|
1850
|
+
reflection_tasks.pop(source, None)
|
|
1851
|
+
elif isinstance(value, BaseException):
|
|
1852
|
+
await settle_reflection(
|
|
1853
|
+
source,
|
|
1854
|
+
failed_reflection_receipt(source, value),
|
|
1855
|
+
)
|
|
1856
|
+
else:
|
|
1857
|
+
await settle_reflection(source, value)
|
|
1858
|
+
return tuple(
|
|
1859
|
+
reflection_receipts[source]
|
|
1860
|
+
for source in sources
|
|
1861
|
+
if source in reflection_receipts
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1864
|
+
def failed_sources(sources: tuple[int, ...]) -> tuple[int, ...]:
|
|
1865
|
+
return tuple(
|
|
1866
|
+
source
|
|
1867
|
+
for source in sources
|
|
1868
|
+
if source in reflection_receipts
|
|
1869
|
+
and reflection_receipts[source].status
|
|
1870
|
+
is CampaignReflectionStatus.FAILED
|
|
1871
|
+
)
|
|
1872
|
+
|
|
1873
|
+
def abstained_sources(sources: tuple[int, ...]) -> tuple[int, ...]:
|
|
1874
|
+
return tuple(
|
|
1875
|
+
source
|
|
1876
|
+
for source in sources
|
|
1877
|
+
if source in reflection_receipts
|
|
1878
|
+
and reflection_receipts[source].status
|
|
1879
|
+
is CampaignReflectionStatus.ABSTAINED
|
|
1880
|
+
)
|
|
1881
|
+
|
|
1882
|
+
async def settle_stage_boundary() -> None:
|
|
1883
|
+
# Give callbacks made runnable by the just-sealed stage one turn to
|
|
1884
|
+
# publish their terminal task state before the next expensive stage.
|
|
1885
|
+
await asyncio.sleep(0)
|
|
1886
|
+
sources = tuple(sorted(reflection_tasks))
|
|
1887
|
+
await settle_sources(sources, wait_for_all=False)
|
|
1888
|
+
failures = failed_sources(tuple(sorted(reflection_receipts)))
|
|
1889
|
+
if not failures:
|
|
1890
|
+
return
|
|
1891
|
+
if supervision.mode is ReflectionFailureMode.FAIL_AT_NEXT_STAGE_BOUNDARY:
|
|
1892
|
+
raise CampaignExecutionContractError(
|
|
1893
|
+
"reflection failed at the next durable stage boundary"
|
|
1894
|
+
)
|
|
1895
|
+
if supervision.mode is ReflectionFailureMode.BEST_EFFORT_DEGRADED:
|
|
1896
|
+
degraded_reflection_sources.update(failures)
|
|
1897
|
+
|
|
1898
|
+
async def join_barrier(barrier: CampaignPromotionBarrier) -> None:
|
|
1899
|
+
nonlocal test_eligible_reflections
|
|
1900
|
+
sources = barrier.reflection_source_generations
|
|
1901
|
+
if any(
|
|
1902
|
+
source not in reflection_tasks and source not in reflection_receipts
|
|
1903
|
+
for source in sources
|
|
1904
|
+
):
|
|
1905
|
+
raise CampaignExecutionContractError(
|
|
1906
|
+
"test-admission barrier lacks a launched reflection"
|
|
1907
|
+
)
|
|
1908
|
+
await settle_sources(sources, wait_for_all=True)
|
|
1909
|
+
joined = tuple(reflection_receipts[source] for source in sources)
|
|
1910
|
+
failures = failed_sources(sources)
|
|
1911
|
+
if failures:
|
|
1912
|
+
for source in sources:
|
|
1913
|
+
reflection_tasks.pop(source, None)
|
|
1914
|
+
reflection_requests.pop(source, None)
|
|
1915
|
+
if supervision.mode is ReflectionFailureMode.BEST_EFFORT_DEGRADED:
|
|
1916
|
+
degraded_reflection_sources.update(failures)
|
|
1917
|
+
return
|
|
1918
|
+
raise CampaignExecutionContractError(
|
|
1919
|
+
"reflection block closed with one or more failed receipts"
|
|
1920
|
+
)
|
|
1921
|
+
abstentions = abstained_sources(sources)
|
|
1922
|
+
if abstentions:
|
|
1923
|
+
# A block containing E0 evidence has no publishable reflection
|
|
1924
|
+
# content. Preserve every receipt, but do not manufacture a
|
|
1925
|
+
# partial admission whose estimand differs from the prepared
|
|
1926
|
+
# barrier.
|
|
1927
|
+
for source in sources:
|
|
1928
|
+
reflection_tasks.pop(source, None)
|
|
1929
|
+
reflection_requests.pop(source, None)
|
|
1930
|
+
return
|
|
1931
|
+
request = CampaignReflectionTestAdmissionRequest(
|
|
1932
|
+
preparation_sha256=preparation_sha256,
|
|
1933
|
+
runtime_start_receipt_sha256=start.receipt_sha256,
|
|
1934
|
+
barrier=barrier,
|
|
1935
|
+
reflections=joined,
|
|
1936
|
+
previously_test_eligible_reflection_receipt_sha256s=(
|
|
1937
|
+
test_eligible_reflections
|
|
1938
|
+
),
|
|
1939
|
+
admission_cutoff_stage=(
|
|
1940
|
+
stage_receipts[barrier.generation - 1]
|
|
1941
|
+
if barrier.generation
|
|
1942
|
+
> barrier.reflection_source_generations[-1]
|
|
1943
|
+
else None
|
|
1944
|
+
),
|
|
1945
|
+
)
|
|
1946
|
+
receipt = await self.reflections.admit_for_testing(request)
|
|
1947
|
+
if type(receipt) is not CampaignReflectionTestAdmissionReceipt:
|
|
1948
|
+
raise TypeError("test admission runtime must return exact receipt")
|
|
1949
|
+
CampaignReflectionTestAdmissionReceipt.__post_init__(receipt)
|
|
1950
|
+
admitted = tuple(sorted(value.receipt_sha256 for value in joined))
|
|
1951
|
+
expected_eligible = tuple(sorted((*test_eligible_reflections, *admitted)))
|
|
1952
|
+
if (
|
|
1953
|
+
receipt.request_sha256 != request.request_sha256
|
|
1954
|
+
or receipt.preparation_sha256 != preparation_sha256
|
|
1955
|
+
or receipt.barrier_generation != barrier.generation
|
|
1956
|
+
or receipt.admitted_reflection_receipt_sha256s != admitted
|
|
1957
|
+
or receipt.test_eligible_reflection_receipt_sha256s != expected_eligible
|
|
1958
|
+
or receipt.lifecycle_promoted
|
|
1959
|
+
):
|
|
1960
|
+
raise CampaignExecutionContractError(
|
|
1961
|
+
"test-admission receipt differs from its barrier"
|
|
1962
|
+
)
|
|
1963
|
+
test_admission_receipts.append(receipt)
|
|
1964
|
+
test_eligible_reflections = expected_eligible
|
|
1965
|
+
await append_event(
|
|
1966
|
+
CampaignExecutionEventKind.REFLECTION_ADMITTED_FOR_TESTING,
|
|
1967
|
+
{"test_admission_receipt": receipt.to_record()},
|
|
1968
|
+
)
|
|
1969
|
+
for source in sources:
|
|
1970
|
+
reflection_tasks.pop(source, None)
|
|
1971
|
+
reflection_requests.pop(source, None)
|
|
1972
|
+
|
|
1973
|
+
try:
|
|
1974
|
+
if (
|
|
1975
|
+
start.preparation_sha256 != preparation_sha256
|
|
1976
|
+
or start.runtime_preflight_receipt_sha256
|
|
1977
|
+
!= self.prepared.runtime_receipt.receipt_sha256
|
|
1978
|
+
or start.seed_batch_sha256 != self.prepared.seeds.batch_sha256
|
|
1979
|
+
):
|
|
1980
|
+
raise CampaignExecutionContractError(
|
|
1981
|
+
"runtime start receipt differs from preparation"
|
|
1982
|
+
)
|
|
1983
|
+
expected_seeds = tuple(
|
|
1984
|
+
(value.seed_id, value.configuration_sha256)
|
|
1985
|
+
for value in self.prepared.seeds.seeds
|
|
1986
|
+
)
|
|
1987
|
+
observed_seeds = tuple(
|
|
1988
|
+
(value.seed_id, value.configuration_sha256)
|
|
1989
|
+
for value in start.seed_receipts
|
|
1990
|
+
)
|
|
1991
|
+
if observed_seeds != expected_seeds:
|
|
1992
|
+
raise CampaignExecutionContractError(
|
|
1993
|
+
"runtime seed accounting differs from prepared seed batch"
|
|
1994
|
+
)
|
|
1995
|
+
counters = CampaignExecutionCounters(
|
|
1996
|
+
generations_completed=0,
|
|
1997
|
+
candidate_occurrences=start.seed_occurrence_count,
|
|
1998
|
+
unique_evaluations=start.seed_unique_evaluation_count,
|
|
1999
|
+
logical_agent_calls=0,
|
|
2000
|
+
)
|
|
2001
|
+
if (
|
|
2002
|
+
counters.unique_evaluations
|
|
2003
|
+
> self.prepared.budget.max_unique_evaluations
|
|
2004
|
+
):
|
|
2005
|
+
raise CampaignExecutionContractError(
|
|
2006
|
+
"seed evaluations already exceed campaign budget"
|
|
2007
|
+
)
|
|
2008
|
+
await append_event(
|
|
2009
|
+
CampaignExecutionEventKind.EXECUTION_STARTED,
|
|
2010
|
+
{
|
|
2011
|
+
"start_receipt": start.to_record(),
|
|
2012
|
+
"initial_counters": counters.to_record(),
|
|
2013
|
+
},
|
|
2014
|
+
)
|
|
2015
|
+
if start.seed_failure_count or any(
|
|
2016
|
+
not value.evaluated or not value.valid for value in start.seed_receipts
|
|
2017
|
+
):
|
|
2018
|
+
raise CampaignExecutionContractError(
|
|
2019
|
+
"runtime start reported failed or unevaluated seeds"
|
|
2020
|
+
)
|
|
2021
|
+
barrier_by_generation = {
|
|
2022
|
+
value.generation: value
|
|
2023
|
+
for value in self.prepared.schedule.promotion_barriers
|
|
2024
|
+
}
|
|
2025
|
+
wave_by_generation = {
|
|
2026
|
+
value.source_generation: value
|
|
2027
|
+
for value in self.prepared.schedule.reflection_waves
|
|
2028
|
+
}
|
|
2029
|
+
prior_stage: CampaignStageReceipt | None = None
|
|
2030
|
+
|
|
2031
|
+
for step in self.prepared.schedule.steps:
|
|
2032
|
+
if step.generation != counters.generations_completed + 1:
|
|
2033
|
+
raise CampaignExecutionContractError(
|
|
2034
|
+
"prepared schedule is not the next generation"
|
|
2035
|
+
)
|
|
2036
|
+
expected_stage_agent_calls = (
|
|
2037
|
+
step.parent_count
|
|
2038
|
+
if step.kind is CampaignGenerationKind.PORTFOLIO
|
|
2039
|
+
else 0
|
|
2040
|
+
)
|
|
2041
|
+
if (
|
|
2042
|
+
counters.logical_agent_calls + expected_stage_agent_calls
|
|
2043
|
+
> self.prepared.budget.max_logical_llm_calls
|
|
2044
|
+
):
|
|
2045
|
+
raise CampaignExecutionContractError(
|
|
2046
|
+
"selector calls would exceed campaign budget"
|
|
2047
|
+
)
|
|
2048
|
+
cutoff_request = CampaignArchiveCutoffRequest(
|
|
2049
|
+
preparation_sha256=preparation_sha256,
|
|
2050
|
+
runtime_start_receipt_sha256=start.receipt_sha256,
|
|
2051
|
+
generation=step.generation,
|
|
2052
|
+
kind=step.kind,
|
|
2053
|
+
step_sha256=campaign_step_sha256(step),
|
|
2054
|
+
prior_stage_receipt_sha256=(
|
|
2055
|
+
None if prior_stage is None else prior_stage.receipt_sha256
|
|
2056
|
+
),
|
|
2057
|
+
)
|
|
2058
|
+
cutoff = await self.stages.snapshot_archive(cutoff_request)
|
|
2059
|
+
if type(cutoff) is not CampaignArchiveCutoffReceipt:
|
|
2060
|
+
raise TypeError("stage runtime must return exact archive cutoff")
|
|
2061
|
+
CampaignArchiveCutoffReceipt.__post_init__(cutoff)
|
|
2062
|
+
if (
|
|
2063
|
+
cutoff.request_sha256 != cutoff_request.request_sha256
|
|
2064
|
+
or cutoff.preparation_sha256 != preparation_sha256
|
|
2065
|
+
or cutoff.generation != step.generation
|
|
2066
|
+
):
|
|
2067
|
+
raise CampaignExecutionContractError(
|
|
2068
|
+
"archive cutoff receipt differs from its request"
|
|
2069
|
+
)
|
|
2070
|
+
utility = freeze_archive_utility(
|
|
2071
|
+
self.policies.archive_utility,
|
|
2072
|
+
benchmark=self.prepared.benchmark_session.benchmark,
|
|
2073
|
+
generation=step.generation,
|
|
2074
|
+
archive=cutoff.archive,
|
|
2075
|
+
)
|
|
2076
|
+
await append_event(
|
|
2077
|
+
CampaignExecutionEventKind.ARCHIVE_UTILITY_FROZEN,
|
|
2078
|
+
{
|
|
2079
|
+
"archive_cutoff": cutoff.to_record(),
|
|
2080
|
+
"archive_utility": utility.to_record(),
|
|
2081
|
+
},
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
source_portfolio: CampaignStageReceipt | None = None
|
|
2085
|
+
if step.kind is CampaignGenerationKind.RECOMBINATION:
|
|
2086
|
+
if (
|
|
2087
|
+
prior_stage is None
|
|
2088
|
+
or prior_stage.kind is not CampaignGenerationKind.PORTFOLIO
|
|
2089
|
+
or prior_stage.generation != step.source_portfolio_generation
|
|
2090
|
+
):
|
|
2091
|
+
raise CampaignExecutionContractError(
|
|
2092
|
+
"recombination lacks its exact portfolio predecessor"
|
|
2093
|
+
)
|
|
2094
|
+
source_portfolio = prior_stage
|
|
2095
|
+
stage_request = CampaignStageRequest(
|
|
2096
|
+
preparation_sha256=preparation_sha256,
|
|
2097
|
+
runtime_start_receipt_sha256=start.receipt_sha256,
|
|
2098
|
+
step=step,
|
|
2099
|
+
archive_cutoff=cutoff,
|
|
2100
|
+
archive_utility=utility,
|
|
2101
|
+
source_portfolio=source_portfolio,
|
|
2102
|
+
test_eligible_reflection_receipt_sha256s=(
|
|
2103
|
+
test_eligible_reflections
|
|
2104
|
+
),
|
|
2105
|
+
prior_selector_audit_set_sha256=selector_audit_set_sha256(
|
|
2106
|
+
tuple(selector_audits)
|
|
2107
|
+
),
|
|
2108
|
+
)
|
|
2109
|
+
stage = await self.stages.execute_stage(stage_request)
|
|
2110
|
+
if type(stage) is not CampaignStageReceipt:
|
|
2111
|
+
raise TypeError("stage runtime must return exact stage receipt")
|
|
2112
|
+
CampaignStageReceipt.__post_init__(stage)
|
|
2113
|
+
if (
|
|
2114
|
+
stage.request_sha256 != stage_request.request_sha256
|
|
2115
|
+
or stage.preparation_sha256 != preparation_sha256
|
|
2116
|
+
or stage.generation != step.generation
|
|
2117
|
+
or stage.kind is not step.kind
|
|
2118
|
+
):
|
|
2119
|
+
raise CampaignExecutionContractError(
|
|
2120
|
+
"stage receipt is out-of-order or foreign"
|
|
2121
|
+
)
|
|
2122
|
+
maximum_occurrences = step.planned_candidate_evaluations
|
|
2123
|
+
if step.kind is CampaignGenerationKind.PORTFOLIO:
|
|
2124
|
+
if stage.candidate_occurrence_count != maximum_occurrences:
|
|
2125
|
+
raise CampaignExecutionContractError(
|
|
2126
|
+
"portfolio stage did not account for every planned child"
|
|
2127
|
+
)
|
|
2128
|
+
if len(stage.selector_audits) != step.parent_count:
|
|
2129
|
+
raise CampaignExecutionContractError(
|
|
2130
|
+
"portfolio stage lacks one fresh selector audit per parent"
|
|
2131
|
+
)
|
|
2132
|
+
expected_slots = tuple(range(step.parent_count))
|
|
2133
|
+
if (
|
|
2134
|
+
tuple(value.parent_slot for value in stage.selector_audits)
|
|
2135
|
+
!= expected_slots
|
|
2136
|
+
):
|
|
2137
|
+
raise CampaignExecutionContractError(
|
|
2138
|
+
"selector audits do not cover canonical parent slots"
|
|
2139
|
+
)
|
|
2140
|
+
prior_audit_hash = selector_audit_set_sha256(tuple(selector_audits))
|
|
2141
|
+
if any(
|
|
2142
|
+
value.generation != step.generation
|
|
2143
|
+
or value.prior_audit_set_sha256 != prior_audit_hash
|
|
2144
|
+
for value in stage.selector_audits
|
|
2145
|
+
):
|
|
2146
|
+
raise CampaignExecutionContractError(
|
|
2147
|
+
"selector audit is stale or names another generation"
|
|
2148
|
+
)
|
|
2149
|
+
existing_calls = {
|
|
2150
|
+
value.selector_call_id for value in selector_audits
|
|
2151
|
+
}
|
|
2152
|
+
existing_requests = {
|
|
2153
|
+
value.request_sha256 for value in selector_audits
|
|
2154
|
+
}
|
|
2155
|
+
existing_traces = {
|
|
2156
|
+
value.trace_receipt_sha256 for value in selector_audits
|
|
2157
|
+
}
|
|
2158
|
+
if (
|
|
2159
|
+
len({value.selector_call_id for value in stage.selector_audits})
|
|
2160
|
+
!= len(stage.selector_audits)
|
|
2161
|
+
or len(
|
|
2162
|
+
{value.request_sha256 for value in stage.selector_audits}
|
|
2163
|
+
)
|
|
2164
|
+
!= len(stage.selector_audits)
|
|
2165
|
+
or len(
|
|
2166
|
+
{
|
|
2167
|
+
value.trace_receipt_sha256
|
|
2168
|
+
for value in stage.selector_audits
|
|
2169
|
+
}
|
|
2170
|
+
)
|
|
2171
|
+
!= len(stage.selector_audits)
|
|
2172
|
+
or any(
|
|
2173
|
+
value.selector_call_id in existing_calls
|
|
2174
|
+
or value.request_sha256 in existing_requests
|
|
2175
|
+
or value.trace_receipt_sha256 in existing_traces
|
|
2176
|
+
for value in stage.selector_audits
|
|
2177
|
+
)
|
|
2178
|
+
):
|
|
2179
|
+
raise CampaignExecutionContractError(
|
|
2180
|
+
"selector audit is duplicate rather than fresh"
|
|
2181
|
+
)
|
|
2182
|
+
selector_audits.extend(stage.selector_audits)
|
|
2183
|
+
else:
|
|
2184
|
+
if stage.candidate_occurrence_count > maximum_occurrences:
|
|
2185
|
+
raise CampaignExecutionContractError(
|
|
2186
|
+
"recombination exceeded its planned child envelope"
|
|
2187
|
+
)
|
|
2188
|
+
if stage.selector_audits:
|
|
2189
|
+
raise CampaignExecutionContractError(
|
|
2190
|
+
"recombination stage cannot publish selector audits"
|
|
2191
|
+
)
|
|
2192
|
+
next_unique = (
|
|
2193
|
+
counters.unique_evaluations + stage.unique_evaluation_count
|
|
2194
|
+
)
|
|
2195
|
+
if next_unique > self.prepared.budget.max_unique_evaluations:
|
|
2196
|
+
raise CampaignExecutionContractError(
|
|
2197
|
+
"stage evaluations exceed campaign budget"
|
|
2198
|
+
)
|
|
2199
|
+
counters = CampaignExecutionCounters(
|
|
2200
|
+
generations_completed=step.generation,
|
|
2201
|
+
candidate_occurrences=(
|
|
2202
|
+
counters.candidate_occurrences
|
|
2203
|
+
+ stage.candidate_occurrence_count
|
|
2204
|
+
),
|
|
2205
|
+
unique_evaluations=next_unique,
|
|
2206
|
+
logical_agent_calls=(
|
|
2207
|
+
counters.logical_agent_calls + expected_stage_agent_calls
|
|
2208
|
+
),
|
|
2209
|
+
logical_agent_calls_dispatched_to_runtime=(
|
|
2210
|
+
counters.logical_agent_calls_dispatched_to_runtime
|
|
2211
|
+
+ expected_stage_agent_calls
|
|
2212
|
+
),
|
|
2213
|
+
logical_agent_calls_succeeded=(
|
|
2214
|
+
counters.logical_agent_calls_succeeded
|
|
2215
|
+
+ expected_stage_agent_calls
|
|
2216
|
+
),
|
|
2217
|
+
logical_agent_calls_abstained=(
|
|
2218
|
+
counters.logical_agent_calls_abstained
|
|
2219
|
+
),
|
|
2220
|
+
logical_agent_calls_failed=(counters.logical_agent_calls_failed),
|
|
2221
|
+
logical_agent_calls_cancelled_before_dispatch=(
|
|
2222
|
+
counters.logical_agent_calls_cancelled_before_dispatch
|
|
2223
|
+
),
|
|
2224
|
+
logical_agent_calls_cancelled_after_dispatch=(
|
|
2225
|
+
counters.logical_agent_calls_cancelled_after_dispatch
|
|
2226
|
+
),
|
|
2227
|
+
)
|
|
2228
|
+
stage_receipts.append(stage)
|
|
2229
|
+
prior_stage = stage
|
|
2230
|
+
await append_event(
|
|
2231
|
+
CampaignExecutionEventKind.STAGE_SEALED,
|
|
2232
|
+
{
|
|
2233
|
+
"stage_receipt": stage.to_record(),
|
|
2234
|
+
"counters": counters.to_record(),
|
|
2235
|
+
},
|
|
2236
|
+
)
|
|
2237
|
+
await settle_stage_boundary()
|
|
2238
|
+
|
|
2239
|
+
wave = wave_by_generation.get(step.generation)
|
|
2240
|
+
if wave is not None:
|
|
2241
|
+
if step.kind is not CampaignGenerationKind.RECOMBINATION:
|
|
2242
|
+
raise CampaignExecutionContractError(
|
|
2243
|
+
"reflection wave follows a non-recombination stage"
|
|
2244
|
+
)
|
|
2245
|
+
if (
|
|
2246
|
+
counters.logical_agent_calls + wave.call_count
|
|
2247
|
+
> self.prepared.budget.max_logical_llm_calls
|
|
2248
|
+
):
|
|
2249
|
+
raise CampaignExecutionContractError(
|
|
2250
|
+
"reflection calls would exceed campaign budget"
|
|
2251
|
+
)
|
|
2252
|
+
reflection_request = CampaignReflectionRequest(
|
|
2253
|
+
preparation_sha256=preparation_sha256,
|
|
2254
|
+
runtime_start_receipt_sha256=start.receipt_sha256,
|
|
2255
|
+
wave=wave,
|
|
2256
|
+
source_stage=stage,
|
|
2257
|
+
)
|
|
2258
|
+
await append_event(
|
|
2259
|
+
CampaignExecutionEventKind.REFLECTION_LAUNCHED,
|
|
2260
|
+
{"reflection_request": reflection_request.to_record()},
|
|
2261
|
+
)
|
|
2262
|
+
reflection_requests[step.generation] = reflection_request
|
|
2263
|
+
update_call_counters(reserved=wave.call_count)
|
|
2264
|
+
started = asyncio.Event()
|
|
2265
|
+
reflection_tasks[step.generation] = asyncio.create_task(
|
|
2266
|
+
run_reflection(reflection_request, started)
|
|
2267
|
+
)
|
|
2268
|
+
await started.wait()
|
|
2269
|
+
update_call_counters(dispatched=wave.call_count)
|
|
2270
|
+
await settle_stage_boundary()
|
|
2271
|
+
|
|
2272
|
+
barrier = barrier_by_generation.get(step.generation)
|
|
2273
|
+
if barrier is not None:
|
|
2274
|
+
await join_barrier(barrier)
|
|
2275
|
+
|
|
2276
|
+
if (
|
|
2277
|
+
counters.generations_completed != len(self.prepared.schedule.steps)
|
|
2278
|
+
or counters.generations_completed > self.prepared.budget.max_generations
|
|
2279
|
+
):
|
|
2280
|
+
raise CampaignExecutionContractError(
|
|
2281
|
+
"execution did not close the exact prepared generation count"
|
|
2282
|
+
)
|
|
2283
|
+
|
|
2284
|
+
if reflection_requests:
|
|
2285
|
+
tail_sources = tuple(sorted(reflection_requests))
|
|
2286
|
+
if any(
|
|
2287
|
+
reflection_requests[source].wave.promotion_barrier_generation
|
|
2288
|
+
is not None
|
|
2289
|
+
for source in tail_sources
|
|
2290
|
+
):
|
|
2291
|
+
raise CampaignExecutionContractError(
|
|
2292
|
+
"a declared promotion barrier was not joined"
|
|
2293
|
+
)
|
|
2294
|
+
await settle_sources(tail_sources, wait_for_all=True)
|
|
2295
|
+
drained = tuple(reflection_receipts[source] for source in tail_sources)
|
|
2296
|
+
failures = failed_sources(tail_sources)
|
|
2297
|
+
if failures and supervision.mode is not (
|
|
2298
|
+
ReflectionFailureMode.BEST_EFFORT_DEGRADED
|
|
2299
|
+
):
|
|
2300
|
+
raise CampaignExecutionContractError(
|
|
2301
|
+
"reflection tail closed with one or more failed receipts"
|
|
2302
|
+
)
|
|
2303
|
+
degraded_reflection_sources.update(failures)
|
|
2304
|
+
tail_drain = CampaignTailDrainReceipt(
|
|
2305
|
+
preparation_sha256=preparation_sha256,
|
|
2306
|
+
drained_reflections=tuple(
|
|
2307
|
+
(value.source_generation, value.receipt_sha256)
|
|
2308
|
+
for value in drained
|
|
2309
|
+
),
|
|
2310
|
+
)
|
|
2311
|
+
await append_event(
|
|
2312
|
+
CampaignExecutionEventKind.REFLECTION_TAIL_DRAINED,
|
|
2313
|
+
{"tail_drain_receipt": tail_drain.to_record()},
|
|
2314
|
+
)
|
|
2315
|
+
reflection_tasks.clear()
|
|
2316
|
+
reflection_requests.clear()
|
|
2317
|
+
except BaseException as exc:
|
|
2318
|
+
primary_error = exc
|
|
2319
|
+
finally:
|
|
2320
|
+
if reflection_tasks:
|
|
2321
|
+
pending_sources = tuple(sorted(reflection_tasks))
|
|
2322
|
+
for task in reflection_tasks.values():
|
|
2323
|
+
if not task.done():
|
|
2324
|
+
task.cancel()
|
|
2325
|
+
raw = await asyncio.gather(
|
|
2326
|
+
*(reflection_tasks[source] for source in pending_sources),
|
|
2327
|
+
return_exceptions=True,
|
|
2328
|
+
)
|
|
2329
|
+
for source, value in zip(pending_sources, raw, strict=True):
|
|
2330
|
+
try:
|
|
2331
|
+
if isinstance(value, asyncio.CancelledError):
|
|
2332
|
+
await record_reflection_cancellation(
|
|
2333
|
+
source,
|
|
2334
|
+
reason="campaign_abort",
|
|
2335
|
+
)
|
|
2336
|
+
elif isinstance(value, BaseException):
|
|
2337
|
+
await settle_reflection(
|
|
2338
|
+
source,
|
|
2339
|
+
failed_reflection_receipt(source, value),
|
|
2340
|
+
)
|
|
2341
|
+
else:
|
|
2342
|
+
await settle_reflection(source, value)
|
|
2343
|
+
except BaseException as settlement_error:
|
|
2344
|
+
if primary_error is None:
|
|
2345
|
+
primary_error = settlement_error
|
|
2346
|
+
reflection_tasks.clear()
|
|
2347
|
+
reflection_requests.clear()
|
|
2348
|
+
|
|
2349
|
+
if primary_error is not None:
|
|
2350
|
+
status = CampaignExecutionStatus.FAILED
|
|
2351
|
+
failure_type = type(primary_error).__name__
|
|
2352
|
+
elif degraded_reflection_sources:
|
|
2353
|
+
status = CampaignExecutionStatus.DEGRADED
|
|
2354
|
+
failure_type = "reflection_failure"
|
|
2355
|
+
try:
|
|
2356
|
+
await append_event(
|
|
2357
|
+
CampaignExecutionEventKind.EXECUTION_DEGRADED,
|
|
2358
|
+
{
|
|
2359
|
+
"approved_summary": (
|
|
2360
|
+
"campaign completed with quarantined reflection "
|
|
2361
|
+
"failures"
|
|
2362
|
+
),
|
|
2363
|
+
"failed_reflection_sources": sorted(
|
|
2364
|
+
degraded_reflection_sources
|
|
2365
|
+
),
|
|
2366
|
+
"counters": counters.to_record(),
|
|
2367
|
+
},
|
|
2368
|
+
)
|
|
2369
|
+
except BaseException as degraded_event_error:
|
|
2370
|
+
primary_error = degraded_event_error
|
|
2371
|
+
status = CampaignExecutionStatus.FAILED
|
|
2372
|
+
failure_type = type(degraded_event_error).__name__
|
|
2373
|
+
else:
|
|
2374
|
+
status = CampaignExecutionStatus.COMPLETED
|
|
2375
|
+
failure_type = None
|
|
2376
|
+
if primary_error is not None:
|
|
2377
|
+
failure_digest = hashlib.sha256(
|
|
2378
|
+
type(primary_error).__qualname__.encode("utf-8", errors="strict")
|
|
2379
|
+
+ b"\x00"
|
|
2380
|
+
+ str(primary_error).encode("utf-8", errors="replace")
|
|
2381
|
+
).hexdigest()
|
|
2382
|
+
try:
|
|
2383
|
+
await append_event(
|
|
2384
|
+
CampaignExecutionEventKind.EXECUTION_FAILED,
|
|
2385
|
+
{
|
|
2386
|
+
"failure_type": failure_type,
|
|
2387
|
+
"failure_digest_sha256": failure_digest,
|
|
2388
|
+
"approved_summary": "campaign execution failed",
|
|
2389
|
+
"counters": counters.to_record(),
|
|
2390
|
+
},
|
|
2391
|
+
)
|
|
2392
|
+
except BaseException:
|
|
2393
|
+
pass
|
|
2394
|
+
|
|
2395
|
+
finalize_request = CampaignFinalizationRequest(
|
|
2396
|
+
preparation_sha256=preparation_sha256,
|
|
2397
|
+
runtime_start_receipt_sha256=start.receipt_sha256,
|
|
2398
|
+
status=status,
|
|
2399
|
+
counters=counters,
|
|
2400
|
+
stage_receipt_sha256s=tuple(
|
|
2401
|
+
value.receipt_sha256 for value in stage_receipts
|
|
2402
|
+
),
|
|
2403
|
+
reflection_receipt_sha256s=tuple(
|
|
2404
|
+
reflection_receipts[source].receipt_sha256
|
|
2405
|
+
for source in sorted(reflection_receipts)
|
|
2406
|
+
),
|
|
2407
|
+
reflection_cancellation_receipt_sha256s=tuple(
|
|
2408
|
+
value.receipt_sha256 for value in reflection_cancellations
|
|
2409
|
+
),
|
|
2410
|
+
test_admission_receipt_sha256s=tuple(
|
|
2411
|
+
value.receipt_sha256 for value in test_admission_receipts
|
|
2412
|
+
),
|
|
2413
|
+
tail_drain_receipt_sha256=(
|
|
2414
|
+
None if tail_drain is None else tail_drain.receipt_sha256
|
|
2415
|
+
),
|
|
2416
|
+
last_durable_event_sha256=last_event_sha256,
|
|
2417
|
+
failure_type=failure_type,
|
|
2418
|
+
)
|
|
2419
|
+
try:
|
|
2420
|
+
candidate_finalization = await self.lifecycle.finalize(finalize_request)
|
|
2421
|
+
if type(candidate_finalization) is not CampaignFinalizationReceipt:
|
|
2422
|
+
raise TypeError(
|
|
2423
|
+
"lifecycle finalize must return CampaignFinalizationReceipt"
|
|
2424
|
+
)
|
|
2425
|
+
CampaignFinalizationReceipt.__post_init__(candidate_finalization)
|
|
2426
|
+
if (
|
|
2427
|
+
candidate_finalization.request_sha256
|
|
2428
|
+
!= finalize_request.request_sha256
|
|
2429
|
+
or candidate_finalization.preparation_sha256 != preparation_sha256
|
|
2430
|
+
or candidate_finalization.status is not status
|
|
2431
|
+
):
|
|
2432
|
+
raise CampaignExecutionContractError(
|
|
2433
|
+
"finalization receipt differs from request"
|
|
2434
|
+
)
|
|
2435
|
+
finalization = candidate_finalization
|
|
2436
|
+
await append_event(
|
|
2437
|
+
CampaignExecutionEventKind.EXECUTION_FINALIZED,
|
|
2438
|
+
{"finalization_receipt": finalization.to_record()},
|
|
2439
|
+
)
|
|
2440
|
+
except BaseException as exc:
|
|
2441
|
+
if primary_error is None:
|
|
2442
|
+
primary_error = exc
|
|
2443
|
+
status = CampaignExecutionStatus.FAILED
|
|
2444
|
+
failure_type = type(exc).__name__
|
|
2445
|
+
|
|
2446
|
+
cleanup_request = CampaignCleanupRequest(
|
|
2447
|
+
preparation_sha256=preparation_sha256,
|
|
2448
|
+
runtime_start_receipt_sha256=start.receipt_sha256,
|
|
2449
|
+
status=status,
|
|
2450
|
+
finalization_receipt_sha256=(
|
|
2451
|
+
None if finalization is None else finalization.receipt_sha256
|
|
2452
|
+
),
|
|
2453
|
+
failure_type=failure_type,
|
|
2454
|
+
)
|
|
2455
|
+
try:
|
|
2456
|
+
cleanup = await self.lifecycle.cleanup(cleanup_request)
|
|
2457
|
+
if type(cleanup) is not CampaignCleanupReceipt:
|
|
2458
|
+
raise TypeError(
|
|
2459
|
+
"lifecycle cleanup must return CampaignCleanupReceipt"
|
|
2460
|
+
)
|
|
2461
|
+
CampaignCleanupReceipt.__post_init__(cleanup)
|
|
2462
|
+
if (
|
|
2463
|
+
cleanup.request_sha256 != cleanup_request.request_sha256
|
|
2464
|
+
or cleanup.preparation_sha256 != preparation_sha256
|
|
2465
|
+
or not cleanup.released
|
|
2466
|
+
):
|
|
2467
|
+
raise CampaignExecutionContractError(
|
|
2468
|
+
"cleanup receipt differs from request or retained resources"
|
|
2469
|
+
)
|
|
2470
|
+
await append_event(
|
|
2471
|
+
CampaignExecutionEventKind.RUNTIME_CLEANED,
|
|
2472
|
+
{"cleanup_receipt": cleanup.to_record()},
|
|
2473
|
+
)
|
|
2474
|
+
except BaseException as exc:
|
|
2475
|
+
if primary_error is None:
|
|
2476
|
+
primary_error = exc
|
|
2477
|
+
|
|
2478
|
+
if primary_error is not None:
|
|
2479
|
+
raise primary_error
|
|
2480
|
+
if finalization is None or cleanup is None: # pragma: no cover - guarded above.
|
|
2481
|
+
raise AssertionError("successful execution lost lifecycle receipts")
|
|
2482
|
+
result = CampaignExecutionResult(
|
|
2483
|
+
preparation_sha256=preparation_sha256,
|
|
2484
|
+
runtime_start_receipt_sha256=start.receipt_sha256,
|
|
2485
|
+
stage_receipts=tuple(stage_receipts),
|
|
2486
|
+
reflection_receipts=tuple(
|
|
2487
|
+
reflection_receipts[source] for source in sorted(reflection_receipts)
|
|
2488
|
+
),
|
|
2489
|
+
reflection_cancellation_receipts=tuple(reflection_cancellations),
|
|
2490
|
+
test_admission_receipts=tuple(test_admission_receipts),
|
|
2491
|
+
tail_drain_receipt=tail_drain,
|
|
2492
|
+
counters=counters,
|
|
2493
|
+
finalization_receipt=finalization,
|
|
2494
|
+
cleanup_receipt=cleanup,
|
|
2495
|
+
durable_event_sha256s=tuple(durable_event_sha256s),
|
|
2496
|
+
)
|
|
2497
|
+
CampaignExecutionResult.__post_init__(result)
|
|
2498
|
+
return result
|
|
2499
|
+
|
|
2500
|
+
|
|
2501
|
+
__all__ = [
|
|
2502
|
+
"CampaignArchiveCutoffReceipt",
|
|
2503
|
+
"CampaignArchiveCutoffRequest",
|
|
2504
|
+
"CampaignCleanupReceipt",
|
|
2505
|
+
"CampaignCleanupRequest",
|
|
2506
|
+
"CampaignExecutionContractError",
|
|
2507
|
+
"CampaignExecutionCounters",
|
|
2508
|
+
"CampaignExecutionEvent",
|
|
2509
|
+
"CampaignExecutionEventKind",
|
|
2510
|
+
"CampaignExecutionJournalPort",
|
|
2511
|
+
"CampaignExecutionLifecyclePort",
|
|
2512
|
+
"CampaignExecutionResult",
|
|
2513
|
+
"CampaignExecutionStartReceipt",
|
|
2514
|
+
"CampaignExecutionStatus",
|
|
2515
|
+
"CampaignFinalizationReceipt",
|
|
2516
|
+
"CampaignFinalizationRequest",
|
|
2517
|
+
"CampaignJournalAck",
|
|
2518
|
+
"CampaignReflectionReceipt",
|
|
2519
|
+
"CampaignReflectionCancellationReceipt",
|
|
2520
|
+
"CampaignReflectionRequest",
|
|
2521
|
+
"CampaignReflectionRuntimePort",
|
|
2522
|
+
"CampaignReflectionStatus",
|
|
2523
|
+
"CampaignReflectionTestAdmissionReceipt",
|
|
2524
|
+
"CampaignReflectionTestAdmissionRequest",
|
|
2525
|
+
"CampaignSeedExecutionReceipt",
|
|
2526
|
+
"CampaignSelectorAuditReceipt",
|
|
2527
|
+
"CampaignStageReceipt",
|
|
2528
|
+
"CampaignStageRequest",
|
|
2529
|
+
"CampaignStageRuntimePort",
|
|
2530
|
+
"CampaignTailDrainReceipt",
|
|
2531
|
+
"EvolutionCampaignScheduler",
|
|
2532
|
+
"SelectorAuditExecutionMode",
|
|
2533
|
+
"campaign_step_sha256",
|
|
2534
|
+
"decode_selector_audit_text",
|
|
2535
|
+
"encode_selector_audit_text",
|
|
2536
|
+
"selector_audit_set_sha256",
|
|
2537
|
+
]
|