selectools 0.17.3__tar.gz → 0.17.4__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 (163) hide show
  1. {selectools-0.17.3/src/selectools.egg-info → selectools-0.17.4}/PKG-INFO +1 -1
  2. {selectools-0.17.3 → selectools-0.17.4}/pyproject.toml +1 -1
  3. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/__init__.py +17 -2
  4. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/agent/_provider_caller.py +25 -25
  5. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/agent/config.py +2 -0
  6. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/agent/core.py +52 -0
  7. selectools-0.17.4/src/selectools/knowledge.py +655 -0
  8. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/observer.py +44 -0
  9. selectools-0.17.4/src/selectools/token_estimation.py +153 -0
  10. {selectools-0.17.3 → selectools-0.17.4/src/selectools.egg-info}/PKG-INFO +1 -1
  11. {selectools-0.17.3 → selectools-0.17.4}/src/selectools.egg-info/SOURCES.txt +4 -0
  12. {selectools-0.17.3 → selectools-0.17.4}/tests/test_knowledge.py +6 -11
  13. selectools-0.17.4/tests/test_knowledge_stores.py +299 -0
  14. selectools-0.17.4/tests/test_model_switching.py +159 -0
  15. selectools-0.17.4/tests/test_token_estimation.py +135 -0
  16. selectools-0.17.3/src/selectools/knowledge.py +0 -198
  17. {selectools-0.17.3 → selectools-0.17.4}/LICENSE +0 -0
  18. {selectools-0.17.3 → selectools-0.17.4}/README.md +0 -0
  19. {selectools-0.17.3 → selectools-0.17.4}/setup.cfg +0 -0
  20. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/agent/__init__.py +0 -0
  21. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/agent/_lifecycle.py +0 -0
  22. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/agent/_memory_manager.py +0 -0
  23. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/agent/_tool_executor.py +0 -0
  24. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/analytics.py +0 -0
  25. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/audit.py +0 -0
  26. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/cache.py +0 -0
  27. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/cache_redis.py +0 -0
  28. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/cancellation.py +0 -0
  29. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/coherence.py +0 -0
  30. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/embeddings/__init__.py +0 -0
  31. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/embeddings/anthropic.py +0 -0
  32. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/embeddings/cohere.py +0 -0
  33. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/embeddings/gemini.py +0 -0
  34. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/embeddings/openai.py +0 -0
  35. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/embeddings/provider.py +0 -0
  36. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/entity_memory.py +0 -0
  37. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/env.py +0 -0
  38. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/__init__.py +0 -0
  39. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/__main__.py +0 -0
  40. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/badge.py +0 -0
  41. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/dataset.py +0 -0
  42. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/evaluators.py +0 -0
  43. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/generator.py +0 -0
  44. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/history.py +0 -0
  45. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/html.py +0 -0
  46. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/junit.py +0 -0
  47. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/llm_evaluators.py +0 -0
  48. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/pairwise.py +0 -0
  49. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/regression.py +0 -0
  50. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/report.py +0 -0
  51. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/serve.py +0 -0
  52. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/snapshot.py +0 -0
  53. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/suite.py +0 -0
  54. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/templates.py +0 -0
  55. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/evals/types.py +0 -0
  56. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/exceptions.py +0 -0
  57. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/guardrails/__init__.py +0 -0
  58. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/guardrails/base.py +0 -0
  59. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/guardrails/format.py +0 -0
  60. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/guardrails/length.py +0 -0
  61. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/guardrails/pii.py +0 -0
  62. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/guardrails/pipeline.py +0 -0
  63. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/guardrails/topic.py +0 -0
  64. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/guardrails/toxicity.py +0 -0
  65. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/knowledge_graph.py +0 -0
  66. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/mcp/__init__.py +0 -0
  67. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/mcp/_loop.py +0 -0
  68. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/mcp/bridge.py +0 -0
  69. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/mcp/client.py +0 -0
  70. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/mcp/config.py +0 -0
  71. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/mcp/multi.py +0 -0
  72. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/mcp/server.py +0 -0
  73. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/memory.py +0 -0
  74. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/models.py +0 -0
  75. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/parser.py +0 -0
  76. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/policy.py +0 -0
  77. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/pricing.py +0 -0
  78. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/prompt.py +0 -0
  79. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/providers/__init__.py +0 -0
  80. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/providers/_openai_compat.py +0 -0
  81. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/providers/anthropic_provider.py +0 -0
  82. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/providers/base.py +0 -0
  83. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/providers/fallback.py +0 -0
  84. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/providers/gemini_provider.py +0 -0
  85. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/providers/ollama_provider.py +0 -0
  86. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/providers/openai_provider.py +0 -0
  87. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/providers/stubs.py +0 -0
  88. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/__init__.py +0 -0
  89. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/bm25.py +0 -0
  90. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/chunking.py +0 -0
  91. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/hybrid.py +0 -0
  92. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/loaders.py +0 -0
  93. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/reranker.py +0 -0
  94. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/stores/__init__.py +0 -0
  95. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/stores/chroma.py +0 -0
  96. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/stores/memory.py +0 -0
  97. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/stores/pinecone.py +0 -0
  98. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/stores/sqlite.py +0 -0
  99. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/tools.py +0 -0
  100. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/rag/vector_store.py +0 -0
  101. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/security.py +0 -0
  102. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/sessions.py +0 -0
  103. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/structured.py +0 -0
  104. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/toolbox/__init__.py +0 -0
  105. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/toolbox/data_tools.py +0 -0
  106. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/toolbox/datetime_tools.py +0 -0
  107. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/toolbox/file_tools.py +0 -0
  108. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/toolbox/memory_tools.py +0 -0
  109. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/toolbox/text_tools.py +0 -0
  110. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/toolbox/web_tools.py +0 -0
  111. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/tools/__init__.py +0 -0
  112. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/tools/base.py +0 -0
  113. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/tools/decorators.py +0 -0
  114. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/tools/loader.py +0 -0
  115. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/tools/registry.py +0 -0
  116. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/trace.py +0 -0
  117. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/types.py +0 -0
  118. {selectools-0.17.3 → selectools-0.17.4}/src/selectools/usage.py +0 -0
  119. {selectools-0.17.3 → selectools-0.17.4}/src/selectools.egg-info/dependency_links.txt +0 -0
  120. {selectools-0.17.3 → selectools-0.17.4}/src/selectools.egg-info/requires.txt +0 -0
  121. {selectools-0.17.3 → selectools-0.17.4}/src/selectools.egg-info/top_level.txt +0 -0
  122. {selectools-0.17.3 → selectools-0.17.4}/tests/test_approval_gate.py +0 -0
  123. {selectools-0.17.3 → selectools-0.17.4}/tests/test_architecture.py +0 -0
  124. {selectools-0.17.3 → selectools-0.17.4}/tests/test_async_observers.py +0 -0
  125. {selectools-0.17.3 → selectools-0.17.4}/tests/test_audit.py +0 -0
  126. {selectools-0.17.3 → selectools-0.17.4}/tests/test_budget.py +0 -0
  127. {selectools-0.17.3 → selectools-0.17.4}/tests/test_cache.py +0 -0
  128. {selectools-0.17.3 → selectools-0.17.4}/tests/test_cache_redis.py +0 -0
  129. {selectools-0.17.3 → selectools-0.17.4}/tests/test_cancellation.py +0 -0
  130. {selectools-0.17.3 → selectools-0.17.4}/tests/test_coherence.py +0 -0
  131. {selectools-0.17.3 → selectools-0.17.4}/tests/test_consolidation_regression.py +0 -0
  132. {selectools-0.17.3 → selectools-0.17.4}/tests/test_entity_memory.py +0 -0
  133. {selectools-0.17.3 → selectools-0.17.4}/tests/test_env.py +0 -0
  134. {selectools-0.17.3 → selectools-0.17.4}/tests/test_evals.py +0 -0
  135. {selectools-0.17.3 → selectools-0.17.4}/tests/test_evals_advanced.py +0 -0
  136. {selectools-0.17.3 → selectools-0.17.4}/tests/test_evals_e2e.py +0 -0
  137. {selectools-0.17.3 → selectools-0.17.4}/tests/test_evals_final.py +0 -0
  138. {selectools-0.17.3 → selectools-0.17.4}/tests/test_evals_hardening.py +0 -0
  139. {selectools-0.17.3 → selectools-0.17.4}/tests/test_evals_new_evaluators.py +0 -0
  140. {selectools-0.17.3 → selectools-0.17.4}/tests/test_evals_release.py +0 -0
  141. {selectools-0.17.3 → selectools-0.17.4}/tests/test_guardrails.py +0 -0
  142. {selectools-0.17.3 → selectools-0.17.4}/tests/test_knowledge_graph.py +0 -0
  143. {selectools-0.17.3 → selectools-0.17.4}/tests/test_mcp.py +0 -0
  144. {selectools-0.17.3 → selectools-0.17.4}/tests/test_memory.py +0 -0
  145. {selectools-0.17.3 → selectools-0.17.4}/tests/test_memory_async.py +0 -0
  146. {selectools-0.17.3 → selectools-0.17.4}/tests/test_memory_boundary.py +0 -0
  147. {selectools-0.17.3 → selectools-0.17.4}/tests/test_memory_integration.py +0 -0
  148. {selectools-0.17.3 → selectools-0.17.4}/tests/test_parser.py +0 -0
  149. {selectools-0.17.3 → selectools-0.17.4}/tests/test_phase1_design_patterns.py +0 -0
  150. {selectools-0.17.3 → selectools-0.17.4}/tests/test_policy.py +0 -0
  151. {selectools-0.17.3 → selectools-0.17.4}/tests/test_prompt.py +0 -0
  152. {selectools-0.17.3 → selectools-0.17.4}/tests/test_routing_mode.py +0 -0
  153. {selectools-0.17.3 → selectools-0.17.4}/tests/test_security.py +0 -0
  154. {selectools-0.17.3 → selectools-0.17.4}/tests/test_sessions.py +0 -0
  155. {selectools-0.17.3 → selectools-0.17.4}/tests/test_sessions_edge_cases.py +0 -0
  156. {selectools-0.17.3 → selectools-0.17.4}/tests/test_sessions_redis.py +0 -0
  157. {selectools-0.17.3 → selectools-0.17.4}/tests/test_simple_observer.py +0 -0
  158. {selectools-0.17.3 → selectools-0.17.4}/tests/test_structured.py +0 -0
  159. {selectools-0.17.3 → selectools-0.17.4}/tests/test_structured_tool_results.py +0 -0
  160. {selectools-0.17.3 → selectools-0.17.4}/tests/test_summarize_on_trim.py +0 -0
  161. {selectools-0.17.3 → selectools-0.17.4}/tests/test_terminal_actions.py +0 -0
  162. {selectools-0.17.3 → selectools-0.17.4}/tests/test_trace.py +0 -0
  163. {selectools-0.17.3 → selectools-0.17.4}/tests/test_v016_regression.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: selectools
