From 4cc5065f63f7adf20705396fbd685660d63fd565 Mon Sep 17 00:00:00 2001 From: Teknium Date: Tue, 21 Apr 2026 05:59:19 -0700 Subject: [PATCH] =?UTF-8?q?fix(acp):=20follow-up=20=E2=80=94=20named-const?= =?UTF-8?q?=20page=20size,=20alias=20kwarg,=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace kwargs.get('limit', 50) with module-level _LIST_SESSIONS_PAGE_SIZE constant. ListSessionsRequest schema has no 'limit' field, so the kwarg path was dead. Constant is the single source of truth for the page cap. - Use next_cursor= (field name) instead of nextCursor= (alias). Both work under the schema's populate_by_name config, but using the declared Python field name is the consistent style in this file. - Add docstring explaining cwd pass-through and cursor semantics. - Add 4 tests: first-page with next_cursor, single-page no next_cursor, cursor resumes after match, unknown cursor returns empty page. --- acp_adapter/server.py | 26 +++++++++++++------- tests/acp/test_server.py | 51 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/acp_adapter/server.py b/acp_adapter/server.py index a989df5d2..1627c22ef 100644 --- a/acp_adapter/server.py +++ b/acp_adapter/server.py @@ -71,6 +71,11 @@ except Exception: # Thread pool for running AIAgent (synchronous) in parallel. _executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="acp-agent") +# Server-side page size for list_sessions. The ACP ListSessionsRequest schema +# does not expose a client-side limit, so this is a fixed cap that clients +# paginate against using `cursor` / `next_cursor`. +_LIST_SESSIONS_PAGE_SIZE = 50 + def _extract_text( prompt: list[ @@ -446,22 +451,27 @@ class HermesACPAgent(acp.Agent): cwd: str | None = None, **kwargs: Any, ) -> ListSessionsResponse: + """List ACP sessions with optional ``cwd`` filtering and cursor pagination. + + ``cwd`` is passed through to ``SessionManager.list_sessions`` which already + normalizes and filters by working directory. ``cursor`` is a ``session_id`` + previously returned as ``next_cursor``; results resume after that entry. + Server-side page size is capped at ``_LIST_SESSIONS_PAGE_SIZE``; when more + results remain, ``next_cursor`` is set to the last returned ``session_id``. + """ infos = self.session_manager.list_sessions(cwd=cwd) if cursor: - # Find the cursor index for idx, s in enumerate(infos): if s["session_id"] == cursor: infos = infos[idx + 1:] break else: - # Cursor not found, return empty + # Unknown cursor -> empty page (do not fall back to full list). infos = [] - # Cap limit - limit = kwargs.get("limit", 50) - has_more = len(infos) > limit - infos = infos[:limit] + has_more = len(infos) > _LIST_SESSIONS_PAGE_SIZE + infos = infos[:_LIST_SESSIONS_PAGE_SIZE] sessions = [] for s in infos: @@ -476,9 +486,9 @@ class HermesACPAgent(acp.Agent): updated_at=updated_at, ) ) - + next_cursor = sessions[-1].session_id if has_more and sessions else None - return ListSessionsResponse(sessions=sessions, nextCursor=next_cursor) + return ListSessionsResponse(sessions=sessions, next_cursor=next_cursor) # ---- Prompt (core) ------------------------------------------------------ diff --git a/tests/acp/test_server.py b/tests/acp/test_server.py index 61db3f9fb..faa4c18a7 100644 --- a/tests/acp/test_server.py +++ b/tests/acp/test_server.py @@ -270,6 +270,57 @@ class TestListAndFork: mock_list.assert_called_once_with(cwd="/mnt/e/Projects/AI/browser-link-3") + @pytest.mark.asyncio + async def test_list_sessions_pagination_first_page(self, agent): + from acp_adapter import server as acp_server + + infos = [ + {"session_id": f"s{i}", "cwd": "/tmp", "title": None, "updated_at": 0.0} + for i in range(acp_server._LIST_SESSIONS_PAGE_SIZE + 5) + ] + with patch.object(agent.session_manager, "list_sessions", return_value=infos): + resp = await agent.list_sessions() + + assert len(resp.sessions) == acp_server._LIST_SESSIONS_PAGE_SIZE + assert resp.next_cursor == resp.sessions[-1].session_id + + @pytest.mark.asyncio + async def test_list_sessions_pagination_no_more(self, agent): + infos = [ + {"session_id": f"s{i}", "cwd": "/tmp", "title": None, "updated_at": 0.0} + for i in range(3) + ] + with patch.object(agent.session_manager, "list_sessions", return_value=infos): + resp = await agent.list_sessions() + + assert len(resp.sessions) == 3 + assert resp.next_cursor is None + + @pytest.mark.asyncio + async def test_list_sessions_cursor_resumes_after_match(self, agent): + infos = [ + {"session_id": "s1", "cwd": "/tmp", "title": None, "updated_at": 0.0}, + {"session_id": "s2", "cwd": "/tmp", "title": None, "updated_at": 0.0}, + {"session_id": "s3", "cwd": "/tmp", "title": None, "updated_at": 0.0}, + ] + with patch.object(agent.session_manager, "list_sessions", return_value=infos): + resp = await agent.list_sessions(cursor="s1") + + assert [s.session_id for s in resp.sessions] == ["s2", "s3"] + assert resp.next_cursor is None + + @pytest.mark.asyncio + async def test_list_sessions_unknown_cursor_returns_empty(self, agent): + infos = [ + {"session_id": "s1", "cwd": "/tmp", "title": None, "updated_at": 0.0}, + {"session_id": "s2", "cwd": "/tmp", "title": None, "updated_at": 0.0}, + ] + with patch.object(agent.session_manager, "list_sessions", return_value=infos): + resp = await agent.list_sessions(cursor="does-not-exist") + + assert resp.sessions == [] + assert resp.next_cursor is None + # --------------------------------------------------------------------------- # session configuration / model routing # ---------------------------------------------------------------------------