Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/a2a/utils/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
from a2a.utils.errors import InvalidParamsError


MAX_HISTORY_LENGTH = 1000
"""Maximum allowed value for a ``history_length`` request.

Keeps a single request from asking the server to materialize an
unbounded history (e.g. ``historyLength=999999999``). The value is a
pragmatic cap aligned with the other A2A SDKs' semantics, where very
large values effectively mean "return everything available"; clients
requesting more than this cap get ``InvalidParamsError``.
"""


@runtime_checkable
class HistoryLengthConfig(Protocol):
"""Protocol for configuration arguments containing history_length field."""
Expand All @@ -25,9 +36,13 @@ def HasField(self, field_name: Literal['history_length']) -> bool: # noqa: N802


def validate_history_length(config: HistoryLengthConfig | None) -> None:
"""Validates that history_length is non-negative."""
"""Validates that history_length is non-negative and within limits."""
if config and config.history_length < 0:
raise InvalidParamsError(message='history length must be non-negative')
if config and config.history_length > MAX_HISTORY_LENGTH:
raise InvalidParamsError(
message=f'history length must be at most {MAX_HISTORY_LENGTH}'
)


def apply_history_length(
Expand Down
51 changes: 51 additions & 0 deletions tests/server/request_handlers/test_default_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2796,6 +2796,57 @@ async def test_on_get_task_negative_history_length_error(agent_card):
assert 'history length must be non-negative' in exc_info.value.message


@pytest.mark.asyncio
async def test_on_get_task_history_length_too_large_error(agent_card):
"""Test on_get_task raises error for history length above the limit."""
from a2a.utils.task import MAX_HISTORY_LENGTH

mock_task_store = AsyncMock(spec=TaskStore)
request_handler = DefaultRequestHandler(
agent_executor=AsyncMock(spec=AgentExecutor),
task_store=mock_task_store,
agent_card=agent_card,
)
params = GetTaskRequest(id='task1', history_length=MAX_HISTORY_LENGTH + 1)
context = create_server_call_context()

with pytest.raises(InvalidParamsError) as exc_info:
await request_handler.on_get_task(params, context)

assert str(MAX_HISTORY_LENGTH) in exc_info.value.message
mock_task_store.get.assert_not_awaited()


@pytest.mark.asyncio
async def test_on_message_send_history_length_too_large_error(agent_card):
"""Test on_message_send raises error for history length above the limit."""
from a2a.utils.task import MAX_HISTORY_LENGTH

mock_task_store = AsyncMock(spec=TaskStore)
request_handler = DefaultRequestHandler(
agent_executor=AsyncMock(spec=AgentExecutor),
task_store=mock_task_store,
agent_card=agent_card,
)

message_config = SendMessageConfiguration(
history_length=MAX_HISTORY_LENGTH + 1,
accepted_output_modes=['text/plain'],
)
params = SendMessageRequest(
message=Message(
role=Role.ROLE_USER, message_id='msg1', parts=[Part(text='Test')]
),
configuration=message_config,
)
context = create_server_call_context()

with pytest.raises(InvalidParamsError) as exc_info:
await request_handler.on_message_send(params, context)

assert str(MAX_HISTORY_LENGTH) in exc_info.value.message


@pytest.mark.asyncio
async def test_on_list_tasks_page_size_too_small(agent_card):
"""Test on_list_tasks raises error for page_size < 1."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,25 @@ async def test_on_get_task_negative_history_length_error():
assert 'history length must be non-negative' in exc_info.value.message


@pytest.mark.asyncio
async def test_on_get_task_history_length_too_large_error():
"""Test on_get_task raises error for history length above the limit."""
from a2a.utils.task import MAX_HISTORY_LENGTH

mock_task_store = AsyncMock(spec=TaskStore)
request_handler = DefaultRequestHandlerV2(
agent_executor=AsyncMock(spec=AgentExecutor),
task_store=mock_task_store,
agent_card=create_default_agent_card(),
)
params = GetTaskRequest(id='task1', history_length=MAX_HISTORY_LENGTH + 1)
context = create_server_call_context()
with pytest.raises(InvalidParamsError) as exc_info:
await request_handler.on_get_task(params, context)
assert str(MAX_HISTORY_LENGTH) in exc_info.value.message
mock_task_store.get.assert_not_awaited()


@pytest.mark.asyncio
async def test_on_list_tasks_page_size_too_small():
"""Test on_list_tasks raises error for page_size < 1."""
Expand Down
34 changes: 34 additions & 0 deletions tests/utils/test_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
)
from a2a.utils.errors import InvalidParamsError
from a2a.utils.task import (
MAX_HISTORY_LENGTH,
apply_history_length,
decode_page_token,
encode_page_token,
validate_history_length,
)


Expand Down Expand Up @@ -89,5 +91,37 @@ def test_zero_history_length_returns_empty_history(self):
self.assertEqual(len(result.history), 0)


class TestValidateHistoryLength(unittest.TestCase):
def test_none_config_passes(self):
# Does not raise
validate_history_length(None)

def test_zero_passes(self):
validate_history_length(GetTaskRequest(history_length=0))

def test_boundary_max_passes(self):
validate_history_length(
GetTaskRequest(history_length=MAX_HISTORY_LENGTH)
)

def test_negative_raises(self):
with pytest.raises(InvalidParamsError) as excinfo:
validate_history_length(GetTaskRequest(history_length=-1))
assert 'non-negative' in str(excinfo.value)

def test_over_max_raises(self):
with pytest.raises(InvalidParamsError) as excinfo:
validate_history_length(
GetTaskRequest(history_length=MAX_HISTORY_LENGTH + 1)
)
assert str(MAX_HISTORY_LENGTH) in str(excinfo.value)

def test_over_max_raises_for_send_configuration(self):
with pytest.raises(InvalidParamsError):
validate_history_length(
SendMessageConfiguration(history_length=MAX_HISTORY_LENGTH + 1)
)


if __name__ == '__main__':
unittest.main()
Loading