selectools 1.0.1__tar.gz → 1.2.0__tar.gz

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 (324) hide show
  1. {selectools-1.0.1/src/selectools.egg-info → selectools-1.2.0}/PKG-INFO +1 -1
  2. {selectools-1.0.1 → selectools-1.2.0}/pyproject.toml +1 -1
  3. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/__init__.py +2 -1
  4. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/agent/_provider_caller.py +77 -27
  5. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/agent/_tool_executor.py +44 -6
  6. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/agent/config.py +8 -0
  7. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/agent/config_groups.py +61 -2
  8. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/agent/core.py +734 -123
  9. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/audit.py +19 -0
  10. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/cache.py +4 -0
  11. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/embeddings/anthropic.py +0 -10
  12. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/embeddings/cohere.py +0 -10
  13. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/embeddings/gemini.py +0 -10
  14. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/embeddings/openai.py +0 -10
  15. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/embeddings/provider.py +6 -2
  16. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/guardrails/base.py +32 -6
  17. selectools-1.2.0/src/selectools/guardrails/pipeline.py +260 -0
  18. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/observer.py +33 -0
  19. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/_openai_compat.py +45 -8
  20. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/anthropic_provider.py +131 -108
  21. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/gemini_provider.py +103 -127
  22. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/openai_provider.py +2 -0
  23. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/_starlette_app.py +7 -1
  24. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/_static/builder.js +1 -1
  25. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/app.py +41 -5
  26. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/sessions.py +37 -104
  27. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/structured.py +19 -1
  28. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/types.py +13 -0
  29. {selectools-1.0.1 → selectools-1.2.0/src/selectools.egg-info}/PKG-INFO +1 -1
  30. {selectools-1.0.1 → selectools-1.2.0}/tests/test_bug_hunt_batch1_core.py +0 -3
  31. {selectools-1.0.1 → selectools-1.2.0}/tests/test_bug_hunt_batch1_security.py +0 -3
  32. {selectools-1.0.1 → selectools-1.2.0}/tests/test_bug_hunt_batch1_tools.py +1 -5
  33. {selectools-1.0.1 → selectools-1.2.0}/tests/test_bug_hunt_regression.py +1 -10
  34. {selectools-1.0.1 → selectools-1.2.0}/tests/test_checkpoint_postgres.py +3 -4
  35. {selectools-1.0.1 → selectools-1.2.0}/tests/test_coherence.py +1 -2
  36. {selectools-1.0.1 → selectools-1.2.0}/tests/test_concurrency_smoke.py +1 -4
  37. {selectools-1.0.1 → selectools-1.2.0}/tests/test_consolidation_regression.py +2 -2
  38. {selectools-1.0.1 → selectools-1.2.0}/tests/test_coverage_orchestration.py +7 -14
  39. {selectools-1.0.1 → selectools-1.2.0}/tests/test_document_loaders_extended.py +0 -3
  40. {selectools-1.0.1 → selectools-1.2.0}/tests/test_e2e_multimodal.py +0 -1
  41. {selectools-1.0.1 → selectools-1.2.0}/tests/test_entity_memory.py +1 -3
  42. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals.py +0 -4
  43. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_advanced.py +2 -4
  44. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_cli.py +5 -7
  45. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_e2e.py +3 -11
  46. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_final.py +1 -1
  47. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_hardening.py +0 -8
  48. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_html_report.py +0 -6
  49. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_new_evaluators.py +2 -4
  50. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_ralph_bugs.py +2 -7
  51. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_release.py +1 -4
  52. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_serve.py +0 -5
  53. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_v017_features.py +1 -2
  54. {selectools-1.0.1 → selectools-1.2.0}/tests/test_evals_v0191.py +1 -2
  55. {selectools-1.0.1 → selectools-1.2.0}/tests/test_features_in_graph.py +7 -12
  56. {selectools-1.0.1 → selectools-1.2.0}/tests/test_final_coverage_a.py +1 -3
  57. {selectools-1.0.1 → selectools-1.2.0}/tests/test_final_coverage_b.py +10 -14
  58. {selectools-1.0.1 → selectools-1.2.0}/tests/test_guardrails.py +159 -2
  59. {selectools-1.0.1 → selectools-1.2.0}/tests/test_hardening.py +1 -7
  60. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge.py +1 -4
  61. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge_backend.py +0 -1
  62. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge_backend_redis.py +1 -1
  63. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge_backend_supabase.py +1 -1
  64. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge_graph.py +1 -3
  65. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge_stores.py +0 -1
  66. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge_stores_error_handling.py +0 -2
  67. {selectools-1.0.1 → selectools-1.2.0}/tests/test_mcp.py +2 -5
  68. {selectools-1.0.1 → selectools-1.2.0}/tests/test_mcp_coverage.py +1 -2
  69. {selectools-1.0.1 → selectools-1.2.0}/tests/test_memory_boundary.py +0 -2
  70. {selectools-1.0.1 → selectools-1.2.0}/tests/test_memory_integration.py +0 -4
  71. {selectools-1.0.1 → selectools-1.2.0}/tests/test_multi_agent_edge_cases.py +2 -3
  72. {selectools-1.0.1 → selectools-1.2.0}/tests/test_multimodal.py +0 -4
  73. {selectools-1.0.1 → selectools-1.2.0}/tests/test_orchestration_checkpoint.py +4 -5
  74. {selectools-1.0.1 → selectools-1.2.0}/tests/test_orchestration_evals.py +0 -1
  75. {selectools-1.0.1 → selectools-1.2.0}/tests/test_orchestration_graph.py +3 -9
  76. {selectools-1.0.1 → selectools-1.2.0}/tests/test_orchestration_integration.py +0 -9
  77. {selectools-1.0.1 → selectools-1.2.0}/tests/test_orchestration_supervisor.py +5 -6
  78. {selectools-1.0.1 → selectools-1.2.0}/tests/test_patterns.py +5 -11
  79. {selectools-1.0.1 → selectools-1.2.0}/tests/test_phase1_design_patterns.py +0 -1
  80. {selectools-1.0.1 → selectools-1.2.0}/tests/test_pipeline.py +0 -2
  81. {selectools-1.0.1 → selectools-1.2.0}/tests/test_pipeline_coverage.py +1 -3
  82. {selectools-1.0.1 → selectools-1.2.0}/tests/test_prompt_compression.py +0 -2
  83. {selectools-1.0.1 → selectools-1.2.0}/tests/test_property_based.py +0 -1
  84. {selectools-1.0.1 → selectools-1.2.0}/tests/test_remaining_coverage.py +4 -9
  85. {selectools-1.0.1 → selectools-1.2.0}/tests/test_results.py +1 -1
  86. {selectools-1.0.1 → selectools-1.2.0}/tests/test_security.py +1 -3
  87. {selectools-1.0.1 → selectools-1.2.0}/tests/test_semantic_cache.py +0 -1
  88. {selectools-1.0.1 → selectools-1.2.0}/tests/test_serve_app_coverage.py +17 -11
  89. {selectools-1.0.1 → selectools-1.2.0}/tests/test_serve_cli.py +0 -1
  90. {selectools-1.0.1 → selectools-1.2.0}/tests/test_sessions_dynamodb.py +1 -1
  91. {selectools-1.0.1 → selectools-1.2.0}/tests/test_sessions_edge_cases.py +1 -3
  92. {selectools-1.0.1 → selectools-1.2.0}/tests/test_sessions_redis.py +1 -1
  93. {selectools-1.0.1 → selectools-1.2.0}/tests/test_sessions_supabase.py +4 -5
  94. {selectools-1.0.1 → selectools-1.2.0}/tests/test_starlette_app.py +0 -2
  95. {selectools-1.0.1 → selectools-1.2.0}/tests/test_structured_config.py +0 -10
  96. {selectools-1.0.1 → selectools-1.2.0}/tests/test_structured_tool_results.py +1 -1
  97. {selectools-1.0.1 → selectools-1.2.0}/tests/test_summarize_on_trim.py +2 -4
  98. {selectools-1.0.1 → selectools-1.2.0}/tests/test_terminal_actions.py +1 -2
  99. {selectools-1.0.1 → selectools-1.2.0}/tests/test_token_estimation.py +0 -2
  100. {selectools-1.0.1 → selectools-1.2.0}/tests/test_trace.py +0 -3
  101. {selectools-1.0.1 → selectools-1.2.0}/tests/test_trace_html.py +0 -4
  102. {selectools-1.0.1 → selectools-1.2.0}/tests/test_trace_store.py +1 -5
  103. {selectools-1.0.1 → selectools-1.2.0}/tests/test_v016_regression.py +0 -2
  104. {selectools-1.0.1 → selectools-1.2.0}/tests/test_v019_features.py +2 -3
  105. {selectools-1.0.1 → selectools-1.2.0}/tests/test_visual_builder.py +51 -26
  106. selectools-1.0.1/src/selectools/guardrails/pipeline.py +0 -124
  107. {selectools-1.0.1 → selectools-1.2.0}/LICENSE +0 -0
  108. {selectools-1.0.1 → selectools-1.2.0}/README.md +0 -0
  109. {selectools-1.0.1 → selectools-1.2.0}/setup.cfg +0 -0
  110. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/_async_utils.py +0 -0
  111. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/_json_utils.py +0 -0
  112. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/_ssrf.py +0 -0
  113. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/_time.py +0 -0
  114. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/a2a/__init__.py +0 -0
  115. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/a2a/client.py +0 -0
  116. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/a2a/server.py +0 -0
  117. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/a2a/types.py +0 -0
  118. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/agent/__init__.py +0 -0
  119. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/agent/_lifecycle.py +0 -0
  120. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/agent/_memory_manager.py +0 -0
  121. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/agent/_planning.py +0 -0
  122. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/analytics.py +0 -0
  123. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/cache_redis.py +0 -0
  124. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/cache_semantic.py +0 -0
  125. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/cancellation.py +0 -0
  126. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/checkpoint_postgres.py +0 -0
  127. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/coherence.py +0 -0
  128. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/compose.py +0 -0
  129. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/embeddings/__init__.py +0 -0
  130. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/entity_memory.py +0 -0
  131. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/env.py +0 -0
  132. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/__init__.py +0 -0
  133. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/__main__.py +0 -0
  134. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/badge.py +0 -0
  135. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/dataset.py +0 -0
  136. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/evaluators.py +0 -0
  137. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/generator.py +0 -0
  138. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/history.py +0 -0
  139. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/html.py +0 -0
  140. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/junit.py +0 -0
  141. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/llm_evaluators.py +0 -0
  142. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/pairwise.py +0 -0
  143. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/regression.py +0 -0
  144. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/report.py +0 -0
  145. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/serve.py +0 -0
  146. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/snapshot.py +0 -0
  147. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/suite.py +0 -0
  148. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/templates.py +0 -0
  149. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/evals/types.py +0 -0
  150. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/exceptions.py +0 -0
  151. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/guardrails/__init__.py +0 -0
  152. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/guardrails/format.py +0 -0
  153. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/guardrails/injection.py +0 -0
  154. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/guardrails/length.py +0 -0
  155. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/guardrails/pii.py +0 -0
  156. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/guardrails/topic.py +0 -0
  157. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/guardrails/toxicity.py +0 -0
  158. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/knowledge.py +0 -0
  159. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/knowledge_backends.py +0 -0
  160. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/knowledge_graph.py +0 -0
  161. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/knowledge_sanitizers.py +0 -0
  162. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/knowledge_store_redis.py +0 -0
  163. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/knowledge_store_supabase.py +0 -0
  164. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/loop_detection.py +0 -0
  165. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/mcp/__init__.py +0 -0
  166. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/mcp/_loop.py +0 -0
  167. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/mcp/bridge.py +0 -0
  168. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/mcp/client.py +0 -0
  169. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/mcp/config.py +0 -0
  170. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/mcp/multi.py +0 -0
  171. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/mcp/server.py +0 -0
  172. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/memory.py +0 -0
  173. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/models.py +0 -0
  174. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/observe/__init__.py +0 -0
  175. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/observe/langfuse.py +0 -0
  176. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/observe/otel.py +0 -0
  177. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/observe/trace_store.py +0 -0
  178. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/orchestration/__init__.py +0 -0
  179. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/orchestration/checkpoint.py +0 -0
  180. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/orchestration/graph.py +0 -0
  181. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/orchestration/node.py +0 -0
  182. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/orchestration/state.py +0 -0
  183. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/orchestration/supervisor.py +0 -0
  184. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/parser.py +0 -0
  185. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/patterns/__init__.py +0 -0
  186. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/patterns/debate.py +0 -0
  187. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/patterns/plan_and_execute.py +0 -0
  188. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/patterns/reflective.py +0 -0
  189. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/patterns/team_lead.py +0 -0
  190. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/pending.py +0 -0
  191. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/pipeline.py +0 -0
  192. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/policy.py +0 -0
  193. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/pricing.py +0 -0
  194. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/prompt.py +0 -0
  195. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/__init__.py +0 -0
  196. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/azure_openai_provider.py +0 -0
  197. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/base.py +0 -0
  198. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/fallback.py +0 -0
  199. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/litellm_provider.py +0 -0
  200. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/ollama_provider.py +0 -0
  201. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/router.py +0 -0
  202. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/providers/stubs.py +0 -0
  203. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/__init__.py +0 -0
  204. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/bm25.py +0 -0
  205. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/chunking.py +0 -0
  206. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/hybrid.py +0 -0
  207. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/loaders.py +0 -0
  208. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/reranker.py +0 -0
  209. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/stores/__init__.py +0 -0
  210. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/stores/chroma.py +0 -0
  211. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/stores/faiss.py +0 -0
  212. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/stores/memory.py +0 -0
  213. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/stores/pgvector.py +0 -0
  214. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/stores/pinecone.py +0 -0
  215. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/stores/qdrant.py +0 -0
  216. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/stores/sqlite.py +0 -0
  217. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/tools.py +0 -0
  218. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/rag/vector_store.py +0 -0
  219. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/results.py +0 -0
  220. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/scheduler.py +0 -0
  221. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/security.py +0 -0
  222. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/__init__.py +0 -0
  223. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/_static/builder.css +0 -0
  224. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/_static/builder.html +0 -0
  225. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/api.py +0 -0
  226. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/builder.py +0 -0
  227. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/cli.py +0 -0
  228. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/models.py +0 -0
  229. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/serve/playground.py +0 -0
  230. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/stability.py +0 -0
  231. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/templates/__init__.py +0 -0
  232. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/templates/code_reviewer.py +0 -0
  233. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/templates/customer_support.py +0 -0
  234. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/templates/data_analyst.py +0 -0
  235. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/templates/rag_chatbot.py +0 -0
  236. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/templates/research_assistant.py +0 -0
  237. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/token_estimation.py +0 -0
  238. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/__init__.py +0 -0
  239. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/browser_tools.py +0 -0
  240. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/calculator_tools.py +0 -0
  241. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/code_tools.py +0 -0
  242. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/data_tools.py +0 -0
  243. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/datetime_tools.py +0 -0
  244. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/db_tools.py +0 -0
  245. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/discord_tools.py +0 -0
  246. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/email_tools.py +0 -0
  247. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/file_tools.py +0 -0
  248. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/github_tools.py +0 -0
  249. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/image_tools.py +0 -0
  250. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/linear_tools.py +0 -0
  251. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/memory_tools.py +0 -0
  252. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/notion_tools.py +0 -0
  253. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/pdf_tools.py +0 -0
  254. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/reasoning_tools.py +0 -0
  255. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/s3_tools.py +0 -0
  256. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/search_tools.py +0 -0
  257. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/slack_tools.py +0 -0
  258. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/text_tools.py +0 -0
  259. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/toolbox/web_tools.py +0 -0
  260. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/tools/__init__.py +0 -0
  261. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/tools/base.py +0 -0
  262. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/tools/decorators.py +0 -0
  263. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/tools/loader.py +0 -0
  264. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/tools/registry.py +0 -0
  265. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/trace.py +0 -0
  266. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/unified_memory.py +0 -0
  267. {selectools-1.0.1 → selectools-1.2.0}/src/selectools/usage.py +0 -0
  268. {selectools-1.0.1 → selectools-1.2.0}/src/selectools.egg-info/SOURCES.txt +0 -0
  269. {selectools-1.0.1 → selectools-1.2.0}/src/selectools.egg-info/dependency_links.txt +0 -0
  270. {selectools-1.0.1 → selectools-1.2.0}/src/selectools.egg-info/entry_points.txt +0 -0
  271. {selectools-1.0.1 → selectools-1.2.0}/src/selectools.egg-info/requires.txt +0 -0
  272. {selectools-1.0.1 → selectools-1.2.0}/src/selectools.egg-info/top_level.txt +0 -0
  273. {selectools-1.0.1 → selectools-1.2.0}/tests/test_a2a.py +0 -0
  274. {selectools-1.0.1 → selectools-1.2.0}/tests/test_agent_api.py +0 -0
  275. {selectools-1.0.1 → selectools-1.2.0}/tests/test_agent_hitl.py +0 -0
  276. {selectools-1.0.1 → selectools-1.2.0}/tests/test_approval_gate.py +0 -0
  277. {selectools-1.0.1 → selectools-1.2.0}/tests/test_architecture.py +0 -0
  278. {selectools-1.0.1 → selectools-1.2.0}/tests/test_async_observers.py +0 -0
  279. {selectools-1.0.1 → selectools-1.2.0}/tests/test_audit.py +0 -0
  280. {selectools-1.0.1 → selectools-1.2.0}/tests/test_bake_window_hunt.py +0 -0
  281. {selectools-1.0.1 → selectools-1.2.0}/tests/test_budget.py +0 -0
  282. {selectools-1.0.1 → selectools-1.2.0}/tests/test_cache.py +0 -0
  283. {selectools-1.0.1 → selectools-1.2.0}/tests/test_cache_redis.py +0 -0
  284. {selectools-1.0.1 → selectools-1.2.0}/tests/test_cancellation.py +0 -0
  285. {selectools-1.0.1 → selectools-1.2.0}/tests/test_conversation_branching.py +0 -0
  286. {selectools-1.0.1 → selectools-1.2.0}/tests/test_e2e_langfuse_observer.py +0 -0
  287. {selectools-1.0.1 → selectools-1.2.0}/tests/test_e2e_otel_observer.py +0 -0
  288. {selectools-1.0.1 → selectools-1.2.0}/tests/test_e2e_v0_21_0_apps.py +0 -0
  289. {selectools-1.0.1 → selectools-1.2.0}/tests/test_e2e_v0_21_0_simulations.py +0 -0
  290. {selectools-1.0.1 → selectools-1.2.0}/tests/test_env.py +0 -0
  291. {selectools-1.0.1 → selectools-1.2.0}/tests/test_guardrails_injection.py +0 -0
  292. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge_sanitizers.py +0 -0
  293. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge_store_redis.py +0 -0
  294. {selectools-1.0.1 → selectools-1.2.0}/tests/test_knowledge_store_supabase.py +0 -0
  295. {selectools-1.0.1 → selectools-1.2.0}/tests/test_langfuse_observer.py +0 -0
  296. {selectools-1.0.1 → selectools-1.2.0}/tests/test_loop_detection.py +0 -0
  297. {selectools-1.0.1 → selectools-1.2.0}/tests/test_memory.py +0 -0
  298. {selectools-1.0.1 → selectools-1.2.0}/tests/test_memory_async.py +0 -0
  299. {selectools-1.0.1 → selectools-1.2.0}/tests/test_model_switching.py +0 -0
  300. {selectools-1.0.1 → selectools-1.2.0}/tests/test_orchestration_e2e.py +0 -0
  301. {selectools-1.0.1 → selectools-1.2.0}/tests/test_orchestration_primitives.py +0 -0
  302. {selectools-1.0.1 → selectools-1.2.0}/tests/test_otel_observer.py +0 -0
  303. {selectools-1.0.1 → selectools-1.2.0}/tests/test_parser.py +0 -0
  304. {selectools-1.0.1 → selectools-1.2.0}/tests/test_pending.py +0 -0
  305. {selectools-1.0.1 → selectools-1.2.0}/tests/test_pending_redis_smoke.py +0 -0
  306. {selectools-1.0.1 → selectools-1.2.0}/tests/test_policy.py +0 -0
  307. {selectools-1.0.1 → selectools-1.2.0}/tests/test_prompt.py +0 -0
  308. {selectools-1.0.1 → selectools-1.2.0}/tests/test_reasoning_strategy.py +0 -0
  309. {selectools-1.0.1 → selectools-1.2.0}/tests/test_routing_mode.py +0 -0
  310. {selectools-1.0.1 → selectools-1.2.0}/tests/test_scheduler.py +0 -0
  311. {selectools-1.0.1 → selectools-1.2.0}/tests/test_sessions.py +0 -0
  312. {selectools-1.0.1 → selectools-1.2.0}/tests/test_sessions_mongo.py +0 -0
  313. {selectools-1.0.1 → selectools-1.2.0}/tests/test_simple_observer.py +0 -0
  314. {selectools-1.0.1 → selectools-1.2.0}/tests/test_simulation_evals.py +0 -0
  315. {selectools-1.0.1 → selectools-1.2.0}/tests/test_ssrf.py +0 -0
  316. {selectools-1.0.1 → selectools-1.2.0}/tests/test_stability.py +0 -0
  317. {selectools-1.0.1 → selectools-1.2.0}/tests/test_structured.py +0 -0
  318. {selectools-1.0.1 → selectools-1.2.0}/tests/test_templates_coverage.py +0 -0
  319. {selectools-1.0.1 → selectools-1.2.0}/tests/test_time.py +0 -0
  320. {selectools-1.0.1 → selectools-1.2.0}/tests/test_tool_caching.py +0 -0
  321. {selectools-1.0.1 → selectools-1.2.0}/tests/test_tool_result_compression.py +0 -0
  322. {selectools-1.0.1 → selectools-1.2.0}/tests/test_unified_memory.py +0 -0
  323. {selectools-1.0.1 → selectools-1.2.0}/tests/test_unified_memory_config.py +0 -0
  324. {selectools-1.0.1 → selectools-1.2.0}/tests/test_yaml_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: selectools
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Production-ready Python framework for AI agents with multi-agent graphs, hybrid RAG, guardrails, audit logging, 50 evaluators, and a visual builder. Supports OpenAI, Anthropic, Gemini, Ollama. By NichevLabs.
5
5
  Author-email: John Nichev <johnnichev@gmail.com>
