selectools 0.25.0__tar.gz → 0.26.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 (307) hide show
  1. {selectools-0.25.0/src/selectools.egg-info → selectools-0.26.0}/PKG-INFO +16 -3
  2. {selectools-0.25.0 → selectools-0.26.0}/README.md +15 -2
  3. {selectools-0.25.0 → selectools-0.26.0}/pyproject.toml +1 -1
  4. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/__init__.py +1 -1
  5. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/a2a/server.py +6 -0
  6. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/embeddings/gemini.py +35 -9
  7. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/exceptions.py +26 -1
  8. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/guardrails/base.py +6 -0
  9. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/knowledge_backends.py +56 -4
  10. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/knowledge_sanitizers.py +35 -4
  11. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/loop_detection.py +15 -0
  12. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/models.py +230 -515
  13. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/pending.py +30 -3
  14. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/pricing.py +41 -8
  15. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/anthropic_provider.py +18 -10
  16. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/_static/builder.js +17 -14
  17. {selectools-0.25.0 → selectools-0.26.0/src/selectools.egg-info}/PKG-INFO +16 -3
  18. {selectools-0.25.0 → selectools-0.26.0}/src/selectools.egg-info/SOURCES.txt +1 -0
  19. selectools-0.26.0/tests/test_bake_window_hunt.py +115 -0
  20. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge_backend_supabase.py +150 -6
  21. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge_sanitizers.py +100 -0
  22. {selectools-0.25.0 → selectools-0.26.0}/tests/test_token_estimation.py +3 -3
  23. {selectools-0.25.0 → selectools-0.26.0}/LICENSE +0 -0
  24. {selectools-0.25.0 → selectools-0.26.0}/setup.cfg +0 -0
  25. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/_async_utils.py +0 -0
  26. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/_json_utils.py +0 -0
  27. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/a2a/__init__.py +0 -0
  28. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/a2a/client.py +0 -0
  29. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/a2a/types.py +0 -0
  30. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/agent/__init__.py +0 -0
  31. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/agent/_lifecycle.py +0 -0
  32. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/agent/_memory_manager.py +0 -0
  33. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/agent/_planning.py +0 -0
  34. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/agent/_provider_caller.py +0 -0
  35. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/agent/_tool_executor.py +0 -0
  36. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/agent/config.py +0 -0
  37. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/agent/config_groups.py +0 -0
  38. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/agent/core.py +0 -0
  39. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/analytics.py +0 -0
  40. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/audit.py +0 -0
  41. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/cache.py +0 -0
  42. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/cache_redis.py +0 -0
  43. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/cache_semantic.py +0 -0
  44. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/cancellation.py +0 -0
  45. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/checkpoint_postgres.py +0 -0
  46. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/coherence.py +0 -0
  47. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/compose.py +0 -0
  48. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/embeddings/__init__.py +0 -0
  49. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/embeddings/anthropic.py +0 -0
  50. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/embeddings/cohere.py +0 -0
  51. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/embeddings/openai.py +0 -0
  52. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/embeddings/provider.py +0 -0
  53. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/entity_memory.py +0 -0
  54. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/env.py +0 -0
  55. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/__init__.py +0 -0
  56. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/__main__.py +0 -0
  57. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/badge.py +0 -0
  58. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/dataset.py +0 -0
  59. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/evaluators.py +0 -0
  60. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/generator.py +0 -0
  61. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/history.py +0 -0
  62. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/html.py +0 -0
  63. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/junit.py +0 -0
  64. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/llm_evaluators.py +0 -0
  65. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/pairwise.py +0 -0
  66. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/regression.py +0 -0
  67. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/report.py +0 -0
  68. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/serve.py +0 -0
  69. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/snapshot.py +0 -0
  70. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/suite.py +0 -0
  71. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/templates.py +0 -0
  72. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/evals/types.py +0 -0
  73. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/guardrails/__init__.py +0 -0
  74. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/guardrails/format.py +0 -0
  75. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/guardrails/length.py +0 -0
  76. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/guardrails/pii.py +0 -0
  77. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/guardrails/pipeline.py +0 -0
  78. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/guardrails/topic.py +0 -0
  79. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/guardrails/toxicity.py +0 -0
  80. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/knowledge.py +0 -0
  81. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/knowledge_graph.py +0 -0
  82. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/knowledge_store_redis.py +0 -0
  83. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/knowledge_store_supabase.py +0 -0
  84. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/mcp/__init__.py +0 -0
  85. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/mcp/_loop.py +0 -0
  86. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/mcp/bridge.py +0 -0
  87. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/mcp/client.py +0 -0
  88. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/mcp/config.py +0 -0
  89. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/mcp/multi.py +0 -0
  90. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/mcp/server.py +0 -0
  91. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/memory.py +0 -0
  92. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/observe/__init__.py +0 -0
  93. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/observe/langfuse.py +0 -0
  94. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/observe/otel.py +0 -0
  95. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/observe/trace_store.py +0 -0
  96. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/observer.py +0 -0
  97. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/orchestration/__init__.py +0 -0
  98. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/orchestration/checkpoint.py +0 -0
  99. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/orchestration/graph.py +0 -0
  100. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/orchestration/node.py +0 -0
  101. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/orchestration/state.py +0 -0
  102. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/orchestration/supervisor.py +0 -0
  103. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/parser.py +0 -0
  104. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/patterns/__init__.py +0 -0
  105. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/patterns/debate.py +0 -0
  106. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/patterns/plan_and_execute.py +0 -0
  107. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/patterns/reflective.py +0 -0
  108. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/patterns/team_lead.py +0 -0
  109. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/pipeline.py +0 -0
  110. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/policy.py +0 -0
  111. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/prompt.py +0 -0
  112. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/__init__.py +0 -0
  113. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/_openai_compat.py +0 -0
  114. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/azure_openai_provider.py +0 -0
  115. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/base.py +0 -0
  116. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/fallback.py +0 -0
  117. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/gemini_provider.py +0 -0
  118. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/litellm_provider.py +0 -0
  119. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/ollama_provider.py +0 -0
  120. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/openai_provider.py +0 -0
  121. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/router.py +0 -0
  122. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/providers/stubs.py +0 -0
  123. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/__init__.py +0 -0
  124. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/bm25.py +0 -0
  125. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/chunking.py +0 -0
  126. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/hybrid.py +0 -0
  127. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/loaders.py +0 -0
  128. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/reranker.py +0 -0
  129. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/stores/__init__.py +0 -0
  130. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/stores/chroma.py +0 -0
  131. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/stores/faiss.py +0 -0
  132. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/stores/memory.py +0 -0
  133. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/stores/pgvector.py +0 -0
  134. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/stores/pinecone.py +0 -0
  135. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/stores/qdrant.py +0 -0
  136. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/stores/sqlite.py +0 -0
  137. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/tools.py +0 -0
  138. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/rag/vector_store.py +0 -0
  139. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/results.py +0 -0
  140. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/security.py +0 -0
  141. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/__init__.py +0 -0
  142. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/_starlette_app.py +0 -0
  143. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/_static/builder.css +0 -0
  144. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/_static/builder.html +0 -0
  145. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/api.py +0 -0
  146. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/app.py +0 -0
  147. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/builder.py +0 -0
  148. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/cli.py +0 -0
  149. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/models.py +0 -0
  150. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/serve/playground.py +0 -0
  151. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/sessions.py +0 -0
  152. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/stability.py +0 -0
  153. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/structured.py +0 -0
  154. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/templates/__init__.py +0 -0
  155. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/templates/code_reviewer.py +0 -0
  156. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/templates/customer_support.py +0 -0
  157. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/templates/data_analyst.py +0 -0
  158. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/templates/rag_chatbot.py +0 -0
  159. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/templates/research_assistant.py +0 -0
  160. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/token_estimation.py +0 -0
  161. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/__init__.py +0 -0
  162. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/calculator_tools.py +0 -0
  163. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/code_tools.py +0 -0
  164. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/data_tools.py +0 -0
  165. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/datetime_tools.py +0 -0
  166. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/db_tools.py +0 -0
  167. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/email_tools.py +0 -0
  168. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/file_tools.py +0 -0
  169. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/github_tools.py +0 -0
  170. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/linear_tools.py +0 -0
  171. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/memory_tools.py +0 -0
  172. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/notion_tools.py +0 -0
  173. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/pdf_tools.py +0 -0
  174. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/search_tools.py +0 -0
  175. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/slack_tools.py +0 -0
  176. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/text_tools.py +0 -0
  177. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/toolbox/web_tools.py +0 -0
  178. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/tools/__init__.py +0 -0
  179. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/tools/base.py +0 -0
  180. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/tools/decorators.py +0 -0
  181. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/tools/loader.py +0 -0
  182. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/tools/registry.py +0 -0
  183. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/trace.py +0 -0
  184. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/types.py +0 -0
  185. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/unified_memory.py +0 -0
  186. {selectools-0.25.0 → selectools-0.26.0}/src/selectools/usage.py +0 -0
  187. {selectools-0.25.0 → selectools-0.26.0}/src/selectools.egg-info/dependency_links.txt +0 -0
  188. {selectools-0.25.0 → selectools-0.26.0}/src/selectools.egg-info/entry_points.txt +0 -0
  189. {selectools-0.25.0 → selectools-0.26.0}/src/selectools.egg-info/requires.txt +0 -0
  190. {selectools-0.25.0 → selectools-0.26.0}/src/selectools.egg-info/top_level.txt +0 -0
  191. {selectools-0.25.0 → selectools-0.26.0}/tests/test_a2a.py +0 -0
  192. {selectools-0.25.0 → selectools-0.26.0}/tests/test_agent_api.py +0 -0
  193. {selectools-0.25.0 → selectools-0.26.0}/tests/test_agent_hitl.py +0 -0
  194. {selectools-0.25.0 → selectools-0.26.0}/tests/test_approval_gate.py +0 -0
  195. {selectools-0.25.0 → selectools-0.26.0}/tests/test_architecture.py +0 -0
  196. {selectools-0.25.0 → selectools-0.26.0}/tests/test_async_observers.py +0 -0
  197. {selectools-0.25.0 → selectools-0.26.0}/tests/test_audit.py +0 -0
  198. {selectools-0.25.0 → selectools-0.26.0}/tests/test_budget.py +0 -0
  199. {selectools-0.25.0 → selectools-0.26.0}/tests/test_bug_hunt_batch1_core.py +0 -0
  200. {selectools-0.25.0 → selectools-0.26.0}/tests/test_bug_hunt_batch1_security.py +0 -0
  201. {selectools-0.25.0 → selectools-0.26.0}/tests/test_bug_hunt_batch1_tools.py +0 -0
  202. {selectools-0.25.0 → selectools-0.26.0}/tests/test_bug_hunt_regression.py +0 -0
  203. {selectools-0.25.0 → selectools-0.26.0}/tests/test_cache.py +0 -0
  204. {selectools-0.25.0 → selectools-0.26.0}/tests/test_cache_redis.py +0 -0
  205. {selectools-0.25.0 → selectools-0.26.0}/tests/test_cancellation.py +0 -0
  206. {selectools-0.25.0 → selectools-0.26.0}/tests/test_checkpoint_postgres.py +0 -0
  207. {selectools-0.25.0 → selectools-0.26.0}/tests/test_coherence.py +0 -0
  208. {selectools-0.25.0 → selectools-0.26.0}/tests/test_concurrency_smoke.py +0 -0
  209. {selectools-0.25.0 → selectools-0.26.0}/tests/test_consolidation_regression.py +0 -0
  210. {selectools-0.25.0 → selectools-0.26.0}/tests/test_conversation_branching.py +0 -0
  211. {selectools-0.25.0 → selectools-0.26.0}/tests/test_coverage_orchestration.py +0 -0
  212. {selectools-0.25.0 → selectools-0.26.0}/tests/test_document_loaders_extended.py +0 -0
  213. {selectools-0.25.0 → selectools-0.26.0}/tests/test_e2e_langfuse_observer.py +0 -0
  214. {selectools-0.25.0 → selectools-0.26.0}/tests/test_e2e_multimodal.py +0 -0
  215. {selectools-0.25.0 → selectools-0.26.0}/tests/test_e2e_otel_observer.py +0 -0
  216. {selectools-0.25.0 → selectools-0.26.0}/tests/test_e2e_v0_21_0_apps.py +0 -0
  217. {selectools-0.25.0 → selectools-0.26.0}/tests/test_e2e_v0_21_0_simulations.py +0 -0
  218. {selectools-0.25.0 → selectools-0.26.0}/tests/test_entity_memory.py +0 -0
  219. {selectools-0.25.0 → selectools-0.26.0}/tests/test_env.py +0 -0
  220. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals.py +0 -0
  221. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_advanced.py +0 -0
  222. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_cli.py +0 -0
  223. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_e2e.py +0 -0
  224. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_final.py +0 -0
  225. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_hardening.py +0 -0
  226. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_html_report.py +0 -0
  227. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_new_evaluators.py +0 -0
  228. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_ralph_bugs.py +0 -0
  229. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_release.py +0 -0
  230. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_serve.py +0 -0
  231. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_v017_features.py +0 -0
  232. {selectools-0.25.0 → selectools-0.26.0}/tests/test_evals_v0191.py +0 -0
  233. {selectools-0.25.0 → selectools-0.26.0}/tests/test_features_in_graph.py +0 -0
  234. {selectools-0.25.0 → selectools-0.26.0}/tests/test_final_coverage_a.py +0 -0
  235. {selectools-0.25.0 → selectools-0.26.0}/tests/test_final_coverage_b.py +0 -0
  236. {selectools-0.25.0 → selectools-0.26.0}/tests/test_guardrails.py +0 -0
  237. {selectools-0.25.0 → selectools-0.26.0}/tests/test_hardening.py +0 -0
  238. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge.py +0 -0
  239. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge_backend.py +0 -0
  240. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge_backend_redis.py +0 -0
  241. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge_graph.py +0 -0
  242. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge_store_redis.py +0 -0
  243. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge_store_supabase.py +0 -0
  244. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge_stores.py +0 -0
  245. {selectools-0.25.0 → selectools-0.26.0}/tests/test_knowledge_stores_error_handling.py +0 -0
  246. {selectools-0.25.0 → selectools-0.26.0}/tests/test_langfuse_observer.py +0 -0
  247. {selectools-0.25.0 → selectools-0.26.0}/tests/test_loop_detection.py +0 -0
  248. {selectools-0.25.0 → selectools-0.26.0}/tests/test_mcp.py +0 -0
  249. {selectools-0.25.0 → selectools-0.26.0}/tests/test_mcp_coverage.py +0 -0
  250. {selectools-0.25.0 → selectools-0.26.0}/tests/test_memory.py +0 -0
  251. {selectools-0.25.0 → selectools-0.26.0}/tests/test_memory_async.py +0 -0
  252. {selectools-0.25.0 → selectools-0.26.0}/tests/test_memory_boundary.py +0 -0
  253. {selectools-0.25.0 → selectools-0.26.0}/tests/test_memory_integration.py +0 -0
  254. {selectools-0.25.0 → selectools-0.26.0}/tests/test_model_switching.py +0 -0
  255. {selectools-0.25.0 → selectools-0.26.0}/tests/test_multi_agent_edge_cases.py +0 -0
  256. {selectools-0.25.0 → selectools-0.26.0}/tests/test_multimodal.py +0 -0
  257. {selectools-0.25.0 → selectools-0.26.0}/tests/test_orchestration_checkpoint.py +0 -0
  258. {selectools-0.25.0 → selectools-0.26.0}/tests/test_orchestration_e2e.py +0 -0
  259. {selectools-0.25.0 → selectools-0.26.0}/tests/test_orchestration_evals.py +0 -0
  260. {selectools-0.25.0 → selectools-0.26.0}/tests/test_orchestration_graph.py +0 -0
  261. {selectools-0.25.0 → selectools-0.26.0}/tests/test_orchestration_integration.py +0 -0
  262. {selectools-0.25.0 → selectools-0.26.0}/tests/test_orchestration_primitives.py +0 -0
  263. {selectools-0.25.0 → selectools-0.26.0}/tests/test_orchestration_supervisor.py +0 -0
  264. {selectools-0.25.0 → selectools-0.26.0}/tests/test_otel_observer.py +0 -0
  265. {selectools-0.25.0 → selectools-0.26.0}/tests/test_parser.py +0 -0
  266. {selectools-0.25.0 → selectools-0.26.0}/tests/test_patterns.py +0 -0
  267. {selectools-0.25.0 → selectools-0.26.0}/tests/test_pending.py +0 -0
  268. {selectools-0.25.0 → selectools-0.26.0}/tests/test_pending_redis_smoke.py +0 -0
  269. {selectools-0.25.0 → selectools-0.26.0}/tests/test_phase1_design_patterns.py +0 -0
  270. {selectools-0.25.0 → selectools-0.26.0}/tests/test_pipeline.py +0 -0
  271. {selectools-0.25.0 → selectools-0.26.0}/tests/test_pipeline_coverage.py +0 -0
  272. {selectools-0.25.0 → selectools-0.26.0}/tests/test_policy.py +0 -0
  273. {selectools-0.25.0 → selectools-0.26.0}/tests/test_prompt.py +0 -0
  274. {selectools-0.25.0 → selectools-0.26.0}/tests/test_prompt_compression.py +0 -0
  275. {selectools-0.25.0 → selectools-0.26.0}/tests/test_property_based.py +0 -0
  276. {selectools-0.25.0 → selectools-0.26.0}/tests/test_reasoning_strategy.py +0 -0
  277. {selectools-0.25.0 → selectools-0.26.0}/tests/test_remaining_coverage.py +0 -0
  278. {selectools-0.25.0 → selectools-0.26.0}/tests/test_results.py +0 -0
  279. {selectools-0.25.0 → selectools-0.26.0}/tests/test_routing_mode.py +0 -0
  280. {selectools-0.25.0 → selectools-0.26.0}/tests/test_security.py +0 -0
  281. {selectools-0.25.0 → selectools-0.26.0}/tests/test_semantic_cache.py +0 -0
  282. {selectools-0.25.0 → selectools-0.26.0}/tests/test_serve_app_coverage.py +0 -0
  283. {selectools-0.25.0 → selectools-0.26.0}/tests/test_serve_cli.py +0 -0
  284. {selectools-0.25.0 → selectools-0.26.0}/tests/test_sessions.py +0 -0
  285. {selectools-0.25.0 → selectools-0.26.0}/tests/test_sessions_edge_cases.py +0 -0
  286. {selectools-0.25.0 → selectools-0.26.0}/tests/test_sessions_redis.py +0 -0
  287. {selectools-0.25.0 → selectools-0.26.0}/tests/test_sessions_supabase.py +0 -0
  288. {selectools-0.25.0 → selectools-0.26.0}/tests/test_simple_observer.py +0 -0
  289. {selectools-0.25.0 → selectools-0.26.0}/tests/test_simulation_evals.py +0 -0
  290. {selectools-0.25.0 → selectools-0.26.0}/tests/test_stability.py +0 -0
  291. {selectools-0.25.0 → selectools-0.26.0}/tests/test_starlette_app.py +0 -0
  292. {selectools-0.25.0 → selectools-0.26.0}/tests/test_structured.py +0 -0
  293. {selectools-0.25.0 → selectools-0.26.0}/tests/test_structured_config.py +0 -0
  294. {selectools-0.25.0 → selectools-0.26.0}/tests/test_structured_tool_results.py +0 -0
  295. {selectools-0.25.0 → selectools-0.26.0}/tests/test_summarize_on_trim.py +0 -0
  296. {selectools-0.25.0 → selectools-0.26.0}/tests/test_templates_coverage.py +0 -0
  297. {selectools-0.25.0 → selectools-0.26.0}/tests/test_terminal_actions.py +0 -0
  298. {selectools-0.25.0 → selectools-0.26.0}/tests/test_tool_caching.py +0 -0
  299. {selectools-0.25.0 → selectools-0.26.0}/tests/test_tool_result_compression.py +0 -0
  300. {selectools-0.25.0 → selectools-0.26.0}/tests/test_trace.py +0 -0
  301. {selectools-0.25.0 → selectools-0.26.0}/tests/test_trace_html.py +0 -0
  302. {selectools-0.25.0 → selectools-0.26.0}/tests/test_trace_store.py +0 -0
  303. {selectools-0.25.0 → selectools-0.26.0}/tests/test_unified_memory.py +0 -0
  304. {selectools-0.25.0 → selectools-0.26.0}/tests/test_v016_regression.py +0 -0
  305. {selectools-0.25.0 → selectools-0.26.0}/tests/test_v019_features.py +0 -0
  306. {selectools-0.25.0 → selectools-0.26.0}/tests/test_visual_builder.py +0 -0
  307. {selectools-0.25.0 → selectools-0.26.0}/tests/test_yaml_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: selectools