3
- Version: 0.17.3
3
+ Version: 0.17.4
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 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 = "0.17.3"
7
+ version = "0.17.4"
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"
@@ -1,6 +1,6 @@
1
1
  """Public exports for the selectools package."""
2
2
 
3
- __version__ = "0.17.3"
3
+ __version__ = "0.17.4"
4
4
 
5
5
  # Import submodules (lazy loading for optional dependencies)
6
6
  from . import embeddings, evals, guardrails, models, rag, toolbox
@@ -34,7 +34,13 @@ from .guardrails import (
34
34
  TopicGuardrail,
35
35
  ToxicityGuardrail,
36
36
  )
37
- from .knowledge import KnowledgeMemory
37
+ from .knowledge import (
38
+ FileKnowledgeStore,
39
+ KnowledgeEntry,
40
+ KnowledgeMemory,
41
+ KnowledgeStore,
42
+ SQLiteKnowledgeStore,
43
+ )
38
44
  from .knowledge_graph import (
39
45
  InMemoryTripleStore,
40
46
  KnowledgeGraphMemory,
@@ -73,6 +79,7 @@ from .sessions import (
73
79
  SQLiteSessionStore,
74
80
  )
75
81
  from .structured import ResponseFormat
82
+ from .token_estimation import TokenEstimate, estimate_run_tokens, estimate_tokens
76
83
  from .tools import Tool, ToolParameter, ToolRegistry, tool
77
84
  from .trace import AgentTrace, StepType, TraceStep
78
85
  from .types import AgentResult, Message, Role, ToolCall
@@ -177,6 +184,14 @@ __all__ = [
177
184
  "EntityMemory",
178
185
  # Knowledge Memory
179
186
  "KnowledgeMemory",
187
+ "KnowledgeEntry",
188
+ "KnowledgeStore",
189
+ "FileKnowledgeStore",
190
+ "SQLiteKnowledgeStore",
191
+ # Token estimation
192
+ "TokenEstimate",
193
+ "estimate_tokens",
194
+ "estimate_run_tokens",
180
195
  # Knowledge Graph
181
196
  "Triple",
182
197
  "TripleStore",
@@ -37,7 +37,7 @@ class _ProviderCallerMixin:
37
37
  self.config.stream and getattr(self.provider, "supports_streaming", False)
38
38
  ):
39
39
  cache_key = CacheKeyBuilder.build(
40
- model=self.config.model,
40
+ model=self._effective_model,
41
41
  system_prompt=self._system_prompt,
42
42
  messages=self._history,
43
43
  tools=self.tools,
@@ -53,7 +53,7 @@ class _ProviderCallerMixin:
53
53
  "on_llm_start",
54
54
  run_id,
55
55
  self._history,
56
- self.config.model,
56
+ self._effective_model,
57
57
  self._system_prompt,
58
58
  )
59
59
  self._notify_observers(
@@ -65,7 +65,7 @@ class _ProviderCallerMixin:
65
65
  self._notify_observers(
66
66
  "on_cache_hit",
67
67
  run_id,
68
- self.config.model,
68
+ self._effective_model,
69
69
  cached_msg.content or "",
70
70
  )
71
71
  self._notify_observers("on_usage", run_id, cached_usage)
@@ -76,8 +76,8 @@ class _ProviderCallerMixin:
76
76
  TraceStep(
77
77
  type=StepType.CACHE_HIT,
78
78
  duration_ms=(time.time() - call_start) * 1000,
79
- model=self.config.model,
80
- summary=f"Cache hit: {self.config.model}",
79
+ model=self._effective_model,
80
+ summary=f"Cache hit: {self._effective_model}",
81
81
  )
82
82
  )
83
83
  return cached_msg
@@ -93,7 +93,7 @@ class _ProviderCallerMixin:
93
93
  "on_llm_start",
94
94
  run_id,
95
95
  self._history,
96
- self.config.model,
96
+ self._effective_model,
97
97
  self._system_prompt,
98
98
  )