6
6
  Maintainer-email: NichevLabs <support@nichevlabs.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "selectools"
7
- version = "1.0.1"
7
+ version = "1.2.0"
8
8
  description = "Production-ready Python framework for AI agents with multi-agent graphs, hybrid RAG, guardrails, audit logging, 50 evaluators, and a visual builder. Supports OpenAI, Anthropic, Gemini, Ollama. By NichevLabs."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  """Public exports for the selectools package."""
2
2
 
3
- __version__ = "1.0.1"
3
+ __version__ = "1.2.0"
4
4
 
5
5
  # Import submodules (lazy loading for optional dependencies)
6
6
  from . import embeddings, evals, guardrails, models, observe, patterns, rag, toolbox
@@ -13,6 +13,7 @@ from .agent.config_groups import (
13
13
  MemoryConfig,
14
14
  RetryConfig,
15
15
  SessionConfig,
16
+ StructuredOutputConfig,
16
17
  SummarizeConfig,
17
18
  ToolConfig,
18
19
  TraceConfig,
@@ -6,7 +6,7 @@ import asyncio
6
6
  import threading
7
7
  import time
8
8
  from concurrent.futures import ThreadPoolExecutor
9
- from typing import TYPE_CHECKING, Callable, List, Optional, Tuple
9
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
10
10
 
11
11
  # Module-level singleton for running sync provider calls in an async context.
12
12
  # Creating a new ThreadPoolExecutor per call (inside a retry loop) wastes
@@ -50,8 +50,18 @@ class _ProviderCallerMixin:
50
50
  stream_handler: Optional[Callable[[str], None]] = None,
51
51
  trace: Optional[AgentTrace] = None,
52
52
  run_id: Optional[str] = None,
53
+ response_format: Optional[Dict[str, Any]] = None,
54
+ tools_override: Optional[List[Any]] = None,
55
+ system_prompt_override: Optional[str] = None,
53
56
  ) -> Message:
54
57
  call_start = time.time()
58
+ effective_tools = self.tools if tools_override is None else tools_override
59
+ effective_system_prompt = (
60
+ system_prompt_override if system_prompt_override is not None else self._system_prompt
61
+ )
62
+ provider_extra: Dict[str, Any] = {}
63
+ if response_format is not None:
64
+ provider_extra["response_format"] = response_format
55
65
 
56
66
  cache_key: Optional[str] = None
57
67
  if self.config.cache and not (
@@ -59,10 +69,11 @@ class _ProviderCallerMixin:
59
69
  ):
60
70
  cache_key = CacheKeyBuilder.build(
61
71
  model=self._effective_model,
62
- system_prompt=self._system_prompt,
72
+ system_prompt=effective_system_prompt,
63
73
  messages=self._history,
64
- tools=self.tools,
74
+ tools=effective_tools,
65
75
  temperature=self.config.temperature,
76
+ response_format=response_format,
66
77
  )
67
78
  cached = self.config.cache.get(cache_key)
68
79
  # Only treat a well-formed (Message, usage) pair as a cache hit;
@@ -82,7 +93,7 @@ class _ProviderCallerMixin:
82
93
  run_id,
83
94
  self._history,
84
95
  self._effective_model,
85
- self._system_prompt,
96
+ effective_system_prompt,
86
97
  )