3
- Version: 0.25.0
3
+ Version: 0.26.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>
@@ -108,6 +108,19 @@ result = AgentGraph.chain(planner, writer, reviewer).run("Write a blog post")
108
108
  # selectools serve agent.yaml
109
109
  ```
110
110
 
111
+ ## What's New in v0.26
112
+
113
+ ### v0.26.0 — Safety Patch & Verified Registry
114
+
115
+ A mid-bake safety patch plus a fully source-verified model registry.
116
+
117
+ - **Confirm parser safety fix** — `RegexConfirmParser` treated replies like "se você não pode apagar, tudo bem" / "tú no puedes borrar" as confirmations: the restated-verb branch matched the action verb without checking for negation elsewhere in the reply. A negation token anywhere now vetoes that branch; bare confirms are unaffected. **All `selectools.pending` consumers should upgrade immediately.**
118
+ - **Model registry refresh** — 152 → 115 models, every entry source-verified: claude-fable-5 / opus-4-8 / opus-4-7, the gpt-5.5 family, and the gemini-3.5 line added; retired and shut-down models removed (**BREAKING** if you referenced their registry constants — e.g. `AnthropicModels.CLAUDE_3_5_SONNET` — but those IDs 404 at their providers regardless); `claude-opus-4-1` mispricing corrected ($5/$25 → $15/$75); gpt-5 family context specs fixed (400k/128k); `GeminiEmbeddingProvider` default moved off the retired `text-embedding-004` to `gemini-embedding-001`.
119
+ - **Cache-aware cost calculation** — `calculate_cost(..., cache_read_input_tokens=..., cache_creation_input_tokens=...)` prices prompt-cache traffic at Anthropic's published rates (reads 0.1×, 5-min writes 1.25×); `AnthropicProvider`'s `UsageStats.cost_usd` is now cache-accurate.
120
+ - **Fixes** — A2A returns JSON-RPC -32602 (not HTTP 500) for non-dict `message.parts` elements; Gemini embedding dimension constant corrected to the actual 3072 for `gemini-embedding-001`/`-2` (vectors themselves unchanged); bake-hunt test module no longer requires optional extras in CI.
121
+
122
+ See `CHANGELOG.md` for the full entry (7,420 tests, 111 examples, 115 models).
123
+
111
124
  ## What's New in v0.25
112
125
 
113
126
  ### v0.25.0 — Hardening & v1.0 Prep
@@ -659,7 +672,7 @@ report.to_html("report.html")
659
672
  - **Advanced Chunking**: Semantic + contextual chunking for better retrieval
660
673
  - **Dynamic Tool Loading**: Plugin system with hot-reload support
661
674
  - **Response Caching**: InMemoryCache and RedisCache with stats tracking
662
- - **152 Model Registry**: Type-safe constants with pricing and metadata
675
+ - **115 Model Registry**: Type-safe constants with pricing and metadata
663
676
  - **Pre-built Toolbox**: 48 tools for files, data, text, datetime, web, code, search, GitHub, DB, calculator, email, PDF, Slack, Notion, Linear
664
677
  - **Persistent Sessions**: 4 backends (JSON file, SQLite, Redis, Supabase) with TTL and cross-session search
665
678
  - **Entity Memory**: LLM-based named entity extraction and tracking
@@ -1035,7 +1048,7 @@ See [docs/modules/STREAMING.md](https://github.com/johnnichev/selectools/blob/ma
1035
1048
  ```python
