chuk-tool-processor 0.6.4__py3-none-any.whl → 0.9.7__py3-none-any.whl

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.

Potentially problematic release.


This version of chuk-tool-processor might be problematic. Click here for more details.

Files changed (66) hide show
  1. chuk_tool_processor/core/__init__.py +32 -1
  2. chuk_tool_processor/core/exceptions.py +225 -13
  3. chuk_tool_processor/core/processor.py +135 -104
  4. chuk_tool_processor/execution/strategies/__init__.py +6 -0
  5. chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
  6. chuk_tool_processor/execution/strategies/subprocess_strategy.py +202 -206
  7. chuk_tool_processor/execution/tool_executor.py +82 -84
  8. chuk_tool_processor/execution/wrappers/__init__.py +42 -0
  9. chuk_tool_processor/execution/wrappers/caching.py +150 -116
  10. chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
  11. chuk_tool_processor/execution/wrappers/rate_limiting.py +76 -43
  12. chuk_tool_processor/execution/wrappers/retry.py +116 -78
  13. chuk_tool_processor/logging/__init__.py +23 -17
  14. chuk_tool_processor/logging/context.py +40 -45
  15. chuk_tool_processor/logging/formatter.py +22 -21
  16. chuk_tool_processor/logging/helpers.py +28 -42
  17. chuk_tool_processor/logging/metrics.py +13 -15
  18. chuk_tool_processor/mcp/__init__.py +8 -12
  19. chuk_tool_processor/mcp/mcp_tool.py +158 -114
  20. chuk_tool_processor/mcp/register_mcp_tools.py +22 -22
  21. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +57 -17
  22. chuk_tool_processor/mcp/setup_mcp_sse.py +57 -17
  23. chuk_tool_processor/mcp/setup_mcp_stdio.py +11 -11
  24. chuk_tool_processor/mcp/stream_manager.py +333 -276
  25. chuk_tool_processor/mcp/transport/__init__.py +22 -29
  26. chuk_tool_processor/mcp/transport/base_transport.py +180 -44
  27. chuk_tool_processor/mcp/transport/http_streamable_transport.py +505 -325
  28. chuk_tool_processor/mcp/transport/models.py +100 -0
  29. chuk_tool_processor/mcp/transport/sse_transport.py +607 -276
  30. chuk_tool_processor/mcp/transport/stdio_transport.py +597 -116
  31. chuk_tool_processor/models/__init__.py +21 -1
  32. chuk_tool_processor/models/execution_strategy.py +16 -21
  33. chuk_tool_processor/models/streaming_tool.py +28 -25
  34. chuk_tool_processor/models/tool_call.py +49 -31
  35. chuk_tool_processor/models/tool_export_mixin.py +22 -8
  36. chuk_tool_processor/models/tool_result.py +40 -77
  37. chuk_tool_processor/models/tool_spec.py +350 -0
  38. chuk_tool_processor/models/validated_tool.py +36 -18
  39. chuk_tool_processor/observability/__init__.py +30 -0
  40. chuk_tool_processor/observability/metrics.py +312 -0
  41. chuk_tool_processor/observability/setup.py +105 -0
  42. chuk_tool_processor/observability/tracing.py +345 -0
  43. chuk_tool_processor/plugins/__init__.py +1 -1
  44. chuk_tool_processor/plugins/discovery.py +11 -11
  45. chuk_tool_processor/plugins/parsers/__init__.py +1 -1
  46. chuk_tool_processor/plugins/parsers/base.py +1 -2
  47. chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
  48. chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
  49. chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
  50. chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
  51. chuk_tool_processor/registry/__init__.py +12 -12
  52. chuk_tool_processor/registry/auto_register.py +22 -30
  53. chuk_tool_processor/registry/decorators.py +127 -129
  54. chuk_tool_processor/registry/interface.py +26 -23
  55. chuk_tool_processor/registry/metadata.py +27 -22
  56. chuk_tool_processor/registry/provider.py +17 -18
  57. chuk_tool_processor/registry/providers/__init__.py +16 -19
  58. chuk_tool_processor/registry/providers/memory.py +18 -25
  59. chuk_tool_processor/registry/tool_export.py +42 -51
  60. chuk_tool_processor/utils/validation.py +15 -16
  61. chuk_tool_processor-0.9.7.dist-info/METADATA +1813 -0
  62. chuk_tool_processor-0.9.7.dist-info/RECORD +67 -0
  63. chuk_tool_processor-0.6.4.dist-info/METADATA +0 -697
  64. chuk_tool_processor-0.6.4.dist-info/RECORD +0 -60
  65. {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/WHEEL +0 -0
  66. {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/top_level.txt +0 -0
@@ -8,28 +8,31 @@ It has special support for streaming tools, accessing their stream_execute metho
8
8
  directly to enable true item-by-item streaming.
9
9
 
10
10
  Enhanced tool name resolution that properly handles:
11
- - Simple names: "get_current_time"
11
+ - Simple names: "get_current_time"
12
12
  - Namespaced names: "diagnostic_test.get_current_time"
13
13
  - Cross-namespace fallback searching
14
14
 
15
15
  Ensures consistent timeout handling across all execution paths.
16
16
  ENHANCED: Clean shutdown handling to prevent anyio cancel scope errors.
17
17
  """
18
+
18
19
  from __future__ import annotations
19
20
 
20
21
  import asyncio
22
+ import builtins
21
23
  import inspect
22
24
  import os
23
- from contextlib import asynccontextmanager
24
- from datetime import datetime, timezone
25
- from typing import Any, List, Optional, AsyncIterator, Set, Tuple
25
+ import platform
26
+ from collections.abc import AsyncIterator
27
+ from contextlib import asynccontextmanager, suppress
28
+ from datetime import UTC, datetime
29
+ from typing import Any
26
30
 
27
- from chuk_tool_processor.core.exceptions import ToolExecutionError
31
+ from chuk_tool_processor.logging import get_logger, log_context_span
28
32
  from chuk_tool_processor.models.execution_strategy import ExecutionStrategy
29
33
  from chuk_tool_processor.models.tool_call import ToolCall
30
34
  from chuk_tool_processor.models.tool_result import ToolResult
31
35
  from chuk_tool_processor.registry.interface import ToolRegistryInterface
32
- from chuk_tool_processor.logging import get_logger, log_context_span
33
36
 
34
37
  logger = get_logger("chuk_tool_processor.execution.inprocess_strategy")
35
38
 
@@ -49,12 +52,12 @@ class InProcessStrategy(ExecutionStrategy):
49
52
  def __init__(
50
53
  self,
51
54
  registry: ToolRegistryInterface,
52
- default_timeout: Optional[float] = None,
53
- max_concurrency: Optional[int] = None,
55
+ default_timeout: float | None = None,
56
+ max_concurrency: int | None = None,
54
57
  ) -> None:
55
58
  """
56
59
  Initialize the in-process execution strategy.
57
-
60
+
58
61
  Args:
59
62
  registry: Tool registry to use for tool lookups
60
63
  default_timeout: Default timeout for tool execution
@@ -63,43 +66,46 @@ class InProcessStrategy(ExecutionStrategy):
63
66
  self.registry = registry
64
67
  self.default_timeout = default_timeout or 30.0 # Always have a default
65
68
  self._sem = asyncio.Semaphore(max_concurrency) if max_concurrency else None
66
-
69
+
67
70
  # Task tracking for cleanup
68
71
  self._active_tasks = set()
69
72
  self._shutting_down = False
70
73
  self._shutdown_event = asyncio.Event()
71
-
74
+
72
75
  # Tracking for which calls are being handled directly by the executor
73
76
  # to prevent duplicate streaming results
74
77
  self._direct_streaming_calls = set()
75
-
76
- logger.debug("InProcessStrategy initialized with timeout: %ss, max_concurrency: %s",
77
- self.default_timeout, max_concurrency)
78
+
79
+ logger.debug(
80
+ "InProcessStrategy initialized with timeout: %ss, max_concurrency: %s",
81
+ self.default_timeout,
82
+ max_concurrency,
83
+ )
78
84
 
79
85
  # ------------------------------------------------------------------ #
80
- def mark_direct_streaming(self, call_ids: Set[str]) -> None:
86
+ def mark_direct_streaming(self, call_ids: set[str]) -> None:
81
87
  """
82
88
  Mark tool calls that are being handled directly by the executor.
83
-
89
+
84
90
  Args:
85
91
  call_ids: Set of call IDs that should be skipped during streaming
86
92
  because they're handled directly
87
93
  """
88
94
  self._direct_streaming_calls.update(call_ids)
89
-
95
+
90
96
  def clear_direct_streaming(self) -> None:
91
97
  """Clear the list of direct streaming calls."""
92
98
  self._direct_streaming_calls.clear()
93
-
99
+
94
100
  # ------------------------------------------------------------------ #
95
101
  # 🔌 legacy façade for older wrappers #
96
102
  # ------------------------------------------------------------------ #
97
103
  async def execute(
98
104
  self,
99
- calls: List[ToolCall],
105
+ calls: list[ToolCall],
100
106
  *,
101
- timeout: Optional[float] = None,
102
- ) -> List[ToolResult]:
107
+ timeout: float | None = None,
108
+ ) -> list[ToolResult]:
103
109
  """
104
110
  Back-compat shim.
105
111
 
@@ -112,26 +118,26 @@ class InProcessStrategy(ExecutionStrategy):
112
118
  # ------------------------------------------------------------------ #
113
119
  async def run(
114
120
  self,
115
- calls: List[ToolCall],
116
- timeout: Optional[float] = None,
117
- ) -> List[ToolResult]:
121
+ calls: list[ToolCall],
122
+ timeout: float | None = None,
123
+ ) -> list[ToolResult]:
118
124
  """
119
125
  Execute tool calls concurrently and preserve order.
120
-
126
+
121
127
  Args:
122
128
  calls: List of tool calls to execute
123
129
  timeout: Optional timeout for execution
124
-
130
+
125
131
  Returns:
126
132
  List of tool results in the same order as calls
127
133
  """
128
134
  if not calls:
129
135
  return []
130
-
136
+
131
137
  # Use default_timeout if no timeout specified
132
138
  effective_timeout = timeout if timeout is not None else self.default_timeout
133
139
  logger.debug("Executing %d calls with %ss timeout each", len(calls), effective_timeout)
134
-
140
+
135
141
  tasks = []
136
142
  for call in calls:
137
143
  task = asyncio.create_task(
@@ -140,15 +146,15 @@ class InProcessStrategy(ExecutionStrategy):
140
146
  self._active_tasks.add(task)
141
147
  task.add_done_callback(self._active_tasks.discard)
142
148
  tasks.append(task)
143
-
149
+
144
150
  async with log_context_span("inprocess_execution", {"num_calls": len(calls)}):
145
151
  return await asyncio.gather(*tasks)
146
152
 
147
153
  # ------------------------------------------------------------------ #
148
154
  async def stream_run(
149
155
  self,
150
- calls: List[ToolCall],
151
- timeout: Optional[float] = None,
156
+ calls: list[ToolCall],
157
+ timeout: float | None = None,
152
158
  ) -> AsyncIterator[ToolResult]:
153
159
  """
154
160
  Execute tool calls concurrently and *yield* results as soon as they are
@@ -183,7 +189,6 @@ class InProcessStrategy(ExecutionStrategy):
183
189
  for t in done:
184
190
  t.result() # re-raise if a task crashed
185
191
 
186
-
187
192
  async def _stream_tool_call(
188
193
  self,
189
194
  call: ToolCall,
@@ -192,10 +197,10 @@ class InProcessStrategy(ExecutionStrategy):
192
197
  ) -> None:
193
198
  """
194
199
  Execute a tool call with streaming support.
195
-
200
+
196
201
  This looks up the tool and if it's a streaming tool, it accesses
197
202
  stream_execute directly to get item-by-item streaming.
198
-
203
+
199
204
  Args:
200
205
  call: The tool call to execute
201
206
  queue: Queue to put results into
@@ -204,48 +209,48 @@ class InProcessStrategy(ExecutionStrategy):
204
209
  # Skip if call is being handled directly by the executor
205
210
  if call.id in self._direct_streaming_calls:
206
211
  return
207
-
212
+
208
213
  if self._shutting_down:
209
214
  # Early exit if shutting down
210
- now = datetime.now(timezone.utc)
215
+ now = datetime.now(UTC)
211
216
  result = ToolResult(
212
217
  tool=call.tool,
213
218
  result=None,
214
219
  error="System is shutting down",
215
220
  start_time=now,
216
221
  end_time=now,
217
- machine=os.uname().nodename,
222
+ machine=platform.node(),
218
223
  pid=os.getpid(),
219
224
  )
220
225
  await queue.put(result)
221
226
  return
222
-
227
+
223
228
  try:
224
229
  # Use enhanced tool resolution instead of direct lookup
225
230
  tool_impl, resolved_namespace = await self._resolve_tool_info(call.tool, call.namespace)
226
231
  if tool_impl is None:
227
232
  # Tool not found
228
- now = datetime.now(timezone.utc)
233
+ now = datetime.now(UTC)
229
234
  result = ToolResult(
230
235
  tool=call.tool,
231
236
  result=None,
232
237
  error=f"Tool '{call.tool}' not found in any namespace",
233
238
  start_time=now,
234
239
  end_time=now,
235
- machine=os.uname().nodename,
240
+ machine=platform.node(),
236
241
  pid=os.getpid(),
237
242
  )
238
243
  await queue.put(result)
239
244
  return
240
-
245
+
241
246
  logger.debug(f"Resolved streaming tool '{call.tool}' to namespace '{resolved_namespace}'")
242
-
247
+
243
248
  # Instantiate if class
244
249
  tool = tool_impl() if inspect.isclass(tool_impl) else tool_impl
245
-
250
+
246
251
  # Use semaphore if available
247
252
  guard = self._sem if self._sem is not None else _noop_cm()
248
-
253
+
249
254
  async with guard:
250
255
  # Check if this is a streaming tool
251
256
  if hasattr(tool, "supports_streaming") and tool.supports_streaming and hasattr(tool, "stream_execute"):
@@ -255,66 +260,66 @@ class InProcessStrategy(ExecutionStrategy):
255
260
  # Use regular execution for non-streaming tools
256
261
  result = await self._execute_single_call(call, timeout)
257
262
  await queue.put(result)
258
-
263
+
259
264
  except asyncio.CancelledError:
260
265
  # Handle cancellation gracefully
261
- now = datetime.now(timezone.utc)
266
+ now = datetime.now(UTC)
262
267
  result = ToolResult(
263
268
  tool=call.tool,
264
269
  result=None,
265
270
  error="Execution was cancelled",
266
271
  start_time=now,
267
272
  end_time=now,
268
- machine=os.uname().nodename,
273
+ machine=platform.node(),
269
274
  pid=os.getpid(),
270
275
  )
271
276
  await queue.put(result)
272
-
277
+
273
278
  except Exception as e:
274
279
  # Handle other errors
275
- now = datetime.now(timezone.utc)
280
+ now = datetime.now(UTC)
276
281
  result = ToolResult(
277
282
  tool=call.tool,
278
283
  result=None,
279
284
  error=f"Error setting up execution: {e}",
280
285
  start_time=now,
281
286
  end_time=now,
282
- machine=os.uname().nodename,
287
+ machine=platform.node(),
283
288
  pid=os.getpid(),
284
289
  )
285
290
  await queue.put(result)
286
-
291
+
287
292
  async def _stream_with_timeout(
288
- self,
289
- tool: Any,
290
- call: ToolCall,
291
- queue: asyncio.Queue,
293
+ self,
294
+ tool: Any,
295
+ call: ToolCall,
296
+ queue: asyncio.Queue,
292
297
  timeout: float, # Make timeout required
293
298
  ) -> None:
294
299
  """
295
300
  Stream results from a streaming tool with timeout support.
296
-
301
+
297
302
  This method accesses the tool's stream_execute method directly
298
303
  and puts each yielded result into the queue.
299
-
304
+
300
305
  Args:
301
306
  tool: The tool instance
302
307
  call: Tool call data
303
308
  queue: Queue to put results into
304
309
  timeout: Timeout in seconds (required)
305
310
  """
306
- start_time = datetime.now(timezone.utc)
307
- machine = os.uname().nodename
311
+ start_time = datetime.now(UTC)
312
+ machine = platform.node()
308
313
  pid = os.getpid()
309
-
314
+
310
315
  logger.debug("Streaming %s with %ss timeout", call.tool, timeout)
311
-
316
+
312
317
  # Define the streaming task
313
318
  async def streamer():
314
319
  try:
315
320
  async for result in tool.stream_execute(**call.arguments):
316
321
  # Create a ToolResult for each streamed item
317
- now = datetime.now(timezone.utc)
322
+ now = datetime.now(UTC)
318
323
  tool_result = ToolResult(
319
324
  tool=call.tool,
320
325
  result=result,
@@ -327,7 +332,7 @@ class InProcessStrategy(ExecutionStrategy):
327
332
  await queue.put(tool_result)
328
333
  except Exception as e:
329
334
  # Handle errors during streaming
330
- now = datetime.now(timezone.utc)
335
+ now = datetime.now(UTC)
331
336
  error_result = ToolResult(
332
337
  tool=call.tool,
333
338
  result=None,
@@ -338,19 +343,18 @@ class InProcessStrategy(ExecutionStrategy):
338
343
  pid=pid,
339
344
  )
340
345
  await queue.put(error_result)
341
-
346
+
342
347
  try:
343
348
  # Always execute with timeout
344
349
  await asyncio.wait_for(streamer(), timeout)
345
350
  logger.debug("%s streaming completed within %ss", call.tool, timeout)
346
-
347
- except asyncio.TimeoutError:
351
+
352
+ except TimeoutError:
348
353
  # Handle timeout
349
- now = datetime.now(timezone.utc)
354
+ now = datetime.now(UTC)
350
355
  actual_duration = (now - start_time).total_seconds()
351
- logger.debug("%s streaming timed out after %.3fs (limit: %ss)",
352
- call.tool, actual_duration, timeout)
353
-
356
+ logger.debug("%s streaming timed out after %.3fs (limit: %ss)", call.tool, actual_duration, timeout)
357
+
354
358
  timeout_result = ToolResult(
355
359
  tool=call.tool,
356
360
  result=None,
@@ -361,12 +365,12 @@ class InProcessStrategy(ExecutionStrategy):
361
365
  pid=pid,
362
366
  )
363
367
  await queue.put(timeout_result)
364
-
368
+
365
369
  except Exception as e:
366
370
  # Handle other errors
367
- now = datetime.now(timezone.utc)
371
+ now = datetime.now(UTC)
368
372
  logger.debug("%s streaming failed: %s", call.tool, e)
369
-
373
+
370
374
  error_result = ToolResult(
371
375
  tool=call.tool,
372
376
  result=None,
@@ -388,7 +392,7 @@ class InProcessStrategy(ExecutionStrategy):
388
392
  # Skip if call is being handled directly by the executor
389
393
  if call.id in self._direct_streaming_calls:
390
394
  return
391
-
395
+
392
396
  result = await self._execute_single_call(call, timeout)
393
397
  await queue.put(result)
394
398
 
@@ -403,20 +407,20 @@ class InProcessStrategy(ExecutionStrategy):
403
407
 
404
408
  The entire invocation - including argument validation - is wrapped
405
409
  by the semaphore to honour *max_concurrency*.
406
-
410
+
407
411
  Args:
408
412
  call: Tool call to execute
409
413
  timeout: Timeout in seconds (required)
410
-
414
+
411
415
  Returns:
412
416
  Tool execution result
413
417
  """
414
418
  pid = os.getpid()
415
- machine = os.uname().nodename
416
- start = datetime.now(timezone.utc)
417
-
419
+ machine = platform.node()
420
+ start = datetime.now(UTC)
421
+
418
422
  logger.debug("Executing %s with %ss timeout", call.tool, timeout)
419
-
423
+
420
424
  # Early exit if shutting down
421
425
  if self._shutting_down:
422
426
  return ToolResult(
@@ -424,7 +428,7 @@ class InProcessStrategy(ExecutionStrategy):
424
428
  result=None,
425
429
  error="System is shutting down",
426
430
  start_time=start,
427
- end_time=datetime.now(timezone.utc),
431
+ end_time=datetime.now(UTC),
428
432
  machine=machine,
429
433
  pid=pid,
430
434
  )
@@ -438,7 +442,7 @@ class InProcessStrategy(ExecutionStrategy):
438
442
  result=None,
439
443
  error=f"Tool '{call.tool}' not found in any namespace",
440
444
  start_time=start,
441
- end_time=datetime.now(timezone.utc),
445
+ end_time=datetime.now(UTC),
442
446
  machine=machine,
443
447
  pid=pid,
444
448
  )
