@@ -126,6 +126,10 @@ def __init__(
126126 # Background task and event queue
127127 self ._response_task = None
128128 self ._event_queue = None
129+
130+ # Track API-provided identifiers
131+ self ._current_completion_id = None
132+ self ._current_role = None
129133
130134 logger .debug ("Nova Sonic bidirectional model initialized: %s" , model_id )
131135
@@ -510,6 +514,28 @@ async def close(self) -> None:
510514
511515 def _convert_nova_event (self , nova_event : dict [str , any ]) -> OutputEvent | None :
512516 """Convert Nova Sonic events to TypedEvent format."""
517+ # Handle completion start - track completionId
518+ if "completionStart" in nova_event :
519+ completion_data = nova_event ["completionStart" ]
520+ self ._current_completion_id = completion_data .get ("completionId" )
521+ logger .debug ("Nova completion started: %s" , self ._current_completion_id )
522+ return None
523+
524+ # Handle completion end
525+ if "completionEnd" in nova_event :
526+ completion_data = nova_event ["completionEnd" ]
527+ completion_id = completion_data .get ("completionId" , self ._current_completion_id )
528+ stop_reason = completion_data .get ("stopReason" , "END_TURN" )
529+
530+ event = ResponseCompleteEvent (
531+ response_id = completion_id or str (uuid .uuid4 ()), # Fallback to UUID if missing
532+ stop_reason = "interrupted" if stop_reason == "INTERRUPTED" else "complete"
533+ )
534+
535+ # Clear completion tracking
536+ self ._current_completion_id = None
537+ return event
538+
513539 # Handle audio output
514540 if "audioOutput" in nova_event :
515541 # Audio is already base64 string from Nova Sonic
@@ -557,7 +583,7 @@ def _convert_nova_event(self, nova_event: dict[str, any]) -> OutputEvent | None:
557583 # Handle interruption
558584 elif nova_event .get ("stopReason" ) == "INTERRUPTED" :
559585 logger .debug ("Nova interruption stop reason" )
560- return InterruptionEvent (reason = "user_speech" , response_id = None )
586+ return InterruptionEvent (reason = "user_speech" )
561587
562588 # Handle usage events - convert to multimodal usage format
563589 elif "usageEvent" in nova_event :
@@ -571,22 +597,26 @@ def _convert_nova_event(self, nova_event: dict[str, any]) -> OutputEvent | None:
571597 total_tokens = usage_data .get ("totalTokens" , total_input + total_output )
572598 )
573599
574- # Handle content start events (track role)
600+ # Handle content start events (track role and emit response start )
575601 elif "contentStart" in nova_event :
576- role = nova_event ["contentStart" ].get ("role" , "unknown" )
602+ content_data = nova_event ["contentStart" ]
603+ role = content_data .get ("role" , "unknown" )
577604 # Store role for subsequent text output events
578605 self ._current_role = role
579- # Emit response start event
580- return ResponseStartEvent (response_id = str (uuid .uuid4 ()))
581-
582- # Handle content stop events
583- elif "contentStop" in nova_event :
584- stop_reason = nova_event ["contentStop" ].get ("stopReason" , "complete" )
585- return ResponseCompleteEvent (
586- response_id = str (uuid .uuid4 ()),
587- stop_reason = "interrupted" if stop_reason == "INTERRUPTED" else "complete"
606+
607+ # Emit response start event using API-provided completionId
608+ # completionId should already be tracked from completionStart event
609+ return ResponseStartEvent (
610+ response_id = self ._current_completion_id or str (uuid .uuid4 ()) # Fallback to UUID if missing
588611 )
589612
613+ # Handle content end events
614+ elif "contentEnd" in nova_event :
615+ # contentEnd doesn't signal response completion in Nova Sonic
616+ # Multiple content blocks can exist in a single response
617+ # Only completionEnd signals the actual response completion
618+ return None
619+
590620 # Handle other events
591621 else :
592622 return None
0 commit comments