1036
1049
  from selectools.models import OpenAI, Anthropic, Gemini, Ollama
1037
1050
 
1038
- # IDE autocomplete for all 152 models with pricing metadata
1051
+ # IDE autocomplete for all 115 models with pricing metadata
1039
1052
  model = OpenAI.GPT_4O_MINI
1040
1053
  print(f"Cost: ${model.prompt_cost}/${model.completion_cost} per 1M tokens")
1041
1054
  print(f"Context: {model.context_window:,} tokens")
@@ -30,6 +30,19 @@ result = AgentGraph.chain(planner, writer, reviewer).run("Write a blog post")
30
30
  # selectools serve agent.yaml
31
31
  ```
32
32
 
33
+ ## What's New in v0.26
34
+
35
+ ### v0.26.0 — Safety Patch & Verified Registry
36
+
37
+ A mid-bake safety patch plus a fully source-verified model registry.
38
+
39
+ - **Confirm parser safety fix** — `RegexConfirmParser` treated replies like "se você não pode apagar, tudo bem" / "tú no puedes borrar" as confirmations: the restated-verb branch matched the action verb without checking for negation elsewhere in the reply. A negation token anywhere now vetoes that branch; bare confirms are unaffected. **All `selectools.pending` consumers should upgrade immediately.**
40
+ - **Model registry refresh** — 152 → 115 models, every entry source-verified: claude-fable-5 / opus-4-8 / opus-4-7, the gpt-5.5 family, and the gemini-3.5 line added; retired and shut-down models removed (**BREAKING** if you referenced their registry constants — e.g. `AnthropicModels.CLAUDE_3_5_SONNET` — but those IDs 404 at their providers regardless); `claude-opus-4-1` mispricing corrected ($5/$25 → $15/$75); gpt-5 family context specs fixed (400k/128k); `GeminiEmbeddingProvider` default moved off the retired `text-embedding-004` to `gemini-embedding-001`.
41
+ - **Cache-aware cost calculation** — `calculate_cost(..., cache_read_input_tokens=..., cache_creation_input_tokens=...)` prices prompt-cache traffic at Anthropic's published rates (reads 0.1×, 5-min writes 1.25×); `AnthropicProvider`'s `UsageStats.cost_usd` is now cache-accurate.
42
+ - **Fixes** — A2A returns JSON-RPC -32602 (not HTTP 500) for non-dict `message.parts` elements; Gemini embedding dimension constant corrected to the actual 3072 for `gemini-embedding-001`/`-2` (vectors themselves unchanged); bake-hunt test module no longer requires optional extras in CI.
43
+
44
+ See `CHANGELOG.md` for the full entry (7,420 tests, 111 examples, 115 models).
45
+
33
46
  ## What's New in v0.25
34
47
 
35
48
  ### v0.25.0 — Hardening & v1.0 Prep
@@ -581,7 +594,7 @@ report.to_html("report.html")
581
594
  - **Advanced Chunking**: Semantic + contextual chunking for better retrieval
582
595
  - **Dynamic Tool Loading**: Plugin system with hot-reload support
583
596
  - **Response Caching**: InMemoryCache and RedisCache with stats tracking
584
- - **152 Model Registry**: Type-safe constants with pricing and metadata
597
+ - **115 Model Registry**: Type-safe constants with pricing and metadata
585
598
  - **Pre-built Toolbox**: 48 tools for files, data, text, datetime, web, code, search, GitHub, DB, calculator, email, PDF, Slack, Notion, Linear
586
599
  - **Persistent Sessions**: 4 backends (JSON file, SQLite, Redis, Supabase) with TTL and cross-session search
587
600
  - **Entity Memory**: LLM-based named entity extraction and tracking
@@ -957,7 +970,7 @@ See [docs/modules/STREAMING.md](https://github.com/johnnichev/selectools/blob/ma
957
970
  ```python
