Skip to content

Commit ffc0279

Browse files
committed
Merge remote-tracking branch 'origin/main' into Extension-support-for-Client_local
2 parents 48ea2ae + 96d70e2 commit ffc0279

24 files changed

+512
-311
lines changed

.github/workflows/linter.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
with:
1919
python-version-file: .python-version
2020
- name: Install uv
21-
uses: astral-sh/setup-uv@v6
21+
uses: astral-sh/setup-uv@v7
2222
- name: Add uv to PATH
2323
run: |
2424
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

.github/workflows/python-publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- uses: actions/checkout@v5
1616

1717
- name: Install uv
18-
uses: astral-sh/setup-uv@v6
18+
uses: astral-sh/setup-uv@v7
1919

2020
- name: "Set up Python"
2121
uses: actions/setup-python@v6
@@ -26,7 +26,7 @@ jobs:
2626
run: uv build
2727

2828
- name: Upload distributions
29-
uses: actions/upload-artifact@v4
29+
uses: actions/upload-artifact@v5
3030
with:
3131
name: release-dists
3232
path: dist/
@@ -40,7 +40,7 @@ jobs:
4040

4141
steps:
4242
- name: Retrieve release distributions
43-
uses: actions/download-artifact@v5
43+
uses: actions/download-artifact@v6
4444
with:
4545
name: release-dists
4646
path: dist/

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
echo "MYSQL_TEST_DSN=mysql+aiomysql://a2a:a2a_password@localhost:3306/a2a_test" >> $GITHUB_ENV
4747
4848
- name: Install uv for Python ${{ matrix.python-version }}
49-
uses: astral-sh/setup-uv@v6
49+
uses: astral-sh/setup-uv@v7
5050
with:
5151
python-version: ${{ matrix.python-version }}
5252
- name: Add uv to PATH

.github/workflows/update-a2a-types.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
with:
1919
python-version: '3.10'
2020
- name: Install uv
21-
uses: astral-sh/setup-uv@v6
21+
uses: astral-sh/setup-uv@v7
2222
- name: Configure uv shell
2323
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
2424
- name: Install dependencies (datamodel-code-generator)

CHANGELOG.md

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

3+
## [0.3.11](https://github.com/a2aproject/a2a-python/compare/v0.3.10...v0.3.11) (2025-11-07)
4+
5+
6+
### Bug Fixes
7+
8+
* add metadata to send message request ([12b4a1d](https://github.com/a2aproject/a2a-python/commit/12b4a1d565a53794f5b55c8bd1728221c906ed41))
9+
310
## [0.3.10](https://github.com/a2aproject/a2a-python/compare/v0.3.9...v0.3.10) (2025-10-21)
411

512

scripts/generate_types.sh

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,105 @@
44
# Treat unset variables as an error.
55
set -euo pipefail
66

7-
REMOTE_URL="https://raw.githubusercontent.com/a2aproject/A2A/refs/heads/main/specification/json/a2a.json"
7+
# A2A specification version to use
8+
# Can be overridden via environment variable: A2A_SPEC_VERSION=v1.2.0 ./generate_types.sh
9+
# Or via command-line flag: ./generate_types.sh --version v1.2.0 output.py
10+
# Use a specific git tag, branch name, or commit SHA
11+
# Examples: "v1.0.0", "v1.2.0", "main", "abc123def"
12+
A2A_SPEC_VERSION="${A2A_SPEC_VERSION:-v0.3.0}"
13+
14+
# Build URL based on version format
15+
# Tags use /refs/tags/, branches use /refs/heads/, commits use direct ref
16+
build_remote_url() {
17+
local version="$1"
18+
local base_url="https://raw.githubusercontent.com/a2aproject/A2A"
19+
local spec_path="specification/json/a2a.json"
20+
local url_part
21+
22+
if [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
23+
# Looks like a version tag (v1.0.0, v1.2.3)
24+
url_part="refs/tags/${version}"
25+
elif [[ "$version" =~ ^[0-9a-f]{7,40}$ ]]; then
26+
# Looks like a commit SHA (7+ hex chars)
27+
url_part="${version}"
28+
else
29+
# Assume it's a branch name (main, develop, etc.)
30+
url_part="refs/heads/${version}"
31+
fi
32+
echo "${base_url}/${url_part}/${spec_path}"
33+
}
34+
35+
REMOTE_URL=$(build_remote_url "$A2A_SPEC_VERSION")
836

937
GENERATED_FILE=""
1038
INPUT_FILE=""
1139

1240
# Parse command-line arguments
1341
while [[ $# -gt 0 ]]; do
1442
case "$1" in
15-
--input-file)
16-
INPUT_FILE="$2"
17-
shift 2
18-
;;
19-
*)
20-
GENERATED_FILE="$1"
21-
shift 1
22-
;;
43+
--input-file)
44+
INPUT_FILE="$2"
45+
shift 2
46+
;;
47+
--version)
48+
A2A_SPEC_VERSION="$2"
49+
REMOTE_URL=$(build_remote_url "$A2A_SPEC_VERSION")
50+
shift 2
51+
;;
52+
*)
53+
GENERATED_FILE="$1"
54+
shift 1
55+
;;
2356
esac
2457
done
2558

