selectools 0.16.4__tar.gz → 0.16.6__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 (124) hide show
  1. {selectools-0.16.4/src/selectools.egg-info → selectools-0.16.6}/PKG-INFO +20 -16
  2. {selectools-0.16.4 → selectools-0.16.6}/README.md +19 -15
  3. {selectools-0.16.4 → selectools-0.16.6}/pyproject.toml +12 -2
  4. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/__init__.py +17 -4
  5. selectools-0.16.6/src/selectools/agent/_lifecycle.py +141 -0
  6. selectools-0.16.6/src/selectools/agent/_memory_manager.py +146 -0
  7. selectools-0.16.6/src/selectools/agent/_provider_caller.py +459 -0
  8. selectools-0.16.6/src/selectools/agent/_tool_executor.py +954 -0
  9. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/agent/config.py +1 -0
  10. selectools-0.16.6/src/selectools/agent/core.py +1448 -0
  11. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/models.py +161 -148
  12. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/observer.py +380 -1
  13. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/__init__.py +5 -0
  14. selectools-0.16.4/src/selectools/providers/openai_provider.py → selectools-0.16.6/src/selectools/providers/_openai_compat.py +218 -221
  15. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/gemini_provider.py +68 -10
  16. selectools-0.16.6/src/selectools/providers/ollama_provider.py +126 -0
  17. selectools-0.16.6/src/selectools/providers/openai_provider.py +86 -0
  18. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/base.py +4 -0
  19. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/decorators.py +4 -0
  20. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/trace.py +31 -22
  21. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/types.py +8 -1
  22. {selectools-0.16.4 → selectools-0.16.6/src/selectools.egg-info}/PKG-INFO +20 -16
  23. {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/SOURCES.txt +9 -0
  24. selectools-0.16.6/tests/test_architecture.py +242 -0
  25. selectools-0.16.6/tests/test_async_observers.py +374 -0
  26. selectools-0.16.6/tests/test_phase1_design_patterns.py +694 -0
  27. selectools-0.16.6/tests/test_terminal_actions.py +410 -0
  28. selectools-0.16.4/src/selectools/agent/core.py +0 -3128
  29. selectools-0.16.4/src/selectools/providers/ollama_provider.py +0 -456
  30. {selectools-0.16.4 → selectools-0.16.6}/LICENSE +0 -0
  31. {selectools-0.16.4 → selectools-0.16.6}/setup.cfg +0 -0
  32. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/agent/__init__.py +0 -0
  33. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/analytics.py +0 -0
  34. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/audit.py +0 -0
  35. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/cache.py +0 -0
  36. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/cache_redis.py +0 -0
  37. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/cli.py +0 -0
  38. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/coherence.py +0 -0
  39. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/__init__.py +0 -0
  40. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/anthropic.py +0 -0
  41. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/cohere.py +0 -0
  42. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/gemini.py +0 -0
  43. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/openai.py +0 -0
  44. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/embeddings/provider.py +0 -0
  45. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/entity_memory.py +0 -0
  46. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/env.py +0 -0
  47. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/exceptions.py +0 -0
  48. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/__init__.py +0 -0
  49. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/base.py +0 -0
  50. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/format.py +0 -0
  51. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/length.py +0 -0
  52. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/pii.py +0 -0
  53. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/pipeline.py +0 -0
  54. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/topic.py +0 -0
  55. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/guardrails/toxicity.py +0 -0
  56. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/knowledge.py +0 -0
  57. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/knowledge_graph.py +0 -0
  58. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/memory.py +0 -0
  59. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/parser.py +0 -0
  60. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/policy.py +0 -0
  61. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/pricing.py +0 -0
  62. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/prompt.py +0 -0
  63. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/anthropic_provider.py +0 -0
  64. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/base.py +0 -0
  65. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/fallback.py +0 -0
  66. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/providers/stubs.py +0 -0
  67. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/__init__.py +0 -0
  68. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/bm25.py +0 -0
  69. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/chunking.py +0 -0
  70. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/hybrid.py +0 -0
  71. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/loaders.py +0 -0
  72. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/reranker.py +0 -0
  73. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/__init__.py +0 -0
  74. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/chroma.py +0 -0
  75. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/memory.py +0 -0
  76. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/pinecone.py +0 -0
  77. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/stores/sqlite.py +0 -0
  78. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/tools.py +0 -0
  79. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/rag/vector_store.py +0 -0
  80. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/security.py +0 -0
  81. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/sessions.py +0 -0
  82. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/structured.py +0 -0
  83. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/__init__.py +0 -0
  84. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/data_tools.py +0 -0
  85. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/datetime_tools.py +0 -0
  86. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/file_tools.py +0 -0
  87. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/memory_tools.py +0 -0
  88. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/text_tools.py +0 -0
  89. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/toolbox/web_tools.py +0 -0
  90. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/__init__.py +0 -0
  91. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/loader.py +0 -0
  92. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/tools/registry.py +0 -0
  93. {selectools-0.16.4 → selectools-0.16.6}/src/selectools/usage.py +0 -0
  94. {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/dependency_links.txt +0 -0
  95. {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/entry_points.txt +0 -0
  96. {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/requires.txt +0 -0
  97. {selectools-0.16.4 → selectools-0.16.6}/src/selectools.egg-info/top_level.txt +0 -0
  98. {selectools-0.16.4 → selectools-0.16.6}/tests/test_audit.py +0 -0
  99. {selectools-0.16.4 → selectools-0.16.6}/tests/test_cache.py +0 -0
  100. {selectools-0.16.4 → selectools-0.16.6}/tests/test_cache_redis.py +0 -0
  101. {selectools-0.16.4 → selectools-0.16.6}/tests/test_cli.py +0 -0
  102. {selectools-0.16.4 → selectools-0.16.6}/tests/test_coherence.py +0 -0
  103. {selectools-0.16.4 → selectools-0.16.6}/tests/test_consolidation_regression.py +0 -0
  104. {selectools-0.16.4 → selectools-0.16.6}/tests/test_entity_memory.py +0 -0
  105. {selectools-0.16.4 → selectools-0.16.6}/tests/test_env.py +0 -0
  106. {selectools-0.16.4 → selectools-0.16.6}/tests/test_guardrails.py +0 -0
  107. {selectools-0.16.4 → selectools-0.16.6}/tests/test_knowledge.py +0 -0
  108. {selectools-0.16.4 → selectools-0.16.6}/tests/test_knowledge_graph.py +0 -0
  109. {selectools-0.16.4 → selectools-0.16.6}/tests/test_memory.py +0 -0
  110. {selectools-0.16.4 → selectools-0.16.6}/tests/test_memory_async.py +0 -0
  111. {selectools-0.16.4 → selectools-0.16.6}/tests/test_memory_boundary.py +0 -0
  112. {selectools-0.16.4 → selectools-0.16.6}/tests/test_memory_integration.py +0 -0
  113. {selectools-0.16.4 → selectools-0.16.6}/tests/test_parser.py +0 -0
  114. {selectools-0.16.4 → selectools-0.16.6}/tests/test_policy.py +0 -0
  115. {selectools-0.16.4 → selectools-0.16.6}/tests/test_prompt.py +0 -0
  116. {selectools-0.16.4 → selectools-0.16.6}/tests/test_routing_mode.py +0 -0
  117. {selectools-0.16.4 → selectools-0.16.6}/tests/test_security.py +0 -0
  118. {selectools-0.16.4 → selectools-0.16.6}/tests/test_sessions.py +0 -0
  119. {selectools-0.16.4 → selectools-0.16.6}/tests/test_sessions_edge_cases.py +0 -0
  120. {selectools-0.16.4 → selectools-0.16.6}/tests/test_sessions_redis.py +0 -0
  121. {selectools-0.16.4 → selectools-0.16.6}/tests/test_structured.py +0 -0
  122. {selectools-0.16.4 → selectools-0.16.6}/tests/test_summarize_on_trim.py +0 -0
  123. {selectools-0.16.4 → selectools-0.16.6}/tests/test_trace.py +0 -0
  124. {selectools-0.16.4 → selectools-0.16.6}/tests/test_v016_regression.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: selectools
3
- Version: 0.16.4
3
+ Version: 0.16.6
4
4
  Summary: Production-ready AI agents with tool calling, structured output, execution traces, and RAG. Provider-agnostic (OpenAI, Anthropic, Gemini, Ollama) with fallback chains, batch processing, tool policies, streaming, caching, and cost tracking.
5
5
  Author-email: John <johnnichev@gmail.com>
6
6
  License-Expression: LGPL-3.0-or-later
@@ -52,23 +52,27 @@ Dynamic: license-file
52
52
  [![PyPI version](https://badge.fury.io/py/selectools.svg)](https://badge.fury.io/py/selectools)
53
53
  [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://johnnichev.github.io/selectools)
54
54
  [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
55
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
55
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
56
56
 
57
57
  **Production-ready AI agents with tool calling, RAG, and hybrid search.** Connect LLMs to your Python functions, embed and search your documents with vector + keyword fusion, stream responses in real time, and dynamically manage tools at runtime. Works with OpenAI, Anthropic, Gemini, and Ollama. Tracks costs automatically.
58
58
 
59
- ## What's New in v0.16.0
59
+ ## What's New in v0.16.6
60
60
 
61
- **Memory & Persistence** — Five new features for durable conversation state:
61
+ **Gemini 3.x thought_signature crash fix** — `thought_signature` from Gemini 3.x is opaque binary (protobuf/hash), not UTF-8 text. The previous code crashed with `UnicodeDecodeError` on non-UTF-8 bytes. Now uses base64 for lossless binary round-trip across all code paths.
62
62
 
63
- - **Persistent Sessions** — `SessionStore` protocol with 3 backends (JSON file, SQLite, Redis). Auto-save/load via `AgentConfig(session_store=store, session_id="user-123")`.
64
- - **Summarize-on-Trim** — LLM-generated summaries of trimmed messages, injected as system context. No more silent context loss.
65
- - **Entity Memory** — Auto-extract named entities (person, org, project) across turns with LRU-pruned registry and system prompt injection.
66
- - **Knowledge Graph** — Relationship triple extraction with in-memory and SQLite storage. Query-relevant triples auto-injected into prompts.
67
- - **Cross-Session Knowledge** — Daily logs + persistent facts with auto-registered `remember` tool. Give your agent durable memory across conversations.
68
- - **182 new tests** (total: 1462 with v0.16.3 refactoring)
63
+ - **1642 tests** — 2 new regression tests for non-UTF-8 binary thought_signature round-trip
69
64
 
70
65
  > Full changelog: [CHANGELOG.md](https://github.com/johnnichev/selectools/blob/main/CHANGELOG.md)
71
66
 
67
+ <details>
68
+ <summary><strong>v0.16.x highlights</strong></summary>
69
+
70
+ - **v0.16.5**: Design Patterns & Code Quality — terminal actions, async observers, Gemini 3.x thought signatures, agent decomposition, hooks deprecated
71
+ - **v0.16.4**: Parallel execution safety — coherence + screening in parallel, guardrail immutability, streaming usage tracking
72
+ - **v0.16.0**: Memory & Persistence — persistent sessions (3 backends), summarize-on-trim, entity memory, knowledge graph
73
+
74
+ </details>
75
+
72
76
  <details>
73
77
  <summary><strong>v0.15.x highlights</strong></summary>
74
78
 
@@ -80,7 +84,7 @@ Dynamic: license-file
80
84
  <summary><strong>v0.14.x highlights</strong></summary>
81
85
 
82
86
  - **v0.14.1**: Critical streaming fix — 13 bugs fixed across all providers; 141 new tests (total: 1100)
83
- - **v0.14.0**: AgentObserver Protocol (15 events), 145 models with March 2026 pricing, OpenAI `max_completion_tokens` auto-detection, 11 bug fixes
87
+ - **v0.14.0**: AgentObserver Protocol (25 events), 145 models with March 2026 pricing, OpenAI `max_completion_tokens` auto-detection, 11 bug fixes
84
88
 
85
89
  </details>
86
90
 
@@ -109,7 +113,7 @@ Dynamic: license-file
109
113
  | **Entity Memory** | LLM-based entity extraction with deduplication, LRU pruning, and system prompt injection. |
110
114
  | **Knowledge Graph** | Relationship triple extraction with in-memory and SQLite storage and keyword-based querying. |
111
115
  | **Cross-Session Knowledge** | Daily logs + persistent facts with auto-registered `remember` tool. |
112
- | **AgentObserver Protocol** | 19-event lifecycle observer with `run_id`/`call_id` correlation. Built-in `LoggingObserver` for structured JSON logs. |
116
+ | **AgentObserver Protocol** | 25-event lifecycle observer with `run_id`/`call_id` correlation. Built-in `LoggingObserver` for structured JSON logs. |
113
117
  | **Production Hardened** | Retries with backoff, per-tool timeouts, iteration caps, cost warnings, observability hooks + observers. |
114
118
  | **Library-First** | Not a framework. No magic globals, no hidden state. Use as much or as little as you need. |
115
119
 
@@ -133,9 +137,9 @@ Dynamic: license-file
133
137
  - **Entity Memory**: LLM-based named entity extraction and tracking
134
138
  - **Knowledge Graph**: Triple extraction with in-memory and SQLite storage
135
139
  - **Cross-Session Knowledge**: Daily logs + persistent memory with `remember` tool
136
- - **37 Examples**: RAG, hybrid search, streaming, structured output, traces, batch, policy, observer, guardrails, audit, sessions, entity memory, knowledge graph, and more
140
+ - **38 Examples**: RAG, hybrid search, streaming, structured output, traces, batch, policy, observer, guardrails, audit, sessions, entity memory, knowledge graph, and more
137
141
  - **AgentObserver Protocol**: 25 lifecycle events with `run_id` correlation, `LoggingObserver`, OTel export
138
- - **1462 Tests**: Unit, integration, regression, and E2E with real API calls
142
+ - **1642 Tests**: Unit, integration, regression, and E2E with real API calls
139
143
 
140
144
  ## Install
141
145
 
@@ -457,7 +461,7 @@ agent = Agent(
457
461
  )
458
462
  ```
459
463
 
460
- 15 lifecycle events: run, LLM, tool, iteration, batch, policy, structured output, fallback, retry, memory trim. See `observer.py` for full reference.
464
+ 25 lifecycle events: run, LLM, tool, iteration, batch, policy, structured output, fallback, retry, memory trim, guardrail, coherence, screening, session, entity, KG. See `observer.py` for full reference.
461
465
 
462
466
  ### E2E Streaming & Parallel Execution
463
467
 
@@ -627,7 +631,7 @@ Examples are numbered by difficulty. Start from 01 and work your way up.
627
631
  |---|---|---|---|
628
632
  | 01 | `01_hello_world.py` | First agent, `@tool`, `ask()` | No |
629
633
  | 02 | `02_search_weather.py` | ToolRegistry, multiple tools | No |
630
- | 03 | `03_toolbox.py` | 22 pre-built tools (file, data, text, datetime) | No |
634
+ | 03 | `03_toolbox.py` | 24 pre-built tools (file, data, text, datetime, web) | No |
631
635
  | 04 | `04_conversation_memory.py` | Multi-turn memory | Yes |
632
636
  | 05 | `05_cost_tracking.py` | Token counting, cost warnings | Yes |
633
637
  | 06 | `06_async_agent.py` | `arun()`, concurrent agents, FastAPI | Yes |
@@ -3,23 +3,27 @@
3
3
  [![PyPI version](https://badge.fury.io/py/selectools.svg)](https://badge.fury.io/py/selectools)
4
4
  [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://johnnichev.github.io/selectools)
5
5
  [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
6
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
6
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
7
7
 
8
8
  **Production-ready AI agents with tool calling, RAG, and hybrid search.** Connect LLMs to your Python functions, embed and search your documents with vector + keyword fusion, stream responses in real time, and dynamically manage tools at runtime. Works with OpenAI, Anthropic, Gemini, and Ollama. Tracks costs automatically.
9
9
 
10
- ## What's New in v0.16.0
10
+ ## What's New in v0.16.6
11
11
 
12
- **Memory & Persistence** — Five new features for durable conversation state:
12
+ **Gemini 3.x thought_signature crash fix** — `thought_signature` from Gemini 3.x is opaque binary (protobuf/hash), not UTF-8 text. The previous code crashed with `UnicodeDecodeError` on non-UTF-8 bytes. Now uses base64 for lossless binary round-trip across all code paths.
13
13
 
14
- - **Persistent Sessions** — `SessionStore` protocol with 3 backends (JSON file, SQLite, Redis). Auto-save/load via `AgentConfig(session_store=store, session_id="user-123")`.
15
- - **Summarize-on-Trim** — LLM-generated summaries of trimmed messages, injected as system context. No more silent context loss.
16
- - **Entity Memory** — Auto-extract named entities (person, org, project) across turns with LRU-pruned registry and system prompt injection.
17
- - **Knowledge Graph** — Relationship triple extraction with in-memory and SQLite storage. Query-relevant triples auto-injected into prompts.
18
- - **Cross-Session Knowledge** — Daily logs + persistent facts with auto-registered `remember` tool. Give your agent durable memory across conversations.
19
- - **182 new tests** (total: 1462 with v0.16.3 refactoring)
14
+ - **1642 tests** — 2 new regression tests for non-UTF-8 binary thought_signature round-trip
20
15
 
21
16
  > Full changelog: [CHANGELOG.md](https://github.com/johnnichev/selectools/blob/main/CHANGELOG.md)
22
17
 
18
+ <details>
19
+ <summary><strong>v0.16.x highlights</strong></summary>
20
+
21
+ - **v0.16.5**: Design Patterns & Code Quality — terminal actions, async observers, Gemini 3.x thought signatures, agent decomposition, hooks deprecated
22
+ - **v0.16.4**: Parallel execution safety — coherence + screening in parallel, guardrail immutability, streaming usage tracking
23
+ - **v0.16.0**: Memory & Persistence — persistent sessions (3 backends), summarize-on-trim, entity memory, knowledge graph
24
+
25
+ </details>
26
+
23
27
  <details>
24
28
  <summary><strong>v0.15.x highlights</strong></summary>
25
29
 
@@ -31,7 +35,7 @@
31
35
  <summary><strong>v0.14.x highlights</strong></summary>
32
36
 
33
37
  - **v0.14.1**: Critical streaming fix — 13 bugs fixed across all providers; 141 new tests (total: 1100)
34
- - **v0.14.0**: AgentObserver Protocol (15 events), 145 models with March 2026 pricing, OpenAI `max_completion_tokens` auto-detection, 11 bug fixes
38
+ - **v0.14.0**: AgentObserver Protocol (25 events), 145 models with March 2026 pricing, OpenAI `max_completion_tokens` auto-detection, 11 bug fixes
35
39
 
36
40
  </details>
37
41
 
@@ -60,7 +64,7 @@
60
64
  | **Entity Memory** | LLM-based entity extraction with deduplication, LRU pruning, and system prompt injection. |
61
65
  | **Knowledge Graph** | Relationship triple extraction with in-memory and SQLite storage and keyword-based querying. |
62
66
  | **Cross-Session Knowledge** | Daily logs + persistent facts with auto-registered `remember` tool. |
63
- | **AgentObserver Protocol** | 19-event lifecycle observer with `run_id`/`call_id` correlation. Built-in `LoggingObserver` for structured JSON logs. |
67
+ | **AgentObserver Protocol** | 25-event lifecycle observer with `run_id`/`call_id` correlation. Built-in `LoggingObserver` for structured JSON logs. |
64
68
  | **Production Hardened** | Retries with backoff, per-tool timeouts, iteration caps, cost warnings, observability hooks + observers. |
65
69
  | **Library-First** | Not a framework. No magic globals, no hidden state. Use as much or as little as you need. |
66
70
 
@@ -84,9 +88,9 @@
84
88
  - **Entity Memory**: LLM-based named entity extraction and tracking
85
89
  - **Knowledge Graph**: Triple extraction with in-memory and SQLite storage
86
90
  - **Cross-Session Knowledge**: Daily logs + persistent memory with `remember` tool
87
- - **37 Examples**: RAG, hybrid search, streaming, structured output, traces, batch, policy, observer, guardrails, audit, sessions, entity memory, knowledge graph, and more
91
+ - **38 Examples**: RAG, hybrid search, streaming, structured output, traces, batch, policy, observer, guardrails, audit, sessions, entity memory, knowledge graph, and more
88
92
  - **AgentObserver Protocol**: 25 lifecycle events with `run_id` correlation, `LoggingObserver`, OTel export
89
- - **1462 Tests**: Unit, integration, regression, and E2E with real API calls
93
+ - **1642 Tests**: Unit, integration, regression, and E2E with real API calls
90
94
 
91
95
  ## Install
92
96
 
@@ -408,7 +412,7 @@ agent = Agent(
408
412
  )
409
413
  ```
410
414
 
411
- 15 lifecycle events: run, LLM, tool, iteration, batch, policy, structured output, fallback, retry, memory trim. See `observer.py` for full reference.
415
+ 25 lifecycle events: run, LLM, tool, iteration, batch, policy, structured output, fallback, retry, memory trim, guardrail, coherence, screening, session, entity, KG. See `observer.py` for full reference.
412
416
 
413
417
  ### E2E Streaming & Parallel Execution
414
418
 
@@ -578,7 +582,7 @@ Examples are numbered by difficulty. Start from 01 and work your way up.
578
582
  |---|---|---|---|
579
583
  | 01 | `01_hello_world.py` | First agent, `@tool`, `ask()` | No |
580
584
  | 02 | `02_search_weather.py` | ToolRegistry, multiple tools | No |
581
- | 03 | `03_toolbox.py` | 22 pre-built tools (file, data, text, datetime) | No |
585
+ | 03 | `03_toolbox.py` | 24 pre-built tools (file, data, text, datetime, web) | No |
582
586
  | 04 | `04_conversation_memory.py` | Multi-turn memory | Yes |
583
587
  | 05 | `05_cost_tracking.py` | Token counting, cost warnings | Yes |
584
588
  | 06 | `06_async_agent.py` | `arun()`, concurrent agents, FastAPI | Yes |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "selectools"
7
- version = "0.16.4"
7
+ version = "0.16.6"
8
8
  description = "Production-ready AI agents with tool calling, structured output, execution traces, and RAG. Provider-agnostic (OpenAI, Anthropic, Gemini, Ollama) with fallback chains, batch processing, tool policies, streaming, caching, and cost tracking."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -96,7 +96,7 @@ use_parentheses = true
96
96
  ensure_newline_before_comments = true
97
97
 
98
98
  [tool.mypy]
99
- python_version = "3.9"
99
+ python_version = "3.13"
100
100
  warn_return_any = true
101
101
  warn_unused_configs = true
102
102
  disallow_untyped_defs = true
@@ -108,6 +108,16 @@ exclude = [
108
108
  "dist/",
109
109
  ]
110
110
 
111
+ [[tool.mypy.overrides]]
112
+ module = [
113
+ "selectools.agent._tool_executor",
114
+ "selectools.agent._provider_caller",
115
+ "selectools.agent._lifecycle",
116
+ "selectools.agent._memory_manager",
117
+ "selectools.providers._openai_compat",
118
+ ]
119
+ disable_error_code = ["attr-defined"]
120
+
111
121
  [tool.bandit]
112
122
  exclude_dirs = ["tests", "examples", "scripts"]
113
123
  skips = ["B101", "B601", "B602"] # Skip assert_used, shell, and subprocess checks
@@ -1,6 +1,6 @@
1
1
  """Public exports for the selectools package."""
2
2
 
3
- __version__ = "0.16.4"
3
+ __version__ = "0.16.6"
4
4
 
5
5
  # Import submodules (lazy loading for optional dependencies)
6
6
  from . import embeddings, guardrails, models, rag, toolbox
@@ -39,8 +39,18 @@ from .knowledge_graph import (
39
39
  TripleStore,
40
40
  )
41
41
  from .memory import ConversationMemory
42
- from .models import ALL_MODELS, MODELS_BY_ID, Anthropic, Cohere, Gemini, ModelInfo, Ollama, OpenAI
43
- from .observer import AgentObserver, LoggingObserver
42
+ from .models import (
43
+ ALL_MODELS,
44
+ MODELS_BY_ID,
45
+ Anthropic,
46
+ Cohere,
47
+ Gemini,
48
+ ModelInfo,
49
+ ModelType,
50
+ Ollama,
51
+ OpenAI,
52
+ )
53
+ from .observer import AgentObserver, AsyncAgentObserver, LoggingObserver
44
54
  from .parser import ToolCallParser
45
55
  from .policy import PolicyDecision, PolicyResult, ToolPolicy
46
56
  from .pricing import PRICING, calculate_cost, calculate_embedding_cost, get_model_pricing
@@ -60,7 +70,7 @@ from .sessions import (
60
70
  )
61
71
  from .structured import ResponseFormat
62
72
  from .tools import Tool, ToolParameter, ToolRegistry, tool
63
- from .trace import AgentTrace, TraceStep
73
+ from .trace import AgentTrace, StepType, TraceStep
64
74
  from .types import AgentResult, Message, Role, ToolCall
65
75
  from .usage import AgentUsage, UsageStats
66
76
 
@@ -104,6 +114,7 @@ __all__ = [
104
114
  # Model Registry
105
115
  "models",
106
116
  "ModelInfo",
117
+ "ModelType",
107
118
  "ALL_MODELS",
108
119
  "MODELS_BY_ID",
109
120
  "OpenAI",
@@ -124,8 +135,10 @@ __all__ = [
124
135
  "ResponseFormat",
125
136
  # Observability
126
137
  "AgentObserver",
138
+ "AsyncAgentObserver",
127
139
  "LoggingObserver",
128
140
  "AgentTrace",
141
+ "StepType",
129
142
  "TraceStep",
130
143
  # Guardrails
131
144
  "guardrails",
@@ -0,0 +1,141 @@
1
+ """Mixin providing observer notification and fallback wiring methods for the Agent class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import threading
7
+ from typing import TYPE_CHECKING, Any, Optional, cast
8
+
9
+ if TYPE_CHECKING:
10
+ from ..memory import ConversationMemory
11
+
12
+
13
+ class _LifecycleMixin:
14
+ """Mixin that provides observer lifecycle methods for the Agent class.
15
+
16
+ All methods access ``self.*`` attributes (config, provider, etc.)
17
+ which are expected to be provided by the Agent class that inherits
18
+ from this mixin.
19
+ """
20
+
21
+ def _notify_observers(self, method: str, *args: Any) -> None:
22
+ """Call *method* on every registered observer, swallowing errors."""
23
+ for obs in self.config.observers:
24
+ try:
25
+ getattr(obs, method)(*args)
26
+ except Exception: # noqa: BLE001 # nosec B110
27
+ pass
28
+
29
+ async def _anotify_observers(self, method: str, *args: Any) -> None:
30
+ """Call async ``a_{method}`` on every AsyncAgentObserver, swallowing errors.
31
+
32
+ - ``blocking=True`` observers: awaited inline (sequential).
33
+ - ``blocking=False`` observers: dispatched via ``asyncio.ensure_future``.
34
+ """
35
+ from ..observer import AsyncAgentObserver
36
+
37
+ async_method = f"a_{method}"
38
+ for obs in self.config.observers:
39
+ if not isinstance(obs, AsyncAgentObserver):
40
+ continue
41
+ handler = getattr(obs, async_method, None)
42
+ if handler is None:
43
+ continue
44
+ try:
45
+ if obs.blocking:
46
+ await handler(*args)
47
+ else:
48
+ asyncio.ensure_future(handler(*args))
49
+ except Exception: # noqa: BLE001 # nosec B110
50
+ pass
51
+
52
+ def _truncate_tool_result(self, result: Optional[str]) -> Optional[str]:
53
+ """Truncate tool result text for trace storage."""
54
+ if result is None:
55
+ return None
56
+ limit = self.config.trace_tool_result_chars
57
+ if limit is None:
58
+ return result
59
+ return result[:limit]
60
+
61
+ _fallback_run_id: threading.local = threading.local()
62
+ _lock_type: type = type(threading.Lock())
63
+
64
+ def _wire_fallback_observer(self, run_id: Optional[str]) -> None:
65
+ """If the provider is a FallbackProvider, wire its on_fallback to observers.
66
+
67
+ Thread-safe: uses a lock + refcount so multiple concurrent ``run()``
68
+ calls (e.g. from ``batch()``) share a single callback on the provider
69
+ while each thread's ``run_id`` is carried via a thread-local.
70
+ The lock persists on the provider to avoid races from delete-then-recreate.
71
+ """
72
+ if not run_id or not self.config.observers:
73
+ return
74
+ provider = self.provider
75
+ if not hasattr(provider, "on_fallback"):
76
+ return
77
+
78
+ _LifecycleMixin._fallback_run_id.value = run_id
79
+
80
+ raw_lock = getattr(provider, "_fb_wire_lock", None)
81
+ if not isinstance(raw_lock, _LifecycleMixin._lock_type):
82
+ raw_lock = threading.Lock()
83
+ provider._fb_wire_lock = raw_lock # type: ignore[attr-defined]
84
+ lock = cast(threading.Lock, raw_lock)
85
+
86
+ agent_ref = self
87
+
88
+ with lock:
89
+ refcount: int = getattr(provider, "_fb_wire_refcount", 0)
90
+ if refcount == 0:
91
+ provider._fb_original_on_fallback = provider.on_fallback # type: ignore[attr-defined]
92
+ user_cb = provider.on_fallback
93
+
94
+ def _observer_fallback(
95
+ failed: str,
96
+ next_p: str,
97
+ exc: Exception,
98
+ ) -> None:
99
+ rid = getattr(_LifecycleMixin._fallback_run_id, "value", "")
100
+ agent_ref._notify_observers(
101
+ "on_provider_fallback",
102
+ rid,
103
+ failed,
104
+ next_p,
105
+ exc,
106
+ )
107
+ if user_cb:
108
+ try:
109
+ user_cb(failed, next_p, exc)
110
+ except Exception: # nosec B110
111
+ pass
112
+
113
+ provider.on_fallback = _observer_fallback # type: ignore[attr-defined]
114
+
115
+ provider._fb_wire_refcount = refcount + 1 # type: ignore[attr-defined]
116
+
117
+ def _unwire_fallback_observer(self) -> None:
118
+ """Restore FallbackProvider's original on_fallback callback (thread-safe).
119
+
120
+ The lock is kept on the provider (never deleted) to prevent races when
121
+ concurrent threads overlap wire / unwire calls.
122
+ """
123
+ provider = self.provider
124
+ raw_lock = getattr(provider, "_fb_wire_lock", None)
125
+ if not isinstance(raw_lock, _LifecycleMixin._lock_type):
126
+ return
127
+ lock = cast(threading.Lock, raw_lock)
128
+
129
+ with lock:
130
+ refcount: int = getattr(provider, "_fb_wire_refcount", 0) - 1
131
+ if refcount < 0:
132
+ refcount = 0
133
+ provider._fb_wire_refcount = refcount # type: ignore[attr-defined]
134
+ if refcount == 0:
135
+ original = getattr(provider, "_fb_original_on_fallback", None)
136
+ provider.on_fallback = original # type: ignore[attr-defined]
137
+ if hasattr(provider, "_fb_original_on_fallback"):
138
+ try:
139
+ delattr(provider, "_fb_original_on_fallback")
140
+ except Exception: # nosec B110
141
+ pass
@@ -0,0 +1,146 @@
1
+ """Mixin providing memory management methods for the Agent class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, List, Optional
6
+
7
+ from ..types import Message, Role
8
+
9
+ if TYPE_CHECKING:
10
+ from ..memory import ConversationMemory
11
+
12
+
13
+ class _MemoryManagerMixin:
14
+ """Mixin that provides memory management methods for the Agent class.
15
+
16
+ All methods access ``self.*`` attributes (config, provider, memory,
17
+ _history, _notify_observers, etc.) which are expected to be provided
18
+ by the Agent class that inherits from this mixin.
19
+ """
20
+
21
+ def _memory_add(self, msg: Message, run_id: str) -> None:
22
+ """Add message to memory and notify observers if trimming occurred."""
23
+ if not self.memory:
24
+ return
25
+ before = len(self.memory)
26
+ self.memory.add(msg)
27
+ after = len(self.memory)
28
+ removed = (before + 1) - after
29
+ if removed > 0:
30
+ self._notify_observers(
31
+ "on_memory_trim",
32
+ run_id,
33
+ removed,
34
+ after,
35
+ "enforce_limits",
36
+ )
37
+ self._maybe_summarize_trim(run_id)
38
+
39
+ def _memory_add_many(self, msgs: List[Message], run_id: str) -> None:
40
+ """Add multiple messages to memory and notify observers if trimming occurred."""
41
+ if not self.memory or not msgs:
42
+ return
43
+ before = len(self.memory)
44
+ self.memory.add_many(msgs)
45
+ after = len(self.memory)
46
+ removed = (before + len(msgs)) - after
47
+ if removed > 0:
48
+ self._notify_observers(
49
+ "on_memory_trim",
50
+ run_id,
51
+ removed,
52
+ after,
53
+ "enforce_limits",
54
+ )
55
+ self._maybe_summarize_trim(run_id)
56
+
57
+ def _maybe_summarize_trim(self, run_id: str) -> None:
58
+ """Generate a summary of trimmed messages if summarize_on_trim is enabled."""
59
+ if not self.config.summarize_on_trim or not self.memory:
60
+ return
61
+ trimmed = self.memory._last_trimmed
62
+ if not trimmed:
63
+ return
64
+ try:
65
+ provider = self.config.summarize_provider or self.provider
66
+ model = self.config.summarize_model or self.config.model
67
+ text_parts = []
68
+ for m in trimmed:
69
+ prefix = m.role.value.upper()
70
+ text_parts.append(f"{prefix}: {m.content or ''}")
71
+ trimmed_text = "\n".join(text_parts)
72
+
73
+ prompt_msg = Message(
74
+ role=Role.USER,
75
+ content=(
76
+ "Summarize the following conversation excerpt in 2-3 sentences. "
77
+ "Focus on key facts, decisions, and context that would be useful "
78
+ "for continuing the conversation:\n\n" + trimmed_text
79
+ ),
80
+ )
81
+ result = provider.complete(
82
+ model=model,
83
+ system_prompt="You are a concise summarizer.",
84
+ messages=[prompt_msg],
85
+ max_tokens=self.config.summarize_max_tokens,
86
+ )
87
+ # Provider returns (Message, UsageStats) tuple
88
+ summary_msg = result[0] if isinstance(result, tuple) else result
89
+ summary_text = summary_msg.content or ""
90
+ if summary_text:
91
+ existing = self.memory.summary
92
+ if existing:
93
+ self.memory.summary = existing + " " + summary_text
94
+ else:
95
+ self.memory.summary = summary_text
96
+ self._notify_observers("on_memory_summarize", run_id, self.memory.summary)
97
+ except Exception: # nosec B110
98
+ pass # never crash the agent for a summarization failure
99
+
100
+ def _session_save(self, run_id: str) -> None:
101
+ """Auto-save memory to session store if configured."""
102
+ store = self.config.session_store
103
+ sid = self.config.session_id
104
+ if not store or not sid or not self.memory:
105
+ return
106
+ try:
107
+ store.save(sid, self.memory)
108
+ self._notify_observers("on_session_save", run_id, sid, len(self.memory))
109
+ except Exception: # nosec B110
110
+ pass # never crash the agent for a persistence failure
111
+
112
+ def _extract_entities(self, run_id: str) -> None:
113
+ """Extract entities from recent messages if entity_memory is configured."""
114
+ em = self.config.entity_memory
115
+ if not em:
116
+ return
117
+ try:
118
+ recent = self._history[-em._relevance_window :]
119
+ entities = em.extract_entities(recent, model=self.config.model)
120
+ if entities:
121
+ em.update(entities)
122
+ self._notify_observers(
123
+ "on_entity_extraction",
124
+ run_id,
125
+ len(entities),
126
+ )
127
+ except Exception: # nosec B110
128
+ pass # never crash the agent for entity extraction failure
129
+
130
+ def _extract_kg_triples(self, run_id: str) -> None:
131
+ """Extract relationship triples from recent messages if knowledge_graph is configured."""
132
+ kg = self.config.knowledge_graph
133
+ if not kg:
134
+ return
135
+ try:
136
+ recent = self._history[-kg._relevance_window :]
137
+ triples = kg.extract_triples(recent, model=self.config.model)
138
+ if triples:
139
+ kg.store.add_many(triples)
140
+ self._notify_observers(
141
+ "on_kg_extraction",
142
+ run_id,
143
+ len(triples),
144
+ )
145
+ except Exception: # nosec B110
146
+ pass # never crash the agent for KG extraction failure