958
971
  from selectools.models import OpenAI, Anthropic, Gemini, Ollama
959
972
 
960
- # IDE autocomplete for all 152 models with pricing metadata
973
+ # IDE autocomplete for all 115 models with pricing metadata
961
974
  model = OpenAI.GPT_4O_MINI
962
975
  print(f"Cost: ${model.prompt_cost}/${model.completion_cost} per 1M tokens")
963
976
  print(f"Context: {model.context_window:,} tokens")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "selectools"
7
- version = "0.25.0"
7
+ version = "0.26.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.9"
@@ -1,6 +1,6 @@
1
1
  """Public exports for the selectools package."""
2
2
 
3
- __version__ = "0.25.0"
3
+ __version__ = "0.26.0"
4
4
 
5
5
  # Import submodules (lazy loading for optional dependencies)
6
6
  from . import embeddings, evals, guardrails, models, observe, patterns, rag, toolbox
@@ -94,6 +94,10 @@ def _extract_prompt(parts: List[Dict[str, Any]]) -> str:
94
94
  """
95
95
  chunks: List[str] = []
96
96
  for part in parts:
97
+ if not isinstance(part, dict):
98
+ # Defensive: validation rejects non-dict parts up front, but a
99
+ # stray list element must never crash prompt assembly.
100
+ continue
97
101
  kind = part.get("kind")
98
102
  if kind == "text" and isinstance(part.get("text"), str):
99
103
  chunks.append(part["text"])
@@ -300,6 +304,8 @@ class A2AServer:
300
304
  parts = message.get("parts")
301
305
  if not isinstance(parts, list) or not parts:
302
306
  return None, "'message.parts' must be a non-empty array"
307
+ if not all(isinstance(p, dict) for p in parts):
308
+ return None, "every entry in 'message.parts' must be an object"
303
309
  if not any(
304
310
  isinstance(p, dict) and p.get("kind") == "text" and isinstance(p.get("text"), str)
305
311
  for p in parts
@@ -16,14 +16,19 @@ class GeminiEmbeddingProvider(EmbeddingProvider):
16
16
  """