99
99
 
@@ -104,7 +104,7 @@ class _ProviderCallerMixin:
104
104
  return Message(role=Role.ASSISTANT, content=response_text)
105
105
 
106
106
  response_msg, usage_stats = self.provider.complete(
107
- model=self.config.model,
107
+ model=self._effective_model,
108
108
  system_prompt=self._system_prompt,
109
109
  messages=self._history,
110
110
  tools=self.tools,
@@ -144,11 +144,11 @@ class _ProviderCallerMixin:
144
144
  TraceStep(
145
145
  type=StepType.LLM_CALL,
146
146
  duration_ms=(time.time() - call_start) * 1000,
147
- model=self.config.model,
147
+ model=self._effective_model,
148
148
  prompt_tokens=usage_stats.prompt_tokens,
149
149
  completion_tokens=usage_stats.completion_tokens,
150
150
  cost_usd=usage_stats.cost_usd,
151
- summary=f"{self.config.model} → {len(response_text)} chars",
151
+ summary=f"{self._effective_model} → {len(response_text)} chars",
152
152
  )
153
153
  )
154
154
  return response_msg
@@ -185,7 +185,7 @@ class _ProviderCallerMixin:
185
185
  TraceStep(
186
186
  type=StepType.LLM_CALL,
187
187
  duration_ms=(time.time() - call_start) * 1000,
188
- model=self.config.model,
188
+ model=self._effective_model,
189
189
  error=last_error,
190
190
  summary=f"Provider error: {last_error}",
191
191
  )
