Skip to content

Commit 08f9341

Browse files
authored
Merge branch 'main' into easy-client
2 parents 5ccf7c3 + 317df0a commit 08f9341

File tree

5 files changed

+127
-21
lines changed

5 files changed

+127
-21
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# Changelog
22

3+
## [0.3.9](https://github.com/a2aproject/a2a-python/compare/v0.3.8...v0.3.9) (2025-10-15)
4+
5+
6+
### Features
7+
8+
* custom ID generators ([051ab20](https://github.com/a2aproject/a2a-python/commit/051ab20c395daa2807b0233cf1c53493e41b60c2))
9+
10+
11+
### Bug Fixes
12+
13+
* apply `history_length` for `message/send` requests ([#498](https://github.com/a2aproject/a2a-python/issues/498)) ([a49f94e](https://github.com/a2aproject/a2a-python/commit/a49f94ef23d81b8375e409b1c1e51afaf1da1956))
14+
* **client:** `A2ACardResolver.get_agent_card` will auto-populate with `agent_card_path` when `relative_card_path` is empty ([#508](https://github.com/a2aproject/a2a-python/issues/508)) ([ba24ead](https://github.com/a2aproject/a2a-python/commit/ba24eadb5b6fcd056a008e4cbcef03b3f72a37c3))
15+
16+
17+
### Documentation
18+
19+
* Fix Docstring formatting for code samples ([#492](https://github.com/a2aproject/a2a-python/issues/492)) ([dca66c3](https://github.com/a2aproject/a2a-python/commit/dca66c3100a2b9701a1c8b65ad6853769eefd511))
20+
321
## [0.3.8](https://github.com/a2aproject/a2a-python/compare/v0.3.7...v0.3.8) (2025-10-06)
422

523

src/a2a/client/card_resolver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def get_agent_card(
5353
Args:
5454
relative_card_path: Optional path to the agent card endpoint,
5555
relative to the base URL. If None, uses the default public
56-
agent card path.
56+
agent card path. Use `'/'` for an empty path.
5757
http_kwargs: Optional dictionary of keyword arguments to pass to the
5858
underlying httpx.get request.
5959
@@ -65,7 +65,7 @@ async def get_agent_card(
6565
A2AClientJSONError: If the response body cannot be decoded as JSON
6666
or validated against the AgentCard schema.
6767
"""
68-
if relative_card_path is None:
68+
if not relative_card_path:
6969
# Use the default public agent card path configured during initialization
7070
path_segment = self.agent_card_path
7171
else:

src/a2a/server/request_handlers/default_request_handler.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
UnsupportedOperationError,
4545
)
4646
from a2a.utils.errors import ServerError
47+
from a2a.utils.task import apply_history_length
4748
from a2a.utils.telemetry import SpanKind, trace_class
4849

4950

@@ -118,25 +119,7 @@ async def on_get_task(
118119
raise ServerError(error=TaskNotFoundError())
119120

120121
# Apply historyLength parameter if specified
121-
if params.history_length is not None and task.history:
122-
# Limit history to the most recent N messages
123-
limited_history = (
124-
task.history[-params.history_length :]
125-
if params.history_length > 0
126-
else []
127-
)
128-
# Create a new task instance with limited history
129-
task = Task(
130-
id=task.id,
131-
context_id=task.context_id,
132-
status=task.status,
133-
artifacts=task.artifacts,
134-
history=limited_history,
135-
metadata=task.metadata,
136-
kind=task.kind,
137-
)
138-
139-
return task
122+
return apply_history_length(task, params.history_length)
140123

141124
async def on_cancel_task(
142125
self, params: TaskIdParams, context: ServerCallContext | None = None
@@ -363,6 +346,10 @@ async def push_notification_callback() -> None:
363346

364347
if isinstance(result, Task):
365348
self._validate_task_id_match(task_id, result.id)
349+
if params.configuration:
350+
result = apply_history_length(
351+
result, params.configuration.history_length
352+
)
366353

367354
await self._send_push_notification_if_needed(task_id, result_aggregator)
368355

src/a2a/utils/task.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,25 @@ def completed_task(
7070
artifacts=artifacts,
7171
history=history,
7272
)
73+
74+
75+
def apply_history_length(task: Task, history_length: int | None) -> Task:
76+
"""Applies history_length parameter on task and returns a new task object.
77+
78+
Args:
79+
task: The original task object with complete history
80+
history_length: History length configuration value
81+
82+
Returns:
83+
A new task object with limited history
84+
"""
85+
# Apply historyLength parameter if specified
86+
if history_length is not None and task.history:
87+
# Limit history to the most recent N messages
88+
limited_history = (
89+
task.history[-history_length:] if history_length > 0 else []
90+
)
91+
# Create a new task instance with limited history
92+
return task.model_copy(update={'history': limited_history})
93+
94+
return task

tests/server/request_handlers/test_default_request_handler.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,85 @@ async def test_on_message_send_non_blocking():
836836
assert task.status.state == TaskState.completed
837837

838838

839+
@pytest.mark.asyncio
840+
async def test_on_message_send_limit_history():
841+
task_store = InMemoryTaskStore()
842+
push_store = InMemoryPushNotificationConfigStore()
843+
844+
request_handler = DefaultRequestHandler(
845+
agent_executor=HelloAgentExecutor(),
846+
task_store=task_store,
847+
push_config_store=push_store,
848+
)
849+
params = MessageSendParams(
850+
message=Message(
851+
role=Role.user,
852+
message_id='msg_push',
853+
parts=[Part(root=TextPart(text='Hi'))],
854+
),
855+
configuration=MessageSendConfiguration(
856+
blocking=True,
857+
accepted_output_modes=['text/plain'],
858+
history_length=0,
859+
),
860+
)
861+
862+
result = await request_handler.on_message_send(
863+
params, create_server_call_context()
864+
)
865+
866+
# verify that history_length is honored
867+
assert result is not None
868+
assert isinstance(result, Task)
869+
assert result.history is not None and len(result.history) == 0
870+
assert result.status.state == TaskState.completed
871+
872+
# verify that history is still persisted to the store
873+
task = await task_store.get(result.id)
874+
assert task is not None
875+
assert task.history is not None and len(task.history) > 0
876+
877+
878+
@pytest.mark.asyncio
879+
async def test_on_task_get_limit_history():
880+
task_store = InMemoryTaskStore()
881+
push_store = InMemoryPushNotificationConfigStore()
882+
883+
request_handler = DefaultRequestHandler(
884+
agent_executor=HelloAgentExecutor(),
885+
task_store=task_store,
886+
push_config_store=push_store,
887+
)
888+
params = MessageSendParams(
889+
message=Message(
890+
role=Role.user,
891+
message_id='msg_push',
892+
parts=[Part(root=TextPart(text='Hi'))],
893+
),
894+
configuration=MessageSendConfiguration(
895+
blocking=True, accepted_output_modes=['text/plain']
896+
),
897+
)
898+
899+
result = await request_handler.on_message_send(
900+
params, create_server_call_context()
901+
)
902+
903+
assert result is not None
904+
assert isinstance(result, Task)
905+
906+
get_task_result = await request_handler.on_get_task(
907+
TaskQueryParams(id=result.id, history_length=0),
908+
create_server_call_context(),
909+
)
910+
assert get_task_result is not None
911+
assert isinstance(get_task_result, Task)
912+
assert (
913+
get_task_result.history is not None
914+
and len(get_task_result.history) == 0
915+
)
916+
917+
839918
@pytest.mark.asyncio
840919
async def test_on_message_send_interrupted_flow():
841920
"""Test on_message_send when flow is interrupted (e.g., auth_required)."""

0 commit comments

Comments
 (0)