17
17
  Google Gemini embedding provider.
18
18
 
19
- Supports models: text-embedding-001, text-embedding-004
19
+ Supports models: gemini-embedding-001, gemini-embedding-2
20
+
21
+ Requests do not set ``output_dimensionality``, so vectors come back at
22
+ each model's default dimensionality (3072 for gemini-embedding-001 and
23
+ gemini-embedding-2, which support MRL truncation but are not truncated
24
+ here). See ``_get_model_dimension`` for the full dimension/MRL story.
20
25
 
21
26
  Example:
22
27
  >>> from selectools.embeddings import GeminiEmbeddingProvider
23
28
  >>> from selectools.models import Gemini
24
29
  >>>
25
30
  >>> embedder = GeminiEmbeddingProvider(
26
- ... model=Gemini.Embeddings.EMBEDDING_004.id
31
+ ... model=Gemini.Embeddings.EMBEDDING_001.id
27
32
  ... )
28
33
  >>> embedding = embedder.embed_text("Hello, world!")
29
34
  >>> print(f"Dimension: {len(embedding)}")
@@ -31,7 +36,9 @@ class GeminiEmbeddingProvider(EmbeddingProvider):
31
36
 
32
37
  def __init__(
33
38
  self,
34
- model: str = "text-embedding-004",
39
+ # Default changed from text-embedding-004 (retired by Google) to its
40
+ # documented replacement gemini-embedding-001.
41
+ model: str = "gemini-embedding-001",
35
42
  api_key: Optional[str] = None,
36
43
  task_type: str = "retrieval_document",
37
44
  ):