@@ -200,7 +200,7 @@ class _ProviderCallerMixin:
200
200
 
201
201
  aggregated: List[str] = []
202
202
  for chunk in self.provider.stream(
203
- model=self.config.model,
203
+ model=self._effective_model,
204
204
  system_prompt=self._system_prompt,
205
205
  messages=self._history,
206
206
  tools=self.tools,
@@ -233,7 +233,7 @@ class _ProviderCallerMixin:
233
233
  self.config.stream and getattr(self.provider, "supports_streaming", False)
234
234
  ):
235
235
  cache_key = CacheKeyBuilder.build(
236
- model=self.config.model,
236
+ model=self._effective_model,
237
237
  system_prompt=self._system_prompt,
238
238
  messages=self._history,
239
239
  tools=self.tools,
@@ -249,7 +249,7 @@ class _ProviderCallerMixin:
249
249
  "on_llm_start",
250
250
  run_id,
251
251
  self._history,
252
- self.config.model,
252
+ self._effective_model,
253
253
  self._system_prompt,
254
254
  )
255
255
  self._notify_observers(
@@ -261,7 +261,7 @@ class _ProviderCallerMixin:
261
261
  self._notify_observers(
262
262
  "on_cache_hit",
263
263
  run_id,
264
- self.config.model,
264
+ self._effective_model,
265
265
  cached_msg.content or "",
266
266
  )
267
267
  self._notify_observers("on_usage", run_id, cached_usage)
@@ -272,8 +272,8 @@ class _ProviderCallerMixin:
272
272
  TraceStep(
273
273
  type=StepType.CACHE_HIT,
274
274
  duration_ms=(time.time() - call_start) * 1000,
275
- model=self.config.model,
276
- summary=f"Cache hit: {self.config.model}",
275
+ model=self._effective_model,
276
+ summary=f"Cache hit: {self._effective_model}",
277
277
  )
278
278
  )