87
98
  self._notify_observers(
88
99
  "on_llm_end",
@@ -122,12 +133,15 @@ class _ProviderCallerMixin:
122
133
  run_id,
123
134
  self._history,
124
135
  self._effective_model,
125
- self._system_prompt,
136
+ effective_system_prompt,
126
137
  )
127
138
 
128
139
  if self.config.stream and getattr(self.provider, "supports_streaming", False):
129
140
  response_text, streamed_tool_calls = self._streaming_call(
130
- stream_handler=stream_handler
141
+ stream_handler=stream_handler,
142
+ response_format=response_format,
143
+ tools=effective_tools,
144
+ system_prompt=effective_system_prompt,
131
145
  )
132
146
  if run_id:
133
147
  self._notify_observers("on_llm_end", run_id, response_text, None)
@@ -139,12 +153,13 @@ class _ProviderCallerMixin:
139
153
 
140
154
  response_msg, usage_stats = self.provider.complete(
141
155
  model=self._effective_model,
142
- system_prompt=self._system_prompt,
156
+ system_prompt=effective_system_prompt,
143
157
  messages=self._history,
144
- tools=self.tools,
158
+ tools=effective_tools,
145
159
  temperature=self.config.temperature,
146
160
  max_tokens=self.config.max_tokens,
147
161
  timeout=self.config.request_timeout,
162
+ **provider_extra,
148
163
  )
