fix(keycardai-a2a)!: align DelegationClient with a2a-sdk 1.x JSONRPC dispatcher#107
Merged
Merged
Conversation
8 tasks
📦 Release PreviewThis analysis shows the expected release impact: 📈 Expected Version Changes📋 Package Details[
{
"package_name": "keycardai-a2a",
"package_dir": "packages/a2a",
"has_changes": true,
"current_version": "0.2.0",
"next_version": "0.3.0",
"increment": "MINOR"
}
]📝 Changelog PreviewThis comment was automatically generated by the release preview workflow. |
…dispatcher (ACC-231) DelegationClient.invoke_service was hardcoded to the 0.x JSONRPC method "message/send", emitted a 0.x message envelope (lowercase role, no messageId), omitted the A2A-Version header, and unwrapped a 0.x response shape (result.parts). Against any real 1.x server the dispatcher rejected the call before the executor ran, so the keycardai-crewai delegation tools (the entire client-side path that runs through DelegationClientSync.invoke_service) were dead-on-arrival. The bug survived because the existing test_a2a_client.py tests mock the http client and the request body never sees a real dispatcher; they asserted "method == message/send" against the stubbed wire shape, which was internally consistent and externally wrong. Caught by the keycardai-crewai integration test added in commit 4: that test sends a real JSONRPC POST through Starlette TestClient against the real a2a-sdk DefaultRequestHandler, so dispatcher contract drift fails fast. Changes: - _build_jsonrpc_message_send -> _build_jsonrpc_send_message: method name is now "SendMessage" (the 1.x CamelCase form), the message envelope carries a messageId (required by the dispatcher) and the canonical enum-string role "ROLE_USER". - Both async and sync invoke_service set the A2A-Version: 1.0 header. The header constant comes from a2a.utils.constants so a future a2a-sdk rename follows our code automatically. - _unwrap_jsonrpc_response unwraps result.message.parts[].text, the shape SendMessageResponse takes when the executor enqueues a Message. Tasks fall back to JSON-stringified for now; full Task lifecycle consumers should reach for a2a.client.create_client directly. Tests: test_a2a_client.py mocks updated to the 1.x response shape and now assert method == "SendMessage", role == "ROLE_USER", messageId presence, and A2A-Version header on the outbound POST. Added test_jsonrpc_dispatch.py: a positive-path TestClient test that drives a real a2a-sdk DefaultRequestHandler with a stub AgentExecutor enqueuing a Message, then asserts the executor saw the user input and the access_token from KeycardServerCallContextBuilder. The auth-gate tests in test_agent_card_server.py only cover 401, so dispatcher contract drift had no local guard. This test fills the gap on the keycardai-a2a side; the keycardai-crewai integration test added in commit 4 covers the same chain with a CrewAIExecutor on top. a2a 45 passed (was 44; +1) crewai 27 passed agents 0 collected (empty package, expected) oauth/starlette/mcp/mcp-fastmcp/fastmcp green ruff clean BREAKING: Wire shape changes; any caller depending on the old "message/send" method name or the lowercase "user" role on the outbound envelope needs to update. No production callers exist (closed alpha; the keycardai-crewai delegation path is the only consumer in this PR).
1a8f4d7 to
4821883
Compare
📦 Release PreviewThis analysis shows the expected release impact: 📈 Expected Version Changes📋 Package Details[
{
"package_name": "keycardai-a2a",
"package_dir": "packages/a2a",
"has_changes": true,
"current_version": "0.2.0",
"next_version": "0.3.0",
"increment": "MINOR"
}
]📝 Changelog PreviewThis comment was automatically generated by the release preview workflow. |
carldunham
approved these changes
Apr 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DelegationClient.invoke_service(and the sync variant) was emitting the 0.x JSONRPC shape againsta2a-sdk1.x: methodmessage/send(the 1.x dispatcher rejects withMethod not found), nomessageIdfield on the message envelope, lowercaserole: "user"instead of the canonicalROLE_USER, and noA2A-Version: 1.0header. Against any real 1.x agent service the dispatcher rejected the request before the executor ran.Existing tests passed because they mocked the HTTP client and asserted the buggy shape; the request never reached a real dispatcher. Surfaced while exploring ACC-231.
Changes
delegation.py— emitSendMessagemethod,messageId,ROLE_USERenum string, and theA2A-Version: 1.0header (constant pulled froma2a.utils.constantsso future a2a-sdk renames track automatically)._unwrap_jsonrpc_response— read the 1.xSendMessageResponse.message.parts[].textshape (wasresult.parts).test_a2a_client.py— mocks updated to the 1.x shape; new assertions on method name, role,messageId, and theA2A-Versionheader.test_jsonrpc_dispatch.py(new) — positive-pathTestClienttest driving a realDefaultRequestHandlerwith a stub_EchoMessageExecutorenqueuing aMessage. Asserts the executor saw the input and the access_token fromKeycardServerCallContextBuilder. The auth-gate tests intest_agent_card_server.pyonly cover 401 (the dispatcher never sees the body), so future dispatcher drift had no local guard.Test plan
cd packages/a2a && uv run --extra test pytest -q→ 45 passed (44 prior + 1 new positive-path)uv run ruff checkcleanoauth/starlette/mcp/mcp-fastmcp/fastmcp/agents(sibling sweep run on the source branch before cherry-pick)Note for merge
Edit the squash-merge body down to one line so the CHANGELOG doesn't balloon (see
packages/a2a/CHANGELOG.mdfrom #105 for what happens otherwise).