@@ -867,3 +867,118 @@ async def test_auto_healing_logs_warning(caplog):
867867 and "test_tool" in record .message
868868 for record in caplog .records
869869 )
870+
871+
872+ @pytest .mark .asyncio
873+ async def test_long_running_tool_not_detected_as_orphaned ():
874+ """Test that long-running tool calls are NOT treated as orphaned.
875+
876+ Long-running tools (e.g., human-in-the-loop) intentionally don't produce
877+ immediate function_response events. They should be excluded from orphaned
878+ call detection.
879+ """
880+ agent = Agent (model = "gemini-2.5-flash" , name = "test_agent" )
881+ llm_request = LlmRequest (model = "gemini-2.5-flash" )
882+ invocation_context = await testing_utils .create_invocation_context (
883+ agent = agent
884+ )
885+
886+ long_running_call = types .FunctionCall (
887+ id = "long_running_123" , name = "request_human_approval" , args = {}
888+ )
889+
890+ events = [
891+ Event (
892+ invocation_id = "inv1" ,
893+ author = "user" ,
894+ content = types .UserContent ("Please approve this action" ),
895+ ),
896+ Event (
897+ invocation_id = "inv2" ,
898+ author = "test_agent" ,
899+ content = types .ModelContent (
900+ [types .Part (function_call = long_running_call )]
901+ ),
902+ long_running_tool_ids = {"long_running_123" },
903+ ),
904+ ]
905+ invocation_context .session .events = events
906+
907+ async for _ in contents .request_processor .run_async (
908+ invocation_context , llm_request
909+ ):
910+ pass
911+
912+ # Verify NO synthetic error response was injected
913+ # Should only have 2 contents: user message + function call
914+ assert len (llm_request .contents ) == 2
915+
916+ # Verify no synthetic error in any content
917+ for content in llm_request .contents :
918+ for part in content .parts :
919+ if part .function_response :
920+ assert part .function_response .response != contents ._ORPHANED_CALL_ERROR_RESPONSE , (
921+ "Long-running tool should not be treated as orphaned"
922+ )
923+
924+
925+ @pytest .mark .asyncio
926+ async def test_mixed_long_running_and_orphaned_calls ():
927+ """Test with both long-running and genuine orphaned calls.
928+
929+ Only the genuine orphaned call should receive synthetic error response.
930+ """
931+ agent = Agent (model = "gemini-2.5-flash" , name = "test_agent" )
932+ llm_request = LlmRequest (model = "gemini-2.5-flash" )
933+ invocation_context = await testing_utils .create_invocation_context (
934+ agent = agent
935+ )
936+
937+ long_running_call = types .FunctionCall (
938+ id = "long_running_call" , name = "request_approval" , args = {}
939+ )
940+ orphaned_call = types .FunctionCall (
941+ id = "orphaned_call" , name = "quick_calc" , args = {}
942+ )
943+
944+ events = [
945+ Event (
946+ invocation_id = "inv1" ,
947+ author = "user" ,
948+ content = types .UserContent ("Do multiple things" ),
949+ ),
950+ Event (
951+ invocation_id = "inv2" ,
952+ author = "test_agent" ,
953+ content = types .ModelContent ([
954+ types .Part (function_call = long_running_call ),
955+ types .Part (function_call = orphaned_call ),
956+ ]),
957+ long_running_tool_ids = {"long_running_call" },
958+ ),
959+ ]
960+ invocation_context .session .events = events
961+
962+ async for _ in contents .request_processor .run_async (
963+ invocation_context , llm_request
964+ ):
965+ pass
966+
967+ # Should have 3 contents: user message + function calls + synthetic response
968+ assert len (llm_request .contents ) == 3
969+
970+ # Find the synthetic response
971+ response_content = llm_request .contents [2 ]
972+ response_ids = {
973+ part .function_response .id
974+ for part in response_content .parts
975+ if part .function_response
976+ }
977+
978+ # Only orphaned_call should have synthetic response
979+ assert "orphaned_call" in response_ids , (
980+ "Genuine orphaned call should be healed"
981+ )
982+ assert "long_running_call" not in response_ids , (
983+ "Long-running call should NOT be healed"
984+ )
0 commit comments