149
164
  response_text = response_msg.content or ""
150
165
 
@@ -229,21 +244,29 @@ class _ProviderCallerMixin:
229
244
  )
230
245
 
231
246
  def _streaming_call(
232
- self, stream_handler: Optional[Callable[[str], None]] = None
247
+ self,
248
+ stream_handler: Optional[Callable[[str], None]] = None,
249
+ response_format: Optional[Dict[str, Any]] = None,
250
+ tools: Optional[List[Any]] = None,
251
+ system_prompt: Optional[str] = None,
233
252
  ) -> Tuple[str, List[ToolCall]]:
234
253
  if not getattr(self.provider, "supports_streaming", False):
235
254
  raise ProviderError(f"Provider {self.provider.name} does not support streaming.")
236
255
 
256
+ provider_extra: Dict[str, Any] = {}
257
+ if response_format is not None:
258
+ provider_extra["response_format"] = response_format
237
259
  aggregated: List[str] = []
238
260
  tool_calls: List[ToolCall] = []
239
261
  for chunk in self.provider.stream(
240
262
  model=self._effective_model,
241
- system_prompt=self._system_prompt,
263
+ system_prompt=self._system_prompt if system_prompt is None else system_prompt,
242
264
  messages=self._history,
243
- tools=self.tools,
265
+ tools=self.tools if tools is None else tools,
244
266
  temperature=self.config.temperature,
245
267
  max_tokens=self.config.max_tokens,
246
268
  timeout=self.config.request_timeout,
269
+ **provider_extra,
247
270
  ):
248
271
  if isinstance(chunk, str):
249
272
  if chunk:
@@ -264,9 +287,19 @@ class _ProviderCallerMixin:
264
287
  stream_handler: Optional[Callable[[str], None]] = None,
265
288
  trace: Optional[AgentTrace] = None,
266
289
  run_id: Optional[str] = None,
290
+ response_format: Optional[Dict[str, Any]] = None,
291
+ tools_override: Optional[List[Any]] = None,
292
+ system_prompt_override: Optional[str] = None,
267
293
  ) -> Message:
268
294
  """Async version of _call_provider with retry logic."""
269
295
  call_start = time.time()
296
+ effective_tools = self.tools if tools_override is None else tools_override
297
+ effective_system_prompt = (
298
+ system_prompt_override if system_prompt_override is not None else self._system_prompt
299
+ )
300
+ provider_extra: Dict[str, Any] = {}
301
+ if response_format is not None:
302
+ provider_extra["response_format"] = response_format
270
303
 
271
304
  cache_key: Optional[str] = None
272
305
  if self.config.cache and not (
@@ -274,10 +307,11 @@ class _ProviderCallerMixin:
274
307
  ):
275
308
  cache_key = CacheKeyBuilder.build(
276
309
  model=self._effective_model,
277
- system_prompt=self._system_prompt,
310
+ system_prompt=effective_system_prompt,
278
311
  messages=self._history,
279
- tools=self.tools,
312
+ tools=effective_tools,
280
313
  temperature=self.config.temperature,
314
+ response_format=response_format,
281
315
  )