279
279
  return cached_msg
@@ -289,14 +289,14 @@ class _ProviderCallerMixin:
289
289
  "on_llm_start",
290
290
  run_id,
291
291
  self._history,
292
- self.config.model,
292
+ self._effective_model,
293
293
  self._system_prompt,
294
294
  )
295
295
  await self._anotify_observers(
296
296
  "on_llm_start",
297
297
  run_id,
298
298
  self._history,
299
- self.config.model,
299
+ self._effective_model,
300
300
  self._system_prompt,
301
301
  )
302
302
 
@@ -312,7 +312,7 @@ class _ProviderCallerMixin:
312
312
  self.provider, "supports_async", False
313
313
  ):
314
314
  response_msg, usage_stats = await self.provider.acomplete(
315
- model=self.config.model,
315
+ model=self._effective_model,
316
316
  system_prompt=self._system_prompt,
317
317
  messages=self._history,
318
318
  tools=self.tools,
@@ -328,7 +328,7 @@ class _ProviderCallerMixin:
328
328
  response_msg, usage_stats = await loop.run_in_executor(
329
329
  executor,
330
330
  lambda: self.provider.complete(
331
- model=self.config.model,
331
+ model=self._effective_model,
332
332
  system_prompt=self._system_prompt,
333
333
  messages=self._history,
334
334
  tools=self.tools,
@@ -371,11 +371,11 @@ class _ProviderCallerMixin:
371
371
  TraceStep(
372
372
  type=StepType.LLM_CALL,
373
373
  duration_ms=(time.time() - call_start) * 1000,
374
- model=self.config.model,
374
+ model=self._effective_model,
375
375
  prompt_tokens=usage_stats.prompt_tokens,
376
376
  completion_tokens=usage_stats.completion_tokens,
377
377
  cost_usd=usage_stats.cost_usd,
378
- summary=f"{self.config.model} → {len(response_text)} chars",
378
+ summary=f"{self._effective_model} → {len(response_text)} chars",
379
379
  )
380
380
  )
381
381
  return response_msg
@@ -412,7 +412,7 @@ class _ProviderCallerMixin:
412
412
  TraceStep(
413
413
  type=StepType.LLM_CALL,
414
414
  duration_ms=(time.time() - call_start) * 1000,
415
- model=self.config.model,
415
+ model=self._effective_model,
416
416
  error=last_error,
417
417
  summary=f"Provider error: {last_error}",
418
418
  )
@@ -430,7 +430,7 @@ class _ProviderCallerMixin:
430
430
 
431
431
  if hasattr(self.provider, "astream") and getattr(self.provider, "supports_async", False):
432
432
  stream = self.provider.astream( # type: ignore[attr-defined]
433
- model=self.config.model,
433
+ model=self._effective_model,
434
434
  system_prompt=self._system_prompt,
435
435
  messages=self._history,
436
436
  tools=self.tools,
@@ -445,7 +445,7 @@ class _ProviderCallerMixin:
445
445
  stream_handler(chunk)
446
446
  else:
447
447
  for chunk in self.provider.stream(
448
- model=self.config.model,
448
+ model=self._effective_model,
449
449
  system_prompt=self._system_prompt,
450
450
  messages=self._history,
451
451
  tools=self.tools,
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
18
18
  from ..policy import ToolPolicy
19
19
  from ..providers.base import Provider
20
20
  from ..sessions import SessionStore
21
+ from ..usage import AgentUsage
21
22
 
22
23
  # Hook type definitions
23
24
  HookCallable = Callable[..., None]
@@ -157,6 +158,7 @@ class AgentConfig:
157
158
  knowledge_graph: Optional[KnowledgeGraphMemory] = None
158
159
  knowledge_memory: Optional[KnowledgeMemory] = None
159
160
  stop_condition: Optional[Callable[[str, str], bool]] = None
161
+ model_selector: Optional[Callable[[int, List, "AgentUsage"], str]] = None
160
162
  max_total_tokens: Optional[int] = None
161
163
  max_cost_usd: Optional[float] = None
162
164
  cancellation_token: Optional[CancellationToken] = None
@@ -189,6 +189,15 @@ class Agent(_ToolExecutorMixin, _ProviderCallerMixin, _LifecycleMixin, _MemoryMa
189
189
  """Return the agent's name from config."""
190
190
  return self.config.name
191
191
 
192
+ @property
193
+ def _effective_model(self) -> str:
194
+ """The model to use for the current iteration.
195
+
196
+ Returns the model set by ``model_selector`` if active, otherwise
197
+ falls back to ``self.config.model``.
198
+ """
199
+ return getattr(self, "_current_model", None) or self.config.model
200
+
192
201
  def __call__(
193
202
  self,
194
203
  messages: Union[str, List[Message]],
@@ -313,6 +322,7 @@ class Agent(_ToolExecutorMixin, _ProviderCallerMixin, _LifecycleMixin, _MemoryMa
313
322
  wires observers, runs input guardrails, loads memory/session, injects
314
323
  knowledge context, and returns a _RunContext carrying all state.
315
324
  """
325
+ self._current_model: Optional[str] = None
316
326
  original_system_prompt = self._system_prompt
317
327
  if response_format is not None:
318
328
  schema = schema_from_response_format(response_format)
@@ -867,6 +877,18 @@ class Agent(_ToolExecutorMixin, _ProviderCallerMixin, _LifecycleMixin, _MemoryMa
867
877
  if budget_msg:
868
878
  return self._build_budget_exceeded_result(ctx, budget_msg)
869
879
 
880
+ # Model selection (R10)
881
+ if self.config.model_selector:
882
+ selected = self.config.model_selector(
883
+ ctx.iteration, ctx.all_tool_calls, self.usage
884
+ )
885
+ if selected != self._effective_model:
886
+ old_model = self._effective_model
887
+ self._current_model = selected
888
+ self._notify_observers(
889
+ "on_model_switch", ctx.run_id, ctx.iteration, old_model, selected
890
+ )
891
+
870
892
  self._notify_observers(
871
893
  "on_iteration_start", ctx.run_id, ctx.iteration, self._history
872
894
  )
@@ -1053,6 +1075,21 @@ class Agent(_ToolExecutorMixin, _ProviderCallerMixin, _LifecycleMixin, _MemoryMa
1053
1075
  yield StreamChunk(content=budget_msg)
1054
1076
  return
1055
1077
 
1078
+ # Model selection (R10)
1079
+ if self.config.model_selector:
1080
+ selected = self.config.model_selector(
1081
+ ctx.iteration, ctx.all_tool_calls, self.usage
1082
+ )
1083
+ if selected != self._effective_model:
1084
+ old_model = self._effective_model
1085
+ self._current_model = selected
1086
+ self._notify_observers(
1087
+ "on_model_switch", ctx.run_id, ctx.iteration, old_model, selected
1088
+ )
1089
+ await self._anotify_observers(
1090
+ "on_model_switch", ctx.run_id, ctx.iteration, old_model, selected
1091
+ )
1092
+
1056
1093
  self._notify_observers(
1057
1094
  "on_iteration_start", ctx.run_id, ctx.iteration, self._history
1058
1095
  )
@@ -1369,6 +1406,21 @@ class Agent(_ToolExecutorMixin, _ProviderCallerMixin, _LifecycleMixin, _MemoryMa
1369
1406
  if budget_msg:
1370
1407
  return self._build_budget_exceeded_result(ctx, budget_msg)
1371
1408
 
1409
+ # Model selection (R10)
1410
+ if self.config.model_selector:
1411
+ selected = self.config.model_selector(
1412
+ ctx.iteration, ctx.all_tool_calls, self.usage
1413
+ )
1414
+ if selected != self._effective_model:
1415
+ old_model = self._effective_model
1416
+ self._current_model = selected
1417
+ self._notify_observers(
1418
+ "on_model_switch", ctx.run_id, ctx.iteration, old_model, selected
1419
+ )
1420
+ await self._anotify_observers(
1421
+ "on_model_switch", ctx.run_id, ctx.iteration, old_model, selected
1422
+ )
1423
+
1372
1424
  self._notify_observers(
1373
1425
  "on_iteration_start", ctx.run_id, ctx.iteration, self._history
1374
1426
  )