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,1454 @@
|
|
|
1
|
+
"""Immutable event schema and canonical JSON codec.
|
|
2
|
+
|
|
3
|
+
The event vocabulary is intentionally small but covers the accounting boundaries
|
|
4
|
+
needed by the current loop. Large or mutable values are represented by artifact
|
|
5
|
+
IDs; objective vectors use immutable tuples.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
import types
|
|
14
|
+
from dataclasses import dataclass, fields
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from decimal import Decimal
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import (
|
|
19
|
+
Any,
|
|
20
|
+
ClassVar,
|
|
21
|
+
Dict,
|
|
22
|
+
Optional,
|
|
23
|
+
Tuple,
|
|
24
|
+
Type,
|
|
25
|
+
Union,
|
|
26
|
+
get_args,
|
|
27
|
+
get_origin,
|
|
28
|
+
get_type_hints,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from agent_evolve.domain.artifact import ArtifactRef, ArtifactRole
|
|
32
|
+
from agent_evolve.domain.ids import (
|
|
33
|
+
ArtifactId,
|
|
34
|
+
CandidateId,
|
|
35
|
+
CorrelationId,
|
|
36
|
+
EvaluationAttemptId,
|
|
37
|
+
EvaluationId,
|
|
38
|
+
EventId,
|
|
39
|
+
GenerationId,
|
|
40
|
+
ID_TYPES,
|
|
41
|
+
LLMCallId,
|
|
42
|
+
OperatorInvocationId,
|
|
43
|
+
ProviderAttemptId,
|
|
44
|
+
RunId,
|
|
45
|
+
StableId,
|
|
46
|
+
)
|
|
47
|
+
from agent_evolve.domain.inline_text import InlineTextPolicy, validate_inline_text
|
|
48
|
+
from agent_evolve.domain.outcome import (
|
|
49
|
+
FailureCategory,
|
|
50
|
+
FailureCode,
|
|
51
|
+
FailureRecord,
|
|
52
|
+
validate_failure_pair,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
CURRENT_EVENT_SCHEMA_VERSION = 1
|
|
56
|
+
ObjectiveValues = Tuple[Tuple[str, float], ...]
|
|
57
|
+
_POLICY_COMPONENT = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
58
|
+
_POLICY_CREDENTIAL_SHAPE = re.compile(
|
|
59
|
+
r"(?:(?<![A-Za-z0-9])(?:sk|pk|rk)-[A-Za-z0-9_-]{12,}(?![A-Za-z0-9])|"
|
|
60
|
+
r"(?<![A-Za-z0-9])or-v1-[A-Za-z0-9_-]{12,}(?![A-Za-z0-9])|"
|
|
61
|
+
r"(?<![A-Za-z0-9])(?:ghp_[A-Za-z0-9]{20,}|"
|
|
62
|
+
r"github_pat_[A-Za-z0-9_]{20,})(?![A-Za-z0-9])|"
|
|
63
|
+
r"(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{12,}(?![A-Za-z0-9])|"
|
|
64
|
+
r"(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])|"
|
|
65
|
+
r"(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\."
|
|
66
|
+
r"[A-Za-z0-9_-]{8,}(?![A-Za-z0-9]))",
|
|
67
|
+
re.IGNORECASE,
|
|
68
|
+
)
|
|
69
|
+
_DECIMAL_TEXT = re.compile(r"^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$")
|
|
70
|
+
_MAX_DECIMAL_DIGITS = 64
|
|
71
|
+
_MAX_DECIMAL_ABS_EXPONENT = 64
|
|
72
|
+
_MAX_DECIMAL_TEXT_LENGTH = 128
|
|
73
|
+
_DECODE_FAILED = object()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class EventCodecError(ValueError):
|
|
77
|
+
"""An event record does not conform to the supported durable schema."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ValidationStage(str, Enum):
|
|
81
|
+
SCHEMA = "schema"
|
|
82
|
+
DETERMINISTIC_PRECHECK = "deterministic_precheck"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class EventPayload:
|
|
86
|
+
"""Marker base for frozen payload value objects."""
|
|
87
|
+
|
|
88
|
+
EVENT_TYPE: ClassVar[str]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True, slots=True)
|
|
92
|
+
class ArtifactRegistered(EventPayload):
|
|
93
|
+
"""A post-write journal record for one verified, sanitized artifact."""
|
|
94
|
+
|
|
95
|
+
EVENT_TYPE: ClassVar[str] = "ArtifactRegistered"
|
|
96
|
+
|
|
97
|
+
artifact_id: ArtifactId
|
|
98
|
+
sha256_hex: str
|
|
99
|
+
size_bytes: int
|
|
100
|
+
media_type: str
|
|
101
|
+
role: ArtifactRole
|
|
102
|
+
minimization_policy_id: str
|
|
103
|
+
minimization_policy_version: str
|
|
104
|
+
minimization_policy_config_sha256: str
|
|
105
|
+
sanitization_policy_id: str
|
|
106
|
+
sanitization_policy_version: str
|
|
107
|
+
|
|
108
|
+
def __post_init__(self) -> None:
|
|
109
|
+
# Reuse the exact ArtifactRef invariants rather than maintaining a
|
|
110
|
+
# second, subtly different metadata schema in the event vocabulary.
|
|
111
|
+
ArtifactRef(
|
|
112
|
+
artifact_id=self.artifact_id,
|
|
113
|
+
sha256_hex=self.sha256_hex,
|
|
114
|
+
size_bytes=self.size_bytes,
|
|
115
|
+
media_type=self.media_type,
|
|
116
|
+
)
|
|
117
|
+
if not isinstance(self.role, ArtifactRole):
|
|
118
|
+
raise TypeError("role must be an ArtifactRole")
|
|
119
|
+
for name, value in (
|
|
120
|
+
("minimization_policy_id", self.minimization_policy_id),
|
|
121
|
+
("minimization_policy_version", self.minimization_policy_version),
|
|
122
|
+
("sanitization_policy_id", self.sanitization_policy_id),
|
|
123
|
+
("sanitization_policy_version", self.sanitization_policy_version),
|
|
124
|
+
):
|
|
125
|
+
if (
|
|
126
|
+
not isinstance(value, str)
|
|
127
|
+
or _POLICY_COMPONENT.fullmatch(value) is None
|
|
128
|
+
or _POLICY_CREDENTIAL_SHAPE.search(value)
|
|
129
|
+
):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"{name} must be a non-secret, storage-safe policy component"
|
|
132
|
+
)
|
|
133
|
+
_require_hash(
|
|
134
|
+
self.minimization_policy_config_sha256,
|
|
135
|
+
"minimization_policy_config_sha256",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def artifact_ref(self) -> ArtifactRef:
|
|
140
|
+
"""Reconstruct the complete reference claimed by this event."""
|
|
141
|
+
|
|
142
|
+
return ArtifactRef(
|
|
143
|
+
artifact_id=self.artifact_id,
|
|
144
|
+
sha256_hex=self.sha256_hex,
|
|
145
|
+
size_bytes=self.size_bytes,
|
|
146
|
+
media_type=self.media_type,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _require_nonempty(value: str, name: str) -> None:
|
|
151
|
+
if not isinstance(value, str) or not value.strip():
|
|
152
|
+
raise ValueError(f"{name} must be a non-empty string")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _require_nonnegative(value: int, name: str) -> None:
|
|
156
|
+
if type(value) is not int or value < 0:
|
|
157
|
+
raise ValueError(f"{name} must be a non-negative integer")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _require_positive(value: int, name: str) -> None:
|
|
161
|
+
if type(value) is not int or value <= 0:
|
|
162
|
+
raise ValueError(f"{name} must be a positive integer")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _require_hash(value: str, name: str) -> None:
|
|
166
|
+
_require_nonempty(value, name)
|
|
167
|
+
if len(value) != 64 or any(c not in "0123456789abcdef" for c in value):
|
|
168
|
+
raise ValueError(f"{name} must be a lowercase SHA-256 hex digest")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _is_bounded_finite_decimal(value: Any) -> bool:
|
|
172
|
+
if type(value) is not Decimal or not value.is_finite():
|
|
173
|
+
return False
|
|
174
|
+
decimal_tuple = value.as_tuple()
|
|
175
|
+
return (
|
|
176
|
+
len(decimal_tuple.digits) <= _MAX_DECIMAL_DIGITS
|
|
177
|
+
and -_MAX_DECIMAL_ABS_EXPONENT
|
|
178
|
+
<= decimal_tuple.exponent
|
|
179
|
+
<= _MAX_DECIMAL_ABS_EXPONENT
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _require_bounded_finite_decimal(value: Any, name: str) -> None:
|
|
184
|
+
if not _is_bounded_finite_decimal(value):
|
|
185
|
+
raise TypeError(f"{name} must be a bounded finite Decimal")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _validate_objective_values(values: ObjectiveValues) -> None:
|
|
189
|
+
if type(values) is not tuple:
|
|
190
|
+
raise TypeError("objective_values must be an immutable tuple")
|
|
191
|
+
if not values:
|
|
192
|
+
raise ValueError("objective_values must contain at least one declared objective")
|
|
193
|
+
names = set()
|
|
194
|
+
for item in values:
|
|
195
|
+
if type(item) is not tuple or len(item) != 2:
|
|
196
|
+
raise TypeError("each objective entry must be a (name, value) tuple")
|
|
197
|
+
name, value = item
|
|
198
|
+
_require_nonempty(name, "objective name")
|
|
199
|
+
validate_inline_text(
|
|
200
|
+
name,
|
|
201
|
+
field_name="objective name",
|
|
202
|
+
policy=InlineTextPolicy.METADATA_TOKEN,
|
|
203
|
+
)
|
|
204
|
+
if name in names:
|
|
205
|
+
raise ValueError(f"duplicate objective value {name!r}")
|
|
206
|
+
names.add(name)
|
|
207
|
+
if type(value) not in (int, float):
|
|
208
|
+
raise TypeError(f"objective {name!r} must be numeric")
|
|
209
|
+
if not math.isfinite(float(value)):
|
|
210
|
+
raise ValueError(f"objective {name!r} must be finite")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True, slots=True)
|
|
214
|
+
class RunStarted(EventPayload):
|
|
215
|
+
EVENT_TYPE: ClassVar[str] = "RunStarted"
|
|
216
|
+
|
|
217
|
+
experiment_spec_hash: str
|
|
218
|
+
manifest_artifact_id: Optional[ArtifactId] = None
|
|
219
|
+
|
|
220
|
+
def __post_init__(self) -> None:
|
|
221
|
+
_require_hash(self.experiment_spec_hash, "experiment_spec_hash")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True, slots=True)
|
|
225
|
+
class RunFinished(EventPayload):
|
|
226
|
+
EVENT_TYPE: ClassVar[str] = "RunFinished"
|
|
227
|
+
|
|
228
|
+
stop_reason: str
|
|
229
|
+
|
|
230
|
+
def __post_init__(self) -> None:
|
|
231
|
+
_require_nonempty(self.stop_reason, "stop_reason")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass(frozen=True, slots=True)
|
|
235
|
+
class RunAborted(EventPayload):
|
|
236
|
+
EVENT_TYPE: ClassVar[str] = "RunAborted"
|
|
237
|
+
|
|
238
|
+
category: FailureCategory
|
|
239
|
+
code: FailureCode
|
|
240
|
+
message: str
|
|
241
|
+
exception_type: Optional[str] = None
|
|
242
|
+
diagnostics_artifact_id: Optional[ArtifactId] = None
|
|
243
|
+
|
|
244
|
+
def __post_init__(self) -> None:
|
|
245
|
+
FailureRecord(
|
|
246
|
+
category=self.category,
|
|
247
|
+
code=self.code,
|
|
248
|
+
message=self.message,
|
|
249
|
+
exception_type=self.exception_type,
|
|
250
|
+
diagnostics_artifact_id=self.diagnostics_artifact_id,
|
|
251
|
+
)
|
|
252
|
+
if self.category is FailureCategory.CANDIDATE:
|
|
253
|
+
raise ValueError("candidate-attributable failures cannot abort a run")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True, slots=True)
|
|
257
|
+
class GenerationStarted(EventPayload):
|
|
258
|
+
EVENT_TYPE: ClassVar[str] = "GenerationStarted"
|
|
259
|
+
|
|
260
|
+
generation_id: GenerationId
|
|
261
|
+
ordinal: int
|
|
262
|
+
|
|
263
|
+
def __post_init__(self) -> None:
|
|
264
|
+
_require_positive(self.ordinal, "ordinal")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass(frozen=True, slots=True)
|
|
268
|
+
class GenerationCompleted(EventPayload):
|
|
269
|
+
EVENT_TYPE: ClassVar[str] = "GenerationCompleted"
|
|
270
|
+
|
|
271
|
+
generation_id: GenerationId
|
|
272
|
+
ordinal: int
|
|
273
|
+
pareto_size: int
|
|
274
|
+
|
|
275
|
+
def __post_init__(self) -> None:
|
|
276
|
+
_require_positive(self.ordinal, "ordinal")
|
|
277
|
+
_require_nonnegative(self.pareto_size, "pareto_size")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@dataclass(frozen=True, slots=True)
|
|
281
|
+
class OperatorSelected(EventPayload):
|
|
282
|
+
EVENT_TYPE: ClassVar[str] = "OperatorSelected"
|
|
283
|
+
|
|
284
|
+
operator_invocation_id: OperatorInvocationId
|
|
285
|
+
generation_id: GenerationId
|
|
286
|
+
operator_name: str
|
|
287
|
+
operator_version: str
|
|
288
|
+
requested_offspring: int
|
|
289
|
+
|
|
290
|
+
def __post_init__(self) -> None:
|
|
291
|
+
_require_nonempty(self.operator_name, "operator_name")
|
|
292
|
+
_require_nonempty(self.operator_version, "operator_version")
|
|
293
|
+
_require_nonnegative(self.requested_offspring, "requested_offspring")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@dataclass(frozen=True, slots=True)
|
|
297
|
+
class LLMCallRequested(EventPayload):
|
|
298
|
+
EVENT_TYPE: ClassVar[str] = "LLMCallRequested"
|
|
299
|
+
|
|
300
|
+
call_id: LLMCallId
|
|
301
|
+
operation: str
|
|
302
|
+
requested_model: str
|
|
303
|
+
generation_id: Optional[GenerationId] = None
|
|
304
|
+
operator_invocation_id: Optional[OperatorInvocationId] = None
|
|
305
|
+
|
|
306
|
+
def __post_init__(self) -> None:
|
|
307
|
+
_require_nonempty(self.operation, "operation")
|
|
308
|
+
_require_nonempty(self.requested_model, "requested_model")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@dataclass(frozen=True, slots=True)
|
|
312
|
+
class LLMRequestArtifactLinked(EventPayload):
|
|
313
|
+
"""Link one sanitized request artifact to a logical LLM call."""
|
|
314
|
+
|
|
315
|
+
EVENT_TYPE: ClassVar[str] = "LLMRequestArtifactLinked"
|
|
316
|
+
|
|
317
|
+
call_id: LLMCallId
|
|
318
|
+
request_artifact_id: ArtifactId
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@dataclass(frozen=True, slots=True)
|
|
322
|
+
class LLMCallStarted(EventPayload):
|
|
323
|
+
EVENT_TYPE: ClassVar[str] = "LLMCallStarted"
|
|
324
|
+
|
|
325
|
+
call_id: LLMCallId
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@dataclass(frozen=True, slots=True)
|
|
329
|
+
class LLMCallCompleted(EventPayload):
|
|
330
|
+
EVENT_TYPE: ClassVar[str] = "LLMCallCompleted"
|
|
331
|
+
|
|
332
|
+
call_id: LLMCallId
|
|
333
|
+
response_artifact_id: Optional[ArtifactId] = None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@dataclass(frozen=True, slots=True)
|
|
337
|
+
class LLMCallFailed(EventPayload):
|
|
338
|
+
EVENT_TYPE: ClassVar[str] = "LLMCallFailed"
|
|
339
|
+
|
|
340
|
+
call_id: LLMCallId
|
|
341
|
+
category: FailureCategory
|
|
342
|
+
code: FailureCode
|
|
343
|
+
retryable: bool
|
|
344
|
+
message: str
|
|
345
|
+
exception_type: Optional[str] = None
|
|
346
|
+
diagnostics_artifact_id: Optional[ArtifactId] = None
|
|
347
|
+
|
|
348
|
+
def __post_init__(self) -> None:
|
|
349
|
+
FailureRecord(
|
|
350
|
+
category=self.category,
|
|
351
|
+
code=self.code,
|
|
352
|
+
message=self.message,
|
|
353
|
+
retryable=self.retryable,
|
|
354
|
+
exception_type=self.exception_type,
|
|
355
|
+
diagnostics_artifact_id=self.diagnostics_artifact_id,
|
|
356
|
+
)
|
|
357
|
+
if self.category is FailureCategory.CANDIDATE:
|
|
358
|
+
raise ValueError("LLM call failures cannot be candidate-attributable")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@dataclass(frozen=True, slots=True)
|
|
362
|
+
class LLMCallRetried(EventPayload):
|
|
363
|
+
EVENT_TYPE: ClassVar[str] = "LLMCallRetried"
|
|
364
|
+
|
|
365
|
+
call_id: LLMCallId
|
|
366
|
+
next_attempt_number: int
|
|
367
|
+
|
|
368
|
+
def __post_init__(self) -> None:
|
|
369
|
+
_require_positive(self.next_attempt_number, "next_attempt_number")
|
|
370
|
+
if self.next_attempt_number < 2:
|
|
371
|
+
raise ValueError("a retry attempt number must be at least 2")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@dataclass(frozen=True, slots=True)
|
|
375
|
+
class ProviderAttemptStarted(EventPayload):
|
|
376
|
+
EVENT_TYPE: ClassVar[str] = "ProviderAttemptStarted"
|
|
377
|
+
|
|
378
|
+
call_id: LLMCallId
|
|
379
|
+
provider_attempt_id: ProviderAttemptId
|
|
380
|
+
attempt_number: int
|
|
381
|
+
|
|
382
|
+
def __post_init__(self) -> None:
|
|
383
|
+
_require_positive(self.attempt_number, "attempt_number")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@dataclass(frozen=True, slots=True)
|
|
387
|
+
class ProviderAttemptCompleted(EventPayload):
|
|
388
|
+
EVENT_TYPE: ClassVar[str] = "ProviderAttemptCompleted"
|
|
389
|
+
|
|
390
|
+
call_id: LLMCallId
|
|
391
|
+
provider_attempt_id: ProviderAttemptId
|
|
392
|
+
resolved_provider: str
|
|
393
|
+
resolved_model: str
|
|
394
|
+
input_tokens: int = 0
|
|
395
|
+
output_tokens: int = 0
|
|
396
|
+
reasoning_tokens: int = 0
|
|
397
|
+
cache_read_tokens: int = 0
|
|
398
|
+
cache_write_tokens: int = 0
|
|
399
|
+
cost_usd: Decimal = Decimal("0")
|
|
400
|
+
latency_ns: int = 0
|
|
401
|
+
|
|
402
|
+
def __post_init__(self) -> None:
|
|
403
|
+
_require_nonempty(self.resolved_provider, "resolved_provider")
|
|
404
|
+
_require_nonempty(self.resolved_model, "resolved_model")
|
|
405
|
+
for name in (
|
|
406
|
+
"input_tokens",
|
|
407
|
+
"output_tokens",
|
|
408
|
+
"reasoning_tokens",
|
|
409
|
+
"cache_read_tokens",
|
|
410
|
+
"cache_write_tokens",
|
|
411
|
+
"latency_ns",
|
|
412
|
+
):
|
|
413
|
+
_require_nonnegative(getattr(self, name), name)
|
|
414
|
+
_require_bounded_finite_decimal(self.cost_usd, "cost_usd")
|
|
415
|
+
if self.cost_usd < 0:
|
|
416
|
+
raise ValueError("cost_usd must be non-negative")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@dataclass(frozen=True, slots=True)
|
|
420
|
+
class ProviderAttemptFailed(EventPayload):
|
|
421
|
+
EVENT_TYPE: ClassVar[str] = "ProviderAttemptFailed"
|
|
422
|
+
|
|
423
|
+
call_id: LLMCallId
|
|
424
|
+
provider_attempt_id: ProviderAttemptId
|
|
425
|
+
category: FailureCategory
|
|
426
|
+
code: FailureCode
|
|
427
|
+
retryable: bool
|
|
428
|
+
message: str
|
|
429
|
+
latency_ns: int = 0
|
|
430
|
+
exception_type: Optional[str] = None
|
|
431
|
+
diagnostics_artifact_id: Optional[ArtifactId] = None
|
|
432
|
+
|
|
433
|
+
def __post_init__(self) -> None:
|
|
434
|
+
FailureRecord(
|
|
435
|
+
category=self.category,
|
|
436
|
+
code=self.code,
|
|
437
|
+
message=self.message,
|
|
438
|
+
retryable=self.retryable,
|
|
439
|
+
exception_type=self.exception_type,
|
|
440
|
+
diagnostics_artifact_id=self.diagnostics_artifact_id,
|
|
441
|
+
)
|
|
442
|
+
if self.category is FailureCategory.CANDIDATE:
|
|
443
|
+
raise ValueError("provider failures cannot be candidate-attributable")
|
|
444
|
+
_require_nonnegative(self.latency_ns, "latency_ns")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@dataclass(frozen=True, slots=True)
|
|
448
|
+
class CandidateProposed(EventPayload):
|
|
449
|
+
EVENT_TYPE: ClassVar[str] = "CandidateProposed"
|
|
450
|
+
|
|
451
|
+
candidate_id: CandidateId
|
|
452
|
+
generation_id: GenerationId
|
|
453
|
+
content_hash: str
|
|
454
|
+
proposal_index: int
|
|
455
|
+
operator_invocation_id: Optional[OperatorInvocationId] = None
|
|
456
|
+
generator_call_id: Optional[LLMCallId] = None
|
|
457
|
+
configuration_artifact_id: Optional[ArtifactId] = None
|
|
458
|
+
|
|
459
|
+
def __post_init__(self) -> None:
|
|
460
|
+
_require_hash(self.content_hash, "content_hash")
|
|
461
|
+
_require_nonnegative(self.proposal_index, "proposal_index")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@dataclass(frozen=True, slots=True)
|
|
465
|
+
class DuplicateDetected(EventPayload):
|
|
466
|
+
EVENT_TYPE: ClassVar[str] = "DuplicateDetected"
|
|
467
|
+
|
|
468
|
+
candidate_id: CandidateId
|
|
469
|
+
content_hash: str
|
|
470
|
+
canonical_candidate_id: Optional[CandidateId] = None
|
|
471
|
+
reason: str = "same_content"
|
|
472
|
+
|
|
473
|
+
def __post_init__(self) -> None:
|
|
474
|
+
_require_hash(self.content_hash, "content_hash")
|
|
475
|
+
_require_nonempty(self.reason, "reason")
|
|
476
|
+
if self.canonical_candidate_id == self.candidate_id:
|
|
477
|
+
raise ValueError("a duplicate cannot identify itself as its canonical occurrence")
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@dataclass(frozen=True, slots=True)
|
|
481
|
+
class ValidationStarted(EventPayload):
|
|
482
|
+
EVENT_TYPE: ClassVar[str] = "ValidationStarted"
|
|
483
|
+
|
|
484
|
+
candidate_id: CandidateId
|
|
485
|
+
stage: ValidationStage
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@dataclass(frozen=True, slots=True)
|
|
489
|
+
class ValidationCompleted(EventPayload):
|
|
490
|
+
EVENT_TYPE: ClassVar[str] = "ValidationCompleted"
|
|
491
|
+
|
|
492
|
+
candidate_id: CandidateId
|
|
493
|
+
stage: ValidationStage
|
|
494
|
+
ok: bool
|
|
495
|
+
failure_category: Optional[FailureCategory] = None
|
|
496
|
+
failure_code: Optional[FailureCode] = None
|
|
497
|
+
|
|
498
|
+
def __post_init__(self) -> None:
|
|
499
|
+
if not isinstance(self.ok, bool):
|
|
500
|
+
raise TypeError("ok must be bool")
|
|
501
|
+
if self.ok:
|
|
502
|
+
if self.failure_category is not None or self.failure_code is not None:
|
|
503
|
+
raise ValueError("successful validation cannot carry failure fields")
|
|
504
|
+
elif self.failure_category is None or self.failure_code is None:
|
|
505
|
+
raise ValueError("failed validation requires a failure category and code")
|
|
506
|
+
else:
|
|
507
|
+
validate_failure_pair(self.failure_category, self.failure_code)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@dataclass(frozen=True, slots=True)
|
|
511
|
+
class EvaluationRequested(EventPayload):
|
|
512
|
+
EVENT_TYPE: ClassVar[str] = "EvaluationRequested"
|
|
513
|
+
|
|
514
|
+
evaluation_id: EvaluationId
|
|
515
|
+
candidate_id: CandidateId
|
|
516
|
+
fidelity: str
|
|
517
|
+
seed: Optional[int] = None
|
|
518
|
+
cache_key_hash: Optional[str] = None
|
|
519
|
+
|
|
520
|
+
def __post_init__(self) -> None:
|
|
521
|
+
_require_nonempty(self.fidelity, "fidelity")
|
|
522
|
+
if self.seed is not None and type(self.seed) is not int:
|
|
523
|
+
raise TypeError("seed must be an integer or None")
|
|
524
|
+
if self.cache_key_hash is not None:
|
|
525
|
+
_require_hash(self.cache_key_hash, "cache_key_hash")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@dataclass(frozen=True, slots=True)
|
|
529
|
+
class EvaluationCacheHit(EventPayload):
|
|
530
|
+
EVENT_TYPE: ClassVar[str] = "EvaluationCacheHit"
|
|
531
|
+
|
|
532
|
+
evaluation_id: EvaluationId
|
|
533
|
+
candidate_id: CandidateId
|
|
534
|
+
fidelity: str
|
|
535
|
+
source_run_id: RunId
|
|
536
|
+
source_evaluation_id: EvaluationId
|
|
537
|
+
objective_values: ObjectiveValues
|
|
538
|
+
|
|
539
|
+
def __post_init__(self) -> None:
|
|
540
|
+
_require_nonempty(self.fidelity, "fidelity")
|
|
541
|
+
_validate_objective_values(self.objective_values)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@dataclass(frozen=True, slots=True)
|
|
545
|
+
class EvaluationCacheMiss(EventPayload):
|
|
546
|
+
EVENT_TYPE: ClassVar[str] = "EvaluationCacheMiss"
|
|
547
|
+
|
|
548
|
+
evaluation_id: EvaluationId
|
|
549
|
+
fidelity: str
|
|
550
|
+
|
|
551
|
+
def __post_init__(self) -> None:
|
|
552
|
+
_require_nonempty(self.fidelity, "fidelity")
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
@dataclass(frozen=True, slots=True)
|
|
556
|
+
class EvaluationCacheBypassed(EventPayload):
|
|
557
|
+
EVENT_TYPE: ClassVar[str] = "EvaluationCacheBypassed"
|
|
558
|
+
|
|
559
|
+
evaluation_id: EvaluationId
|
|
560
|
+
fidelity: str
|
|
561
|
+
reason: str
|
|
562
|
+
|
|
563
|
+
def __post_init__(self) -> None:
|
|
564
|
+
_require_nonempty(self.fidelity, "fidelity")
|
|
565
|
+
_require_nonempty(self.reason, "reason")
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@dataclass(frozen=True, slots=True)
|
|
569
|
+
class EvaluationCacheStored(EventPayload):
|
|
570
|
+
EVENT_TYPE: ClassVar[str] = "EvaluationCacheStored"
|
|
571
|
+
|
|
572
|
+
evaluation_id: EvaluationId
|
|
573
|
+
fidelity: str
|
|
574
|
+
cache_key_hash: str
|
|
575
|
+
|
|
576
|
+
def __post_init__(self) -> None:
|
|
577
|
+
_require_nonempty(self.fidelity, "fidelity")
|
|
578
|
+
_require_hash(self.cache_key_hash, "cache_key_hash")
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@dataclass(frozen=True, slots=True)
|
|
582
|
+
class EvaluationStarted(EventPayload):
|
|
583
|
+
EVENT_TYPE: ClassVar[str] = "EvaluationStarted"
|
|
584
|
+
|
|
585
|
+
evaluation_id: EvaluationId
|
|
586
|
+
evaluation_attempt_id: EvaluationAttemptId
|
|
587
|
+
fidelity: str
|
|
588
|
+
attempt_number: int
|
|
589
|
+
|
|
590
|
+
def __post_init__(self) -> None:
|
|
591
|
+
_require_nonempty(self.fidelity, "fidelity")
|
|
592
|
+
_require_positive(self.attempt_number, "attempt_number")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
@dataclass(frozen=True, slots=True)
|
|
596
|
+
class EvaluationCompleted(EventPayload):
|
|
597
|
+
EVENT_TYPE: ClassVar[str] = "EvaluationCompleted"
|
|
598
|
+
|
|
599
|
+
evaluation_id: EvaluationId
|
|
600
|
+
evaluation_attempt_id: EvaluationAttemptId
|
|
601
|
+
fidelity: str
|
|
602
|
+
objective_values: ObjectiveValues
|
|
603
|
+
worker_time_ns: int = 0
|
|
604
|
+
|
|
605
|
+
def __post_init__(self) -> None:
|
|
606
|
+
_require_nonempty(self.fidelity, "fidelity")
|
|
607
|
+
_validate_objective_values(self.objective_values)
|
|
608
|
+
_require_nonnegative(self.worker_time_ns, "worker_time_ns")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@dataclass(frozen=True, slots=True)
|
|
612
|
+
class EvaluationFailed(EventPayload):
|
|
613
|
+
EVENT_TYPE: ClassVar[str] = "EvaluationFailed"
|
|
614
|
+
|
|
615
|
+
evaluation_id: EvaluationId
|
|
616
|
+
fidelity: str
|
|
617
|
+
category: FailureCategory
|
|
618
|
+
code: FailureCode
|
|
619
|
+
retryable: bool
|
|
620
|
+
terminal: bool
|
|
621
|
+
message: str
|
|
622
|
+
evaluation_attempt_id: Optional[EvaluationAttemptId] = None
|
|
623
|
+
worker_time_ns: int = 0
|
|
624
|
+
exception_type: Optional[str] = None
|
|
625
|
+
diagnostics_artifact_id: Optional[ArtifactId] = None
|
|
626
|
+
|
|
627
|
+
def __post_init__(self) -> None:
|
|
628
|
+
_require_nonempty(self.fidelity, "fidelity")
|
|
629
|
+
FailureRecord(
|
|
630
|
+
category=self.category,
|
|
631
|
+
code=self.code,
|
|
632
|
+
message=self.message,
|
|
633
|
+
retryable=self.retryable,
|
|
634
|
+
exception_type=self.exception_type,
|
|
635
|
+
diagnostics_artifact_id=self.diagnostics_artifact_id,
|
|
636
|
+
)
|
|
637
|
+
if not isinstance(self.retryable, bool) or not isinstance(self.terminal, bool):
|
|
638
|
+
raise TypeError("retryable and terminal must be bool")
|
|
639
|
+
if not self.terminal and not self.retryable:
|
|
640
|
+
raise ValueError("a non-terminal failure must be retryable")
|
|
641
|
+
if self.category is FailureCategory.CANDIDATE and not self.terminal:
|
|
642
|
+
raise ValueError("candidate-attributable evaluation failures must be terminal")
|
|
643
|
+
_require_nonnegative(self.worker_time_ns, "worker_time_ns")
|
|
644
|
+
if self.evaluation_attempt_id is None and self.worker_time_ns:
|
|
645
|
+
raise ValueError("worker_time_ns requires an evaluation_attempt_id")
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@dataclass(frozen=True, slots=True)
|
|
649
|
+
class EvaluationRetried(EventPayload):
|
|
650
|
+
EVENT_TYPE: ClassVar[str] = "EvaluationRetried"
|
|
651
|
+
|
|
652
|
+
evaluation_id: EvaluationId
|
|
653
|
+
fidelity: str
|
|
654
|
+
next_attempt_number: int
|
|
655
|
+
|
|
656
|
+
def __post_init__(self) -> None:
|
|
657
|
+
_require_nonempty(self.fidelity, "fidelity")
|
|
658
|
+
_require_positive(self.next_attempt_number, "next_attempt_number")
|
|
659
|
+
if self.next_attempt_number < 2:
|
|
660
|
+
raise ValueError("a retry attempt number must be at least 2")
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@dataclass(frozen=True, slots=True)
|
|
664
|
+
class CandidateAdmitted(EventPayload):
|
|
665
|
+
EVENT_TYPE: ClassVar[str] = "CandidateAdmitted"
|
|
666
|
+
|
|
667
|
+
candidate_id: CandidateId
|
|
668
|
+
evaluation_id: EvaluationId
|
|
669
|
+
archive_name: str = "all_time"
|
|
670
|
+
|
|
671
|
+
def __post_init__(self) -> None:
|
|
672
|
+
_require_nonempty(self.archive_name, "archive_name")
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
EVENT_PAYLOAD_TYPES: Tuple[Type[EventPayload], ...] = (
|
|
676
|
+
ArtifactRegistered,
|
|
677
|
+
RunStarted,
|
|
678
|
+
RunFinished,
|
|
679
|
+
RunAborted,
|
|
680
|
+
GenerationStarted,
|
|
681
|
+
GenerationCompleted,
|
|
682
|
+
OperatorSelected,
|
|
683
|
+
LLMCallRequested,
|
|
684
|
+
LLMRequestArtifactLinked,
|
|
685
|
+
LLMCallStarted,
|
|
686
|
+
LLMCallCompleted,
|
|
687
|
+
LLMCallFailed,
|
|
688
|
+
LLMCallRetried,
|
|
689
|
+
ProviderAttemptStarted,
|
|
690
|
+
ProviderAttemptCompleted,
|
|
691
|
+
ProviderAttemptFailed,
|
|
692
|
+
CandidateProposed,
|
|
693
|
+
DuplicateDetected,
|
|
694
|
+
ValidationStarted,
|
|
695
|
+
ValidationCompleted,
|
|
696
|
+
EvaluationRequested,
|
|
697
|
+
EvaluationCacheHit,
|
|
698
|
+
EvaluationCacheMiss,
|
|
699
|
+
EvaluationCacheBypassed,
|
|
700
|
+
EvaluationCacheStored,
|
|
701
|
+
EvaluationStarted,
|
|
702
|
+
EvaluationCompleted,
|
|
703
|
+
EvaluationFailed,
|
|
704
|
+
EvaluationRetried,
|
|
705
|
+
CandidateAdmitted,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
_INLINE_TEXT_POLICIES: Dict[Type[EventPayload], Dict[str, InlineTextPolicy]] = {
|
|
710
|
+
ArtifactRegistered: {
|
|
711
|
+
"sha256_hex": InlineTextPolicy.SHA256,
|
|
712
|
+
"media_type": InlineTextPolicy.MEDIA_TYPE,
|
|
713
|
+
"minimization_policy_id": InlineTextPolicy.POLICY_COMPONENT,
|
|
714
|
+
"minimization_policy_version": InlineTextPolicy.POLICY_COMPONENT,
|
|
715
|
+
"minimization_policy_config_sha256": InlineTextPolicy.SHA256,
|
|
716
|
+
"sanitization_policy_id": InlineTextPolicy.POLICY_COMPONENT,
|
|
717
|
+
"sanitization_policy_version": InlineTextPolicy.POLICY_COMPONENT,
|
|
718
|
+
},
|
|
719
|
+
RunStarted: {"experiment_spec_hash": InlineTextPolicy.SHA256},
|
|
720
|
+
RunFinished: {"stop_reason": InlineTextPolicy.SAFE_SUMMARY},
|
|
721
|
+
RunAborted: {
|
|
722
|
+
"message": InlineTextPolicy.SAFE_SUMMARY,
|
|
723
|
+
"exception_type": InlineTextPolicy.EXCEPTION_TYPE,
|
|
724
|
+
},
|
|
725
|
+
OperatorSelected: {
|
|
726
|
+
"operator_name": InlineTextPolicy.METADATA_TOKEN,
|
|
727
|
+
"operator_version": InlineTextPolicy.METADATA_TOKEN,
|
|
728
|
+
},
|
|
729
|
+
LLMCallRequested: {
|
|
730
|
+
"operation": InlineTextPolicy.METADATA_TOKEN,
|
|
731
|
+
"requested_model": InlineTextPolicy.ROUTING_LABEL,
|
|
732
|
+
},
|
|
733
|
+
LLMCallFailed: {
|
|
734
|
+
"message": InlineTextPolicy.SAFE_SUMMARY,
|
|
735
|
+
"exception_type": InlineTextPolicy.EXCEPTION_TYPE,
|
|
736
|
+
},
|
|
737
|
+
ProviderAttemptCompleted: {
|
|
738
|
+
"resolved_provider": InlineTextPolicy.ROUTING_LABEL,
|
|
739
|
+
"resolved_model": InlineTextPolicy.ROUTING_LABEL,
|
|
740
|
+
},
|
|
741
|
+
ProviderAttemptFailed: {
|
|
742
|
+
"message": InlineTextPolicy.SAFE_SUMMARY,
|
|
743
|
+
"exception_type": InlineTextPolicy.EXCEPTION_TYPE,
|
|
744
|
+
},
|
|
745
|
+
CandidateProposed: {"content_hash": InlineTextPolicy.SHA256},
|
|
746
|
+
DuplicateDetected: {
|
|
747
|
+
"content_hash": InlineTextPolicy.SHA256,
|
|
748
|
+
"reason": InlineTextPolicy.METADATA_TOKEN,
|
|
749
|
+
},
|
|
750
|
+
EvaluationRequested: {
|
|
751
|
+
"fidelity": InlineTextPolicy.METADATA_TOKEN,
|
|
752
|
+
"cache_key_hash": InlineTextPolicy.SHA256,
|
|
753
|
+
},
|
|
754
|
+
EvaluationCacheHit: {"fidelity": InlineTextPolicy.METADATA_TOKEN},
|
|
755
|
+
EvaluationCacheMiss: {"fidelity": InlineTextPolicy.METADATA_TOKEN},
|
|
756
|
+
EvaluationCacheBypassed: {
|
|
757
|
+
"fidelity": InlineTextPolicy.METADATA_TOKEN,
|
|
758
|
+
"reason": InlineTextPolicy.SAFE_SUMMARY,
|
|
759
|
+
},
|
|
760
|
+
EvaluationCacheStored: {
|
|
761
|
+
"fidelity": InlineTextPolicy.METADATA_TOKEN,
|
|
762
|
+
"cache_key_hash": InlineTextPolicy.SHA256,
|
|
763
|
+
},
|
|
764
|
+
EvaluationStarted: {"fidelity": InlineTextPolicy.METADATA_TOKEN},
|
|
765
|
+
EvaluationCompleted: {"fidelity": InlineTextPolicy.METADATA_TOKEN},
|
|
766
|
+
EvaluationFailed: {
|
|
767
|
+
"fidelity": InlineTextPolicy.METADATA_TOKEN,
|
|
768
|
+
"message": InlineTextPolicy.SAFE_SUMMARY,
|
|
769
|
+
"exception_type": InlineTextPolicy.EXCEPTION_TYPE,
|
|
770
|
+
},
|
|
771
|
+
EvaluationRetried: {"fidelity": InlineTextPolicy.METADATA_TOKEN},
|
|
772
|
+
CandidateAdmitted: {"archive_name": InlineTextPolicy.METADATA_TOKEN},
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _annotation_is_optional_or_required_string(annotation: Any) -> bool:
|
|
777
|
+
if annotation is str:
|
|
778
|
+
return True
|
|
779
|
+
origin = get_origin(annotation)
|
|
780
|
+
args = get_args(annotation)
|
|
781
|
+
return origin in (Union, types.UnionType) and set(args) == {str, type(None)}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _annotation_contains_string(annotation: Any) -> bool:
|
|
785
|
+
if annotation is str:
|
|
786
|
+
return True
|
|
787
|
+
return any(_annotation_contains_string(arg) for arg in get_args(annotation))
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _annotation_contains_type(annotation: Any, expected: Any) -> bool:
|
|
791
|
+
if annotation is expected:
|
|
792
|
+
return True
|
|
793
|
+
return any(
|
|
794
|
+
_annotation_contains_type(argument, expected)
|
|
795
|
+
for argument in get_args(annotation)
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _annotation_contains_tuple(annotation: Any) -> bool:
|
|
800
|
+
if get_origin(annotation) is tuple:
|
|
801
|
+
return True
|
|
802
|
+
return any(_annotation_contains_tuple(argument) for argument in get_args(annotation))
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _validate_supported_value_annotation(annotation: Any) -> None:
|
|
806
|
+
origin = get_origin(annotation)
|
|
807
|
+
arguments = get_args(annotation)
|
|
808
|
+
if origin in (Union, types.UnionType):
|
|
809
|
+
if len(arguments) != 2 or type(None) not in arguments:
|
|
810
|
+
raise RuntimeError("event unions must be a scalar optional value")
|
|
811
|
+
for argument in arguments:
|
|
812
|
+
if argument is not type(None):
|
|
813
|
+
_validate_supported_value_annotation(argument)
|
|
814
|
+
return
|
|
815
|
+
if origin is tuple:
|
|
816
|
+
for argument in arguments:
|
|
817
|
+
if argument is not Ellipsis:
|
|
818
|
+
_validate_supported_value_annotation(argument)
|
|
819
|
+
return
|
|
820
|
+
if origin is not None:
|
|
821
|
+
raise RuntimeError("event containers require an explicit codec design")
|
|
822
|
+
if annotation in (str, bool, int, float, Decimal, type(None)):
|
|
823
|
+
return
|
|
824
|
+
if annotation in ID_TYPES:
|
|
825
|
+
return
|
|
826
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
827
|
+
return
|
|
828
|
+
raise RuntimeError("event field annotation has no durable codec policy")
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
_NESTED_STRING_FIELDS = {
|
|
832
|
+
(EvaluationCacheHit, "objective_values"),
|
|
833
|
+
(EvaluationCompleted, "objective_values"),
|
|
834
|
+
}
|
|
835
|
+
_CONTAINER_FIELDS = frozenset(_NESTED_STRING_FIELDS)
|
|
836
|
+
_DECIMAL_TEXT_FIELDS = {(ProviderAttemptCompleted, "cost_usd")}
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def validate_event_value_schema() -> None:
|
|
840
|
+
"""Audit every payload path that can serialize to durable JSON text."""
|
|
841
|
+
|
|
842
|
+
for payload_type in EVENT_PAYLOAD_TYPES:
|
|
843
|
+
try:
|
|
844
|
+
validate_inline_text(
|
|
845
|
+
payload_type.EVENT_TYPE,
|
|
846
|
+
field_name="event type",
|
|
847
|
+
policy=InlineTextPolicy.METADATA_TOKEN,
|
|
848
|
+
)
|
|
849
|
+
except (TypeError, ValueError):
|
|
850
|
+
valid_event_type = False
|
|
851
|
+
else:
|
|
852
|
+
valid_event_type = True
|
|
853
|
+
if not valid_event_type:
|
|
854
|
+
raise RuntimeError("event type violates the durable metadata policy")
|
|
855
|
+
|
|
856
|
+
hints = get_type_hints(payload_type)
|
|
857
|
+
for field in fields(payload_type):
|
|
858
|
+
annotation = hints[field.name]
|
|
859
|
+
_validate_supported_value_annotation(annotation)
|
|
860
|
+
path = (payload_type, field.name)
|
|
861
|
+
contains_nested_string = (
|
|
862
|
+
_annotation_contains_string(annotation)
|
|
863
|
+
and not _annotation_is_optional_or_required_string(annotation)
|
|
864
|
+
)
|
|
865
|
+
if contains_nested_string != (path in _NESTED_STRING_FIELDS):
|
|
866
|
+
raise RuntimeError(
|
|
867
|
+
"non-scalar event string fields require an explicit policy"
|
|
868
|
+
)
|
|
869
|
+
if _annotation_contains_tuple(annotation) != (path in _CONTAINER_FIELDS):
|
|
870
|
+
raise RuntimeError(
|
|
871
|
+
"event container fields require an explicit immutable policy"
|
|
872
|
+
)
|
|
873
|
+
contains_decimal = _annotation_contains_type(annotation, Decimal)
|
|
874
|
+
if contains_decimal != (path in _DECIMAL_TEXT_FIELDS):
|
|
875
|
+
raise RuntimeError(
|
|
876
|
+
"Decimal event fields require an explicit bounded-text policy"
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
for nested_type in _iter_annotation_leaf_types(annotation):
|
|
880
|
+
if not (
|
|
881
|
+
isinstance(nested_type, type)
|
|
882
|
+
and issubclass(nested_type, Enum)
|
|
883
|
+
):
|
|
884
|
+
continue
|
|
885
|
+
for member in nested_type:
|
|
886
|
+
try:
|
|
887
|
+
validate_inline_text(
|
|
888
|
+
member.value,
|
|
889
|
+
field_name="enum value",
|
|
890
|
+
policy=InlineTextPolicy.ENUM_VALUE,
|
|
891
|
+
)
|
|
892
|
+
except (TypeError, ValueError):
|
|
893
|
+
valid_member = False
|
|
894
|
+
else:
|
|
895
|
+
valid_member = True
|
|
896
|
+
if not valid_member:
|
|
897
|
+
raise RuntimeError(
|
|
898
|
+
"event enum value violates the durable metadata policy"
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _iter_annotation_leaf_types(annotation: Any) -> Tuple[Any, ...]:
|
|
903
|
+
arguments = get_args(annotation)
|
|
904
|
+
if not arguments:
|
|
905
|
+
return (annotation,)
|
|
906
|
+
leaves = []
|
|
907
|
+
for argument in arguments:
|
|
908
|
+
if argument is not Ellipsis:
|
|
909
|
+
leaves.extend(_iter_annotation_leaf_types(argument))
|
|
910
|
+
return tuple(leaves)
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def validate_inline_text_schema() -> None:
|
|
914
|
+
"""Require an explicit storage policy for every direct event string field."""
|
|
915
|
+
|
|
916
|
+
validate_event_value_schema()
|
|
917
|
+
payload_types = set(EVENT_PAYLOAD_TYPES)
|
|
918
|
+
if set(_INLINE_TEXT_POLICIES) - payload_types:
|
|
919
|
+
raise RuntimeError("inline-text policy references an unregistered payload")
|
|
920
|
+
for payload_type in EVENT_PAYLOAD_TYPES:
|
|
921
|
+
hints = get_type_hints(payload_type)
|
|
922
|
+
nested_string_fields = {
|
|
923
|
+
field.name
|
|
924
|
+
for field in fields(payload_type)
|
|
925
|
+
if _annotation_contains_string(hints[field.name])
|
|
926
|
+
and not _annotation_is_optional_or_required_string(hints[field.name])
|
|
927
|
+
}
|
|
928
|
+
allowed_nested = {
|
|
929
|
+
field_name
|
|
930
|
+
for declared_type, field_name in _NESTED_STRING_FIELDS
|
|
931
|
+
if declared_type is payload_type
|
|
932
|
+
}
|
|
933
|
+
if nested_string_fields != allowed_nested:
|
|
934
|
+
raise RuntimeError(
|
|
935
|
+
"non-scalar event string fields require an explicit storage policy"
|
|
936
|
+
)
|
|
937
|
+
string_fields = {
|
|
938
|
+
field.name
|
|
939
|
+
for field in fields(payload_type)
|
|
940
|
+
if _annotation_is_optional_or_required_string(hints[field.name])
|
|
941
|
+
}
|
|
942
|
+
declared = _INLINE_TEXT_POLICIES.get(payload_type, {})
|
|
943
|
+
if string_fields != set(declared):
|
|
944
|
+
raise RuntimeError(
|
|
945
|
+
"event inline-text fields and policies do not match for "
|
|
946
|
+
f"{payload_type.EVENT_TYPE}"
|
|
947
|
+
)
|
|
948
|
+
if any(not isinstance(policy, InlineTextPolicy) for policy in declared.values()):
|
|
949
|
+
raise RuntimeError("event inline-text policies must use InlineTextPolicy")
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _validate_payload_inline_text(payload: EventPayload) -> None:
|
|
953
|
+
policies = _INLINE_TEXT_POLICIES.get(type(payload), {})
|
|
954
|
+
for field_name, policy in policies.items():
|
|
955
|
+
value = getattr(payload, field_name)
|
|
956
|
+
if value is None:
|
|
957
|
+
continue
|
|
958
|
+
validate_inline_text(value, field_name=field_name, policy=policy)
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
validate_inline_text_schema()
|
|
962
|
+
_PAYLOAD_BY_EVENT_TYPE: Dict[str, Type[EventPayload]] = {
|
|
963
|
+
payload_type.EVENT_TYPE: payload_type for payload_type in EVENT_PAYLOAD_TYPES
|
|
964
|
+
}
|
|
965
|
+
if len(_PAYLOAD_BY_EVENT_TYPE) != len(EVENT_PAYLOAD_TYPES): # pragma: no cover
|
|
966
|
+
raise RuntimeError("Event payload types must have unique EVENT_TYPE values")
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
@dataclass(frozen=True, slots=True)
|
|
970
|
+
class EventEnvelope:
|
|
971
|
+
schema_version: int
|
|
972
|
+
event_id: EventId
|
|
973
|
+
run_id: RunId
|
|
974
|
+
sequence_number: int
|
|
975
|
+
event_type: str
|
|
976
|
+
wall_timestamp_utc: datetime
|
|
977
|
+
monotonic_offset_ns: int
|
|
978
|
+
correlation_id: Optional[CorrelationId]
|
|
979
|
+
causation_event_id: Optional[EventId]
|
|
980
|
+
payload: EventPayload
|
|
981
|
+
|
|
982
|
+
def __post_init__(self) -> None:
|
|
983
|
+
if type(self.schema_version) is not int:
|
|
984
|
+
raise TypeError("schema_version must be an integer")
|
|
985
|
+
if self.schema_version != CURRENT_EVENT_SCHEMA_VERSION:
|
|
986
|
+
raise ValueError(
|
|
987
|
+
f"Unsupported event schema version {self.schema_version!r}; "
|
|
988
|
+
f"supported={CURRENT_EVENT_SCHEMA_VERSION}"
|
|
989
|
+
)
|
|
990
|
+
_require_positive(self.sequence_number, "sequence_number")
|
|
991
|
+
_require_nonnegative(self.monotonic_offset_ns, "monotonic_offset_ns")
|
|
992
|
+
if type(self.event_id) is not EventId or not _payload_field_round_trip_is_valid(
|
|
993
|
+
EventId,
|
|
994
|
+
self.event_id,
|
|
995
|
+
"event_id",
|
|
996
|
+
):
|
|
997
|
+
raise TypeError("event_id must be an EventId")
|
|
998
|
+
if type(self.run_id) is not RunId or not _payload_field_round_trip_is_valid(
|
|
999
|
+
RunId,
|
|
1000
|
+
self.run_id,
|
|
1001
|
+
"run_id",
|
|
1002
|
+
):
|
|
1003
|
+
raise TypeError("run_id must be a RunId")
|
|
1004
|
+
if self.correlation_id is not None and (
|
|
1005
|
+
type(self.correlation_id) is not CorrelationId
|
|
1006
|
+
or not _payload_field_round_trip_is_valid(
|
|
1007
|
+
CorrelationId,
|
|
1008
|
+
self.correlation_id,
|
|
1009
|
+
"correlation_id",
|
|
1010
|
+
)
|
|
1011
|
+
):
|
|
1012
|
+
raise TypeError("correlation_id must be a CorrelationId or None")
|
|
1013
|
+
if self.causation_event_id is not None and (
|
|
1014
|
+
type(self.causation_event_id) is not EventId
|
|
1015
|
+
or not _payload_field_round_trip_is_valid(
|
|
1016
|
+
EventId,
|
|
1017
|
+
self.causation_event_id,
|
|
1018
|
+
"causation_event_id",
|
|
1019
|
+
)
|
|
1020
|
+
):
|
|
1021
|
+
raise TypeError("causation_event_id must be an EventId or None")
|
|
1022
|
+
if type(self.payload) not in EVENT_PAYLOAD_TYPES:
|
|
1023
|
+
raise TypeError("payload must be a registered EventPayload")
|
|
1024
|
+
if type(self.event_type) is not str:
|
|
1025
|
+
raise TypeError("event_type must be an exact string")
|
|
1026
|
+
validate_inline_text(
|
|
1027
|
+
self.event_type,
|
|
1028
|
+
field_name="event_type",
|
|
1029
|
+
policy=InlineTextPolicy.METADATA_TOKEN,
|
|
1030
|
+
)
|
|
1031
|
+
payload_event_type = type(self.payload).EVENT_TYPE
|
|
1032
|
+
if type(payload_event_type) is not str:
|
|
1033
|
+
raise TypeError("payload event type must be an exact string")
|
|
1034
|
+
validate_inline_text(
|
|
1035
|
+
payload_event_type,
|
|
1036
|
+
field_name="payload event type",
|
|
1037
|
+
policy=InlineTextPolicy.METADATA_TOKEN,
|
|
1038
|
+
)
|
|
1039
|
+
_validate_payload_inline_text(self.payload)
|
|
1040
|
+
hints = get_type_hints(type(self.payload))
|
|
1041
|
+
for field in fields(self.payload):
|
|
1042
|
+
# Encoding followed by typed decoding is also the runtime type check
|
|
1043
|
+
# for direct (non-JSON) construction. It prevents, for example, a
|
|
1044
|
+
# RunId from being stored in a CandidateId field.
|
|
1045
|
+
if not _payload_field_round_trip_is_valid(
|
|
1046
|
+
hints[field.name],
|
|
1047
|
+
getattr(self.payload, field.name),
|
|
1048
|
+
field.name,
|
|
1049
|
+
):
|
|
1050
|
+
raise TypeError(f"Invalid payload field {field.name}")
|
|
1051
|
+
if self.event_type != payload_event_type:
|
|
1052
|
+
raise ValueError("event_type does not match payload type")
|
|
1053
|
+
if type(self.wall_timestamp_utc) is not datetime:
|
|
1054
|
+
raise TypeError("wall_timestamp_utc must be an exact datetime")
|
|
1055
|
+
if self.wall_timestamp_utc.tzinfo is not timezone.utc:
|
|
1056
|
+
raise ValueError("wall_timestamp_utc must use the canonical UTC timezone")
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
_ENVELOPE_FIELDS = {
|
|
1060
|
+
"schema_version",
|
|
1061
|
+
"event_id",
|
|
1062
|
+
"run_id",
|
|
1063
|
+
"sequence_number",
|
|
1064
|
+
"event_type",
|
|
1065
|
+
"wall_timestamp_utc",
|
|
1066
|
+
"monotonic_offset_ns",
|
|
1067
|
+
"correlation_id",
|
|
1068
|
+
"causation_event_id",
|
|
1069
|
+
"payload",
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _encode_value(value: Any) -> Any:
|
|
1074
|
+
if isinstance(value, StableId):
|
|
1075
|
+
return value.value
|
|
1076
|
+
if isinstance(value, Enum):
|
|
1077
|
+
return value.value
|
|
1078
|
+
if isinstance(value, Decimal):
|
|
1079
|
+
if not _is_bounded_finite_decimal(value):
|
|
1080
|
+
raise EventCodecError("Decimal exceeds the durable numeric-text policy")
|
|
1081
|
+
encoded = format(value, "f")
|
|
1082
|
+
if (
|
|
1083
|
+
len(encoded) > _MAX_DECIMAL_TEXT_LENGTH
|
|
1084
|
+
or _DECIMAL_TEXT.fullmatch(encoded) is None
|
|
1085
|
+
):
|
|
1086
|
+
raise EventCodecError("Decimal exceeds the durable numeric-text policy")
|
|
1087
|
+
return encoded
|
|
1088
|
+
if isinstance(value, tuple):
|
|
1089
|
+
return [_encode_value(item) for item in value]
|
|
1090
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
1091
|
+
return value
|
|
1092
|
+
raise EventCodecError(f"Unsupported event payload value {type(value).__name__}")
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def _payload_field_round_trip_is_valid(
|
|
1096
|
+
annotation: Any,
|
|
1097
|
+
value: Any,
|
|
1098
|
+
field_name: str,
|
|
1099
|
+
) -> bool:
|
|
1100
|
+
try:
|
|
1101
|
+
_decode_value(annotation, _encode_value(value), field_name)
|
|
1102
|
+
except Exception:
|
|
1103
|
+
return False
|
|
1104
|
+
return True
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _decode_value(annotation: Any, value: Any, field_name: str) -> Any:
|
|
1108
|
+
origin = get_origin(annotation)
|
|
1109
|
+
args = get_args(annotation)
|
|
1110
|
+
|
|
1111
|
+
if origin in (Union, types.UnionType):
|
|
1112
|
+
if value is None and type(None) in args:
|
|
1113
|
+
return None
|
|
1114
|
+
non_none = [arg for arg in args if arg is not type(None)]
|
|
1115
|
+
if len(non_none) != 1:
|
|
1116
|
+
raise EventCodecError(f"Unsupported union annotation for {field_name}")
|
|
1117
|
+
return _decode_value(non_none[0], value, field_name)
|
|
1118
|
+
|
|
1119
|
+
if origin is tuple:
|
|
1120
|
+
if type(value) is not list:
|
|
1121
|
+
raise EventCodecError(f"{field_name} must be a JSON array")
|
|
1122
|
+
if len(args) == 2 and args[1] is Ellipsis:
|
|
1123
|
+
return tuple(_decode_value(args[0], item, field_name) for item in value)
|
|
1124
|
+
if len(args) != len(value):
|
|
1125
|
+
raise EventCodecError(f"{field_name} has the wrong tuple length")
|
|
1126
|
+
return tuple(
|
|
1127
|
+
_decode_value(item_type, item, field_name)
|
|
1128
|
+
for item_type, item in zip(args, value)
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
if isinstance(annotation, type) and issubclass(annotation, StableId):
|
|
1132
|
+
if type(value) is not str:
|
|
1133
|
+
raise EventCodecError(f"{field_name} must be a string ID")
|
|
1134
|
+
try:
|
|
1135
|
+
decoded_id = annotation(value)
|
|
1136
|
+
except (TypeError, ValueError):
|
|
1137
|
+
decoded_id = _DECODE_FAILED
|
|
1138
|
+
if decoded_id is _DECODE_FAILED:
|
|
1139
|
+
raise EventCodecError(f"Invalid durable ID for {field_name}")
|
|
1140
|
+
return decoded_id
|
|
1141
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
1142
|
+
try:
|
|
1143
|
+
decoded_enum = annotation(value)
|
|
1144
|
+
except Exception:
|
|
1145
|
+
decoded_enum = _DECODE_FAILED
|
|
1146
|
+
if decoded_enum is _DECODE_FAILED:
|
|
1147
|
+
raise EventCodecError(f"Invalid enum value for {field_name}")
|
|
1148
|
+
return decoded_enum
|
|
1149
|
+
if annotation is Decimal:
|
|
1150
|
+
if (
|
|
1151
|
+
type(value) is not str
|
|
1152
|
+
or len(value) > _MAX_DECIMAL_TEXT_LENGTH
|
|
1153
|
+
or _DECIMAL_TEXT.fullmatch(value) is None
|
|
1154
|
+
):
|
|
1155
|
+
raise EventCodecError(f"{field_name} Decimal must be encoded as a string")
|
|
1156
|
+
try:
|
|
1157
|
+
decoded_decimal = Decimal(value)
|
|
1158
|
+
except Exception:
|
|
1159
|
+
decoded_decimal = _DECODE_FAILED
|
|
1160
|
+
if (
|
|
1161
|
+
decoded_decimal is _DECODE_FAILED
|
|
1162
|
+
or not _is_bounded_finite_decimal(decoded_decimal)
|
|
1163
|
+
or format(decoded_decimal, "f") != value
|
|
1164
|
+
):
|
|
1165
|
+
raise EventCodecError(f"Invalid bounded Decimal for {field_name}")
|
|
1166
|
+
return decoded_decimal
|
|
1167
|
+
if annotation is str:
|
|
1168
|
+
if type(value) is not str:
|
|
1169
|
+
raise EventCodecError(f"{field_name} must be a string")
|
|
1170
|
+
return value
|
|
1171
|
+
if annotation is bool:
|
|
1172
|
+
if not isinstance(value, bool):
|
|
1173
|
+
raise EventCodecError(f"{field_name} must be bool")
|
|
1174
|
+
return value
|
|
1175
|
+
if annotation is int:
|
|
1176
|
+
if type(value) is not int:
|
|
1177
|
+
raise EventCodecError(f"{field_name} must be an integer")
|
|
1178
|
+
return value
|
|
1179
|
+
if annotation is float:
|
|
1180
|
+
if type(value) not in (int, float):
|
|
1181
|
+
raise EventCodecError(f"{field_name} must be numeric")
|
|
1182
|
+
return float(value)
|
|
1183
|
+
raise EventCodecError(f"Unsupported annotation {annotation!r} for {field_name}")
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _payload_to_record(payload: EventPayload) -> Dict[str, Any]:
|
|
1187
|
+
return {field.name: _encode_value(getattr(payload, field.name)) for field in fields(payload)}
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def _payload_from_record(event_type: str, record: Any) -> EventPayload:
|
|
1191
|
+
payload_type = _PAYLOAD_BY_EVENT_TYPE.get(event_type)
|
|
1192
|
+
if payload_type is None:
|
|
1193
|
+
raise EventCodecError("Unsupported event_type")
|
|
1194
|
+
if type(record) is not dict:
|
|
1195
|
+
raise EventCodecError("payload must be a JSON object")
|
|
1196
|
+
if any(type(key) is not str for key in record):
|
|
1197
|
+
raise EventCodecError("payload keys must be exact strings")
|
|
1198
|
+
expected = {field.name for field in fields(payload_type)}
|
|
1199
|
+
if set(record) != expected:
|
|
1200
|
+
raise EventCodecError("Invalid payload fields")
|
|
1201
|
+
hints = get_type_hints(payload_type)
|
|
1202
|
+
try:
|
|
1203
|
+
kwargs = {
|
|
1204
|
+
name: _decode_value(hints[name], record[name], name)
|
|
1205
|
+
for name in expected
|
|
1206
|
+
}
|
|
1207
|
+
except Exception:
|
|
1208
|
+
kwargs = None
|
|
1209
|
+
if kwargs is None:
|
|
1210
|
+
raise EventCodecError("Invalid payload field encoding")
|
|
1211
|
+
try:
|
|
1212
|
+
payload = payload_type(**kwargs)
|
|
1213
|
+
except Exception:
|
|
1214
|
+
payload = None
|
|
1215
|
+
if payload is None:
|
|
1216
|
+
raise EventCodecError("Invalid event payload")
|
|
1217
|
+
return payload
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def _try_snapshot_payload(
|
|
1221
|
+
event_type: str,
|
|
1222
|
+
payload: EventPayload,
|
|
1223
|
+
) -> EventPayload | object:
|
|
1224
|
+
try:
|
|
1225
|
+
return _payload_from_record(event_type, _payload_to_record(payload))
|
|
1226
|
+
except Exception:
|
|
1227
|
+
# Return inside the handler so secret-bearing extension exceptions do
|
|
1228
|
+
# not become context on the public snapshot/codec error.
|
|
1229
|
+
return _DECODE_FAILED
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def _try_construct_event_snapshot(
|
|
1233
|
+
event: EventEnvelope,
|
|
1234
|
+
payload: EventPayload,
|
|
1235
|
+
) -> EventEnvelope | object:
|
|
1236
|
+
try:
|
|
1237
|
+
source_timestamp = event.wall_timestamp_utc
|
|
1238
|
+
timestamp = datetime(
|
|
1239
|
+
source_timestamp.year,
|
|
1240
|
+
source_timestamp.month,
|
|
1241
|
+
source_timestamp.day,
|
|
1242
|
+
source_timestamp.hour,
|
|
1243
|
+
source_timestamp.minute,
|
|
1244
|
+
source_timestamp.second,
|
|
1245
|
+
source_timestamp.microsecond,
|
|
1246
|
+
tzinfo=timezone.utc,
|
|
1247
|
+
fold=source_timestamp.fold,
|
|
1248
|
+
)
|
|
1249
|
+
return EventEnvelope(
|
|
1250
|
+
schema_version=event.schema_version,
|
|
1251
|
+
event_id=EventId(event.event_id.value),
|
|
1252
|
+
run_id=RunId(event.run_id.value),
|
|
1253
|
+
sequence_number=event.sequence_number,
|
|
1254
|
+
event_type=event.event_type,
|
|
1255
|
+
wall_timestamp_utc=timestamp,
|
|
1256
|
+
monotonic_offset_ns=event.monotonic_offset_ns,
|
|
1257
|
+
correlation_id=(
|
|
1258
|
+
CorrelationId(event.correlation_id.value)
|
|
1259
|
+
if event.correlation_id is not None
|
|
1260
|
+
else None
|
|
1261
|
+
),
|
|
1262
|
+
causation_event_id=(
|
|
1263
|
+
EventId(event.causation_event_id.value)
|
|
1264
|
+
if event.causation_event_id is not None
|
|
1265
|
+
else None
|
|
1266
|
+
),
|
|
1267
|
+
payload=payload,
|
|
1268
|
+
)
|
|
1269
|
+
except Exception:
|
|
1270
|
+
return _DECODE_FAILED
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
def validated_event_snapshot(event: EventEnvelope) -> EventEnvelope:
|
|
1274
|
+
"""Return an independently reconstructed event safe for persistence.
|
|
1275
|
+
|
|
1276
|
+
Frozen dataclasses can be mutated through low-level APIs, so adapters and
|
|
1277
|
+
serializers never trust a caller-owned instance merely because its original
|
|
1278
|
+
constructor ran.
|
|
1279
|
+
"""
|
|
1280
|
+
|
|
1281
|
+
if type(event) is not EventEnvelope:
|
|
1282
|
+
raise EventCodecError("event must be an exact EventEnvelope")
|
|
1283
|
+
if (
|
|
1284
|
+
type(event.schema_version) is not int
|
|
1285
|
+
or type(event.event_id) is not EventId
|
|
1286
|
+
or type(event.run_id) is not RunId
|
|
1287
|
+
or type(event.sequence_number) is not int
|
|
1288
|
+
or type(event.event_type) is not str
|
|
1289
|
+
or type(event.wall_timestamp_utc) is not datetime
|
|
1290
|
+
or event.wall_timestamp_utc.tzinfo is not timezone.utc
|
|
1291
|
+
or type(event.monotonic_offset_ns) is not int
|
|
1292
|
+
or (
|
|
1293
|
+
event.correlation_id is not None
|
|
1294
|
+
and type(event.correlation_id) is not CorrelationId
|
|
1295
|
+
)
|
|
1296
|
+
or (
|
|
1297
|
+
event.causation_event_id is not None
|
|
1298
|
+
and type(event.causation_event_id) is not EventId
|
|
1299
|
+
)
|
|
1300
|
+
or type(event.payload) not in EVENT_PAYLOAD_TYPES
|
|
1301
|
+
):
|
|
1302
|
+
raise EventCodecError("event violates the canonical envelope policy")
|
|
1303
|
+
payload = _try_snapshot_payload(event.event_type, event.payload)
|
|
1304
|
+
if payload is _DECODE_FAILED:
|
|
1305
|
+
raise EventCodecError("event payload could not be canonically reconstructed")
|
|
1306
|
+
assert isinstance(payload, EventPayload)
|
|
1307
|
+
snapshot = _try_construct_event_snapshot(event, payload)
|
|
1308
|
+
if snapshot is _DECODE_FAILED:
|
|
1309
|
+
raise EventCodecError("event could not be canonically reconstructed")
|
|
1310
|
+
assert isinstance(snapshot, EventEnvelope)
|
|
1311
|
+
return snapshot
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def event_to_record(event: EventEnvelope) -> Dict[str, Any]:
|
|
1315
|
+
"""Return the schema-stable JSON-compatible representation of *event*."""
|
|
1316
|
+
|
|
1317
|
+
snapshot = validated_event_snapshot(event)
|
|
1318
|
+
timestamp = snapshot.wall_timestamp_utc.isoformat().replace(
|
|
1319
|
+
"+00:00", "Z"
|
|
1320
|
+
)
|
|
1321
|
+
return {
|
|
1322
|
+
"schema_version": snapshot.schema_version,
|
|
1323
|
+
"event_id": snapshot.event_id.value,
|
|
1324
|
+
"run_id": snapshot.run_id.value,
|
|
1325
|
+
"sequence_number": snapshot.sequence_number,
|
|
1326
|
+
"event_type": snapshot.event_type,
|
|
1327
|
+
"wall_timestamp_utc": timestamp,
|
|
1328
|
+
"monotonic_offset_ns": snapshot.monotonic_offset_ns,
|
|
1329
|
+
"correlation_id": (
|
|
1330
|
+
snapshot.correlation_id.value
|
|
1331
|
+
if snapshot.correlation_id is not None
|
|
1332
|
+
else None
|
|
1333
|
+
),
|
|
1334
|
+
"causation_event_id": (
|
|
1335
|
+
snapshot.causation_event_id.value
|
|
1336
|
+
if snapshot.causation_event_id is not None
|
|
1337
|
+
else None
|
|
1338
|
+
),
|
|
1339
|
+
"payload": _payload_to_record(snapshot.payload),
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def event_from_record(record: Any) -> EventEnvelope:
|
|
1344
|
+
"""Decode and validate one event record."""
|
|
1345
|
+
|
|
1346
|
+
if type(record) is not dict:
|
|
1347
|
+
raise EventCodecError("event record must be a JSON object")
|
|
1348
|
+
if any(type(key) is not str for key in record):
|
|
1349
|
+
raise EventCodecError("event envelope keys must be exact strings")
|
|
1350
|
+
if set(record) != _ENVELOPE_FIELDS:
|
|
1351
|
+
raise EventCodecError("Invalid envelope fields")
|
|
1352
|
+
schema_version = record["schema_version"]
|
|
1353
|
+
if type(schema_version) is not int:
|
|
1354
|
+
raise EventCodecError("schema_version must be an integer")
|
|
1355
|
+
if schema_version != CURRENT_EVENT_SCHEMA_VERSION:
|
|
1356
|
+
raise EventCodecError(
|
|
1357
|
+
f"Unsupported event schema version {schema_version!r}; "
|
|
1358
|
+
f"supported={CURRENT_EVENT_SCHEMA_VERSION}"
|
|
1359
|
+
)
|
|
1360
|
+
event_type = record["event_type"]
|
|
1361
|
+
if type(event_type) is not str:
|
|
1362
|
+
raise EventCodecError("event_type must be a string")
|
|
1363
|
+
timestamp_text = record["wall_timestamp_utc"]
|
|
1364
|
+
if type(timestamp_text) is not str:
|
|
1365
|
+
raise EventCodecError("wall_timestamp_utc must be a string")
|
|
1366
|
+
try:
|
|
1367
|
+
timestamp = datetime.fromisoformat(timestamp_text.replace("Z", "+00:00"))
|
|
1368
|
+
except ValueError:
|
|
1369
|
+
timestamp = None
|
|
1370
|
+
if timestamp is None:
|
|
1371
|
+
raise EventCodecError("wall_timestamp_utc is not ISO-8601")
|
|
1372
|
+
try:
|
|
1373
|
+
payload = _payload_from_record(event_type, record["payload"])
|
|
1374
|
+
envelope = EventEnvelope(
|
|
1375
|
+
schema_version=schema_version,
|
|
1376
|
+
event_id=EventId(record["event_id"]),
|
|
1377
|
+
run_id=RunId(record["run_id"]),
|
|
1378
|
+
sequence_number=record["sequence_number"],
|
|
1379
|
+
event_type=event_type,
|
|
1380
|
+
wall_timestamp_utc=timestamp,
|
|
1381
|
+
monotonic_offset_ns=record["monotonic_offset_ns"],
|
|
1382
|
+
correlation_id=(
|
|
1383
|
+
CorrelationId(record["correlation_id"])
|
|
1384
|
+
if record["correlation_id"] is not None
|
|
1385
|
+
else None
|
|
1386
|
+
),
|
|
1387
|
+
causation_event_id=(
|
|
1388
|
+
EventId(record["causation_event_id"])
|
|
1389
|
+
if record["causation_event_id"] is not None
|
|
1390
|
+
else None
|
|
1391
|
+
),
|
|
1392
|
+
payload=payload,
|
|
1393
|
+
)
|
|
1394
|
+
except Exception:
|
|
1395
|
+
envelope = None
|
|
1396
|
+
if envelope is None:
|
|
1397
|
+
raise EventCodecError("Invalid event envelope")
|
|
1398
|
+
return envelope
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
def event_to_json(event: EventEnvelope) -> str:
|
|
1402
|
+
"""Encode one event as canonical compact JSON (without a trailing newline)."""
|
|
1403
|
+
|
|
1404
|
+
try:
|
|
1405
|
+
encoded = json.dumps(
|
|
1406
|
+
event_to_record(event),
|
|
1407
|
+
sort_keys=True,
|
|
1408
|
+
separators=(",", ":"),
|
|
1409
|
+
ensure_ascii=False,
|
|
1410
|
+
allow_nan=False,
|
|
1411
|
+
)
|
|
1412
|
+
except Exception:
|
|
1413
|
+
encoded = None
|
|
1414
|
+
if encoded is None:
|
|
1415
|
+
raise EventCodecError("Could not encode event")
|
|
1416
|
+
return encoded
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def event_from_json(data: str) -> EventEnvelope:
|
|
1420
|
+
"""Decode one JSON event line."""
|
|
1421
|
+
|
|
1422
|
+
if type(data) is not str:
|
|
1423
|
+
raise EventCodecError("event JSON must be an exact string")
|
|
1424
|
+
|
|
1425
|
+
def reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> Dict[str, Any]:
|
|
1426
|
+
record: Dict[str, Any] = {}
|
|
1427
|
+
for key, value in pairs:
|
|
1428
|
+
if key in record:
|
|
1429
|
+
raise EventCodecError("Duplicate JSON object key")
|
|
1430
|
+
record[key] = value
|
|
1431
|
+
return record
|
|
1432
|
+
|
|
1433
|
+
def reject_nonstandard_number(value: str) -> Any:
|
|
1434
|
+
raise EventCodecError("Non-standard JSON number")
|
|
1435
|
+
|
|
1436
|
+
try:
|
|
1437
|
+
record = json.loads(
|
|
1438
|
+
data,
|
|
1439
|
+
object_pairs_hook=reject_duplicate_keys,
|
|
1440
|
+
parse_constant=reject_nonstandard_number,
|
|
1441
|
+
)
|
|
1442
|
+
except EventCodecError as exc:
|
|
1443
|
+
parse_failure = str(exc)
|
|
1444
|
+
record = None
|
|
1445
|
+
except Exception:
|
|
1446
|
+
parse_failure = "syntax"
|
|
1447
|
+
record = None
|
|
1448
|
+
else:
|
|
1449
|
+
parse_failure = None
|
|
1450
|
+
if parse_failure not in (None, "syntax"):
|
|
1451
|
+
raise EventCodecError(parse_failure)
|
|
1452
|
+
if parse_failure == "syntax":
|
|
1453
|
+
raise EventCodecError("Malformed event JSON")
|
|
1454
|
+
return event_from_record(record)
|