1- """
2- In-process execution strategy with sync/async support.
3-
4- This version prefers the public `execute()` wrapper (with validation and
5- defaults) over the private `_execute` implementation, fixing missing-argument
6- errors for `ValidatedTool` subclasses.
7- """
8-
1+ # chuk_tool_processor/execution/strategies/inprocess_strategy.py
92from __future__ import annotations
103
114import asyncio
125import inspect
136import os
147from datetime import datetime , timezone
15- from typing import Any , List , Optional
8+ from typing import Any , List
169
1710from chuk_tool_processor .core .exceptions import ToolExecutionError
1811from chuk_tool_processor .models .execution_strategy import ExecutionStrategy
2316
2417logger = get_logger ("chuk_tool_processor.execution.inprocess_strategy" )
2518
19+ from contextlib import asynccontextmanager
20+
21+ # Async no-op context manager when no semaphore
22+ @asynccontextmanager
23+ async def _noop_cm ():
24+ yield
2625
2726class InProcessStrategy (ExecutionStrategy ):
28- """Run tools inside the current interpreter, concurrently ."""
27+ """Execute tools concurrently in the current event-loop ."""
2928
3029 def __init__ (
3130 self ,
@@ -37,9 +36,6 @@ def __init__(
3736 self .default_timeout = default_timeout
3837 self ._sem = asyncio .Semaphore (max_concurrency ) if max_concurrency else None
3938
40- # ------------------------------------------------------------------ #
41- # public API
42- # ------------------------------------------------------------------ #
4339 async def run (
4440 self ,
4541 calls : List [ToolCall ],
@@ -51,9 +47,6 @@ async def run(
5147 ]
5248 return await asyncio .gather (* tasks )
5349
54- # ------------------------------------------------------------------ #
55- # helpers
56- # ------------------------------------------------------------------ #
5750 async def _execute_single_call (
5851 self ,
5952 call : ToolCall ,
@@ -75,68 +68,37 @@ async def _execute_single_call(
7568 pid = pid ,
7669 )
7770
78- try :
79- run = self ._run_with_timeout
80- if self ._sem is None :
81- return await run (impl , call , timeout , start , machine , pid )
82- async with self ._sem :
83- return await run (impl , call , timeout , start , machine , pid )
84- except Exception as exc : # pragma: no cover – safety net
85- logger .exception ("Unexpected error while executing %s" , call .tool )
71+ tool = impl () if inspect .isclass (impl ) else impl
72+ guard = self ._sem if self ._sem is not None else _noop_cm ()
73+
74+ # Determine correct async entry-point, even on bound methods
75+ if hasattr (tool , "_aexecute" ) and inspect .iscoroutinefunction (type (tool )._aexecute ):
76+ fn = tool ._aexecute
77+ elif hasattr (tool , "execute" ) and inspect .iscoroutinefunction (tool .execute ):
78+ fn = tool .execute
79+ else :
8680 return ToolResult (
8781 tool = call .tool ,
8882 result = None ,
89- error = f"Unexpected error: { exc } " ,
83+ error = (
84+ "Tool must implement async '_aexecute' or 'execute'."
85+ ),
9086 start_time = start ,
9187 end_time = datetime .now (timezone .utc ),
9288 machine = machine ,
9389 pid = pid ,
9490 )
9591
96- # ------------------------------------------------------------------ #
97- # core execution with timeout
98- # ------------------------------------------------------------------ #
99- async def _run_with_timeout (
100- self ,
101- impl : Any ,
102- call : ToolCall ,
103- timeout : float | None ,
104- start : datetime ,
105- machine : str ,
106- pid : int ,
107- ) -> ToolResult :
108- tool = impl () if isinstance (impl , type ) else impl
109-
110- # ------------------------------------------------------------------
111- # Entry-point selection order:
112- # 1. `_aexecute` (async special case)
113- # 2. `execute` (public wrapper WITH validation & defaults)
114- # 3. `_execute` (fallback / legacy)
115- # ------------------------------------------------------------------
116- if hasattr (tool , "_aexecute" ) and inspect .iscoroutinefunction (tool ._aexecute ):
117- fn = tool ._aexecute
118- is_async = True
119- elif hasattr (tool , "execute" ):
120- fn = tool .execute
121- is_async = inspect .iscoroutinefunction (fn )
122- elif hasattr (tool , "_execute" ):
123- fn = tool ._execute
124- is_async = inspect .iscoroutinefunction (fn )
125- else :
126- raise ToolExecutionError (
127- f"Tool '{ call .tool } ' must implement _execute, execute or _aexecute"
128- )
129-
13092 async def _invoke ():
131- if is_async :
132- return await fn (** call .arguments )
133- loop = asyncio .get_running_loop ()
134- return await loop .run_in_executor (None , lambda : fn (** call .arguments ))
93+ return await fn (** call .arguments )
13594
13695 try :
137- result_val = (
138- await asyncio .wait_for (_invoke (), timeout ) if timeout else await _invoke ()
139- )
96+ async with guard :
97+ result_val = (
98+ await asyncio .wait_for (_invoke (), timeout )
99+ if timeout
100+ else await _invoke ()
101+ )
140102 return ToolResult (
141103 tool = call .tool ,
142104 result = result_val ,
@@ -157,6 +119,7 @@ async def _invoke():
157119 pid = pid ,
158120 )
159121 except Exception as exc :
122+ logger .exception ("Error while executing %s" , call .tool )
160123 return ToolResult (
161124 tool = call .tool ,
162125 result = None ,
0 commit comments