From e26f9b207041c03d1aa9a982d29dbfda66df3a82 Mon Sep 17 00:00:00 2001 From: Henkey Date: Sat, 2 May 2026 00:16:27 +0100 Subject: [PATCH] fix(acp): route Zed thoughts to reasoning callbacks --- acp_adapter/server.py | 25 +++++++++++++++++++------ tests/acp/test_server.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/acp_adapter/server.py b/acp_adapter/server.py index f8dade72af..7395f2557c 100644 --- a/acp_adapter/server.py +++ b/acp_adapter/server.py @@ -744,24 +744,37 @@ class HermesACPAgent(acp.Agent): tool_call_meta: dict[str, dict[str, Any]] = {} previous_approval_cb = None + streamed_message = False + if conn: tool_progress_cb = make_tool_progress_cb(conn, session_id, loop, tool_call_ids, tool_call_meta) - thinking_cb = make_thinking_cb(conn, session_id, loop) + reasoning_cb = make_thinking_cb(conn, session_id, loop) step_cb = make_step_cb(conn, session_id, loop, tool_call_ids, tool_call_meta) message_cb = make_message_cb(conn, session_id, loop) + + def stream_delta_cb(text: str) -> None: + nonlocal streamed_message + if text: + streamed_message = True + message_cb(text) + approval_cb = make_approval_callback(conn.request_permission, loop, session_id) else: tool_progress_cb = None - thinking_cb = None + reasoning_cb = None step_cb = None - message_cb = None + stream_delta_cb = None approval_cb = None agent = state.agent agent.tool_progress_callback = tool_progress_cb - agent.thinking_callback = thinking_cb + # ACP thought panes should not receive Hermes' local kawaii waiting/status + # updates. Route provider/model reasoning deltas instead; if the provider + # emits no reasoning, Zed should not get a fake "thinking" accordion. + agent.thinking_callback = None + agent.reasoning_callback = reasoning_cb agent.step_callback = step_cb - agent.message_callback = message_cb + agent.stream_delta_callback = stream_delta_cb # Approval callback is per-thread (thread-local, GHSA-qg5c-hvr5-hjgr). # Set it INSIDE _run_agent so the TLS write happens in the executor @@ -867,7 +880,7 @@ class HermesACPAgent(acp.Agent): ) except Exception: logger.debug("Failed to auto-title ACP session %s", session_id, exc_info=True) - if final_response and conn: + if final_response and conn and not streamed_message: update = acp.update_agent_message_text(final_response) await conn.session_update(session_id, update) diff --git a/tests/acp/test_server.py b/tests/acp/test_server.py index 35aafc603e..d292ade3fe 100644 --- a/tests/acp/test_server.py +++ b/tests/acp/test_server.py @@ -200,6 +200,8 @@ class TestSessionOps: "context", "reset", "compact", + "steer", + "queue", "version", ] model_cmd = next( @@ -522,6 +524,11 @@ class TestPrompt: assert isinstance(resp, PromptResponse) assert resp.stop_reason == "end_turn" state.agent.run_conversation.assert_called_once() + assert state.agent.tool_progress_callback is not None + assert state.agent.step_callback is not None + assert state.agent.stream_delta_callback is not None + assert state.agent.reasoning_callback is not None + assert state.agent.thinking_callback is None @pytest.mark.asyncio async def test_prompt_updates_history(self, agent): @@ -572,6 +579,27 @@ class TestPrompt: update = last_call[1].get("update") or last_call[0][1] assert update.session_update == "agent_message_chunk" + @pytest.mark.asyncio + async def test_prompt_does_not_duplicate_streamed_final_message(self, agent): + """If ACP already streamed response chunks, final_response should not be sent again.""" + new_resp = await agent.new_session(cwd=".") + state = agent.session_manager.get_session(new_resp.session_id) + + def mock_run(*args, **kwargs): + state.agent.stream_delta_callback("streamed answer") + return {"final_response": "streamed answer", "messages": []} + + state.agent.run_conversation = mock_run + + mock_conn = MagicMock(spec=acp.Client) + mock_conn.session_update = AsyncMock() + agent._conn = mock_conn + + prompt = [TextContentBlock(type="text", text="hello")] + await agent.prompt(prompt=prompt, session_id=new_resp.session_id) + + assert mock_conn.session_update.call_count == 1 + @pytest.mark.asyncio async def test_prompt_auto_titles_session(self, agent): new_resp = await agent.new_session(cwd=".")