@@ -1291,6 +1291,178 @@ def test_input_required_followup_across_per_rpc_event_loops():
12911291 assert result2 .status .state == TaskState .TASK_STATE_COMPLETED
12921292
12931293
1294+ # Scenario: Streaming resumption that enqueues the current Task first
1295+ @pytest .mark .timeout (2.0 )
1296+ @pytest .mark .asyncio
1297+ @pytest .mark .parametrize (
1298+ ('task_enqueues' , 'replacement_errors' ),
1299+ [(1 , 0 ), (2 , 1 )],
1300+ ids = ['once' , 'twice' ],
1301+ )
1302+ async def test_scenario_streaming_resumption_enqueues_current_task (
1303+ caplog : pytest .LogCaptureFixture ,
1304+ task_enqueues : int ,
1305+ replacement_errors : int ,
1306+ ) -> None :
1307+ class CurrentTaskAgent (AgentExecutor ):
1308+ async def execute (
1309+ self , context : RequestContext , event_queue : EventQueue
1310+ ) -> None :
1311+ task = context .current_task or new_task_from_user_message (
1312+ context .message
1313+ )
1314+ for _ in range (task_enqueues if context .current_task else 1 ):
1315+ await event_queue .enqueue_event (task )
1316+ await event_queue .enqueue_event (
1317+ TaskStatusUpdateEvent (
1318+ task_id = task .id ,
1319+ context_id = task .context_id ,
1320+ status = TaskStatus (
1321+ state = TaskState .TASK_STATE_COMPLETED
1322+ if context .current_task
1323+ else TaskState .TASK_STATE_INPUT_REQUIRED
1324+ ),
1325+ )
1326+ )
1327+
1328+ async def cancel (
1329+ self , context : RequestContext , event_queue : EventQueue
1330+ ) -> None :
1331+ pass
1332+
1333+ handler = create_handler (CurrentTaskAgent (), use_legacy = False )
1334+ client = await create_client (
1335+ handler , agent_card = agent_card (), streaming = True
1336+ )
1337+
1338+ msg1 = Message (
1339+ message_id = 'msg-start' , role = Role .ROLE_USER , parts = [Part (text = 'start' )]
1340+ )
1341+ it = client .send_message (
1342+ SendMessageRequest (
1343+ message = msg1 ,
1344+ configuration = SendMessageConfiguration (return_immediately = False ),
1345+ )
1346+ )
1347+ events1 = [event async for event in it ]
1348+ task_id = get_task_id (events1 [0 ])
1349+ context_id = get_task_context_id (events1 [0 ])
1350+
1351+ msg2 = Message (
1352+ task_id = task_id ,
1353+ context_id = context_id ,
1354+ message_id = 'msg-resume' ,
1355+ role = Role .ROLE_USER ,
1356+ parts = [Part (text = 'here is input' )],
1357+ )
1358+ it2 = client .send_message (
1359+ SendMessageRequest (
1360+ message = msg2 ,
1361+ configuration = SendMessageConfiguration (return_immediately = False ),
1362+ )
1363+ )
1364+ events2 = [event async for event in it2 ]
1365+
1366+ assert [event .WhichOneof ('payload' ) for event in events2 ] == [
1367+ * ['task' ] * task_enqueues ,
1368+ 'status_update' ,
1369+ ]
1370+ assert [message .message_id for message in events2 [0 ].task .history ] == [
1371+ 'msg-start' ,
1372+ 'msg-resume' ,
1373+ ]
1374+ validate_state (events2 [- 1 ], TaskState .TASK_STATE_COMPLETED )
1375+
1376+ final_task = await client .get_task (GetTaskRequest (id = task_id ))
1377+ assert [message .message_id for message in final_task .history ] == [
1378+ 'msg-start' ,
1379+ 'msg-resume' ,
1380+ ]
1381+ error_logs = [
1382+ record .message
1383+ for record in caplog .records
1384+ if record .levelname == 'ERROR'
1385+ and 'Ignoring task replacement' in record .message
1386+ ]
1387+ assert len (error_logs ) == replacement_errors
1388+
1389+
1390+ # Scenario: Resending a follow-up message adds it to history once
1391+ @pytest .mark .timeout (2.0 )
1392+ @pytest .mark .asyncio
1393+ @pytest .mark .parametrize (
1394+ 'enqueue_current_task' , [False , True ], ids = ['status_only' , 'current_task' ]
1395+ )
1396+ async def test_scenario_resent_follow_up_message_saved_once (
1397+ enqueue_current_task : bool ,
1398+ ) -> None :
1399+ class InputRequiredAgent (AgentExecutor ):
1400+ async def execute (
1401+ self , context : RequestContext , event_queue : EventQueue
1402+ ) -> None :
1403+ task = context .current_task or new_task_from_user_message (
1404+ context .message
1405+ )
1406+ if enqueue_current_task or not context .current_task :
1407+ await event_queue .enqueue_event (task )
1408+ await event_queue .enqueue_event (
1409+ TaskStatusUpdateEvent (
1410+ task_id = task .id ,
1411+ context_id = task .context_id ,
1412+ status = TaskStatus (
1413+ state = TaskState .TASK_STATE_INPUT_REQUIRED
1414+ ),
1415+ )
1416+ )
1417+
1418+ async def cancel (
1419+ self , context : RequestContext , event_queue : EventQueue
1420+ ) -> None :
1421+ pass
1422+
1423+ handler = create_handler (InputRequiredAgent (), use_legacy = False )
1424+ client = await create_client (
1425+ handler , agent_card = agent_card (), streaming = True
1426+ )
1427+
1428+ msg1 = Message (
1429+ message_id = 'msg-start' , role = Role .ROLE_USER , parts = [Part (text = 'start' )]
1430+ )
1431+ it = client .send_message (
1432+ SendMessageRequest (
1433+ message = msg1 ,
1434+ configuration = SendMessageConfiguration (return_immediately = False ),
1435+ )
1436+ )
1437+ events1 = [event async for event in it ]
1438+ task_id = get_task_id (events1 [0 ])
1439+ context_id = get_task_context_id (events1 [0 ])
1440+
1441+ msg2 = Message (
1442+ task_id = task_id ,
1443+ context_id = context_id ,
1444+ message_id = 'msg-resume' ,
1445+ role = Role .ROLE_USER ,
1446+ parts = [Part (text = 'here is input' )],
1447+ )
1448+ for _ in range (2 ):
1449+ it2 = client .send_message (
1450+ SendMessageRequest (
1451+ message = msg2 ,
1452+ configuration = SendMessageConfiguration (
1453+ return_immediately = False
1454+ ),
1455+ )
1456+ )
1457+ _ = [event async for event in it2 ]
1458+
1459+ final_task = await client .get_task (GetTaskRequest (id = task_id ))
1460+ assert [message .message_id for message in final_task .history ] == [
1461+ 'msg-start' ,
1462+ 'msg-resume' ,
1463+ ]
1464+
1465+
12941466# Scenario: Auth required and side channel unblocking
12951467# Migrated from: test_workflow_auth_required_side_channel in test_handler_comparison
12961468@pytest .mark .timeout (2.0 )
0 commit comments