282
316
  cached = self.config.cache.get(cache_key)
283
317
  # Only treat a well-formed (Message, usage) pair as a cache hit;
@@ -297,14 +331,14 @@ class _ProviderCallerMixin:
297
331
  run_id,
298
332
  self._history,
299
333
  self._effective_model,
300
- self._system_prompt,
334
+ effective_system_prompt,
301
335
  )
302
336
  await self._anotify_observers(
303
337
  "on_llm_start",
304
338
  run_id,
305
339
  self._history,
306
340
  self._effective_model,
307
- self._system_prompt,
341
+ effective_system_prompt,
308
342
  )
309
343
  self._notify_observers(
310
344
  "on_llm_end",
@@ -357,19 +391,22 @@ class _ProviderCallerMixin:
357
391
  run_id,
358
392
  self._history,
359
393
  self._effective_model,
360
- self._system_prompt,
394
+ effective_system_prompt,
361
395
  )
362
396
  await self._anotify_observers(
363
397
  "on_llm_start",
364
398
  run_id,
365
399
  self._history,
366
400
  self._effective_model,
367
- self._system_prompt,
401
+ effective_system_prompt,
368
402
  )
369
403
 
370
404
  if self.config.stream and getattr(self.provider, "supports_streaming", False):
371
405
  response_text, streamed_tool_calls = await self._astreaming_call(
372
- stream_handler=stream_handler
406
+ stream_handler=stream_handler,
407
+ response_format=response_format,
408
+ tools=effective_tools,
409
+ system_prompt=effective_system_prompt,
373
410
  )
374
411
  if run_id:
375
412
  self._notify_observers("on_llm_end", run_id, response_text, None)
@@ -386,12 +423,13 @@ class _ProviderCallerMixin:
386
423
  ):
387
424
  response_msg, usage_stats = await self.provider.acomplete(
388
425
  model=self._effective_model,
389
- system_prompt=self._system_prompt,
426
+ system_prompt=effective_system_prompt,
390
427
  messages=self._history,
391
- tools=self.tools,
428
+ tools=effective_tools,
392
429
  temperature=self.config.temperature,
393
430
  max_tokens=self.config.max_tokens,
394
431
  timeout=self.config.request_timeout,
432
+ **provider_extra,
395
433
  )
396
434
  response_text = response_msg.content or ""
397
435
  else:
@@ -405,12 +443,13 @@ class _ProviderCallerMixin:
405
443
  _get_async_provider_executor(),
406
444
  lambda: self.provider.complete(
407
445
  model=self._effective_model,
408
- system_prompt=self._system_prompt,
446
+ system_prompt=effective_system_prompt,
409
447
  messages=self._history,
410
- tools=self.tools,
448
+ tools=effective_tools,
411
449
  temperature=self.config.temperature,
412
450
  max_tokens=self.config.max_tokens,
413
451
  timeout=self.config.request_timeout,
452
+ **provider_extra,
414
453
  ),
415
454
  )
416
455
  response_text = response_msg.content or ""
@@ -506,12 +545,21 @@ class _ProviderCallerMixin:
506
545
  )
507
546
 
508
547
  async def _astreaming_call(
509
- self, stream_handler: Optional[Callable[[str], None]] = None
548
+ self,
549
+ stream_handler: Optional[Callable[[str], None]] = None,
550
+ response_format: Optional[Dict[str, Any]] = None,
551
+ tools: Optional[List[Any]] = None,
552
+ system_prompt: Optional[str] = None,
510
553
  ) -> Tuple[str, List[ToolCall]]:
511
554
  """Async version of _streaming_call."""
512
555
  if not getattr(self.provider, "supports_streaming", False):
513
556
  raise ProviderError(f"Provider {self.provider.name} does not support streaming.")
514
557
 
558
+ provider_extra: Dict[str, Any] = {}
559
+ if response_format is not None:
560
+ provider_extra["response_format"] = response_format
561
+ effective_tools = self.tools if tools is None else tools
562
+ effective_system_prompt = self._system_prompt if system_prompt is None else system_prompt
515
563
  aggregated: List[str] = []
516
564
  tool_calls: List[ToolCall] = []
517
565
 
@@ -521,12 +569,13 @@ class _ProviderCallerMixin:
521
569
  async with aclosing(
522
570
  self.provider.astream( # type: ignore[attr-defined]
523
571
  model=self._effective_model,
524
- system_prompt=self._system_prompt,
572
+ system_prompt=effective_system_prompt,
525
573
  messages=self._history,
526
- tools=self.tools,
574
+ tools=effective_tools,
527
575
  temperature=self.config.temperature,
528
576
  max_tokens=self.config.max_tokens,
529
577
  timeout=self.config.request_timeout,
578
+ **provider_extra,
530
579
  )
531
580
  ) as stream:
532
581
  async for chunk in stream:
@@ -546,12 +595,13 @@ class _ProviderCallerMixin:
546
595
  with closing(
547
596
  self.provider.stream(
548
597
  model=self._effective_model,
549
- system_prompt=self._system_prompt,
598
+ system_prompt=effective_system_prompt,
550
599
  messages=self._history,
551
- tools=self.tools,
600
+ tools=effective_tools,
552
601
  temperature=self.config.temperature,
553
602
  max_tokens=self.config.max_tokens,
554
603
  timeout=self.config.request_timeout,
604
+ **provider_extra,
555
605
  )
556
606
  ) as stream:
557
607
  for chunk in stream:
@@ -861,6 +861,11 @@ class _ToolExecutorMixin:
861
861
  cached_entry = self._check_tool_cache(tool, parameters)
862
862
  if cached_entry is not None:
863
863
  raw, compressed = cached_entry
864
+ # Guardrails run on cache hits too — an entry written before
865
+ # a guardrail was configured must not bypass the stage.
866
+ guarded = self._run_tool_result_guardrails(tool_name, raw, trace, run_ctx=run_ctx)
867
+ if guarded != raw:
868
+ raw, compressed = guarded, None
864
869
  return _Result(
865
870
  tc, raw, False, 0.0, tool, 0, cached=True, cached_compressed=compressed
866
871
  )
