2727from ....types .content import Message , Messages
2828from ....types .tools import ToolResult , ToolUse , AgentTool
2929
30- from ..event_loop .bidirectional_event_loop import BidirectionalConnection
30+ from ..event_loop .bidirectional_event_loop import (
31+ BidirectionalConnection ,
32+ start_bidirectional_connection ,
33+ stop_bidirectional_connection ,
34+ )
3135from ..models .bidirectional_model import BidiModel
3236from ..models .novasonic import BidiNovaSonicModel
33- from ..types .bidirectional_streaming import AudioInputEvent , BidirectionalStreamEvent , ImageInputEvent
37+ from ..types .agent import BidiAgentInput
38+ from ..types .events import BidiAudioInputEvent , BidiImageInputEvent , BidiTextInputEvent , BidiInputEvent , BidiOutputEvent
3439from ..types import BidiIO
3540from ....experimental .tools import ToolProvider
3641
3742logger = logging .getLogger (__name__ )
3843
3944_DEFAULT_AGENT_NAME = "Strands Agents"
4045_DEFAULT_AGENT_ID = "default"
41- # Type alias for cleaner send() method signature
42- BidirectionalInput = str | AudioInputEvent | ImageInputEvent
4346
4447
4548class BidiAgent :
@@ -250,47 +253,81 @@ async def start(self) -> None:
250253 raise ValueError ("Conversation already active. Call end() first." )
251254
252255 logger .debug ("Conversation start - initializing connection" )
256+ self ._agent_loop = await start_bidirectional_connection (self )
253257
254- # Create model session and event loop directly
255- await self .model .start (
256- system_prompt = self .system_prompt , tools = self .tool_registry .get_all_tool_specs (), messages = self .messages
257- )
258-
259- self ._agent_loop = BidirectionalConnection (model = self .model , agent = self )
260- await self ._agent_loop .start ()
261-
262- logger .debug ("Conversation ready" )
263-
264- async def send (self , input_data : BidirectionalInput ) -> None :
265- """Send input to the model (text or audio).
266-
267- Unified method for sending both text and audio input to the model during
268- an active conversation connection. User input is automatically added to
269- conversation history for complete message tracking.
270-
258+ async def send (self , input_data : BidiAgentInput ) -> None :
259+ """Send input to the model (text, audio, image, or event dict).
260+
261+ Unified method for sending text, audio, and image input to the model during
262+ an active conversation session. Accepts TypedEvent instances or plain dicts
263+ (e.g., from WebSocket clients) which are automatically reconstructed.
264+
271265 Args:
272- input_data: String for text, AudioInputEvent for audio, or ImageInputEvent for images.
273-
266+ input_data: Can be:
267+ - str: Text message from user
268+ - BidiAudioInputEvent: Audio data with format/sample rate
269+ - BidiImageInputEvent: Image data with MIME type
270+ - dict: Event dictionary (will be reconstructed to TypedEvent)
271+
274272 Raises:
275- ValueError: If no active connection or invalid input type.
273+ ValueError: If no active session or invalid input type.
274+
275+ Example:
276+ await agent.send("Hello")
277+ await agent.send(BidiAudioInputEvent(audio="base64...", format="pcm", ...))
278+ await agent.send({"type": "bidirectional_text_input", "text": "Hello", "role": "user"})
276279 """
277280 self ._validate_active_connection ()
278281
282+ # Handle string input
279283 if isinstance (input_data , str ):
280284 # Add user text message to history
281285 user_message : Message = {"role" : "user" , "content" : [{"text" : input_data }]}
282286
283287 self .messages .append (user_message )
284288
285289 logger .debug ("Text sent: %d characters" , len (input_data ))
286- # Create TextInputEvent for send()
287- text_event = { " text" : input_data , " role" : " user"}
290+ # Create BidiTextInputEvent for send()
291+ text_event = BidiTextInputEvent ( text = input_data , role = " user")
288292 await self ._agent_loop .model .send (text_event )
289- else :
290- # For audio, image, or any other input - let model handle it
293+ return
294+
295+ # Handle BidiInputEvent instances
296+ # Check this before dict since TypedEvent inherits from dict
297+ if isinstance (input_data , BidiInputEvent ):
291298 await self ._agent_loop .model .send (input_data )
299+ return
300+
301+ # Handle plain dict - reconstruct TypedEvent for WebSocket integration
302+ if isinstance (input_data , dict ) and "type" in input_data :
303+ event_type = input_data ["type" ]
304+ if event_type == "bidi_text_input" :
305+ input_event = BidiTextInputEvent (text = input_data ["text" ], role = input_data ["role" ])
306+ elif event_type == "bidi_audio_input" :
307+ input_event = BidiAudioInputEvent (
308+ audio = input_data ["audio" ],
309+ format = input_data ["format" ],
310+ sample_rate = input_data ["sample_rate" ],
311+ channels = input_data ["channels" ]
312+ )
313+ elif event_type == "bidi_image_input" :
314+ input_event = BidiImageInputEvent (
315+ image = input_data ["image" ],
316+ mime_type = input_data ["mime_type" ]
317+ )
318+ else :
319+ raise ValueError (f"Unknown event type: { event_type } " )
320+
321+ # Send the reconstructed TypedEvent
322+ await self ._agent_loop .model .send (input_event )
323+ return
324+
325+ # If we get here, input type is invalid
326+ raise ValueError (
327+ f"Input must be a string, BidiInputEvent (BidiTextInputEvent/BidiAudioInputEvent/BidiImageInputEvent), or event dict with 'type' field, got: { type (input_data )} "
328+ )
292329
293- async def receive (self ) -> AsyncIterable [BidirectionalStreamEvent ]:
330+ async def receive (self ) -> AsyncIterable [BidiOutputEvent ]:
294331 """Receive events from the model including audio, text, and tool calls.
295332
296333 Yields model output events processed by background tasks including audio output,
@@ -301,9 +338,11 @@ async def receive(self) -> AsyncIterable[BidirectionalStreamEvent]:
301338 """
302339 while self .active :
303340 try :
304- event = await self ._output_queue .get ()
341+ # Use a timeout to periodically check if we should stop
342+ event = await asyncio .wait_for (self ._output_queue .get (), timeout = 0.5 )
305343 yield event
306344 except asyncio .TimeoutError :
345+ # Timeout allows us to check self.active periodically
307346 continue
308347
309348 async def stop (self ) -> None :
@@ -313,7 +352,7 @@ async def stop(self) -> None:
313352 closes the connection to the model provider.
314353 """
315354 if self ._agent_loop :
316- await self ._agent_loop . stop ( )
355+ await stop_bidirectional_connection ( self ._agent_loop )
317356 self ._agent_loop = None
318357
319358 async def __aenter__ (self ) -> "BidiAgent" :
0 commit comments