diff --git a/tests/integrations/acp/acp_toolkit/agent.py b/tests/integrations/acp/acp_toolkit/agent.py index d95d253a3..ad7bb8cc5 100644 --- a/tests/integrations/acp/acp_toolkit/agent.py +++ b/tests/integrations/acp/acp_toolkit/agent.py @@ -364,12 +364,21 @@ def will_call_mcp_tool( *, arguments: dict[str, Any], server: str = "band", + title: str | None = None, ) -> FakeACPAgent: - """Call an advertised MCP tool between ACP call and result updates.""" + """Call an advertised MCP tool between ACP call and result updates. + + ``title`` models agents that report an intent phrase instead of a tool name. + """ async def _action(a: FakeACPAgent, sid: str) -> None: await a.emit( - sid, start_tool_call(tool_call_id, tool_name, raw_input=arguments) + sid, + start_tool_call( + tool_call_id=tool_call_id, + title=title or tool_name, + raw_input=arguments, + ), ) result = await a.call_mcp_tool( session_id=sid, diff --git a/tests/integrations/acp/test_client_adapter_behavior.py b/tests/integrations/acp/test_client_adapter_behavior.py index cb6a8bd7a..5a4e93021 100644 --- a/tests/integrations/acp/test_client_adapter_behavior.py +++ b/tests/integrations/acp/test_client_adapter_behavior.py @@ -440,6 +440,69 @@ async def test_band_mcp_reply_is_narrated_around_the_message(fake_agent) -> None assert len(reply.tool_results) == 1 +@pytest.mark.asyncio +async def test_intent_titled_no_reply_suppresses_closing_text( + fake_agent: FakeACPAgent, +) -> None: + fake_agent.will_call_mcp_tool( + tool_call_id="tc-silent", + tool_name="band_no_reply", + title="Ending the turn silently", + arguments={"room_id": "room-1", "reason": "nothing asked of me"}, + ).will_say("No action is needed, so I ended the turn without replying.") + + async with acp_adapter( + fake_agent, fake_agent_config(inject_band_tools=True) + ) as session: + reply = await session.send("fyi, no action needed", room="room-1") + + assert reply.texts == [] + + +@pytest.mark.asyncio +async def test_intent_titled_send_message_is_not_duplicated_by_text( + fake_agent: FakeACPAgent, +) -> None: + answer = "The fix is ready for review." + fake_agent.will_call_mcp_tool( + tool_call_id="tc-message", + tool_name="band_send_message", + title="Sending the review handoff", + arguments={ + "room_id": "room-1", + "content": answer, + "mentions": ["@pat"], + }, + ).will_say("I sent the review handoff.") + + async with acp_adapter( + fake_agent, fake_agent_config(inject_band_tools=True) + ) as session: + reply = await session.send("please hand off", room="room-1") + + assert reply.texts == [answer] + + +@pytest.mark.asyncio +async def test_intent_titled_read_only_tool_keeps_text_reply( + fake_agent: FakeACPAgent, +) -> None: + answer = "I checked the room roster." + fake_agent.will_call_mcp_tool( + tool_call_id="tc-roster", + tool_name="band_get_participants", + title="Checking who is in the room", + arguments={"room_id": "room-1"}, + ).will_say(answer) + + async with acp_adapter( + fake_agent, fake_agent_config(inject_band_tools=True) + ) as session: + reply = await session.send("who is here?", room="room-1") + + assert reply.texts == [answer] + + @pytest.mark.asyncio async def test_band_mcp_event_is_narrated_around_the_thought(fake_agent) -> None: """A room-visible Band event still gets real ACP tool_call/tool_result