@@ -39,7 +46,7 @@ class GeminiEmbeddingProvider(EmbeddingProvider):
39
46
  Initialize Gemini embedding provider.
40
47
 
41
48
  Args:
42
- model: Model name (default: text-embedding-004)
49
+ model: Model name (default: gemini-embedding-001)
43
50
  api_key: Google API key (defaults to GEMINI_API_KEY or GOOGLE_API_KEY env var)
44
51
  task_type: Task type for embeddings (retrieval_document, retrieval_query,
45
52
  semantic_similarity, classification, clustering)
@@ -64,12 +71,31 @@ class GeminiEmbeddingProvider(EmbeddingProvider):
64
71
  self._dimension = self._get_model_dimension()
65
72
 
66
73
  def _get_model_dimension(self) -> int:
67
- """Get the embedding dimension for the model."""
68
- if "embedding-004" in self.model or "embedding-001" in self.model:
69
- return 768
70
- else:
71
- logger.warning(f"Unknown Gemini embedding model '{self.model}', assuming dimension 768")
74
+ """Get the embedding dimension for the model.
75
+
76
+ The declared dimension must match what the API actually returns. This
77
+ provider never passes ``output_dimensionality`` in its requests (see
78
+ ``embed_text``/``embed_texts``/``embed_query``), so the API returns
79
+ each model's default, full-size vector:
80
+
81
+ - ``gemini-embedding-001`` and ``gemini-embedding-2``: 3072 by
82
+ default. Both are trained with Matryoshka Representation Learning
83
+ (MRL), so vectors *can* be truncated server-side (Google recommends
84
+ 768, 1536, or 3072) by passing ``output_dimensionality`` — but this
85
+ provider does not request truncation, and existing vector store
86
+ indexes built with it hold 3072-dim vectors. Note that
87
+ ``gemini-embedding-001`` only normalizes the full 3072-dim output;
88
+ truncated outputs would need manual re-normalization.
89
+ - ``text-embedding-004`` (retired by Google): natively 768.
90
+
91
+ Source: https://ai.google.dev/gemini-api/docs/embeddings
92
+ """
93
+ if "embedding-004" in self.model:
72
94
  return 768
95
+ if "embedding-001" in self.model or "embedding-2" in self.model:
96
+ return 3072
97
+ logger.warning(f"Unknown Gemini embedding model '{self.model}', assuming dimension 3072")
98
+ return 3072
73
99
 
74
100
  def embed_text(self, text: str) -> List[float]:
75
101
  """
@@ -9,7 +9,7 @@ These exceptions provide PyTorch-style error messages with:
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from typing import Any, Dict
12
+ from typing import Any, Dict, Tuple
13
13
 
14
14
  from .stability import beta, stable
15
15
 
@@ -42,6 +42,13 @@ class ToolValidationError(SelectoolsError):
42
42
 
43
43
  super().__init__(message)
44
44
 
45
+ def __reduce__(self) -> Tuple[Any, ...]:
46
+ # Exception's default __reduce__ replays ``self.args`` (the rendered
47
+ # message) into __init__, which raises TypeError for the multi-arg
48
+ # signature above. Replay the original constructor args instead so
49
+ # instances survive pickle (multiprocessing, concurrent.futures).
50
+ return (self.__class__, (self.tool_name, self.param_name, self.issue, self.suggestion))
51
+
45
52
 
46
53
  @stable
47
54
  class ToolExecutionError(SelectoolsError):
@@ -65,6 +72,11 @@ class ToolExecutionError(SelectoolsError):
65
72
 
66
73
  super().__init__(message)
67
74
 
75
+ def __reduce__(self) -> Tuple[Any, ...]:
76
+ # See ToolValidationError.__reduce__ — requires ``error`` itself to
77
+ # be picklable, which holds for built-in and well-behaved exceptions.
78
+ return (self.__class__, (self.tool_name, self.error, self.params))
79
+
68
80
 
69
81
  @stable
70
82
  class ProviderConfigurationError(SelectoolsError):
@@ -89,6 +101,10 @@ class ProviderConfigurationError(SelectoolsError):
89
101
 
90
102
  super().__init__(message)
91
103
 
104
+ def __reduce__(self) -> Tuple[Any, ...]:
105
+ # See ToolValidationError.__reduce__.
106
+ return (self.__class__, (self.provider_name, self.missing_config, self.env_var))
107
+
92
108
 
93
109
  @stable
94
110
  class MemoryLimitExceededError(SelectoolsError):
@@ -117,6 +133,10 @@ class MemoryLimitExceededError(SelectoolsError):
117
133
 
118
134
  super().__init__(message)
119
135
 
136
+ def __reduce__(self) -> Tuple[Any, ...]:
137
+ # See ToolValidationError.__reduce__.
138
+ return (self.__class__, (self.current, self.limit, self.limit_type))
139
+
120
140
 
121
141
  @stable
122
142
  class GraphExecutionError(SelectoolsError):
@@ -147,6 +167,11 @@ class GraphExecutionError(SelectoolsError):
147
167
 
148
168
  super().__init__(message)
149
169
 
170
+ def __reduce__(self) -> Tuple[Any, ...]:
171
+ # See ToolValidationError.__reduce__ — requires ``error`` itself to
172
+ # be picklable, which holds for built-in and well-behaved exceptions.
173
+ return (self.__class__, (self.graph_name, self.node_name, self.error, self.step))
174
+
150
175
 
151
176
  @stable
152
177
  class BudgetExceededError(SelectoolsError):
@@ -87,6 +87,12 @@ class GuardrailError(Exception):
87
87
  self.reason = reason
88
88
  super().__init__(f"Guardrail '{guardrail_name}' blocked: {reason}")
89
89
 
90
+ def __reduce__(self) -> "tuple[type, tuple[str, str]]":
91
+ # Default exception pickling replays the rendered message into the
92
+ # two-arg __init__ and fails; reconstruct from the original args
93
+ # (same fix family as selectools.exceptions, PR #100).
94
+ return (self.__class__, (self.guardrail_name, self.reason))
95
+
90
96
 
91
97
  __stability__ = "stable"
92
98
 
@@ -30,6 +30,8 @@ from .stability import beta
30
30
 
31
31
  _B64_MARKER = "b64:"
32
32
 
33
+ _WRITE_MODES = ("upsert", "update")
34
+
33
35
 
34
36
  def _validate_key(key: str) -> None:
35
37
  """Reject keys that could cause collisions or storage problems."""
@@ -86,6 +88,12 @@ class SupabaseKnowledgeBackend:
86
88
  table_name: Postgres table to use. Default: ``"selectools_knowledge"``.
87
89
  key_column: Primary-key column name. Default: ``"key"``.
88
90
  data_column: Text column holding the blob. Default: ``"data"``.
91
+ write_mode: How ``save_bytes`` writes the row. ``"upsert"`` (default)
92
+ issues ``INSERT ... ON CONFLICT DO UPDATE`` and is right for the
93
+ dedicated table below. ``"update"`` issues a plain
94
+ ``UPDATE ... WHERE key_column = key`` and requires the row to
95
+ already exist — use it when reusing a table you don't fully
96
+ control (see the warning below).
89
97
 
90
98
  Required table DDL (run once in your Supabase project)::
91
99
 
@@ -95,7 +103,10 @@ class SupabaseKnowledgeBackend:
95
103
  updated_at timestamptz not null default now()
96
104
  );
97
105
 
98
- To reuse an existing table (e.g. Sheriff's ``users.memory_text``)::
106
+ To reuse an existing table (e.g. Sheriff's ``users.memory_text``), pass
107
+ ``write_mode="update"`` — the row must already exist (Sheriff's auth
108
+ trigger guarantees it; sheriff#302 ships exactly this update-by-key
109
+ pattern in production)::
99
110
 
100
111
  SupabaseKnowledgeBackend(
101
112
  client,
@@ -103,7 +114,20 @@ class SupabaseKnowledgeBackend:
103
114
  table_name="users",
104
115
  key_column="user_id",
105
116
  data_column="memory_text",
117
+ write_mode="update",
106
118
  )
119
+
120
+ .. warning::
121
+
122
+ Do **not** use the default ``write_mode="upsert"`` against a table
123
+ that has NOT NULL columns without defaults beyond the key and data
124
+ columns (e.g. ``users.email``). PostgREST translates the upsert to
125
+ ``INSERT ... ON CONFLICT DO UPDATE``, and Postgres validates NOT NULL
126
+ constraints on the *proposed insert tuple* **before** conflict
127
+ arbitration — so the partial-column payload raises
128
+ ``not_null_violation`` (error 23502) on every save, even when the
129
+ row already exists and the ``DO UPDATE`` half would never touch the
130
+ offending column. ``write_mode="update"`` sidesteps this entirely.
107
131
  """
108
132
 
109
133
  def __init__(
@@ -113,6 +137,7 @@ class SupabaseKnowledgeBackend:
113
137
  table_name: str = "selectools_knowledge",
114
138
  key_column: str = "key",
115
139
  data_column: str = "data",
140
+ write_mode: str = "upsert",
116
141
  ) -> None:
117
142
  try:
118
143
  import supabase as supabase_lib # type: ignore[import-untyped] # noqa: F401
@@ -123,11 +148,14 @@ class SupabaseKnowledgeBackend:
123
148
  ) from exc
124
149
 
125
150
  _validate_key(key)
151
+ if write_mode not in _WRITE_MODES:
152
+ raise ValueError(f"write_mode must be one of {_WRITE_MODES}, got {write_mode!r}")
126
153
  self._client = client
127
154
  self._key = key
128
155
  self._table = table_name
129
156
  self._key_column = key_column
130
157
  self._data_column = data_column
158
+ self._write_mode = write_mode
131
159
 
132
160
  def load_bytes(self) -> Optional[bytes]:
133
161
  """Return the stored blob for this key, or ``None`` if absent."""
@@ -146,13 +174,37 @@ class SupabaseKnowledgeBackend:
146
174
  return _decode_text(str(value))
147
175
 
148
176
  def save_bytes(self, data: bytes) -> None:
149
- """Upsert *data* under this key (idempotent on repeated saves)."""
177
+ """Write *data* under this key (idempotent on repeated saves).
178
+
179
+ ``write_mode="upsert"`` inserts-or-updates the row;
180
+ ``write_mode="update"`` updates an existing row and raises
181
+ ``RuntimeError`` when no row matches the key.
182
+ """
150
183
  from datetime import datetime, timezone
151
184
 
185
+ encoded = _encode_text(data)
186
+ updated_at = datetime.now(timezone.utc).isoformat()
187
+
188
+ if self._write_mode == "update":
189
+ response = (
190
+ self._client.table(self._table)
191
+ .update({self._data_column: encoded, "updated_at": updated_at})
192
+ .eq(self._key_column, self._key)
193
+ .execute()
194
+ )
195
+ if not response.data:
196
+ raise RuntimeError(
197
+ f"write_mode='update' matched no row in {self._table!r} for "
198
+ f"{self._key_column}={self._key!r} — the row must pre-exist "
199
+ "in update mode (create it first, or use write_mode='upsert' "
200
+ "with a dedicated table)"
201
+ )
202
+ return
203
+
152
204
  payload = {
153
205
  self._key_column: self._key,
154
- self._data_column: _encode_text(data),
155
- "updated_at": datetime.now(timezone.utc).isoformat(),
206
+ self._data_column: encoded,
207
+ "updated_at": updated_at,
156
208
  }
