@@ -231,15 +231,34 @@ async def test_single_turn_input_event_inherits_branch_and_scope(
231231 ctx = Context (invocation_context = ic )
232232 ctx .isolation_scope = 'scope-1'
233233
234- prepare_llm_agent_input (agent , ctx , 'hello' )
234+ await prepare_llm_agent_input (agent , ctx , 'hello' )
235235
236236 event = ic .session .events [- 1 ]
237237 assert event .author == 'user'
238+ assert event .invocation_id == ic .invocation_id
238239 assert event .content and event .content .role == 'user'
239240 assert event .branch == 'parent.worker@1'
240241 assert event .isolation_scope == 'scope-1'
241242
242243
244+ @pytest .mark .asyncio
245+ async def test_single_turn_input_event_carries_run_custom_metadata (
246+ request : pytest .FixtureRequest ,
247+ ):
248+ """Run-level custom metadata tags the synthetic input like any other event."""
249+ from google .adk .agents .run_config import RunConfig
250+ from google .adk .workflow ._llm_agent_wrapper import prepare_llm_agent_input
251+
252+ agent = _make_agent (mode = 'single_turn' )
253+ ic = await create_parent_invocation_context (request .function .__name__ , agent )
254+ ic .run_config = RunConfig (custom_metadata = {'tenant' : 'acme' })
255+ ctx = Context (invocation_context = ic )
256+
257+ await prepare_llm_agent_input (agent , ctx , 'hello' )
258+
259+ assert ic .session .events [- 1 ].custom_metadata == {'tenant' : 'acme' }
260+
261+
243262@pytest .mark .asyncio
244263async def test_single_turn_input_skipped_when_resuming (
245264 request : pytest .FixtureRequest ,
@@ -265,14 +284,92 @@ async def test_single_turn_input_skipped_when_resuming(
265284 )
266285
267286 initial_len = len (ic .session .events )
268- prepare_llm_agent_input (agent , ctx , 'hello' )
287+ await prepare_llm_agent_input (agent , ctx , 'hello' )
269288
270289 # Verify no duplicate user input was appended on resume
271290 assert len (ic .session .events ) == initial_len
272291 # Verify the resumed node still sees the initial user input from turn 1
273292 assert ic .session .events [- 1 ].content .parts [0 ].text == 'turn 1 initial input'
274293
275294
295+ @pytest .mark .asyncio
296+ async def test_single_turn_input_skipped_when_it_is_the_user_message (
297+ request : pytest .FixtureRequest ,
298+ ):
299+ """The runner already recorded the invocation's own user message."""
300+ from google .adk .workflow ._llm_agent_wrapper import prepare_llm_agent_input
301+
302+ agent = _make_agent (mode = 'single_turn' )
303+ ic = await create_parent_invocation_context (request .function .__name__ , agent )
304+ ic .user_content = types .Content (
305+ role = 'user' , parts = [types .Part (text = 'who am i' )]
306+ )
307+ ctx = Context (invocation_context = ic )
308+
309+ await prepare_llm_agent_input (agent , ctx , ic .user_content )
310+
311+ assert not ic .session .events
312+
313+
314+ @pytest .mark .asyncio
315+ async def test_single_turn_input_event_reaches_the_stored_session (
316+ request : pytest .FixtureRequest ,
317+ ):
318+ """A single-turn node's input becomes part of the stored session.
319+
320+ Appending to the in-memory events list reaches only the Session object the
321+ node happens to hold, so the input is missing from the session anyone loads
322+ afterwards and the model turn it prompted has no prompt.
323+ """
324+ from . import testing_utils
325+
326+ async def brief (node_input : Any ) -> str :
327+ return 'write the brief'
328+
329+ wrapper = build_node (_make_agent (mode = 'single_turn' ))
330+ wf = Workflow (name = 'wf' , edges = [(START , brief ), (brief , wrapper )])
331+ runner = _new_workflow_runner (wf , request .function .__name__ )
332+
333+ agent_clone = next (n for n in wf .graph .nodes if n .name == wrapper .name )
334+ with _mock_leaf_run (agent_clone , content_text = 'Done.' ):
335+ await runner .run_async (testing_utils .get_user_content ('start' ))
336+
337+ stored = runner .session
338+ assert any (
339+ event .author == 'user'
340+ and event .content
341+ and event .content .parts
342+ and event .content .parts [0 ].text == 'write the brief'
343+ for event in stored .events
344+ )
345+
346+
347+ @pytest .mark .asyncio
348+ async def test_first_node_does_not_store_the_user_message_twice (
349+ request : pytest .FixtureRequest ,
350+ ):
351+ """A node wired to START is fed the turn the runner already stored."""
352+ from . import testing_utils
353+
354+ wrapper = build_node (_make_agent (mode = 'single_turn' ))
355+ wf = Workflow (name = 'wf' , edges = [(START , wrapper )])
356+ runner = _new_workflow_runner (wf , request .function .__name__ )
357+
358+ agent_clone = next (n for n in wf .graph .nodes if n .name == wrapper .name )
359+ with _mock_leaf_run (agent_clone , content_text = 'Done.' ):
360+ await runner .run_async (testing_utils .get_user_content ('who am i' ))
361+
362+ user_turns = [
363+ event
364+ for event in runner .session .events
365+ if event .author == 'user'
366+ and event .content
367+ and event .content .parts
368+ and event .content .parts [0 ].text == 'who am i'
369+ ]
370+ assert len (user_turns ) == 1
371+
372+
276373# --- build_node auto-wrapping ---
277374
278375
@@ -340,17 +437,16 @@ async def mock_run_async(*args, **kwargs):
340437 content = types .Content (parts = [types .Part (text = 'ok' )]),
341438 )
342439
440+ async def skip_input (agent , ctx , node_input ):
441+ return None
442+
343443 object .__setattr__ (wrapper , 'run_async' , mock_run_async )
344444 monkeypatch .setattr (
345445 agent_wrapper ,
346446 'prepare_llm_agent_context' ,
347447 lambda agent , ctx : ctx ,
348448 )
349- monkeypatch .setattr (
350- agent_wrapper ,
351- 'prepare_llm_agent_input' ,
352- lambda agent , ctx , node_input : None ,
353- )
449+ monkeypatch .setattr (agent_wrapper , 'prepare_llm_agent_input' , skip_input )
354450 ctx = MagicMock (spec = Context )
355451 ic = MagicMock ()
356452 ctx .get_invocation_context .return_value = ic
0 commit comments