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,1604 @@
|
|
|
1
|
+
"""One-attempt Pydantic-AI/OpenRouter implementation of the generator port.
|
|
2
|
+
|
|
3
|
+
The OpenAI-compatible SDK is constructed with ``max_retries=0``. This adapter
|
|
4
|
+
never sleeps and never retries; the application scheduler is the sole retry owner.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from collections import deque
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from contextlib import suppress
|
|
18
|
+
from dataclasses import dataclass, replace
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from decimal import Decimal, InvalidOperation
|
|
21
|
+
from email.utils import parsedate_to_datetime
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import Any, Literal
|
|
24
|
+
|
|
25
|
+
from agent_evolve.domain.llm_task_queue import (
|
|
26
|
+
CanonicalProviderErrorCode,
|
|
27
|
+
MAX_VALIDATION_ISSUES,
|
|
28
|
+
MAX_VALIDATION_LOCATION_DEPTH,
|
|
29
|
+
SanitizedValidationIssue,
|
|
30
|
+
StructuredOutputFailureMode,
|
|
31
|
+
ValidationIssueCategory,
|
|
32
|
+
ValidationIssueReasonCode,
|
|
33
|
+
)
|
|
34
|
+
from agent_evolve.ports.structured_generator import (
|
|
35
|
+
GenerationFailureKind,
|
|
36
|
+
OutputT,
|
|
37
|
+
StructuredGenerationError,
|
|
38
|
+
StructuredGenerationRequest,
|
|
39
|
+
StructuredGenerationResponse,
|
|
40
|
+
StructuredStreamChannel,
|
|
41
|
+
StructuredStreamLivenessPolicy,
|
|
42
|
+
StructuredStreamProgressKind,
|
|
43
|
+
StructuredStreamProgressSink,
|
|
44
|
+
)
|
|
45
|
+
from agent_evolve.infrastructure.stream_liveness import (
|
|
46
|
+
AsyncioContentBlindStreamSupervisor,
|
|
47
|
+
ContentBlindStreamSupervisor,
|
|
48
|
+
StreamProgressMarker,
|
|
49
|
+
)
|
|
50
|
+
from agent_evolve.infrastructure.exception_provenance import (
|
|
51
|
+
sanitized_exception_provenance,
|
|
52
|
+
)
|
|
53
|
+
from agent_evolve.integrations.pydantic_ai.outbound_request_manifest import (
|
|
54
|
+
OpenRouterOutboundRequestManifestPublisher,
|
|
55
|
+
OpenRouterOutboundRequestManifestSink,
|
|
56
|
+
)
|
|
57
|
+
from agent_evolve.integrations.pydantic_ai.json_schema_dialect import (
|
|
58
|
+
OpenRouterJsonSchemaDialect,
|
|
59
|
+
json_schema_transformer_for_dialect,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_RETRYABLE_HTTP_STATUS = frozenset({408, 429})
|
|
64
|
+
_RETRY_AFTER_MAX_SECONDS = 3_600.0
|
|
65
|
+
_MAX_EXCEPTION_NODES = 16
|
|
66
|
+
_MAX_SCHEMA_NODES = 256
|
|
67
|
+
_MAX_SCHEMA_PROPERTIES = 256
|
|
68
|
+
_SAFE_SCHEMA_PROPERTY = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{0,63}$")
|
|
69
|
+
_STREAM_CONTENT_IDENTITY_DOMAIN = (
|
|
70
|
+
b"agent-evolve:structured-stream-semantic-content:v1\x00"
|
|
71
|
+
)
|
|
72
|
+
_PROVIDER_ERROR_ENVELOPE_DOMAIN = (
|
|
73
|
+
b"agent-evolve:provider-error-redacted-envelope:v1\x00"
|
|
74
|
+
)
|
|
75
|
+
PROVIDER_ERROR_ENVELOPE_FINGERPRINT_ALGORITHM = (
|
|
76
|
+
"sha256_domain_and_canonical_redacted_structure_v1"
|
|
77
|
+
)
|
|
78
|
+
PROVIDER_ERROR_ENVELOPE_DOMAIN_SHA256 = hashlib.sha256(
|
|
79
|
+
_PROVIDER_ERROR_ENVELOPE_DOMAIN
|
|
80
|
+
).hexdigest()
|
|
81
|
+
STREAM_CONTENT_IDENTITY_ALGORITHM = "sha256_domain_and_length_framed_semantic_utf8_v1"
|
|
82
|
+
STREAM_CONTENT_IDENTITY_DOMAIN_SHA256 = hashlib.sha256(
|
|
83
|
+
_STREAM_CONTENT_IDENTITY_DOMAIN
|
|
84
|
+
).hexdigest()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _consume_close_task(task: "asyncio.Task[None]") -> None:
|
|
88
|
+
with suppress(BaseException):
|
|
89
|
+
task.exception()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
OpenRouterReasoningEffort = Literal[
|
|
93
|
+
"xhigh",
|
|
94
|
+
"high",
|
|
95
|
+
"medium",
|
|
96
|
+
"low",
|
|
97
|
+
"minimal",
|
|
98
|
+
"none",
|
|
99
|
+
]
|
|
100
|
+
_OPENROUTER_REASONING_EFFORTS = frozenset(
|
|
101
|
+
{"xhigh", "high", "medium", "low", "minimal", "none"}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class OpenRouterStructuredOutputMode(str, Enum):
|
|
106
|
+
"""Provider-capability-owned transport for one typed response."""
|
|
107
|
+
|
|
108
|
+
TOOL = "tool"
|
|
109
|
+
NATIVE_JSON_SCHEMA = "native_json_schema"
|
|
110
|
+
_MISSING_ERRORS = frozenset(
|
|
111
|
+
{
|
|
112
|
+
"missing",
|
|
113
|
+
"missing_argument",
|
|
114
|
+
"missing_keyword_only_argument",
|
|
115
|
+
"missing_positional_only_argument",
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
_MALFORMED_ARGUMENT_ERRORS = frozenset(
|
|
119
|
+
{
|
|
120
|
+
"arguments_type",
|
|
121
|
+
"json_invalid",
|
|
122
|
+
"json_type",
|
|
123
|
+
"model_attributes_type",
|
|
124
|
+
"model_type",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
_BOUND_ERRORS = frozenset(
|
|
128
|
+
{
|
|
129
|
+
"bytes_too_long",
|
|
130
|
+
"bytes_too_short",
|
|
131
|
+
"decimal_max_digits",
|
|
132
|
+
"decimal_max_places",
|
|
133
|
+
"decimal_whole_digits",
|
|
134
|
+
"greater_than",
|
|
135
|
+
"greater_than_equal",
|
|
136
|
+
"less_than",
|
|
137
|
+
"less_than_equal",
|
|
138
|
+
"multiple_of",
|
|
139
|
+
"string_pattern_mismatch",
|
|
140
|
+
"string_too_long",
|
|
141
|
+
"string_too_short",
|
|
142
|
+
"too_long",
|
|
143
|
+
"too_short",
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
_VALIDATION_REASON_ERROR_TYPES = frozenset(
|
|
147
|
+
reason.value for reason in ValidationIssueReasonCode
|
|
148
|
+
)
|
|
149
|
+
_ABSENT = object()
|
|
150
|
+
_CAPABILITY_MISMATCH_ERROR_TYPES = frozenset(
|
|
151
|
+
{
|
|
152
|
+
"capability_mismatch",
|
|
153
|
+
"no_compatible_endpoint",
|
|
154
|
+
"no_endpoints_found",
|
|
155
|
+
"unsupported_parameter",
|
|
156
|
+
"unsupported_parameters",
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _exact_dict_value(value: object, key: str) -> object:
|
|
162
|
+
"""Read one fixed key without accepting arbitrary mapping behavior."""
|
|
163
|
+
|
|
164
|
+
if type(value) is not dict:
|
|
165
|
+
return _ABSENT
|
|
166
|
+
return value.get(key, _ABSENT)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _redacted_value_kind(value: object) -> str:
|
|
170
|
+
"""Return a closed JSON-shape label without coercing or rendering a value."""
|
|
171
|
+
|
|
172
|
+
if value is _ABSENT:
|
|
173
|
+
return "absent"
|
|
174
|
+
if value is None:
|
|
175
|
+
return "null"
|
|
176
|
+
if type(value) is dict:
|
|
177
|
+
return "object"
|
|
178
|
+
if type(value) is list:
|
|
179
|
+
return "array"
|
|
180
|
+
if type(value) is str:
|
|
181
|
+
return "string"
|
|
182
|
+
if type(value) is bool:
|
|
183
|
+
return "boolean"
|
|
184
|
+
if type(value) is int:
|
|
185
|
+
return "integer"
|
|
186
|
+
if type(value) is float:
|
|
187
|
+
return "number"
|
|
188
|
+
return "other"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _fixed_provider_error_fields(body: object) -> tuple[tuple[str, object], ...]:
|
|
192
|
+
"""Read only fixed direct/wrapped typed fields from exact dictionaries."""
|
|
193
|
+
|
|
194
|
+
wrapped_error = _exact_dict_value(body, "error")
|
|
195
|
+
direct_metadata = _exact_dict_value(body, "metadata")
|
|
196
|
+
wrapped_metadata = _exact_dict_value(wrapped_error, "metadata")
|
|
197
|
+
return (
|
|
198
|
+
(
|
|
199
|
+
"body.metadata.error_type",
|
|
200
|
+
_exact_dict_value(direct_metadata, "error_type"),
|
|
201
|
+
),
|
|
202
|
+
("body.error_type", _exact_dict_value(body, "error_type")),
|
|
203
|
+
("body.type", _exact_dict_value(body, "type")),
|
|
204
|
+
("body.code", _exact_dict_value(body, "code")),
|
|
205
|
+
(
|
|
206
|
+
"body.error.metadata.error_type",
|
|
207
|
+
_exact_dict_value(wrapped_metadata, "error_type"),
|
|
208
|
+
),
|
|
209
|
+
(
|
|
210
|
+
"body.error.error_type",
|
|
211
|
+
_exact_dict_value(wrapped_error, "error_type"),
|
|
212
|
+
),
|
|
213
|
+
("body.error.type", _exact_dict_value(wrapped_error, "type")),
|
|
214
|
+
("body.error.code", _exact_dict_value(wrapped_error, "code")),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _is_structured_capability_mismatch(body: object) -> bool:
|
|
219
|
+
"""Admit only an unambiguous finite typed capability error.
|
|
220
|
+
|
|
221
|
+
Numeric HTTP codes and absent fields are ignored. If multiple typed string
|
|
222
|
+
fields are present, every one must name the same closed capability family;
|
|
223
|
+
a conflicting or unfamiliar typed value fails closed. Messages, metadata
|
|
224
|
+
``raw`` values, arbitrary keys, mappings, and object rendering are never
|
|
225
|
+
inspected.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
typed_values = tuple(
|
|
229
|
+
value for _, value in _fixed_provider_error_fields(body) if type(value) is str
|
|
230
|
+
)
|
|
231
|
+
return bool(typed_values) and all(
|
|
232
|
+
value in _CAPABILITY_MISMATCH_ERROR_TYPES for value in typed_values
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _provider_error_diagnostics(
|
|
237
|
+
status_code: int,
|
|
238
|
+
body: object,
|
|
239
|
+
) -> tuple[CanonicalProviderErrorCode | None, str]:
|
|
240
|
+
"""Extract finite HTTP diagnostics without retaining provider content.
|
|
241
|
+
|
|
242
|
+
Fixed direct and wrapped error-object paths are considered in
|
|
243
|
+
priority-neutral fashion. OpenAI SDK status exceptions normally carry the
|
|
244
|
+
direct error object after unwrapping a wire ``{"error": ...}`` response;
|
|
245
|
+
other adapters can retain the wrapper. An unfamiliar string, a non-string,
|
|
246
|
+
or conflicting admitted values yields no canonical code. The fingerprint
|
|
247
|
+
authenticates only a redacted envelope of status and value *kinds*;
|
|
248
|
+
provider values, arbitrary keys, messages, and raw payloads never enter its
|
|
249
|
+
preimage.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
wrapped_error = _exact_dict_value(body, "error")
|
|
253
|
+
direct_metadata = _exact_dict_value(body, "metadata")
|
|
254
|
+
wrapped_metadata = _exact_dict_value(wrapped_error, "metadata")
|
|
255
|
+
fields = _fixed_provider_error_fields(body)
|
|
256
|
+
admitted: list[tuple[str, CanonicalProviderErrorCode]] = []
|
|
257
|
+
for source, raw_value in fields:
|
|
258
|
+
if type(raw_value) is not str:
|
|
259
|
+
continue
|
|
260
|
+
try:
|
|
261
|
+
code = CanonicalProviderErrorCode(raw_value)
|
|
262
|
+
except ValueError:
|
|
263
|
+
continue
|
|
264
|
+
admitted.append((source, code))
|
|
265
|
+
|
|
266
|
+
unique_codes = {code for _, code in admitted}
|
|
267
|
+
provider_error_code = next(iter(unique_codes)) if len(unique_codes) == 1 else None
|
|
268
|
+
redacted_envelope = {
|
|
269
|
+
"schema_version": 1,
|
|
270
|
+
"status_code": status_code,
|
|
271
|
+
"body_kind": _redacted_value_kind(body),
|
|
272
|
+
"direct_metadata_kind": _redacted_value_kind(direct_metadata),
|
|
273
|
+
"wrapped_error_kind": _redacted_value_kind(wrapped_error),
|
|
274
|
+
"wrapped_metadata_kind": _redacted_value_kind(wrapped_metadata),
|
|
275
|
+
"fixed_field_kinds": {
|
|
276
|
+
source: _redacted_value_kind(value) for source, value in fields
|
|
277
|
+
},
|
|
278
|
+
"admitted_provider_error_code": (
|
|
279
|
+
None if provider_error_code is None else provider_error_code.value
|
|
280
|
+
),
|
|
281
|
+
"admitted_sources": sorted(
|
|
282
|
+
source for source, code in admitted if code is provider_error_code
|
|
283
|
+
),
|
|
284
|
+
"conflicting_admitted_codes": len(unique_codes) > 1,
|
|
285
|
+
}
|
|
286
|
+
canonical = json.dumps(
|
|
287
|
+
redacted_envelope,
|
|
288
|
+
allow_nan=False,
|
|
289
|
+
ensure_ascii=True,
|
|
290
|
+
separators=(",", ":"),
|
|
291
|
+
sort_keys=True,
|
|
292
|
+
).encode("ascii")
|
|
293
|
+
fingerprint = hashlib.sha256(
|
|
294
|
+
_PROVIDER_ERROR_ENVELOPE_DOMAIN + canonical
|
|
295
|
+
).hexdigest()
|
|
296
|
+
return provider_error_code, fingerprint
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _bounded_retry_after(value: object) -> float | None:
|
|
300
|
+
if type(value) is not str:
|
|
301
|
+
return None
|
|
302
|
+
stripped = value.strip()
|
|
303
|
+
if not stripped or len(stripped) > 128:
|
|
304
|
+
return None
|
|
305
|
+
try:
|
|
306
|
+
seconds = float(stripped)
|
|
307
|
+
except ValueError:
|
|
308
|
+
try:
|
|
309
|
+
parsed = parsedate_to_datetime(stripped)
|
|
310
|
+
except (TypeError, ValueError, OverflowError):
|
|
311
|
+
return None
|
|
312
|
+
if parsed.tzinfo is None:
|
|
313
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
314
|
+
seconds = (parsed - datetime.now(timezone.utc)).total_seconds()
|
|
315
|
+
if not math.isfinite(seconds):
|
|
316
|
+
return None
|
|
317
|
+
return min(max(seconds, 0.0), _RETRY_AFTER_MAX_SECONDS)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _retry_after_from_exception(exc: BaseException) -> float | None:
|
|
321
|
+
"""Read only the standard retry header from a bounded cause/context chain."""
|
|
322
|
+
|
|
323
|
+
seen: set[int] = set()
|
|
324
|
+
current: BaseException | None = exc
|
|
325
|
+
for _ in range(8):
|
|
326
|
+
if current is None or id(current) in seen:
|
|
327
|
+
break
|
|
328
|
+
seen.add(id(current))
|
|
329
|
+
response = getattr(current, "response", None)
|
|
330
|
+
headers = getattr(response, "headers", None)
|
|
331
|
+
if headers is not None:
|
|
332
|
+
try:
|
|
333
|
+
value = headers.get("retry-after")
|
|
334
|
+
except Exception:
|
|
335
|
+
value = None
|
|
336
|
+
parsed = _bounded_retry_after(value)
|
|
337
|
+
if parsed is not None:
|
|
338
|
+
return parsed
|
|
339
|
+
current = current.__cause__ or current.__context__
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _http_failure(
|
|
344
|
+
status_code: int,
|
|
345
|
+
body: object,
|
|
346
|
+
exc: BaseException,
|
|
347
|
+
) -> StructuredGenerationError:
|
|
348
|
+
provider_error_code, provider_error_envelope_sha256 = _provider_error_diagnostics(
|
|
349
|
+
status_code, body
|
|
350
|
+
)
|
|
351
|
+
if status_code == 429:
|
|
352
|
+
kind = GenerationFailureKind.RATE_LIMITED
|
|
353
|
+
message = "provider rate limit"
|
|
354
|
+
elif status_code == 408:
|
|
355
|
+
kind = GenerationFailureKind.TIMEOUT
|
|
356
|
+
message = "provider request timed out"
|
|
357
|
+
elif 500 <= status_code <= 599:
|
|
358
|
+
kind = (
|
|
359
|
+
GenerationFailureKind.TIMEOUT
|
|
360
|
+
if status_code == 504
|
|
361
|
+
else GenerationFailureKind.PROVIDER_UNAVAILABLE
|
|
362
|
+
)
|
|
363
|
+
message = (
|
|
364
|
+
"provider request timed out"
|
|
365
|
+
if status_code == 504
|
|
366
|
+
else "provider temporarily unavailable"
|
|
367
|
+
)
|
|
368
|
+
elif status_code in {409, 425}:
|
|
369
|
+
kind = GenerationFailureKind.PROVIDER_UNAVAILABLE
|
|
370
|
+
message = "provider request conflict is terminal"
|
|
371
|
+
elif status_code == 404 and _is_structured_capability_mismatch(body):
|
|
372
|
+
kind = GenerationFailureKind.CAPABILITY_MISMATCH
|
|
373
|
+
message = "no model endpoint supports the requested capability set"
|
|
374
|
+
elif status_code == 401:
|
|
375
|
+
kind = GenerationFailureKind.AUTHENTICATION
|
|
376
|
+
message = "provider authentication failed"
|
|
377
|
+
elif status_code == 402:
|
|
378
|
+
kind = GenerationFailureKind.PAYMENT_REQUIRED
|
|
379
|
+
message = "provider payment or credit requirement failed"
|
|
380
|
+
elif status_code == 403:
|
|
381
|
+
kind = GenerationFailureKind.CONTENT_REJECTED
|
|
382
|
+
message = "provider rejected the request"
|
|
383
|
+
elif status_code in {400, 404, 422}:
|
|
384
|
+
kind = GenerationFailureKind.INVALID_REQUEST
|
|
385
|
+
message = "provider rejected invalid request parameters"
|
|
386
|
+
else:
|
|
387
|
+
kind = GenerationFailureKind.UNKNOWN
|
|
388
|
+
message = "unclassified provider HTTP failure"
|
|
389
|
+
return StructuredGenerationError(
|
|
390
|
+
kind=kind,
|
|
391
|
+
retryable=(status_code in _RETRYABLE_HTTP_STATUS or 500 <= status_code <= 599),
|
|
392
|
+
safe_message=message,
|
|
393
|
+
status_code=status_code,
|
|
394
|
+
retry_after_seconds=_retry_after_from_exception(exc),
|
|
395
|
+
provider_error_code=provider_error_code,
|
|
396
|
+
provider_error_envelope_sha256=provider_error_envelope_sha256,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _validation_category(error_type: object) -> ValidationIssueCategory:
|
|
401
|
+
if type(error_type) is not str:
|
|
402
|
+
return ValidationIssueCategory.OTHER_VALIDATION
|
|
403
|
+
if error_type in _MISSING_ERRORS:
|
|
404
|
+
return ValidationIssueCategory.MISSING
|
|
405
|
+
if error_type == "extra_forbidden":
|
|
406
|
+
return ValidationIssueCategory.EXTRA_FIELD
|
|
407
|
+
if error_type in {"enum", "literal_error"}:
|
|
408
|
+
return ValidationIssueCategory.LITERAL_OR_ENUM
|
|
409
|
+
if error_type in _MALFORMED_ARGUMENT_ERRORS:
|
|
410
|
+
return ValidationIssueCategory.MALFORMED_ARGUMENTS
|
|
411
|
+
if error_type in _BOUND_ERRORS:
|
|
412
|
+
return ValidationIssueCategory.BOUNDS_OR_LENGTH
|
|
413
|
+
if error_type in {"assertion_error", "value_error"} or (
|
|
414
|
+
error_type in _VALIDATION_REASON_ERROR_TYPES
|
|
415
|
+
):
|
|
416
|
+
return ValidationIssueCategory.SEMANTIC_CONSTRAINT
|
|
417
|
+
if error_type.endswith(("_type", "_parsing")) or error_type in {
|
|
418
|
+
"is_instance_of",
|
|
419
|
+
"is_subclass_of",
|
|
420
|
+
}:
|
|
421
|
+
return ValidationIssueCategory.WRONG_TYPE
|
|
422
|
+
return ValidationIssueCategory.OTHER_VALIDATION
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _validation_reason_code(
|
|
426
|
+
error_type: object,
|
|
427
|
+
) -> ValidationIssueReasonCode | None:
|
|
428
|
+
"""Admit only a closed trusted validator error type.
|
|
429
|
+
|
|
430
|
+
In particular, this function does not inspect Pydantic's free-form
|
|
431
|
+
``msg``, ``ctx``, or ``input`` fields, any of which can contain model data.
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
if type(error_type) is not str:
|
|
435
|
+
return None
|
|
436
|
+
try:
|
|
437
|
+
return ValidationIssueReasonCode(error_type)
|
|
438
|
+
except ValueError:
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _safe_schema_properties(output_type: type[Any] | None) -> frozenset[str]:
|
|
443
|
+
"""Collect bounded schema property names; values and descriptions are ignored."""
|
|
444
|
+
|
|
445
|
+
if output_type is None:
|
|
446
|
+
return frozenset()
|
|
447
|
+
try:
|
|
448
|
+
schema = output_type.model_json_schema()
|
|
449
|
+
except Exception:
|
|
450
|
+
return frozenset()
|
|
451
|
+
if type(schema) is not dict:
|
|
452
|
+
return frozenset()
|
|
453
|
+
|
|
454
|
+
properties: set[str] = set()
|
|
455
|
+
stack: list[tuple[object, int]] = [(schema, 0)]
|
|
456
|
+
visited = 0
|
|
457
|
+
while stack and visited < _MAX_SCHEMA_NODES:
|
|
458
|
+
value, depth = stack.pop()
|
|
459
|
+
visited += 1
|
|
460
|
+
if depth > MAX_VALIDATION_LOCATION_DEPTH:
|
|
461
|
+
continue
|
|
462
|
+
if type(value) is dict:
|
|
463
|
+
declared = value.get("properties")
|
|
464
|
+
if type(declared) is dict:
|
|
465
|
+
for name in declared:
|
|
466
|
+
if len(properties) >= _MAX_SCHEMA_PROPERTIES:
|
|
467
|
+
break
|
|
468
|
+
if type(name) is str and _SAFE_SCHEMA_PROPERTY.fullmatch(name):
|
|
469
|
+
properties.add(name)
|
|
470
|
+
for child in value.values():
|
|
471
|
+
if visited + len(stack) >= _MAX_SCHEMA_NODES:
|
|
472
|
+
break
|
|
473
|
+
if type(child) in {dict, list}:
|
|
474
|
+
stack.append((child, depth + 1))
|
|
475
|
+
elif type(value) is list:
|
|
476
|
+
for child in value:
|
|
477
|
+
if visited + len(stack) >= _MAX_SCHEMA_NODES:
|
|
478
|
+
break
|
|
479
|
+
if type(child) in {dict, list}:
|
|
480
|
+
stack.append((child, depth + 1))
|
|
481
|
+
return frozenset(properties)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _safe_validation_location(
|
|
485
|
+
raw_location: object,
|
|
486
|
+
*,
|
|
487
|
+
schema_properties: frozenset[str],
|
|
488
|
+
) -> tuple[str, ...]:
|
|
489
|
+
if type(raw_location) not in {tuple, list}:
|
|
490
|
+
return ("unknown_field",)
|
|
491
|
+
safe: list[str] = []
|
|
492
|
+
for segment in raw_location[:MAX_VALIDATION_LOCATION_DEPTH]:
|
|
493
|
+
if type(segment) is str and segment in schema_properties:
|
|
494
|
+
safe.append(segment)
|
|
495
|
+
elif type(segment) is int and segment >= 0:
|
|
496
|
+
safe.append("item")
|
|
497
|
+
else:
|
|
498
|
+
# Extra-field locations may contain arbitrary model output. Never
|
|
499
|
+
# preserve those strings merely because Pydantic calls them a loc.
|
|
500
|
+
safe.append("unknown_field")
|
|
501
|
+
return tuple(safe) if safe else ("root",)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _bounded_exception_nodes(exc: BaseException) -> tuple[BaseException, ...]:
|
|
505
|
+
pending: deque[BaseException] = deque([exc])
|
|
506
|
+
result: list[BaseException] = []
|
|
507
|
+
seen: set[int] = set()
|
|
508
|
+
while pending and len(result) < _MAX_EXCEPTION_NODES:
|
|
509
|
+
current = pending.popleft()
|
|
510
|
+
if id(current) in seen:
|
|
511
|
+
continue
|
|
512
|
+
seen.add(id(current))
|
|
513
|
+
result.append(current)
|
|
514
|
+
for linked in (current.__cause__, current.__context__):
|
|
515
|
+
if isinstance(linked, BaseException) and id(linked) not in seen:
|
|
516
|
+
pending.append(linked)
|
|
517
|
+
if type(current).__module__ == "builtins" and type(current).__name__ in {
|
|
518
|
+
"BaseExceptionGroup",
|
|
519
|
+
"ExceptionGroup",
|
|
520
|
+
}:
|
|
521
|
+
grouped = getattr(current, "exceptions", ())
|
|
522
|
+
if type(grouped) is tuple:
|
|
523
|
+
for linked in grouped[:_MAX_EXCEPTION_NODES]:
|
|
524
|
+
if isinstance(linked, BaseException) and id(linked) not in seen:
|
|
525
|
+
pending.append(linked)
|
|
526
|
+
return tuple(result)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _structured_output_diagnostics(
|
|
530
|
+
exc: BaseException,
|
|
531
|
+
*,
|
|
532
|
+
output_type: type[Any] | None,
|
|
533
|
+
) -> tuple[StructuredOutputFailureMode, tuple[SanitizedValidationIssue, ...]]:
|
|
534
|
+
from pydantic import ValidationError
|
|
535
|
+
from pydantic_ai.exceptions import ToolRetryError
|
|
536
|
+
|
|
537
|
+
schema_properties = _safe_schema_properties(output_type)
|
|
538
|
+
details: list[object] = []
|
|
539
|
+
validation_seen = False
|
|
540
|
+
for current in _bounded_exception_nodes(exc):
|
|
541
|
+
if isinstance(current, ValidationError):
|
|
542
|
+
validation_seen = True
|
|
543
|
+
try:
|
|
544
|
+
details.extend(
|
|
545
|
+
current.errors(
|
|
546
|
+
include_url=False,
|
|
547
|
+
include_context=False,
|
|
548
|
+
include_input=False,
|
|
549
|
+
)[:MAX_VALIDATION_ISSUES]
|
|
550
|
+
)
|
|
551
|
+
except Exception:
|
|
552
|
+
continue
|
|
553
|
+
elif isinstance(current, ToolRetryError):
|
|
554
|
+
content = current.tool_retry.content
|
|
555
|
+
if type(content) is list:
|
|
556
|
+
validation_seen = True
|
|
557
|
+
details.extend(content[:MAX_VALIDATION_ISSUES])
|
|
558
|
+
|
|
559
|
+
issues: list[SanitizedValidationIssue] = []
|
|
560
|
+
seen_issues: set[
|
|
561
|
+
tuple[
|
|
562
|
+
ValidationIssueCategory,
|
|
563
|
+
tuple[str, ...],
|
|
564
|
+
ValidationIssueReasonCode | None,
|
|
565
|
+
]
|
|
566
|
+
] = set()
|
|
567
|
+
for detail in details:
|
|
568
|
+
if len(issues) >= MAX_VALIDATION_ISSUES:
|
|
569
|
+
break
|
|
570
|
+
if type(detail) is not dict:
|
|
571
|
+
continue
|
|
572
|
+
error_type = detail.get("type")
|
|
573
|
+
issue = SanitizedValidationIssue(
|
|
574
|
+
category=_validation_category(error_type),
|
|
575
|
+
location=_safe_validation_location(
|
|
576
|
+
detail.get("loc"),
|
|
577
|
+
schema_properties=schema_properties,
|
|
578
|
+
),
|
|
579
|
+
reason_code=_validation_reason_code(error_type),
|
|
580
|
+
)
|
|
581
|
+
identity = (issue.category, issue.location, issue.reason_code)
|
|
582
|
+
if identity not in seen_issues:
|
|
583
|
+
seen_issues.add(identity)
|
|
584
|
+
issues.append(issue)
|
|
585
|
+
|
|
586
|
+
mode = (
|
|
587
|
+
StructuredOutputFailureMode.SCHEMA_VALIDATION
|
|
588
|
+
if validation_seen
|
|
589
|
+
else StructuredOutputFailureMode.TYPED_OUTPUT_CONTRACT
|
|
590
|
+
)
|
|
591
|
+
return mode, tuple(issues)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _model_api_failure(exc: BaseException) -> StructuredGenerationError:
|
|
595
|
+
"""Classify a non-HTTP model API failure from typed, bounded causes only."""
|
|
596
|
+
|
|
597
|
+
try:
|
|
598
|
+
from openai import APIConnectionError, APITimeoutError
|
|
599
|
+
except ImportError: # pragma: no cover - OpenRouter installs the OpenAI SDK.
|
|
600
|
+
APIConnectionError = () # type: ignore[assignment,misc]
|
|
601
|
+
APITimeoutError = () # type: ignore[assignment,misc]
|
|
602
|
+
|
|
603
|
+
nodes = _bounded_exception_nodes(exc)
|
|
604
|
+
# OpenAI's SDK wraps exceptions raised by HTTPX request hooks in
|
|
605
|
+
# ``APIConnectionError``. Our pre-transport evidence hook is local and has
|
|
606
|
+
# not sent provider bytes, so it must outrank that outer transport wrapper:
|
|
607
|
+
# retrying cannot help and calling it provider unavailability hides the
|
|
608
|
+
# actionable integrity failure.
|
|
609
|
+
from agent_evolve.integrations.pydantic_ai.outbound_request_manifest import (
|
|
610
|
+
OpenRouterOutboundRequestManifestError,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if any(
|
|
614
|
+
type(node) is OpenRouterOutboundRequestManifestError for node in nodes
|
|
615
|
+
):
|
|
616
|
+
return StructuredGenerationError(
|
|
617
|
+
kind=GenerationFailureKind.INVALID_REQUEST,
|
|
618
|
+
retryable=False,
|
|
619
|
+
safe_message="local outbound request evidence contract failed",
|
|
620
|
+
)
|
|
621
|
+
if any(isinstance(node, APITimeoutError) for node in nodes):
|
|
622
|
+
return StructuredGenerationError(
|
|
623
|
+
kind=GenerationFailureKind.TIMEOUT,
|
|
624
|
+
retryable=True,
|
|
625
|
+
safe_message="provider API transport timed out",
|
|
626
|
+
retry_after_seconds=_retry_after_from_exception(exc),
|
|
627
|
+
)
|
|
628
|
+
if any(isinstance(node, APIConnectionError) for node in nodes):
|
|
629
|
+
return StructuredGenerationError(
|
|
630
|
+
kind=GenerationFailureKind.PROVIDER_UNAVAILABLE,
|
|
631
|
+
retryable=True,
|
|
632
|
+
safe_message="provider API transport unavailable",
|
|
633
|
+
retry_after_seconds=_retry_after_from_exception(exc),
|
|
634
|
+
)
|
|
635
|
+
in_band_failure = _in_band_openai_api_failure(exc)
|
|
636
|
+
if in_band_failure is not None:
|
|
637
|
+
return in_band_failure
|
|
638
|
+
return StructuredGenerationError(
|
|
639
|
+
kind=GenerationFailureKind.UNKNOWN,
|
|
640
|
+
retryable=False,
|
|
641
|
+
safe_message="unclassified provider API failure",
|
|
642
|
+
retry_after_seconds=_retry_after_from_exception(exc),
|
|
643
|
+
exception_provenance=sanitized_exception_provenance(exc),
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _in_band_openai_api_failure(
|
|
648
|
+
exc: BaseException,
|
|
649
|
+
) -> StructuredGenerationError | None:
|
|
650
|
+
"""Admit only an exact integer status from an OpenAI SSE error body.
|
|
651
|
+
|
|
652
|
+
OpenAI-compatible streams can raise the base ``openai.APIError`` for an
|
|
653
|
+
error event delivered after the HTTP response has already become a stream.
|
|
654
|
+
Such an exception has no ``APIStatusError.status_code``. OpenRouter's
|
|
655
|
+
documented in-band envelope instead carries ``body.code``. We accept that
|
|
656
|
+
code only from an exact dictionary and only in the HTTP error range, then
|
|
657
|
+
reuse the ordinary value-redacting HTTP classifier. Messages and all
|
|
658
|
+
unfamiliar/free-form bodies continue to fail closed as UNKNOWN.
|
|
659
|
+
"""
|
|
660
|
+
|
|
661
|
+
try:
|
|
662
|
+
from openai import APIError
|
|
663
|
+
except ImportError: # pragma: no cover - OpenRouter installs the OpenAI SDK.
|
|
664
|
+
return None
|
|
665
|
+
for node in _bounded_exception_nodes(exc):
|
|
666
|
+
# The SDK's SSE error-event path raises the exact base APIError.
|
|
667
|
+
# Subclasses such as APIStatusError and APIResponseValidationError have
|
|
668
|
+
# distinct authoritative semantics; an in-body code must not override
|
|
669
|
+
# them even when a hostile body contradicts their typed state.
|
|
670
|
+
if type(node) is not APIError:
|
|
671
|
+
continue
|
|
672
|
+
try:
|
|
673
|
+
body = BaseException.__getattribute__(node, "body")
|
|
674
|
+
except BaseException:
|
|
675
|
+
continue
|
|
676
|
+
code = _exact_dict_value(body, "code")
|
|
677
|
+
if type(code) is int and 400 <= code <= 599:
|
|
678
|
+
return _http_failure(code, body, exc)
|
|
679
|
+
return None
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def classify_generation_exception(
|
|
683
|
+
exc: BaseException,
|
|
684
|
+
*,
|
|
685
|
+
output_type: type[Any] | None = None,
|
|
686
|
+
semantic_progress_observed: bool = False,
|
|
687
|
+
) -> StructuredGenerationError:
|
|
688
|
+
"""Translate framework/provider failures without retaining raw provider text."""
|
|
689
|
+
|
|
690
|
+
if type(semantic_progress_observed) is not bool:
|
|
691
|
+
raise TypeError("semantic_progress_observed must be an exact bool")
|
|
692
|
+
|
|
693
|
+
# Imports stay local so importing the provider-neutral port remains cheap.
|
|
694
|
+
from pydantic_ai.exceptions import (
|
|
695
|
+
ContentFilterError,
|
|
696
|
+
IncompleteToolCall,
|
|
697
|
+
ModelAPIError,
|
|
698
|
+
ModelHTTPError,
|
|
699
|
+
UnexpectedModelBehavior,
|
|
700
|
+
UsageLimitExceeded,
|
|
701
|
+
UserError,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
if isinstance(exc, StructuredGenerationError):
|
|
705
|
+
return exc
|
|
706
|
+
# Pydantic-AI 1.107.1 assumes every decoded OpenAI SSE value is a
|
|
707
|
+
# ChatCompletionChunk before validating its runtime type. Admit only our
|
|
708
|
+
# exact, payload-free compatibility-boundary exception, including bounded
|
|
709
|
+
# framework wrapping.
|
|
710
|
+
# Generic AttributeError, ValidationError, and name lookalikes remain
|
|
711
|
+
# terminal UNKNOWN failures.
|
|
712
|
+
from agent_evolve.integrations.pydantic_ai.validated_openrouter_model import (
|
|
713
|
+
InvalidOpenRouterStreamItemError,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
if any(
|
|
717
|
+
type(node) is InvalidOpenRouterStreamItemError
|
|
718
|
+
for node in _bounded_exception_nodes(exc)
|
|
719
|
+
):
|
|
720
|
+
return StructuredGenerationError(
|
|
721
|
+
kind=GenerationFailureKind.PROVIDER_UNAVAILABLE,
|
|
722
|
+
retryable=not semantic_progress_observed,
|
|
723
|
+
safe_message=(
|
|
724
|
+
"provider stream returned an invalid item"
|
|
725
|
+
if not semantic_progress_observed
|
|
726
|
+
else (
|
|
727
|
+
"provider stream returned an invalid item after "
|
|
728
|
+
"semantic progress"
|
|
729
|
+
)
|
|
730
|
+
),
|
|
731
|
+
)
|
|
732
|
+
if isinstance(exc, ModelHTTPError):
|
|
733
|
+
return _http_failure(exc.status_code, exc.body, exc)
|
|
734
|
+
if isinstance(exc, ModelAPIError):
|
|
735
|
+
return _model_api_failure(exc)
|
|
736
|
+
if isinstance(exc, ContentFilterError):
|
|
737
|
+
return StructuredGenerationError(
|
|
738
|
+
kind=GenerationFailureKind.CONTENT_REJECTED,
|
|
739
|
+
retryable=False,
|
|
740
|
+
safe_message="provider content filter rejected the model response",
|
|
741
|
+
)
|
|
742
|
+
if isinstance(exc, IncompleteToolCall):
|
|
743
|
+
return StructuredGenerationError(
|
|
744
|
+
kind=GenerationFailureKind.OUTPUT_INVALID,
|
|
745
|
+
retryable=True,
|
|
746
|
+
safe_message="model stopped while emitting the typed output tool call",
|
|
747
|
+
output_failure_mode=StructuredOutputFailureMode.INCOMPLETE_TOOL_CALL,
|
|
748
|
+
)
|
|
749
|
+
if isinstance(exc, UnexpectedModelBehavior):
|
|
750
|
+
output_failure_mode, validation_issues = _structured_output_diagnostics(
|
|
751
|
+
exc,
|
|
752
|
+
output_type=output_type,
|
|
753
|
+
)
|
|
754
|
+
return StructuredGenerationError(
|
|
755
|
+
kind=GenerationFailureKind.OUTPUT_INVALID,
|
|
756
|
+
retryable=True,
|
|
757
|
+
safe_message="model output violated the typed response contract",
|
|
758
|
+
output_failure_mode=output_failure_mode,
|
|
759
|
+
validation_issues=validation_issues,
|
|
760
|
+
)
|
|
761
|
+
if isinstance(exc, UsageLimitExceeded):
|
|
762
|
+
return StructuredGenerationError(
|
|
763
|
+
kind=GenerationFailureKind.OUTPUT_INVALID,
|
|
764
|
+
retryable=False,
|
|
765
|
+
safe_message="logical call exceeded its frozen usage limit",
|
|
766
|
+
)
|
|
767
|
+
if isinstance(exc, UserError):
|
|
768
|
+
return StructuredGenerationError(
|
|
769
|
+
kind=GenerationFailureKind.INVALID_REQUEST,
|
|
770
|
+
retryable=False,
|
|
771
|
+
safe_message="invalid Pydantic-AI request configuration",
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
in_band_failure = _in_band_openai_api_failure(exc)
|
|
775
|
+
if in_band_failure is not None:
|
|
776
|
+
return in_band_failure
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
import httpx
|
|
780
|
+
except ImportError: # pragma: no cover - OpenRouter installs HTTPX.
|
|
781
|
+
httpx_timeout_types: tuple[type[BaseException], ...] = ()
|
|
782
|
+
httpx_network_types: tuple[type[BaseException], ...] = ()
|
|
783
|
+
httpx_remote_protocol_types: tuple[type[BaseException], ...] = ()
|
|
784
|
+
else:
|
|
785
|
+
httpx_timeout_types = (httpx.TimeoutException,)
|
|
786
|
+
httpx_network_types = (httpx.NetworkError,)
|
|
787
|
+
# ``RemoteProtocolError`` is a sibling of ``NetworkError`` under
|
|
788
|
+
# HTTPX's ``TransportError`` hierarchy. A provider/proxy closing an
|
|
789
|
+
# otherwise valid response stream therefore used to fall through to
|
|
790
|
+
# UNKNOWN and terminate an entire concurrent forecast wave. Admit
|
|
791
|
+
# only the remote subtype: ``LocalProtocolError`` can indicate a bad
|
|
792
|
+
# request and must continue to fail closed.
|
|
793
|
+
httpx_remote_protocol_types = (httpx.RemoteProtocolError,)
|
|
794
|
+
|
|
795
|
+
try:
|
|
796
|
+
import httpcore
|
|
797
|
+
except ImportError: # pragma: no cover - HTTPX installs HTTPCore.
|
|
798
|
+
httpcore_remote_protocol_types: tuple[type[BaseException], ...] = ()
|
|
799
|
+
else:
|
|
800
|
+
# Preserve the same typed classification when a framework exposes the
|
|
801
|
+
# transport cause directly instead of translating it to HTTPX.
|
|
802
|
+
httpcore_remote_protocol_types = (httpcore.RemoteProtocolError,)
|
|
803
|
+
|
|
804
|
+
nodes = _bounded_exception_nodes(exc)
|
|
805
|
+
if any(isinstance(node, (TimeoutError, *httpx_timeout_types)) for node in nodes):
|
|
806
|
+
return StructuredGenerationError(
|
|
807
|
+
kind=GenerationFailureKind.TIMEOUT,
|
|
808
|
+
retryable=True,
|
|
809
|
+
safe_message="provider transport timed out",
|
|
810
|
+
retry_after_seconds=_retry_after_from_exception(exc),
|
|
811
|
+
)
|
|
812
|
+
if any(
|
|
813
|
+
isinstance(
|
|
814
|
+
node,
|
|
815
|
+
(*httpx_remote_protocol_types, *httpcore_remote_protocol_types),
|
|
816
|
+
)
|
|
817
|
+
for node in nodes
|
|
818
|
+
):
|
|
819
|
+
return StructuredGenerationError(
|
|
820
|
+
kind=GenerationFailureKind.PROVIDER_UNAVAILABLE,
|
|
821
|
+
retryable=True,
|
|
822
|
+
safe_message="provider response stream was interrupted remotely",
|
|
823
|
+
retry_after_seconds=_retry_after_from_exception(exc),
|
|
824
|
+
)
|
|
825
|
+
if any(isinstance(node, (ConnectionError, *httpx_network_types)) for node in nodes):
|
|
826
|
+
return StructuredGenerationError(
|
|
827
|
+
kind=GenerationFailureKind.PROVIDER_UNAVAILABLE,
|
|
828
|
+
retryable=True,
|
|
829
|
+
safe_message="provider transport unavailable",
|
|
830
|
+
retry_after_seconds=_retry_after_from_exception(exc),
|
|
831
|
+
)
|
|
832
|
+
return StructuredGenerationError(
|
|
833
|
+
kind=GenerationFailureKind.UNKNOWN,
|
|
834
|
+
retryable=False,
|
|
835
|
+
safe_message="unclassified generation adapter failure",
|
|
836
|
+
exception_provenance=sanitized_exception_provenance(exc),
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _usage_detail(details: Mapping[str, object], *names: str) -> int:
|
|
841
|
+
for name in names:
|
|
842
|
+
value = details.get(name)
|
|
843
|
+
if type(value) is int and value >= 0:
|
|
844
|
+
return value
|
|
845
|
+
return 0
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _cost(value: object) -> Decimal | None:
|
|
849
|
+
if value is None or isinstance(value, bool):
|
|
850
|
+
return None
|
|
851
|
+
try:
|
|
852
|
+
result = Decimal(str(value))
|
|
853
|
+
except (InvalidOperation, ValueError):
|
|
854
|
+
return None
|
|
855
|
+
if not result.is_finite() or result < 0:
|
|
856
|
+
return None
|
|
857
|
+
return result
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
@dataclass(frozen=True, slots=True)
|
|
861
|
+
class OpenRouterReasoningConfig:
|
|
862
|
+
"""Validated OpenRouter reasoning control owned by the composition root.
|
|
863
|
+
|
|
864
|
+
OpenRouter accepts either a qualitative effort level or an explicit reasoning
|
|
865
|
+
token budget. Keeping that choice in a frozen value object prevents arbitrary
|
|
866
|
+
provider request fields from leaking through benchmark adapters.
|
|
867
|
+
"""
|
|
868
|
+
|
|
869
|
+
effort: OpenRouterReasoningEffort | None = None
|
|
870
|
+
max_tokens: int | None = None
|
|
871
|
+
|
|
872
|
+
def __post_init__(self) -> None:
|
|
873
|
+
has_effort = self.effort is not None
|
|
874
|
+
has_max_tokens = self.max_tokens is not None
|
|
875
|
+
if has_effort == has_max_tokens:
|
|
876
|
+
raise ValueError("exactly one of effort or max_tokens must be supplied")
|
|
877
|
+
if has_effort and (
|
|
878
|
+
type(self.effort) is not str
|
|
879
|
+
or self.effort not in _OPENROUTER_REASONING_EFFORTS
|
|
880
|
+
):
|
|
881
|
+
raise ValueError("effort is not a supported OpenRouter reasoning level")
|
|
882
|
+
if has_max_tokens and (
|
|
883
|
+
type(self.max_tokens) is not int or self.max_tokens <= 0
|
|
884
|
+
):
|
|
885
|
+
raise ValueError("max_tokens must be a positive integer")
|
|
886
|
+
|
|
887
|
+
def to_model_setting(self) -> dict[str, object]:
|
|
888
|
+
"""Return the closed provider payload; no caller-owned mapping is reused."""
|
|
889
|
+
|
|
890
|
+
if self.effort is not None:
|
|
891
|
+
return {"effort": self.effort}
|
|
892
|
+
return {"max_tokens": self.max_tokens}
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class PydanticAIStructuredGenerator:
|
|
896
|
+
"""Reusable async Pydantic-AI agent that executes one attempt per call."""
|
|
897
|
+
|
|
898
|
+
def __init__(
|
|
899
|
+
self,
|
|
900
|
+
*,
|
|
901
|
+
agent: Any,
|
|
902
|
+
requested_model: str,
|
|
903
|
+
provider_options: Mapping[str, object] | None = None,
|
|
904
|
+
reasoning_config: OpenRouterReasoningConfig | None = None,
|
|
905
|
+
structured_output_mode: OpenRouterStructuredOutputMode = (
|
|
906
|
+
OpenRouterStructuredOutputMode.TOOL
|
|
907
|
+
),
|
|
908
|
+
structured_output_strict: bool = False,
|
|
909
|
+
supports_forced_tool_choice: bool = True,
|
|
910
|
+
owned_openai_client: Any | None = None,
|
|
911
|
+
stream_liveness_policy: StructuredStreamLivenessPolicy | None = None,
|
|
912
|
+
stream_progress_sink: StructuredStreamProgressSink | None = None,
|
|
913
|
+
stream_supervisor: ContentBlindStreamSupervisor | None = None,
|
|
914
|
+
outbound_request_manifest_publisher: (
|
|
915
|
+
OpenRouterOutboundRequestManifestPublisher | None
|
|
916
|
+
) = None,
|
|
917
|
+
) -> None:
|
|
918
|
+
if type(requested_model) is not str or not requested_model.strip():
|
|
919
|
+
raise ValueError("requested_model must be non-empty")
|
|
920
|
+
if (
|
|
921
|
+
reasoning_config is not None
|
|
922
|
+
and type(reasoning_config) is not OpenRouterReasoningConfig
|
|
923
|
+
):
|
|
924
|
+
raise TypeError("reasoning_config must be an OpenRouterReasoningConfig")
|
|
925
|
+
if type(structured_output_mode) is not OpenRouterStructuredOutputMode:
|
|
926
|
+
raise TypeError(
|
|
927
|
+
"structured_output_mode must be an exact "
|
|
928
|
+
"OpenRouterStructuredOutputMode"
|
|
929
|
+
)
|
|
930
|
+
if type(structured_output_strict) is not bool:
|
|
931
|
+
raise TypeError("structured_output_strict must be an exact bool")
|
|
932
|
+
if type(supports_forced_tool_choice) is not bool:
|
|
933
|
+
raise TypeError("supports_forced_tool_choice must be an exact bool")
|
|
934
|
+
if stream_liveness_policy is not None and (
|
|
935
|
+
type(stream_liveness_policy) is not StructuredStreamLivenessPolicy
|
|
936
|
+
):
|
|
937
|
+
raise TypeError(
|
|
938
|
+
"stream_liveness_policy must be a StructuredStreamLivenessPolicy"
|
|
939
|
+
)
|
|
940
|
+
if stream_progress_sink is not None and not callable(stream_progress_sink):
|
|
941
|
+
raise TypeError("stream_progress_sink must be callable or None")
|
|
942
|
+
if stream_liveness_policy is None and (
|
|
943
|
+
stream_progress_sink is not None or stream_supervisor is not None
|
|
944
|
+
):
|
|
945
|
+
raise ValueError(
|
|
946
|
+
"stream progress dependencies require a stream liveness policy"
|
|
947
|
+
)
|
|
948
|
+
if stream_supervisor is not None and not isinstance(
|
|
949
|
+
stream_supervisor,
|
|
950
|
+
ContentBlindStreamSupervisor,
|
|
951
|
+
):
|
|
952
|
+
raise TypeError(
|
|
953
|
+
"stream_supervisor must implement ContentBlindStreamSupervisor"
|
|
954
|
+
)
|
|
955
|
+
if (
|
|
956
|
+
outbound_request_manifest_publisher is not None
|
|
957
|
+
and type(outbound_request_manifest_publisher)
|
|
958
|
+
is not OpenRouterOutboundRequestManifestPublisher
|
|
959
|
+
):
|
|
960
|
+
raise TypeError(
|
|
961
|
+
"outbound_request_manifest_publisher must be an exact "
|
|
962
|
+
"OpenRouterOutboundRequestManifestPublisher or None"
|
|
963
|
+
)
|
|
964
|
+
self._agent = agent
|
|
965
|
+
self.requested_model = requested_model
|
|
966
|
+
self._provider_options = dict(provider_options or {"allow_fallbacks": True})
|
|
967
|
+
self._reasoning_config = reasoning_config
|
|
968
|
+
self._structured_output_mode = structured_output_mode
|
|
969
|
+
self._structured_output_strict = structured_output_strict
|
|
970
|
+
self._supports_forced_tool_choice = supports_forced_tool_choice
|
|
971
|
+
self._owned_openai_client = owned_openai_client
|
|
972
|
+
self._stream_liveness_policy = stream_liveness_policy
|
|
973
|
+
self._stream_progress_sink = stream_progress_sink
|
|
974
|
+
self._outbound_request_manifest_publisher = outbound_request_manifest_publisher
|
|
975
|
+
self._transport_retired = False
|
|
976
|
+
self._stream_supervisor = (
|
|
977
|
+
AsyncioContentBlindStreamSupervisor(
|
|
978
|
+
retirement_operation=self._retire_owned_transport,
|
|
979
|
+
)
|
|
980
|
+
if stream_liveness_policy is not None and stream_supervisor is None
|
|
981
|
+
else stream_supervisor
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
@property
|
|
985
|
+
def stream_liveness_policy(self) -> StructuredStreamLivenessPolicy | None:
|
|
986
|
+
"""Return the immutable content-blind policy, if streaming is enabled."""
|
|
987
|
+
|
|
988
|
+
return self._stream_liveness_policy
|
|
989
|
+
|
|
990
|
+
@classmethod
|
|
991
|
+
def openrouter(
|
|
992
|
+
cls,
|
|
993
|
+
*,
|
|
994
|
+
api_key: str,
|
|
995
|
+
model_name: str,
|
|
996
|
+
max_connections: int,
|
|
997
|
+
timeout_seconds: float = 90.0,
|
|
998
|
+
provider_options: Mapping[str, object] | None = None,
|
|
999
|
+
reasoning_config: OpenRouterReasoningConfig | None = None,
|
|
1000
|
+
structured_output_mode: OpenRouterStructuredOutputMode = (
|
|
1001
|
+
OpenRouterStructuredOutputMode.TOOL
|
|
1002
|
+
),
|
|
1003
|
+
structured_output_strict: bool = False,
|
|
1004
|
+
supports_forced_tool_choice: bool = True,
|
|
1005
|
+
json_schema_dialect: OpenRouterJsonSchemaDialect = (
|
|
1006
|
+
OpenRouterJsonSchemaDialect.PROVIDER_DEFAULT
|
|
1007
|
+
),
|
|
1008
|
+
app_title: str = "AgentEvolve research",
|
|
1009
|
+
stream_liveness_policy: StructuredStreamLivenessPolicy | None = None,
|
|
1010
|
+
stream_progress_sink: StructuredStreamProgressSink | None = None,
|
|
1011
|
+
outbound_request_manifest_sink: (
|
|
1012
|
+
OpenRouterOutboundRequestManifestSink | None
|
|
1013
|
+
) = None,
|
|
1014
|
+
) -> "PydanticAIStructuredGenerator":
|
|
1015
|
+
"""Build the production adapter with SDK retries explicitly disabled."""
|
|
1016
|
+
|
|
1017
|
+
if type(api_key) is not str or not api_key:
|
|
1018
|
+
raise ValueError("api_key must be supplied at the composition root")
|
|
1019
|
+
if type(model_name) is not str or "/" not in model_name:
|
|
1020
|
+
raise ValueError("model_name must be an OpenRouter model slug")
|
|
1021
|
+
if type(max_connections) is not int or not 1 <= max_connections <= 256:
|
|
1022
|
+
raise ValueError("max_connections must lie in [1,256]")
|
|
1023
|
+
if (
|
|
1024
|
+
isinstance(timeout_seconds, bool)
|
|
1025
|
+
or not isinstance(timeout_seconds, (int, float))
|
|
1026
|
+
or not math.isfinite(float(timeout_seconds))
|
|
1027
|
+
or not 1 <= float(timeout_seconds) <= 600
|
|
1028
|
+
):
|
|
1029
|
+
raise ValueError("timeout_seconds must lie in [1,600]")
|
|
1030
|
+
if stream_liveness_policy is not None and (
|
|
1031
|
+
type(stream_liveness_policy) is not StructuredStreamLivenessPolicy
|
|
1032
|
+
):
|
|
1033
|
+
raise TypeError(
|
|
1034
|
+
"stream_liveness_policy must be a StructuredStreamLivenessPolicy"
|
|
1035
|
+
)
|
|
1036
|
+
if stream_progress_sink is not None and not callable(stream_progress_sink):
|
|
1037
|
+
raise TypeError("stream_progress_sink must be callable or None")
|
|
1038
|
+
if outbound_request_manifest_sink is not None and not callable(
|
|
1039
|
+
outbound_request_manifest_sink
|
|
1040
|
+
):
|
|
1041
|
+
raise TypeError("outbound_request_manifest_sink must be callable or None")
|
|
1042
|
+
if type(structured_output_mode) is not OpenRouterStructuredOutputMode:
|
|
1043
|
+
raise TypeError(
|
|
1044
|
+
"structured_output_mode must be an exact "
|
|
1045
|
+
"OpenRouterStructuredOutputMode"
|
|
1046
|
+
)
|
|
1047
|
+
if type(structured_output_strict) is not bool:
|
|
1048
|
+
raise TypeError("structured_output_strict must be an exact bool")
|
|
1049
|
+
if type(supports_forced_tool_choice) is not bool:
|
|
1050
|
+
raise TypeError("supports_forced_tool_choice must be an exact bool")
|
|
1051
|
+
if type(json_schema_dialect) is not OpenRouterJsonSchemaDialect:
|
|
1052
|
+
raise TypeError(
|
|
1053
|
+
"json_schema_dialect must be an exact "
|
|
1054
|
+
"OpenRouterJsonSchemaDialect"
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
import httpx
|
|
1058
|
+
from openai import AsyncOpenAI
|
|
1059
|
+
from pydantic_ai import Agent
|
|
1060
|
+
from pydantic_ai.providers.openrouter import OpenRouterProvider
|
|
1061
|
+
|
|
1062
|
+
from agent_evolve.integrations.pydantic_ai.validated_openrouter_model import (
|
|
1063
|
+
ValidatedOpenRouterModel,
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
# A streamed response's read liveness is owned by the content-blind
|
|
1067
|
+
# first-event/idle supervisor. Connect, pool, and write operations
|
|
1068
|
+
# retain bounded SDK timeouts, while reads have no competing fixed
|
|
1069
|
+
# total boundary that could censor a healthy long generation.
|
|
1070
|
+
transport_timeout = (
|
|
1071
|
+
httpx.Timeout(
|
|
1072
|
+
connect=float(timeout_seconds),
|
|
1073
|
+
pool=float(timeout_seconds),
|
|
1074
|
+
write=float(timeout_seconds),
|
|
1075
|
+
read=None,
|
|
1076
|
+
)
|
|
1077
|
+
if stream_liveness_policy is not None
|
|
1078
|
+
else httpx.Timeout(float(timeout_seconds))
|
|
1079
|
+
)
|
|
1080
|
+
resolved_profile = OpenRouterProvider.model_profile(model_name)
|
|
1081
|
+
if resolved_profile is None:
|
|
1082
|
+
raise ValueError("OpenRouter model has no resolvable execution profile")
|
|
1083
|
+
if (
|
|
1084
|
+
structured_output_mode
|
|
1085
|
+
is OpenRouterStructuredOutputMode.NATIVE_JSON_SCHEMA
|
|
1086
|
+
):
|
|
1087
|
+
resolved_profile = replace(
|
|
1088
|
+
resolved_profile,
|
|
1089
|
+
supports_json_schema_output=True,
|
|
1090
|
+
)
|
|
1091
|
+
if not supports_forced_tool_choice:
|
|
1092
|
+
resolved_profile = replace(
|
|
1093
|
+
resolved_profile,
|
|
1094
|
+
openai_supports_tool_choice_required=False,
|
|
1095
|
+
)
|
|
1096
|
+
resolved_profile = replace(
|
|
1097
|
+
resolved_profile,
|
|
1098
|
+
json_schema_transformer=json_schema_transformer_for_dialect(
|
|
1099
|
+
resolved_profile.json_schema_transformer,
|
|
1100
|
+
json_schema_dialect,
|
|
1101
|
+
),
|
|
1102
|
+
)
|
|
1103
|
+
outbound_publisher = (
|
|
1104
|
+
None
|
|
1105
|
+
if outbound_request_manifest_sink is None
|
|
1106
|
+
else OpenRouterOutboundRequestManifestPublisher(
|
|
1107
|
+
outbound_request_manifest_sink,
|
|
1108
|
+
json_schema_transformer=(
|
|
1109
|
+
resolved_profile.json_schema_transformer
|
|
1110
|
+
),
|
|
1111
|
+
)
|
|
1112
|
+
)
|
|
1113
|
+
http_client = httpx.AsyncClient(
|
|
1114
|
+
timeout=transport_timeout,
|
|
1115
|
+
limits=httpx.Limits(
|
|
1116
|
+
max_connections=max_connections,
|
|
1117
|
+
max_keepalive_connections=max_connections,
|
|
1118
|
+
),
|
|
1119
|
+
event_hooks=(
|
|
1120
|
+
None
|
|
1121
|
+
if outbound_publisher is None
|
|
1122
|
+
else {"request": [outbound_publisher.httpx_request_hook]}
|
|
1123
|
+
),
|
|
1124
|
+
)
|
|
1125
|
+
openai_client = AsyncOpenAI(
|
|
1126
|
+
base_url="https://openrouter.ai/api/v1",
|
|
1127
|
+
api_key=api_key,
|
|
1128
|
+
max_retries=0,
|
|
1129
|
+
http_client=http_client,
|
|
1130
|
+
default_headers={"X-Title": app_title},
|
|
1131
|
+
)
|
|
1132
|
+
provider = OpenRouterProvider(openai_client=openai_client)
|
|
1133
|
+
model = ValidatedOpenRouterModel(
|
|
1134
|
+
model_name,
|
|
1135
|
+
provider=provider,
|
|
1136
|
+
profile=resolved_profile,
|
|
1137
|
+
)
|
|
1138
|
+
# One integer freezes both tool and output retry budgets at zero on the
|
|
1139
|
+
# maintained Pydantic-AI v1 API. The application queue remains the
|
|
1140
|
+
# only retry owner.
|
|
1141
|
+
agent = Agent(model, retries=0)
|
|
1142
|
+
return cls(
|
|
1143
|
+
agent=agent,
|
|
1144
|
+
requested_model=model_name,
|
|
1145
|
+
provider_options=provider_options,
|
|
1146
|
+
reasoning_config=reasoning_config,
|
|
1147
|
+
structured_output_mode=structured_output_mode,
|
|
1148
|
+
structured_output_strict=structured_output_strict,
|
|
1149
|
+
supports_forced_tool_choice=supports_forced_tool_choice,
|
|
1150
|
+
owned_openai_client=openai_client,
|
|
1151
|
+
stream_liveness_policy=stream_liveness_policy,
|
|
1152
|
+
stream_progress_sink=stream_progress_sink,
|
|
1153
|
+
outbound_request_manifest_publisher=outbound_publisher,
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
async def aclose(self) -> None:
|
|
1157
|
+
self._transport_retired = True
|
|
1158
|
+
if self._owned_openai_client is not None:
|
|
1159
|
+
client = self._owned_openai_client
|
|
1160
|
+
self._owned_openai_client = None
|
|
1161
|
+
if self._stream_liveness_policy is None:
|
|
1162
|
+
await client.close()
|
|
1163
|
+
return
|
|
1164
|
+
close_task = asyncio.create_task(client.close())
|
|
1165
|
+
done, _ = await asyncio.wait(
|
|
1166
|
+
(close_task,),
|
|
1167
|
+
timeout=(
|
|
1168
|
+
self._stream_liveness_policy.cleanup_policy.transport_retire_timeout_ns
|
|
1169
|
+
/ 1_000_000_000
|
|
1170
|
+
),
|
|
1171
|
+
return_when=asyncio.ALL_COMPLETED,
|
|
1172
|
+
)
|
|
1173
|
+
if close_task in done:
|
|
1174
|
+
# Preserve an immediate close failure for explicit shutdown.
|
|
1175
|
+
await close_task
|
|
1176
|
+
return
|
|
1177
|
+
close_task.cancel()
|
|
1178
|
+
close_task.add_done_callback(_consume_close_task)
|
|
1179
|
+
|
|
1180
|
+
async def _retire_owned_transport(self) -> None:
|
|
1181
|
+
"""Irreversibly reject new calls, then close the detached owned client."""
|
|
1182
|
+
|
|
1183
|
+
self._transport_retired = True
|
|
1184
|
+
if self._owned_openai_client is not None:
|
|
1185
|
+
client = self._owned_openai_client
|
|
1186
|
+
self._owned_openai_client = None
|
|
1187
|
+
await client.close()
|
|
1188
|
+
|
|
1189
|
+
async def __aenter__(self) -> "PydanticAIStructuredGenerator":
|
|
1190
|
+
return self
|
|
1191
|
+
|
|
1192
|
+
async def __aexit__(self, *_: object) -> None:
|
|
1193
|
+
await self.aclose()
|
|
1194
|
+
|
|
1195
|
+
async def generate_once(
|
|
1196
|
+
self, request: StructuredGenerationRequest[OutputT]
|
|
1197
|
+
) -> StructuredGenerationResponse[OutputT]:
|
|
1198
|
+
from pydantic_ai import NativeOutput, ToolOutput
|
|
1199
|
+
from pydantic_ai.messages import ModelResponse
|
|
1200
|
+
from pydantic_ai.usage import UsageLimits
|
|
1201
|
+
|
|
1202
|
+
if type(request) is not StructuredGenerationRequest:
|
|
1203
|
+
raise TypeError("request must be an exact StructuredGenerationRequest")
|
|
1204
|
+
StructuredGenerationRequest.__post_init__(request)
|
|
1205
|
+
if self._transport_retired:
|
|
1206
|
+
raise StructuredGenerationError(
|
|
1207
|
+
kind=GenerationFailureKind.CANCELLED,
|
|
1208
|
+
retryable=False,
|
|
1209
|
+
safe_message=(
|
|
1210
|
+
"provider transport is retired after incomplete stream cleanup"
|
|
1211
|
+
),
|
|
1212
|
+
)
|
|
1213
|
+
settings: dict[str, object] = {
|
|
1214
|
+
"max_tokens": request.max_output_tokens,
|
|
1215
|
+
"openrouter_provider": dict(self._provider_options),
|
|
1216
|
+
"openrouter_usage": {"include": True},
|
|
1217
|
+
}
|
|
1218
|
+
if request.temperature is not None:
|
|
1219
|
+
settings["temperature"] = float(request.temperature)
|
|
1220
|
+
if self._reasoning_config is not None:
|
|
1221
|
+
settings["openrouter_reasoning"] = self._reasoning_config.to_model_setting()
|
|
1222
|
+
|
|
1223
|
+
started = time.perf_counter_ns()
|
|
1224
|
+
sanitized_failure: StructuredGenerationError | None = None
|
|
1225
|
+
semantic_progress_observed = False
|
|
1226
|
+
try:
|
|
1227
|
+
output_type = (
|
|
1228
|
+
ToolOutput(
|
|
1229
|
+
request.output_type,
|
|
1230
|
+
name=request.output_tool_name,
|
|
1231
|
+
strict=self._structured_output_strict,
|
|
1232
|
+
)
|
|
1233
|
+
if self._structured_output_mode
|
|
1234
|
+
is OpenRouterStructuredOutputMode.TOOL
|
|
1235
|
+
else NativeOutput(
|
|
1236
|
+
request.output_type,
|
|
1237
|
+
name=request.output_tool_name,
|
|
1238
|
+
strict=self._structured_output_strict,
|
|
1239
|
+
)
|
|
1240
|
+
)
|
|
1241
|
+
usage_limits = UsageLimits(
|
|
1242
|
+
request_limit=1,
|
|
1243
|
+
output_tokens_limit=request.max_output_tokens,
|
|
1244
|
+
)
|
|
1245
|
+
if self._stream_liveness_policy is None:
|
|
1246
|
+
if self._outbound_request_manifest_publisher is None:
|
|
1247
|
+
result = await self._agent.run(
|
|
1248
|
+
request.prompt,
|
|
1249
|
+
output_type=output_type,
|
|
1250
|
+
model_settings=settings,
|
|
1251
|
+
usage_limits=usage_limits,
|
|
1252
|
+
)
|
|
1253
|
+
else:
|
|
1254
|
+
with self._outbound_request_manifest_publisher.bind(
|
|
1255
|
+
request,
|
|
1256
|
+
requested_model=self.requested_model,
|
|
1257
|
+
provider=self._provider_options,
|
|
1258
|
+
reasoning=(
|
|
1259
|
+
None
|
|
1260
|
+
if self._reasoning_config is None
|
|
1261
|
+
else self._reasoning_config.to_model_setting()
|
|
1262
|
+
),
|
|
1263
|
+
stream=False,
|
|
1264
|
+
output_mode=self._structured_output_mode.value,
|
|
1265
|
+
output_strict=self._structured_output_strict,
|
|
1266
|
+
expected_tool_choice=(
|
|
1267
|
+
"required"
|
|
1268
|
+
if self._supports_forced_tool_choice
|
|
1269
|
+
else "auto"
|
|
1270
|
+
),
|
|
1271
|
+
):
|
|
1272
|
+
result = await self._agent.run(
|
|
1273
|
+
request.prompt,
|
|
1274
|
+
output_type=output_type,
|
|
1275
|
+
model_settings=settings,
|
|
1276
|
+
usage_limits=usage_limits,
|
|
1277
|
+
)
|
|
1278
|
+
else:
|
|
1279
|
+
assert self._stream_supervisor is not None
|
|
1280
|
+
content_hasher = hashlib.sha256(_STREAM_CONTENT_IDENTITY_DOMAIN)
|
|
1281
|
+
cumulative_content_utf8_bytes = 0
|
|
1282
|
+
|
|
1283
|
+
async def streamed_operation(mark_progress: StreamProgressMarker):
|
|
1284
|
+
async def handle_events(_context: Any, events: Any) -> None:
|
|
1285
|
+
nonlocal cumulative_content_utf8_bytes
|
|
1286
|
+
nonlocal semantic_progress_observed
|
|
1287
|
+
async for event in events:
|
|
1288
|
+
projection = _stream_progress_projection(event)
|
|
1289
|
+
if projection is not None:
|
|
1290
|
+
kind, channel, fragments = projection
|
|
1291
|
+
event_content_utf8_bytes = 0
|
|
1292
|
+
for field, content in fragments:
|
|
1293
|
+
field_bytes = field.encode("ascii", errors="strict")
|
|
1294
|
+
content_bytes = content.encode(
|
|
1295
|
+
"utf-8", errors="strict"
|
|
1296
|
+
)
|
|
1297
|
+
event_content_utf8_bytes += len(content_bytes)
|
|
1298
|
+
content_hasher.update(
|
|
1299
|
+
len(field_bytes).to_bytes(2, "big")
|
|
1300
|
+
)
|
|
1301
|
+
content_hasher.update(field_bytes)
|
|
1302
|
+
content_hasher.update(
|
|
1303
|
+
len(content_bytes).to_bytes(8, "big")
|
|
1304
|
+
)
|
|
1305
|
+
content_hasher.update(content_bytes)
|
|
1306
|
+
cumulative_content_utf8_bytes += (
|
|
1307
|
+
event_content_utf8_bytes
|
|
1308
|
+
)
|
|
1309
|
+
mark_progress(
|
|
1310
|
+
kind,
|
|
1311
|
+
channel,
|
|
1312
|
+
event_content_utf8_bytes=(event_content_utf8_bytes),
|
|
1313
|
+
cumulative_content_utf8_bytes=(
|
|
1314
|
+
cumulative_content_utf8_bytes
|
|
1315
|
+
),
|
|
1316
|
+
rolling_content_sha256=(content_hasher.hexdigest()),
|
|
1317
|
+
)
|
|
1318
|
+
# This flag is deliberately attempt-local and
|
|
1319
|
+
# content-blind. Once any supported semantic
|
|
1320
|
+
# model event has been durably projected, an
|
|
1321
|
+
# invalid later decoded SSE item must not cause
|
|
1322
|
+
# an exact-payload replay of an ambiguous
|
|
1323
|
+
# partial generation.
|
|
1324
|
+
semantic_progress_observed = True
|
|
1325
|
+
|
|
1326
|
+
if self._outbound_request_manifest_publisher is None:
|
|
1327
|
+
result = await self._agent.run(
|
|
1328
|
+
request.prompt,
|
|
1329
|
+
output_type=output_type,
|
|
1330
|
+
model_settings=settings,
|
|
1331
|
+
usage_limits=usage_limits,
|
|
1332
|
+
event_stream_handler=handle_events,
|
|
1333
|
+
)
|
|
1334
|
+
else:
|
|
1335
|
+
with self._outbound_request_manifest_publisher.bind(
|
|
1336
|
+
request,
|
|
1337
|
+
requested_model=self.requested_model,
|
|
1338
|
+
provider=self._provider_options,
|
|
1339
|
+
reasoning=(
|
|
1340
|
+
None
|
|
1341
|
+
if self._reasoning_config is None
|
|
1342
|
+
else self._reasoning_config.to_model_setting()
|
|
1343
|
+
),
|
|
1344
|
+
stream=True,
|
|
1345
|
+
output_mode=self._structured_output_mode.value,
|
|
1346
|
+
output_strict=self._structured_output_strict,
|
|
1347
|
+
expected_tool_choice=(
|
|
1348
|
+
"required"
|
|
1349
|
+
if self._supports_forced_tool_choice
|
|
1350
|
+
else "auto"
|
|
1351
|
+
),
|
|
1352
|
+
):
|
|
1353
|
+
result = await self._agent.run(
|
|
1354
|
+
request.prompt,
|
|
1355
|
+
output_type=output_type,
|
|
1356
|
+
model_settings=settings,
|
|
1357
|
+
usage_limits=usage_limits,
|
|
1358
|
+
event_stream_handler=handle_events,
|
|
1359
|
+
)
|
|
1360
|
+
# Pydantic-AI's FinalResultEvent means that the output tool
|
|
1361
|
+
# has been selected; tool argument deltas may follow it.
|
|
1362
|
+
# Only the return from Agent.run proves the stream is done
|
|
1363
|
+
# and a typed output is now available. Keep this local
|
|
1364
|
+
# marker inside liveness supervision so completion itself
|
|
1365
|
+
# remains subject to the idle/absolute policy.
|
|
1366
|
+
_ = result.output
|
|
1367
|
+
mark_progress(
|
|
1368
|
+
StructuredStreamProgressKind.STREAM_COMPLETED,
|
|
1369
|
+
StructuredStreamChannel.OTHER,
|
|
1370
|
+
event_content_utf8_bytes=0,
|
|
1371
|
+
cumulative_content_utf8_bytes=(cumulative_content_utf8_bytes),
|
|
1372
|
+
rolling_content_sha256=content_hasher.hexdigest(),
|
|
1373
|
+
)
|
|
1374
|
+
return result
|
|
1375
|
+
|
|
1376
|
+
result = await self._stream_supervisor.run(
|
|
1377
|
+
streamed_operation,
|
|
1378
|
+
call_id=request.call_id.value,
|
|
1379
|
+
provider_attempt_id=(
|
|
1380
|
+
None
|
|
1381
|
+
if request.provider_attempt_id is None
|
|
1382
|
+
else request.provider_attempt_id.value
|
|
1383
|
+
),
|
|
1384
|
+
policy=self._stream_liveness_policy,
|
|
1385
|
+
progress_sink=self._stream_progress_sink,
|
|
1386
|
+
)
|
|
1387
|
+
except Exception as exc:
|
|
1388
|
+
sanitized_failure = classify_generation_exception(
|
|
1389
|
+
exc,
|
|
1390
|
+
output_type=request.output_type,
|
|
1391
|
+
semantic_progress_observed=semantic_progress_observed,
|
|
1392
|
+
)
|
|
1393
|
+
# ``raise ... from None`` suppresses display of an active exception,
|
|
1394
|
+
# but Python still retains it in ``__context__`` together with its
|
|
1395
|
+
# traceback-frame locals. A malformed decoded stream item can be
|
|
1396
|
+
# arbitrary provider content, so detach the admitted sanitized
|
|
1397
|
+
# failure while still inside the classification boundary and raise
|
|
1398
|
+
# it only after the raw exception scope has ended. Clearing the
|
|
1399
|
+
# returned failure as well covers an already-sanitized exception
|
|
1400
|
+
# that ``classify_generation_exception`` returned unchanged.
|
|
1401
|
+
sanitized_failure.__traceback__ = None
|
|
1402
|
+
sanitized_failure.__cause__ = None
|
|
1403
|
+
sanitized_failure.__context__ = None
|
|
1404
|
+
if sanitized_failure is not None:
|
|
1405
|
+
raise sanitized_failure from None
|
|
1406
|
+
latency_ns = time.perf_counter_ns() - started
|
|
1407
|
+
|
|
1408
|
+
response = result.response
|
|
1409
|
+
if not isinstance(
|
|
1410
|
+
response, ModelResponse
|
|
1411
|
+
): # pragma: no cover - framework guard.
|
|
1412
|
+
raise StructuredGenerationError(
|
|
1413
|
+
kind=GenerationFailureKind.UNKNOWN,
|
|
1414
|
+
retryable=False,
|
|
1415
|
+
safe_message="Pydantic-AI returned no terminal model response",
|
|
1416
|
+
)
|
|
1417
|
+
# ``usage`` became a property late in Pydantic-AI v1. Admit the
|
|
1418
|
+
# maintained API while retaining compatibility with older injected
|
|
1419
|
+
# test doubles that exposed the historical method.
|
|
1420
|
+
usage = result.usage
|
|
1421
|
+
if not hasattr(usage, "input_tokens") and callable(usage):
|
|
1422
|
+
usage = usage()
|
|
1423
|
+
details = usage.details if isinstance(usage.details, Mapping) else {}
|
|
1424
|
+
provider_details = (
|
|
1425
|
+
response.provider_details
|
|
1426
|
+
if isinstance(response.provider_details, Mapping)
|
|
1427
|
+
else {}
|
|
1428
|
+
)
|
|
1429
|
+
resolved_provider = provider_details.get("downstream_provider")
|
|
1430
|
+
if type(resolved_provider) is not str or not resolved_provider.strip():
|
|
1431
|
+
resolved_provider = response.provider_name or "unknown"
|
|
1432
|
+
resolved_model = response.model_name or self.requested_model
|
|
1433
|
+
return StructuredGenerationResponse(
|
|
1434
|
+
value=result.output,
|
|
1435
|
+
requested_model=self.requested_model,
|
|
1436
|
+
resolved_model=resolved_model,
|
|
1437
|
+
resolved_provider=resolved_provider,
|
|
1438
|
+
provider_response_id=response.provider_response_id,
|
|
1439
|
+
finish_reason=response.finish_reason,
|
|
1440
|
+
input_tokens=usage.input_tokens,
|
|
1441
|
+
output_tokens=usage.output_tokens,
|
|
1442
|
+
reasoning_tokens=_usage_detail(
|
|
1443
|
+
details,
|
|
1444
|
+
"reasoning_tokens",
|
|
1445
|
+
"reasoning",
|
|
1446
|
+
"completion_tokens_details.reasoning_tokens",
|
|
1447
|
+
),
|
|
1448
|
+
cache_read_tokens=usage.cache_read_tokens,
|
|
1449
|
+
cache_write_tokens=usage.cache_write_tokens,
|
|
1450
|
+
cost_usd=_cost(provider_details.get("cost")),
|
|
1451
|
+
latency_ns=latency_ns,
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
def _stream_progress_projection(
|
|
1456
|
+
event: object,
|
|
1457
|
+
) -> (
|
|
1458
|
+
tuple[
|
|
1459
|
+
StructuredStreamProgressKind,
|
|
1460
|
+
StructuredStreamChannel,
|
|
1461
|
+
tuple[tuple[str, str], ...],
|
|
1462
|
+
]
|
|
1463
|
+
| None
|
|
1464
|
+
):
|
|
1465
|
+
"""Project exact semantic fragments from supported Pydantic stream events.
|
|
1466
|
+
|
|
1467
|
+
This is deliberately not a wire-byte projection: Pydantic-AI does not
|
|
1468
|
+
expose the original SSE framing here. For its closed text, thinking, and
|
|
1469
|
+
string tool-call fields, however, it exposes exact semantic string
|
|
1470
|
+
fragments. Unsupported model-response parts, including dictionary tool
|
|
1471
|
+
argument deltas whose original serialization is unknowable, fail closed.
|
|
1472
|
+
Agent workflow events outside the model-response stream return ``None``.
|
|
1473
|
+
"""
|
|
1474
|
+
|
|
1475
|
+
from pydantic_ai.messages import (
|
|
1476
|
+
FinalResultEvent,
|
|
1477
|
+
PartDeltaEvent,
|
|
1478
|
+
PartEndEvent,
|
|
1479
|
+
PartStartEvent,
|
|
1480
|
+
TextPart,
|
|
1481
|
+
TextPartDelta,
|
|
1482
|
+
ThinkingPart,
|
|
1483
|
+
ThinkingPartDelta,
|
|
1484
|
+
ToolCallPart,
|
|
1485
|
+
ToolCallPartDelta,
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
def exact_text(value: object, *, field: str) -> tuple[str, str] | None:
|
|
1489
|
+
if value is None:
|
|
1490
|
+
return None
|
|
1491
|
+
if type(value) is not str:
|
|
1492
|
+
raise StructuredGenerationError(
|
|
1493
|
+
kind=GenerationFailureKind.UNKNOWN,
|
|
1494
|
+
retryable=False,
|
|
1495
|
+
safe_message=("stream semantic content cannot be projected exactly"),
|
|
1496
|
+
)
|
|
1497
|
+
if not value:
|
|
1498
|
+
return None
|
|
1499
|
+
return field, value
|
|
1500
|
+
|
|
1501
|
+
if type(event) is PartStartEvent:
|
|
1502
|
+
part = event.part
|
|
1503
|
+
if type(part) is TextPart:
|
|
1504
|
+
fragment = exact_text(part.content, field="text")
|
|
1505
|
+
return (
|
|
1506
|
+
StructuredStreamProgressKind.PART_STARTED,
|
|
1507
|
+
StructuredStreamChannel.TEXT,
|
|
1508
|
+
() if fragment is None else (fragment,),
|
|
1509
|
+
)
|
|
1510
|
+
if type(part) is ThinkingPart:
|
|
1511
|
+
fragment = exact_text(part.content, field="thinking")
|
|
1512
|
+
return (
|
|
1513
|
+
StructuredStreamProgressKind.PART_STARTED,
|
|
1514
|
+
StructuredStreamChannel.THINKING,
|
|
1515
|
+
() if fragment is None else (fragment,),
|
|
1516
|
+
)
|
|
1517
|
+
if type(part) is ToolCallPart:
|
|
1518
|
+
tool_name = exact_text(part.tool_name, field="tool_name")
|
|
1519
|
+
tool_args = exact_text(part.args, field="tool_args")
|
|
1520
|
+
return (
|
|
1521
|
+
StructuredStreamProgressKind.PART_STARTED,
|
|
1522
|
+
StructuredStreamChannel.TOOL_CALL,
|
|
1523
|
+
tuple(
|
|
1524
|
+
fragment
|
|
1525
|
+
for fragment in (tool_name, tool_args)
|
|
1526
|
+
if fragment is not None
|
|
1527
|
+
),
|
|
1528
|
+
)
|
|
1529
|
+
raise StructuredGenerationError(
|
|
1530
|
+
kind=GenerationFailureKind.UNKNOWN,
|
|
1531
|
+
retryable=False,
|
|
1532
|
+
safe_message="unsupported streamed model-response part",
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
if type(event) is PartDeltaEvent:
|
|
1536
|
+
delta = event.delta
|
|
1537
|
+
if type(delta) is TextPartDelta:
|
|
1538
|
+
fragment = exact_text(delta.content_delta, field="text")
|
|
1539
|
+
return (
|
|
1540
|
+
StructuredStreamProgressKind.PART_DELTA,
|
|
1541
|
+
StructuredStreamChannel.TEXT,
|
|
1542
|
+
() if fragment is None else (fragment,),
|
|
1543
|
+
)
|
|
1544
|
+
if type(delta) is ThinkingPartDelta:
|
|
1545
|
+
fragment = exact_text(delta.content_delta, field="thinking")
|
|
1546
|
+
return (
|
|
1547
|
+
StructuredStreamProgressKind.PART_DELTA,
|
|
1548
|
+
StructuredStreamChannel.THINKING,
|
|
1549
|
+
() if fragment is None else (fragment,),
|
|
1550
|
+
)
|
|
1551
|
+
if type(delta) is ToolCallPartDelta:
|
|
1552
|
+
tool_name = exact_text(delta.tool_name_delta, field="tool_name")
|
|
1553
|
+
tool_args = exact_text(delta.args_delta, field="tool_args")
|
|
1554
|
+
return (
|
|
1555
|
+
StructuredStreamProgressKind.PART_DELTA,
|
|
1556
|
+
StructuredStreamChannel.TOOL_CALL,
|
|
1557
|
+
tuple(
|
|
1558
|
+
fragment
|
|
1559
|
+
for fragment in (tool_name, tool_args)
|
|
1560
|
+
if fragment is not None
|
|
1561
|
+
),
|
|
1562
|
+
)
|
|
1563
|
+
raise StructuredGenerationError(
|
|
1564
|
+
kind=GenerationFailureKind.UNKNOWN,
|
|
1565
|
+
retryable=False,
|
|
1566
|
+
safe_message="unsupported streamed model-response delta",
|
|
1567
|
+
)
|
|
1568
|
+
|
|
1569
|
+
if type(event) is PartEndEvent:
|
|
1570
|
+
part = event.part
|
|
1571
|
+
if type(part) is TextPart:
|
|
1572
|
+
channel = StructuredStreamChannel.TEXT
|
|
1573
|
+
elif type(part) is ThinkingPart:
|
|
1574
|
+
channel = StructuredStreamChannel.THINKING
|
|
1575
|
+
elif type(part) is ToolCallPart:
|
|
1576
|
+
channel = StructuredStreamChannel.TOOL_CALL
|
|
1577
|
+
else:
|
|
1578
|
+
raise StructuredGenerationError(
|
|
1579
|
+
kind=GenerationFailureKind.UNKNOWN,
|
|
1580
|
+
retryable=False,
|
|
1581
|
+
safe_message="unsupported completed model-response part",
|
|
1582
|
+
)
|
|
1583
|
+
return StructuredStreamProgressKind.PART_ENDED, channel, ()
|
|
1584
|
+
|
|
1585
|
+
if type(event) is FinalResultEvent:
|
|
1586
|
+
return (
|
|
1587
|
+
# Despite its framework name, this event only announces which
|
|
1588
|
+
# output/tool Pydantic-AI selected. Its arguments can still stream.
|
|
1589
|
+
StructuredStreamProgressKind.OUTPUT_SELECTED,
|
|
1590
|
+
StructuredStreamChannel.OTHER,
|
|
1591
|
+
(),
|
|
1592
|
+
)
|
|
1593
|
+
return None
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
__all__ = [
|
|
1597
|
+
"OpenRouterReasoningConfig",
|
|
1598
|
+
"OpenRouterReasoningEffort",
|
|
1599
|
+
"OpenRouterStructuredOutputMode",
|
|
1600
|
+
"PydanticAIStructuredGenerator",
|
|
1601
|
+
"STREAM_CONTENT_IDENTITY_ALGORITHM",
|
|
1602
|
+
"STREAM_CONTENT_IDENTITY_DOMAIN_SHA256",
|
|
1603
|
+
"classify_generation_exception",
|
|
1604
|
+
]
|