chuk-tool-processor 0.9.7__tar.gz → 0.10__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 (75) hide show
  1. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/PKG-INFO +666 -153
  2. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/README.md +665 -152
  3. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/pyproject.toml +35 -13
  4. chuk_tool_processor-0.10/src/chuk_tool_processor/__init__.py +114 -0
  5. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/core/processor.py +363 -44
  6. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/__init__.py +5 -8
  7. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/context.py +2 -5
  8. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/__init__.py +3 -0
  9. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/mcp_tool.py +8 -3
  10. chuk_tool_processor-0.10/src/chuk_tool_processor/mcp/models.py +87 -0
  11. chuk_tool_processor-0.10/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +162 -0
  12. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/stream_manager.py +94 -0
  13. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/tool_export_mixin.py +4 -4
  14. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/observability/metrics.py +3 -3
  15. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/observability/tracing.py +13 -12
  16. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/interface.py +7 -7
  17. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/providers/__init__.py +2 -1
  18. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/tool_export.py +1 -6
  19. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor.egg-info/PKG-INFO +666 -153
  20. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor.egg-info/SOURCES.txt +2 -0
  21. chuk_tool_processor-0.9.7/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -82
  22. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/setup.cfg +0 -0
  23. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/core/__init__.py +0 -0
  24. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/core/exceptions.py +0 -0
  25. {chuk_tool_processor-0.9.7/src/chuk_tool_processor → chuk_tool_processor-0.10/src/chuk_tool_processor/execution}/__init__.py +0 -0
  26. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
  27. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
  28. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
  29. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
  30. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
  31. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
  32. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
  33. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
  34. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
  35. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/formatter.py +0 -0
  36. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/helpers.py +0 -0
  37. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/metrics.py +0 -0
  38. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
  39. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
  40. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
  41. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
  42. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
  43. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
  44. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
  45. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
  46. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
  47. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/__init__.py +0 -0
  48. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
  49. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
  50. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/tool_call.py +0 -0
  51. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/tool_result.py +0 -0
  52. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/tool_spec.py +0 -0
  53. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/validated_tool.py +0 -0
  54. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/observability/__init__.py +0 -0
  55. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/observability/setup.py +0 -0
  56. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/__init__.py +0 -0
  57. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/discovery.py +0 -0
  58. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
  59. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
  60. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
  61. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
  62. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
  63. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
  64. /chuk_tool_processor-0.9.7/src/chuk_tool_processor/execution/__init__.py → /chuk_tool_processor-0.10/src/chuk_tool_processor/py.typed +0 -0
  65. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/__init__.py +0 -0
  66. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/auto_register.py +0 -0
  67. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/decorators.py +0 -0
  68. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/metadata.py +0 -0
  69. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/provider.py +0 -0
  70. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
  71. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/utils/__init__.py +0 -0
  72. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/utils/validation.py +0 -0
  73. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
  74. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
  75. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.9.7
3
+ Version: 0.10
4
4
  Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
5
5
  Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
6
6
  Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
@@ -26,65 +26,147 @@ Requires-Dist: psutil>=7.0.0
26
26
  Requires-Dist: pydantic>=2.11.3
27
27
  Requires-Dist: uuid>=1.30
28
28
 
29
- # CHUK Tool Processor
29
+ # CHUK Tool Processor — Production-grade execution for LLM tool calls
30
30
 
