@@ -631,3 +631,144 @@ async def process():
631631 args = fc_part .function_call .args
632632 assert args ["num" ] == 100
633633 assert args ["s" ] == "ADK"
634+
635+
636+ class PartialFunctionCallMockModel (BaseLlm ):
637+ """A mock model that yields partial function call events followed by final."""
638+
639+ model : str = "partial-fc-mock"
640+ tool_call_count : int = 0
641+
642+ @classmethod
643+ def supported_models (cls ) -> list [str ]:
644+ return ["partial-fc-mock" ]
645+
646+ async def generate_content_async (
647+ self , llm_request : LlmRequest , stream : bool = False
648+ ) -> AsyncGenerator [LlmResponse , None ]:
649+ """Yield partial FC events then final, simulating streaming behavior."""
650+
651+ # Check if this is a follow-up call (after function response)
652+ has_function_response = False
653+ for content in llm_request .contents :
654+ for part in content .parts or []:
655+ if part .function_response :
656+ has_function_response = True
657+ break
658+
659+ if has_function_response :
660+ # Final response after function execution
661+ yield LlmResponse (
662+ content = types .Content (
663+ role = "model" ,
664+ parts = [types .Part .from_text (text = "Function executed once." )],
665+ ),
666+ partial = False ,
667+ )
668+ return
669+
670+ # First call: yield partial FC events then final
671+ # Partial event 1
672+ yield LlmResponse (
673+ content = types .Content (
674+ role = "model" ,
675+ parts = [
676+ types .Part .from_function_call (
677+ name = "track_execution" , args = {"call_id" : "partial_1" }
678+ )
679+ ],
680+ ),
681+ partial = True ,
682+ )
683+
684+ # Partial event 2
685+ yield LlmResponse (
686+ content = types .Content (
687+ role = "model" ,
688+ parts = [
689+ types .Part .from_function_call (
690+ name = "track_execution" , args = {"call_id" : "partial_2" }
691+ )
692+ ],
693+ ),
694+ partial = True ,
695+ )
696+
697+ # Final aggregated event (only this should trigger execution)
698+ yield LlmResponse (
699+ content = types .Content (
700+ role = "model" ,
701+ parts = [
702+ types .Part .from_function_call (
703+ name = "track_execution" , args = {"call_id" : "final" }
704+ )
705+ ],
706+ ),
707+ partial = False ,
708+ finish_reason = types .FinishReason .STOP ,
709+ )
710+
711+
712+ def test_partial_function_calls_not_executed_in_none_streaming_mode ():
713+ """Test that partial function call events are skipped regardless of mode."""
714+ execution_log = []
715+
716+ def track_execution (call_id : str ) -> str :
717+ """A tool that logs each execution to verify call count."""
718+ execution_log .append (call_id )
719+ return f"Executed: { call_id } "
720+
721+ mock_model = PartialFunctionCallMockModel ()
722+
723+ agent = Agent (
724+ name = "partial_fc_test_agent" ,
725+ model = mock_model ,
726+ tools = [track_execution ],
727+ )
728+
729+ # Use StreamingMode.NONE to verify partial FCs are still skipped
730+ run_config = RunConfig (streaming_mode = StreamingMode .NONE )
731+
732+ runner = InMemoryRunner (agent = agent )
733+
734+ session = runner .session_service .create_session_sync (
735+ app_name = runner .app_name , user_id = "test_user"
736+ )
737+
738+ events = []
739+ for event in runner .run (
740+ user_id = "test_user" ,
741+ session_id = session .id ,
742+ new_message = types .Content (
743+ role = "user" ,
744+ parts = [types .Part .from_text (text = "Test partial FC handling" )],
745+ ),
746+ run_config = run_config ,
747+ ):
748+ events .append (event )
749+
750+ # Verify the tool was only executed once (from the final event)
751+ assert (
752+ len (execution_log ) == 1
753+ ), f"Expected 1 execution, got { len (execution_log )} : { execution_log } "
754+ assert (
755+ execution_log [0 ] == "final"
756+ ), f"Expected 'final' execution, got: { execution_log [0 ]} "
757+
758+ # Verify partial events were yielded but not executed
759+ partial_events = [e for e in events if e .partial ]
760+ assert (
761+ len (partial_events ) == 2
762+ ), f"Expected 2 partial events, got { len (partial_events )} "
763+
764+ # Verify there's a function response event (from the final FC execution)
765+ function_response_events = [
766+ e
767+ for e in events
768+ if e .content
769+ and e .content .parts
770+ and any (p .function_response for p in e .content .parts )
771+ ]
772+ assert (
773+ len (function_response_events ) == 1
774+ ), f"Expected 1 function response event, got { len (function_response_events )} "
0 commit comments