1+ from typing import Literal
2+
13from fastapi import APIRouter
4+ from pydantic import Field
25
36from src .api .schemas .authorization_types import (
47 AgentexResourceType ,
1417from src .domain .entities .task_messages import convert_task_message_content_to_entity
1518from src .domain .use_cases .messages_use_case import DMessageUseCase
1619from src .utils .authorization_shortcuts import DAuthorizedBodyId , DAuthorizedQuery
20+ from src .utils .model_utils import BaseModel
21+ from src .utils .pagination import decode_cursor , encode_cursor
1722
1823router = APIRouter (prefix = "/messages" , tags = ["Messages" ])
1924
2025
26+ class PaginatedMessagesResponse (BaseModel ):
27+ """Response with cursor pagination metadata."""
28+
29+ data : list [TaskMessage ] = Field (..., description = "List of messages" )
30+ next_cursor : str | None = Field (
31+ None , description = "Cursor for fetching the next page of older messages"
32+ )
33+ has_more : bool = Field (
34+ False , description = "Whether there are more messages to fetch"
35+ )
36+
37+
2138@router .post (
2239 "/batch" ,
2340 response_model = list [TaskMessage ],
@@ -118,6 +135,11 @@ async def list_messages(
118135 order_by : str | None = None ,
119136 order_direction : str = "desc" ,
120137) -> list [TaskMessage ]:
138+ """
139+ List messages for a task with offset-based pagination.
140+
141+ For cursor-based pagination with infinite scroll support, use /messages/paginated.
142+ """
121143 task_message_entities = await message_use_case .list_messages (
122144 task_id = task_id ,
123145 limit = limit ,
@@ -132,6 +154,85 @@ async def list_messages(
132154 ]
133155
134156
157+ @router .get (
158+ "/paginated" ,
159+ response_model = PaginatedMessagesResponse ,
160+ )
161+ async def list_messages_paginated (
162+ task_id : DAuthorizedQuery (AgentexResourceType .task , AuthorizedOperationType .read ),
163+ message_use_case : DMessageUseCase ,
164+ limit : int = 50 ,
165+ cursor : str | None = None ,
166+ direction : Literal ["older" , "newer" ] = "older" ,
167+ ) -> PaginatedMessagesResponse :
168+ """
169+ List messages for a task with cursor-based pagination.
170+
171+ This endpoint is designed for infinite scroll UIs where new messages may arrive
172+ while paginating through older ones.
173+
174+ Args:
175+ task_id: The task ID to filter messages by
176+ limit: Maximum number of messages to return (default: 50)
177+ cursor: Opaque cursor string for pagination. Pass the `next_cursor` from
178+ a previous response to get the next page.
179+ direction: Pagination direction - "older" to get older messages (default),
180+ "newer" to get newer messages.
181+
182+ Returns:
183+ PaginatedMessagesResponse with:
184+ - data: List of messages (newest first when direction="older")
185+ - next_cursor: Cursor for fetching the next page (null if no more pages)
186+ - has_more: Whether there are more messages to fetch
187+
188+ Example:
189+ First request: GET /messages/paginated?task_id=xxx&limit=50
190+ Next page: GET /messages/paginated?task_id=xxx&limit=50&cursor=<next_cursor>
191+ """
192+ # Decode cursor if provided
193+ before_id = None
194+ after_id = None
195+ if cursor :
196+ try :
197+ cursor_data = decode_cursor (cursor )
198+ if direction == "older" :
199+ before_id = cursor_data .id
200+ else :
201+ after_id = cursor_data .id
202+ except ValueError :
203+ # Invalid cursor, ignore and return from start
204+ pass
205+
206+ # Fetch one extra to determine if there are more results
207+ task_message_entities = await message_use_case .list_messages (
208+ task_id = task_id ,
209+ limit = limit + 1 ,
210+ page_number = 1 ,
211+ order_by = None ,
212+ order_direction = "desc" ,
213+ before_id = before_id ,
214+ after_id = after_id ,
215+ )
216+
217+ # Check if there are more results
218+ has_more = len (task_message_entities ) > limit
219+ task_message_entities = task_message_entities [:limit ]
220+
221+ # Build next cursor from last message
222+ next_cursor = None
223+ if has_more and task_message_entities :
224+ last_message = task_message_entities [- 1 ]
225+ next_cursor = encode_cursor (last_message .id , last_message .created_at )
226+
227+ messages = [TaskMessage .model_validate (entity ) for entity in task_message_entities ]
228+
229+ return PaginatedMessagesResponse (
230+ data = messages ,
231+ next_cursor = next_cursor ,
232+ has_more = has_more ,
233+ )
234+
235+
135236@router .get (
136237 "/{message_id}" ,
137238 response_model = TaskMessage ,
0 commit comments