@@ -447,15 +451,13 @@ class InProcessStrategy(ExecutionStrategy):
447
451
 
448
452
  # Instantiate if class
449
453
  tool = impl() if inspect.isclass(impl) else impl
450
-
454
+
451
455
  # Use semaphore if available
452
456
  guard = self._sem if self._sem is not None else _noop_cm()
453
457
 
454
458
  try:
455
459
  async with guard:
456
- return await self._run_with_timeout(
457
- tool, call, timeout, start, machine, pid
458
- )
460
+ return await self._run_with_timeout(tool, call, timeout, start, machine, pid)
459
461
  except Exception as exc:
460
462
  logger.exception("Unexpected error while executing %s", call.tool)
461
463
  return ToolResult(
@@ -463,7 +465,7 @@ class InProcessStrategy(ExecutionStrategy):
463
465
  result=None,
464
466
  error=f"Unexpected error: {exc}",
465
467
  start_time=start,
466
- end_time=datetime.now(timezone.utc),
468
+ end_time=datetime.now(UTC),
467
469
  machine=machine,
468
470
  pid=pid,
469
471
  )
@@ -474,7 +476,7 @@ class InProcessStrategy(ExecutionStrategy):
474
476
  result=None,
475
477
  error="Execution was cancelled",
476
478
  start_time=start,