31
31
  [![PyPI](https://img.shields.io/pypi/v/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
32
32
  [![Python](https://img.shields.io/pypi/pyversions/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
33
33
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
34
+ [![Type Checked](https://img.shields.io/badge/type%20checked-PEP%20561-blue.svg)](https://www.python.org/dev/peps/pep-0561/)
35
+ [![Wheels](https://img.shields.io/badge/wheels-macOS%20%7C%20Linux%20%7C%20Windows-blue.svg)](https://pypi.org/project/chuk-tool-processor/)
36
+ [![OpenTelemetry](https://img.shields.io/badge/observability-OpenTelemetry%20%7C%20Prometheus-blue.svg)](docs/OBSERVABILITY.md)
34
37
 
35
- **The missing link between LLM tool calls and reliable execution.**
38
+ **Reliable tool execution for LLMs timeouts, retries, caching, rate limits, circuit breakers, and MCP integration — in one composable layer.**
36
39
 
37
- CHUK Tool Processor is a focused, production-ready framework that solves one problem exceptionally well: **processing tool calls from LLM outputs**. It's not a chatbot framework or LLM orchestration platform—it's the glue layer that bridges LLM responses and actual tool execution.
40
+ ---
38
41
 
39
- ## The Problem
42
+ ## The Missing Layer for Reliable Tool Execution
40
43
 
41
- When you build LLM applications, you face a gap:
44
+ LLMs are good at *calling* tools. The hard part is **executing** those tools reliably.
42
45
 
43
- 1. **LLM generates tool calls** in various formats (XML tags, OpenAI `tool_calls`, JSON)
44
- 2. **??? Mystery step ???** where you need to:
45
- - Parse those calls reliably
46
- - Handle timeouts, retries, failures
47
- - Cache expensive results
48
- - Rate limit API calls
49
- - Run untrusted code safely
50
- - Connect to external tool servers
51
- - Log everything for debugging
52
- 3. **Get results back** to continue the LLM conversation
46
+ **CHUK Tool Processor:**
47
+ - Parses tool calls from any model (Anthropic XML, OpenAI `tool_calls`, JSON)
48
+ - Executes them with **timeouts, retries, caching, rate limits, circuit breaker, observability**
49
+ - Runs tools locally, in **isolated subprocesses**, or **remote via MCP**
53
50
 
54
- Most frameworks give you steps 1 and 3, but step 2 is where the complexity lives. CHUK Tool Processor **is** step 2.
51
+ CHUK Tool Processor is the execution layer between LLM responses and real tools.
55
52
 
56
- ## Why chuk-tool-processor?
53
+ It sits **below** agent frameworks and prompt orchestration, and **above** raw tool implementations.
57
54
 
58
- ### It's a Building Block, Not a Framework
55
+ ```
56
+ LLM Output
57
+
58
+ CHUK Tool Processor
59
+
60
+ ┌──────────────┬────────────────────┐
61
+ │ Local Tools │ Remote Tools (MCP) │
62
+ └──────────────┴────────────────────┘
63
+ ```
59
64
 
60
- Unlike full-fledged LLM frameworks (LangChain, LlamaIndex, etc.), CHUK Tool Processor:
65
+ **How it works internally:**
61
66
 
62
- - ✅ **Does one thing well**: Process tool calls reliably
63
- - ✅ **Plugs into any LLM app**: Works with any framework or no framework
64
- - ✅ **Composable by design**: Stack strategies and wrappers like middleware
65
- - **No opinions about your LLM**: Bring your own OpenAI, Anthropic, local model
66
- - ❌ **Doesn't manage conversations**: That's your job
67
- - ❌ **Doesn't do prompt engineering**: Use whatever prompting you want
68
- - ❌ **Doesn't bundle an LLM client**: Use any client library you prefer
67
+ ```
68
+ LLM Output
69
+
70
+ Parsers (XML / OpenAI / JSON)
71
+
72
+ ┌─────────────────────────────┐
73
+ │ Execution Middleware │
74
+ │ (Applied in this order) │
75
+ │ • Cache │
76
+ │ • Rate Limit │
77
+ │ • Retry (with backoff) │
78
+ │ • Circuit Breaker │
79
+ └─────────────────────────────┘
80
+
81
+ Execution Strategy
82
+ ┌──────────────────────┐
83
+ │ • InProcess │ ← Fast, trusted
84
+ │ • Isolated/Subprocess│ ← Safe, untrusted
85
+ │ • Remote via MCP │ ← Distributed
86
+ └──────────────────────┘
87
+ ```
69
88
 
70
- ### It's Built for Production
89
+ Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework (LangChain, LlamaIndex, custom).
90
+
91
+ ## Executive TL;DR
92
+
93
+ * **Parse any format:** `XML` (Anthropic), `OpenAI tool_calls`, or raw `JSON`
94
+ * **Execute with production policies:** timeouts/retries/cache/rate-limits/circuit-breaker/idempotency
95
+ * **Run anywhere:** locally (fast), isolated (subprocess sandbox), or remote via MCP (HTTP/STDIO/SSE)
96
+
97
+ ```python
98
+ import asyncio
99
+ from chuk_tool_processor import ToolProcessor, register_tool, initialize
100
+
101
+ @register_tool(name="weather")
102
+ class WeatherTool:
103
+ async def execute(self, city: str) -> dict:
104
+ return {"temp": 72, "condition": "sunny", "city": city}
105
+
106
+ async def main():
107
+ await initialize()
108
+ async with ToolProcessor(enable_caching=True, enable_retries=True) as p:
109
+ # Works with OpenAI, Anthropic, or JSON formats
110
+ result = await p.process('<tool name="weather" args=\'{"city": "SF"}\'/>')
111
+ print(result[0].result) # {'temp': 72, 'condition': 'sunny', 'city': 'SF'}
71
112
 
72
- Research code vs production code is about handling the edges:
113
+ asyncio.run(main())
114
+ ```
73
115
 
74
- - **Timeouts**: Every tool execution has proper timeout handling
75
- - **Retries**: Automatic retry with exponential backoff and deadline awareness
76
- - **Rate Limiting**: Global and per-tool rate limits with sliding windows
77
- - **Caching**: Intelligent result caching with TTL and idempotency key support
78
- - **Circuit Breakers**: Prevent cascading failures with automatic fault detection
79
- - **Error Handling**: Machine-readable error codes with structured details
80
- - **Observability**: Structured logging, metrics, request tracing
81
- - **Safety**: Subprocess isolation for untrusted code
82
- - **Type Safety**: Pydantic validation with LLM-friendly argument coercion
83
- - **Tool Discovery**: Formal schema export (OpenAI, Anthropic, MCP formats)
116
+ > **If you only remember three things:**
117
+ >
118
+ > 1. **Parse** `XML`, `OpenAI tool_calls`, or raw `JSON` automatically
119
+ > 2. **Execute** with timeouts/retries/cache/rate-limits/circuit-breaker
120
+ > 3. **Run** tools locally, isolated (subprocess), or remote via MCP
121
+
122
+ ## When to Use This
123
+
124
+ Use **CHUK Tool Processor** when:
125
+ - Your LLM calls tools or APIs
126
+ - You need **retries, timeouts, caching, or rate limits**
127
+ - You need to **run untrusted tools safely**
128
+ - Your tools are **local or remote (MCP)**
129
+
130
+ Do **not** use this if:
131
+ - You want an agent framework
132
+ - You want conversation flow/memory orchestration
133
+
134
+ **This is the execution layer, not the agent.**
135
+
136
+ > **Not a framework.**
137
+ > If LangChain/LlamaIndex help decide *which* tool to call,
138
+ > CHUK Tool Processor makes sure the tool call **actually succeeds**.
139
+
140
+ ## Table of Contents
141
+
142
+ - [The Problem](#the-problem)
143
+ - [Why chuk-tool-processor?](#why-chuk-tool-processor)
144
+ - [Compatibility Matrix](#compatibility-matrix)
145
+ - [Developer Experience Highlights](#developer-experience-highlights)
146
+ - [Quick Start](#quick-start)
147
+ - [Documentation Quick Reference](#documentation-quick-reference)
148
+ - [Choose Your Path](#choose-your-path)
149
+ - [Core Concepts](#core-concepts)
150
+ - [Getting Started](#getting-started)
151
+ - [Advanced Topics](#advanced-topics)
152
+ - [Configuration](#configuration)
153
+ - [Architecture Principles](#architecture-principles)
154
+ - [Examples](#examples)
155
+ - [FAQ](#faq)
156
+ - [Comparison with Other Tools](#comparison-with-other-tools)
157
+ - [Development & Publishing](#development--publishing)
158
+ - [Stability & Versioning](#stability--versioning)
159
+ - [Contributing & Support](#contributing--support)
160
+
161
+ ## The Problem
84
162
 
85
- ### It's About Stacks
163
+ LLMs generate tool calls. **The hard part is executing them reliably.**
164
+
165
+ CHUK Tool Processor **is that execution layer.**
166
+
167
+ ## Why chuk-tool-processor?
86
168
 
87
- CHUK Tool Processor uses a **composable stack architecture**:
169
+ **Composable execution layers:**
88
170
 
89
171
  ```
90
172
  ┌─────────────────────────────────┐
@@ -104,7 +186,7 @@ CHUK Tool Processor uses a **composable stack architecture**:
104
186
  ├─────────────────────────────────┤
105
187
  │ Execution Strategy │ ← How to run tools
106
188
  │ • InProcess (fast) │
107
- │ • Subprocess (isolated) │
189
+ │ • Isolated (subprocess) │
108
190
  ├─────────────────────────────────┤
109
191
  │ Tool Registry │ ← Your registered tools
110
192
  └─────────────────────────────────┘
@@ -112,8 +194,40 @@ CHUK Tool Processor uses a **composable stack architecture**:
112
194
 
113
195
  Each layer is **optional** and **configurable**. Mix and match what you need.
114
196
 
197
+ ### It's a Building Block, Not a Framework
198
+
199
+ Unlike full-fledged LLM frameworks (LangChain, LlamaIndex, etc.), CHUK Tool Processor:
200
+
201
+ - ✅ **Does one thing well**: Process tool calls reliably
202
+ - ✅ **Plugs into any LLM app**: Works with any framework or no framework
203
+ - ✅ **Composable by design**: Stack strategies and wrappers like middleware
204
+ - ✅ **No opinions about your LLM**: Bring your own OpenAI, Anthropic, local model
205
+ - ❌ **Doesn't manage conversations**: That's your job
206
+ - ❌ **Doesn't do prompt engineering**: Use whatever prompting you want
207
+ - ❌ **Doesn't bundle an LLM client**: Use any client library you prefer
208
+
209
+ ### It's Built for Production
210
+
211
+ Research code vs production code is about handling the edges. CHUK Tool Processor includes:
212
+
213
+ - ✅ **Timeouts** — Every tool execution has proper timeout handling
214
+ - ✅ **Retries** — Automatic retry with exponential backoff and deadline awareness
215
+ - ✅ **Rate Limiting** — Global and per-tool rate limits with sliding windows → [CONFIGURATION.md](docs/CONFIGURATION.md)
216
+ - ✅ **Caching** — Intelligent result caching with TTL and idempotency key support
217
+ - ✅ **Circuit Breakers** — Prevent cascading failures with automatic fault detection
218
+ - ✅ **Idempotency** — SHA256-based deduplication of LLM retry quirks
219
+ - ✅ **Error Handling** — Machine-readable error codes with structured details → [ERRORS.md](docs/ERRORS.md)
220
+ - ✅ **Observability** — Structured logging, metrics, OpenTelemetry tracing → [OBSERVABILITY.md](docs/OBSERVABILITY.md)
221
+ - ✅ **Safety** — Subprocess isolation for untrusted code (zero crash blast radius)
222
+ - ✅ **Type Safety** — PEP 561 compliant with full mypy support
223
+ - ✅ **Resource Management** — Context managers for automatic cleanup
224
+ - ✅ **Tool Discovery** — Formal schema export (OpenAI, Anthropic, MCP formats)
225
+ - ✅ **Cancellation** — Cooperative cancellation with request-scoped deadlines
226
+
115
227
  ## Compatibility Matrix
116
228
 
229
+ Runs the same on macOS, Linux, and Windows — locally, serverside, and inside containers.
230
+
117
231
  | Component | Supported Versions | Notes |
118
232
  |-----------|-------------------|-------|
119
233
  | **Python** | 3.11, 3.12, 3.13 | Python 3.11+ required |
@@ -131,6 +245,19 @@ Each layer is **optional** and **configurable**. Mix and match what you need.
131
245
  - ✅ Anthropic Claude 3 (Opus, Sonnet, Haiku)
132
246
  - ✅ Local models (Ollama, LM Studio)
133
247
 
248
+ ## Developer Experience Highlights
249
+
250
+ **What makes CHUK Tool Processor easy to use:**
251
+
252
+ * **Auto-parsing**: XML (Claude), OpenAI `tool_calls`, direct JSON—all work automatically
253
+ * **One call**: `process()` handles multiple calls & formats in a single invocation
254
+ * **Auto-coercion**: Pydantic-powered argument cleanup (whitespace, type conversion, extra fields ignored)
255
+ * **Safe defaults**: timeouts, retries, caching toggles built-in
256
+ * **Observability in one line**: `setup_observability(...)` for traces + metrics
257
+ * **MCP in one call**: `setup_mcp_http_streamable|stdio|sse(...)` connects to remote tools instantly
258
+ * **Context managers**: `async with ToolProcessor() as p:` ensures automatic cleanup
259
+ * **Full type safety**: PEP 561 compliant—mypy, pyright, and IDEs get complete type information
260
+
134
261
  ## Quick Start
135
262
 
136
263
  ### Installation
@@ -143,19 +270,95 @@ pip install chuk-tool-processor
143
270
 
144
271
  # Using uv (recommended)
145
272
  uv pip install chuk-tool-processor
273
+ ```
274
+
275
+ <details>
276
+ <summary><strong>Install from source or with extras</strong></summary>
146
277
 
147
- # Or from source
278
+ ```bash
279
+ # From source
148
280
  git clone https://github.com/chrishayuk/chuk-tool-processor.git
149
281
  cd chuk-tool-processor
150
282
  uv pip install -e .
283
+
284
+ # With observability extras (OpenTelemetry + Prometheus)
285
+ pip install chuk-tool-processor[observability]
286
+
287
+ # With MCP extras
288
+ pip install chuk-tool-processor[mcp]
289
+
290
+ # All extras
291
+ pip install chuk-tool-processor[all]
292
+ ```
293
+
294
+ </details>
295
+
296
+ <details>
297
+ <summary><strong>Type Checking Support (PEP 561 compliant)</strong></summary>
298
+
299
+ CHUK Tool Processor includes **full type checking support**:
300
+
301
+ ```python
302
+ # mypy, pyright, and IDEs get full type information!
303
+ from chuk_tool_processor import ToolProcessor, ToolCall, ToolResult
304
+
305
+ async with ToolProcessor() as processor:
306
+ # Full autocomplete and type checking
307
+ results: list[ToolResult] = await processor.process(llm_output)
308
+ tools: list[str] = await processor.list_tools()
151
309
  ```
152
310
 
311
+ **Features:**
312
+ - ✅ `py.typed` marker for PEP 561 compliance
313
+ - ✅ Comprehensive type hints on all public APIs
314
+ - ✅ Works with mypy, pyright, pylance
315
+ - ✅ Full IDE autocomplete support
316
+
317
+ **No special mypy configuration needed** - just import and use!
318
+
319
+ </details>
320
+
153
321
  ## 60-Second Quick Start
154
322
 
155
- **Absolutely minimal example** See `examples/hello_tool.py`:
323
+ ### From raw LLM output to safe execution in 3 lines
324
+
325
+ ```python
326
+ from chuk_tool_processor import ToolProcessor, initialize
327
+
328
+ await initialize()
329
+ async with ToolProcessor() as p:
330
+ results = await p.process('<tool name="calculator" args=\'{"operation":"multiply","a":15,"b":23}\'/>')
331
+ ```
332
+
333
+ **Note:** This assumes you've registered a "calculator" tool. See complete example below.
334
+
335
+ ### Works with Both OpenAI and Anthropic (No Adapters Needed)
336
+
337
+ ```python
338
+ from chuk_tool_processor import ToolProcessor, register_tool, initialize
339
+
340
+ @register_tool(name="search")
341
+ class SearchTool:
342
+ async def execute(self, query: str) -> dict:
343
+ return {"results": [f"Found: {query}"]}
344
+
345
+ await initialize()
346
+ async with ToolProcessor() as p:
347
+ # OpenAI format
348
+ openai_response = {"tool_calls": [{"type": "function", "function": {"name": "search", "arguments": '{"query": "Python"}'}}]}
349
+
350
+ # Anthropic format
351
+ anthropic_response = '<tool name="search" args=\'{"query": "Python"}\'/>'
352
+
353
+ # Both work identically
354
+ results_openai = await p.process(openai_response)
355
+ results_anthropic = await p.process(anthropic_response)
356
+ ```
357
+
358
+ **Absolutely minimal example** → See `examples/01_getting_started/hello_tool.py`:
156
359
 
157
360
  ```bash
158
- python examples/hello_tool.py
361
+ python examples/01_getting_started/hello_tool.py
159
362
  ```
160
363
 
161
364
  Single file that demonstrates:
@@ -171,8 +374,7 @@ Copy-paste this into a file and run it:
171
374
 
172
375
  ```python
173
376
  import asyncio
174
- from chuk_tool_processor.core.processor import ToolProcessor
175
- from chuk_tool_processor.registry import initialize, register_tool
377
+ from chuk_tool_processor import ToolProcessor, register_tool, initialize
176
378
 
177
379
  # Step 1: Define a tool
178
380
  @register_tool(name="calculator")
@@ -186,29 +388,142 @@ class Calculator:
186
388
  # Step 2: Process LLM output
187
389
  async def main():
188
390
  await initialize()
189
- processor = ToolProcessor()
190
391
 
191
- # Your LLM returned this tool call
192
- llm_output = '<tool name="calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>'
392
+ # Use context manager for automatic cleanup
393
+ async with ToolProcessor() as processor:
394
+ # Your LLM returned this tool call
395
+ llm_output = '<tool name="calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>'
193
396
 
194
- # Process it
195
- results = await processor.process(llm_output)
397
+ # Process it
398
+ results = await processor.process(llm_output)
196
399
 
197
- # Each result is a ToolExecutionResult with: tool, args, result, error, duration, cached
198
- # results[0].result contains the tool output
199
- # results[0].error contains any error message (None if successful)
200
- if results[0].error:
201
- print(f"Error: {results[0].error}")
202
- else:
203
- print(results[0].result) # {'result': 345}
400
+ # Each result is a ToolResult with: tool, result, error, duration, cached
401
+ if results[0].error:
402
+ print(f"Error: {results[0].error}")
403
+ else:
404
+ print(results[0].result) # {'result': 345}
405
+
406
+ # Processor automatically cleaned up!
204
407
 
205
408
  asyncio.run(main())
206
409
  ```
207
410
 
208
- **That's it.** You now have production-ready tool execution with timeouts, retries, and caching.
411
+ **That's it.** You now have production-ready tool execution with:
412
+ - ✅ Automatic timeouts, retries, and caching
413
+ - ✅ Clean resource management (context manager)
414
+ - ✅ Full type checking support
209
415
 
210
416
  > **Why not just use OpenAI tool calls?**
211
- > OpenAI's function calling is great for parsing, but you still need: parsing multiple formats (Anthropic XML, etc.), timeouts, retries, rate limits, caching, subprocess isolation, and connecting to external MCP servers. CHUK Tool Processor **is** that missing middle layer.
417
+ > OpenAI's function calling is great for parsing, but you still need: parsing multiple formats (Anthropic XML, etc.), timeouts, retries, rate limits, caching, subprocess isolation, connecting to external MCP servers, and **per-tool** policy control with cross-provider parsing and MCP fan-out. CHUK Tool Processor **is** that missing middle layer.
418
+
419
+ ## Quick Decision Tree (Commit This to Memory)
420
+
421
+ ```
422
+ ╭──────────────────────────────────────────╮
423
+ │ Do you trust the code you're executing? │
424
+ │ ✅ Yes → InProcessStrategy │
425
+ │ ⚠️ No → IsolatedStrategy (sandboxed) │
426
+ │ │
427
+ │ Where do your tools live? │
428
+ │ 📦 Local → @register_tool │
429
+ │ 🌐 Remote → setup_mcp_http_streamable │
430
+ ╰──────────────────────────────────────────╯
431
+ ```
432
+
433
+ **That's all you need to pick the right pattern.**
434
+
435
+ ## Registry & Processor Lifecycle
436
+
437
+ Understanding the lifecycle helps you use CHUK Tool Processor correctly:
438
+
439
+ 1. **`await initialize()`** — loads the global registry; call **once per process** at application startup
440
+ 2. Create a **`ToolProcessor(...)`** (or use the one returned by `setup_mcp_*`)
441
+ 3. Use **`async with ToolProcessor() as p:`** to ensure cleanup
442
+ 4. **`setup_mcp_*`** returns `(processor, manager)` — reuse that `processor`
443
+ 5. If you need a custom registry, pass it explicitly to the strategy
444
+ 6. You rarely need `get_default_registry()` unless you're composing advanced setups
445
+
446
+ **⚠️ Important:** `initialize()` must run **once per process**, not once per request or processor instance. Running it multiple times will duplicate tools in the registry.
447
+
448
+ ```python
449
+ # Standard pattern
450
+ await initialize() # Step 1: Register tools
451
+
452
+ async with ToolProcessor() as p: # Step 2-3: Create + auto cleanup
453
+ results = await p.process(llm_output)
454
+ # Step 4: Processor automatically cleaned up on exit
455
+ ```
456
+
457
+ ## Production Features by Example
458
+
459
+ ### Idempotency & Deduplication
460
+
461
+ Automatically deduplicate LLM retry quirks using SHA256-based idempotency keys:
462
+
463
+ ```python
464
+ from chuk_tool_processor import ToolProcessor, initialize
465
+
466
+ await initialize()
467
+ async with ToolProcessor(enable_caching=True, cache_ttl=300) as p:
468
+ # LLM retries the same call (common with streaming or errors)
469
+ call1 = '<tool name="search" args=\'{"query": "Python"}\'/>'
470
+ call2 = '<tool name="search" args=\'{"query": "Python"}\'/>' # Identical
471
+
472
+ results1 = await p.process(call1) # Executes
473
+ results2 = await p.process(call2) # Cache hit! (idempotency key match)
474
+
475
+ assert results1[0].cached == False
476
+ assert results2[0].cached == True
477
+ ```
478
+
479
+ ### Cancellation & Deadlines
480
+
481
+ Cooperative cancellation with request-scoped deadlines:
482
+
483
+ ```python
484
+ import asyncio
485
+ from chuk_tool_processor import ToolProcessor, initialize
486
+
487
+ async def main():
488
+ await initialize()
489
+ async with ToolProcessor(default_timeout=60.0) as p:
490
+ try:
491
+ # Hard deadline for the whole batch (e.g., user request budget)
492
+ async with asyncio.timeout(5.0):
493
+ async for event in p.astream('<tool name="slow_report" args=\'{"n": 1000000}\'/>'):
494
+ print("chunk:", event)
495
+ except TimeoutError:
496
+ print("Request cancelled: deadline exceeded")
497
+ # Processor automatically cancels the tool and cleans up
498
+
499
+ asyncio.run(main())
500
+ ```
501
+
502
+ ### Per-Tool Policy Overrides
503
+
504
+ Override timeouts, retries, and rate limits per tool:
505
+
506
+ ```python
507
+ from chuk_tool_processor import ToolProcessor, initialize
508
+
509
+ await initialize()
510
+ async with ToolProcessor(
511
+ default_timeout=30.0,
512
+ enable_retries=True,
513
+ max_retries=2,
514
+ enable_rate_limiting=True,
515
+ global_rate_limit=120, # 120 requests/min across all tools
516
+ tool_rate_limits={
517
+ "expensive_api": (5, 60), # 5 requests per 60 seconds
518
+ "fast_local": (1000, 60), # 1000 requests per 60 seconds
519
+ }
520
+ ) as p:
521
+ # Tools run with their specific policies
522
+ results = await p.process('''
523
+ <tool name="expensive_api" args='{"q":"abc"}'/>
524
+ <tool name="fast_local" args='{"data":"xyz"}'/>
525
+ ''')
526
+ ```
212
527
 
213
528
  ## Documentation Quick Reference
214
529
 
@@ -217,17 +532,19 @@ asyncio.run(main())
217
532
  | 📘 [CONFIGURATION.md](docs/CONFIGURATION.md) | **All config knobs & defaults**: ToolProcessor options, timeouts, retry policy, rate limits, circuit breakers, caching, environment variables |
218
533
  | 🚨 [ERRORS.md](docs/ERRORS.md) | **Error taxonomy**: All error codes, exception classes, error details structure, handling patterns, retryability guide |
219
534
  | 📊 [OBSERVABILITY.md](docs/OBSERVABILITY.md) | **Metrics & tracing**: OpenTelemetry setup, Prometheus metrics, spans reference, PromQL queries |
220
- | 🔌 [examples/hello_tool.py](examples/hello_tool.py) | **60-second starter**: Single-file, copy-paste-and-run example |
535
+ | 🔌 [examples/01_getting_started/hello_tool.py](examples/01_getting_started/hello_tool.py) | **60-second starter**: Single-file, copy-paste-and-run example |
221
536
  | 🎯 [examples/](examples/) | **20+ working examples**: MCP integration, OAuth flows, streaming, production patterns |
222
537
 
223
538
  ## Choose Your Path
224
539
 
540
+ **Use this when OpenAI/Claude tool calling is not enough** — because you need retries, caching, rate limits, subprocess isolation, or MCP integration.
541
+
225
542
  | Your Goal | What You Need | Where to Look |
226
543
  |-----------|---------------|---------------|
227
544
  | ☕ **Just process LLM tool calls** | Basic tool registration + processor | [60-Second Quick Start](#60-second-quick-start) |
228
545
  | 🔌 **Connect to external tools** | MCP integration (HTTP/STDIO/SSE) | [MCP Integration](#5-mcp-integration-external-tools) |
229
546
  | 🛡️ **Production deployment** | Timeouts, retries, rate limits, caching | [CONFIGURATION.md](docs/CONFIGURATION.md) |
230
- | 🔒 **Run untrusted code safely** | Subprocess isolation strategy | [Subprocess Strategy](#using-subprocess-strategy) |
547
+ | 🔒 **Run untrusted code safely** | Isolated strategy (subprocess) | [Isolated Strategy](#using-isolated-strategy) |
231
548
  | 📊 **Monitor and observe** | OpenTelemetry + Prometheus | [OBSERVABILITY.md](docs/OBSERVABILITY.md) |
232
549
  | 🌊 **Stream incremental results** | StreamingTool pattern | [StreamingTool](#streamingtool-real-time-results) |
233
550
  | 🚨 **Handle errors reliably** | Error codes & taxonomy | [ERRORS.md](docs/ERRORS.md) |
@@ -239,8 +556,7 @@ Here are the most common patterns you'll use:
239
556
  **Pattern 1: Local tools only**
240
557
  ```python
241
558
  import asyncio
242
- from chuk_tool_processor.core.processor import ToolProcessor
243
- from chuk_tool_processor.registry import initialize, register_tool
559
+ from chuk_tool_processor import ToolProcessor, register_tool, initialize
244
560
 
245
561
  @register_tool(name="my_tool")
246
562
  class MyTool:
@@ -249,20 +565,22 @@ class MyTool:
249
565
 
250
566
  async def main():
251
567
  await initialize()
252
- processor = ToolProcessor()
253
568
 
254
- llm_output = '<tool name="my_tool" args=\'{"arg": "hello"}\'/>'
255
- results = await processor.process(llm_output)
256
- print(results[0].result) # {'result': 'Processed: hello'}
569
+ async with ToolProcessor() as processor:
570
+ llm_output = '<tool name="my_tool" args=\'{"arg": "hello"}\'/>'
571
+ results = await processor.process(llm_output)
572
+ print(results[0].result) # {'result': 'Processed: hello'}
257
573
 
258
574
  asyncio.run(main())
259
575
  ```
260
576
 
577
+ <details>
578
+ <summary><strong>More patterns: MCP integration (local + remote tools)</strong></summary>
579
+
261
580
  **Pattern 2: Mix local + remote MCP tools (Notion)**
262
581
  ```python
263
582
  import asyncio
264
- from chuk_tool_processor.registry import initialize, register_tool
265
- from chuk_tool_processor.mcp import setup_mcp_http_streamable
583
+ from chuk_tool_processor import register_tool, initialize, setup_mcp_http_streamable
266
584
 
267
585
  @register_tool(name="local_calculator")
268
586
  class Calculator:
@@ -292,10 +610,13 @@ async def main():
292
610
  print(f"Local result: {results[0].result}")
293
611
  print(f"Notion result: {results[1].result}")
294
612
 
613
+ # Clean up
614
+ await manager.close()
615
+
295
616
  asyncio.run(main())
296
617
  ```
297
618
 
298
- See `examples/notion_oauth.py` for complete OAuth flow.
619
+ See `examples/04_mcp_integration/notion_oauth.py` for complete OAuth flow.
299
620
 
300
621
  **Pattern 3: Local SQLite database via STDIO**
301
622
  ```python
@@ -334,7 +655,9 @@ async def main():
334
655
  asyncio.run(main())
335
656
  ```
336
657
 
337
- See `examples/stdio_sqlite.py` for complete working example.
658
+ See `examples/04_mcp_integration/stdio_sqlite.py` for complete working example.
659
+
660
+ </details>
338
661
 
339
662
  ## Core Concepts
340
663
 
@@ -347,8 +670,10 @@ The **registry** is where you register tools for execution. Tools can be:
347
670
  - **StreamingTool** for real-time incremental results
348
671
  - **Functions** registered via `register_fn_tool()`
349
672
 
673
+ > **Note:** The registry is global, processors are scoped.
674
+
350
675
  ```python
351
- from chuk_tool_processor.registry import register_tool
676
+ from chuk_tool_processor import register_tool
352
677
  from chuk_tool_processor.models.validated_tool import ValidatedTool
353
678
  from pydantic import BaseModel, Field
354
679
 
@@ -374,18 +699,16 @@ class WeatherTool(ValidatedTool):
374
699
  | Strategy | Use Case | Trade-offs |
375
700
  |----------|----------|------------|
376
701
  | **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
377
- | **SubprocessStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
702
+ | **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
378
703
 
379
704
  ```python
380
705
  import asyncio
381
- from chuk_tool_processor.core.processor import ToolProcessor
382
- from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
383
- from chuk_tool_processor.registry import get_default_registry
706
+ from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
384
707
 
385
708
  async def main():
386
709
  registry = await get_default_registry()
387
710
  processor = ToolProcessor(
388
- strategy=SubprocessStrategy(
711
+ strategy=IsolatedStrategy(
389
712
  registry=registry,
390
713
  max_workers=4,
391
714
  default_timeout=30.0
@@ -396,6 +719,8 @@ async def main():
396
719
  asyncio.run(main())
397
720
  ```
398
721
 
722
+ **Note:** `IsolatedStrategy` is an alias of `SubprocessStrategy` for backwards compatibility. Use `IsolatedStrategy` for clarity—it better communicates the security boundary intent.
723
+
399
724
  ### 3. Execution Wrappers (Middleware)
400
725
 
401
726
  **Wrappers** add production features as composable layers:
@@ -461,6 +786,8 @@ Connect to **remote tool servers** using the [Model Context Protocol](https://mo
461
786
 
462
787
  #### HTTP Streamable (⭐ Recommended for Cloud Services)
463
788
 
789
+ **Use for:** Cloud SaaS services (OAuth, long-running streams, resilient reconnects)
790
+
464
791
  Modern HTTP streaming transport for cloud-based MCP servers like Notion:
465
792
 
466
793
  ```python
@@ -489,8 +816,13 @@ results = await processor.process(
489
816
  )
490
817
  ```
491
818
 
819
+ <details>
820
+ <summary><strong>Other MCP Transports (STDIO for local tools, SSE for legacy)</strong></summary>
821
+
492
822
  #### STDIO (Best for Local/On-Device Tools)
493
823
 
824
+ **Use for:** Local/embedded tools and databases (SQLite, file systems, local services)
825
+
494
826
  For running local MCP servers as subprocesses—great for databases, file systems, and local tools:
495
827
 
496
828
  ```python
@@ -529,6 +861,8 @@ results = await processor.process(
529
861
 
530
862
  #### SSE (Legacy Support)
531
863
 
864
+ **Use for:** Legacy compatibility only. Prefer HTTP Streamable for new integrations.
865
+
532
866
  For backward compatibility with older MCP servers using Server-Sent Events:
533
867
 
534
868
  ```python
@@ -550,6 +884,8 @@ processor, manager = await setup_mcp_sse(
550
884
  )
551
885
  ```
552
886
 
887
+ </details>
888
+
553
889
  **Transport Comparison:**
554
890
 
555
891
  | Transport | Use Case | Real Examples |
@@ -558,6 +894,18 @@ processor, manager = await setup_mcp_sse(
558
894
  | **STDIO** | Local tools, databases | SQLite (`mcp-server-sqlite`), Echo (`chuk-mcp-echo`) |
559
895
  | **SSE** | Legacy cloud services | Atlassian (`mcp.atlassian.com`) |
560
896
 
897
+ **How MCP fits into the architecture:**
898
+
899
+ ```
900
+ LLM Output
901
+
902
+ Tool Processor
903
+
904
+ ┌──────────────┬────────────────────┐
905
+ │ Local Tools │ Remote Tools (MCP) │
906
+ └──────────────┴────────────────────┘
907
+ ```
908
+
561
909
  **Relationship with [chuk-mcp](https://github.com/chrishayuk/chuk-mcp):**
562
910
  - `chuk-mcp` is a low-level MCP protocol client (handles transports, protocol negotiation)
563
911
  - `chuk-tool-processor` wraps `chuk-mcp` to integrate external tools into your execution pipeline
@@ -571,7 +919,7 @@ CHUK Tool Processor supports multiple patterns for defining tools:
571
919
 
572
920
  #### Simple Function-Based Tools
573
921
  ```python
574
- from chuk_tool_processor.registry.auto_register import register_fn_tool
922
+ from chuk_tool_processor import register_fn_tool
575
923
  from datetime import datetime
576
924
  from zoneinfo import ZoneInfo
577
925
 
@@ -629,17 +977,26 @@ class FileProcessor(StreamingTool):
629
977
 
630
978
  ```python
631
979
  import asyncio
632
- from chuk_tool_processor.core.processor import ToolProcessor
633
- from chuk_tool_processor.registry import initialize
980
+ from chuk_tool_processor import ToolProcessor, initialize
634
981
 
635
982
  async def main():
636
983
  await initialize()
637
984
  processor = ToolProcessor()
638
- async for event in processor.astream('<tool name="file_processor" args=\'{"file_path":"README.md"}\'/>'):
639
- # 'event' is a streamed chunk (either your Result model instance or a dict)
640
- line = event["line"] if isinstance(event, dict) else getattr(event, "line", None)
641
- content = event["content"] if isinstance(event, dict) else getattr(event, "content", None)
642
- print(f"Line {line}: {content}")
985
+
986
+ # Stream can be cancelled by breaking or raising an exception
987
+ try:
988
+ async for event in processor.astream('<tool name="file_processor" args=\'{"file_path":"README.md"}\'/>'):
989
+ # 'event' is a streamed chunk (either your Result model instance or a dict)
990
+ line = event["line"] if isinstance(event, dict) else getattr(event, "line", None)
991
+ content = event["content"] if isinstance(event, dict) else getattr(event, "content", None)
992
+ print(f"Line {line}: {content}")
993
+
994
+ # Example: cancel after 100 lines
995
+ if line and line > 100:
996
+ break # Cleanup happens automatically
997
+ except asyncio.CancelledError:
998
+ # Stream cleanup is automatic even on cancellation
999
+ pass
643
1000
 
644
1001
  asyncio.run(main())
645
1002
  ```
@@ -648,23 +1005,32 @@ asyncio.run(main())
648
1005
 
649
1006
  #### Basic Usage
650
1007
 
651
- Call `await initialize()` once at startup to load your registry.
1008
+ Call `await initialize()` once at startup to load your registry. Use context managers for automatic cleanup:
652
1009
 
653
1010
  ```python
654
1011
  import asyncio
655
- from chuk_tool_processor.core.processor import ToolProcessor
656
- from chuk_tool_processor.registry import initialize
1012
+ from chuk_tool_processor import ToolProcessor, initialize
657
1013
 
658
1014
  async def main():
659
1015
  await initialize()
660
- processor = ToolProcessor()
661
- llm_output = '<tool name="calculator" args=\'{"operation":"add","a":2,"b":3}\'/>'
662
- results = await processor.process(llm_output)
663
- for result in results:
664
- if result.error:
665
- print(f"Error: {result.error}")
666
- else:
667
- print(f"Success: {result.result}")
1016
+
1017
+ # Context manager automatically handles cleanup
1018
+ async with ToolProcessor() as processor:
1019
+ # Discover available tools
1020
+ tools = await processor.list_tools()
1021
+ print(f"Available tools: {tools}")
1022
+
1023
+ # Process LLM output
1024
+ llm_output = '<tool name="calculator" args=\'{"operation":"add","a":2,"b":3}\'/>'
1025
+ results = await processor.process(llm_output)
1026
+
1027
+ for result in results:
1028
+ if result.error:
1029
+ print(f"Error: {result.error}")
1030
+ else:
1031
+ print(f"Success: {result.result}")
1032
+
1033
+ # Processor automatically cleaned up here!
668
1034
 
669
1035
  asyncio.run(main())
670
1036
  ```
@@ -672,21 +1038,32 @@ asyncio.run(main())
672
1038
  #### Production Configuration
673
1039
 
674
1040
  ```python
675
- from chuk_tool_processor.core.processor import ToolProcessor
1041
+ from chuk_tool_processor import ToolProcessor, initialize
1042
+ import asyncio
676
1043
 
677
- processor = ToolProcessor(
678
- # Execution settings
679
- default_timeout=30.0,
680
- max_concurrency=20,
1044
+ async def main():
1045
+ await initialize()
681
1046
 
682
- # Production features
683
- enable_caching=True,
684
- cache_ttl=600,
685
- enable_rate_limiting=True,
686
- global_rate_limit=100,
687
- enable_retries=True,
688
- max_retries=3
689
- )
1047
+ # Use context manager with production config
1048
+ async with ToolProcessor(
1049
+ # Execution settings
1050
+ default_timeout=30.0,
1051
+ max_concurrency=20,
1052
+
1053
+ # Production features
1054
+ enable_caching=True,
1055
+ cache_ttl=600,
1056
+ enable_rate_limiting=True,
1057
+ global_rate_limit=100,
1058
+ enable_retries=True,
1059
+ max_retries=3
1060
+ ) as processor:
1061
+ # Use processor...
1062
+ results = await processor.process(llm_output)
1063
+
1064
+ # Automatic cleanup on exit
1065
+
1066
+ asyncio.run(main())
690
1067
  ```
691
1068
 
692
1069
  ### Advanced Production Features
@@ -698,7 +1075,7 @@ Beyond basic configuration, CHUK Tool Processor includes several advanced featur
698
1075
  Prevent cascading failures by automatically opening circuits for failing tools:
699
1076
 
700
1077
  ```python
701
- from chuk_tool_processor.core.processor import ToolProcessor
1078
+ from chuk_tool_processor import ToolProcessor
702
1079
 
703
1080
  processor = ToolProcessor(
704
1081
  enable_circuit_breaker=True,
@@ -740,8 +1117,8 @@ assert call1.idempotency_key == call2.idempotency_key
740
1117
 
741
1118
  # Used automatically by caching layer
742
1119
  processor = ToolProcessor(enable_caching=True)
743
- results1 = await processor.execute([call1]) # Executes
744
- results2 = await processor.execute([call2]) # Cache hit!
1120
+ results1 = await processor.process([call1]) # Executes
1121
+ results2 = await processor.process([call2]) # Cache hit!
745
1122
  ```
746
1123
 
747
1124
  **Benefits:**
@@ -749,6 +1126,8 @@ results2 = await processor.execute([call2]) # Cache hit!
749
1126
  - Deterministic cache keys
750
1127
  - No manual key management needed
751
1128
 
1129
+ **Cache scope:** In-memory per-process by default. Cache backend is pluggable—see [CONFIGURATION.md](docs/CONFIGURATION.md) for custom cache backends.
1130
+
752
1131
  #### Tool Schema Export
753
1132
 
754
1133
  Export tool definitions to multiple formats for LLM prompting:
@@ -795,7 +1174,9 @@ mcp_format = spec.to_mcp() # For MCP servers
795
1174
 
796
1175
  #### Machine-Readable Error Codes
797
1176
 
798
- Structured error handling with error codes for programmatic responses:
1177
+ Structured error handling with error codes for programmatic responses.
1178
+
1179
+ **Error Contract:** Every error includes a machine-readable code, human-readable message, and structured details:
799
1180
 
800
1181
  ```python
801
1182
  from chuk_tool_processor.core.exceptions import (
@@ -877,22 +1258,20 @@ result = await tool.execute(**llm_output)
877
1258
 
878
1259
  ## Advanced Topics
879
1260
 
880
- ### Using Subprocess Strategy
1261
+ ### Using Isolated Strategy
881
1262
 
882
- Use `SubprocessStrategy` when running untrusted, third-party, or potentially unsafe code that shouldn't share the same process as your main app.
1263
+ Use `IsolatedStrategy` when running untrusted, third-party, or potentially unsafe code that shouldn't share the same process as your main app.
883
1264
 
884
1265
  For isolation and safety when running untrusted code:
885
1266
 
886
1267
  ```python
887
1268
  import asyncio
888
- from chuk_tool_processor.core.processor import ToolProcessor
889
- from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
890
- from chuk_tool_processor.registry import get_default_registry
1269
+ from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
891
1270
 
892
1271
  async def main():
893
1272
  registry = await get_default_registry()
894
1273
  processor = ToolProcessor(
895
- strategy=SubprocessStrategy(
1274
+ strategy=IsolatedStrategy(
896
1275
  registry=registry,
897
1276
  max_workers=4,
898
1277
  default_timeout=30.0
@@ -903,6 +1282,10 @@ async def main():
903
1282
  asyncio.run(main())
904
1283
  ```
905
1284
 
1285
+ > **Security & Isolation — Threat Model**
1286
+ >
1287
+ > Untrusted tool code runs in subprocesses; faults and crashes don't bring down your app. **Zero crash blast radius.** For hard CPU/RAM/network limits, run the processor inside a container with `--cpus`, `--memory`, and egress filtering. Secrets are never injected by default—pass them explicitly via tool arguments or scoped environment variables.
1288
+
906
1289
  ### Real-World MCP Examples
907
1290
 
908
1291
  #### Example 1: Notion Integration with OAuth
@@ -912,7 +1295,7 @@ Complete OAuth flow connecting to Notion's MCP server:
912
1295
  ```python
913
1296
  from chuk_tool_processor.mcp import setup_mcp_http_streamable
914
1297
 
915
- # After completing OAuth flow (see examples/notion_oauth.py for full flow)
1298
+ # After completing OAuth flow (see examples/04_mcp_integration/notion_oauth.py for full flow)
916
1299
  processor, manager = await setup_mcp_http_streamable(
917
1300
  servers=[{
918
1301
  "name": "notion",
@@ -933,6 +1316,9 @@ results = await processor.process(
933
1316
  )
934
1317
  ```
935
1318
 
1319
+ <details>
1320
+ <summary><strong>Click to expand more MCP examples (SQLite, Echo Server)</strong></summary>
1321
+
936
1322
  #### Example 2: Local SQLite Database Access
937
1323
 
938
1324
  Run SQLite MCP server locally for database operations:
@@ -1004,10 +1390,15 @@ results = await processor.process(
1004
1390
  )
1005
1391
  ```
1006
1392
 
1007
- See `examples/notion_oauth.py`, `examples/stdio_sqlite.py`, and `examples/stdio_echo.py` for complete working implementations.
1393
+ </details>
1394
+
1395
+ See `examples/04_mcp_integration/notion_oauth.py`, `examples/04_mcp_integration/stdio_sqlite.py`, and `examples/04_mcp_integration/stdio_echo.py` for complete working implementations.
1008
1396
 
1009
1397
  #### OAuth Token Refresh
1010
1398
 
1399
+ <details>
1400
+ <summary><strong>Click to expand OAuth token refresh guide</strong></summary>
1401
+
1011
1402
  For MCP servers that use OAuth authentication, CHUK Tool Processor supports automatic token refresh when access tokens expire. This prevents your tools from failing due to expired tokens during long-running sessions.
1012
1403
 
1013
1404
  **How it works:**
@@ -1076,7 +1467,9 @@ processor, manager = await setup_mcp_sse(
1076
1467
  - Token refresh is attempted only once per tool call (no infinite retry loops)
1077
1468
  - After successful refresh, the updated headers are used for all subsequent calls
1078
1469
 
1079
- See `examples/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
1470
+ See `examples/04_mcp_integration/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
1471
+
1472
+ </details>
1080
1473
 
1081
1474
  ### Observability
1082
1475
 
@@ -1145,6 +1538,9 @@ asyncio.run(main())
1145
1538
 
1146
1539
  #### OpenTelemetry & Prometheus (Drop-in Observability)
1147
1540
 
1541
+ <details>
1542
+ <summary><strong>Click to expand complete observability guide</strong></summary>
1543
+
1148
1544
  **3-Line Setup:**
1149
1545
 
1150
1546
  ```python
@@ -1208,8 +1604,7 @@ uv pip install chuk-tool-processor --group observability
1208
1604
  ```python
1209
1605
  import asyncio
1210
1606
  from chuk_tool_processor.observability import setup_observability
1211
- from chuk_tool_processor.core.processor import ToolProcessor
1212
- from chuk_tool_processor.registry import initialize, register_tool
1607
+ from chuk_tool_processor import ToolProcessor, initialize, register_tool
1213
1608
 
1214
1609
  @register_tool(name="weather_api")
1215
1610
  class WeatherTool:
@@ -1427,7 +1822,7 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
1427
1822
  - Testing observability features
1428
1823
  - Environment variable configuration
1429
1824
 
1430
- 🎯 **Working Example**: See `examples/observability_demo.py` for a complete demonstration with retries, caching, and circuit breakers
1825
+ 🎯 **Working Example**: See `examples/02_production_features/observability_demo.py` for a complete demonstration with retries, caching, and circuit breakers
1431
1826
 
1432
1827
  **Benefits**
1433
1828
 
@@ -1438,6 +1833,8 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
1438
1833
  ✅ **Optional** - Gracefully degrades if packages not installed
1439
1834
  ✅ **Zero-overhead** - No performance impact when disabled
1440
1835
 
1836
+ </details>
1837
+
1441
1838
  ### Error Handling
1442
1839
 
1443
1840
  ```python
@@ -1455,8 +1852,7 @@ for result in results:
1455
1852
 
1456
1853
  ```python
1457
1854
  import pytest
1458
- from chuk_tool_processor.core.processor import ToolProcessor
1459
- from chuk_tool_processor.registry import initialize
1855
+ from chuk_tool_processor import ToolProcessor, initialize
1460
1856
 
1461
1857
  @pytest.mark.asyncio
1462
1858
  async def test_calculator():
@@ -1470,6 +1866,40 @@ async def test_calculator():
1470
1866
  assert results[0].result["result"] == 8
1471
1867
  ```
1472
1868
 
1869
+ **Fake tool pattern for testing:**
1870
+
1871
+ ```python
1872
+ import pytest
1873
+ from chuk_tool_processor import ToolProcessor, register_tool, initialize
1874
+
1875
+ @register_tool(name="fake_tool")
1876
+ class FakeTool:
1877
+ """No-op tool for testing processor behavior."""
1878
+ call_count = 0
1879
+
1880
+ async def execute(self, **kwargs) -> dict:
1881
+ FakeTool.call_count += 1
1882
+ return {"called": True, "args": kwargs}
1883
+
1884
+ @pytest.mark.asyncio
1885
+ async def test_processor_with_fake_tool():
1886
+ await initialize()
1887
+ processor = ToolProcessor()
1888
+
1889
+ # Reset counter
1890
+ FakeTool.call_count = 0
1891
+
1892
+ # Execute fake tool
1893
+ results = await processor.process(
1894
+ '<tool name="fake_tool" args=\'{"test_arg": "value"}\'/>'
1895
+ )
1896
+
1897
+ # Assert behavior
1898
+ assert FakeTool.call_count == 1
1899
+ assert results[0].result["called"] is True
1900
+ assert results[0].result["args"]["test_arg"] == "value"
1901
+ ```
1902
+
1473
1903
  ## Configuration
1474
1904
 
1475
1905
  ### Timeout Configuration
@@ -1480,6 +1910,7 @@ CHUK Tool Processor uses a unified timeout configuration system that applies to
1480
1910
  from chuk_tool_processor.mcp.transport import TimeoutConfig
1481
1911
 
1482
1912
  # Create custom timeout configuration
1913
+ # (Defaults are: connect=30, operation=30, quick=5, shutdown=2)
1483
1914
  timeout_config = TimeoutConfig(
1484
1915
  connect=30.0, # Connection establishment, initialization, session discovery
1485
1916
  operation=30.0, # Normal operations (tool calls, listing tools/resources/prompts)
@@ -1607,7 +2038,7 @@ CHUK Tool Processor provides multiple layers of safety:
1607
2038
  | Concern | Protection | Configuration |
1608
2039
  |---------|------------|---------------|
1609
2040
  | **Timeouts** | Every tool has a timeout | `default_timeout=30.0` |
1610
- | **Process Isolation** | Run tools in separate processes | `strategy=SubprocessStrategy()` |
2041
+ | **Process Isolation** | Run tools in separate processes | `strategy=IsolatedStrategy()` |
1611
2042
  | **Rate Limiting** | Prevent abuse and API overuse | `enable_rate_limiting=True` |
1612
2043
  | **Input Validation** | Pydantic validation on arguments | Use `ValidatedTool` |
1613
2044
  | **Error Containment** | Failures don't crash the processor | Built-in exception handling |
@@ -1619,13 +2050,58 @@ CHUK Tool Processor provides multiple layers of safety:
1619
2050
  - **Resource Limits**: For hard CPU/memory caps, use OS-level controls (cgroups on Linux, Job Objects on Windows, or Docker resource limits).
1620
2051
  - **Secrets**: Never injected automatically. Pass secrets explicitly via tool arguments or environment variables, and prefer scoped env vars for subprocess tools to minimize exposure.
1621
2052
 
2053
+ #### OS-Level Hardening
2054
+
2055
+ For production deployments, add these hardening measures:
2056
+
2057
+ | Concern | Docker/Container Solution | Direct Example |
2058
+ |---------|--------------------------|----------------|
2059
+ | **CPU/RAM caps** | `--cpus`, `--memory` flags | `docker run --cpus="1.5" --memory="512m" myapp` |
2060
+ | **Network egress** | Deny-by-default with firewall rules | `--network=none` or custom network with egress filtering |
2061
+ | **Filesystem** | Read-only root + writable scratch | `--read-only --tmpfs /tmp:rw,size=100m` |
2062
+
2063
+ **Example: Run processor in locked-down container**
2064
+
2065
+ ```bash
2066
+ # Dockerfile
2067
+ FROM python:3.11-slim
2068
+ WORKDIR /app
2069
+ COPY requirements.txt .
2070
+ RUN pip install -r requirements.txt --no-cache-dir
2071
+ COPY . .
2072
+ USER nobody # Run as non-root
2073
+ CMD ["python", "app.py"]
2074
+
2075
+ # Run with resource limits and network restrictions
2076
+ docker run \
2077
+ --cpus="2" \
2078
+ --memory="1g" \
2079
+ --memory-swap="1g" \
2080
+ --read-only \
2081
+ --tmpfs /tmp:rw,size=200m,mode=1777 \
2082
+ --network=custom-net \
2083
+ --cap-drop=ALL \
2084
+ myapp:latest
2085
+ ```
2086
+
2087
+ **Network egress controls (deny-by-default)**
2088
+
2089
+ ```bash
2090
+ # Create restricted network with no internet access (for local-only tools)
2091
+ docker network create --internal restricted-net
2092
+
2093
+ # Or use iptables for per-tool CIDR allowlists
2094
+ iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT # Allow private ranges
2095
+ iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
2096
+ iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
2097
+ iptables -A OUTPUT -j DROP # Deny everything else
2098
+ ```
2099
+
1622
2100
  Example security-focused setup for untrusted code:
1623
2101
 
1624
2102
  ```python
1625
2103
  import asyncio
1626
- from chuk_tool_processor.core.processor import ToolProcessor
1627
- from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
1628
- from chuk_tool_processor.registry import get_default_registry
2104
+ from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
1629
2105
 
1630
2106
  async def create_secure_processor():
1631
2107
  # Maximum isolation for untrusted code
@@ -1633,7 +2109,7 @@ async def create_secure_processor():
1633
2109
  registry = await get_default_registry()
1634
2110
 
1635
2111
  processor = ToolProcessor(
1636
- strategy=SubprocessStrategy(
2112
+ strategy=IsolatedStrategy(
1637
2113
  registry=registry,
1638
2114
  max_workers=4,
1639
2115
  default_timeout=10.0
@@ -1651,6 +2127,25 @@ async def create_secure_processor():
1651
2127
  # - Use read-only filesystems where possible
1652
2128
  ```
1653
2129
 
2130
+ ## Design Goals & Non-Goals
2131
+
2132
+ **What CHUK Tool Processor does:**
2133
+ - ✅ Parse tool calls from any LLM format (XML, OpenAI, JSON)
2134
+ - ✅ Execute tools with production policies (timeouts, retries, rate limits, caching)
2135
+ - ✅ Isolate untrusted code in subprocesses
2136
+ - ✅ Connect to remote tool servers via MCP (HTTP/STDIO/SSE)
2137
+ - ✅ Provide composable execution layers (strategies + wrappers)
2138
+ - ✅ Export tool schemas for LLM prompting
2139
+
2140
+ **What CHUK Tool Processor explicitly does NOT do:**
2141
+ - ❌ Manage conversations or chat history
2142
+ - ❌ Provide prompt engineering or prompt templates
2143
+ - ❌ Bundle an LLM client (bring your own OpenAI/Anthropic/local)
2144
+ - ❌ Implement agent frameworks or chains
2145
+ - ❌ Make decisions about which tools to call
2146
+
2147
+ **Why this matters:** CHUK Tool Processor stays focused on reliable tool execution. It's a building block, not a framework. This makes it composable with any LLM application architecture.
2148
+
1654
2149
  ## Architecture Principles
1655
2150
 
1656
2151
  1. **Composability**: Stack strategies and wrappers like middleware
@@ -1664,29 +2159,26 @@ async def create_secure_processor():
1664
2159
  Check out the [`examples/`](examples/) directory for complete working examples:
1665
2160
 
1666
2161
  ### Getting Started
1667
- - **60-second hello**: `examples/hello_tool.py` - Absolute minimal example (copy-paste-run)
1668
- - **Quick start**: `examples/quickstart_demo.py` - Basic tool registration and execution
1669
- - **Execution strategies**: `examples/execution_strategies_demo.py` - InProcess vs Subprocess
1670
- - **Production wrappers**: `examples/wrappers_demo.py` - Caching, retries, rate limiting
1671
- - **Streaming tools**: `examples/streaming_demo.py` - Real-time incremental results
1672
- - **Streaming tool calls**: `examples/streaming_tool_calls_demo.py` - Handle partial tool calls from streaming LLMs
1673
- - **Schema helper**: `examples/schema_helper_demo.py` - Auto-generate schemas from typed tools (Pydantic → OpenAI/Anthropic/MCP)
1674
- - **Observability**: `examples/observability_demo.py` - OpenTelemetry + Prometheus integration
2162
+ - **60-second hello**: `examples/01_getting_started/hello_tool.py` - Absolute minimal example (copy-paste-run)
2163
+ - **Quick start**: `examples/01_getting_started/quickstart_demo.py` - Basic tool registration and execution
2164
+ - **Execution strategies**: `examples/01_getting_started/execution_strategies_demo.py` - InProcess vs Subprocess
2165
+ - **Production wrappers**: `examples/02_production_features/wrappers_demo.py` - Caching, retries, rate limiting
2166
+ - **Streaming tools**: `examples/03_streaming/streaming_demo.py` - Real-time incremental results
2167
+ - **Streaming tool calls**: `examples/03_streaming/streaming_tool_calls_demo.py` - Handle partial tool calls from streaming LLMs
2168
+ - **Schema helper**: `examples/05_schema_and_types/schema_helper_demo.py` - Auto-generate schemas from typed tools (Pydantic → OpenAI/Anthropic/MCP)
2169
+ - **Observability**: `examples/02_production_features/observability_demo.py` - OpenTelemetry + Prometheus integration
1675
2170
 
1676
2171
  ### MCP Integration (Real-World)
1677
- - **Notion + OAuth**: `examples/notion_oauth.py` - Complete OAuth 2.1 flow with HTTP Streamable
2172
+ - **Notion + OAuth**: `examples/04_mcp_integration/notion_oauth.py` - Complete OAuth 2.1 flow with HTTP Streamable
1678
2173
  - Shows: Authorization Server discovery, client registration, PKCE flow, token exchange
1679
- - **SQLite Local**: `examples/stdio_sqlite.py` - Local database access via STDIO
2174
+ - **SQLite Local**: `examples/04_mcp_integration/stdio_sqlite.py` - Local database access via STDIO
1680
2175
  - Shows: Command/args passing, environment variables, file paths, initialization timeouts
1681
- - **Echo Server**: `examples/stdio_echo.py` - Minimal STDIO transport example
2176
+ - **Echo Server**: `examples/04_mcp_integration/stdio_echo.py` - Minimal STDIO transport example
1682
2177
  - Shows: Simplest possible MCP integration for testing
1683
- - **Atlassian + OAuth**: `examples/atlassian_sse.py` - OAuth with SSE transport (legacy)
2178
+ - **Atlassian + OAuth**: `examples/04_mcp_integration/atlassian_sse.py` - OAuth with SSE transport (legacy)
1684
2179
 
1685
2180
  ### Advanced MCP
1686
- - **HTTP Streamable**: `examples/mcp_http_streamable_example.py`
1687
- - **STDIO**: `examples/mcp_stdio_example.py`
1688
- - **SSE**: `examples/mcp_sse_example.py`
1689
- - **Plugin system**: `examples/plugins_builtins_demo.py`, `examples/plugins_custom_parser_demo.py`
2181
+ - **Plugin system**: `examples/06_plugins/plugins_builtins_demo.py`, `examples/06_plugins/plugins_custom_parser_demo.py`
1690
2182
 
1691
2183
  ## FAQ
1692
2184
 
@@ -1711,18 +2203,20 @@ A: Use pytest with `@pytest.mark.asyncio`. See [Testing Tools](#testing-tools) f
1711
2203
  **Q: Does this work with streaming LLM responses?**
1712
2204
  A: Yes—as tool calls appear in the stream, extract and process them. The processor handles partial/incremental tool call lists.
1713
2205
 
1714
- **Q: What's the difference between InProcess and Subprocess strategies?**
1715
- A: InProcess is faster (same process), Subprocess is safer (isolated process). Use InProcess for trusted code, Subprocess for untrusted.
2206
+ **Q: What's the difference between InProcess and Isolated strategies?**
2207
+ A: InProcess is faster (same process), Isolated is safer (separate subprocess). Use InProcess for trusted code, Isolated for untrusted.
1716
2208
 
1717
2209
  ## Comparison with Other Tools
1718
2210
 
1719
2211
  | Feature | chuk-tool-processor | LangChain Tools | OpenAI Tools | MCP SDK |
1720
2212
  |---------|-------------------|-----------------|--------------|---------|
1721
2213
  | **Async-native** | ✅ | ⚠️ Partial | ✅ | ✅ |
1722
- | **Process isolation** | ✅ SubprocessStrategy | ❌ | ❌ | ⚠️ |
2214
+ | **Process isolation** | ✅ IsolatedStrategy | ❌ | ❌ | ⚠️ |
1723
2215
  | **Built-in retries** | ✅ | ❌ † | ❌ | ❌ |
1724
2216
  | **Rate limiting** | ✅ | ❌ † | ⚠️ ‡ | ❌ |
1725
2217
  | **Caching** | ✅ | ⚠️ † | ❌ ‡ | ❌ |
2218
+ | **Idempotency & de-dup** | ✅ SHA256 keys | ❌ | ❌ | ❌ |
2219
+ | **Per-tool policies** | ✅ (timeouts/retries/limits) | ⚠️ | ❌ | ❌ |
1726
2220
  | **Multiple parsers** | ✅ (XML, OpenAI, JSON) | ⚠️ | ✅ | ✅ |
1727
2221
  | **Streaming tools** | ✅ | ⚠️ | ⚠️ | ✅ |
1728
2222
  | **MCP integration** | ✅ All transports | ❌ | ❌ | ✅ (protocol only) |
@@ -1799,6 +2293,25 @@ For detailed release documentation, see:
1799
2293
  - **[RELEASING.md](RELEASING.md)** - Complete release process guide
1800
2294
  - **[docs/CI-CD.md](docs/CI-CD.md)** - Full CI/CD pipeline documentation
1801
2295
 
2296
+ ## Stability & Versioning
2297
+
2298
+ CHUK Tool Processor follows **[Semantic Versioning 2.0.0](https://semver.org/)** for predictable upgrades:
2299
+
2300
+ * **Breaking changes** = **major** version bump (e.g., 1.x → 2.0)
2301
+ * **New features** (backward-compatible) = **minor** version bump (e.g., 1.2 → 1.3)
2302
+ * **Bug fixes** (backward-compatible) = **patch** version bump (e.g., 1.2.3 → 1.2.4)
2303
+
2304
+ **Public API surface**: Everything exported via the package root (`from chuk_tool_processor import ...`) is considered public API and follows semver guarantees.
2305
+
2306
+ **Deprecation policy**: Deprecated APIs will:
2307
+ 1. Log a warning for **one minor release**
2308
+ 2. Be removed in the **next major release**
2309
+
2310
+ **Upgrading safely**:
2311
+ * Patch and minor updates are **safe to deploy** without code changes
2312
+ * Major updates may require migration—see release notes
2313
+ * Pin to `chuk-tool-processor~=1.2` for minor updates only, or `chuk-tool-processor==1.2.3` for exact versions
2314
+
1802
2315
  ## Contributing & Support
1803
2316
 
1804
2317
  - **GitHub**: [chrishayuk/chuk-tool-processor](https://github.com/chrishayuk/chuk-tool-processor)