157
209
  self._client.table(self._table).upsert(payload, on_conflict=self._key_column).execute()
158
210
 
@@ -60,12 +60,14 @@ _ROLE_MARKER_RE = re.compile(
60
60
 
61
61
  # Speaker-label prefix at line start (``Assistant: I'll wire the money``).
62
62
  # Inside a remembered-context block this reads as a forged conversation turn.
63
- # Matches ASCII and fullwidth (U+FF1A) colons. Indentation is unbounded
63
+ # Matches ASCII and fullwidth (U+FF1A) colons. Covers English and
64
+ # Brazilian-Portuguese labels (Usuário/Sistema/Assistente/Humano) so
65
+ # pt-BR consumers (Sheriff) need no supplement. Indentation is unbounded
64
66
  # (speaker labels are not markdown structure, so no 0-3 cap) but restricted
65
67
  # to same-line whitespace (`[ \t]`, not `\s`) so a match never spans
66
68
  # newlines (review finding PR #84).
67
69
  _SPEAKER_LABEL_RE = re.compile(
68
- r"^([ \t]*)(User|Assistant|System|Human)([ \t]*)([::])",
70
+ r"^([ \t]*)(User|Assistant|System|Human|Usu[aá]rio|Sistema|Assistente|Humano)([ \t]*)([::])",
69
71
  re.MULTILINE | re.IGNORECASE,
70
72
  )
71
73
 
@@ -81,6 +83,25 @@ _FENCE_RE = re.compile(r"^([ \t]{0,3})(`{3,}|~{3,})", re.MULTILINE)
81
83
  # backtick, U+02DC SMALL TILDE for the tilde.
82
84
  _FENCE_SUBSTITUTES = {"`": "ˋ", "~": "˜"}
83
85
 
86
+ # Whitespace already sitting after an opening bracket inside a matched role
87
+ # marker. Collapsed before the defang space is inserted so the rewrite is
88
+ # canonical: ``[INST]`` and ``[ INST]`` both become ``[ INST]``, and
89
+ # re-running the sanitizer is a no-op (idempotency — see _defang_role_marker).
90
+ _POST_BRACKET_WS_RE = re.compile(r"(?<=[\[<])[ \t]+")
91
+
92
+
93
+ def _defang_role_marker(match: "re.Match[str]") -> str:
94
+ """Rewrite a role marker into its canonical defanged form.
95
+
96
+ Canonical means a fixed point: applying ``defang_delimiters`` to its own
97
+ output must change nothing (sheriff#302 review note 9 — the previous
98
+ naive ``replace("[", "[ ")`` added one more space per pass because
99
+ ``_ROLE_MARKER_RE``'s ``\\s*`` also matches the already-defanged form,
100
+ causing per-turn churn in any pipeline that re-sanitizes stored text).
101
+ """
102
+ collapsed = _POST_BRACKET_WS_RE.sub("", match.group(0))
103
+ return collapsed.replace("[", "[ ").replace("<", "< ")
104
+
84
105
 
85
106
  @beta
86
107
  def defang_delimiters(text: str) -> str:
@@ -89,6 +110,12 @@ def defang_delimiters(text: str) -> str:
89
110
  Conservative by design: every rewrite keeps the content human-readable
90
111
  and meaning-preserving; only the *structural* interpretation is broken.
91
112
 
113
+ Idempotent: every rewrite produces a fixed point, so
114
+ ``defang_delimiters(defang_delimiters(x)) == defang_delimiters(x)``.
115
+ Pipelines that re-sanitize already-stored text on every turn (e.g.
116
+ Sheriff's per-message knowledge pass) must not churn entries with one
117
+ extra space per pass (sheriff#302 review note 9).
118
+
92
119
  What is defanged, and why:
93
120
 
94
121
  1. ``--- Label ---`` and ``--- Label`` lines become ``— Label —`` /
@@ -136,8 +163,12 @@ def defang_delimiters(text: str) -> str:
136
163
  s = text or ""
137
164
  s = _DELIMITER_RE.sub(r"\1— \2 —", s)
138
165
  s = _HALF_DELIMITER_RE.sub(r"\1— \2", s)
139
- s = _ROLE_MARKER_RE.sub(lambda m: m.group(0).replace("[", "[ ").replace("<", "< "), s)
140
- s = _SPEAKER_LABEL_RE.sub(r"\1\2\3 \4", s)
166
+ s = _ROLE_MARKER_RE.sub(_defang_role_marker, s)
167
+ # Canonical single space before the colon (``\3``, any pre-existing
168
+ # whitespace, is dropped rather than kept): ``User:`` and ``User :``
169
+ # both become ``User :``, so re-sanitizing is a no-op (sheriff#302
170
+ # review note 9 — keeping ``\3`` added one space per pass).
171
+ s = _SPEAKER_LABEL_RE.sub(r"\1\2 \4", s)
141
172
  s = _FENCE_RE.sub(lambda m: m.group(1) + _FENCE_SUBSTITUTES[m.group(2)[0]] * len(m.group(2)), s)
142
173
  return s
143
174
 
@@ -50,6 +50,21 @@ class LoopDetectedError(SelectoolsError):
50
50
  self.details = details
51
51
  super().__init__(message)
52
52
 
53
+ def __reduce__(self) -> "tuple[Any, ...]":
54
+ # Keyword-only args defeat default exception pickling; rebuild via a
55
+ # partial-style tuple (same fix family as selectools.exceptions).
56
+ return (
57
+ _rebuild_loop_detected_error,
58
+ (self.args[0] if self.args else "", self.detector, self.details),
59
+ )
60
+
61
+
62
+ def _rebuild_loop_detected_error(
63
+ message: str, detector: str, details: Dict[str, Any]
64
+ ) -> "LoopDetectedError":
65
+ """Unpickle helper for :class:`LoopDetectedError` (kw-only ctor)."""
66
+ return LoopDetectedError(message, detector=detector, details=details)
67
+
53
68
 
54
69
  @beta
55
70
  @dataclass