{"repo":"NousResearch/hermes-agent","public_id":"7007","display_label":"ACP adapter missing usage field population fix","summary":"A bug was identified where PromptResponse.usage was always None in the ACP adapter due to top-level result keys being missed. Related fixes address populating the usage field correctly from top-level result fields.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":7086,"merged_at":"2026-04-10T10:00:12+00:00"},"resolution":{"state":"resolved","by":"pr-7086"},"freshness":{"state":"stale","last_verified_at":"2026-04-10T10:00:12+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":7007,"pr_number":7086,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-10T10:00:12+00:00"}],"members":[{"kind":"issue","number":7007,"title":"ACP adapter: PromptResponse.usage is always None — top-level keys missed","url":null,"state":"closed"},{"kind":"pr","number":7047,"title":"fix(acp): populate PromptResponse usage from top-level result fields","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#7086","number":7086}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":7086,"title":"fix(acp): populate PromptResponse usage from top-level result fields","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["acp_adapter/server.py","tests/acp/test_server.py"],"fix_hunks":[{"file":"acp_adapter/server.py","hunks":["@@ -451,14 +451,13 @@ def _run_agent() -> dict:\n             await conn.session_update(session_id, update)\n \n         usage = None\n-        usage_data = result.get(\"usage\")\n-        if usage_data and isinstance(usage_data, dict):\n+        if any(result.get(key) is not None for key in (\"prompt_tokens\", \"completion_tokens\", \"total_tokens\")):\n             usage = Usage(\n-                input_tokens=usage_data.get(\"prompt_tokens\", 0),\n-                output_tokens=usage_data.get(\"completion_tokens\", 0),\n-                total_tokens=usage_data.get(\"total_tokens\", 0),\n-                thought_tokens=usage_data.get(\"reasoning_tokens\"),\n-                cached_read_tokens=usage_data.get(\"cached_tokens\"),\n+                input_tokens=result.get(\"prompt_tokens\", 0),\n+                output_tokens=result.get(\"completion_tokens\", 0),\n+                total_tokens=result.get(\"total_tokens\", 0),\n+                thought_tokens=result.get(\"reasoning_tokens\"),\n+                cached_read_tokens=result.get(\"cache_read_tokens\"),\n             )\n \n         stop_reason = \"cancelled\" if state.cancel_event and state.cancel_event.is_set() else \"end_turn\""]},{"file":"tests/acp/test_server.py","hunks":["@@ -410,6 +410,37 @@ async def test_prompt_sends_final_message_update(self, agent):\n         update = last_call[1].get(\"update\") or last_call[0][1]\n         assert update.session_update == \"agent_message_chunk\"\n \n+    @pytest.mark.asyncio\n+    async def test_prompt_populates_usage_from_top_level_run_conversation_fields(self, agent):\n+        \"\"\"ACP should map top-level token fields into PromptResponse.usage.\"\"\"\n+        new_resp = await agent.new_session(cwd=\".\")\n+        state = agent.session_manager.get_session(new_resp.session_id)\n+\n+        state.agent.run_conversation = MagicMock(return_value={\n+            \"final_response\": \"usage attached\",\n+            \"messages\": [],\n+            \"prompt_tokens\": 123,\n+            \"completion_tokens\": 45,\n+            \"total_tokens\": 168,\n+            \"reasoning_tokens\": 7,\n+            \"cache_read_tokens\": 11,\n+        })\n+\n+        mock_conn = MagicMock(spec=acp.Client)\n+        mock_conn.session_update = AsyncMock()\n+        agent._conn = mock_conn\n+\n+        prompt = [TextContentBlock(type=\"text\", text=\"show usage\")]\n+        resp = await agent.prompt(prompt=prompt, session_id=new_resp.session_id)\n+\n+        assert isinstance(resp, PromptResponse)\n+        assert resp.usage is not None\n+        assert resp.usage.input_tokens == 123\n+        assert resp.usage.output_tokens == 45\n+        assert resp.usage.total_tokens == 168\n+        assert resp.usage.thought_tokens == 7\n+        assert resp.usage.cached_read_tokens == 11\n+\n     @pytest.mark.asyncio\n     async def test_prompt_cancelled_returns_cancelled_stop_reason(self, agent):\n         \"\"\"If cancel is called during prompt, stop_reason should be 'cancelled'.\"\"\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}