evidor 1.10.2.dev1__tar.gz → 1.10.3.dev1__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 (105) hide show
  1. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/CHANGELOG.md +19 -0
  2. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/PKG-INFO +42 -9
  3. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/README.md +41 -8
  4. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/pyproject.toml +1 -1
  5. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/agent.py +26 -24
  6. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/context.py +16 -0
  7. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/runner.py +33 -19
  8. evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/__init__.py +14 -0
  9. evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/builder.py +120 -0
  10. evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/parser.py +102 -0
  11. evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/provider.py +245 -0
  12. evidor-1.10.3.dev1/src/evidor/agent/providers/anthropic/stream.py +67 -0
  13. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/providers/base.py +2 -4
  14. evidor-1.10.3.dev1/src/evidor/agent/providers/client_pool.py +97 -0
  15. evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/__init__.py +15 -0
  16. evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/builder.py +243 -0
  17. evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/parser.py +174 -0
  18. evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/provider.py +198 -0
  19. evidor-1.10.3.dev1/src/evidor/agent/providers/gemini/stream.py +184 -0
  20. evidor-1.10.3.dev1/src/evidor/agent/providers/openai/__init__.py +15 -0
  21. evidor-1.10.3.dev1/src/evidor/agent/providers/openai/builder.py +92 -0
  22. evidor-1.10.3.dev1/src/evidor/agent/providers/openai/parser.py +145 -0
  23. evidor-1.10.3.dev1/src/evidor/agent/providers/openai/provider.py +217 -0
  24. evidor-1.10.3.dev1/src/evidor/agent/providers/openai/stream.py +57 -0
  25. evidor-1.10.3.dev1/src/evidor/constants.py +115 -0
  26. evidor-1.10.3.dev1/src/evidor/errors.py +46 -0
  27. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/client.py +8 -12
  28. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/session.py +10 -7
  29. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/models/thinking.py +12 -9
  30. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/core/tool_executor.py +2 -4
  31. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/core/tools.py +2 -1
  32. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/utils/execution.py +3 -5
  33. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_agent.py +139 -0
  34. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_providers.py +249 -3
  35. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_streaming.py +199 -0
  36. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_thinking.py +13 -17
  37. evidor-1.10.2.dev1/src/evidor/agent/providers/anthropic.py +0 -506
  38. evidor-1.10.2.dev1/src/evidor/agent/providers/gemini.py +0 -643
  39. evidor-1.10.2.dev1/src/evidor/agent/providers/openai.py +0 -510
  40. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/.github/workflows/release.yml +0 -0
  41. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/.gitignore +0 -0
  42. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/AGENTS.md +0 -0
  43. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/CODE_OF_CONDUCT.md +0 -0
  44. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/LICENSE +0 -0
  45. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/requirements.txt +0 -0
  46. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/__init__.py +0 -0
  47. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/__init__.py +0 -0
  48. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/__init__.py +0 -0
  49. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/core/telemetry_bridge.py +0 -0
  50. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/agent/providers/__init__.py +0 -0
  51. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/__init__.py +0 -0
  52. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/config.py +0 -0
  53. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/mcp/runner.py +0 -0
  54. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/models/__init__.py +0 -0
  55. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/models/generation.py +0 -0
  56. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/models/streaming.py +0 -0
  57. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/__init__.py +0 -0
  58. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/__init__.py +0 -0
  59. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/langfuse.py +0 -0
  60. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/opentelemetry.py +0 -0
  61. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/phoenix.py +0 -0
  62. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/adapters/prometheus.py +0 -0
  63. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/context.py +0 -0
  64. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/events.py +0 -0
  65. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/runtime.py +0 -0
  66. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/semconv.py +0 -0
  67. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/telemetry/sink.py +0 -0
  68. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/__init__.py +0 -0
  69. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/core/__init__.py +0 -0
  70. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/core/utils.py +0 -0
  71. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/__init__.py +0 -0
  72. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/calculator.py +0 -0
  73. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/evidence_store.py +0 -0
  74. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/__init__.py +0 -0
  75. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/_common.py +0 -0
  76. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/list_files.py +0 -0
  77. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/mutate_files.py +0 -0
  78. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/read_file.py +0 -0
  79. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/filesystem/search_files.py +0 -0
  80. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/get_current_time.py +0 -0
  81. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/read_webpage.py +0 -0
  82. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/__init__.py +0 -0
  83. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/_base.py +0 -0
  84. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/http.py +0 -0
  85. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/providers/__init__.py +0 -0
  86. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/providers/brave.py +0 -0
  87. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/providers/exa.py +0 -0
  88. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/providers/tavily.py +0 -0
  89. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/tools/native/web_search/tool.py +0 -0
  90. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/utils/__init__.py +0 -0
  91. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/src/evidor/utils/schema.py +0 -0
  92. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_builtin_tools.py +0 -0
  93. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_conversation.py +0 -0
  94. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_filesystem_tools.py +0 -0
  95. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_langfuse_adapter.py +0 -0
  96. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_mcp.py +0 -0
  97. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_models.py +0 -0
  98. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_native_tools.py +0 -0
  99. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_opentelemetry_adapter.py +0 -0
  100. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_phoenix_adapter.py +0 -0
  101. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_prometheus_adapter.py +0 -0
  102. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_telemetry_events.py +0 -0
  103. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_telemetry_integration.py +0 -0
  104. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_tools.py +0 -0
  105. {evidor-1.10.2.dev1 → evidor-1.10.3.dev1}/tests/test_web_search.py +0 -0