@@ -886,7 +891,10 @@ class _ToolExecutorMixin:
886
891
  try:
887
892
  result = self._execute_tool_with_timeout(tool, parameters, chunk_cb)
888
893
  result = self._screen_tool_result(tool_name, result)
894
+ # Cache the pre-guardrail value: guardrails run at use time
895
+ # (fresh AND cached), so config changes apply retroactively.
889
896
  self._store_tool_cache(tool, parameters, result)
897
+ result = self._run_tool_result_guardrails(tool_name, result, trace, run_ctx=run_ctx)
890
898
  dur = time.time() - start
891
899
  if run_id:
892
900
  self._notify_observers(
@@ -1103,6 +1111,13 @@ class _ToolExecutorMixin:
1103
1111
  cached_entry = self._check_tool_cache(tool, parameters)
1104
1112
  if cached_entry is not None:
1105
1113
  raw, compressed = cached_entry
1114
+ # Guardrails run on cache hits too — an entry written before
1115
+ # a guardrail was configured must not bypass the stage.
1116
+ guarded = await self._arun_tool_result_guardrails(
1117
+ tool_name, raw, trace, run_ctx=run_ctx
1118
+ )
1119
+ if guarded != raw:
1120
+ raw, compressed = guarded, None
1106
1121
  return _Result(
1107
1122
  tc, raw, False, 0.0, tool, 0, cached=True, cached_compressed=compressed
1108
1123
  )
@@ -1130,7 +1145,12 @@ class _ToolExecutorMixin:
1130
1145
  try:
1131
1146
  result = await self._aexecute_tool_with_timeout(tool, parameters, chunk_cb)
1132
1147
  result = self._screen_tool_result(tool_name, result)
1148
+ # Cache the pre-guardrail value: guardrails run at use time
1149
+ # (fresh AND cached), so config changes apply retroactively.
1133
1150
  self._store_tool_cache(tool, parameters, result)
1151
+ result = await self._arun_tool_result_guardrails(
1152
+ tool_name, result, trace, run_ctx=run_ctx
1153
+ )
1134
1154
  dur = time.time() - start
1135
1155
  if run_id:
1136
1156
  self._notify_observers(
@@ -1437,6 +1457,13 @@ class _ToolExecutorMixin:
1437
1457
  cached_entry = self._check_tool_cache(tool, parameters)
1438
1458
  if cached_entry is not None:
1439
1459
  cached_raw, cached_compressed = cached_entry
1460
+ # Guardrails run on cache hits too — an entry written before a
1461
+ # guardrail was configured must not bypass the stage.
1462
+ guarded = self._run_tool_result_guardrails(
1463
+ tool_name, cached_raw, ctx.trace, run_ctx=ctx
1464
+ )
1465
+ if guarded != cached_raw:
1466
+ cached_raw, cached_compressed = guarded, None
1440
1467
  ctx.trace.add(
1441
1468
  TraceStep(
1442
1469
  type=StepType.CACHE_HIT,
@@ -1475,15 +1502,16 @@ class _ToolExecutorMixin:
1475
1502
 
1476
1503
  result = self._execute_tool_with_timeout(tool, parameters, chunk_callback)
1477
1504
  result = self._screen_tool_result(tool_name, result)
1505
+ # Cache the pre-guardrail value: guardrails run at use time
1506
+ # (fresh AND cached), so config changes apply retroactively.
1507
+ self._store_tool_cache(tool, parameters, result)
1508
+ result = self._run_tool_result_guardrails(tool_name, result, ctx.trace, run_ctx=ctx)
1478
1509
  duration = time.time() - start_time
1479
1510
 
1480
1511
  self._notify_observers(
1481
1512
  "on_tool_end", ctx.run_id, call_id, tool_name, result, duration * 1000
1482
1513
  )
1483
1514
 
1484
- # Store in tool result cache
1485
- self._store_tool_cache(tool, parameters, result)
1486
-
1487
1515
  ctx.trace.add(
1488
1516
  TraceStep(
1489
1517
  type=StepType.TOOL_EXECUTION,
@@ -1673,6 +1701,13 @@ class _ToolExecutorMixin:
1673
1701
  cached_entry = self._check_tool_cache(tool, parameters)
1674
1702
  if cached_entry is not None:
1675
1703
  cached_raw, cached_compressed = cached_entry
1704
+ # Guardrails run on cache hits too — an entry written before a
1705
+ # guardrail was configured must not bypass the stage.
1706
+ guarded = await self._arun_tool_result_guardrails(
1707
+ tool_name, cached_raw, ctx.trace, run_ctx=ctx
1708
+ )
1709
+ if guarded != cached_raw:
1710
+ cached_raw, cached_compressed = guarded, None
1676
1711
  ctx.trace.add(
1677
1712
  TraceStep(
1678
1713
  type=StepType.CACHE_HIT,
@@ -1714,6 +1749,12 @@ class _ToolExecutorMixin:
1714
1749
 
1715
1750
  result = await self._aexecute_tool_with_timeout(tool, parameters, chunk_callback)
1716
1751
  result = self._screen_tool_result(tool_name, result)
1752
+ # Cache the pre-guardrail value: guardrails run at use time
1753
+ # (fresh AND cached), so config changes apply retroactively.
1754
+ self._store_tool_cache(tool, parameters, result)
1755
+ result = await self._arun_tool_result_guardrails(
1756
+ tool_name, result, ctx.trace, run_ctx=ctx
1757
+ )
1717
1758
  duration = time.time() - start_time
1718
1759
 
1719
1760
  self._notify_observers(
@@ -1723,9 +1764,6 @@ class _ToolExecutorMixin:
1723
1764
  "on_tool_end", ctx.run_id, call_id, tool_name, result, duration * 1000
1724
1765
  )
1725
1766
 
1726
- # Store in tool result cache
1727
- self._store_tool_cache(tool, parameters, result)
1728
-
1729
1767
  ctx.trace.add(
1730
1768
  TraceStep(
1731
1769
  type=StepType.TOOL_EXECUTION,
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
30
30
  MemoryConfig,
31
31
  RetryConfig,
32
32
  SessionConfig,
33
+ StructuredOutputConfig,
33
34
  SummarizeConfig,
34
35
  ToolConfig,
35
36
  TraceConfig,
@@ -202,6 +203,8 @@ class AgentConfig:
202
203
  budget: Optional["BudgetConfig"] = None
203
204
  trace: Optional["TraceConfig"] = None
204
205
  compress: Optional["CompressConfig"] = None
206
+ # Structured-output behavior (beta, v1.1). Group-only — no flat mirrors.
207
+ structured_output: Optional["StructuredOutputConfig"] = None
205
208
 
206
209
  # -- Planning-as-config (ROADMAP P2). See PlanningConfig below and
207
210
  # agent/_planning.py for the adapter. Self-contained: default None means
@@ -217,6 +220,7 @@ class AgentConfig:
217
220
  MemoryConfig,
218
221
  RetryConfig,
219
222
  SessionConfig,
223
+ StructuredOutputConfig,
220
224
  SummarizeConfig,
221
225
  ToolConfig,
222
226
  TraceConfig,
@@ -382,6 +386,10 @@ class AgentConfig:
382
386
  keep_recent=self.compress_keep_recent,
383
387
  )
384
388
 
389
+ self.structured_output = _unpack(self.structured_output, StructuredOutputConfig)
390
+ if self.structured_output is None:
391
+ self.structured_output = StructuredOutputConfig()
392
+
385
393
  # -- Planning-as-config: auto-unpack dicts (for YAML / dict configs) --
386
394
  if isinstance(self.planning, dict):
387
395
  self.planning = PlanningConfig(**self.planning)
@@ -24,9 +24,9 @@ Usage::
24
24
  from __future__ import annotations
25
25
 
26
26
  from dataclasses import dataclass, field
27
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
27
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
28
28
 
29
- from ..stability import stable
29
+ from ..stability import beta, stable
30
30
 
31
31
  if TYPE_CHECKING:
32
32
  from ..cancellation import CancellationToken
@@ -37,6 +37,7 @@ if TYPE_CHECKING:
37
37
  from ..policy import ToolPolicy
38
38
  from ..providers.base import Provider
39
39
  from ..sessions import SessionStore
40
+ from ..types import Message
40
41
  from ..unified_memory import UnifiedMemory
41
42
 
42
43
 
@@ -174,6 +175,63 @@ class GuardrailsConfig:
174
175
  output_screening_patterns: Optional[List[str]] = None
175
176
 
176
177
 
178
+ @beta
179
+ @dataclass
180
+ class StructuredOutputConfig:
181
+ """Structured-output (``response_format``) behavior settings (beta, v1.1).
182
+
183
+ Attributes:
184
+ native: Use the provider's native structured-output mode (OpenAI
185
+ ``json_schema``, Gemini ``response_json_schema``) when the provider
186
+ advertises support, instead of injecting a schema instruction into
187
+ the system prompt. Providers without native support keep the
188
+ prompt-injection + parse behavior unchanged. Default: True.
189
+ final_turn_only: Keep the schema out of tool-loop turns entirely.
190
+ The loop runs to convergence with no schema pressure (so tool
191
+ calling and the text ``ToolCallParser`` work normally), then one
192
+ extra synthesis call — with the schema applied and no tools —
193
+ produces the validated structured final answer. In ``astream()``
194
+ the synthesis JSON is delivered only via the terminal
195
+ ``AgentResult`` (``.content`` / ``.parsed``), never leaked as
196
+ content chunks. Costs at most one extra provider call per run
197
+ (see ``reuse_loop_answer`` / ``should_finalize`` /
198
+ ``single_pass`` for the paths that avoid it). Default: False.
199
+ reuse_loop_answer: In ``final_turn_only`` mode, when the loop's
200
+ converged answer validates against the schema, use it directly
201
+ and skip the synthesis call entirely (v1.2, issue #164). The
202
+ gate is conservative: the WHOLE answer must be a single JSON
203
+ object (fragments embedded in prose never qualify), Pydantic
204
+ schemas validate fully, and dict schemas must carry every
205
+ top-level ``required`` key. Anything else falls back to the
206
+ synthesis call. Default: True.
207
+ should_finalize: Optional predicate ``(messages, last_response_text)
208
+ -> bool`` consulted in ``final_turn_only`` mode when the
209
+ converged answer did NOT validate. Return ``False`` to skip the
210
+ synthesis call for turns that need no structured output — the
211
+ run finishes with ``parsed=None`` and
212
+ ``AgentResult.structured_status == "skipped"``. A validating
213
+ answer wins before the predicate runs (even with
214
+ ``reuse_loop_answer=False``). Like ``stop_condition``, an
215
+ exception raised by the predicate propagates and aborts the run.
216
+ Default: None (always finalize, the v1.1 behavior).
217
+ single_pass: In ``final_turn_only`` mode with a provider that
218
+ advertises ``supports_native_structured_output_with_tools``
219
+ (OpenAI/Azure), carry the schema natively on the loop calls so
220
+ the converged answer IS the structured object — combined with
221
+ ``reuse_loop_answer`` this eliminates the synthesis call (v1.2,
222
+ issue #166). Providers without combined support fall back to the
223
+ separate synthesis call. NOTE: in ``astream()`` the final answer
224
+ is then JSON and streams as content chunks (like plain native
225
+ mode). Default: False.
226
+ """
227
+
228
+ native: bool = True
229
+ final_turn_only: bool = False
230
+ reuse_loop_answer: bool = True
231
+ should_finalize: Optional[Callable[[List["Message"], str], bool]] = None
232
+ single_pass: bool = False
233
+
234
+
177
235
  @stable
178
236
  @dataclass
179
237
  class SessionConfig:
@@ -323,6 +381,7 @@ __stability__ = "stable"
323
381
 
324
382
  __all__ = [
325
383
  "RetryConfig",
384
+ "StructuredOutputConfig",
326
385
  "ToolConfig",
327
386
  "CoherenceConfig",
328
387
  "GuardrailsConfig",