@@ -87,6 +87,11 @@ def long_running_tool_func():
8787 return None
8888
8989
90+ def request_approval_tool () -> dict [str , str ]:
91+ """A test tool that asks a human to approve and answers only later."""
92+ return {'status' : 'awaiting_approval' }
93+
94+
9095@pytest .mark .parametrize ('resumable' , [False , True ])
9196@pytest .mark .asyncio
9297async def test_workflow_pause_and_resume (
@@ -229,6 +234,69 @@ async def test_workflow_pause_and_resume(
229234 assert len (end_events ) == 1
230235
231236
237+ @pytest .mark .asyncio
238+ async def test_agent_node_that_spoke_before_pausing_reruns_on_resume (
239+ request : pytest .FixtureRequest ,
240+ ):
241+ """An agent node that says something before it pauses still reruns.
242+
243+ The message is recorded as the node's output, so replaying it as a finished
244+ node skips the node on resume: the answer never reaches the model and
245+ approving or refusing reads the same.
246+ """
247+ mock_model = testing_utils .MockModel .create (
248+ responses = [
249+ types .Part .from_function_call (name = 'request_approval_tool' , args = {}),
250+ types .Part .from_text (text = 'Waiting for you to approve.' ),
251+ types .Part .from_text (text = 'Approved, so I sent it.' ),
252+ ]
253+ )
254+ sender = LlmAgent (
255+ name = 'sender' ,
256+ model = mock_model ,
257+ tools = [LongRunningFunctionTool (func = request_approval_tool )],
258+ )
259+ app = App (
260+ name = request .function .__name__ ,
261+ root_agent = Workflow (name = 'approval_workflow' , edges = [(START , sender )]),
262+ resumability_config = ResumabilityConfig (is_resumable = True ),
263+ )
264+ runner = testing_utils .InMemoryRunner (app = app )
265+
266+ events1 = await runner .run_async (testing_utils .get_user_content ('send it' ))
267+ fc_event = workflow_testing_utils .find_function_call_event (
268+ events1 , 'request_approval_tool'
269+ )
270+ interrupt_id = fc_event .content .parts [0 ].function_call .id
271+ requests_before_resume = len (mock_model .requests )
272+
273+ events2 = await runner .run_async (
274+ new_message = testing_utils .UserContent (
275+ types .Part (
276+ function_response = types .FunctionResponse (
277+ id = interrupt_id ,
278+ name = 'request_approval_tool' ,
279+ response = {'result' : 'approved' },
280+ )
281+ )
282+ ),
283+ invocation_id = events1 [0 ].invocation_id ,
284+ )
285+
286+ # A replayed node hands back its old message without ever reaching the
287+ # model, so a fresh request is what proves the node ran again.
288+ assert len (mock_model .requests ) == requests_before_resume + 1
289+ resumed_texts = [
290+ part .text
291+ for event in events2
292+ if event .content and event .content .parts
293+ for part in event .content .parts
294+ if part .text
295+ ]
296+ assert resumed_texts == ['Approved, so I sent it.' ]
297+ assert 'Waiting for you to approve.' not in [e .output for e in events2 ]
298+
299+
232300@pytest .mark .asyncio
233301async def test_workflow_interrupt_allows_parallel_execution (
234302 request : pytest .FixtureRequest ,
0 commit comments