@@ -2,6 +2,25 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.10.3-dev.1 (2026-10-22)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **agent**: Fix gemini streaming issues
10
+ ([`f9d64f2`](https://github.com/vedangiitb/evidor-core/commit/f9d64f25f195c97c3ee5dd01c18b1d852e92fb70))
11
+
12
+ - **agent**: Fixing gemini request building, response parsing and adapter lifecycle issues
13
+ ([`a67efbe`](https://github.com/vedangiitb/evidor-core/commit/a67efbecc0c9331fc61a14ddbc7512b656ae5e95))
14
+
15
+ ### Documentation
16
+
17
+ - **agent**: Docs update for stateless sessions update
18
+ ([`57569f8`](https://github.com/vedangiitb/evidor-core/commit/57569f843aa7ceeece9f47ac3abec5cfe48a175d))
19
+
20
+
21
+ ## v1.10.2 (2026-10-21)
22
+
23
+
5
24
  ## v1.10.2-dev.1 (2026-10-21)
6
25
 
7
26
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: evidor
3
- Version: 1.10.2-dev.1
3
+ Version: 1.10.3-dev.1
4
4
  Summary: A small, provider-agnostic harness for LLM input and output.
5
5
  Project-URL: Homepage, https://github.com/evidor/evidor-core
6
6
  Project-URL: Repository, https://github.com/evidor/evidor-core
@@ -108,12 +108,13 @@ response = agent.send("Give a one-sentence explanation of dependency inversion."
108
108
  print(response.text)
109
109
  ```
110
110
 
111
- ## Core Architecture: Two Execution Paradigms
111
+ ## Core Architecture: Execution Paradigms
112
112
 
113
- Evidor cleanly separates AI execution into two paradigms:
113
+ Evidor cleanly separates AI execution into flexible paradigms:
114
114
 
115
- 1. **Stateful Conversations (`Agent`) (Recommended)**: The primary and recommended way to build applications with Evidor. Conversations maintain state automatically across turns, so you don't have to pass the entire message history each time. Evidor manages session state, context continuation, and tool execution loops seamlessly under the hood.
116
- 2. **Stateless LLM Calls (`provider.generate`)**: Direct single-turn or user-managed requests with tools, structured outputs, and raw message histories. Available when you need low-level control or want to manage message storage and persistence yourself.
115
+ 1. **Stateful Conversations (`Agent`) (Default & Recommended)**: The primary and recommended way to build applications with Evidor. Conversations maintain state automatically across turns, so you don't have to pass the entire message history each time. Evidor manages session state, context continuation, and tool execution loops seamlessly under the hood.
116
+ 2. **Stateless Agents (`Agent(..., stateless=True)`)**: When you want high-level agent capabilities (automatic multi-step tool execution loops, MCP tools, system prompts, and telemetry) but want every `.send()` / `.send_async()` call to execute completely independently without remembering past turns.
117
+ 3. **Stateless LLM Calls (`provider.generate`)**: Direct single-turn or user-managed requests with tools, structured outputs, and raw message histories. Available when you need low-level control or want to manage message storage and persistence yourself.
117
118
 
118
119
  ---
119
120
 
@@ -231,6 +232,38 @@ agent.reset() # or agent.clear_history()
231
232
  print(agent.last_response_id) # None
232
233
  ```
233
234
 
235
+ ### Stateless Agent (`stateless=True`)
236
+
237
+ If you want the high-level capabilities of an `Agent`—automatic multi-step tool execution loops, MCP tools, system prompts, and telemetry—but want each `.send()` or `.send_async()` call to execute independently without remembering prior turns, pass `stateless=True`:
238
+
239
+ ```python
240
+ from evidor import Agent, OpenAIProvider
241
+ from evidor.tools import tool
242
+
243
+ @tool
244
+ def get_stock_price(ticker: str) -> float:
245
+ return 150.25
246
+
247
+ # Each send() call executes independently without cross-turn memory
248
+ agent = Agent(
249
+ OpenAIProvider(model="gpt-6-astra"),
250
+ system_prompt="You are a financial assistant.",
251
+ tools=[get_stock_price],
252
+ stateless=True,
253
+ )
254
+
255
+ # Turn 1: Invokes tool loop and returns answer
256
+ resp1 = agent.send("What is AAPL's price?")
257
+
258
+ # Turn 2: Starts completely fresh without prior turns or response chaining
259
+ resp2 = agent.send("What did I just ask?")
260
+ ```
261
+
262
+ **Key Advantages of `stateless=True`**:
263
+ - **Automatic Inter-turn Isolation**: For OpenAI and Gemini, `previous_response_id` / `previous_interaction_id` are not chained across turns. For Anthropic, earlier turns are not replayed.
264
+ - **Intra-turn Tool Loops Intact**: Multi-step tool calls *within* a single turn (tool call $\to$ tool result $\to$ final response) work seamlessly because state is preserved during the turn.
265
+ - **Concurrency-Safe**: Each turn runs within an isolated ephemeral context frame, enabling concurrent `.send_async()` calls without cross-talk.
266
+
234
267
  ---
235
268
 
236
269
  ## 2. Stateless LLM Calls
@@ -514,15 +547,15 @@ provider = AnthropicProvider(
514
547
  thinking=AnthropicThinkingConfig(budget_tokens=4096),
515
548
  )
516
549
 
517
- # 3. Gemini: Configure thinking level ('minimal', 'low', 'medium', 'high') or budget
550
+ # 3. Gemini: Configure thinking level ('minimal', 'low', 'medium', 'high') and summaries ('auto', 'none')
518
551
  # Shorthand string (Gemini 3.x thinking_level):
519
552
  provider = GeminiProvider(model="gemini-3.8-flash", thinking="high")
520
- # Shorthand bool (False disables thinking; True enables dynamic thinking):
553
+ # Shorthand bool (False disables thinking; True enables default thinking):
521
554
  provider = GeminiProvider(model="gemini-3.8-flash", thinking=False)
522
555
  # Or via typed config:
523
556
  provider = GeminiProvider(
524
557
  model="gemini-3.8-flash",
525
- thinking=GeminiThinkingConfig(thinking_level="high", include_thoughts=True),
558
+ thinking=GeminiThinkingConfig(thinking_level="high", thinking_summaries="auto"),
526
559
  )
527
560
  ```
528
561
 
@@ -1253,7 +1286,7 @@ Supported providers, example models, and reasoning/thinking support:
1253
1286
  | --- | --- | --- | --- | --- |
1254
1287
  | `OpenAIProvider` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`, `gpt-4.1-mini` | `reasoning_effort` (`"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`) | `OPENAI_API_KEY` | `evidor[openai]` |
1255
1288
  | `AnthropicProvider` | `claude-3-7-sonnet` | Extended budget (`budget_tokens >= 1024`) or adaptive | `ANTHROPIC_API_KEY` | `evidor[anthropic]` |
1256
- | `GeminiProvider` | `gemini-3.8-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview` | `thinking_level` (`"minimal"`, `"low"`, `"medium"`, `"high"`) or budget | `GEMINI_API_KEY` | `evidor[gemini]` |
1289
+ | `GeminiProvider` | `gemini-3.8-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview` | `thinking_level` (`"minimal"`, `"low"`, `"medium"`, `"high"`), `thinking_summaries` (`"auto"`, `"none"`) | `GEMINI_API_KEY` | `evidor[gemini]` |
1257
1290
 
1258
1291
  API keys can be supplied explicitly via the `api_key` constructor argument or read automatically from environment variables. Custom base URLs for OpenAI-compatible providers (such as DeepSeek) can be supplied via the `base_url` constructor argument or `OPENAI_BASE_URL` environment variable:
1259
1292
 
@@ -21,12 +21,13 @@ response = agent.send("Give a one-sentence explanation of dependency inversion."
21
21
  print(response.text)
22
22
  ```
23
23
 
24
- ## Core Architecture: Two Execution Paradigms
24
+ ## Core Architecture: Execution Paradigms
25
25
 
26
- Evidor cleanly separates AI execution into two paradigms:
26
+ Evidor cleanly separates AI execution into flexible paradigms:
27
27
 
28
- 1. **Stateful Conversations (`Agent`) (Recommended)**: The primary and recommended way to build applications with Evidor. Conversations maintain state automatically across turns, so you don't have to pass the entire message history each time. Evidor manages session state, context continuation, and tool execution loops seamlessly under the hood.
29
- 2. **Stateless LLM Calls (`provider.generate`)**: Direct single-turn or user-managed requests with tools, structured outputs, and raw message histories. Available when you need low-level control or want to manage message storage and persistence yourself.
28
+ 1. **Stateful Conversations (`Agent`) (Default & Recommended)**: The primary and recommended way to build applications with Evidor. Conversations maintain state automatically across turns, so you don't have to pass the entire message history each time. Evidor manages session state, context continuation, and tool execution loops seamlessly under the hood.
29
+ 2. **Stateless Agents (`Agent(..., stateless=True)`)**: When you want high-level agent capabilities (automatic multi-step tool execution loops, MCP tools, system prompts, and telemetry) but want every `.send()` / `.send_async()` call to execute completely independently without remembering past turns.
30
+ 3. **Stateless LLM Calls (`provider.generate`)**: Direct single-turn or user-managed requests with tools, structured outputs, and raw message histories. Available when you need low-level control or want to manage message storage and persistence yourself.
30
31
 
31
32
  ---
32
33
 
@@ -144,6 +145,38 @@ agent.reset() # or agent.clear_history()
144
145
  print(agent.last_response_id) # None
145
146
  ```
146
147
 
148
+ ### Stateless Agent (`stateless=True`)
149
+
150
+ If you want the high-level capabilities of an `Agent`—automatic multi-step tool execution loops, MCP tools, system prompts, and telemetry—but want each `.send()` or `.send_async()` call to execute independently without remembering prior turns, pass `stateless=True`:
151
+
152
+ ```python
153
+ from evidor import Agent, OpenAIProvider
154
+ from evidor.tools import tool
155
+
156
+ @tool
157
+ def get_stock_price(ticker: str) -> float:
158
+ return 150.25
159
+
160
+ # Each send() call executes independently without cross-turn memory
161
+ agent = Agent(
162
+ OpenAIProvider(model="gpt-6-astra"),
163
+ system_prompt="You are a financial assistant.",
164
+ tools=[get_stock_price],
165
+ stateless=True,
166
+ )
167
+
168
+ # Turn 1: Invokes tool loop and returns answer
169
+ resp1 = agent.send("What is AAPL's price?")
170
+
171
+ # Turn 2: Starts completely fresh without prior turns or response chaining
172
+ resp2 = agent.send("What did I just ask?")
173
+ ```
174
+
175
+ **Key Advantages of `stateless=True`**:
176
+ - **Automatic Inter-turn Isolation**: For OpenAI and Gemini, `previous_response_id` / `previous_interaction_id` are not chained across turns. For Anthropic, earlier turns are not replayed.
177
+ - **Intra-turn Tool Loops Intact**: Multi-step tool calls *within* a single turn (tool call $\to$ tool result $\to$ final response) work seamlessly because state is preserved during the turn.
178
+ - **Concurrency-Safe**: Each turn runs within an isolated ephemeral context frame, enabling concurrent `.send_async()` calls without cross-talk.
179
+
147
180
  ---
148
181
 
149
182
  ## 2. Stateless LLM Calls
@@ -427,15 +460,15 @@ provider = AnthropicProvider(
427
460
  thinking=AnthropicThinkingConfig(budget_tokens=4096),
428
461
  )
429
462
 
430
- # 3. Gemini: Configure thinking level ('minimal', 'low', 'medium', 'high') or budget
463
+ # 3. Gemini: Configure thinking level ('minimal', 'low', 'medium', 'high') and summaries ('auto', 'none')
431
464
  # Shorthand string (Gemini 3.x thinking_level):
432
465
  provider = GeminiProvider(model="gemini-3.8-flash", thinking="high")
433
- # Shorthand bool (False disables thinking; True enables dynamic thinking):
466
+ # Shorthand bool (False disables thinking; True enables default thinking):
434
467
  provider = GeminiProvider(model="gemini-3.8-flash", thinking=False)
435
468
  # Or via typed config:
436
469
  provider = GeminiProvider(
437
470
  model="gemini-3.8-flash",
438
- thinking=GeminiThinkingConfig(thinking_level="high", include_thoughts=True),
471
+ thinking=GeminiThinkingConfig(thinking_level="high", thinking_summaries="auto"),
439
472
  )
440
473
  ```
441
474
 
@@ -1166,7 +1199,7 @@ Supported providers, example models, and reasoning/thinking support:
1166
1199
  | --- | --- | --- | --- | --- |
1167
1200
  | `OpenAIProvider` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`, `gpt-4.1-mini` | `reasoning_effort` (`"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`) | `OPENAI_API_KEY` | `evidor[openai]` |
1168
1201
  | `AnthropicProvider` | `claude-3-7-sonnet` | Extended budget (`budget_tokens >= 1024`) or adaptive | `ANTHROPIC_API_KEY` | `evidor[anthropic]` |
1169
- | `GeminiProvider` | `gemini-3.8-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview` | `thinking_level` (`"minimal"`, `"low"`, `"medium"`, `"high"`) or budget | `GEMINI_API_KEY` | `evidor[gemini]` |
1202
+ | `GeminiProvider` | `gemini-3.8-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview` | `thinking_level` (`"minimal"`, `"low"`, `"medium"`, `"high"`), `thinking_summaries` (`"auto"`, `"none"`) | `GEMINI_API_KEY` | `evidor[gemini]` |
1170
1203
 
1171
1204
  API keys can be supplied explicitly via the `api_key` constructor argument or read automatically from environment variables. Custom base URLs for OpenAI-compatible providers (such as DeepSeek) can be supplied via the `base_url` constructor argument or `OPENAI_BASE_URL` environment variable:
1172
1205
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "evidor"
7
- version = "1.10.2-dev.1"
7
+ version = "1.10.3-dev.1"
8
8
  description = "A small, provider-agnostic harness for LLM input and output."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -7,6 +7,12 @@ from collections.abc import Callable, Sequence
7
7
  import threading
8
8
  from typing import Any, Literal, overload
9
9
 
10
+ from evidor.constants import (
11
+ AGENT_CLOSE_SYNC_ERROR,
12
+ AGENT_SEND_SYNC_ERROR,
13
+ AGENT_STREAM_SYNC_ERROR,
14
+ AGENT_SYNC_MCP_TOOLS_ERROR,
15
+ )
10
16
  from evidor.models import (
11
17
  AsyncStreamResponse,
12
18
  GenerationResponse,
@@ -48,7 +54,15 @@ class Agent:
48
54
  store: bool | None = None,
49
55
  compact_threshold: int | None = None,
50
56
  telemetry: TelemetryRuntime | TelemetrySink | Sequence[TelemetrySink] | None = None,
57
+ stateless: bool = False,
51
58
  ) -> None:
59
+ if stateless:
60
+ if previous_response_id is not None:
61
+ raise ValueError("Cannot configure previous_response_id on a stateless Agent.")
62
+ if conversation_id is not None:
63
+ raise ValueError("Cannot configure conversation_id on a stateless Agent.")
64
+
65
+ self._stateless = stateless
52
66
  self._provider = provider
53
67
  self._system_prompt = system_prompt
54
68
  self._compact_threshold = compact_threshold or getattr(provider, "compact_threshold", None)
@@ -93,6 +107,7 @@ class Agent:
93
107
  tool_executor=self._tool_executor,
94
108
  telemetry_bridge=self._telemetry_bridge,
95
109
  max_tool_iterations=max_tool_iterations,
110
+ stateless=self._stateless,
96
111
  )
97
112
 
98
113
  # Background event loop and thread synchronization
@@ -117,6 +132,11 @@ class Agent:
117
132
  """
118
133
  return getattr(self._provider, "context_strategy", "stateless_replay")
119
134
 
135
+ @property
136
+ def stateless(self) -> bool:
137
+ """Whether this agent runs each turn statelessly without retaining conversation memory across turns."""
138
+ return self._stateless
139
+
120
140
  @property
121
141
  def last_response_id(self) -> str | None:
122
142
  """The identifier of the last response or interaction in this conversation."""
@@ -192,10 +212,7 @@ class Agent:
192
212
  return
193
213
  self._sync_loop.run(
194
214
  lambda: self._sync_mcp_tools_async(force=force),
195
- error_message=(
196
- "Cannot synchronize MCP tools synchronously from a running event loop. "
197
- "Access agent.tools outside an event loop or run an async agent turn first."
198
- ),
215
+ error_message=AGENT_SYNC_MCP_TOOLS_ERROR,
199
216
  )
200
217
 
201
218
  async def _sync_mcp_tools_async(self, force: bool = False) -> None:
@@ -230,16 +247,10 @@ class Agent:
230
247
  with self._lock:
231
248
  return self._sync_loop.run(
232
249
  lambda: self.send_async(message, stream=False),
233
- error_message=(
234
- "Agent.send() cannot be called from a running event loop. "
235
- "Use 'await agent.send_async()' instead."
236
- ),
250
+ error_message=AGENT_SEND_SYNC_ERROR,
237
251
  )
238
252
 
239
- check_sync_allowed(
240
- "Agent.send(..., stream=True) cannot be called from a running event loop. "
241
- "Use 'await agent.send_async(..., stream=True)' instead."
242
- )
253
+ check_sync_allowed(AGENT_STREAM_SYNC_ERROR)
243
254
  with self._lock:
244
255
  bridge = sync_stream_bridge(
245
256
  self._sync_loop, lambda: self.send_async(message, stream=True)
@@ -282,10 +293,7 @@ class Agent:
282
293
  if self._background_loop is not None and self._background_loop.is_running:
283
294
  self._background_loop.run(
284
295
  self._close_async_resources,
285
- error_message=(
286
- "Agent.close() cannot be called from a running event loop. "
287
- "Use 'await agent.close_async()' or 'async with agent:' instead."
288
- ),
296
+ error_message=AGENT_CLOSE_SYNC_ERROR,
289
297
  )
290
298
  else:
291
299
  running_loop = None
@@ -296,18 +304,12 @@ class Agent:
296
304
 
297
305
  if running_loop is not None:
298
306
  if self._mcp_client is not None:
299
- check_sync_allowed(
300
- "Agent.close() cannot be called from a running event loop. "
301
- "Use 'await agent.close_async()' or 'async with agent:' instead."
302
- )
307
+ check_sync_allowed(AGENT_CLOSE_SYNC_ERROR)
303
308
  running_loop.create_task(self._close_async_resources())
304
309
  else:
305
310
  run_async_in_sync(
306
311
  self._close_async_resources,
307
- error_message=(
308
- "Agent.close() cannot be called from a running event loop. "
309
- "Use 'await agent.close_async()' or 'async with agent:' instead."
310
- ),
312
+ error_message=AGENT_CLOSE_SYNC_ERROR,
311
313
  )
312
314
  self._close_sync_resources()
313
315
  finally:
@@ -50,6 +50,22 @@ class ConversationContext:
50
50
  if self.system_prompt:
51
51
  self._messages.append(Message(role="system", content=self.system_prompt))
52
52
 
53
+ def spawn_turn_context(self) -> ConversationContext:
54
+ """Create a fresh isolated turn context with the same configuration."""
55
+ return ConversationContext(
56
+ strategy=self.strategy,
57
+ system_prompt=self.system_prompt,
58
+ previous_response_id=None,
59
+ conversation_id=None,
60
+ store=self.store,
61
+ compact_threshold=self.compact_threshold,
62
+ )
63
+
64
+ def copy_from(self, other: ConversationContext) -> None:
65
+ """Update messages and last_response_id from another turn context."""
66
+ self._messages = list(other._messages)
67
+ self.last_response_id = other.last_response_id
68
+
53
69
  def append_user_message(self, message: str) -> Message:
54
70
  """Record a new user message in the conversation history."""
55
71
  msg = Message(role="user", content=message)
@@ -38,12 +38,14 @@ class TurnExecutor:
38
38
  tool_executor: ToolExecutor,
39
39
  telemetry_bridge: AgentTelemetryBridge,
40
40
  max_tool_iterations: int = 10,
41
+ stateless: bool = False,
41
42
  ) -> None:
42
43
  self._provider = provider
43
44
  self._context = context
44
45
  self._tool_executor = tool_executor
45
46
  self._telemetry_bridge = telemetry_bridge
46
47
  self._max_tool_iterations = max_tool_iterations
48
+ self._stateless = stateless
47
49
 
48
50
  def update_tool_executor(self, executor: ToolExecutor) -> None:
49
51
  """Update the active ToolExecutor (e.g. after MCP tool synchronization)."""
@@ -78,8 +80,10 @@ class TurnExecutor:
78
80
  self,
79
81
  final_response: GenerationResponse,
80
82
  last_response: GenerationResponse | None,
83
+ context: ConversationContext | None = None,
81
84
  ) -> GenerationResponse:
82
85
  """Synthesize a fallback response if the model exhausts tool iterations without concluding."""
86
+ ctx = context or self._context
83
87
  final_text = (final_response.text or "").strip()
84
88
  if not final_text and last_response:
85
89
  final_text = (last_response.text or "").strip()
@@ -87,7 +91,7 @@ class TurnExecutor:
87
91
  final_text = (
88
92
  f"Reached maximum tool iterations ({self._max_tool_iterations}) without a final response."
89
93
  )
90
- self._context.append_assistant_response(text=final_text)
94
+ ctx.append_assistant_response(text=final_text)
91
95
  return GenerationResponse(
92
96
  id=final_response.id or (last_response.id if last_response else None),
93
97
  text=final_text,
@@ -96,20 +100,21 @@ class TurnExecutor:
96
100
 
97
101
  async def run_turn_async(self, message: str) -> GenerationResponse:
98
102
  """Asynchronously advance one complete user turn with automatic tool calling."""
103
+ turn_ctx = self._context.spawn_turn_context() if self._stateless else self._context
99
104
  run_id = f"run_{secrets.token_hex(6)}"
100
105
  with self._telemetry_bridge.run_scope(
101
- run_id, message, self._context.system_prompt
106
+ run_id, message, turn_ctx.system_prompt
102
107
  ) as root_ctx:
103
108
  t0 = time.perf_counter()
104
109
  iterations = 0
105
110
  try:
106
- user_msg = self._context.append_user_message(message)
111
+ user_msg = turn_ctx.append_user_message(message)
107
112
  current_turn_messages: Sequence[Message] = [user_msg]
108
113
  response: GenerationResponse | None = None
109
114
 
110
115
  for _ in range(self._max_tool_iterations):
111
116
  iterations += 1
112
- req = self._context.build_generation_request(
117
+ req = turn_ctx.build_generation_request(
113
118
  current_turn_messages, self._tool_executor.tools
114
119
  )
115
120
 
@@ -117,9 +122,9 @@ class TurnExecutor:
117
122
  self._provider, req
118
123
  )
119
124
  if response.id:
120
- self._context.last_response_id = response.id
125
+ turn_ctx.last_response_id = response.id
121
126
 
122
- self._context.append_assistant_response(
127
+ turn_ctx.append_assistant_response(
123
128
  text=response.text,
124
129
  tool_calls=response.tool_calls,
125
130
  thinking=response.thinking,
@@ -128,6 +133,8 @@ class TurnExecutor:
128
133
  )
129
134
 
130
135
  if not response.tool_calls:
136
+ if self._stateless:
137
+ self._context.copy_from(turn_ctx)
131
138
  duration_ms = (time.perf_counter() - t0) * 1000
132
139
  self._telemetry_bridge.emit_run_end(
133
140
  root_ctx=root_ctx,
@@ -141,19 +148,21 @@ class TurnExecutor:
141
148
 
142
149
  # Execute tool calls and record results
143
150
  tool_results = await self.execute_tool_calls_async(response.tool_calls)
144
- self._context.append_tool_results(tool_results)
151
+ turn_ctx.append_tool_results(tool_results)
145
152
  current_turn_messages = tool_results
146
153
 
147
154
  # Iterations exhausted: perform terminal synthesis without tools
148
155
  iterations += 1
149
- final_req = self._context.build_final_request(current_turn_messages)
156
+ final_req = turn_ctx.build_final_request(current_turn_messages)
150
157
  final_response = await self._telemetry_bridge.generate_with_telemetry_async(
151
158
  self._provider, final_req
152
159
  )
153
160
  if final_response.id:
154
- self._context.last_response_id = final_response.id
161
+ turn_ctx.last_response_id = final_response.id
155
162
 
156
- result = self.finalize_loop_expiry(final_response, response)
163
+ result = self.finalize_loop_expiry(final_response, response, turn_ctx)
164
+ if self._stateless:
165
+ self._context.copy_from(turn_ctx)
157
166
  duration_ms = (time.perf_counter() - t0) * 1000
158
167
  self._telemetry_bridge.emit_run_end(
159
168
  root_ctx=root_ctx,
@@ -179,20 +188,21 @@ class TurnExecutor:
179
188
 
180
189
  async def stream_turn_async(self, message: str) -> AsyncIterator[StreamChunk]:
181
190
  """Asynchronously stream chunks across multi-turn tool execution."""
191
+ turn_ctx = self._context.spawn_turn_context() if self._stateless else self._context
182
192
  run_id = f"run_{secrets.token_hex(6)}"
183
193
  with self._telemetry_bridge.run_scope(
184
- run_id, message, self._context.system_prompt
194
+ run_id, message, turn_ctx.system_prompt
185
195
  ) as root_ctx:
186
196
  t0 = time.perf_counter()
187
197
  iterations = 0
188
198
  try:
189
- user_msg = self._context.append_user_message(message)
199
+ user_msg = turn_ctx.append_user_message(message)
190
200
  current_turn_messages: Sequence[Message] = [user_msg]
191
201
  response: GenerationResponse | None = None
192
202
 
193
203
  for _ in range(self._max_tool_iterations):
194
204
  iterations += 1
195
- req = self._context.build_generation_request(
205
+ req = turn_ctx.build_generation_request(
196
206
  current_turn_messages, self._tool_executor.tools, stream=True
197
207
  )
198
208
 
@@ -207,9 +217,9 @@ class TurnExecutor:
207
217
  response = await turn_stream.get_final_response()
208
218
 
209
219
  if response.id:
210
- self._context.last_response_id = response.id
220
+ turn_ctx.last_response_id = response.id
211
221
 
212
- self._context.append_assistant_response(
222
+ turn_ctx.append_assistant_response(
213
223
  text=response.text,
214
224
  tool_calls=response.tool_calls,
215
225
  thinking=response.thinking,
@@ -218,6 +228,8 @@ class TurnExecutor:
218
228
  )
219
229
 
220
230
  if not response.tool_calls:
231
+ if self._stateless:
232
+ self._context.copy_from(turn_ctx)
221
233
  duration_ms = (time.perf_counter() - t0) * 1000
222
234
  self._telemetry_bridge.emit_run_end(
223
235
  root_ctx=root_ctx,
@@ -231,12 +243,12 @@ class TurnExecutor:
231
243
 
232
244
  # Execute tool calls asynchronously
233
245
  tool_results = await self.execute_tool_calls_async(response.tool_calls)
234
- self._context.append_tool_results(tool_results)
246
+ turn_ctx.append_tool_results(tool_results)
235
247
  current_turn_messages = tool_results
236
248
 
237
249
  # Iterations exhausted: stream final synthesis without tools
238
250
  iterations += 1
239
- final_req = self._context.build_final_request(current_turn_messages, stream=True)
251
+ final_req = turn_ctx.build_final_request(current_turn_messages, stream=True)
240
252
  final_stream = await self._telemetry_bridge.generate_with_telemetry_stream_async(
241
253
  self._provider, final_req
242
254
  )
@@ -247,9 +259,11 @@ class TurnExecutor:
247
259
  if final_response is None:
248
260
  final_response = await final_stream.get_final_response()
249
261
  if final_response.id:
250
- self._context.last_response_id = final_response.id
262
+ turn_ctx.last_response_id = final_response.id
251
263
 
252
- result = self.finalize_loop_expiry(final_response, response)
264
+ result = self.finalize_loop_expiry(final_response, response, turn_ctx)
265
+ if self._stateless:
266
+ self._context.copy_from(turn_ctx)
253
267
  duration_ms = (time.perf_counter() - t0) * 1000
254
268
  self._telemetry_bridge.emit_run_end(
255
269
  root_ctx=root_ctx,
@@ -0,0 +1,14 @@
1
+ """Anthropic provider package."""
2
+
3
+ from .builder import AnthropicPayloadBuilder
4
+ from .parser import AnthropicResponseParser
5
+ from .provider import AnthropicProvider
6
+ from .stream import AnthropicStreamHandler
7
+
8
+ __all__ = [
9
+ "AnthropicPayloadBuilder",
10
+ "AnthropicProvider",
11
+ "AnthropicResponseParser",
12
+ "AnthropicStreamHandler",
13
+ ]
14
+