diff --git a/src/ai/ui/ai_sdk/inbound_messages.py b/src/ai/ui/ai_sdk/inbound_messages.py index 209e07f3..db541065 100644 --- a/src/ai/ui/ai_sdk/inbound_messages.py +++ b/src/ai/ui/ai_sdk/inbound_messages.py @@ -245,6 +245,8 @@ def _parse( ) case ui_messages_.UIToolInvocationPart() as inv: + if inv.state == "input-streaming": + continue tool_args = json.dumps(inv.args) if inv.args else "{}" is_completed = ( inv.state in _TOOL_RESULT_STATES @@ -297,6 +299,8 @@ def _parse( | ui_messages_.UIDynamicToolPart() ) as tp ): + if tp.state == "input-streaming": + continue tool_input = ( tp.raw_input if tp.state == "output-error" and tp.input is None diff --git a/tests/ui/ai_sdk/test_inbound_messages.py b/tests/ui/ai_sdk/test_inbound_messages.py index 8d2dfe1d..599ab13e 100644 --- a/tests/ui/ai_sdk/test_inbound_messages.py +++ b/tests/ui/ai_sdk/test_inbound_messages.py @@ -71,6 +71,39 @@ def test_to_messages_splits_at_tool_boundary() -> None: assert messages[1].tool_results[0].tool_call_id == "tc1" +@pytest.mark.parametrize( + "tool_part", + [ + _tool("search", "tc1", "input-streaming", input={"q": "par"}), + { + "type": "dynamic-tool", + "toolName": "search", + "toolCallId": "tc1", + "state": "input-streaming", + "input": {"q": "par"}, + }, + { + "type": "tool-invocation", + "toolName": "search", + "toolInvocationId": "tc1", + "state": "input-streaming", + "args": {"q": "par"}, + }, + ], +) +def test_to_messages_skips_incomplete_tool_calls( + tool_part: dict[str, Any], +) -> None: + messages, approvals = to_messages( + [_ui("assistant", _text("Searching"), tool_part)] + ) + + assert approvals == [] + assert len(messages) == 1 + assert messages[0].text == "Searching" + assert messages[0].tool_calls == [] + + def test_to_messages_keeps_deferred_approval_tombstone() -> None: """Deferred approvals carry no response — leave the tombstone in history.""" messages, _ = to_messages(