2659
if [ -z "$GENERATED_FILE" ]; then
27-
echo "Error: Output file path must be provided." >&2
28-
echo "Usage: $0 [--input-file <path>] <output-file-path>"
60+
cat >&2 <<EOF
61+
Error: Output file path must be provided.
62+
Usage: $0 [--input-file <path>] [--version <version>] <output-file-path>
63+
Options:
64+
--input-file <path> Use a local JSON schema file instead of fetching from remote
65+
--version <version> Specify A2A spec version (default: v0.3.0)
66+
Can be a git tag (v1.0.0), branch (main), or commit SHA
67+
Environment variables:
68+
A2A_SPEC_VERSION Override default spec version
69+
Examples:
70+
$0 src/a2a/types.py
71+
$0 --version v1.2.0 src/a2a/types.py
72+
$0 --input-file local/a2a.json src/a2a/types.py
73+
A2A_SPEC_VERSION=main $0 src/a2a/types.py
74+
EOF
2975
exit 1
3076
fi
3177

3278
echo "Running datamodel-codegen..."
3379
declare -a source_args
3480
if [ -n "$INPUT_FILE" ]; then
3581
echo " - Source File: $INPUT_FILE"
82+
if [ ! -f "$INPUT_FILE" ]; then
83+
echo "Error: Input file does not exist: $INPUT_FILE" >&2
84+
exit 1
85+
fi
3686
source_args=("--input" "$INPUT_FILE")
3787
else
88+
echo " - A2A Spec Version: $A2A_SPEC_VERSION"
3889
echo " - Source URL: $REMOTE_URL"
90+
91+
# Validate that the remote URL is accessible
92+
echo " - Validating remote URL..."
93+
if ! curl --fail --silent --head "$REMOTE_URL" >/dev/null 2>&1; then
94+
cat >&2 <<EOF
95+
96+
Error: Unable to access A2A specification at version '$A2A_SPEC_VERSION'
97+
URL: $REMOTE_URL
98+
99+
The version may not exist. Available versions can be found at:
100+
https://github.com/a2aproject/A2A/tags
101+
102+
EOF
103+
exit 1
104+
fi
105+
39106
source_args=("--url" "$REMOTE_URL")
40107
fi
41108
echo " - Output File: $GENERATED_FILE"

src/a2a/client/base_client.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections.abc import AsyncIterator
2+
from typing import Any
23

