chuk-tool-processor 0.14__tar.gz → 0.15__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 (88) hide show
  1. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/PKG-INFO +144 -13
  2. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/README.md +143 -12
  3. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/pyproject.toml +1 -1
  4. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/__init__.py +19 -0
  5. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/core/processor.py +12 -1
  6. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/bulkhead.py +70 -3
  7. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +51 -15
  8. chuk_tool_processor-0.15/src/chuk_tool_processor/mcp/__init__.py +93 -0
  9. chuk_tool_processor-0.15/src/chuk_tool_processor/mcp/middleware.py +490 -0
  10. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/stream_manager.py +104 -3
  11. chuk_tool_processor-0.15/src/chuk_tool_processor/models/return_order.py +19 -0
  12. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/models/tool_result.py +2 -0
  13. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/registry/decorators.py +39 -12
  14. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/registry/providers/memory.py +21 -3
  15. chuk_tool_processor-0.15/src/chuk_tool_processor/scheduling/__init__.py +48 -0
  16. chuk_tool_processor-0.15/src/chuk_tool_processor/scheduling/greedy_dag.py +473 -0
  17. chuk_tool_processor-0.15/src/chuk_tool_processor/scheduling/policy.py +66 -0
  18. chuk_tool_processor-0.15/src/chuk_tool_processor/scheduling/types.py +220 -0
  19. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor.egg-info/PKG-INFO +144 -13
  20. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor.egg-info/SOURCES.txt +6 -0
  21. chuk_tool_processor-0.14/src/chuk_tool_processor/mcp/__init__.py +0 -34
  22. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/setup.cfg +0 -0
  23. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/core/__init__.py +0 -0
  24. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/core/context.py +0 -0
  25. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/core/exceptions.py +0 -0
  26. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/__init__.py +0 -0
  27. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/code_sandbox.py +0 -0
  28. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
  29. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
  30. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
  31. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
  32. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
  33. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
  34. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
  35. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
  36. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/logging/__init__.py +0 -0
  37. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/logging/context.py +0 -0
  38. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/logging/formatter.py +0 -0
  39. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/logging/helpers.py +0 -0
  40. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/logging/metrics.py +0 -0
  41. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
  42. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/models.py +0 -0
  43. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
  44. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
  45. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
  46. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
  47. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
  48. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
  49. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
  50. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
  51. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
  52. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
  53. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/models/__init__.py +0 -0
  54. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
  55. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
  56. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/models/tool_call.py +0 -0
  57. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
  58. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/models/tool_spec.py +0 -0
  59. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/models/validated_tool.py +0 -0
  60. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/observability/__init__.py +0 -0
  61. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/observability/metrics.py +0 -0
  62. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/observability/setup.py +0 -0
  63. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/observability/tracing.py +0 -0
  64. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/plugins/__init__.py +0 -0
  65. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/plugins/discovery.py +0 -0
  66. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
  67. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
  68. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
  69. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
  70. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
  71. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
  72. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/py.typed +0 -0
  73. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/registry/__init__.py +0 -0
  74. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/registry/auto_register.py +0 -0
  75. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/registry/interface.py +0 -0
  76. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/registry/metadata.py +0 -0
  77. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/registry/provider.py +0 -0
  78. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
  79. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/registry/tool_export.py +0 -0
  80. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/utils/__init__.py +0 -0
  81. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/utils/fast_json.py +0 -0
  82. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor/utils/validation.py +0 -0
  83. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
  84. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
  85. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
  86. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/tests/test_bulkhead.py +0 -0
  87. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/tests/test_execution_context.py +0 -0
  88. {chuk_tool_processor-0.14 → chuk_tool_processor-0.15}/tests/test_scoped_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.14
3
+ Version: 0.15
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>
@@ -30,7 +30,7 @@ Requires-Dist: orjson<4,>=3.10.0; extra == "fast-json"
30
30
  Provides-Extra: full
31
31
  Requires-Dist: orjson<4,>=3.10.0; extra == "full"
32
32
 
33
- # CHUK Tool Processor — Production-grade execution for LLM tool calls
33
+ # CHUK Tool Processor — A Tool Execution Runtime for AI Systems
34
34
 
