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.
Files changed (414) hide show
  1. agent_evolve/__init__.py +722 -0
  2. agent_evolve/agentic.py +2800 -0
  3. agent_evolve/api.py +767 -0
  4. agent_evolve/application/__init__.py +1580 -0
  5. agent_evolve/application/action_allocation.py +744 -0
  6. agent_evolve/application/action_allocation_frame.py +347 -0
  7. agent_evolve/application/action_allocation_frame_commit.py +185 -0
  8. agent_evolve/application/action_allocation_frame_commit_v3.py +184 -0
  9. agent_evolve/application/action_allocation_frame_v3.py +338 -0
  10. agent_evolve/application/action_archive_value.py +497 -0
  11. agent_evolve/application/action_evidence_consistency.py +455 -0
  12. agent_evolve/application/action_forecast_partitioning.py +1471 -0
  13. agent_evolve/application/action_metric_projection.py +211 -0
  14. agent_evolve/application/action_role_value.py +680 -0
  15. agent_evolve/application/action_score_authorities.py +363 -0
  16. agent_evolve/application/action_structural_signature.py +116 -0
  17. agent_evolve/application/action_target_realization.py +402 -0
  18. agent_evolve/application/agentic_evolution.py +7734 -0
  19. agent_evolve/application/agentic_portfolio_residual_expert.py +835 -0
  20. agent_evolve/application/anchor_residual_identification.py +463 -0
  21. agent_evolve/application/archive_conditioned_action_target.py +208 -0
  22. agent_evolve/application/artifact_journal.py +246 -0
  23. agent_evolve/application/artifact_replay.py +347 -0
  24. agent_evolve/application/budgeted_optimizer.py +1828 -0
  25. agent_evolve/application/calibrated_campaign.py +485 -0
  26. agent_evolve/application/calibrated_current_prefix_forecast_opportunity.py +322 -0
  27. agent_evolve/application/calibrated_positive_gain_opportunity.py +1581 -0
  28. agent_evolve/application/campaign_capacity_recourse.py +254 -0
  29. agent_evolve/application/campaign_contextual_outcomes.py +119 -0
  30. agent_evolve/application/campaign_diagnostic_blocks.py +930 -0
  31. agent_evolve/application/campaign_evidence_registry.py +262 -0
  32. agent_evolve/application/campaign_execution.py +2537 -0
  33. agent_evolve/application/campaign_generation_audit.py +942 -0
  34. agent_evolve/application/campaign_learning.py +1812 -0
  35. agent_evolve/application/campaign_learning_runtime.py +1977 -0
  36. agent_evolve/application/campaign_search_phase.py +227 -0
  37. agent_evolve/application/campaign_selector_context_extension.py +220 -0
  38. agent_evolve/application/campaign_variation_envelope.py +649 -0
  39. agent_evolve/application/campaign_variation_trace.py +451 -0
  40. agent_evolve/application/candidate_archive_consequence.py +128 -0
  41. agent_evolve/application/causal_opportunity_portfolio_gate.py +385 -0
  42. agent_evolve/application/composite_outcome_updater.py +145 -0
  43. agent_evolve/application/composition_portfolio_selection.py +363 -0
  44. agent_evolve/application/concurrent_stage.py +144 -0
  45. agent_evolve/application/contextual_action_allocation.py +181 -0
  46. agent_evolve/application/contextual_campaign_outcomes.py +267 -0
  47. agent_evolve/application/contextual_campaign_planning.py +1366 -0
  48. agent_evolve/application/contextual_delayed_credit.py +651 -0
  49. agent_evolve/application/contextual_search_controller.py +2374 -0
  50. agent_evolve/application/current_prefix_forecast_opportunity.py +714 -0
  51. agent_evolve/application/decision_metric_projection.py +112 -0
  52. agent_evolve/application/derived_action_semantics.py +129 -0
  53. agent_evolve/application/detailed_evaluation.py +449 -0
  54. agent_evolve/application/earned_lineage.py +1011 -0
  55. agent_evolve/application/effective_choice_audit.py +484 -0
  56. agent_evolve/application/empirical_consequence_calibration.py +908 -0
  57. agent_evolve/application/evaluation_accounting.py +325 -0
  58. agent_evolve/application/evaluation_cache.py +199 -0
  59. agent_evolve/application/evaluation_escrow.py +547 -0
  60. agent_evolve/application/evaluation_recourse.py +253 -0
  61. agent_evolve/application/event_recorder.py +151 -0
  62. agent_evolve/application/evolution_campaign.py +1840 -0
  63. agent_evolve/application/executable_hypothesis.py +323 -0
  64. agent_evolve/application/factorial_branch_pilot.py +772 -0
  65. agent_evolve/application/finite_acquisition_capacity_recourse.py +672 -0
  66. agent_evolve/application/finite_acquisition_residual_expert.py +373 -0
  67. agent_evolve/application/finite_acquisition_variation_envelope.py +802 -0
  68. agent_evolve/application/finite_action_hypothesis_semantics.py +446 -0
  69. agent_evolve/application/finite_action_selection.py +188 -0
  70. agent_evolve/application/finite_action_set.py +306 -0
  71. agent_evolve/application/finite_action_transition.py +537 -0
  72. agent_evolve/application/finite_variation_eligibility.py +296 -0
  73. agent_evolve/application/forecast_geometry_portfolio.py +799 -0
  74. agent_evolve/application/forecast_opportunity_shadow_calibration.py +316 -0
  75. agent_evolve/application/front_proximity_admission.py +311 -0
  76. agent_evolve/application/front_proximity_parent_basis.py +458 -0
  77. agent_evolve/application/frozen_hurdle_score.py +659 -0
  78. agent_evolve/application/g3_causal_screen.py +2257 -0
  79. agent_evolve/application/g3_causal_validation.py +1046 -0
  80. agent_evolve/application/g3_postseal_curation.py +818 -0
  81. agent_evolve/application/gated_agentic_generator.py +205 -0
  82. agent_evolve/application/generation_feedback.py +293 -0
  83. agent_evolve/application/generative_proposal_journal.py +185 -0
  84. agent_evolve/application/geometry_conditional_elasticity.py +453 -0
  85. agent_evolve/application/global_wave_action_allocation.py +1151 -0
  86. agent_evolve/application/head_mass_conditional_seat.py +268 -0
  87. agent_evolve/application/identifiable_reflection_evidence.py +1147 -0
  88. agent_evolve/application/identifiable_reflection_learning.py +395 -0
  89. agent_evolve/application/identifiable_reflection_request.py +364 -0
  90. agent_evolve/application/in_memory_residual_archive.py +341 -0
  91. agent_evolve/application/insight_memory.py +1804 -0
  92. agent_evolve/application/live_runtime_manifest.py +758 -0
  93. agent_evolve/application/llm_task_queue.py +769 -0
  94. agent_evolve/application/matched_finite_action_block.py +409 -0
  95. agent_evolve/application/materialized_action_broker.py +2328 -0
  96. agent_evolve/application/materialized_action_constraints.py +83 -0
  97. agent_evolve/application/materialized_variation.py +211 -0
  98. agent_evolve/application/multi_option_evolution.py +1536 -0
  99. agent_evolve/application/outcome_adaptive_action_racing.py +2827 -0
  100. agent_evolve/application/outcome_adaptive_residual_campaign_runtime.py +580 -0
  101. agent_evolve/application/outcome_adaptive_residual_portfolio_evolution.py +3671 -0
  102. agent_evolve/application/outcome_conditioned_portfolio_selection.py +1374 -0
  103. agent_evolve/application/outcome_relation.py +193 -0
  104. agent_evolve/application/paired_allocation_comparison.py +241 -0
  105. agent_evolve/application/paired_block_schedule.py +127 -0
  106. agent_evolve/application/parent_measurement.py +226 -0
  107. agent_evolve/application/pareto_archive.py +811 -0
  108. agent_evolve/application/portfolio_campaign_runtime.py +4739 -0
  109. agent_evolve/application/portfolio_evolution.py +2950 -0
  110. agent_evolve/application/portfolio_hypothesis_observations.py +814 -0
  111. agent_evolve/application/portfolio_memory_attribution.py +581 -0
  112. agent_evolve/application/portfolio_memory_dose.py +788 -0
  113. agent_evolve/application/portfolio_memory_matched_control.py +938 -0
  114. agent_evolve/application/portfolio_memory_transfer.py +297 -0
  115. agent_evolve/application/portfolio_optimization_memory.py +363 -0
  116. agent_evolve/application/portfolio_outcome_feedback.py +1613 -0
  117. agent_evolve/application/portfolio_projection.py +335 -0
  118. agent_evolve/application/portfolio_recombination.py +2032 -0
  119. agent_evolve/application/post_evolution_reflection.py +834 -0
  120. agent_evolve/application/postcommit_rank_authority.py +245 -0
  121. agent_evolve/application/precommitted_portfolio_racing.py +2762 -0
  122. agent_evolve/application/prequential_archive_opportunity_calibration.py +1154 -0
  123. agent_evolve/application/prequential_residual_exploration.py +343 -0
  124. agent_evolve/application/prequential_score_portfolio.py +954 -0
  125. agent_evolve/application/projections.py +292 -0
  126. agent_evolve/application/protected_action_committee.py +1027 -0
  127. agent_evolve/application/protected_branch_pilot.py +376 -0
  128. agent_evolve/application/protected_current_prefix_forecast_opportunity.py +552 -0
  129. agent_evolve/application/provider_replay.py +910 -0
  130. agent_evolve/application/rank_balanced_causal_pilot.py +1372 -0
  131. agent_evolve/application/recombination_residual_expert.py +403 -0
  132. agent_evolve/application/reflection_workflow.py +571 -0
  133. agent_evolve/application/region_conditional_credit.py +911 -0
  134. agent_evolve/application/residual_campaign_runtime.py +531 -0
  135. agent_evolve/application/residual_headroom_campaign_runtime.py +459 -0
  136. agent_evolve/application/residual_headroom_ledger.py +1544 -0
  137. agent_evolve/application/residual_learning_transaction.py +396 -0
  138. agent_evolve/application/residual_portfolio_evolution.py +1228 -0
  139. agent_evolve/application/residual_reachability.py +749 -0
  140. agent_evolve/application/residual_stage_credit.py +499 -0
  141. agent_evolve/application/same_prefix_paired_audit.py +1580 -0
  142. agent_evolve/application/semantic_coverage_score_portfolio.py +838 -0
  143. agent_evolve/application/sequential_lineage_allocation.py +1017 -0
  144. agent_evolve/application/sequential_market_replay.py +1395 -0
  145. agent_evolve/application/sequential_residual_campaign_runtime.py +305 -0
  146. agent_evolve/application/sequential_residual_portfolio_evolution.py +940 -0
  147. agent_evolve/application/single_score_action_allocation.py +299 -0
  148. agent_evolve/application/source_exposure_allocation.py +906 -0
  149. agent_evolve/application/staged_memory.py +210 -0
  150. agent_evolve/application/stratified_cold_start_allocation.py +732 -0
  151. agent_evolve/application/support_guarded_hurdle_score.py +549 -0
  152. agent_evolve/application/target_conditioned_action_forecast.py +595 -0
  153. agent_evolve/application/target_conditioned_campaign.py +566 -0
  154. agent_evolve/application/treatment_assignment.py +201 -0
  155. agent_evolve/application/trusted_objective_evidence.py +217 -0
  156. agent_evolve/application/two_stage_action_evolution.py +1131 -0
  157. agent_evolve/application/v8lite_allocation_policy.py +1083 -0
  158. agent_evolve/application/v9_candidate_policy.py +1303 -0
  159. agent_evolve/bootstrap.py +108 -0
  160. agent_evolve/campaign_presets.py +517 -0
  161. agent_evolve/campaign_profiles.py +452 -0
  162. agent_evolve/campaign_variation_topology.py +288 -0
  163. agent_evolve/campaign_workload.py +950 -0
  164. agent_evolve/cli.py +797 -0
  165. agent_evolve/contract.py +241 -0
  166. agent_evolve/core/__init__.py +91 -0
  167. agent_evolve/core/action_semantics.py +411 -0
  168. agent_evolve/core/authored.py +105 -0
  169. agent_evolve/core/formatting.py +286 -0
  170. agent_evolve/core/optimization_semantics.py +324 -0
  171. agent_evolve/core/problem.py +167 -0
  172. agent_evolve/core/results.py +323 -0
  173. agent_evolve/core/stats.py +70 -0
  174. agent_evolve/core/telemetry.py +100 -0
  175. agent_evolve/domain/__init__.py +89 -0
  176. agent_evolve/domain/artifact.py +162 -0
  177. agent_evolve/domain/durable_text.py +68 -0
  178. agent_evolve/domain/event.py +1454 -0
  179. agent_evolve/domain/finite_action_set.py +426 -0
  180. agent_evolve/domain/finite_variation.py +526 -0
  181. agent_evolve/domain/generative_emission.py +559 -0
  182. agent_evolve/domain/ids.py +163 -0
  183. agent_evolve/domain/inline_text.py +106 -0
  184. agent_evolve/domain/insight.py +27 -0
  185. agent_evolve/domain/lineage.py +737 -0
  186. agent_evolve/domain/llm_task_queue.py +960 -0
  187. agent_evolve/domain/outcome.py +96 -0
  188. agent_evolve/domain/patch.py +854 -0
  189. agent_evolve/domain/typed_json.py +542 -0
  190. agent_evolve/domain/variation_space.py +158 -0
  191. agent_evolve/driver.py +1014 -0
  192. agent_evolve/harness/__init__.py +29 -0
  193. agent_evolve/harness/base.py +242 -0
  194. agent_evolve/harness/directives.py +163 -0
  195. agent_evolve/harness/generative_seal.py +479 -0
  196. agent_evolve/harness/registry.py +41 -0
  197. agent_evolve/infrastructure/__init__.py +39 -0
  198. agent_evolve/infrastructure/artifacts/__init__.py +6 -0
  199. agent_evolve/infrastructure/artifacts/_verification.py +67 -0
  200. agent_evolve/infrastructure/artifacts/filesystem.py +343 -0
  201. agent_evolve/infrastructure/artifacts/in_memory.py +73 -0
  202. agent_evolve/infrastructure/asyncio_runtime.py +109 -0
  203. agent_evolve/infrastructure/authored_runtime.py +188 -0
  204. agent_evolve/infrastructure/authored_worker.py +171 -0
  205. agent_evolve/infrastructure/clock.py +53 -0
  206. agent_evolve/infrastructure/events/__init__.py +6 -0
  207. agent_evolve/infrastructure/events/_validation.py +89 -0
  208. agent_evolve/infrastructure/events/in_memory.py +56 -0
  209. agent_evolve/infrastructure/events/jsonl.py +193 -0
  210. agent_evolve/infrastructure/exception_provenance.py +215 -0
  211. agent_evolve/infrastructure/ids.py +118 -0
  212. agent_evolve/infrastructure/lineage_codec.py +1836 -0
  213. agent_evolve/infrastructure/outcome_adaptive_phase_journal.py +170 -0
  214. agent_evolve/infrastructure/residual_headroom_journal.py +221 -0
  215. agent_evolve/infrastructure/resource_lease.py +370 -0
  216. agent_evolve/infrastructure/sanitization/__init__.py +8 -0
  217. agent_evolve/infrastructure/sanitization/strict_json.py +484 -0
  218. agent_evolve/infrastructure/sequential_phase_journal.py +170 -0
  219. agent_evolve/infrastructure/stream_liveness.py +383 -0
  220. agent_evolve/infrastructure/subprocess_boundary.py +136 -0
  221. agent_evolve/integrations/__init__.py +1 -0
  222. agent_evolve/integrations/botorch/__init__.py +28 -0
  223. agent_evolve/integrations/botorch/finite_qlognehvi.py +190 -0
  224. agent_evolve/integrations/botorch/finite_qlognehvi_batch.py +155 -0
  225. agent_evolve/integrations/botorch/finite_qlognehvi_batch_identity.py +20 -0
  226. agent_evolve/integrations/botorch/finite_qlognehvi_batch_worker.py +55 -0
  227. agent_evolve/integrations/botorch/finite_qlognehvi_identity.py +22 -0
  228. agent_evolve/integrations/botorch/finite_qlognehvi_worker.py +55 -0
  229. agent_evolve/integrations/botorch/subprocess_qlognehvi.py +261 -0
  230. agent_evolve/integrations/botorch/subprocess_qlognehvi_batch.py +273 -0
  231. agent_evolve/integrations/completion.py +242 -0
  232. agent_evolve/integrations/pydantic_ai/__init__.py +441 -0
  233. agent_evolve/integrations/pydantic_ai/action_forecast.py +1068 -0
  234. agent_evolve/integrations/pydantic_ai/agentic_generator.py +2308 -0
  235. agent_evolve/integrations/pydantic_ai/async_generator.py +1604 -0
  236. agent_evolve/integrations/pydantic_ai/boundary_codec.py +1526 -0
  237. agent_evolve/integrations/pydantic_ai/calibrated_portfolio_campaign.py +756 -0
  238. agent_evolve/integrations/pydantic_ai/calibrated_portfolio_selection.py +7537 -0
  239. agent_evolve/integrations/pydantic_ai/campaign_acquisition.py +609 -0
  240. agent_evolve/integrations/pydantic_ai/execution_binding.py +138 -0
  241. agent_evolve/integrations/pydantic_ai/forecast_geometry_action_committee.py +217 -0
  242. agent_evolve/integrations/pydantic_ai/harness.py +159 -0
  243. agent_evolve/integrations/pydantic_ai/heterogeneous_model_execution.py +306 -0
  244. agent_evolve/integrations/pydantic_ai/hierarchical_residual_adaptive_semantic_view.py +179 -0
  245. agent_evolve/integrations/pydantic_ai/json_schema_dialect.py +108 -0
  246. agent_evolve/integrations/pydantic_ai/materialized_hierarchical_residual_expert.py +952 -0
  247. agent_evolve/integrations/pydantic_ai/materialized_portfolio_judge.py +520 -0
  248. agent_evolve/integrations/pydantic_ai/model_execution_profile.py +659 -0
  249. agent_evolve/integrations/pydantic_ai/outbound_request_manifest.py +1170 -0
  250. agent_evolve/integrations/pydantic_ai/portable_residual_consequence_features.py +575 -0
  251. agent_evolve/integrations/pydantic_ai/portfolio_selection.py +422 -0
  252. agent_evolve/integrations/pydantic_ai/progress_aware_openrouter.py +416 -0
  253. agent_evolve/integrations/pydantic_ai/provider_attempt_join.py +1523 -0
  254. agent_evolve/integrations/pydantic_ai/provider_free_calibrated_runner.py +607 -0
  255. agent_evolve/integrations/pydantic_ai/queued_runner.py +2634 -0
  256. agent_evolve/integrations/pydantic_ai/reconciled_residual_reachability.py +1417 -0
  257. agent_evolve/integrations/pydantic_ai/residual_forecast_geometry.py +445 -0
  258. agent_evolve/integrations/pydantic_ai/residual_reachability.py +674 -0
  259. agent_evolve/integrations/pydantic_ai/residual_semantic_cells.py +239 -0
  260. agent_evolve/integrations/pydantic_ai/sealed_output_replay.py +1068 -0
  261. agent_evolve/integrations/pydantic_ai/semantic_coverage_residual_portfolio.py +770 -0
  262. agent_evolve/integrations/pydantic_ai/semantic_decision_replay.py +383 -0
  263. agent_evolve/integrations/pydantic_ai/support_adaptive_residual_portfolio.py +135 -0
  264. agent_evolve/integrations/pydantic_ai/trusted_residual_prompt_context.py +143 -0
  265. agent_evolve/integrations/pydantic_ai/validated_openrouter_model.py +107 -0
  266. agent_evolve/integrations/pymoo_adapter.py +242 -0
  267. agent_evolve/policies/__init__.py +17 -0
  268. agent_evolve/policies/check.py +469 -0
  269. agent_evolve/policies/emit_scaffold.py +451 -0
  270. agent_evolve/policies/feedback/__init__.py +37 -0
  271. agent_evolve/policies/feedback/held_out_asn.py +1325 -0
  272. agent_evolve/policies/genetic.py +607 -0
  273. agent_evolve/policies/llm_backoff.py +183 -0
  274. agent_evolve/policies/llm_chooser.py +226 -0
  275. agent_evolve/policies/llm_generator.py +1760 -0
  276. agent_evolve/policies/llm_init.py +267 -0
  277. agent_evolve/policies/llm_operator.py +109 -0
  278. agent_evolve/policies/llm_prior.py +194 -0
  279. agent_evolve/policies/llm_surrogate.py +334 -0
  280. agent_evolve/policies/measurement_evidence.py +704 -0
  281. agent_evolve/policies/memory/__init__.py +223 -0
  282. agent_evolve/policies/memory/balanced_subset_blocks.py +707 -0
  283. agent_evolve/policies/memory/compatibility_matching.py +593 -0
  284. agent_evolve/policies/memory/global_falsification.py +1841 -0
  285. agent_evolve/policies/memory/prompt_shape.py +503 -0
  286. agent_evolve/policies/memory/randomized_subset.py +714 -0
  287. agent_evolve/policies/memory/staged_causal.py +1270 -0
  288. agent_evolve/policies/memory/treatment_compliance.py +759 -0
  289. agent_evolve/policies/objective_resolution/__init__.py +17 -0
  290. agent_evolve/policies/objective_resolution/fixed_grid.py +364 -0
  291. agent_evolve/policies/operator_portfolio.py +407 -0
  292. agent_evolve/policies/reguidance.py +1133 -0
  293. agent_evolve/policies/reward/__init__.py +83 -0
  294. agent_evolve/policies/reward/affine_candidate_consequence.py +156 -0
  295. agent_evolve/policies/reward/affine_candidate_consequence_3d.py +159 -0
  296. agent_evolve/policies/reward/affine_hypervolume.py +490 -0
  297. agent_evolve/policies/reward/affine_hypervolume_3d.py +567 -0
  298. agent_evolve/policies/reward/contextual_marginal_utility.py +318 -0
  299. agent_evolve/policies/reward/frozen_archive.py +360 -0
  300. agent_evolve/policies/reward/frozen_wave_archive.py +368 -0
  301. agent_evolve/policies/search_state.py +208 -0
  302. agent_evolve/policies/selection/__init__.py +345 -0
  303. agent_evolve/policies/selection/acquisition_certified_slate.py +684 -0
  304. agent_evolve/policies/selection/affine_frontier_context.py +330 -0
  305. agent_evolve/policies/selection/affine_frontier_target.py +473 -0
  306. agent_evolve/policies/selection/archive_elite.py +1346 -0
  307. agent_evolve/policies/selection/calibrated_portfolio_binding.py +640 -0
  308. agent_evolve/policies/selection/calibrated_slate.py +1394 -0
  309. agent_evolve/policies/selection/calibrated_slate_codec.py +579 -0
  310. agent_evolve/policies/selection/common_candidate_pool.py +685 -0
  311. agent_evolve/policies/selection/diagnostic_sampling.py +319 -0
  312. agent_evolve/policies/selection/disjoint_pairs.py +479 -0
  313. agent_evolve/policies/selection/elite_explorer.py +719 -0
  314. agent_evolve/policies/selection/finite_action.py +187 -0
  315. agent_evolve/policies/selection/finite_option_prompt_projection.py +377 -0
  316. agent_evolve/policies/selection/finite_palette_evidence.py +247 -0
  317. agent_evolve/policies/selection/forecast_calibration.py +922 -0
  318. agent_evolve/policies/selection/frontier_probe_slate.py +814 -0
  319. agent_evolve/policies/selection/frozen_archive_pairs.py +762 -0
  320. agent_evolve/policies/selection/full_support_slate.py +91 -0
  321. agent_evolve/policies/selection/meaningful_direction.py +240 -0
  322. agent_evolve/policies/selection/memory_dose_feasibility.py +259 -0
  323. agent_evolve/policies/selection/model_anchored_slate.py +826 -0
  324. agent_evolve/policies/selection/phenotype_recourse.py +979 -0
  325. agent_evolve/policies/selection/proposal_support.py +368 -0
  326. agent_evolve/policies/selection/random_portfolio.py +254 -0
  327. agent_evolve/policies/selection/regret_bounded_slate.py +1084 -0
  328. agent_evolve/policies/selection/residual_frontier.py +463 -0
  329. agent_evolve/policies/selection/residual_frontier_target.py +605 -0
  330. agent_evolve/policies/selection/structural_posterior_slate.py +1571 -0
  331. agent_evolve/policies/selection/target_conditioned_allocator.py +648 -0
  332. agent_evolve/policies/selection/target_conditioned_features.py +812 -0
  333. agent_evolve/policies/selection/target_conditioned_prequential.py +1527 -0
  334. agent_evolve/policies/selection/task_keyed_palette.py +906 -0
  335. agent_evolve/policies/semantics.py +147 -0
  336. agent_evolve/policies/structure.py +362 -0
  337. agent_evolve/policies/structured_output_budget.py +62 -0
  338. agent_evolve/policies/surrogate.py +696 -0
  339. agent_evolve/policies/variation/__init__.py +1 -0
  340. agent_evolve/policies/variation/compositional_finite_catalog.py +426 -0
  341. agent_evolve/policies/variation/crossover_inheritance.py +575 -0
  342. agent_evolve/policies/variation/disjoint_recombination.py +611 -0
  343. agent_evolve/policies/variation/exact_composition_capacity.py +214 -0
  344. agent_evolve/policies/variation/exact_parent_crossover.py +950 -0
  345. agent_evolve/policies/variation/multiscale_restart_catalog.py +372 -0
  346. agent_evolve/policies/variation/source_union_finite_catalog.py +403 -0
  347. agent_evolve/policies/variation/typed_patch.py +1981 -0
  348. agent_evolve/policies/weighted_prior.py +394 -0
  349. agent_evolve/ports/__init__.py +383 -0
  350. agent_evolve/ports/action_allocation.py +733 -0
  351. agent_evolve/ports/action_allocation_frame.py +1153 -0
  352. agent_evolve/ports/action_allocation_frame_commit.py +294 -0
  353. agent_evolve/ports/action_allocation_frame_commit_v3.py +432 -0
  354. agent_evolve/ports/action_allocation_frame_v3.py +995 -0
  355. agent_evolve/ports/action_forecast.py +1568 -0
  356. agent_evolve/ports/action_metric_projection.py +165 -0
  357. agent_evolve/ports/agentic_generator.py +1561 -0
  358. agent_evolve/ports/archive_context.py +136 -0
  359. agent_evolve/ports/artifact_sanitizer.py +44 -0
  360. agent_evolve/ports/artifact_store.py +225 -0
  361. agent_evolve/ports/clock.py +13 -0
  362. agent_evolve/ports/contextual_search_allocation.py +827 -0
  363. agent_evolve/ports/decision_metric_projection.py +258 -0
  364. agent_evolve/ports/event_store.py +55 -0
  365. agent_evolve/ports/executable_hypothesis.py +557 -0
  366. agent_evolve/ports/finite_acquisition.py +377 -0
  367. agent_evolve/ports/finite_acquisition_batch.py +296 -0
  368. agent_evolve/ports/finite_acquisition_batch_json.py +164 -0
  369. agent_evolve/ports/finite_acquisition_json.py +247 -0
  370. agent_evolve/ports/finite_acquisition_space.py +168 -0
  371. agent_evolve/ports/finite_action_selection.py +348 -0
  372. agent_evolve/ports/finite_action_set.py +256 -0
  373. agent_evolve/ports/frontier_target.py +396 -0
  374. agent_evolve/ports/generation_failure.py +43 -0
  375. agent_evolve/ports/hard_feasibility.py +233 -0
  376. agent_evolve/ports/id_factory.py +34 -0
  377. agent_evolve/ports/llm_task_queue.py +93 -0
  378. agent_evolve/ports/objective_resolution.py +419 -0
  379. agent_evolve/ports/paired_allocation_comparison.py +401 -0
  380. agent_evolve/ports/paired_block_schedule.py +475 -0
  381. agent_evolve/ports/parent_measurement.py +336 -0
  382. agent_evolve/ports/portfolio_memory_dose.py +643 -0
  383. agent_evolve/ports/portfolio_selection.py +3169 -0
  384. agent_evolve/ports/postcommit_rank_authority.py +467 -0
  385. agent_evolve/ports/presented_action_evidence.py +794 -0
  386. agent_evolve/ports/resource_lease.py +162 -0
  387. agent_evolve/ports/structured_generator.py +734 -0
  388. agent_evolve/ports/structured_output_budget.py +120 -0
  389. agent_evolve/ports/subprocess_boundary.py +138 -0
  390. agent_evolve/ports/treatment_assignment.py +466 -0
  391. agent_evolve/ports/variation_catalog.py +76 -0
  392. agent_evolve/ports/variation_source.py +226 -0
  393. agent_evolve/proposal_mode.py +157 -0
  394. agent_evolve/proposers/__init__.py +10 -0
  395. agent_evolve/proposers/random_proposer.py +188 -0
  396. agent_evolve/provider_accounting.py +163 -0
  397. agent_evolve/py.typed +0 -0
  398. agent_evolve/reference_method.py +1570 -0
  399. agent_evolve/session/__init__.py +11 -0
  400. agent_evolve/session/authorship.py +864 -0
  401. agent_evolve/session/evaluate.py +236 -0
  402. agent_evolve/session/fidelity.py +237 -0
  403. agent_evolve/session/genetic_loop.py +742 -0
  404. agent_evolve/session/loop.py +803 -0
  405. agent_evolve/session/screening.py +671 -0
  406. agent_evolve/settings.py +376 -0
  407. agent_evolve/workload_kit.py +368 -0
  408. agent_evolve/workload_prompt.py +398 -0
  409. agentevolve_optimizer-0.5.0.dist-info/METADATA +599 -0
  410. agentevolve_optimizer-0.5.0.dist-info/RECORD +414 -0
  411. agentevolve_optimizer-0.5.0.dist-info/WHEEL +5 -0
  412. agentevolve_optimizer-0.5.0.dist-info/entry_points.txt +2 -0
  413. agentevolve_optimizer-0.5.0.dist-info/licenses/LICENSE +21 -0
  414. 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
+ ]