@@ -1422,3 +1422,110 @@ async def test_process_stream_keeps_tool_use_stop_reason_unchanged(agenerator, a
14221422 last_event = cast (ModelStopReason , (await alist (stream ))[- 1 ])
14231423
14241424 assert last_event ["stop" ][0 ] == "tool_use"
1425+
1426+
1427+ def test_handle_content_block_delta_captures_tool_use_id_and_name_from_delta ():
1428+ """Delta events that include toolUseId and name should populate current_tool_use."""
1429+ event = {"delta" : {"toolUse" : {"input" : '{"x": 1}' , "toolUseId" : "abc123" , "name" : "output_slide" }}}
1430+ state = {"current_tool_use" : {}}
1431+
1432+ updated_state , _ = strands .event_loop .streaming .handle_content_block_delta (event , state )
1433+
1434+ assert updated_state ["current_tool_use" ]["toolUseId" ] == "abc123"
1435+ assert updated_state ["current_tool_use" ]["name" ] == "output_slide"
1436+ assert updated_state ["current_tool_use" ]["input" ] == '{"x": 1}'
1437+
1438+
1439+ def test_handle_content_block_delta_does_not_override_existing_tool_use_id_and_name ():
1440+ """toolUseId and name from contentBlockStart should not be overridden by a later delta."""
1441+ event = {"delta" : {"toolUse" : {"input" : '{"x": 1}' , "toolUseId" : "from_delta" , "name" : "from_delta" }}}
1442+ state = {"current_tool_use" : {"toolUseId" : "from_start" , "name" : "from_start" , "input" : "" }}
1443+
1444+ updated_state , _ = strands .event_loop .streaming .handle_content_block_delta (event , state )
1445+
1446+ assert updated_state ["current_tool_use" ]["toolUseId" ] == "from_start"
1447+ assert updated_state ["current_tool_use" ]["name" ] == "from_start"
1448+
1449+
1450+ def test_handle_content_block_delta_tool_use_without_input_key ():
1451+ """A toolUse delta missing the input key should not raise KeyError."""
1452+ event = {"delta" : {"toolUse" : {}}}
1453+ state = {"current_tool_use" : {"toolUseId" : "t1" , "name" : "tool" }}
1454+
1455+ updated_state , _ = strands .event_loop .streaming .handle_content_block_delta (event , state )
1456+
1457+ assert updated_state ["current_tool_use" ]["input" ] == ""
1458+
1459+
1460+ def test_handle_content_block_stop_skips_incomplete_tool_use_missing_id (caplog ):
1461+ """A tool use block missing toolUseId is skipped with a warning."""
1462+ import logging
1463+
1464+ state = {
1465+ "content" : [],
1466+ "current_tool_use" : {"name" : "output_slide" , "input" : '{"x": 1}' },
1467+ "text" : "" ,
1468+ "reasoningText" : "" ,
1469+ "citationsContent" : [],
1470+ }
1471+
1472+ with caplog .at_level (logging .WARNING , logger = "strands.event_loop.streaming" ):
1473+ updated_state = strands .event_loop .streaming .handle_content_block_stop (state )
1474+
1475+ assert updated_state ["content" ] == []
1476+ assert updated_state ["current_tool_use" ] == {}
1477+ assert "Incomplete tool use block" in caplog .text
1478+
1479+
1480+ def test_handle_content_block_stop_skips_incomplete_tool_use_missing_name (caplog ):
1481+ """A tool use block missing name is skipped with a warning."""
1482+ import logging
1483+
1484+ state = {
1485+ "content" : [],
1486+ "current_tool_use" : {"toolUseId" : "abc123" , "input" : '{"x": 1}' },
1487+ "text" : "" ,
1488+ "reasoningText" : "" ,
1489+ "citationsContent" : [],
1490+ }
1491+
1492+ with caplog .at_level (logging .WARNING , logger = "strands.event_loop.streaming" ):
1493+ updated_state = strands .event_loop .streaming .handle_content_block_stop (state )
1494+
1495+ assert updated_state ["content" ] == []
1496+ assert updated_state ["current_tool_use" ] == {}
1497+ assert "Incomplete tool use block" in caplog .text
1498+
1499+
1500+ @pytest .mark .asyncio
1501+ async def test_process_stream_tool_use_info_in_delta (agenerator , alist ):
1502+ """Models that provide toolUseId and name in contentBlockDelta (not contentBlockStart) work correctly."""
1503+ response = [
1504+ {"messageStart" : {"role" : "assistant" }},
1505+ {"contentBlockStart" : {"start" : {}}},
1506+ {
1507+ "contentBlockDelta" : {
1508+ "delta" : {"toolUse" : {"input" : '{"title": "Test"}' , "toolUseId" : "xyz789" , "name" : "output_slide" }}
1509+ }
1510+ },
1511+ {"contentBlockStop" : {}},
1512+ {"messageStop" : {"stopReason" : "tool_use" }},
1513+ {
1514+ "metadata" : {
1515+ "usage" : {"inputTokens" : 5 , "outputTokens" : 10 , "totalTokens" : 15 },
1516+ "metrics" : {"latencyMs" : 50 },
1517+ }
1518+ },
1519+ ]
1520+
1521+ stream = strands .event_loop .streaming .process_stream (agenerator (response ))
1522+ events = await alist (stream )
1523+ last_event = cast (ModelStopReason , events [- 1 ])
1524+
1525+ stop_reason , message , _ , _ = last_event ["stop" ]
1526+ assert stop_reason == "tool_use"
1527+ assert len (message ["content" ]) == 1
1528+ tool_use = message ["content" ][0 ]["toolUse" ]
1529+ assert tool_use ["toolUseId" ] == "xyz789"
1530+ assert tool_use ["name" ] == "output_slide"
1531+ assert tool_use ["input" ] == {"title" : "Test" }
0 commit comments