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,960 @@
|
|
|
1
|
+
"""Provider-neutral values for asynchronous LLM task scheduling.
|
|
2
|
+
|
|
3
|
+
The values in this module contain no provider request, SDK exception, URL, or
|
|
4
|
+
transport type. Provider integrations classify their own exceptions through
|
|
5
|
+
the port defined in :mod:`agent_evolve.ports.llm_task_queue`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from email.utils import parsedate_to_datetime
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Generic, Optional, Tuple, TypeVar
|
|
17
|
+
|
|
18
|
+
from agent_evolve.domain.ids import ProviderAttemptId
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
RequestT = TypeVar("RequestT")
|
|
22
|
+
ResponseT = TypeVar("ResponseT")
|
|
23
|
+
|
|
24
|
+
NANOSECONDS_PER_SECOND = 1_000_000_000
|
|
25
|
+
MAX_ATTEMPTS = 100
|
|
26
|
+
MAX_TASK_ID_LENGTH = 128
|
|
27
|
+
_TASK_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
28
|
+
_RETRY_AFTER_SECONDS = re.compile(r"^(?:0|[1-9][0-9]*)$")
|
|
29
|
+
_FAILURE_KIND = re.compile(r"^[a-z][a-z0-9_]{0,63}$")
|
|
30
|
+
_VALIDATION_LOCATION_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{0,63}$")
|
|
31
|
+
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
32
|
+
MAX_VALIDATION_ISSUES = 8
|
|
33
|
+
MAX_VALIDATION_LOCATION_DEPTH = 8
|
|
34
|
+
MAX_EXCEPTION_PROVENANCE_NODES = 16
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _require_nonnegative_ns(value: object, name: str) -> None:
|
|
38
|
+
if type(value) is not int or value < 0:
|
|
39
|
+
raise ValueError(f"{name} must be a non-negative integer number of nanoseconds")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _require_nonnegative_int(value: object, name: str) -> None:
|
|
43
|
+
if type(value) is not int or value < 0:
|
|
44
|
+
raise ValueError(f"{name} must be a non-negative integer")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _require_positive_ns(value: object, name: str) -> None:
|
|
48
|
+
if type(value) is not int or value <= 0:
|
|
49
|
+
raise ValueError(f"{name} must be a positive integer number of nanoseconds")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RetryDisposition(str, Enum):
|
|
53
|
+
RETRY = "retry"
|
|
54
|
+
FAIL = "fail"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RetryReason(str, Enum):
|
|
58
|
+
RATE_LIMIT = "rate_limit"
|
|
59
|
+
TRANSIENT = "transient"
|
|
60
|
+
TIMEOUT = "timeout"
|
|
61
|
+
OUTPUT_INVALID = "output_invalid"
|
|
62
|
+
PERMANENT = "permanent"
|
|
63
|
+
INTERNAL = "internal"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RetryBudgetPartition(str, Enum):
|
|
67
|
+
"""Independent retry allowances beneath the hard physical-attempt cap."""
|
|
68
|
+
|
|
69
|
+
OUTPUT_INVALID = "output_invalid"
|
|
70
|
+
TRANSPORT = "transport"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def retry_budget_partition(reason: RetryReason) -> RetryBudgetPartition:
|
|
74
|
+
"""Map every retryable reason to its provider-neutral budget partition.
|
|
75
|
+
|
|
76
|
+
``PERMANENT`` and ``INTERNAL`` classifications are terminal by definition;
|
|
77
|
+
admitting either as a retry would hide a broken classifier, so this helper
|
|
78
|
+
rejects them instead of silently charging an unrelated allowance.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if type(reason) is not RetryReason:
|
|
82
|
+
raise TypeError("reason must be a RetryReason")
|
|
83
|
+
if reason is RetryReason.OUTPUT_INVALID:
|
|
84
|
+
return RetryBudgetPartition.OUTPUT_INVALID
|
|
85
|
+
if reason in {
|
|
86
|
+
RetryReason.RATE_LIMIT,
|
|
87
|
+
RetryReason.TRANSIENT,
|
|
88
|
+
RetryReason.TIMEOUT,
|
|
89
|
+
}:
|
|
90
|
+
return RetryBudgetPartition.TRANSPORT
|
|
91
|
+
raise ValueError(f"{reason.value} is not a retryable budget reason")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True, slots=True)
|
|
95
|
+
class PartitionedRetryBudget:
|
|
96
|
+
"""Bound semantic repair and transport replay independently.
|
|
97
|
+
|
|
98
|
+
Values count *additional attempts admitted after failures*, while
|
|
99
|
+
:attr:`LLMTask.max_attempts` remains the final bound on physical calls.
|
|
100
|
+
This prevents a transient provider failure from consuming the allowance
|
|
101
|
+
reserved for repairing invalid structured output, and vice versa.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
output_invalid_retries: int
|
|
105
|
+
transport_retries: int
|
|
106
|
+
|
|
107
|
+
def __post_init__(self) -> None:
|
|
108
|
+
for name, value in (
|
|
109
|
+
("output_invalid_retries", self.output_invalid_retries),
|
|
110
|
+
("transport_retries", self.transport_retries),
|
|
111
|
+
):
|
|
112
|
+
_require_nonnegative_int(value, name)
|
|
113
|
+
if value >= MAX_ATTEMPTS:
|
|
114
|
+
raise ValueError(f"{name} must be less than {MAX_ATTEMPTS}")
|
|
115
|
+
|
|
116
|
+
def limit(self, partition: RetryBudgetPartition) -> int:
|
|
117
|
+
if type(partition) is not RetryBudgetPartition:
|
|
118
|
+
raise TypeError("partition must be a RetryBudgetPartition")
|
|
119
|
+
if partition is RetryBudgetPartition.OUTPUT_INVALID:
|
|
120
|
+
return self.output_invalid_retries
|
|
121
|
+
return self.transport_retries
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True, slots=True)
|
|
125
|
+
class RetryBudgetUsage:
|
|
126
|
+
"""Immutable count of retry admissions before one physical attempt."""
|
|
127
|
+
|
|
128
|
+
output_invalid_retries: int = 0
|
|
129
|
+
transport_retries: int = 0
|
|
130
|
+
|
|
131
|
+
def __post_init__(self) -> None:
|
|
132
|
+
for name, value in (
|
|
133
|
+
("output_invalid_retries", self.output_invalid_retries),
|
|
134
|
+
("transport_retries", self.transport_retries),
|
|
135
|
+
):
|
|
136
|
+
_require_nonnegative_int(value, name)
|
|
137
|
+
if value >= MAX_ATTEMPTS:
|
|
138
|
+
raise ValueError(f"{name} must be less than {MAX_ATTEMPTS}")
|
|
139
|
+
|
|
140
|
+
def used(self, partition: RetryBudgetPartition) -> int:
|
|
141
|
+
if type(partition) is not RetryBudgetPartition:
|
|
142
|
+
raise TypeError("partition must be a RetryBudgetPartition")
|
|
143
|
+
if partition is RetryBudgetPartition.OUTPUT_INVALID:
|
|
144
|
+
return self.output_invalid_retries
|
|
145
|
+
return self.transport_retries
|
|
146
|
+
|
|
147
|
+
def consume(self, partition: RetryBudgetPartition) -> "RetryBudgetUsage":
|
|
148
|
+
if type(partition) is not RetryBudgetPartition:
|
|
149
|
+
raise TypeError("partition must be a RetryBudgetPartition")
|
|
150
|
+
if partition is RetryBudgetPartition.OUTPUT_INVALID:
|
|
151
|
+
return RetryBudgetUsage(
|
|
152
|
+
output_invalid_retries=self.output_invalid_retries + 1,
|
|
153
|
+
transport_retries=self.transport_retries,
|
|
154
|
+
)
|
|
155
|
+
return RetryBudgetUsage(
|
|
156
|
+
output_invalid_retries=self.output_invalid_retries,
|
|
157
|
+
transport_retries=self.transport_retries + 1,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class RetryAfterSource(str, Enum):
|
|
162
|
+
DELAY_SECONDS = "delay_seconds"
|
|
163
|
+
HTTP_DATE = "http_date"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class StructuredOutputFailureMode(str, Enum):
|
|
167
|
+
"""Closed, non-content-bearing modes for typed-output failures."""
|
|
168
|
+
|
|
169
|
+
SCHEMA_VALIDATION = "schema_validation"
|
|
170
|
+
TYPED_OUTPUT_CONTRACT = "typed_output_contract"
|
|
171
|
+
INCOMPLETE_TOOL_CALL = "incomplete_tool_call"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class CanonicalProviderErrorCode(str, Enum):
|
|
175
|
+
"""Closed provider error codes safe to retain in durable telemetry.
|
|
176
|
+
|
|
177
|
+
Provider adapters may admit a value only from a structured error field;
|
|
178
|
+
messages, response bodies, and free-form provider metadata are never
|
|
179
|
+
coerced into this enum. The deliberately finite vocabulary covers the
|
|
180
|
+
OpenAI-compatible error types currently exposed by OpenRouter and common
|
|
181
|
+
upstream providers while failing closed for an unfamiliar value.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
# Current OpenRouter typed error vocabulary.
|
|
185
|
+
AUTHENTICATION = "authentication"
|
|
186
|
+
CONTENT_POLICY_VIOLATION = "content_policy_violation"
|
|
187
|
+
CONTEXT_LENGTH_EXCEEDED = "context_length_exceeded"
|
|
188
|
+
IMAGE_DOWNLOAD_FAILED = "image_download_failed"
|
|
189
|
+
IMAGE_NOT_FOUND = "image_not_found"
|
|
190
|
+
IMAGE_TOO_LARGE = "image_too_large"
|
|
191
|
+
IMAGE_TOO_SMALL = "image_too_small"
|
|
192
|
+
INVALID_IMAGE = "invalid_image"
|
|
193
|
+
INVALID_PROMPT = "invalid_prompt"
|
|
194
|
+
INVALID_REQUEST = "invalid_request"
|
|
195
|
+
MAX_TOKENS_EXCEEDED = "max_tokens_exceeded"
|
|
196
|
+
NOT_FOUND = "not_found"
|
|
197
|
+
PAYLOAD_TOO_LARGE = "payload_too_large"
|
|
198
|
+
PAYMENT_REQUIRED = "payment_required"
|
|
199
|
+
PERMISSION_DENIED = "permission_denied"
|
|
200
|
+
PRECONDITION_FAILED = "precondition_failed"
|
|
201
|
+
PROVIDER_OVERLOADED = "provider_overloaded"
|
|
202
|
+
PROVIDER_UNAVAILABLE = "provider_unavailable"
|
|
203
|
+
RATE_LIMIT_EXCEEDED = "rate_limit_exceeded"
|
|
204
|
+
REFUSAL = "refusal"
|
|
205
|
+
SERVER = "server"
|
|
206
|
+
STRING_TOO_LONG = "string_too_long"
|
|
207
|
+
TIMEOUT = "timeout"
|
|
208
|
+
TOKEN_LIMIT_EXCEEDED = "token_limit_exceeded"
|
|
209
|
+
UNMAPPED = "unmapped"
|
|
210
|
+
UNPROCESSABLE = "unprocessable"
|
|
211
|
+
UNSUPPORTED_IMAGE_FORMAT = "unsupported_image_format"
|
|
212
|
+
|
|
213
|
+
# OpenAI SDK and upstream compatibility codes observed on compatible APIs.
|
|
214
|
+
API_ERROR = "api_error"
|
|
215
|
+
AUTHENTICATION_ERROR = "authentication_error"
|
|
216
|
+
BAD_REQUEST = "bad_request"
|
|
217
|
+
CONFLICT_ERROR = "conflict_error"
|
|
218
|
+
CONTENT_FILTER_ERROR = "content_filter_error"
|
|
219
|
+
INSUFFICIENT_QUOTA = "insufficient_quota"
|
|
220
|
+
INTERNAL_SERVER_ERROR = "internal_server_error"
|
|
221
|
+
INVALID_REQUEST_ERROR = "invalid_request_error"
|
|
222
|
+
MODEL_NOT_FOUND = "model_not_found"
|
|
223
|
+
NOT_FOUND_ERROR = "not_found_error"
|
|
224
|
+
OVERLOADED_ERROR = "overloaded_error"
|
|
225
|
+
PERMISSION_ERROR = "permission_error"
|
|
226
|
+
PROVIDER_ERROR = "provider_error"
|
|
227
|
+
RATE_LIMIT_ERROR = "rate_limit_error"
|
|
228
|
+
REQUEST_TIMEOUT = "request_timeout"
|
|
229
|
+
SERVER_ERROR = "server_error"
|
|
230
|
+
SERVICE_UNAVAILABLE_ERROR = "service_unavailable_error"
|
|
231
|
+
TOOL_ERROR = "tool_error"
|
|
232
|
+
UNPROCESSABLE_ENTITY_ERROR = "unprocessable_entity_error"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class StreamTimeoutPhase(str, Enum):
|
|
236
|
+
"""Closed progress-aware stream boundary that expired."""
|
|
237
|
+
|
|
238
|
+
FIRST_EVENT = "first_event"
|
|
239
|
+
IDLE = "idle"
|
|
240
|
+
ABSOLUTE = "absolute"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class ExceptionOriginFamily(str, Enum):
|
|
244
|
+
"""Closed runtime families safe to expose in durable diagnostics.
|
|
245
|
+
|
|
246
|
+
Exact exception type identity is carried separately as a domain-separated
|
|
247
|
+
fingerprint. Keeping this value closed prevents an attacker-controlled
|
|
248
|
+
module or class name from becoming durable journal text.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
BUILTINS = "builtins"
|
|
252
|
+
ASYNCIO = "asyncio"
|
|
253
|
+
ANYIO = "anyio"
|
|
254
|
+
HTTPX = "httpx"
|
|
255
|
+
HTTPCORE = "httpcore"
|
|
256
|
+
OPENAI = "openai"
|
|
257
|
+
PYDANTIC = "pydantic"
|
|
258
|
+
PYDANTIC_AI = "pydantic_ai"
|
|
259
|
+
AGENT_EVOLVE = "agent_evolve"
|
|
260
|
+
OTHER = "other"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class ExceptionProvenanceLink(str, Enum):
|
|
264
|
+
"""Closed relationship from one sanitized exception node to its parent."""
|
|
265
|
+
|
|
266
|
+
ROOT = "root"
|
|
267
|
+
CAUSE = "cause"
|
|
268
|
+
CONTEXT = "context"
|
|
269
|
+
GROUP_MEMBER = "group_member"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass(frozen=True, slots=True)
|
|
273
|
+
class SanitizedExceptionProvenanceNode:
|
|
274
|
+
"""One content-free exception-type identity in a bounded exception graph."""
|
|
275
|
+
|
|
276
|
+
parent_index: Optional[int]
|
|
277
|
+
link: ExceptionProvenanceLink
|
|
278
|
+
family: ExceptionOriginFamily
|
|
279
|
+
type_identity_sha256: str
|
|
280
|
+
|
|
281
|
+
def __post_init__(self) -> None:
|
|
282
|
+
if type(self.link) is not ExceptionProvenanceLink:
|
|
283
|
+
raise TypeError("link must be an ExceptionProvenanceLink")
|
|
284
|
+
if type(self.family) is not ExceptionOriginFamily:
|
|
285
|
+
raise TypeError("family must be an ExceptionOriginFamily")
|
|
286
|
+
if (
|
|
287
|
+
type(self.type_identity_sha256) is not str
|
|
288
|
+
or _SHA256.fullmatch(self.type_identity_sha256) is None
|
|
289
|
+
):
|
|
290
|
+
raise ValueError("type_identity_sha256 must be lowercase SHA-256")
|
|
291
|
+
if self.link is ExceptionProvenanceLink.ROOT:
|
|
292
|
+
if self.parent_index is not None:
|
|
293
|
+
raise ValueError("root exception provenance cannot have a parent")
|
|
294
|
+
elif type(self.parent_index) is not int or self.parent_index < 0:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
"non-root exception provenance requires a non-negative parent index"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass(frozen=True, slots=True)
|
|
301
|
+
class SanitizedExceptionProvenance:
|
|
302
|
+
"""Bounded, topology-preserving provenance with no exception text.
|
|
303
|
+
|
|
304
|
+
Nodes contain only closed families, SHA-256 fingerprints of bounded type
|
|
305
|
+
identity projections, and integer graph edges. Messages, reprs, response
|
|
306
|
+
objects, URLs, payloads, tracebacks, and arbitrary metadata are absent by
|
|
307
|
+
construction.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
nodes: Tuple[SanitizedExceptionProvenanceNode, ...]
|
|
311
|
+
truncated: bool
|
|
312
|
+
|
|
313
|
+
def __post_init__(self) -> None:
|
|
314
|
+
if type(self.nodes) is not tuple or not self.nodes:
|
|
315
|
+
raise ValueError("exception provenance nodes must be a non-empty tuple")
|
|
316
|
+
if len(self.nodes) > MAX_EXCEPTION_PROVENANCE_NODES:
|
|
317
|
+
raise ValueError(
|
|
318
|
+
"exception provenance exceeds the bounded node allowance"
|
|
319
|
+
)
|
|
320
|
+
if any(
|
|
321
|
+
type(node) is not SanitizedExceptionProvenanceNode
|
|
322
|
+
for node in self.nodes
|
|
323
|
+
):
|
|
324
|
+
raise TypeError(
|
|
325
|
+
"exception provenance must contain exact sanitized nodes"
|
|
326
|
+
)
|
|
327
|
+
if type(self.truncated) is not bool:
|
|
328
|
+
raise TypeError("exception provenance truncated must be bool")
|
|
329
|
+
for index, node in enumerate(self.nodes):
|
|
330
|
+
SanitizedExceptionProvenanceNode.__post_init__(node)
|
|
331
|
+
if index == 0:
|
|
332
|
+
if node.link is not ExceptionProvenanceLink.ROOT:
|
|
333
|
+
raise ValueError("first exception provenance node must be root")
|
|
334
|
+
elif (
|
|
335
|
+
node.link is ExceptionProvenanceLink.ROOT
|
|
336
|
+
or node.parent_index is None
|
|
337
|
+
or node.parent_index >= index
|
|
338
|
+
):
|
|
339
|
+
raise ValueError(
|
|
340
|
+
"exception provenance parents must precede their child nodes"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class ValidationIssueCategory(str, Enum):
|
|
345
|
+
"""Coarse validation categories safe to expose to a repair attempt."""
|
|
346
|
+
|
|
347
|
+
MISSING = "missing"
|
|
348
|
+
EXTRA_FIELD = "extra_field"
|
|
349
|
+
LITERAL_OR_ENUM = "literal_or_enum"
|
|
350
|
+
WRONG_TYPE = "wrong_type"
|
|
351
|
+
BOUNDS_OR_LENGTH = "bounds_or_length"
|
|
352
|
+
SEMANTIC_CONSTRAINT = "semantic_constraint"
|
|
353
|
+
MALFORMED_ARGUMENTS = "malformed_arguments"
|
|
354
|
+
OTHER_VALIDATION = "other_validation"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class ValidationIssueReasonCode(str, Enum):
|
|
358
|
+
"""Closed semantic-output reasons safe for durable diagnostics.
|
|
359
|
+
|
|
360
|
+
Integrations may populate this field only from a trusted validator error
|
|
361
|
+
*type*. Free-form validation messages, model output, and Pydantic context
|
|
362
|
+
values are never parsed into these codes.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
DUPLICATE_FINITE_OPTIONS = "duplicate_finite_options"
|
|
366
|
+
FINITE_OPTION_OUT_OF_CONTRACT = "finite_option_out_of_contract"
|
|
367
|
+
ASSIGNED_MEMORY_CARD_OMITTED = "assigned_memory_card_omitted"
|
|
368
|
+
PROPOSAL_SUPPORT_OPTION_OMITTED = "proposal_support_option_omitted"
|
|
369
|
+
NO_FEASIBLE_DISJOINT_PORTFOLIO = "no_feasible_disjoint_portfolio"
|
|
370
|
+
PORTFOLIO_MEMORY_DOSE_VIOLATION = "portfolio_memory_dose_violation"
|
|
371
|
+
REFLECTION_METRIC_CONTRACT_VIOLATION = (
|
|
372
|
+
"reflection_metric_contract_violation"
|
|
373
|
+
)
|
|
374
|
+
REFLECTION_ACTION_CONTRACT_VIOLATION = (
|
|
375
|
+
"reflection_action_contract_violation"
|
|
376
|
+
)
|
|
377
|
+
REFLECTION_SEMANTIC_CONTRACT_VIOLATION = (
|
|
378
|
+
"reflection_semantic_contract_violation"
|
|
379
|
+
)
|
|
380
|
+
REFLECTION_DIRECTION_OR_ANCHOR_VIOLATION = (
|
|
381
|
+
"reflection_direction_or_anchor_violation"
|
|
382
|
+
)
|
|
383
|
+
RESIDUAL_RADIUS_CONTRACT_VIOLATION = (
|
|
384
|
+
"residual_radius_contract_violation"
|
|
385
|
+
)
|
|
386
|
+
RESIDUAL_OPTION_CONTRACT_VIOLATION = (
|
|
387
|
+
"residual_option_contract_violation"
|
|
388
|
+
)
|
|
389
|
+
RESIDUAL_METRIC_CONTRACT_VIOLATION = (
|
|
390
|
+
"residual_metric_contract_violation"
|
|
391
|
+
)
|
|
392
|
+
RESIDUAL_QUANTILE_ORDER_VIOLATION = (
|
|
393
|
+
"residual_quantile_order_violation"
|
|
394
|
+
)
|
|
395
|
+
RESIDUAL_PLAN_DIVERSITY_VIOLATION = (
|
|
396
|
+
"residual_plan_diversity_violation"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class AttemptRequestVariant(str, Enum):
|
|
401
|
+
"""Closed variants for the exact prompt used by one provider attempt."""
|
|
402
|
+
|
|
403
|
+
ORIGINAL = "original"
|
|
404
|
+
# Retained so historical telemetry remains decodable. New attempts use v4.
|
|
405
|
+
SCHEMA_REPAIR_V1 = "schema_repair_v1"
|
|
406
|
+
SCHEMA_REPAIR_V2 = "schema_repair_v2"
|
|
407
|
+
SCHEMA_REPAIR_V3 = "schema_repair_v3"
|
|
408
|
+
SCHEMA_REPAIR_V4 = "schema_repair_v4"
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@dataclass(frozen=True, slots=True)
|
|
412
|
+
class AttemptRequestEvidence:
|
|
413
|
+
"""Content-free identity evidence for the request sent by one attempt."""
|
|
414
|
+
|
|
415
|
+
variant: AttemptRequestVariant
|
|
416
|
+
prompt_sha256: str
|
|
417
|
+
provider_attempt_id: Optional[ProviderAttemptId] = None
|
|
418
|
+
|
|
419
|
+
def __post_init__(self) -> None:
|
|
420
|
+
if type(self.variant) is not AttemptRequestVariant:
|
|
421
|
+
raise TypeError("variant must be an AttemptRequestVariant")
|
|
422
|
+
if (
|
|
423
|
+
type(self.prompt_sha256) is not str
|
|
424
|
+
or _SHA256.fullmatch(self.prompt_sha256) is None
|
|
425
|
+
):
|
|
426
|
+
raise ValueError("prompt_sha256 must be a lowercase SHA-256 digest")
|
|
427
|
+
if self.provider_attempt_id is not None and (
|
|
428
|
+
type(self.provider_attempt_id) is not ProviderAttemptId
|
|
429
|
+
):
|
|
430
|
+
raise TypeError("provider_attempt_id must be a ProviderAttemptId or None")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@dataclass(frozen=True, slots=True)
|
|
434
|
+
class SanitizedValidationIssue:
|
|
435
|
+
"""One bounded issue with no message, input value, or provider content."""
|
|
436
|
+
|
|
437
|
+
category: ValidationIssueCategory
|
|
438
|
+
location: Tuple[str, ...]
|
|
439
|
+
reason_code: Optional[ValidationIssueReasonCode] = None
|
|
440
|
+
|
|
441
|
+
def __post_init__(self) -> None:
|
|
442
|
+
if type(self.category) is not ValidationIssueCategory:
|
|
443
|
+
raise TypeError("category must be a ValidationIssueCategory")
|
|
444
|
+
if type(self.location) is not tuple:
|
|
445
|
+
raise TypeError("location must be an exact tuple")
|
|
446
|
+
if not 1 <= len(self.location) <= MAX_VALIDATION_LOCATION_DEPTH:
|
|
447
|
+
raise ValueError(
|
|
448
|
+
"location must contain between one and "
|
|
449
|
+
f"{MAX_VALIDATION_LOCATION_DEPTH} safe segments"
|
|
450
|
+
)
|
|
451
|
+
for segment in self.location:
|
|
452
|
+
if (
|
|
453
|
+
type(segment) is not str
|
|
454
|
+
or _VALIDATION_LOCATION_TOKEN.fullmatch(segment) is None
|
|
455
|
+
):
|
|
456
|
+
raise ValueError("location segments must use the safe token grammar")
|
|
457
|
+
if self.reason_code is not None and (
|
|
458
|
+
type(self.reason_code) is not ValidationIssueReasonCode
|
|
459
|
+
):
|
|
460
|
+
raise TypeError("reason_code must be a ValidationIssueReasonCode or None")
|
|
461
|
+
if (
|
|
462
|
+
self.reason_code is not None
|
|
463
|
+
and self.category is not ValidationIssueCategory.SEMANTIC_CONSTRAINT
|
|
464
|
+
):
|
|
465
|
+
raise ValueError(
|
|
466
|
+
"reason_code is valid only for a semantic_constraint issue"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@dataclass(frozen=True, slots=True)
|
|
471
|
+
class RetryAfter:
|
|
472
|
+
"""A validated Retry-After delay; sleeping remains the queue's job."""
|
|
473
|
+
|
|
474
|
+
delay_ns: int
|
|
475
|
+
source: RetryAfterSource
|
|
476
|
+
|
|
477
|
+
def __post_init__(self) -> None:
|
|
478
|
+
_require_nonnegative_ns(self.delay_ns, "delay_ns")
|
|
479
|
+
if type(self.source) is not RetryAfterSource:
|
|
480
|
+
raise TypeError("source must be a RetryAfterSource")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def parse_retry_after(value: object, *, now_utc: datetime) -> Optional[RetryAfter]:
|
|
484
|
+
"""Parse an HTTP Retry-After value without sleeping or provider coupling.
|
|
485
|
+
|
|
486
|
+
RFC delay-seconds and timezone-aware HTTP dates are accepted. Invalid,
|
|
487
|
+
non-ASCII, fractional, or unbounded values return ``None``. Past dates map
|
|
488
|
+
to a zero delay. Date resolution is rounded up to the next nanosecond so a
|
|
489
|
+
parsed server delay is never shortened.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
if type(value) is not str or not value or len(value) > 128 or not value.isascii():
|
|
493
|
+
return None
|
|
494
|
+
text = value.strip()
|
|
495
|
+
if _RETRY_AFTER_SECONDS.fullmatch(text):
|
|
496
|
+
seconds = int(text)
|
|
497
|
+
if seconds > (2**63 - 1) // NANOSECONDS_PER_SECOND:
|
|
498
|
+
return None
|
|
499
|
+
return RetryAfter(
|
|
500
|
+
delay_ns=seconds * NANOSECONDS_PER_SECOND,
|
|
501
|
+
source=RetryAfterSource.DELAY_SECONDS,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if type(now_utc) is not datetime or now_utc.utcoffset() is None:
|
|
505
|
+
raise ValueError("now_utc must be a timezone-aware datetime")
|
|
506
|
+
try:
|
|
507
|
+
parsed = parsedate_to_datetime(text)
|
|
508
|
+
except (TypeError, ValueError, OverflowError):
|
|
509
|
+
return None
|
|
510
|
+
if parsed is None or parsed.utcoffset() is None:
|
|
511
|
+
return None
|
|
512
|
+
delta_seconds = (
|
|
513
|
+
parsed.astimezone(timezone.utc) - now_utc.astimezone(timezone.utc)
|
|
514
|
+
).total_seconds()
|
|
515
|
+
if not math.isfinite(delta_seconds):
|
|
516
|
+
return None
|
|
517
|
+
delay_ns = max(0, math.ceil(delta_seconds * NANOSECONDS_PER_SECOND))
|
|
518
|
+
if delay_ns > 2**63 - 1:
|
|
519
|
+
return None
|
|
520
|
+
return RetryAfter(delay_ns=delay_ns, source=RetryAfterSource.HTTP_DATE)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@dataclass(frozen=True, slots=True)
|
|
524
|
+
class SanitizedAttemptFailure:
|
|
525
|
+
"""Bounded failure evidence safe to retain in durable attempt telemetry.
|
|
526
|
+
|
|
527
|
+
The queue never derives this value from ``str(error)``. Integrations may
|
|
528
|
+
attach it only after replacing provider text and response bodies with a
|
|
529
|
+
closed kind and an explicitly sanitized message.
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
kind: str
|
|
533
|
+
retryable: bool
|
|
534
|
+
safe_message: str
|
|
535
|
+
status_code: Optional[int] = None
|
|
536
|
+
retry_after_seconds: Optional[float] = None
|
|
537
|
+
output_failure_mode: Optional[StructuredOutputFailureMode] = None
|
|
538
|
+
validation_issues: Tuple[SanitizedValidationIssue, ...] = ()
|
|
539
|
+
stream_timeout_phase: Optional[StreamTimeoutPhase] = None
|
|
540
|
+
provider_error_code: Optional[CanonicalProviderErrorCode] = None
|
|
541
|
+
provider_error_envelope_sha256: Optional[str] = None
|
|
542
|
+
exception_provenance: Optional[SanitizedExceptionProvenance] = None
|
|
543
|
+
|
|
544
|
+
def __post_init__(self) -> None:
|
|
545
|
+
if type(self.kind) is not str or _FAILURE_KIND.fullmatch(self.kind) is None:
|
|
546
|
+
raise ValueError("kind must use the closed lowercase token grammar")
|
|
547
|
+
if type(self.retryable) is not bool:
|
|
548
|
+
raise TypeError("retryable must be bool")
|
|
549
|
+
if type(self.safe_message) is not str or not self.safe_message.strip():
|
|
550
|
+
raise ValueError("safe_message must be non-empty")
|
|
551
|
+
if len(self.safe_message.encode("utf-8", errors="strict")) > 512:
|
|
552
|
+
raise ValueError("safe_message is too large for inline telemetry")
|
|
553
|
+
if self.status_code is not None and (
|
|
554
|
+
type(self.status_code) is not int or not 100 <= self.status_code <= 599
|
|
555
|
+
):
|
|
556
|
+
raise ValueError("status_code must be an HTTP status or None")
|
|
557
|
+
if self.retry_after_seconds is not None and (
|
|
558
|
+
isinstance(self.retry_after_seconds, bool)
|
|
559
|
+
or not isinstance(self.retry_after_seconds, (int, float))
|
|
560
|
+
or not math.isfinite(float(self.retry_after_seconds))
|
|
561
|
+
or float(self.retry_after_seconds) < 0
|
|
562
|
+
):
|
|
563
|
+
raise ValueError("retry_after_seconds must be finite and non-negative")
|
|
564
|
+
if (
|
|
565
|
+
self.output_failure_mode is not None
|
|
566
|
+
and type(self.output_failure_mode) is not StructuredOutputFailureMode
|
|
567
|
+
):
|
|
568
|
+
raise TypeError(
|
|
569
|
+
"output_failure_mode must be a StructuredOutputFailureMode or None"
|
|
570
|
+
)
|
|
571
|
+
if self.output_failure_mode is not None and self.kind != "output_invalid":
|
|
572
|
+
raise ValueError(
|
|
573
|
+
"only output_invalid failures may carry output diagnostics"
|
|
574
|
+
)
|
|
575
|
+
if type(self.validation_issues) is not tuple:
|
|
576
|
+
raise TypeError("validation_issues must be an exact tuple")
|
|
577
|
+
if len(self.validation_issues) > MAX_VALIDATION_ISSUES:
|
|
578
|
+
raise ValueError(
|
|
579
|
+
f"validation_issues cannot exceed {MAX_VALIDATION_ISSUES} entries"
|
|
580
|
+
)
|
|
581
|
+
if any(
|
|
582
|
+
type(issue) is not SanitizedValidationIssue
|
|
583
|
+
for issue in self.validation_issues
|
|
584
|
+
):
|
|
585
|
+
raise TypeError(
|
|
586
|
+
"validation_issues must contain exact SanitizedValidationIssue values"
|
|
587
|
+
)
|
|
588
|
+
if self.output_failure_mode is None and self.validation_issues:
|
|
589
|
+
raise ValueError("validation issues require an output failure mode")
|
|
590
|
+
if (
|
|
591
|
+
self.validation_issues
|
|
592
|
+
and self.output_failure_mode
|
|
593
|
+
is not StructuredOutputFailureMode.SCHEMA_VALIDATION
|
|
594
|
+
):
|
|
595
|
+
raise ValueError("validation issues require schema_validation mode")
|
|
596
|
+
if self.stream_timeout_phase is not None and (
|
|
597
|
+
type(self.stream_timeout_phase) is not StreamTimeoutPhase
|
|
598
|
+
):
|
|
599
|
+
raise TypeError("stream_timeout_phase must be a StreamTimeoutPhase or None")
|
|
600
|
+
if self.stream_timeout_phase is not None and self.kind != "timeout":
|
|
601
|
+
raise ValueError("only timeout failures may carry a stream timeout phase")
|
|
602
|
+
if self.provider_error_code is not None and (
|
|
603
|
+
type(self.provider_error_code) is not CanonicalProviderErrorCode
|
|
604
|
+
):
|
|
605
|
+
raise TypeError(
|
|
606
|
+
"provider_error_code must be a CanonicalProviderErrorCode or None"
|
|
607
|
+
)
|
|
608
|
+
if self.provider_error_envelope_sha256 is not None and (
|
|
609
|
+
type(self.provider_error_envelope_sha256) is not str
|
|
610
|
+
or _SHA256.fullmatch(self.provider_error_envelope_sha256) is None
|
|
611
|
+
):
|
|
612
|
+
raise ValueError(
|
|
613
|
+
"provider_error_envelope_sha256 must be lowercase SHA-256 or None"
|
|
614
|
+
)
|
|
615
|
+
if (
|
|
616
|
+
self.provider_error_code is not None
|
|
617
|
+
or self.provider_error_envelope_sha256 is not None
|
|
618
|
+
) and self.status_code is None:
|
|
619
|
+
raise ValueError("provider HTTP diagnostics require status_code")
|
|
620
|
+
if self.exception_provenance is not None and (
|
|
621
|
+
type(self.exception_provenance) is not SanitizedExceptionProvenance
|
|
622
|
+
):
|
|
623
|
+
raise TypeError(
|
|
624
|
+
"exception_provenance must be SanitizedExceptionProvenance or None"
|
|
625
|
+
)
|
|
626
|
+
if self.exception_provenance is not None:
|
|
627
|
+
SanitizedExceptionProvenance.__post_init__(self.exception_provenance)
|
|
628
|
+
if self.kind != "unknown":
|
|
629
|
+
raise ValueError(
|
|
630
|
+
"exception provenance is retained only for unknown failures"
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
@dataclass(frozen=True, slots=True)
|
|
635
|
+
class RetryClassification:
|
|
636
|
+
disposition: RetryDisposition
|
|
637
|
+
reason: RetryReason
|
|
638
|
+
retry_after: Optional[RetryAfter] = None
|
|
639
|
+
sanitized_failure: Optional[SanitizedAttemptFailure] = None
|
|
640
|
+
|
|
641
|
+
def __post_init__(self) -> None:
|
|
642
|
+
if type(self.disposition) is not RetryDisposition:
|
|
643
|
+
raise TypeError("disposition must be a RetryDisposition")
|
|
644
|
+
if type(self.reason) is not RetryReason:
|
|
645
|
+
raise TypeError("reason must be a RetryReason")
|
|
646
|
+
if self.retry_after is not None and type(self.retry_after) is not RetryAfter:
|
|
647
|
+
raise TypeError("retry_after must be a RetryAfter or None")
|
|
648
|
+
if (
|
|
649
|
+
self.sanitized_failure is not None
|
|
650
|
+
and type(self.sanitized_failure) is not SanitizedAttemptFailure
|
|
651
|
+
):
|
|
652
|
+
raise TypeError(
|
|
653
|
+
"sanitized_failure must be a SanitizedAttemptFailure or None"
|
|
654
|
+
)
|
|
655
|
+
if self.disposition is RetryDisposition.FAIL and self.retry_after is not None:
|
|
656
|
+
raise ValueError("a terminal classification cannot carry Retry-After")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
@dataclass(frozen=True, slots=True)
|
|
660
|
+
class LLMTask(Generic[RequestT]):
|
|
661
|
+
task_id: str
|
|
662
|
+
request: RequestT
|
|
663
|
+
max_attempts: int
|
|
664
|
+
attempt_timeout_ns: Optional[int] = None
|
|
665
|
+
retry_budget: Optional[PartitionedRetryBudget] = None
|
|
666
|
+
|
|
667
|
+
def __post_init__(self) -> None:
|
|
668
|
+
if type(self.task_id) is not str or _TASK_ID.fullmatch(self.task_id) is None:
|
|
669
|
+
raise ValueError(
|
|
670
|
+
"task_id must be a non-secret ASCII identifier of at most "
|
|
671
|
+
f"{MAX_TASK_ID_LENGTH} characters"
|
|
672
|
+
)
|
|
673
|
+
if (
|
|
674
|
+
type(self.max_attempts) is not int
|
|
675
|
+
or not 1 <= self.max_attempts <= MAX_ATTEMPTS
|
|
676
|
+
):
|
|
677
|
+
raise ValueError(f"max_attempts must be an integer in [1, {MAX_ATTEMPTS}]")
|
|
678
|
+
if self.attempt_timeout_ns is not None:
|
|
679
|
+
_require_positive_ns(self.attempt_timeout_ns, "attempt_timeout_ns")
|
|
680
|
+
if self.retry_budget is not None and (
|
|
681
|
+
type(self.retry_budget) is not PartitionedRetryBudget
|
|
682
|
+
):
|
|
683
|
+
raise TypeError(
|
|
684
|
+
"retry_budget must be a PartitionedRetryBudget or None"
|
|
685
|
+
)
|
|
686
|
+
if self.retry_budget is not None:
|
|
687
|
+
PartitionedRetryBudget.__post_init__(self.retry_budget)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
@dataclass(frozen=True, slots=True)
|
|
691
|
+
class LLMAttemptContext:
|
|
692
|
+
task_id: str
|
|
693
|
+
attempt_number: int
|
|
694
|
+
attempt_timeout_ns: Optional[int]
|
|
695
|
+
previous_failure: Optional[SanitizedAttemptFailure] = None
|
|
696
|
+
active_output_failure: Optional[SanitizedAttemptFailure] = None
|
|
697
|
+
retry_budget_usage: Optional[RetryBudgetUsage] = None
|
|
698
|
+
|
|
699
|
+
def __post_init__(self) -> None:
|
|
700
|
+
if type(self.task_id) is not str or _TASK_ID.fullmatch(self.task_id) is None:
|
|
701
|
+
raise ValueError("task_id violates the queue identifier policy")
|
|
702
|
+
if type(self.attempt_number) is not int or self.attempt_number < 1:
|
|
703
|
+
raise ValueError("attempt_number must be a positive integer")
|
|
704
|
+
if self.attempt_timeout_ns is not None:
|
|
705
|
+
_require_positive_ns(self.attempt_timeout_ns, "attempt_timeout_ns")
|
|
706
|
+
if (
|
|
707
|
+
self.previous_failure is not None
|
|
708
|
+
and type(self.previous_failure) is not SanitizedAttemptFailure
|
|
709
|
+
):
|
|
710
|
+
raise TypeError(
|
|
711
|
+
"previous_failure must be a SanitizedAttemptFailure or None"
|
|
712
|
+
)
|
|
713
|
+
if (
|
|
714
|
+
self.active_output_failure is not None
|
|
715
|
+
and type(self.active_output_failure) is not SanitizedAttemptFailure
|
|
716
|
+
):
|
|
717
|
+
raise TypeError(
|
|
718
|
+
"active_output_failure must be a SanitizedAttemptFailure or None"
|
|
719
|
+
)
|
|
720
|
+
if self.active_output_failure is not None and (
|
|
721
|
+
self.active_output_failure.kind != "output_invalid"
|
|
722
|
+
or not self.active_output_failure.retryable
|
|
723
|
+
):
|
|
724
|
+
raise ValueError(
|
|
725
|
+
"active_output_failure must be a retryable output_invalid failure"
|
|
726
|
+
)
|
|
727
|
+
if self.retry_budget_usage is not None and (
|
|
728
|
+
type(self.retry_budget_usage) is not RetryBudgetUsage
|
|
729
|
+
):
|
|
730
|
+
raise TypeError(
|
|
731
|
+
"retry_budget_usage must be a RetryBudgetUsage or None"
|
|
732
|
+
)
|
|
733
|
+
if self.retry_budget_usage is not None:
|
|
734
|
+
RetryBudgetUsage.__post_init__(self.retry_budget_usage)
|
|
735
|
+
admitted_retries = (
|
|
736
|
+
self.retry_budget_usage.output_invalid_retries
|
|
737
|
+
+ self.retry_budget_usage.transport_retries
|
|
738
|
+
)
|
|
739
|
+
if admitted_retries != self.attempt_number - 1:
|
|
740
|
+
raise ValueError(
|
|
741
|
+
"partitioned retry usage must account for every prior attempt"
|
|
742
|
+
)
|
|
743
|
+
if self.attempt_number == 1 and (
|
|
744
|
+
self.previous_failure is not None or self.active_output_failure is not None
|
|
745
|
+
):
|
|
746
|
+
raise ValueError("attempt one cannot carry prior failure state")
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class AttemptStatus(str, Enum):
|
|
750
|
+
SUCCEEDED = "succeeded"
|
|
751
|
+
RETRYABLE_FAILURE = "retryable_failure"
|
|
752
|
+
TERMINAL_FAILURE = "terminal_failure"
|
|
753
|
+
TIMED_OUT = "timed_out"
|
|
754
|
+
CANCELLED = "cancelled"
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
@dataclass(frozen=True, slots=True)
|
|
758
|
+
class AttemptTelemetry:
|
|
759
|
+
"""One provider attempt's wait, execution, and retry scheduling evidence.
|
|
760
|
+
|
|
761
|
+
``wait_time_ns`` is initial scheduler queue time for attempt one and the
|
|
762
|
+
actual inter-attempt wait for later attempts. ``service_time_ns`` covers
|
|
763
|
+
only execution inside the timeout owner.
|
|
764
|
+
"""
|
|
765
|
+
|
|
766
|
+
attempt_number: int
|
|
767
|
+
status: AttemptStatus
|
|
768
|
+
wait_time_ns: int
|
|
769
|
+
service_time_ns: int
|
|
770
|
+
will_retry: bool
|
|
771
|
+
policy_backoff_ns: int = 0
|
|
772
|
+
retry_after_ns: int = 0
|
|
773
|
+
scheduled_delay_ns: int = 0
|
|
774
|
+
classification: Optional[RetryClassification] = None
|
|
775
|
+
error_type: Optional[str] = None
|
|
776
|
+
request_evidence: Optional[AttemptRequestEvidence] = None
|
|
777
|
+
|
|
778
|
+
def __post_init__(self) -> None:
|
|
779
|
+
if type(self.attempt_number) is not int or self.attempt_number < 1:
|
|
780
|
+
raise ValueError("attempt_number must be a positive integer")
|
|
781
|
+
if type(self.status) is not AttemptStatus:
|
|
782
|
+
raise TypeError("status must be an AttemptStatus")
|
|
783
|
+
for name, value in (
|
|
784
|
+
("wait_time_ns", self.wait_time_ns),
|
|
785
|
+
("service_time_ns", self.service_time_ns),
|
|
786
|
+
("policy_backoff_ns", self.policy_backoff_ns),
|
|
787
|
+
("retry_after_ns", self.retry_after_ns),
|
|
788
|
+
("scheduled_delay_ns", self.scheduled_delay_ns),
|
|
789
|
+
):
|
|
790
|
+
_require_nonnegative_ns(value, name)
|
|
791
|
+
if type(self.will_retry) is not bool:
|
|
792
|
+
raise TypeError("will_retry must be bool")
|
|
793
|
+
if (
|
|
794
|
+
self.classification is not None
|
|
795
|
+
and type(self.classification) is not RetryClassification
|
|
796
|
+
):
|
|
797
|
+
raise TypeError("classification must be a RetryClassification or None")
|
|
798
|
+
if self.error_type is not None and (
|
|
799
|
+
type(self.error_type) is not str
|
|
800
|
+
or not self.error_type
|
|
801
|
+
or len(self.error_type) > 256
|
|
802
|
+
):
|
|
803
|
+
raise ValueError("error_type must be a bounded non-empty string or None")
|
|
804
|
+
if (
|
|
805
|
+
self.request_evidence is not None
|
|
806
|
+
and type(self.request_evidence) is not AttemptRequestEvidence
|
|
807
|
+
):
|
|
808
|
+
raise TypeError(
|
|
809
|
+
"request_evidence must be an AttemptRequestEvidence or None"
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
if self.status is AttemptStatus.SUCCEEDED:
|
|
813
|
+
if (
|
|
814
|
+
self.will_retry
|
|
815
|
+
or self.classification is not None
|
|
816
|
+
or self.error_type is not None
|
|
817
|
+
or self.policy_backoff_ns
|
|
818
|
+
or self.retry_after_ns
|
|
819
|
+
or self.scheduled_delay_ns
|
|
820
|
+
):
|
|
821
|
+
raise ValueError("successful attempt telemetry carries failure fields")
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
if self.status is AttemptStatus.CANCELLED:
|
|
825
|
+
if self.will_retry or self.classification is not None:
|
|
826
|
+
raise ValueError("cancelled attempt cannot schedule a retry")
|
|
827
|
+
return
|
|
828
|
+
|
|
829
|
+
if self.classification is None or self.error_type is None:
|
|
830
|
+
raise ValueError(
|
|
831
|
+
"failed attempt telemetry requires classification and error type"
|
|
832
|
+
)
|
|
833
|
+
expected_retry_after = (
|
|
834
|
+
self.classification.retry_after.delay_ns
|
|
835
|
+
if self.classification.retry_after is not None
|
|
836
|
+
else 0
|
|
837
|
+
)
|
|
838
|
+
if self.retry_after_ns != expected_retry_after:
|
|
839
|
+
raise ValueError("retry_after_ns disagrees with the classification")
|
|
840
|
+
if self.will_retry:
|
|
841
|
+
if self.classification.disposition is not RetryDisposition.RETRY:
|
|
842
|
+
raise ValueError("will_retry requires a retry classification")
|
|
843
|
+
if self.scheduled_delay_ns != max(
|
|
844
|
+
self.policy_backoff_ns,
|
|
845
|
+
self.retry_after_ns,
|
|
846
|
+
):
|
|
847
|
+
raise ValueError(
|
|
848
|
+
"scheduled delay must honor policy backoff and Retry-After"
|
|
849
|
+
)
|
|
850
|
+
elif self.policy_backoff_ns or self.scheduled_delay_ns:
|
|
851
|
+
raise ValueError("a terminal attempt cannot carry a scheduled backoff")
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
class TaskOutcomeStatus(str, Enum):
|
|
855
|
+
SUCCEEDED = "succeeded"
|
|
856
|
+
TERMINAL_FAILURE = "terminal_failure"
|
|
857
|
+
ATTEMPTS_EXHAUSTED = "attempts_exhausted"
|
|
858
|
+
CANCELLED = "cancelled"
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
class CancellationReason(str, Enum):
|
|
862
|
+
QUEUE_CLOSED = "queue_closed"
|
|
863
|
+
EXECUTOR_RETIRED = "executor_retired"
|
|
864
|
+
SUBMITTER_CANCELLED = "submitter_cancelled"
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
@dataclass(frozen=True, slots=True)
|
|
868
|
+
class TaskTelemetry:
|
|
869
|
+
"""Whole-task timing.
|
|
870
|
+
|
|
871
|
+
Queue time ends at the first attempt start. Service time runs from that
|
|
872
|
+
start through the terminal outcome and therefore includes retry waits.
|
|
873
|
+
"""
|
|
874
|
+
|
|
875
|
+
task_id: str
|
|
876
|
+
queue_time_ns: int
|
|
877
|
+
service_time_ns: int
|
|
878
|
+
total_time_ns: int
|
|
879
|
+
attempts: Tuple[AttemptTelemetry, ...]
|
|
880
|
+
|
|
881
|
+
def __post_init__(self) -> None:
|
|
882
|
+
if type(self.task_id) is not str or _TASK_ID.fullmatch(self.task_id) is None:
|
|
883
|
+
raise ValueError("task_id violates the queue identifier policy")
|
|
884
|
+
for name, value in (
|
|
885
|
+
("queue_time_ns", self.queue_time_ns),
|
|
886
|
+
("service_time_ns", self.service_time_ns),
|
|
887
|
+
("total_time_ns", self.total_time_ns),
|
|
888
|
+
):
|
|
889
|
+
_require_nonnegative_ns(value, name)
|
|
890
|
+
if type(self.attempts) is not tuple or any(
|
|
891
|
+
type(attempt) is not AttemptTelemetry for attempt in self.attempts
|
|
892
|
+
):
|
|
893
|
+
raise TypeError(
|
|
894
|
+
"attempts must be an exact tuple of AttemptTelemetry values"
|
|
895
|
+
)
|
|
896
|
+
if [attempt.attempt_number for attempt in self.attempts] != list(
|
|
897
|
+
range(1, len(self.attempts) + 1)
|
|
898
|
+
):
|
|
899
|
+
raise ValueError("attempt telemetry numbers must be contiguous")
|
|
900
|
+
if self.queue_time_ns + self.service_time_ns != self.total_time_ns:
|
|
901
|
+
raise ValueError("total_time_ns must equal queue plus service time")
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
@dataclass(frozen=True, slots=True)
|
|
905
|
+
class LLMTaskOutcome(Generic[ResponseT]):
|
|
906
|
+
status: TaskOutcomeStatus
|
|
907
|
+
telemetry: TaskTelemetry
|
|
908
|
+
response: Optional[ResponseT] = None
|
|
909
|
+
cancellation_reason: Optional[CancellationReason] = None
|
|
910
|
+
|
|
911
|
+
def __post_init__(self) -> None:
|
|
912
|
+
if type(self.status) is not TaskOutcomeStatus:
|
|
913
|
+
raise TypeError("status must be a TaskOutcomeStatus")
|
|
914
|
+
if type(self.telemetry) is not TaskTelemetry:
|
|
915
|
+
raise TypeError("telemetry must be a TaskTelemetry")
|
|
916
|
+
if self.status is TaskOutcomeStatus.SUCCEEDED:
|
|
917
|
+
if self.cancellation_reason is not None:
|
|
918
|
+
raise ValueError("a successful task cannot carry cancellation state")
|
|
919
|
+
if not self.telemetry.attempts or (
|
|
920
|
+
self.telemetry.attempts[-1].status is not AttemptStatus.SUCCEEDED
|
|
921
|
+
):
|
|
922
|
+
raise ValueError(
|
|
923
|
+
"a successful task requires a successful final attempt"
|
|
924
|
+
)
|
|
925
|
+
elif self.status is TaskOutcomeStatus.CANCELLED:
|
|
926
|
+
if type(self.cancellation_reason) is not CancellationReason:
|
|
927
|
+
raise ValueError("a cancelled task requires a cancellation reason")
|
|
928
|
+
if self.response is not None:
|
|
929
|
+
raise ValueError("a cancelled task cannot carry a response")
|
|
930
|
+
else:
|
|
931
|
+
if self.response is not None or self.cancellation_reason is not None:
|
|
932
|
+
raise ValueError(
|
|
933
|
+
"a failed task cannot carry response or cancellation state"
|
|
934
|
+
)
|
|
935
|
+
if not self.telemetry.attempts:
|
|
936
|
+
raise ValueError("a failed task requires at least one attempt")
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
@dataclass(frozen=True, slots=True)
|
|
940
|
+
class QueueSnapshot:
|
|
941
|
+
max_in_flight: int
|
|
942
|
+
max_pending: int
|
|
943
|
+
in_flight: int
|
|
944
|
+
pending: int
|
|
945
|
+
closed: bool
|
|
946
|
+
|
|
947
|
+
def __post_init__(self) -> None:
|
|
948
|
+
for name, value in (
|
|
949
|
+
("max_in_flight", self.max_in_flight),
|
|
950
|
+
("max_pending", self.max_pending),
|
|
951
|
+
("in_flight", self.in_flight),
|
|
952
|
+
("pending", self.pending),
|
|
953
|
+
):
|
|
954
|
+
_require_nonnegative_int(value, name)
|
|
955
|
+
if self.max_in_flight < 1:
|
|
956
|
+
raise ValueError("max_in_flight must be positive")
|
|
957
|
+
if self.in_flight > self.max_in_flight or self.pending > self.max_pending:
|
|
958
|
+
raise ValueError("queue snapshot exceeds configured bounds")
|
|
959
|
+
if type(self.closed) is not bool:
|
|
960
|
+
raise TypeError("closed must be bool")
|