@@ -2151,6 +2151,82 @@ async def fanout(ctx: Context) -> dict[str, Any]:
21512151 assert not any ('SECRET_FOR_A' in t for t in texts_b )
21522152
21532153
2154+ @pytest .mark .parametrize ('include_contents' , ['none' , 'default' ])
2155+ async def test_parallel_single_turn_nodes_with_sub_branch_see_own_node_input (
2156+ request : pytest .FixtureRequest ,
2157+ include_contents : str ,
2158+ ):
2159+ """use_sub_branch=True keeps each parallel single_turn node's own node_input.
2160+
2161+ Main already isolates unbranched parallel single_turn nodes by node path
2162+ (097ae1ea3). This covers the branched workaround: with use_sub_branch=True,
2163+ worker_b must still see INPUT_B under include_contents 'none' and 'default'.
2164+ On 2.11.0 without the sub-branch, worker_b lost INPUT_B and only saw
2165+ worker_a's reply when A finished first.
2166+ """
2167+ a_finished = _asyncio .Event ()
2168+
2169+ async def wait_for_a (callback_context : Context ) -> None :
2170+ del callback_context
2171+ await a_finished .wait ()
2172+
2173+ async def mark_a_finished (callback_context : Context ) -> None :
2174+ del callback_context
2175+ a_finished .set ()
2176+
2177+ model_a = _testing_utils .MockModel .create (responses = ['out-a' ])
2178+ model_b = _testing_utils .MockModel .create (responses = ['out-b' ])
2179+ worker_a = LlmAgent (
2180+ name = 'worker_a' ,
2181+ model = model_a ,
2182+ instruction = 'Worker A.' ,
2183+ mode = AgentMode .SINGLE_TURN ,
2184+ include_contents = include_contents ,
2185+ after_agent_callback = mark_a_finished ,
2186+ )
2187+ worker_b = LlmAgent (
2188+ name = 'worker_b' ,
2189+ model = model_b ,
2190+ instruction = 'Worker B.' ,
2191+ mode = AgentMode .SINGLE_TURN ,
2192+ include_contents = include_contents ,
2193+ before_agent_callback = wait_for_a ,
2194+ )
2195+
2196+ @node (rerun_on_resume = True )
2197+ async def fanout (ctx : Context ) -> dict [str , Any ]:
2198+ res_a , res_b = await _asyncio .gather (
2199+ ctx .run_node (
2200+ worker_a , node_input = 'INPUT_A' , run_id = 'a' , use_sub_branch = True
2201+ ),
2202+ ctx .run_node (
2203+ worker_b , node_input = 'INPUT_B' , run_id = 'b' , use_sub_branch = True
2204+ ),
2205+ )
2206+ return {'a' : res_a , 'b' : res_b }
2207+
2208+ wf = Workflow (name = 'parallel_sub_branch_wf' , edges = [(START , fanout )])
2209+ runner = _new_workflow_runner (wf , request .function .__name__ )
2210+ await runner .run_async (_testing_utils .get_user_content ('start' ))
2211+
2212+ assert len (model_a .requests ) == 1
2213+ texts_a = [
2214+ p .text for p in _request_parts (model_a .requests [0 ]) if p .text
2215+ ]
2216+ assert any ('INPUT_A' in t for t in texts_a )
2217+ assert not any ('INPUT_B' in t for t in texts_a )
2218+ assert not any ('out-b' in t for t in texts_a )
2219+
2220+ assert len (model_b .requests ) == 1
2221+ texts_b = [
2222+ p .text for p in _request_parts (model_b .requests [0 ]) if p .text
2223+ ]
2224+ # Primary regression: worker_b must keep its own node_input.
2225+ assert any ('INPUT_B' in t for t in texts_b )
2226+ assert not any ('INPUT_A' in t for t in texts_b )
2227+ assert not any ('out-a' in t for t in texts_b )
2228+
2229+
21542230def _request_parts (request_obj : Any ) -> list [types .Part ]:
21552231 parts : list [types .Part ] = []
21562232 for content in request_obj .contents :
0 commit comments