477
- end_time=datetime.now(timezone.utc),
479
+ end_time=datetime.now(UTC),
478
480
  machine=machine,
479
481
  pid=pid,
480
482
  )
@@ -485,7 +487,7 @@ class InProcessStrategy(ExecutionStrategy):
485
487
  result=None,
486
488
  error=f"Setup error: {exc}",
487
489
  start_time=start,
488
- end_time=datetime.now(timezone.utc),
490
+ end_time=datetime.now(UTC),
489
491
  machine=machine,
490
492
  pid=pid,
491
493
  )
@@ -501,7 +503,7 @@ class InProcessStrategy(ExecutionStrategy):
501
503
  ) -> ToolResult:
502
504
  """
503
505
  Resolve the correct async entry-point and invoke it with a guaranteed timeout.
504
-
506
+
505
507
  Args:
506
508
  tool: Tool instance
507
509
  call: Tool call data
@@ -509,28 +511,23 @@ class InProcessStrategy(ExecutionStrategy):
509
511
  start: Start time for the execution
510
512
  machine: Machine name
511
513
  pid: Process ID
512
-
514
+
513
515
  Returns:
514
516
  Tool execution result
515
517
  """
516
- if hasattr(tool, "_aexecute") and inspect.iscoroutinefunction(
517
- getattr(type(tool), "_aexecute", None)
518
- ):
518
+ if hasattr(tool, "_aexecute") and inspect.iscoroutinefunction(getattr(type(tool), "_aexecute", None)):
519
519
  fn = tool._aexecute
