chuk-tool-processor 0.12.2__tar.gz → 0.13__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 (76) hide show
  1. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/PKG-INFO +54 -1
  2. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/README.md +53 -0
  3. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/pyproject.toml +1 -1
  4. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +49 -8
  5. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +25 -6
  6. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/tool_executor.py +2 -1
  7. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/execution_strategy.py +19 -4
  8. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/PKG-INFO +54 -1
  9. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/setup.cfg +0 -0
  10. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/__init__.py +0 -0
  11. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/core/__init__.py +0 -0
  12. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/core/exceptions.py +0 -0
  13. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/core/processor.py +0 -0
  14. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/__init__.py +0 -0
  15. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/code_sandbox.py +0 -0
  16. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
  17. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
  18. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
  19. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
  20. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
  21. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
  22. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/__init__.py +0 -0
  23. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/context.py +0 -0
  24. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/formatter.py +0 -0
  25. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/helpers.py +0 -0
  26. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/metrics.py +0 -0
  27. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/__init__.py +0 -0
  28. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
  29. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/models.py +0 -0
  30. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
  31. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
  32. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
  33. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
  34. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
  35. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
  36. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
  37. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
  38. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
  39. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
  40. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
  41. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/__init__.py +0 -0
  42. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
  43. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_call.py +0 -0
  44. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
  45. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_result.py +0 -0
  46. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_spec.py +0 -0
  47. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/validated_tool.py +0 -0
  48. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/observability/__init__.py +0 -0
  49. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/observability/metrics.py +0 -0
  50. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/observability/setup.py +0 -0
  51. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/observability/tracing.py +0 -0
  52. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/__init__.py +0 -0
  53. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/discovery.py +0 -0
  54. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
  55. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
  56. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
  57. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
  58. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
  59. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
  60. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/py.typed +0 -0
  61. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/__init__.py +0 -0
  62. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/auto_register.py +0 -0
  63. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/decorators.py +0 -0
  64. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/interface.py +0 -0
  65. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/metadata.py +0 -0
  66. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/provider.py +0 -0
  67. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
  68. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
  69. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/tool_export.py +0 -0
  70. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/utils/__init__.py +0 -0
  71. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/utils/fast_json.py +0 -0
  72. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/utils/validation.py +0 -0
  73. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
  74. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
  75. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
  76. {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/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.12.2
3
+ Version: 0.13
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>
@@ -652,6 +652,57 @@ async with ToolProcessor(
652
652
  ''')
653
653
  ```
654
654
 
655
+ ### Parallel Execution & Streaming Results
656
+
657
+ Tools execute concurrently by default. Results return in **completion order** — faster tools return immediately without waiting for slower ones:
658
+
659
+ ```python
660
+ import asyncio
661
+ from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
662
+ from chuk_tool_processor.models.tool_call import ToolCall
663
+
664
+ # Tools with different execution times
665
+ calls = [
666
+ ToolCall(tool="slow_api", arguments={"query": "complex"}), # 500ms
667
+ ToolCall(tool="medium_api", arguments={"query": "medium"}), # 200ms
668
+ ToolCall(tool="fast_api", arguments={"query": "simple"}), # 50ms
669
+ ]
670
+
671
+ # Results return as: fast_api, medium_api, slow_api (completion order)
672
+ results = await strategy.run(calls)
673
+
674
+ # Match results back to original calls by tool name
675
+ for result in results:
676
+ print(f"{result.tool}: {result.result}")
677
+ ```
678
+
679
+ **Stream results as they arrive** with `stream_run()`:
680
+
681
+ ```python
682
+ async for result in strategy.stream_run(calls):
683
+ # Process each result immediately as it completes
684
+ print(f"Completed: {result.tool}")
685
+ ```
686
+
687
+ **Track when tools start** with `on_tool_start` callback:
688
+
689
+ ```python
690
+ async def on_start(call: ToolCall):
691
+ print(f"Starting: {call.tool}")
692
+
693
+ async for result in strategy.stream_run(calls, on_tool_start=on_start):
694
+ print(f"Completed: {result.tool}")
695
+ ```
696
+
697
+ **Control concurrency** with `max_concurrency`:
698
+
699
+ ```python
700
+ # Limit to 2 concurrent tools (others queue)
701
+ strategy = InProcessStrategy(registry, max_concurrency=2)
702
+ ```
703
+
704
+ > **See:** `examples/parallel_execution_demo.py` for a complete demonstration.
705
+
655
706
  ## Documentation Quick Reference
656
707
 
657
708
  | Document | What It Covers |
@@ -863,6 +914,8 @@ class WeatherTool(ValidatedTool):
863
914
  | **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
864
915
  | **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
865
916
 
917
+ **Parallel Execution:** Both strategies execute tools concurrently by default. Results return in **completion order** (faster tools return first), not submission order. Use `ToolResult.tool` to match results to original calls.
918
+
866
919
  ```python
867
920
  import asyncio
868
921
  from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
@@ -620,6 +620,57 @@ async with ToolProcessor(
620
620
  ''')
621
621
  ```
622
622
 
623
+ ### Parallel Execution & Streaming Results
624
+
625
+ Tools execute concurrently by default. Results return in **completion order** — faster tools return immediately without waiting for slower ones:
626
+
627
+ ```python
628
+ import asyncio
629
+ from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
630
+ from chuk_tool_processor.models.tool_call import ToolCall
631
+
632
+ # Tools with different execution times
633
+ calls = [
634
+ ToolCall(tool="slow_api", arguments={"query": "complex"}), # 500ms
635
+ ToolCall(tool="medium_api", arguments={"query": "medium"}), # 200ms
636
+ ToolCall(tool="fast_api", arguments={"query": "simple"}), # 50ms
637
+ ]
638
+
639
+ # Results return as: fast_api, medium_api, slow_api (completion order)
640
+ results = await strategy.run(calls)
641
+
642
+ # Match results back to original calls by tool name
643
+ for result in results:
644
+ print(f"{result.tool}: {result.result}")
645
+ ```
646
+
647
+ **Stream results as they arrive** with `stream_run()`:
648
+
649
+ ```python
650
+ async for result in strategy.stream_run(calls):
651
+ # Process each result immediately as it completes
652
+ print(f"Completed: {result.tool}")
653
+ ```
654
+
655
+ **Track when tools start** with `on_tool_start` callback:
656
+
657
+ ```python
658
+ async def on_start(call: ToolCall):
659
+ print(f"Starting: {call.tool}")
660
+
661
+ async for result in strategy.stream_run(calls, on_tool_start=on_start):
662
+ print(f"Completed: {result.tool}")
663
+ ```
664
+
665
+ **Control concurrency** with `max_concurrency`:
666
+
667
+ ```python
668
+ # Limit to 2 concurrent tools (others queue)
669
+ strategy = InProcessStrategy(registry, max_concurrency=2)
670
+ ```
671
+
672
+ > **See:** `examples/parallel_execution_demo.py` for a complete demonstration.
673
+
623
674
  ## Documentation Quick Reference
624
675
 
625
676
  | Document | What It Covers |
@@ -831,6 +882,8 @@ class WeatherTool(ValidatedTool):
831
882
  | **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
832
883
  | **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
833
884
 
885
+ **Parallel Execution:** Both strategies execute tools concurrently by default. Results return in **completion order** (faster tools return first), not submission order. Use `ToolResult.tool` to match results to original calls.
886
+
834
887
  ```python
835
888
  import asyncio
836
889
  from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chuk-tool-processor"
7
- version = "0.12.2"
7
+ version = "0.13"
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"
@@ -7,6 +7,18 @@ This strategy executes tools concurrently in the same process using asyncio.
7
7
  It has special support for streaming tools, accessing their stream_execute method
8
8
  directly to enable true item-by-item streaming.
9
9
 
10
+ PARALLEL EXECUTION:
11
+ - All tool calls execute concurrently using asyncio tasks
12
+ - Results are returned/yielded as each tool completes (completion order, not submission order)
13
+ - Faster tools return immediately without waiting for slower ones
14
+ - Use run() for batch results in completion order
15
+ - Use stream_run() to yield results as they arrive
16
+
17
+ STREAMING SUPPORT:
18
+ - stream_run() yields ToolResult objects as each tool completes
19
+ - Optional on_tool_start callback for emitting start events
20
+ - True streaming for tools that implement stream_execute
21
+
10
22
  Enhanced tool name resolution that properly handles:
11
23
  - Simple names: "get_current_time"
12
24
  - Namespaced names: "diagnostic_test.get_current_time"
@@ -23,7 +35,7 @@ import builtins
23
35
  import inspect
24
36
  import os
25
37
  import platform
26
- from collections.abc import AsyncIterator
38
+ from collections.abc import AsyncIterator, Awaitable, Callable
27
39
  from contextlib import asynccontextmanager, suppress
28
40
  from datetime import UTC, datetime
29
41
  from typing import Any
@@ -122,14 +134,17 @@ class InProcessStrategy(ExecutionStrategy):
122
134
  timeout: float | None = None,
123
135
  ) -> list[ToolResult]:
124
136
  """
125
- Execute tool calls concurrently and preserve order.
137
+ Execute tool calls concurrently and return results as they complete.
138
+
139
+ NOTE: Results are returned in COMPLETION ORDER, not submission order.
140
+ This allows faster tools to return immediately without waiting for slower ones.
126
141
 
127
142
  Args:
128
143
  calls: List of tool calls to execute
129
144
  timeout: Optional timeout for execution
130
145
 
131
146
  Returns:
132
- List of tool results in the same order as calls
147
+ List of tool results in completion order (not submission order)
133
148
  """
134
149
  if not calls:
135
150
  return []
@@ -138,6 +153,7 @@ class InProcessStrategy(ExecutionStrategy):
138
153
  effective_timeout = timeout if timeout is not None else self.default_timeout
139
154
  logger.debug("Executing %d calls with %ss timeout each", len(calls), effective_timeout)
140
155
 
156
+ # Create all tasks immediately so they start running in parallel
141
157
  tasks = []
142
158
  for call in calls:
143
159
  task = asyncio.create_task(
@@ -148,17 +164,35 @@ class InProcessStrategy(ExecutionStrategy):
148
164
  tasks.append(task)
149
165
 
150
166
  async with log_context_span("inprocess_execution", {"num_calls": len(calls)}):
151
- return await asyncio.gather(*tasks)
167
+ # Use as_completed to return results as they finish, not all at once
168
+ results = []
169
+ for completed_task in asyncio.as_completed(tasks):
170
+ result = await completed_task
171
+ results.append(result)
172
+ return results
152
173
 
153
174
  # ------------------------------------------------------------------ #
154
175
  async def stream_run(
155
176
  self,
156
177
  calls: list[ToolCall],
157
178
  timeout: float | None = None,
179
+ on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None,
158
180
  ) -> AsyncIterator[ToolResult]:
159
181
  """
160
182
  Execute tool calls concurrently and *yield* results as soon as they are
161
- produced, preserving completion order.
183
+ produced in completion order (not submission order).
184
+
185
+ This method allows results to stream back as each tool completes, without
186
+ waiting for all tools to finish. Faster tools return immediately.
187
+
188
+ Args:
189
+ calls: List of tool calls to execute concurrently
190
+ timeout: Optional timeout for each tool execution
191
+ on_tool_start: Optional callback invoked when each tool starts execution.
192
+ Useful for emitting start events before results arrive.
193
+
194
+ Yields:
195
+ ToolResult objects as each tool completes (in completion order)
162
196
  """
163
197
  if not calls:
164
198
  return
@@ -168,9 +202,7 @@ class InProcessStrategy(ExecutionStrategy):
168
202
 
169
203
  queue: asyncio.Queue[ToolResult] = asyncio.Queue()
170
204
  tasks = {
171
- asyncio.create_task(
172
- self._stream_tool_call(call, queue, effective_timeout) # Always pass timeout
173
- )
205
+ asyncio.create_task(self._stream_tool_call(call, queue, effective_timeout, on_tool_start))
174
206
  for call in calls
175
207
  if call.id not in self._direct_streaming_calls
176
208
  }
@@ -194,6 +226,7 @@ class InProcessStrategy(ExecutionStrategy):
194
226
  call: ToolCall,
195
227
  queue: asyncio.Queue,
196
228
  timeout: float, # Make timeout required
229
+ on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None,
197
230
  ) -> None:
198
231
  """
199
232
  Execute a tool call with streaming support.
@@ -205,6 +238,7 @@ class InProcessStrategy(ExecutionStrategy):
205
238
  call: The tool call to execute
206
239
  queue: Queue to put results into
207
240
  timeout: Timeout in seconds (required)
241
+ on_tool_start: Optional callback to invoke when tool execution starts
208
242
  """
209
243
  # Skip if call is being handled directly by the executor
210
244
  if call.id in self._direct_streaming_calls:
@@ -225,6 +259,13 @@ class InProcessStrategy(ExecutionStrategy):
225
259
  await queue.put(result)
226
260
  return
227
261
 
262
+ # Invoke start callback if provided
263
+ if on_tool_start:
264
+ try:
265
+ await on_tool_start(call)
266
+ except Exception as e:
267
+ logger.warning(f"on_tool_start callback failed for {call.tool}: {e}")
268
+
228
269
  try:
229
270
  # Use enhanced tool resolution instead of direct lookup
230
271
  tool_impl, resolved_namespace = await self._resolve_tool_info(call.tool, call.namespace)
@@ -25,7 +25,7 @@ import os
25
25
  import pickle
26
26
  import platform
27
27
  import signal
28
- from collections.abc import AsyncIterator
28
+ from collections.abc import AsyncIterator, Awaitable, Callable
29
29
  from datetime import UTC, datetime
