Skip to content

Commit 72bce93

Browse files
physicsrobKludex
andauthored
Upgrade a2a to spec v0.2.5 (#2144)
Co-authored-by: Marcelo Trylesinski <[email protected]>
1 parent 6add458 commit 72bce93

File tree

11 files changed

+1320
-287
lines changed

11 files changed

+1320
-287
lines changed

docs/a2a.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ The library is designed to be used with any agentic framework, and is **not excl
3232
Given the nature of the A2A protocol, it's important to understand the design before using it, as a developer
3333
you'll need to provide some components:
3434

35-
- [`Storage`][fasta2a.Storage]: to save and load tasks
35+
- [`Storage`][fasta2a.Storage]: to save and load tasks, as well as store context for conversations
3636
- [`Broker`][fasta2a.Broker]: to schedule tasks
3737
- [`Worker`][fasta2a.Worker]: to execute tasks
3838

@@ -55,6 +55,28 @@ flowchart TB
5555

5656
FastA2A allows you to bring your own [`Storage`][fasta2a.Storage], [`Broker`][fasta2a.Broker] and [`Worker`][fasta2a.Worker].
5757

58+
#### Understanding Tasks and Context
59+
60+
In the A2A protocol:
61+
62+
- **Task**: Represents one complete execution of an agent. When a client sends a message to the agent, a new task is created. The agent runs until completion (or failure), and this entire execution is considered one task. The final output is stored as a task artifact.
63+
64+
- **Context**: Represents a conversation thread that can span multiple tasks. The A2A protocol uses a `context_id` to maintain conversation continuity:
65+
- When a new message is sent without a `context_id`, the server generates a new one
66+
- Subsequent messages can include the same `context_id` to continue the conversation
67+
- All tasks sharing the same `context_id` have access to the complete message history
68+
69+
#### Storage Architecture
70+
71+
The [`Storage`][fasta2a.Storage] component serves two purposes:
72+
73+
1. **Task Storage**: Stores tasks in A2A protocol format, including their status, artifacts, and message history
74+
2. **Context Storage**: Stores conversation context in a format optimized for the specific agent implementation
75+
76+
This design allows for agents to store rich internal state (e.g., tool calls, reasoning traces) as well as store task-specific A2A-formatted messages and artifacts.
77+
78+
For example, a PydanticAI agent might store its complete internal message format (including tool calls and responses) in the context storage, while storing only the A2A-compliant messages in the task history.
79+
5880

5981
### Installation
6082

@@ -94,3 +116,12 @@ uvicorn agent_to_a2a:app --host 0.0.0.0 --port 8000
94116
```
95117

96118
Since the goal of `to_a2a` is to be a convenience method, it accepts the same arguments as the [`FastA2A`][fasta2a.FastA2A] constructor.
119+
120+
When using `to_a2a()`, PydanticAI automatically:
121+
122+
- Stores the complete conversation history (including tool calls and responses) in the context storage
123+
- Ensures that subsequent messages with the same `context_id` have access to the full conversation history
124+
- Persists agent results as A2A artifacts:
125+
- String results become `TextPart` artifacts and also appear in the message history
126+
- Structured data (Pydantic models, dataclasses, tuples, etc.) become `DataPart` artifacts with the data wrapped as `{"result": <your_data>}`
127+
- Artifacts include metadata with type information and JSON schema when available

fasta2a/fasta2a/applications.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@
1313

1414
from .broker import Broker
1515
from .schema import (
16+
AgentCapabilities,
1617
AgentCard,
17-
Authentication,
18-
Capabilities,
19-
Provider,
18+
AgentProvider,
2019
Skill,
2120
a2a_request_ta,
2221
a2a_response_ta,
@@ -39,7 +38,7 @@ def __init__(
3938
url: str = 'http://localhost:8000',
4039
version: str = '1.0.0',
4140
description: str | None = None,
42-
provider: Provider | None = None,
41+
provider: AgentProvider | None = None,
4342
skills: list[Skill] | None = None,
4443
# Starlette
4544
debug: bool = False,
@@ -85,16 +84,17 @@ async def _agent_card_endpoint(self, request: Request) -> Response:
8584
if self._agent_card_json_schema is None:
8685
agent_card = AgentCard(
8786
name=self.name,
87+
description=self.description or 'FastA2A Agent',
8888
url=self.url,
8989
version=self.version,
90+
protocol_version='0.2.5',
9091
skills=self.skills,
9192
default_input_modes=self.default_input_modes,
9293
default_output_modes=self.default_output_modes,
93-
capabilities=Capabilities(streaming=False, push_notifications=False, state_transition_history=False),
94-
authentication=Authentication(schemes=[]),
94+
capabilities=AgentCapabilities(
95+
streaming=False, push_notifications=False, state_transition_history=False
96+
),
9597
)
96-
if self.description is not None:
97-
agent_card['description'] = self.description
9898
if self.provider is not None:
9999
agent_card['provider'] = self.provider
100100
self._agent_card_json_schema = agent_card_ta.dump_json(agent_card, by_alias=True)
@@ -116,8 +116,8 @@ async def _agent_run_endpoint(self, request: Request) -> Response:
116116
data = await request.body()
117117
a2a_request = a2a_request_ta.validate_json(data)
118118

119-
if a2a_request['method'] == 'tasks/send':
120-
jsonrpc_response = await self.task_manager.send_task(a2a_request)
119+
if a2a_request['method'] == 'message/send':
120+
jsonrpc_response = await self.task_manager.send_message(a2a_request)
121121
elif a2a_request['method'] == 'tasks/get':
122122
jsonrpc_response = await self.task_manager.get_task(a2a_request)
123123
elif a2a_request['method'] == 'tasks/cancel':

fasta2a/fasta2a/client.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
GetTaskRequest,
1010
GetTaskResponse,
1111
Message,
12-
PushNotificationConfig,
13-
SendTaskRequest,
14-
SendTaskResponse,
15-
TaskSendParams,
12+
MessageSendConfiguration,
13+
MessageSendParams,
14+
SendMessageRequest,
15+
SendMessageResponse,
1616
a2a_request_ta,
17+
send_message_request_ta,
18+
send_message_response_ta,
1719
)
1820

19-
send_task_response_ta = pydantic.TypeAdapter(SendTaskResponse)
2021
get_task_response_ta = pydantic.TypeAdapter(GetTaskResponse)
2122

2223
try:
@@ -37,26 +38,30 @@ def __init__(self, base_url: str = 'http://localhost:8000', http_client: httpx.A
3738
self.http_client = http_client
3839
self.http_client.base_url = base_url
3940

40-
async def send_task(
41+
async def send_message(
4142
self,
4243
message: Message,
43-
history_length: int | None = None,
44-
push_notification: PushNotificationConfig | None = None,
44+
*,
4545
metadata: dict[str, Any] | None = None,
46-
) -> SendTaskResponse:
47-
task = TaskSendParams(message=message, id=str(uuid.uuid4()))
48-
if history_length is not None:
49-
task['history_length'] = history_length
50-
if push_notification is not None:
51-
task['push_notification'] = push_notification
46+
configuration: MessageSendConfiguration | None = None,
47+
) -> SendMessageResponse:
48+
"""Send a message using the A2A protocol.
49+
50+
Returns a JSON-RPC response containing either a result (Task) or an error.
51+
"""
52+
params = MessageSendParams(message=message)
5253
if metadata is not None:
53-
task['metadata'] = metadata
54+
params['metadata'] = metadata
55+
if configuration is not None:
56+
params['configuration'] = configuration
5457

55-
payload = SendTaskRequest(jsonrpc='2.0', id=None, method='tasks/send', params=task)
56-
content = a2a_request_ta.dump_json(payload, by_alias=True)
58+
request_id = str(uuid.uuid4())
59+
payload = SendMessageRequest(jsonrpc='2.0', id=request_id, method='message/send', params=params)
60+
content = send_message_request_ta.dump_json(payload, by_alias=True)
5761
response = await self.http_client.post('/', content=content, headers={'Content-Type': 'application/json'})
5862
self._raise_for_status(response)
59-
return send_task_response_ta.validate_json(response.content)
63+
64+
return send_message_response_ta.validate_json(response.content)
6065

6166
async def get_task(self, task_id: str) -> GetTaskResponse:
6267
payload = GetTaskRequest(jsonrpc='2.0', id=None, method='tasks/get', params={'id': task_id})

0 commit comments

Comments
 (0)