35
35
  [![PyPI](https://img.shields.io/pypi/v/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
36
36
  [![Python](https://img.shields.io/pypi/pyversions/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
@@ -43,16 +43,18 @@ Requires-Dist: orjson<4,>=3.10.0; extra == "full"
43
43
 
44
44
  ---
45
45
 
46
- ## The Missing Layer for Reliable Tool Execution
46
+ ## The Missing Runtime Layer
47
47
 
48
- LLMs are good at *calling* tools. The hard part is **executing** those tools reliably.
48
+ LLMs are good at *deciding which tools to call*. The hard part is **executing** those tools reliably.
49
49
 
50
- **CHUK Tool Processor:**
50
+ **CHUK Tool Processor** is a **tool execution runtime** — it doesn't plan workflows or decide which tools to call. It executes tool calls reliably, under constraints, as directed by higher-level planners (your agent, LangChain, LlamaIndex, or a custom orchestrator).
51
+
52
+ **What it does:**
51
53
  - Parses tool calls from any model (Anthropic XML, OpenAI `tool_calls`, JSON)
52
54
  - Executes them with **timeouts, retries, caching, rate limits, circuit breaker, observability**
53
55
  - Runs tools locally, in **isolated subprocesses**, or **remote via MCP**
54
56
 
55
- Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework (LangChain, LlamaIndex, custom).
57
+ Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework.
56
58
 
57
59
  ---
58
60
 
@@ -110,18 +112,20 @@ uv pip install chuk-tool-processor
110
112
 
111
113
  ```python
112
114
  import asyncio
113
- from chuk_tool_processor import ToolProcessor, tool
115
+ from chuk_tool_processor import ToolProcessor, create_registry
114
116
 
115
- @tool(name="calculator")
116
117
  class Calculator:
117
118
  async def execute(self, operation: str, a: float, b: float) -> dict:
118
119
  ops = {"add": a + b, "multiply": a * b, "subtract": a - b}
119
120
  return {"result": ops.get(operation, 0)}
120
121
 
121
122
  async def main():
122
- async with ToolProcessor(enable_caching=True, enable_retries=True) as p:
123
+ registry = create_registry()
124
+ await registry.register_tool(Calculator, name="math.calculator") # Dotted name → namespace="math"
125
+
126
+ async with ToolProcessor(registry=registry, enable_caching=True, enable_retries=True) as p:
123
127
  # Works with OpenAI, Anthropic, or JSON formats
124
- result = await p.process('<tool name="calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>')
128
+ result = await p.process('<tool name="math.calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>')
125
129
  print(result[0].result) # {'result': 345}
126
130
 
127
131
  asyncio.run(main())
@@ -129,6 +133,19 @@ asyncio.run(main())
129
133
 
130
134
  **That's it.** You now have production-ready tool execution with timeouts, retries, and caching.
131
135
 
136
+ ### Dotted Names for Namespacing
137
+
138
+ Dotted names are auto-parsed into namespace and tool name:
139
+
140
+ ```python
141
+ # These are equivalent:
142
+ await registry.register_tool(FetchUser, name="web.fetch_user") # Auto-parsed
143
+ await registry.register_tool(FetchUser, name="fetch_user", namespace="web") # Explicit
144
+
145
+ # Call using the full dotted name
146
+ result = await processor.process([{"tool": "web.fetch_user", "arguments": {"user_id": "123"}}])
147
+ ```
148
+
132
149
  ### Works with Any LLM Format
133
150
 
134
151
  ```python
@@ -171,10 +188,19 @@ results = await processor.process(json_output)
171
188
  | Feature | Description |
172
189
  |---------|-------------|
173
190
  | **Bulkheads** | Per-tool/namespace concurrency limits to prevent resource starvation |
191
+ | **Pattern Bulkheads** | Glob patterns like `"db.*": 3` for grouped concurrency limits |
174
192
  | **Scoped Registries** | Isolated registries for multi-tenant apps and testing |
175
193
  | **ExecutionContext** | Request-scoped metadata propagation (user, tenant, tracing, deadlines) |
176
194
  | **Isolated Strategy** | Subprocess execution for untrusted code (zero crash blast radius) |
177
195
 
196
+ ### Advanced Scheduling
197
+
198
+ | Feature | Description |
199
+ |---------|-------------|
200
+ | **Return Order** | Choose completion order (fast first) or submission order (deterministic) |
201
+ | **SchedulerPolicy** | DAG-based scheduling with dependencies, deadlines, pool limits |
202
+ | **GreedyDagScheduler** | Built-in scheduler with topological sort and deadline-aware skipping |
203
+
178
204
  ### Integration & Observability
179
205
 
180
206
  | Feature | Description |
@@ -211,6 +237,7 @@ async with ToolProcessor(
211
237
  bulkhead_config=BulkheadConfig(
212
238
  default_limit=10,
213
239
  tool_limits={"slow_api": 2},
240
+ patterns={"db.*": 3, "mcp.notion.*": 2}, # Pattern-based limits
214
241
  ),
215
242
  ) as processor:
216
243
  # Execute with request context
@@ -224,6 +251,56 @@ async with ToolProcessor(
224
251
 
225
252
  ---
226
253
 
254
+ ## Return Order & Scheduling
255
+
256
+ Control how results are returned and plan complex execution graphs:
257
+
258
+ ```python
259
+ from chuk_tool_processor import ToolProcessor, ReturnOrder
260
+
261
+ async with ToolProcessor() as processor:
262
+ # Results return as tools complete (fast tools first) - default
263
+ results = await processor.process(calls, return_order="completion")
264
+
265
+ # Results return in submission order (deterministic)
266
+ results = await processor.process(calls, return_order="submission")
267
+ ```
268
+
269
+ ### DAG Scheduling with Dependencies
270
+
271
+ ```python
272
+ from chuk_tool_processor import (
273
+ GreedyDagScheduler,
274
+ SchedulingConstraints,
275
+ ToolCallSpec,
276
+ ToolMetadata,
277
+ )
278
+
279
+ scheduler = GreedyDagScheduler()
280
+
281
+ # Define calls with dependencies
282
+ calls = [
283
+ ToolCallSpec(call_id="fetch", tool_name="api.fetch",
284
+ metadata=ToolMetadata(pool="web", est_ms=300)),
285
+ ToolCallSpec(call_id="transform", tool_name="compute.transform",
286
+ depends_on=("fetch",)),
287
+ ToolCallSpec(call_id="store", tool_name="db.write",
288
+ depends_on=("transform",)),
289
+ ]
290
+
291
+ # Plan execution with constraints
292
+ constraints = SchedulingConstraints(
293
+ deadline_ms=5000,
294
+ pool_limits={"web": 2, "db": 1},
295
+ )
296
+ plan = scheduler.plan(calls, constraints)
297
+
298
+ # plan.stages: (('fetch',), ('transform',), ('store',))
299
+ # plan.skip: () or low-priority calls that would miss deadline
300
+ ```
301
+
302
+ ---
303
+
227
304
  ## MCP Integration
228
305
 
229
306
  Connect to remote tool servers using the [Model Context Protocol](https://modelcontextprotocol.io):
@@ -259,6 +336,33 @@ results = await processor.process(
259
336
 
260
337
  See [MCP_INTEGRATION.md](docs/MCP_INTEGRATION.md) for complete examples with OAuth token refresh.
261
338
 
339
+ ### MCP Middleware Stack
340
+
341
+ For production deployments, wrap MCP connections with resilience middleware:
342
+
343
+ ```python
344
+ from chuk_tool_processor.mcp.middleware import (
345
+ MiddlewareConfig,
346
+ MiddlewareStack,
347
+ RetrySettings,
348
+ CircuitBreakerSettings,
349
+ RateLimitSettings,
350
+ )
351
+
352
+ # Configure middleware layers
353
+ config = MiddlewareConfig(
354
+ retry=RetrySettings(max_retries=3, base_delay=1.0),
355
+ circuit_breaker=CircuitBreakerSettings(failure_threshold=5),
356
+ rate_limiting=RateLimitSettings(enabled=True, global_limit=100),
357
+ )
358
+
359
+ # Wrap StreamManager with middleware
360
+ middleware = MiddlewareStack(stream_manager, config=config)
361
+
362
+ # Execute with automatic retry, circuit breaking, and rate limiting
363
+ result = await middleware.call_tool("notion.search", {"query": "docs"})
364
+ ```
365
+
262
366
  ---
263
367
 
264
368
  ## Observability
@@ -294,7 +398,7 @@ See [OBSERVABILITY.md](docs/OBSERVABILITY.md) for complete setup guide.
294
398
  | [**GETTING_STARTED.md**](docs/GETTING_STARTED.md) | Creating tools, using the processor, ValidatedTool, StreamingTool |
295
399
  | [**CORE_CONCEPTS.md**](docs/CORE_CONCEPTS.md) | Registry, strategies, wrappers, parsers, MCP overview |
296
400
  | [**PRODUCTION_PATTERNS.md**](docs/PRODUCTION_PATTERNS.md) | Bulkheads, scoped registries, ExecutionContext, parallel execution |
297
- | [**MCP_INTEGRATION.md**](docs/MCP_INTEGRATION.md) | HTTP Streamable, STDIO, SSE, OAuth token refresh |
401
+ | [**MCP_INTEGRATION.md**](docs/MCP_INTEGRATION.md) | HTTP Streamable, STDIO, SSE, OAuth, Middleware Stack |
298
402
  | [**ADVANCED_TOPICS.md**](docs/ADVANCED_TOPICS.md) | Deferred loading, code sandbox, isolated strategy, testing |
299
403
  | [**CONFIGURATION.md**](docs/CONFIGURATION.md) | All config options and environment variables |
300
404
  | [**OBSERVABILITY.md**](docs/OBSERVABILITY.md) | OpenTelemetry, Prometheus, metrics reference |
@@ -308,15 +412,22 @@ See [OBSERVABILITY.md](docs/OBSERVABILITY.md) for complete setup guide.
308
412
  # Getting started
309
413
  python examples/01_getting_started/hello_tool.py
310
414
 
415
+ # Hero demo: 8 tools, 5-second deadline, 3 pools (DAG + bulkheads + context)
416
+ python examples/02_production_features/hero_runtime_demo.py
417
+
311
418
  # Production patterns (bulkheads, context, scoped registries)
312
419
  python examples/02_production_features/production_patterns_demo.py
313
420
 
421
+ # Runtime features (return order, pattern bulkheads, scheduling)
422
+ python examples/02_production_features/runtime_features_demo.py
423
+
314
424
  # Observability demo
315
425
  python examples/02_production_features/observability_demo.py
316
426
 
317
427
  # MCP integration
318
428
  python examples/04_mcp_integration/stdio_echo.py
319
429
  python examples/04_mcp_integration/notion_oauth.py
430
+ python examples/04_mcp_integration/middleware_demo.py
320
431
  ```
321
432
 
322
433
  See [examples/](examples/) for 20+ working examples.
@@ -367,10 +478,30 @@ pip install chuk-tool-processor[all]
367
478
  - You want production-grade observability
368
479
 
369
480
  **Don't use this if:**
370
- - You want an agent framework (this is the execution layer, not the agent)
481
+ - You want an agent framework (this is the execution runtime, not the agent)
371
482
  - You want conversation flow/memory orchestration
483
+ - You need a planner to decide *which* tools to call
484
+
485
+ ### The Seam: Runtime vs Planner
486
+
487
+ CHUK Tool Processor deliberately does not plan workflows or decide which tools to call. It executes tool calls reliably, under constraints, as directed by higher-level planners.
488
+
489
+ ```
490
+ ┌─────────────────────────────────────────────────────┐
491
+ │ Your Agent / LangChain / LlamaIndex / Custom │ ← Decides WHICH tools
492
+ └─────────────────────────────────────────────────────┘
493
+
494
+ ┌─────────────────────────────────────────────────────┐
495
+ │ CHUK Tool Processor │ ← Executes tools RELIABLY
496
+ │ (timeouts, retries, caching, rate limits, etc.) │
497
+ └─────────────────────────────────────────────────────┘
498
+
499
+ ┌─────────────────────────────────────────────────────┐
500
+ │ Local Tools / MCP Servers │ ← Does the actual work
501
+ └─────────────────────────────────────────────────────┘
502
+ ```
372
503
 
373
- > **Not a framework.** If LangChain/LlamaIndex help decide *which* tool to call, CHUK Tool Processor makes sure the tool call **actually succeeds**.
504
+ This separation means you can swap planners without changing execution infrastructure, and vice versa.
374
505
 
375
506
  ---
376
507
 
@@ -1,4 +1,4 @@
1
- # CHUK Tool Processor — Production-grade execution for LLM tool calls
1
+ # CHUK Tool Processor — A Tool Execution Runtime for AI Systems
2
2
 
3
3
  [![PyPI](https://img.shields.io/pypi/v/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
4
4
  [![Python](https://img.shields.io/pypi/pyversions/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
@@ -11,16 +11,18 @@
11
11
 
12
12
  ---
13
13
 
14
- ## The Missing Layer for Reliable Tool Execution
14
+ ## The Missing Runtime Layer
15
15
 
16
- LLMs are good at *calling* tools. The hard part is **executing** those tools reliably.
16
+ LLMs are good at *deciding which tools to call*. The hard part is **executing** those tools reliably.
17
17
 
18
- **CHUK Tool Processor:**
18
+ **CHUK Tool Processor** is a **tool execution runtime** — it doesn't plan workflows or decide which tools to call. It executes tool calls reliably, under constraints, as directed by higher-level planners (your agent, LangChain, LlamaIndex, or a custom orchestrator).
19
+
20
+ **What it does:**
19
21
  - Parses tool calls from any model (Anthropic XML, OpenAI `tool_calls`, JSON)
20
22
  - Executes them with **timeouts, retries, caching, rate limits, circuit breaker, observability**
21
23
  - Runs tools locally, in **isolated subprocesses**, or **remote via MCP**
22
24
 
23
- Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework (LangChain, LlamaIndex, custom).
25
+ Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework.
24
26
 
25
27
  ---
26
28
 
@@ -78,18 +80,20 @@ uv pip install chuk-tool-processor
78
80
 
79
81
  ```python
80
82
  import asyncio
81
- from chuk_tool_processor import ToolProcessor, tool
83
+ from chuk_tool_processor import ToolProcessor, create_registry
82
84
 
83
- @tool(name="calculator")
84
85
  class Calculator:
85
86
  async def execute(self, operation: str, a: float, b: float) -> dict:
86
87
  ops = {"add": a + b, "multiply": a * b, "subtract": a - b}
87
88
  return {"result": ops.get(operation, 0)}
88
89
 
89
90
  async def main():
90
- async with ToolProcessor(enable_caching=True, enable_retries=True) as p:
91
+ registry = create_registry()
92
+ await registry.register_tool(Calculator, name="math.calculator") # Dotted name → namespace="math"
93
+
94
+ async with ToolProcessor(registry=registry, enable_caching=True, enable_retries=True) as p:
91
95
  # Works with OpenAI, Anthropic, or JSON formats
92
- result = await p.process('<tool name="calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>')
96
+ result = await p.process('<tool name="math.calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>')
93
97
  print(result[0].result) # {'result': 345}
94
98
 
95
99
  asyncio.run(main())
@@ -97,6 +101,19 @@ asyncio.run(main())
97
101
 
98
102
  **That's it.** You now have production-ready tool execution with timeouts, retries, and caching.
99
103
 
104
+ ### Dotted Names for Namespacing
105
+
106
+ Dotted names are auto-parsed into namespace and tool name:
107
+
108
+ ```python
109
+ # These are equivalent:
110
+ await registry.register_tool(FetchUser, name="web.fetch_user") # Auto-parsed
111
+ await registry.register_tool(FetchUser, name="fetch_user", namespace="web") # Explicit
112
+
113
+ # Call using the full dotted name
114
+ result = await processor.process([{"tool": "web.fetch_user", "arguments": {"user_id": "123"}}])
115
+ ```
116
+
100
117
  ### Works with Any LLM Format
101
118
 
102
119
  ```python
@@ -139,10 +156,19 @@ results = await processor.process(json_output)
139
156
  | Feature | Description |
140
157
  |---------|-------------|
141
158
  | **Bulkheads** | Per-tool/namespace concurrency limits to prevent resource starvation |
159
+ | **Pattern Bulkheads** | Glob patterns like `"db.*": 3` for grouped concurrency limits |
142
160
  | **Scoped Registries** | Isolated registries for multi-tenant apps and testing |
143
161
  | **ExecutionContext** | Request-scoped metadata propagation (user, tenant, tracing, deadlines) |
144
162
  | **Isolated Strategy** | Subprocess execution for untrusted code (zero crash blast radius) |
145
163
 
164
+ ### Advanced Scheduling
165
+
166
+ | Feature | Description |
167
+ |---------|-------------|
168
+ | **Return Order** | Choose completion order (fast first) or submission order (deterministic) |
169
+ | **SchedulerPolicy** | DAG-based scheduling with dependencies, deadlines, pool limits |
170
+ | **GreedyDagScheduler** | Built-in scheduler with topological sort and deadline-aware skipping |
171
+
146
172
  ### Integration & Observability
147
173
 
148
174
  | Feature | Description |
@@ -179,6 +205,7 @@ async with ToolProcessor(
179
205
  bulkhead_config=BulkheadConfig(
180
206
  default_limit=10,
181
207
  tool_limits={"slow_api": 2},
208
+ patterns={"db.*": 3, "mcp.notion.*": 2}, # Pattern-based limits
182
209
  ),
183
210
  ) as processor:
184
211
  # Execute with request context
@@ -192,6 +219,56 @@ async with ToolProcessor(
192
219
 
193
220
  ---
194
221
 
222
+ ## Return Order & Scheduling
223
+
224
+ Control how results are returned and plan complex execution graphs:
225
+
226
+ ```python
227
+ from chuk_tool_processor import ToolProcessor, ReturnOrder
228
+
229
+ async with ToolProcessor() as processor:
230
+ # Results return as tools complete (fast tools first) - default
231
+ results = await processor.process(calls, return_order="completion")
232
+
233
+ # Results return in submission order (deterministic)
234
+ results = await processor.process(calls, return_order="submission")
235
+ ```
236
+
237
+ ### DAG Scheduling with Dependencies
238
+
239
+ ```python
240
+ from chuk_tool_processor import (
241
+ GreedyDagScheduler,
242
+ SchedulingConstraints,
243
+ ToolCallSpec,
244
+ ToolMetadata,
245
+ )
246
+
247
+ scheduler = GreedyDagScheduler()
248
+
249
+ # Define calls with dependencies
250
+ calls = [
251
+ ToolCallSpec(call_id="fetch", tool_name="api.fetch",
252
+ metadata=ToolMetadata(pool="web", est_ms=300)),
253
+ ToolCallSpec(call_id="transform", tool_name="compute.transform",
254
+ depends_on=("fetch",)),
255
+ ToolCallSpec(call_id="store", tool_name="db.write",
256
+ depends_on=("transform",)),
257
+ ]
258
+
259
+ # Plan execution with constraints
260
+ constraints = SchedulingConstraints(
261
+ deadline_ms=5000,
262
+ pool_limits={"web": 2, "db": 1},
263
+ )
264
+ plan = scheduler.plan(calls, constraints)
265
+
266
+ # plan.stages: (('fetch',), ('transform',), ('store',))
267
+ # plan.skip: () or low-priority calls that would miss deadline
268
+ ```
269
+
270
+ ---
271
+
195
272
  ## MCP Integration
196
273
 
197
274
  Connect to remote tool servers using the [Model Context Protocol](https://modelcontextprotocol.io):
@@ -227,6 +304,33 @@ results = await processor.process(
227
304
 
228
305
  See [MCP_INTEGRATION.md](docs/MCP_INTEGRATION.md) for complete examples with OAuth token refresh.
229
306
 
307
+ ### MCP Middleware Stack
308
+
309
+ For production deployments, wrap MCP connections with resilience middleware:
310
+
311
+ ```python
312
+ from chuk_tool_processor.mcp.middleware import (
313
+ MiddlewareConfig,
314
+ MiddlewareStack,
315
+ RetrySettings,
316
+ CircuitBreakerSettings,
317
+ RateLimitSettings,
318
+ )
319
+
320
+ # Configure middleware layers
321
+ config = MiddlewareConfig(
322
+ retry=RetrySettings(max_retries=3, base_delay=1.0),
323
+ circuit_breaker=CircuitBreakerSettings(failure_threshold=5),
324
+ rate_limiting=RateLimitSettings(enabled=True, global_limit=100),
325
+ )
326
+
327
+ # Wrap StreamManager with middleware
328
+ middleware = MiddlewareStack(stream_manager, config=config)
329
+
330
+ # Execute with automatic retry, circuit breaking, and rate limiting
331
+ result = await middleware.call_tool("notion.search", {"query": "docs"})
332
+ ```
333
+
230
334
  ---
231
335
 
232
336
  ## Observability
@@ -262,7 +366,7 @@ See [OBSERVABILITY.md](docs/OBSERVABILITY.md) for complete setup guide.
262
366
  | [**GETTING_STARTED.md**](docs/GETTING_STARTED.md) | Creating tools, using the processor, ValidatedTool, StreamingTool |
263
367
  | [**CORE_CONCEPTS.md**](docs/CORE_CONCEPTS.md) | Registry, strategies, wrappers, parsers, MCP overview |
264
368
  | [**PRODUCTION_PATTERNS.md**](docs/PRODUCTION_PATTERNS.md) | Bulkheads, scoped registries, ExecutionContext, parallel execution |
265
- | [**MCP_INTEGRATION.md**](docs/MCP_INTEGRATION.md) | HTTP Streamable, STDIO, SSE, OAuth token refresh |
369
+ | [**MCP_INTEGRATION.md**](docs/MCP_INTEGRATION.md) | HTTP Streamable, STDIO, SSE, OAuth, Middleware Stack |
266
370
  | [**ADVANCED_TOPICS.md**](docs/ADVANCED_TOPICS.md) | Deferred loading, code sandbox, isolated strategy, testing |
267
371
  | [**CONFIGURATION.md**](docs/CONFIGURATION.md) | All config options and environment variables |
268
372
  | [**OBSERVABILITY.md**](docs/OBSERVABILITY.md) | OpenTelemetry, Prometheus, metrics reference |
@@ -276,15 +380,22 @@ See [OBSERVABILITY.md](docs/OBSERVABILITY.md) for complete setup guide.
276
380
  # Getting started
277
381
  python examples/01_getting_started/hello_tool.py
278
382
 
383
+ # Hero demo: 8 tools, 5-second deadline, 3 pools (DAG + bulkheads + context)
384
+ python examples/02_production_features/hero_runtime_demo.py
385
+
279
386
  # Production patterns (bulkheads, context, scoped registries)
280
387
  python examples/02_production_features/production_patterns_demo.py
281
388
 
389
+ # Runtime features (return order, pattern bulkheads, scheduling)
390
+ python examples/02_production_features/runtime_features_demo.py
391
+
282
392
  # Observability demo
283
393
  python examples/02_production_features/observability_demo.py
284
394
 
285
395
  # MCP integration
286
396
  python examples/04_mcp_integration/stdio_echo.py
287
397
  python examples/04_mcp_integration/notion_oauth.py
398
+ python examples/04_mcp_integration/middleware_demo.py
288
399
  ```
289
400
 
290
401
  See [examples/](examples/) for 20+ working examples.
@@ -335,10 +446,30 @@ pip install chuk-tool-processor[all]
335
446
  - You want production-grade observability
336
447
 
337
448
  **Don't use this if:**
338
- - You want an agent framework (this is the execution layer, not the agent)
449
+ - You want an agent framework (this is the execution runtime, not the agent)
339
450
  - You want conversation flow/memory orchestration
451
+ - You need a planner to decide *which* tools to call
452
+
453
+ ### The Seam: Runtime vs Planner
454
+
455
+ CHUK Tool Processor deliberately does not plan workflows or decide which tools to call. It executes tool calls reliably, under constraints, as directed by higher-level planners.
456
+
457
+ ```
458
+ ┌─────────────────────────────────────────────────────┐
459
+ │ Your Agent / LangChain / LlamaIndex / Custom │ ← Decides WHICH tools
460
+ └─────────────────────────────────────────────────────┘
461
+
462
+ ┌─────────────────────────────────────────────────────┐
463
+ │ CHUK Tool Processor │ ← Executes tools RELIABLY
464
+ │ (timeouts, retries, caching, rate limits, etc.) │
465
+ └─────────────────────────────────────────────────────┘
466
+
467
+ ┌─────────────────────────────────────────────────────┐
468
+ │ Local Tools / MCP Servers │ ← Does the actual work
469
+ └─────────────────────────────────────────────────────┘
470
+ ```
340
471
 
341
- > **Not a framework.** If LangChain/LlamaIndex help decide *which* tool to call, CHUK Tool Processor makes sure the tool call **actually succeeds**.
472
+ This separation means you can swap planners without changing execution infrastructure, and vice versa.
342
473
 
343
474
  ---
344
475
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chuk-tool-processor"
7
- version = "0.14"
7
+ version = "0.15"
8
8
  description = "Async-native framework for registering, discovering, and executing tools referenced in LLM responses"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -59,6 +59,7 @@ from chuk_tool_processor.mcp import (
59
59
  from chuk_tool_processor.mcp.stream_manager import StreamManager
60
60
 
61
61
  # Models (commonly used)
62
+ from chuk_tool_processor.models.return_order import ReturnOrder
62
63
  from chuk_tool_processor.models.tool_call import ToolCall
63
64
  from chuk_tool_processor.models.tool_result import ToolResult
64
65
 
@@ -75,6 +76,16 @@ from chuk_tool_processor.registry.auto_register import register_fn_tool
75
76
  # Decorators for registering tools
76
77
  from chuk_tool_processor.registry.decorators import register_tool, tool
77
78
 
79
+ # Scheduling
80
+ from chuk_tool_processor.scheduling import (
81
+ ExecutionPlan,
82
+ GreedyDagScheduler,
83
+ SchedulerPolicy,
84
+ SchedulingConstraints,
85
+ ToolCallSpec,
86
+ ToolMetadata,
87
+ )
88
+
78
89
  # Type checking imports (not available at runtime)
79
90
  if TYPE_CHECKING:
80
91
  # Advanced models for type hints
@@ -114,6 +125,14 @@ __all__ = [
114
125
  # Models
115
126
  "ToolCall",
116
127
  "ToolResult",
128
+ "ReturnOrder",
129
+ # Scheduling
130
+ "ToolMetadata",
131
+ "ToolCallSpec",
132
+ "SchedulingConstraints",
133
+ "ExecutionPlan",
134
+ "SchedulerPolicy",
135
+ "GreedyDagScheduler",
117
136
  # Registry
118
137
  "ToolInfo",
119
138
  "initialize",
@@ -339,6 +339,7 @@ class ToolProcessor:
339
339
  use_cache: bool = True, # noqa: ARG002
340
340
  request_id: str | None = None,
341
341
  context: ExecutionContext | None = None,
342
+ return_order: str | None = None,
342
343
  ) -> list[ToolResult]:
343
344
  """
344
345
  Process tool calls from various LLM output formats.
@@ -393,6 +394,9 @@ class ToolProcessor:
393
394
  context: Optional ExecutionContext for request-scoped data.
394
395
  Carries user_id, tenant_id, traceparent, deadline, etc.
395
396
  If provided, context.request_id takes precedence over request_id.
397
+ return_order: Order to return results in. Can be:
398
+ - "completion" (default): Results return as each tool completes
399
+ - "submission": Results return in the same order as submitted
396
400
 
397
401
  Returns:
398
402
  List of ToolResult objects. Each result contains:
@@ -514,7 +518,14 @@ class ToolProcessor:
514
518
  # Execute tools (with context scope if provided)
515
519
  async def _execute_with_context() -> list[ToolResult]:
516
520
  assert self.executor is not None
517
- result: list[ToolResult] = await self.executor.execute(calls, timeout=effective_timeout)
521
+ # If return_order is specified and strategy supports it, call run() directly
522
+ # This bypasses the wrapper chain but preserves return_order semantics
523
+ if return_order is not None and self.strategy is not None and hasattr(self.strategy, "run"):
524
+ result: list[ToolResult] = await self.strategy.run(
525
+ calls, timeout=effective_timeout, return_order=return_order
526
+ )
527
+ else:
528
+ result = await self.executor.execute(calls, timeout=effective_timeout)
518
529
  return result
519
530
 
520
531
  if context_manager: