1818 replace_values ,
1919)
2020from haystack .components .agents .state .state_utils import merge_lists
21+ from haystack .components .agents .tool_calling import _run_tool , _run_tool_async
2122from haystack .components .builders import ChatPromptBuilder
2223from haystack .components .generators .chat .types import ChatGenerator
23- from haystack .components .tools import ToolInvoker
2424from haystack .core .serialization import component_to_dict , default_from_dict , default_to_dict
2525from haystack .dataclasses import ChatMessage , ChatRole , StreamingCallbackT , select_streaming_callback
2626from haystack .human_in_the_loop .strategies import (
3636 deserialize_tools_or_toolset_inplace ,
3737 flatten_tools_or_toolsets ,
3838 serialize_tools_or_toolset ,
39+ warm_up_tools ,
3940)
4041from haystack .utils .callable_serialization import deserialize_callable , serialize_callable
4142from haystack .utils .deserialization import deserialize_component_inplace
@@ -127,7 +128,7 @@ class _ExecutionContext:
127128
128129 :param state: The current state of the agent, including messages and any additional data.
129130 :param chat_generator_inputs: Runtime inputs to be passed to the chat generator.
130- :param tool_invoker_inputs : Runtime inputs to be passed to the tool invoker .
131+ :param tool_execution_inputs : Runtime inputs to be passed to tool execution .
131132 :param counter: A counter to track the number of steps taken in the agent's run.
132133 :param confirmation_strategy_context: Optional dictionary for passing request-scoped resources
133134 to confirmation strategies. In web/server environments, this enables passing per-request
@@ -138,7 +139,7 @@ class _ExecutionContext:
138139
139140 state : State
140141 chat_generator_inputs : dict
141- tool_invoker_inputs : dict
142+ tool_execution_inputs : dict
142143 counter : int = 0
143144 confirmation_strategy_context : dict [str , Any ] | None = None
144145
@@ -264,7 +265,8 @@ def __init__(
264265 max_agent_steps : int = 100 ,
265266 streaming_callback : StreamingCallbackT | None = None ,
266267 raise_on_tool_invocation_failure : bool = False ,
267- tool_invoker_kwargs : dict [str , Any ] | None = None ,
268+ tool_concurrency_limit : int = 4 ,
269+ tool_streaming_callback_passthrough : bool = False ,
268270 confirmation_strategies : dict [str | tuple [str , ...], ConfirmationStrategy ] | None = None ,
269271 ) -> None :
270272 """
@@ -295,7 +297,9 @@ def __init__(
295297 The same callback can be configured to emit tool results when a tool is called.
296298 :param raise_on_tool_invocation_failure: Should the agent raise an exception when a tool invocation fails?
297299 If set to False, the exception will be turned into a chat message and passed to the LLM.
298- :param tool_invoker_kwargs: Additional keyword arguments to pass to the ToolInvoker.
300+ :param tool_concurrency_limit: Maximum number of tool calls to execute at the same time.
301+ Defaults to 4. Set to 1 to disable parallel tool execution.
302+ :param tool_streaming_callback_passthrough: If True, pass the streaming callback to tools that accept it.
299303 :param confirmation_strategies: A dictionary mapping tool names to ConfirmationStrategy instances.
300304 :raises TypeError: If the chat_generator does not support tools parameter in its run method.
301305 :raises ValueError: If the exit_conditions are not valid.
@@ -322,6 +326,8 @@ def __init__(
322326 if state_schema is not None :
323327 _validate_schema (state_schema )
324328 _validate_prompt_message_blocks (user_prompt , system_prompt )
329+ if tool_concurrency_limit < 1 :
330+ raise ValueError ("tool_concurrency_limit must be greater than or equal to 1." )
325331
326332 # --- Attributes ---
327333 self .chat_generator = chat_generator
@@ -333,7 +339,8 @@ def __init__(
333339 self .max_agent_steps = max_agent_steps
334340 self .raise_on_tool_invocation_failure = raise_on_tool_invocation_failure
335341 self .streaming_callback = streaming_callback
336- self .tool_invoker_kwargs = tool_invoker_kwargs
342+ self .tool_concurrency_limit = tool_concurrency_limit
343+ self .tool_streaming_callback_passthrough = tool_streaming_callback_passthrough
337344 self ._confirmation_strategies = confirmation_strategies or {}
338345 self ._is_warmed_up = False
339346
@@ -368,15 +375,8 @@ def __init__(
368375 )
369376 self ._register_prompt_variables ()
370377
371- # --- Tool invoker ---
372- self ._tool_invoker = None
373- if self .tools :
374- self ._tool_invoker = ToolInvoker (
375- tools = self .tools ,
376- raise_on_failure = self .raise_on_tool_invocation_failure ,
377- ** (self .tool_invoker_kwargs or {}),
378- )
379- elif type (self ).__name__ == "Agent" :
378+ # --- No-tools warning ---
379+ if not self .tools and type (self ).__name__ == "Agent" :
380380 logger .warning (
381381 "No tools provided to the Agent. The Agent will behave like a ChatGenerator and only return text "
382382 "responses. To enable tool usage, pass tools directly to the Agent, not to the chat_generator."
@@ -440,8 +440,8 @@ def warm_up(self) -> None:
440440 if not self ._is_warmed_up :
441441 if hasattr (self .chat_generator , "warm_up" ):
442442 self .chat_generator .warm_up ()
443- if hasattr ( self ._tool_invoker , "warm_up" ) and self . _tool_invoker is not None :
444- self ._tool_invoker . warm_up ( )
443+ if self .tools :
444+ warm_up_tools ( self .tools )
445445 self ._is_warmed_up = True
446446
447447 def to_dict (self ) -> dict [str , Any ]:
@@ -463,7 +463,8 @@ def to_dict(self) -> dict[str, Any]:
463463 max_agent_steps = self .max_agent_steps ,
464464 streaming_callback = serialize_callable (self .streaming_callback ) if self .streaming_callback else None ,
465465 raise_on_tool_invocation_failure = self .raise_on_tool_invocation_failure ,
466- tool_invoker_kwargs = self .tool_invoker_kwargs ,
466+ tool_concurrency_limit = self .tool_concurrency_limit ,
467+ tool_streaming_callback_passthrough = self .tool_streaming_callback_passthrough ,
467468 confirmation_strategies = {
468469 (list (key ) if isinstance (key , tuple ) else key ): component_to_dict (
469470 obj = strategy , name = "confirmation_strategy"
@@ -577,26 +578,26 @@ def _initialize_fresh_execution(
577578 )
578579
579580 selected_tools = self ._select_tools (tools )
580- tool_invoker_inputs : dict [str , Any ] = {"tools" : selected_tools }
581581 generator_inputs : dict [str , Any ] = {}
582582 if self ._chat_generator_supports_tools :
583583 generator_inputs ["tools" ] = selected_tools
584584 if streaming_callback is not None :
585- tool_invoker_inputs ["streaming_callback" ] = streaming_callback
586585 generator_inputs ["streaming_callback" ] = streaming_callback
587586 if generation_kwargs is not None :
588587 generator_inputs ["generation_kwargs" ] = generation_kwargs
589588
590- # We add enable_streaming_callback_passthrough to the tool invoker inputs
591- if self ._tool_invoker :
592- tool_invoker_inputs ["enable_streaming_callback_passthrough" ] = (
593- self ._tool_invoker .enable_streaming_callback_passthrough
594- )
589+ tool_execution_inputs : dict [str , Any ] = {
590+ "tools" : selected_tools ,
591+ "raise_on_failure" : self .raise_on_tool_invocation_failure ,
592+ "streaming_callback" : streaming_callback ,
593+ "max_workers" : self .tool_concurrency_limit ,
594+ "enable_streaming_callback_passthrough" : self .tool_streaming_callback_passthrough ,
595+ }
595596
596597 return _ExecutionContext (
597598 state = state ,
598599 chat_generator_inputs = generator_inputs ,
599- tool_invoker_inputs = tool_invoker_inputs ,
600+ tool_execution_inputs = tool_execution_inputs ,
600601 confirmation_strategy_context = confirmation_strategy_context ,
601602 )
602603
@@ -787,28 +788,32 @@ def _run_step(self, exe_context: _ExecutionContext, agent_span: tracing.Span) ->
787788 llm_messages = result ["replies" ]
788789 exe_context .state .set ("messages" , llm_messages )
789790
790- if not any (msg .tool_call for msg in llm_messages ) or self ._tool_invoker is None :
791+ if not any (msg .tool_call for msg in llm_messages ) or not self .tools :
791792 exe_context .counter += 1
792793 return False
793794
794795 modified_tool_call_messages , new_chat_history = _process_confirmation_strategies (
795796 confirmation_strategies = self ._confirmation_strategies ,
796797 messages_with_tool_calls = llm_messages ,
797- execution_context = exe_context ,
798+ state = exe_context .state ,
799+ confirmation_strategy_context = exe_context .confirmation_strategy_context ,
800+ tools = exe_context .tool_execution_inputs ["tools" ],
801+ streaming_callback = exe_context .tool_execution_inputs ["streaming_callback" ],
802+ enable_streaming_passthrough = exe_context .tool_execution_inputs ["enable_streaming_callback_passthrough" ],
798803 )
799804 exe_context .state .set (key = "messages" , value = new_chat_history , handler_override = replace_values )
800805
801- tool_invoker_inputs = {
806+ tool_execution_inputs = {
802807 "messages" : modified_tool_call_messages ,
803808 "state" : exe_context .state ,
804- ** exe_context .tool_invoker_inputs ,
809+ ** exe_context .tool_execution_inputs ,
805810 }
806811 with tracing .tracer .trace ("haystack.agent.step.tool" , parent_span = step_span ) as tool_span :
807- tool_span .set_content_tag ("haystack.agent.step.tool.input" , tool_invoker_inputs )
808- tool_invoker_result = self . _tool_invoker . run (** tool_invoker_inputs )
809- tool_span .set_content_tag ("haystack.agent.step.tool.output" , tool_invoker_result )
810- tool_messages = tool_invoker_result [ " tool_messages"]
811- exe_context . state = tool_invoker_result [ "state" ]
812+ tool_span .set_content_tag ("haystack.agent.step.tool.input" , tool_execution_inputs )
813+ tool_messages , exe_context . state = _run_tool (** tool_execution_inputs )
814+ tool_span .set_content_tag (
815+ "haystack.agent.step.tool.output" , { " tool_messages": tool_messages , "state" : exe_context . state }
816+ )
812817 exe_context .state .set ("messages" , tool_messages )
813818
814819 if self .exit_conditions != ["text" ] and self ._check_exit_conditions (llm_messages , tool_messages ):
@@ -844,28 +849,32 @@ async def _run_step_async(self, exe_context: _ExecutionContext, agent_span: trac
844849 llm_messages = result ["replies" ]
845850 exe_context .state .set ("messages" , llm_messages )
846851
847- if not any (msg .tool_call for msg in llm_messages ) or self ._tool_invoker is None :
852+ if not any (msg .tool_call for msg in llm_messages ) or not self .tools :
848853 exe_context .counter += 1
849854 return False
850855
851856 modified_tool_call_messages , new_chat_history = await _process_confirmation_strategies_async (
852857 confirmation_strategies = self ._confirmation_strategies ,
853858 messages_with_tool_calls = llm_messages ,
854- execution_context = exe_context ,
859+ tools = exe_context .tool_execution_inputs ["tools" ],
860+ state = exe_context .state ,
861+ streaming_callback = exe_context .tool_execution_inputs ["streaming_callback" ],
862+ enable_streaming_passthrough = exe_context .tool_execution_inputs ["enable_streaming_callback_passthrough" ],
863+ confirmation_strategy_context = exe_context .confirmation_strategy_context ,
855864 )
856865 exe_context .state .set (key = "messages" , value = new_chat_history , handler_override = replace_values )
857866
858- tool_invoker_inputs = {
867+ tool_execution_inputs = {
859868 "messages" : modified_tool_call_messages ,
860869 "state" : exe_context .state ,
861- ** exe_context .tool_invoker_inputs ,
870+ ** exe_context .tool_execution_inputs ,
862871 }
863872 with tracing .tracer .trace ("haystack.agent.step.tool" , parent_span = step_span ) as tool_span :
864- tool_span .set_content_tag ("haystack.agent.step.tool.input" , tool_invoker_inputs )
865- tool_invoker_result = await self . _tool_invoker . run_async (** tool_invoker_inputs )
866- tool_span .set_content_tag ("haystack.agent.step.tool.output" , tool_invoker_result )
867- tool_messages = tool_invoker_result [ " tool_messages"]
868- exe_context . state = tool_invoker_result [ "state" ]
873+ tool_span .set_content_tag ("haystack.agent.step.tool.input" , tool_execution_inputs )
874+ tool_messages , exe_context . state = await _run_tool_async (** tool_execution_inputs )
875+ tool_span .set_content_tag (
876+ "haystack.agent.step.tool.output" , { " tool_messages": tool_messages , "state" : exe_context . state }
877+ )
869878 exe_context .state .set ("messages" , tool_messages )
870879
871880 if self .exit_conditions != ["text" ] and self ._check_exit_conditions (llm_messages , tool_messages ):
@@ -880,7 +889,7 @@ def _check_exit_conditions(self, llm_messages: list[ChatMessage], tool_messages:
880889 Check if any of the LLM messages' tool calls match an exit condition and if there are no errors.
881890
882891 :param llm_messages: List of messages from the LLM
883- :param tool_messages: List of messages from the tool invoker
892+ :param tool_messages: List of messages from tool execution.
884893 :return: True if an exit condition is met and there are no errors, False otherwise
885894 """
886895 matched_exit_conditions = set ()
0 commit comments