feat(agents): support invoke over the a2a protocol#8989
Conversation
Adds A2A (Agent2Agent) protocol support to `azd ai agent invoke`, alongside the existing responses and invocations protocols (issue #8956). A2A was previously deployment-only and excluded from invocation. - Make a2a invocable: include it in InvocableProtocols()/IsInvocable(). A2A is remote-only, gated by a new IsLocalInvocable() (activity_protocol stays fully deployment-only). - Route --protocol a2a (and agent.yaml auto-detection) to a new a2aRemote handler; --local a2a fails fast with actionable guidance. - Wrap a plain message in a JSON-RPC 2.0 message/send request with a single user text part; forward --input-file contents verbatim as a complete JSON-RPC request. - Parse the JSON-RPC response for the agent's reply text across A2A result shapes (Message parts, Task status.message/artifacts/history), surface JSON-RPC error envelopes, and support --output raw passthrough. - Add a2a endpoint URL builder/parsing (.../protocols/a2a) and update --protocol help, validation messages, and command examples. The A2A endpoint path tail and Foundry request/response wire shape are centralized in buildA2AInvokeURL / buildA2ARequestBody / handleA2AResponse so they can be adjusted in one place if the Foundry A2A contract differs; the JSON-RPC envelope follows the A2A spec (a2a-protocol.org). `--output raw` provides a format-independent escape hatch. Fixes #8956 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📋 Prioritization NoteThanks for the contribution! The linked issue isn't in the current milestone yet. |
There was a problem hiding this comment.
Pull request overview
This PR adds A2A (Agent2Agent) protocol support to azd ai agent invoke in the azure.ai.agents extension. A2A was previously deployment-only; it is now invocable against deployed Foundry agents (remote-only), alongside the existing responses and invocations protocols. The implementation closely mirrors the established invocationsRemote flow (session-bound memory, bearer-token acquisition after body validation, session-header capture, raw-output escape hatch, next-step guidance) and centralizes the A2A-specific wire format in three well-scoped helpers.
Changes:
- Makes
AgentProtocolA2Ainvocable viaInvocableProtocols()/IsInvocable(), and addsIsLocalInvocable()to keep A2A remote-only;--local a2afails fast with guidance. - Adds a new
a2aRemotehandler (invoke_a2a.go) that wraps a plain message into a JSON-RPC 2.0message/sendrequest (or forwards--input-fileverbatim), and extracts reply text across A2A Message/Task result shapes. - Wires
--agent-endpointparsing and abuildA2AInvokeURLbuilder for the.../protocols/a2atail; updates help text, validation messages, examples, and the changelog.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
internal/pkg/agents/agent_api/models.go |
Adds A2A to invocable protocols and introduces IsLocalInvocable() (A2A remote-only). |
internal/pkg/agents/agent_api/models_test.go |
Updates IsInvocable case for A2A and adds TestIsLocalInvocable. |
internal/cmd/invoke.go |
Routes A2A local/remote, adds local-invocability validation, updates help/examples/flag text. |
internal/cmd/invoke_a2a.go |
New A2A handler: request wrapping, remote POST flow, JSON-RPC error/result parsing, raw output. |
internal/cmd/invoke_a2a_test.go |
Unit tests for request wrapping, text extraction, error envelopes, local rejection, URL fallback. |
internal/cmd/agent_endpoint.go |
Extends endpoint regex/parsing for a2a and adds buildA2AInvokeURL. |
internal/cmd/agent_endpoint_test.go |
Adds A2A parse and URL-builder tests. |
internal/cmd/helpers.go |
Updates auto-detection error text to mention a2a. |
internal/cmd/helpers_test.go |
Adds single-a2a auto-detect case; retargets the non-invocable case to activity_protocol. |
CHANGELOG.md |
Adds an unreleased 1.0.0-beta.5 feature entry. |
Notes for the human reviewer:
- The routing, header handling, session capture, raw-mode
Accept-Encoding: identity, trace-ID printing, and next-step emission are consistent with the existinginvocationsRemoteimplementation, and test coverage matches the pattern used for the other protocols. - Per the author's own note, the Foundry hosted-agent A2A endpoint path and whether it accepts the vanilla JSON-RPC
message/sendbody are not publicly specced and remain unverified; these assumptions cannot be confirmed from the codebase and warrant confirmation before merge.
jongio
left a comment
There was a problem hiding this comment.
Reviewed the full changeset (10 files, +734/-15). The A2A handler follows the same session-based pattern as invocations, URL construction and request wrapping are centralized for easy adjustment if the Foundry contract changes, and test coverage spans all A2A result shapes (Message parts, Task status/artifacts/history, JSON-RPC errors, local rejection). No issues found.
RickWinter
left a comment
There was a problem hiding this comment.
This makes a2a an invocable protocol for azd ai agent invoke, remote-only, routed to a new a2aRemote handler that wraps a plain message in a JSON-RPC 2.0 message/send request (or forwards --input-file verbatim) and extracts the reply from the JSON-RPC result across Message and Task shapes. The approach is the right shape: it follows the existing responses/invocations conventions (session-bound memory, --new-session reset, --new-conversation no-op, --output raw passthrough), and the new IsLocalInvocable() split cleanly expresses that A2A is deployable and remotely invocable but has no local surface.
I checked the parts that depend on code outside the diff. The --local a2a rejection is covered twice, once as early flag validation in RunE when --protocol is explicit, and once as the runtime a2aLocal handler for the agent.yaml auto-detected path, so there is no gap. captureResponseSession reads only the x-agent-session-id header, so calling it before handleA2AResponse does not consume the body. buildA2AInvokeURL matches buildInvocationsURL (query escaping on api-version and session id, agent name left unescaped consistent with the sibling builders since it is pattern-validated upstream).
The one open item is external, not code: the Foundry hosted-agent A2A endpoint path and body contract are unconfirmed. You have already centralized those assumptions in buildA2AInvokeURL / buildA2ARequestBody / handleA2AResponse and left --output raw as an escape hatch, which is the correct containment. Worth confirming the wire contract with the Foundry team before merge, but that does not block the code review.
One nit inline on multi-part text joining, otherwise this is ready. Disposition: mergeable pending the external endpoint confirmation; nothing here requires a code change before merge.
Co-authored-by: Rick Winter <rick.winter@microsoft.com>
|
@copilot resolve the merge conflicts in this pull request |
Co-authored-by: glharper <64209257+glharper@users.noreply.github.com>
Resolved by merging |
jongio
left a comment
There was a problem hiding this comment.
Re-approving after the merge of main and the one-line fix to use newline as the text-parts separator in a2aTextFromParts. The change makes multi-part A2A responses more readable. No issues.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
jongio
left a comment
There was a problem hiding this comment.
Re-approving. The only change since my last review is a test comment rename (TestBuildA2AURL -> TestBuildA2AInvokeURL) to match the actual function name. No logic changes.
Addresses review feedback (@huimiu): the request body uses the v0.3 JSON-RPC message/send shape, and the A2A spec treats an omitted version as a 0.3 compatibility default. Send the A2A-Version header explicitly so a Foundry endpoint on a newer contract negotiates deterministically (returning VersionNotSupportedError if unsupported) rather than silently applying a default and potentially misparsing the body. The version is a constant tied to the request shape we emit, not derived from the agent's advertised protocol version, because the header must describe the body we actually send. Extracts applyA2ARequestHeaders and adds a unit test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jongio
left a comment
There was a problem hiding this comment.
Reviewed the new commit (6c1ea5e). The explicit A2A-Version header addition is correct per spec section 3.2, and centralizing header setup in applyA2ARequestHeaders keeps the call site clean. Test covers all three headers. No issues.
Closes #8956
What
Adds A2A (Agent2Agent) protocol support to
azd ai agent invoke, alongside the existingresponsesandinvocationsprotocols. A2A was previously deployment-only and excluded from invocation.Changes
a2ainvocable — added toInvocableProtocols()/IsInvocable(). A2A is remote-only, gated by a newIsLocalInvocable();activity_protocolremains fully deployment-only.--protocol a2aandagent.yamlprotocol auto-detection route to a newa2aRemotehandler (new fileinvoke_a2a.go), modeled on the invocations handler (session-based memory,--new-sessionresets,--new-conversationno-op).--local a2afails fast with actionable guidance.message/sendrequest with a single user text part;--input-filecontents are forwarded verbatim as a complete JSON-RPC request.resultacross A2A result shapes (Messageparts, Taskstatus.message/artifacts[]/history[]), a JSON-RPCerrorsurfaces as an agent error, and--output rawdumps the response verbatim.buildA2AInvokeURLand--agent-endpointparsing for the.../endpoint/protocols/a2atail; updated--protocolhelp, the unsupported-protocol / local-protocol validation messages, the multi-protocol guidance, and command examples.Tests
models_test.go:a2ais nowIsInvocable()==trueandIsLocalInvocable()==false.agent_endpoint_test.go: parse +buildA2AInvokeURL(api-version/session-id escaping, default api-version).helpers_test.go: singlea2aauto-detects; a non-invocable case retained viaactivity_protocol.invoke_a2a_test.go: JSON-RPC request wrapping, file passthrough, reply-text extraction across Message/Task shapes, JSON-RPC error detection,--localrejection, request-URL fallback.Validation
go build ./..., fullgo test ./...,go vet,gofmt,golangci-lint run(0 issues), andcspell— all clean.The A2A protocol itself is a public open standard (JSON-RPC 2.0
message/send, https://a2a-protocol.org), so the request/response envelope is well-defined. What is not publicly specced is the Foundry hosted-agent A2A invoke endpoint path. This PR follows the codebase's establishedresponses/invocationsconvention:{projectEndpoint}/agents/{name}/endpoint/protocols/a2a?api-version=...[&agent_session_id=...].These assumptions are centralized in
buildA2AInvokeURL,buildA2ARequestBody, andhandleA2AResponse, so if the Foundry contract differs they can be corrected in one place, and--output rawis a format-independent escape hatch. Please confirm the exact Foundry A2A endpoint path (and whether it expects the vanilla JSON-RPCmessage/sendbody) before merge. A2A local invocation is intentionally not supported (A2A is a remote agent-to-agent protocol).