520
- elif hasattr(tool, "execute") and inspect.iscoroutinefunction(
521
- getattr(tool, "execute", None)
522
- ):
520
+ elif hasattr(tool, "execute") and inspect.iscoroutinefunction(getattr(tool, "execute", None)):
523
521
  fn = tool.execute
524
522
  else:
525
523
  return ToolResult(
526
524
  tool=call.tool,
527
525
  result=None,
528
526
  error=(
529
- "Tool must implement *async* '_aexecute' or 'execute'. "
530
- "Synchronous entry-points are not supported."
527
+ "Tool must implement *async* '_aexecute' or 'execute'. Synchronous entry-points are not supported."
531
528
  ),
532
529
  start_time=start,
533
- end_time=datetime.now(timezone.utc),
530
+ end_time=datetime.now(UTC),
534
531
  machine=machine,
535
532
  pid=pid,
536
533
  )
@@ -538,15 +535,14 @@ class InProcessStrategy(ExecutionStrategy):
538
535
  try:
539
536
  # Always apply timeout
540
537
  logger.debug("Applying %ss timeout to %s", timeout, call.tool)
541
-
538
+
542
539
  try:
543
540
  result_val = await asyncio.wait_for(fn(**call.arguments), timeout=timeout)
544
-
545
- end_time = datetime.now(timezone.utc)
541
+
542
+ end_time = datetime.now(UTC)
546
543
  actual_duration = (end_time - start).total_seconds()
547
- logger.debug("%s completed in %.3fs (limit: %ss)",
548
- call.tool, actual_duration, timeout)
549
-
544
+ logger.debug("%s completed in %.3fs (limit: %ss)", call.tool, actual_duration, timeout)
545
+
550
546
  return ToolResult(
551
547
  tool=call.tool,
552
548
  result=result_val,
@@ -556,13 +552,12 @@ class InProcessStrategy(ExecutionStrategy):
556
552
  machine=machine,
557
553
  pid=pid,
558
554
  )
559
- except asyncio.TimeoutError:
555
+ except TimeoutError:
560
556
  # Handle timeout
561
- end_time = datetime.now(timezone.utc)
557
+ end_time = datetime.now(UTC)
562
558
  actual_duration = (end_time - start).total_seconds()
563
- logger.debug("%s timed out after %.3fs (limit: %ss)",
564
- call.tool, actual_duration, timeout)
565
-
559
+ logger.debug("%s timed out after %.3fs (limit: %ss)", call.tool, actual_duration, timeout)
560
+
566
561
  return ToolResult(
567
562
  tool=call.tool,
568
563
  result=None,
@@ -572,7 +567,7 @@ class InProcessStrategy(ExecutionStrategy):
572
567
  machine=machine,
573
568
  pid=pid,
574
569
  )
