Skip to content

Commit 023deb2

Browse files
adamra-msftCopilot
andauthored
feat(azure.ai.agents): add A2A endpoint protocol and agent card support (#7999)
* feat(azure.ai.agents): add A2A endpoint protocol and agent card support Add support for setting A2A as an agent endpoint protocol and including agent card metadata in create/update agent requests. Changes: - Add AgentProtocolA2A constant and IsInvokable() helper to distinguish invokable vs deploy-only protocols - Add AgentEndpoint, AgentCard, and AgentCardSkill types to API models - Add corresponding YAML types and fields on ContainerAgent - Map YAML agentEndpoint/agentCard to API request in createAgentAPIRequest - Add PatchAgent operation (HTTP PATCH) for partial agent updates - Deploy path: create version first, then PATCH agent-level endpoint/card - Protocol resolution: require --protocol flag when multiple protocols declared - Invoke validation: use IsInvokable() to block non-invokable protocols - Add comprehensive tests for all new types and behaviors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5cd8feb commit 023deb2

11 files changed

Lines changed: 860 additions & 26 deletions

File tree

cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -724,8 +724,12 @@ func resolveAgentProtocol(
724724
}
725725

726726
// protocolFromAgentYaml reads and parses the agent.yaml file at the given path
727-
// and extracts the protocol. Returns an error with a contextual suggestion when
728-
// the file cannot be read, parsed, or does not declare exactly one protocol.
727+
// and extracts the protocol to use for invocation. Returns an error with a
728+
// contextual suggestion when the file cannot be read, parsed, or does not
729+
// declare exactly one invocable protocol.
730+
//
731+
// When multiple protocols are declared (e.g. "responses" + "a2a"), the caller
732+
// must use --protocol to disambiguate.
729733
func protocolFromAgentYaml(
730734
agentYamlPath string,
731735
) (agent_api.AgentProtocol, error) {
@@ -753,15 +757,18 @@ func protocolFromAgentYaml(
753757
)
754758
}
755759

756-
switch len(hosted.Protocols) {
757-
case 0:
760+
if len(hosted.Protocols) == 0 {
758761
return "", exterrors.Validation(
759762
exterrors.CodeInvalidParameter,
760763
"agent.yaml does not declare any protocols",
761764
"add a protocols section to agent.yaml",
762765
)
763-
case 1:
764-
p := strings.TrimSpace(hosted.Protocols[0].Protocol)
766+
}
767+
768+
// Validate that no protocol entry has an empty value, and collect invocable ones.
769+
var invocable []agent_api.AgentProtocol
770+
for _, rec := range hosted.Protocols {
771+
p := agent_api.AgentProtocol(strings.TrimSpace(rec.Protocol))
765772
if p == "" {
766773
return "", exterrors.Validation(
767774
exterrors.CodeInvalidParameter,
@@ -770,19 +777,60 @@ func protocolFromAgentYaml(
770777
"set a non-empty protocol value in agent.yaml",
771778
)
772779
}
773-
return agent_api.AgentProtocol(p), nil
774-
default:
780+
if p.IsInvocable() {
781+
invocable = append(invocable, p)
782+
}
783+
}
784+
785+
switch len(invocable) {
786+
case 0:
775787
names := make([]string, len(hosted.Protocols))
776788
for i, p := range hosted.Protocols {
777789
names[i] = p.Protocol
778790
}
779791
return "", exterrors.Validation(
780792
exterrors.CodeInvalidParameter,
781793
fmt.Sprintf(
782-
"agent.yaml declares multiple protocols: %s",
794+
"agent.yaml declares only non-invocable protocols: %s",
783795
strings.Join(names, ", "),
784796
),
785-
"use --protocol to specify which protocol to use",
797+
"azd can only invoke agents using the responses or invocations protocols",
786798
)
799+
case 1:
800+
// Exactly one invocable protocol — but if the agent declares
801+
// multiple protocols overall, require --protocol to be explicit.
802+
if len(hosted.Protocols) > 1 {
803+
return "", multiProtocolError(hosted.Protocols)
804+
}
805+
return invocable[0], nil
806+
default:
807+
return "", multiProtocolError(hosted.Protocols)
787808
}
788809
}
810+
811+
// multiProtocolError builds a validation error for agents that declare
812+
// multiple protocols, listing the valid invocable choices.
813+
func multiProtocolError(
814+
protocols []agent_yaml.ProtocolVersionRecord,
815+
) error {
816+
names := make([]string, len(protocols))
817+
for i, p := range protocols {
818+
names[i] = p.Protocol
819+
}
820+
supported := make([]string, len(agent_api.InvocableProtocols()))
821+
for i, p := range agent_api.InvocableProtocols() {
822+
supported[i] = string(p)
823+
}
824+
return exterrors.Validation(
825+
exterrors.CodeInvalidParameter,
826+
fmt.Sprintf(
827+
"agent.yaml declares multiple protocols (%s)",
828+
strings.Join(names, ", "),
829+
),
830+
fmt.Sprintf(
831+
"use --protocol to specify which invocable protocol "+
832+
"to use (supported: %s)",
833+
strings.Join(supported, ", "),
834+
),
835+
)
836+
}

cli/azd/extensions/azure.ai.agents/internal/cmd/helpers_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,20 @@ func TestProtocolFromAgentYaml(t *testing.T) {
245245
wantErr: true,
246246
errContain: "declares multiple protocols",
247247
},
248+
{
249+
name: "responses plus a2a requires --protocol",
250+
yaml: "protocols:\n - protocol: responses\n" +
251+
" version: \"1.0\"\n - protocol: a2a\n" +
252+
" version: \"1.0\"\n",
253+
wantErr: true,
254+
errContain: "declares multiple protocols",
255+
},
256+
{
257+
name: "a2a only is not invocable",
258+
yaml: "protocols:\n - protocol: a2a\n version: \"1.0\"\n",
259+
wantErr: true,
260+
errContain: "non-invocable protocols",
261+
},
248262
}
249263

250264
for _, tt := range tests {

cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,11 @@ session automatically. Pass --new-session to force a reset.`,
131131
}
132132

133133
if flags.protocol != "" {
134-
switch agent_api.AgentProtocol(flags.protocol) {
135-
case agent_api.AgentProtocolResponses,
136-
agent_api.AgentProtocolInvocations:
137-
// valid
138-
default:
134+
p := agent_api.AgentProtocol(flags.protocol)
135+
if !p.IsInvocable() {
139136
return exterrors.Validation(
140137
exterrors.CodeInvalidParameter,
141-
fmt.Sprintf("unsupported protocol %q", flags.protocol),
138+
fmt.Sprintf("unsupported protocol %q for invocation", flags.protocol),
142139
"supported protocols are: responses, invocations",
143140
)
144141
}

cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/models.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,29 @@ const (
1717
AgentProtocolActivityProtocol AgentProtocol = "activity_protocol"
1818
AgentProtocolInvocations AgentProtocol = "invocations"
1919
AgentProtocolResponses AgentProtocol = "responses"
20+
AgentProtocolA2A AgentProtocol = "a2a"
2021
)
2122

23+
// InvocableProtocols returns the set of protocols that azd can invoke directly.
24+
// A2A and activity_protocol are deployment-only — they cannot be used for local
25+
// or remote invocation through azd.
26+
func InvocableProtocols() []AgentProtocol {
27+
return []AgentProtocol{
28+
AgentProtocolResponses,
29+
AgentProtocolInvocations,
30+
}
31+
}
32+
33+
// IsInvocable reports whether the protocol can be used for invocation through azd.
34+
func (p AgentProtocol) IsInvocable() bool {
35+
switch p {
36+
case AgentProtocolResponses, AgentProtocolInvocations:
37+
return true
38+
default:
39+
return false
40+
}
41+
}
42+
2243
// AgentKind represents the different types of agents
2344
type AgentKind string
2445

@@ -58,6 +79,27 @@ type ProtocolVersionRecord struct {
5879
Version string `json:"version"`
5980
}
6081

82+
// AgentEndpoint describes the endpoint protocols an agent supports.
83+
type AgentEndpoint struct {
84+
Protocols []AgentProtocol `json:"protocols"`
85+
}
86+
87+
// AgentCardSkill describes a single capability that an agent can perform.
88+
type AgentCardSkill struct {
89+
ID string `json:"id"`
90+
Name string `json:"name"`
91+
Description string `json:"description"`
92+
Tags []string `json:"tags,omitempty"`
93+
Examples []string `json:"examples,omitempty"`
94+
}
95+
96+
// AgentCard is the A2A agent card that advertises an agent's capabilities.
97+
type AgentCard struct {
98+
Description string `json:"description"`
99+
Version *string `json:"version,omitempty"`
100+
Skills []AgentCardSkill `json:"skills"`
101+
}
102+
61103
// WorkflowDefinition represents a workflow agent
62104
type WorkflowDefinition struct {
63105
AgentDefinition
@@ -88,7 +130,9 @@ type CreateAgentVersionRequest struct {
88130

89131
// CreateAgentRequest represents a request to create an agent
90132
type CreateAgentRequest struct {
91-
Name string `json:"name"`
133+
Name string `json:"name"`
134+
AgentEndpoint *AgentEndpoint `json:"agent_endpoint,omitempty"`
135+
AgentCard *AgentCard `json:"agent_card,omitempty"`
92136
CreateAgentVersionRequest
93137
}
94138

@@ -97,6 +141,14 @@ type UpdateAgentRequest struct {
97141
CreateAgentVersionRequest
98142
}
99143

144+
// PatchAgentRequest represents a partial update to agent-level fields.
145+
// Only the fields set here are sent; Definition is intentionally excluded
146+
// to avoid accidentally clearing it.
147+
type PatchAgentRequest struct {
148+
AgentEndpoint *AgentEndpoint `json:"agent_endpoint,omitempty"`
149+
AgentCard *AgentCard `json:"agent_card,omitempty"`
150+
}
151+
100152
// AgentIdentityInfo represents the instance identity assigned to an agent version.
101153
type AgentIdentityInfo struct {
102154
PrincipalID string `json:"principal_id"`

0 commit comments

Comments
 (0)