30
30
  from typing import Any
31
31
 
@@ -264,14 +264,17 @@ class SubprocessStrategy(ExecutionStrategy):
264
264
  timeout: float | None = None,
265
265
  ) -> list[ToolResult]:
266
266
  """
267
- Execute tool calls in separate processes.
267
+ Execute tool calls in separate processes and return results as they complete.
268
+
269
+ NOTE: Results are returned in COMPLETION ORDER, not submission order.
270
+ This allows faster tools to return immediately without waiting for slower ones.
268
271
 
269
272
  Args:
270
273
  calls: List of tool calls to execute
271
274
  timeout: Optional timeout for each execution (overrides default)
272
275
 
273
276
  Returns:
274
- List of tool results in the same order as calls
277
+ List of tool results in completion order (not submission order)
275
278
  """
276
279
  if not calls:
277
280
  return []
@@ -308,14 +311,19 @@ class SubprocessStrategy(ExecutionStrategy):
308
311
  task.add_done_callback(self._active_tasks.discard)
309
312
  tasks.append(task)
310
313
 
311
- # Execute all tasks concurrently
314
+ # Execute all tasks concurrently and return results in completion order
312
315
  async with log_context_span("subprocess_execution", {"num_calls": len(calls)}):
313
- return await asyncio.gather(*tasks)
316
+ results = []
317
+ for completed_task in asyncio.as_completed(tasks):
318
+ result = await completed_task
319
+ results.append(result)
320
+ return results
314
321
 
315
322
  async def stream_run(
316
323
  self,
317
324
  calls: list[ToolCall],
318
325
  timeout: float | None = None,
326
+ on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None,
319
327
  ) -> AsyncIterator[ToolResult]:
320
328
  """
321
329
  Execute tool calls and yield results as they become available.
@@ -323,9 +331,11 @@ class SubprocessStrategy(ExecutionStrategy):
323
331
  Args:
324
332
  calls: List of tool calls to execute
325
333
  timeout: Optional timeout for each execution
334
+ on_tool_start: Optional callback invoked when each tool starts execution.
335
+ Useful for emitting start events before results arrive.
326
336
 
327
337
  Yields:
328
- Tool results as they complete (not necessarily in order)
338
+ Tool results as they complete (in completion order)
329
339
  """
330
340
  if not calls:
331
341
  return
@@ -358,6 +368,7 @@ class SubprocessStrategy(ExecutionStrategy):
358
368
  call,
359
369
  queue,
360
370
  effective_timeout, # Always pass concrete timeout
371
+ on_tool_start,
361
372
  )
362
373
  )
363
374
  self._active_tasks.add(task)
@@ -385,8 +396,16 @@ class SubprocessStrategy(ExecutionStrategy):
385
396
  call: ToolCall,
386
397
  queue: asyncio.Queue,
387
398
  timeout: float, # Make timeout required
399
+ on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None,
388
400
  ) -> None:
389
401
  """Execute a single call and put the result in the queue."""
402
+ # Invoke start callback if provided
403
+ if on_tool_start:
404
+ try:
405
+ await on_tool_start(call)
406
+ except Exception as e:
407
+ logger.warning(f"on_tool_start callback failed for {call.tool}: {e}")
408
+
390
409
  result = await self._execute_single_call(call, timeout)
391
410
  await queue.put(result)
392
411
 
@@ -107,7 +107,8 @@ class ToolExecutor:
107
107
  use_cache: Whether to use cached results (for caching wrappers)
108
108
 
109
109
  Returns:
110
- List of tool results in the same order as calls
110
+ List of tool results in completion order (not submission order).
111
+ Use ToolResult.tool to match results back to their original calls.
111
112
  """
112
113
  if not calls:
113
114
  return []
@@ -6,7 +6,7 @@ Abstract base class for tool execution strategies.
6
6
  from __future__ import annotations
7
7
 
8
8
  from abc import ABC, abstractmethod
9
- from collections.abc import AsyncIterator
9
+ from collections.abc import AsyncIterator, Awaitable, Callable
10
10
 
11
11
  from chuk_tool_processor.models.tool_call import ToolCall
12
12
  from chuk_tool_processor.models.tool_result import ToolResult
@@ -18,6 +18,11 @@ class ExecutionStrategy(ABC):
18
18
 
19
19
  All execution strategies must implement at least the run method,
20
20
  and optionally stream_run for streaming support.
21
+
22
+ PARALLEL EXECUTION NOTE:
23
+ Results are returned in COMPLETION ORDER, not submission order.
24
+ This allows faster tools to return immediately without waiting for slower ones.
25
+ Use the ToolResult.tool attribute to match results back to their original calls.
21
26
  """