575
-
570
+
576
571
  except asyncio.CancelledError:
577
572
  # Handle cancellation explicitly
578
573
  logger.debug("%s was cancelled", call.tool)
@@ -581,16 +576,16 @@ class InProcessStrategy(ExecutionStrategy):
581
576
  result=None,
582
577
  error="Execution was cancelled",
583
578
  start_time=start,
584
- end_time=datetime.now(timezone.utc),
579
+ end_time=datetime.now(UTC),
585
580
  machine=machine,
586
581
  pid=pid,
587
582
  )
588
583
  except Exception as exc:
589
584
  logger.exception("Error executing %s: %s", call.tool, exc)
590
- end_time = datetime.now(timezone.utc)
585
+ end_time = datetime.now(UTC)
591
586
  actual_duration = (end_time - start).total_seconds()
592
587
  logger.debug("%s failed after %.3fs: %s", call.tool, actual_duration, exc)
593
-
588
+
594
589
  return ToolResult(
595
590
  tool=call.tool,
596
591
  result=None,
@@ -601,32 +596,34 @@ class InProcessStrategy(ExecutionStrategy):
601
596
  pid=pid,
602
597
  )
603
598
 
604
- async def _resolve_tool_info(self, tool_name: str, preferred_namespace: str = "default") -> Tuple[Optional[Any], Optional[str]]:
599
+ async def _resolve_tool_info(
600
+ self, tool_name: str, preferred_namespace: str = "default"
601
+ ) -> tuple[Any | None, str | None]:
605
602
  """
606
603
  Enhanced tool name resolution with comprehensive fallback logic.
607
-
604
+
608
605
  This method handles:
609
606
  1. Simple names: "get_current_time" -> search in specified namespace first, then all namespaces
610
607
  2. Namespaced names: "diagnostic_test.get_current_time" -> extract namespace and tool name
611
608
  3. Fallback searching across all namespaces when not found in default
612
-
609
+
613
610
  Args:
614
611
  tool_name: Name of the tool to resolve
615
612
  preferred_namespace: Preferred namespace to search first
616
-
613
+
617
614
  Returns:
618
615
  Tuple of (tool_object, resolved_namespace) or (None, None) if not found
619
616
  """
620
617
  logger.debug(f"Resolving tool: '{tool_name}' (preferred namespace: '{preferred_namespace}')")
621
-
618
+
622
619
  # Strategy 1: Handle namespaced tool names (namespace.tool_name format)
623
- if '.' in tool_name:
624
- parts = tool_name.split('.', 1) # Split on first dot only
620
+ if "." in tool_name:
621
+ parts = tool_name.split(".", 1) # Split on first dot only
625
622
  namespace = parts[0]
626
623
  actual_tool_name = parts[1]
627
-
624
+
628
625
  logger.debug(f"Namespaced lookup: namespace='{namespace}', tool='{actual_tool_name}'")
629
-
626
+
630
627
  tool = await self.registry.get_tool(actual_tool_name, namespace)
631
628
  if tool is not None:
632
629
  logger.debug(f"Found tool '{actual_tool_name}' in namespace '{namespace}'")
@@ -634,7 +631,7 @@ class InProcessStrategy(ExecutionStrategy):
634
631
  else:
635
632
  logger.debug(f"Tool '{actual_tool_name}' not found in namespace '{namespace}'")
636
633
  return None, None
637
-
634
+
638
635
  # Strategy 2: Simple tool name - try preferred namespace first
639
636
  if preferred_namespace:
640
637
  logger.debug(f"Simple tool lookup: trying preferred namespace '{preferred_namespace}' for '{tool_name}'")
@@ -642,7 +639,7 @@ class InProcessStrategy(ExecutionStrategy):
642
639
  if tool is not None:
643
640
  logger.debug(f"Found tool '{tool_name}' in preferred namespace '{preferred_namespace}'")
644
641
  return tool, preferred_namespace
645
-
642
+
646
643
  # Strategy 3: Try default namespace if different from preferred
647
644
  if preferred_namespace != "default":
648
645
  logger.debug(f"Simple tool lookup: trying default namespace for '{tool_name}'")
@@ -650,30 +647,30 @@ class InProcessStrategy(ExecutionStrategy):
650
647
  if tool is not None:
651
648
  logger.debug(f"Found tool '{tool_name}' in default namespace")
652
649
  return tool, "default"
653
-
650
+
654
651
  # Strategy 4: Search all namespaces as fallback
655
652
  logger.debug(f"Tool '{tool_name}' not in preferred/default namespace, searching all namespaces...")
656
-
653
+
657
654
  try:
658
655
  # Get all available namespaces
659
656
  namespaces = await self.registry.list_namespaces()
660
657
  logger.debug(f"Available namespaces: {namespaces}")
661
-
658
+
662
659
  # Search each namespace
663
660
  for namespace in namespaces:
664
661
  if namespace in [preferred_namespace, "default"]:
665
662
  continue # Already tried these
666
-
663
+
667
664
  logger.debug(f"Searching namespace '{namespace}' for tool '{tool_name}'")
668
665
  tool = await self.registry.get_tool(tool_name, namespace)
669
666
  if tool is not None:
670
667
  logger.debug(f"Found tool '{tool_name}' in namespace '{namespace}'")
671
668
  return tool, namespace
672
-
669
+
673
670
  # Strategy 5: Final fallback - list all tools and do fuzzy matching
674
671
  logger.debug(f"Tool '{tool_name}' not found in any namespace, trying fuzzy matching...")
675
672
  all_tools = await self.registry.list_tools()
676
-
673
+
677
674
  # Look for exact matches in tool name (ignoring namespace)
678
675
  for namespace, registered_name in all_tools:
679
676
  if registered_name == tool_name:
@@ -681,40 +678,40 @@ class InProcessStrategy(ExecutionStrategy):
681
678
  tool = await self.registry.get_tool(registered_name, namespace)
682
679
  if tool is not None:
683
680
  return tool, namespace
684
-
681
+
685
682
  # Log all available tools for debugging
686
683
  logger.debug(f"Available tools: {all_tools}")
687
-
684
+
688
685
  except Exception as e:
689
686
  logger.error(f"Error during namespace search: {e}")
690
-
687
+
691
688
  logger.warning(f"Tool '{tool_name}' not found in any namespace")
692
689
  return None, None
693
-
690
+
694
691
  @property
695
692
  def supports_streaming(self) -> bool:
696
693
  """Check if this strategy supports streaming execution."""
697
694
  return True
698
-
695
+
699
696
  async def shutdown(self) -> None:
700
697
  """
701
698
  Enhanced shutdown with clean task management.
702
-
699
+
703
700
  This version prevents anyio cancel scope errors by handling
704
701
  task cancellation more gracefully with individual error handling
705
702
  and reasonable timeouts.
706
703
  """
707
704
  if self._shutting_down:
708
705
  return
709
-
706
+
710
707
  self._shutting_down = True
711
708
  self._shutdown_event.set()
712
-
709
+
713
710
  # Manage active tasks cleanly
714
711
  active_tasks = list(self._active_tasks)
715
712
  if active_tasks:
716
713
  logger.debug(f"Completing {len(active_tasks)} in-process operations")
717
-
714
+
718
715
  # Handle each task individually with brief delays
719
716
  for task in active_tasks:
720
717
  try:
@@ -723,17 +720,12 @@ class InProcessStrategy(ExecutionStrategy):
723
720
  except Exception:
724
721
  pass
725
722
  # Small delay between cancellations to avoid overwhelming the event loop
726
- try:
723
+ with suppress(builtins.BaseException):
727
724
  await asyncio.sleep(0.001)
728
- except:
729
- pass
730
-
725
+
731
726
  # Allow reasonable time for completion with timeout
732
727
  try:
733
- await asyncio.wait_for(
734
- asyncio.gather(*active_tasks, return_exceptions=True),
735
- timeout=2.0
736
- )
728
+ await asyncio.wait_for(asyncio.gather(*active_tasks, return_exceptions=True), timeout=2.0)
737
729
  except Exception:
738
730
  # Suppress all errors during shutdown to prevent cancel scope issues
739
- logger.debug("In-process operations completed within expected parameters")
731
+ logger.debug("In-process operations completed within expected parameters")