2020from pydantic import BaseModel
2121
2222from .. import _identifier
23- from ..event_loop .event_loop import event_loop_cycle , run_tool
23+ from ..event_loop .event_loop import event_loop_cycle
2424from ..handlers .callback_handler import PrintingCallbackHandler , null_callback_handler
2525from ..hooks import (
2626 AfterInvocationEvent ,
3535from ..session .session_manager import SessionManager
3636from ..telemetry .metrics import EventLoopMetrics
3737from ..telemetry .tracer import get_tracer , serialize
38+ from ..tools .executors import ConcurrentToolExecutor
39+ from ..tools .executors ._executor import ToolExecutor
3840from ..tools .registry import ToolRegistry
3941from ..tools .watcher import ToolWatcher
4042from ..types .content import ContentBlock , Message , Messages
@@ -136,13 +138,14 @@ def caller(
136138 "name" : normalized_name ,
137139 "input" : kwargs .copy (),
138140 }
141+ tool_results : list [ToolResult ] = []
142+ invocation_state = kwargs
139143
140144 async def acall () -> ToolResult :
141- # Pass kwargs as invocation_state
142- async for event in run_tool (self ._agent , tool_use , kwargs ):
145+ async for event in ToolExecutor ._stream (self ._agent , tool_use , tool_results , invocation_state ):
143146 _ = event
144147
145- return cast ( ToolResult , event )
148+ return tool_results [ 0 ]
146149
147150 def tcall () -> ToolResult :
148151 return asyncio .run (acall ())
@@ -208,6 +211,7 @@ def __init__(
208211 state : Optional [Union [AgentState , dict ]] = None ,
209212 hooks : Optional [list [HookProvider ]] = None ,
210213 session_manager : Optional [SessionManager ] = None ,
214+ tool_executor : Optional [ToolExecutor ] = None ,
211215 ):
212216 """Initialize the Agent with the specified configuration.
213217
@@ -250,6 +254,7 @@ def __init__(
250254 Defaults to None.
251255 session_manager: Manager for handling agent sessions including conversation history and state.
252256 If provided, enables session-based persistence and state management.
257+ tool_executor: Definition of tool execution stragety (e.g., sequential, concurrent, etc.).
253258
254259 Raises:
255260 ValueError: If agent id contains path separators.
@@ -324,6 +329,8 @@ def __init__(
324329 if self ._session_manager :
325330 self .hooks .add_hook (self ._session_manager )
326331
332+ self .tool_executor = tool_executor or ConcurrentToolExecutor ()
333+
327334 if hooks :
328335 for hook in hooks :
329336 self .hooks .add_hook (hook )
@@ -354,14 +361,21 @@ def tool_names(self) -> list[str]:
354361 all_tools = self .tool_registry .get_all_tools_config ()
355362 return list (all_tools .keys ())
356363
357- def __call__ (self , prompt : Union [ str , list [ContentBlock ]] , ** kwargs : Any ) -> AgentResult :
364+ def __call__ (self , prompt : str | list [ContentBlock ] | Messages | None = None , ** kwargs : Any ) -> AgentResult :
358365 """Process a natural language prompt through the agent's event loop.
359366
360- This method implements the conversational interface (e.g., `agent("hello!")`). It adds the user's prompt to
361- the conversation history, processes it through the model, executes any tool calls, and returns the final result.
367+ This method implements the conversational interface with multiple input patterns:
368+ - String input: `agent("hello!")`
369+ - ContentBlock list: `agent([{"text": "hello"}, {"image": {...}}])`
370+ - Message list: `agent([{"role": "user", "content": [{"text": "hello"}]}])`
371+ - No input: `agent()` - uses existing conversation history
362372
363373 Args:
364- prompt: User input as text or list of ContentBlock objects for multi-modal content.
374+ prompt: User input in various formats:
375+ - str: Simple text input
376+ - list[ContentBlock]: Multi-modal content blocks
377+ - list[Message]: Complete messages with roles
378+ - None: Use existing conversation history
365379 **kwargs: Additional parameters to pass through the event loop.
366380
367381 Returns:
@@ -380,14 +394,23 @@ def execute() -> AgentResult:
380394 future = executor .submit (execute )
381395 return future .result ()
382396
383- async def invoke_async (self , prompt : Union [str , list [ContentBlock ]], ** kwargs : Any ) -> AgentResult :
397+ async def invoke_async (
398+ self , prompt : str | list [ContentBlock ] | Messages | None = None , ** kwargs : Any
399+ ) -> AgentResult :
384400 """Process a natural language prompt through the agent's event loop.
385401
386- This method implements the conversational interface (e.g., `agent("hello!")`). It adds the user's prompt to
387- the conversation history, processes it through the model, executes any tool calls, and returns the final result.
402+ This method implements the conversational interface with multiple input patterns:
403+ - String input: Simple text input
404+ - ContentBlock list: Multi-modal content blocks
405+ - Message list: Complete messages with roles
406+ - No input: Use existing conversation history
388407
389408 Args:
390- prompt: User input as text or list of ContentBlock objects for multi-modal content.
409+ prompt: User input in various formats:
410+ - str: Simple text input
411+ - list[ContentBlock]: Multi-modal content blocks
412+ - list[Message]: Complete messages with roles
413+ - None: Use existing conversation history
391414 **kwargs: Additional parameters to pass through the event loop.
392415
393416 Returns:
@@ -404,7 +427,7 @@ async def invoke_async(self, prompt: Union[str, list[ContentBlock]], **kwargs: A
404427
405428 return cast (AgentResult , event ["result" ])
406429
407- def structured_output (self , output_model : Type [T ], prompt : Optional [ Union [ str , list [ContentBlock ]]] = None ) -> T :
430+ def structured_output (self , output_model : Type [T ], prompt : str | list [ContentBlock ] | Messages | None = None ) -> T :
408431 """This method allows you to get structured output from the agent.
409432
410433 If you pass in a prompt, it will be used temporarily without adding it to the conversation history.
@@ -416,7 +439,11 @@ def structured_output(self, output_model: Type[T], prompt: Optional[Union[str, l
416439 Args:
417440 output_model: The output model (a JSON schema written as a Pydantic BaseModel)
418441 that the agent will use when responding.
419- prompt: The prompt to use for the agent (will not be added to conversation history).
442+ prompt: The prompt to use for the agent in various formats:
443+ - str: Simple text input
444+ - list[ContentBlock]: Multi-modal content blocks
445+ - list[Message]: Complete messages with roles
446+ - None: Use existing conversation history
420447
421448 Raises:
422449 ValueError: If no conversation history or prompt is provided.
@@ -430,7 +457,7 @@ def execute() -> T:
430457 return future .result ()
431458
432459 async def structured_output_async (
433- self , output_model : Type [T ], prompt : Optional [ Union [ str , list [ContentBlock ]]] = None
460+ self , output_model : Type [T ], prompt : str | list [ContentBlock ] | Messages | None = None
434461 ) -> T :
435462 """This method allows you to get structured output from the agent.
436463
@@ -455,12 +482,8 @@ async def structured_output_async(
455482 try :
456483 if not self .messages and not prompt :
457484 raise ValueError ("No conversation history or prompt provided" )
458- # Create temporary messages array if prompt is provided
459- if prompt :
460- content : list [ContentBlock ] = [{"text" : prompt }] if isinstance (prompt , str ) else prompt
461- temp_messages = self .messages + [{"role" : "user" , "content" : content }]
462- else :
463- temp_messages = self .messages
485+
486+ temp_messages : Messages = self .messages + self ._convert_prompt_to_messages (prompt )
464487
465488 structured_output_span .set_attributes (
466489 {
@@ -470,16 +493,16 @@ async def structured_output_async(
470493 "gen_ai.operation.name" : "execute_structured_output" ,
471494 }
472495 )
473- for message in temp_messages :
474- structured_output_span .add_event (
475- f"gen_ai.{ message ['role' ]} .message" ,
476- attributes = {"role" : message ["role" ], "content" : serialize (message ["content" ])},
477- )
478496 if self .system_prompt :
479497 structured_output_span .add_event (
480498 "gen_ai.system.message" ,
481499 attributes = {"role" : "system" , "content" : serialize ([{"text" : self .system_prompt }])},
482500 )
501+ for message in temp_messages :
502+ structured_output_span .add_event (
503+ f"gen_ai.{ message ['role' ]} .message" ,
504+ attributes = {"role" : message ["role" ], "content" : serialize (message ["content" ])},
505+ )
483506 events = self .model .structured_output (output_model , temp_messages , system_prompt = self .system_prompt )
484507 async for event in events :
485508 if "callback" in event :
@@ -492,16 +515,25 @@ async def structured_output_async(
492515 finally :
493516 self .hooks .invoke_callbacks (AfterInvocationEvent (agent = self ))
494517
495- async def stream_async (self , prompt : Union [str , list [ContentBlock ]], ** kwargs : Any ) -> AsyncIterator [Any ]:
518+ async def stream_async (
519+ self ,
520+ prompt : str | list [ContentBlock ] | Messages | None = None ,
521+ ** kwargs : Any ,
522+ ) -> AsyncIterator [Any ]:
496523 """Process a natural language prompt and yield events as an async iterator.
497524
498- This method provides an asynchronous interface for streaming agent events, allowing
499- consumers to process stream events programmatically through an async iterator pattern
500- rather than callback functions. This is particularly useful for web servers and other
501- async environments.
525+ This method provides an asynchronous interface for streaming agent events with multiple input patterns:
526+ - String input: Simple text input
527+ - ContentBlock list: Multi-modal content blocks
528+ - Message list: Complete messages with roles
529+ - No input: Use existing conversation history
502530
503531 Args:
504- prompt: User input as text or list of ContentBlock objects for multi-modal content.
532+ prompt: User input in various formats:
533+ - str: Simple text input
534+ - list[ContentBlock]: Multi-modal content blocks
535+ - list[Message]: Complete messages with roles
536+ - None: Use existing conversation history
505537 **kwargs: Additional parameters to pass to the event loop.
506538
507539 Yields:
@@ -525,13 +557,15 @@ async def stream_async(self, prompt: Union[str, list[ContentBlock]], **kwargs: A
525557 """
526558 callback_handler = kwargs .get ("callback_handler" , self .callback_handler )
527559
528- content : list [ContentBlock ] = [{"text" : prompt }] if isinstance (prompt , str ) else prompt
529- message : Message = {"role" : "user" , "content" : content }
560+ # Process input and get message to add (if any)
561+ messages = self ._convert_prompt_to_messages (prompt )
562+
563+ self .trace_span = self ._start_agent_trace_span (messages )
530564
531- self .trace_span = self ._start_agent_trace_span (message )
532565 with trace_api .use_span (self .trace_span ):
533566 try :
534- events = self ._run_loop (message , invocation_state = kwargs )
567+ events = self ._run_loop (messages , invocation_state = kwargs )
568+
535569 async for event in events :
536570 if "callback" in event :
537571 callback_handler (** event ["callback" ])
@@ -548,12 +582,12 @@ async def stream_async(self, prompt: Union[str, list[ContentBlock]], **kwargs: A
548582 raise
549583
550584 async def _run_loop (
551- self , message : Message , invocation_state : dict [str , Any ]
585+ self , messages : Messages , invocation_state : dict [str , Any ]
552586 ) -> AsyncGenerator [dict [str , Any ], None ]:
553587 """Execute the agent's event loop with the given message and parameters.
554588
555589 Args:
556- message : The user message to add to the conversation.
590+ messages : The input messages to add to the conversation.
557591 invocation_state: Additional parameters to pass to the event loop.
558592
559593 Yields:
@@ -564,7 +598,8 @@ async def _run_loop(
564598 try :
565599 yield {"callback" : {"init_event_loop" : True , ** invocation_state }}
566600
567- self ._append_message (message )
601+ for message in messages :
602+ self ._append_message (message )
568603
569604 # Execute the event loop cycle with retry logic for context limits
570605 events = self ._execute_event_loop_cycle (invocation_state )
@@ -622,6 +657,34 @@ async def _execute_event_loop_cycle(self, invocation_state: dict[str, Any]) -> A
622657 async for event in events :
623658 yield event
624659
660+ def _convert_prompt_to_messages (self , prompt : str | list [ContentBlock ] | Messages | None ) -> Messages :
661+ messages : Messages | None = None
662+ if prompt is not None :
663+ if isinstance (prompt , str ):
664+ # String input - convert to user message
665+ messages = [{"role" : "user" , "content" : [{"text" : prompt }]}]
666+ elif isinstance (prompt , list ):
667+ if len (prompt ) == 0 :
668+ # Empty list
669+ messages = []
670+ # Check if all item in input list are dictionaries
671+ elif all (isinstance (item , dict ) for item in prompt ):
672+ # Check if all items are messages
673+ if all (all (key in item for key in Message .__annotations__ .keys ()) for item in prompt ):
674+ # Messages input - add all messages to conversation
675+ messages = cast (Messages , prompt )
676+
677+ # Check if all items are content blocks
678+ elif all (any (key in ContentBlock .__annotations__ .keys () for key in item ) for item in prompt ):
679+ # Treat as List[ContentBlock] input - convert to user message
680+ # This allows invalid structures to be passed through to the model
681+ messages = [{"role" : "user" , "content" : cast (list [ContentBlock ], prompt )}]
682+ else :
683+ messages = []
684+ if messages is None :
685+ raise ValueError ("Input prompt must be of type: `str | list[Contentblock] | Messages | None`." )
686+ return messages
687+
625688 def _record_tool_execution (
626689 self ,
627690 tool : ToolUse ,
@@ -687,15 +750,15 @@ def _record_tool_execution(
687750 self ._append_message (tool_result_msg )
688751 self ._append_message (assistant_msg )
689752
690- def _start_agent_trace_span (self , message : Message ) -> trace_api .Span :
753+ def _start_agent_trace_span (self , messages : Messages ) -> trace_api .Span :
691754 """Starts a trace span for the agent.
692755
693756 Args:
694- message : The user message .
757+ messages : The input messages .
695758 """
696759 model_id = self .model .config .get ("model_id" ) if hasattr (self .model , "config" ) else None
697760 return self .tracer .start_agent_span (
698- message = message ,
761+ messages = messages ,
699762 agent_name = self .name ,
700763 model_id = model_id ,
701764 tools = self .tool_names ,
0 commit comments