34
from a2a.client.client import (
45
Client,
@@ -48,6 +49,7 @@ async def send_message(
4849
request: Message,
4950
*,
5051
context: ClientCallContext | None = None,
52+
request_metadata: dict[str, Any] | None = None,
5153
) -> AsyncIterator[ClientEvent | Message]:
5254
"""Sends a message to the agent.
5355
@@ -58,6 +60,7 @@ async def send_message(
5860
Args:
5961
request: The message to send to the agent.
6062
context: The client call context.
63+
request_metadata: Extensions Metadata attached to the request.
6164
6265
Yields:
6366
An async iterator of `ClientEvent` or a final `Message` response.
@@ -71,7 +74,9 @@ async def send_message(
7174
else None
7275
),
7376
)
74-
params = MessageSendParams(message=request, configuration=config)
77+
params = MessageSendParams(
78+
message=request, configuration=config, metadata=request_metadata
79+
)
7580

7681
if not self._config.streaming or not self._card.capabilities.streaming:
7782
response = await self._transport.send_message(

src/a2a/client/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ async def send_message(
117117
request: Message,
118118
*,
119119
context: ClientCallContext | None = None,
120+
request_metadata: dict[str, Any] | None = None,
120121
) -> AsyncIterator[ClientEvent | Message]:
121122
"""Sends a message to the server.
122123

src/a2a/utils/proto_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def make_dict_serializable(value: Any) -> Any:
5757
Returns:
5858
A serializable value.
5959
"""
60-
if isinstance(value, (str, int, float, bool)) or value is None:
60+
if isinstance(value, str | int | float | bool) or value is None:
6161
return value
6262
if isinstance(value, dict):
6363
return {k: make_dict_serializable(v) for k, v in value.items()}
@@ -140,6 +140,7 @@ def message(cls, message: types.Message | None) -> a2a_pb2.Message | None:
140140
task_id=message.task_id or '',
141141
role=cls.role(message.role),
142142
metadata=cls.metadata(message.metadata),
143+
extensions=message.extensions or [],
143144
)
144145

145146
@classmethod
@@ -239,6 +240,7 @@ def artifact(cls, artifact: types.Artifact) -> a2a_pb2.Artifact:
239240
metadata=cls.metadata(artifact.metadata),
240241
name=artifact.name,
241242
parts=[cls.part(p) for p in artifact.parts],
243+
extensions=artifact.extensions or [],
242244
)
243245

244246
@classmethod
@@ -695,6 +697,7 @@ def artifact(cls, artifact: a2a_pb2.Artifact) -> types.Artifact:
695697
metadata=cls.metadata(artifact.metadata),
696698
name=artifact.name,
697699
parts=[cls.part(p) for p in artifact.parts],
700+
extensions=artifact.extensions or None,
698701
)
699702

700703
@classmethod

tests/client/test_auth_middleware.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ def store():
106106

107107

108108
@pytest.mark.asyncio
109-
async def test_auth_interceptor_skips_when_no_agent_card(store):
110-
"""
111-
Tests that the AuthInterceptor does not modify the request when no AgentCard is provided.
112-
"""
109+
async def test_auth_interceptor_skips_when_no_agent_card(
110+
store: InMemoryContextCredentialStore,
111+
) -> None:
112+
"""Tests that the AuthInterceptor does not modify the request when no AgentCard is provided."""
113113
request_payload = {'foo': 'bar'}
114114
http_kwargs = {'fizz': 'buzz'}
115115
auth_interceptor = AuthInterceptor(credential_service=store)
@@ -126,9 +126,10 @@ async def test_auth_interceptor_skips_when_no_agent_card(store):
126126

127127

128128
@pytest.mark.asyncio
129-
async def test_in_memory_context_credential_store(store):
130-
"""
131-
Verifies that InMemoryContextCredentialStore correctly stores and retrieves
129+
async def test_in_memory_context_credential_store(
130+
store: InMemoryContextCredentialStore,
131+
) -> None:
132+
"""Verifies that InMemoryContextCredentialStore correctly stores and retrieves
132133
credentials based on the session ID in the client context.
133134
"""
134135
session_id = 'session-id'
@@ -163,11 +164,8 @@ async def test_in_memory_context_credential_store(store):
163164

164165
@pytest.mark.asyncio
165166
@respx.mock
166-
async def test_client_with_simple_interceptor():
167-
"""
168-
Ensures that a custom HeaderInterceptor correctly injects a static header
169-
into outbound HTTP requests from the A2AClient.
170-
"""
167+
async def test_client_with_simple_interceptor() -> None:
168+
"""Ensures that a custom HeaderInterceptor correctly injects a static header into outbound HTTP requests from the A2AClient."""
171169
url = 'http://agent.com/rpc'
172170
interceptor = HeaderInterceptor('X-Test-Header', 'Test-Value-123')
173171
card = AgentCard(
@@ -196,9 +194,7 @@ async def test_client_with_simple_interceptor():
196194

197195
@dataclass
198196
class AuthTestCase:
199-
"""
200-
Represents a test scenario for verifying authentication behavior in AuthInterceptor.
201-
"""
197+
"""Represents a test scenario for verifying authentication behavior in AuthInterceptor."""
202198

203199
url: str
204200
"""The endpoint URL of the agent to which the request is sent."""
@@ -284,11 +280,10 @@ class AuthTestCase:
284280
[api_key_test_case, oauth2_test_case, oidc_test_case, bearer_test_case],
285281
)
286282
@respx.mock
287-
async def test_auth_interceptor_variants(test_case, store):
288-
"""
289-
Parametrized test verifying that AuthInterceptor correctly attaches credentials
290-
based on the defined security scheme in the AgentCard.
291-
"""
283+
async def test_auth_interceptor_variants(
284+
test_case: AuthTestCase, store: InMemoryContextCredentialStore
285+
) -> None:
286+
"""Parametrized test verifying that AuthInterceptor correctly attaches credentials based on the defined security scheme in the AgentCard."""
292287
await store.set_credentials(
293288
test_case.session_id, test_case.scheme_name, test_case.credential
294289
)
@@ -329,12 +324,9 @@ async def test_auth_interceptor_variants(test_case, store):
329324

330325
@pytest.mark.asyncio
331326
async def test_auth_interceptor_skips_when_scheme_not_in_security_schemes(
332-
store,
333-
):
334-
"""
335-
Tests that AuthInterceptor skips a scheme if it's listed in security requirements
336-
but not defined in security_schemes.
337-
"""
327+
store: InMemoryContextCredentialStore,
328+
) -> None:
329+
"""Tests that AuthInterceptor skips a scheme if it's listed in security requirements but not defined in security_schemes."""
338330
scheme_name = 'missing'
339331
session_id = 'session-id'
340332
credential = 'dummy-token'

0 commit comments

Comments
 (0)