22
27
 
23
28
  @abstractmethod
@@ -30,11 +35,17 @@ class ExecutionStrategy(ABC):
30
35
  timeout: Optional timeout in seconds for each call
31
36
 
32
37
  Returns:
33
- List of ToolResult objects in the same order as the calls
38
+ List of ToolResult objects in completion order (not submission order).
39
+ Use ToolResult.tool to match results back to their original calls.
34
40
  """
35
41
  pass
36
42
 
37
- async def stream_run(self, calls: list[ToolCall], timeout: float | None = None) -> AsyncIterator[ToolResult]:
43
+ async def stream_run(
44
+ self,
45
+ calls: list[ToolCall],
46
+ timeout: float | None = None,
47
+ on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None, # noqa: ARG002
48
+ ) -> AsyncIterator[ToolResult]:
38
49
  """
39
50
  Execute tool calls and yield results as they become available.
40
51
 
@@ -44,10 +55,14 @@ class ExecutionStrategy(ABC):
44
55
  Args:
45
56
  calls: List of ToolCall objects to execute
46
57
  timeout: Optional timeout in seconds for each call
58
+ on_tool_start: Optional callback invoked when each tool starts execution.
59
+ Useful for emitting start events before results arrive.
47
60
 
48
61
  Yields:
49
- ToolResult objects as they become available
62
+ ToolResult objects as they become available (in completion order)
50
63
  """
64
+ # Default implementation ignores on_tool_start since we batch execute
65
+ # Subclasses with true streaming can use the callback
51
66
  results = await self.run(calls, timeout=timeout)
52
67
  for result in results:
53
68
  yield result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.12.2
3
+ Version: 0.13
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>
@@ -652,6 +652,57 @@ async with ToolProcessor(
652
652
  ''')
653
653
  ```
654
654
 
655
+ ### Parallel Execution & Streaming Results
656
+
657
+ Tools execute concurrently by default. Results return in **completion order** — faster tools return immediately without waiting for slower ones:
658
+
659
+ ```python
660
+ import asyncio
661
+ from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
662
+ from chuk_tool_processor.models.tool_call import ToolCall
663
+
664
+ # Tools with different execution times
665
+ calls = [
666
+ ToolCall(tool="slow_api", arguments={"query": "complex"}), # 500ms
667
+ ToolCall(tool="medium_api", arguments={"query": "medium"}), # 200ms
668
+ ToolCall(tool="fast_api", arguments={"query": "simple"}), # 50ms
669
+ ]
670
+
671
+ # Results return as: fast_api, medium_api, slow_api (completion order)
672
+ results = await strategy.run(calls)
673
+
674
+ # Match results back to original calls by tool name
675
+ for result in results:
676
+ print(f"{result.tool}: {result.result}")
677
+ ```
678
+
679
+ **Stream results as they arrive** with `stream_run()`:
680
+
681
+ ```python
682
+ async for result in strategy.stream_run(calls):
683
+ # Process each result immediately as it completes
684
+ print(f"Completed: {result.tool}")
685
+ ```
686
+
687
+ **Track when tools start** with `on_tool_start` callback:
688
+
689
+ ```python
690
+ async def on_start(call: ToolCall):
691
+ print(f"Starting: {call.tool}")
692
+
693
+ async for result in strategy.stream_run(calls, on_tool_start=on_start):
694
+ print(f"Completed: {result.tool}")
695
+ ```
696
+
697
+ **Control concurrency** with `max_concurrency`:
698
+
699
+ ```python
700
+ # Limit to 2 concurrent tools (others queue)
701
+ strategy = InProcessStrategy(registry, max_concurrency=2)
702
+ ```
703
+
704
+ > **See:** `examples/parallel_execution_demo.py` for a complete demonstration.
705
+
655
706
  ## Documentation Quick Reference
656
707
 
657
708
  | Document | What It Covers |
@@ -863,6 +914,8 @@ class WeatherTool(ValidatedTool):
863
914
  | **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
864
915
  | **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
865
916
 
917
+ **Parallel Execution:** Both strategies execute tools concurrently by default. Results return in **completion order** (faster tools return first), not submission order. Use `ToolResult.tool` to match results to original calls.
918
+
866
919
  ```python
867
920
  import asyncio
868
921
  from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry