From f68e40f5e8ca490ab0694543268843086f48e25f Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 29 Apr 2026 13:40:38 -0700 Subject: [PATCH 1/2] Update end of deploy handling to print the urls which a user would invoke Signed-off-by: trangevi --- .../internal/project/service_target_agent.go | 138 +++++++++++----- .../project/service_target_agent_test.go | 150 ++++++++++++++++++ 2 files changed, 250 insertions(+), 38 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index 7156cbd985e..30902f8acd6 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -30,6 +30,9 @@ import ( // Reference implementation +// agentAPIVersion is the API version used for agent endpoint invocation URLs. +const agentAPIVersion = "2025-11-15-preview" + // Ensure AgentServiceTargetProvider implements ServiceTargetProvider interface var _ azdext.ServiceTargetProvider = &AgentServiceTargetProvider{} @@ -231,9 +234,16 @@ func (p *AgentServiceTargetProvider) Endpoints( ) } - endpoint := p.agentEndpoint(azdEnv["AZURE_AI_PROJECT_ENDPOINT"], azdEnv[agentNameKey], azdEnv[agentVersionKey]) + // Collect per-protocol endpoint env vars + var endpoints []string + for _, suffix := range []string{"RESPONSES", "INVOCATIONS"} { + key := fmt.Sprintf("AGENT_%s_%s_ENDPOINT", serviceKey, suffix) + if val := azdEnv[key]; val != "" { + endpoints = append(endpoints, val) + } + } - return []string{endpoint}, nil + return endpoints, nil } // GetTargetResource returns a custom target resource for the agent service @@ -566,8 +576,11 @@ func (p *AgentServiceTargetProvider) deployPromptAgent( return nil, err } - // Register agent info in environment - err = p.registerAgentEnvironmentVariables(ctx, azdEnv, serviceConfig, agentVersionResponse) + // Register agent info in environment (prompt agents use the responses protocol) + promptProtocols := []agent_yaml.ProtocolVersionRecord{ + {Protocol: string(agent_api.AgentProtocolResponses), Version: "1.0.0"}, + } + err = p.registerAgentEnvironmentVariables(ctx, azdEnv, serviceConfig, agentVersionResponse, promptProtocols) if err != nil { return nil, err } @@ -579,6 +592,7 @@ func (p *AgentServiceTargetProvider) deployPromptAgent( agentVersionResponse.Version, azdEnv["AZURE_AI_PROJECT_ID"], azdEnv["AZURE_AI_PROJECT_ENDPOINT"], + promptProtocols, ) return &azdext.ServiceDeployResult{ @@ -586,7 +600,7 @@ func (p *AgentServiceTargetProvider) deployPromptAgent( }, nil } -// deployHostedAgent handles deployment of hosted container agents + func (p *AgentServiceTargetProvider) deployHostedAgent( ctx context.Context, serviceConfig *azdext.ServiceConfig, @@ -691,7 +705,7 @@ func (p *AgentServiceTargetProvider) deployHostedAgent( // Register agent info in environment progress("Registering agent environment variables") - err = p.registerAgentEnvironmentVariables(ctx, azdEnv, serviceConfig, agentVersionResponse) + err = p.registerAgentEnvironmentVariables(ctx, azdEnv, serviceConfig, agentVersionResponse, agentDef.Protocols) if err != nil { return nil, err } @@ -701,6 +715,7 @@ func (p *AgentServiceTargetProvider) deployHostedAgent( agentVersionResponse.Version, azdEnv["AZURE_AI_PROJECT_ID"], azdEnv["AZURE_AI_PROJECT_ENDPOINT"], + agentDef.Protocols, ) return &azdext.ServiceDeployResult{ @@ -708,12 +723,14 @@ func (p *AgentServiceTargetProvider) deployHostedAgent( }, nil } -// deployArtifacts constructs the artifacts list for deployment results +// deployArtifacts constructs the artifacts list for deployment results. +// It produces one endpoint artifact per displayable protocol. func (p *AgentServiceTargetProvider) deployArtifacts( agentName string, agentVersion string, projectResourceID string, projectEndpoint string, + protocols []agent_yaml.ProtocolVersionRecord, ) []*azdext.Artifact { artifacts := []*azdext.Artifact{} @@ -734,30 +751,74 @@ func (p *AgentServiceTargetProvider) deployArtifacts( } } - // Add agent endpoint + // Add agent endpoint(s) — one per displayable protocol if projectEndpoint != "" { - agentEndpoint := p.agentEndpoint(projectEndpoint, agentName, agentVersion) - artifacts = append(artifacts, &azdext.Artifact{ - Kind: azdext.ArtifactKind_ARTIFACT_KIND_ENDPOINT, - Location: agentEndpoint, - LocationKind: azdext.LocationKind_LOCATION_KIND_REMOTE, - Metadata: map[string]string{ - "agentName": agentName, - "agentVersion": agentVersion, - "label": "Agent endpoint", - "clickable": "false", - "note": "For information on invoking the agent, see " + output.WithLinkFormat( - "https://aka.ms/azd-agents-invoke"), - }, - }) + endpoints := agentInvocationEndpoints(projectEndpoint, agentName, protocols) + for _, ep := range endpoints { + artifacts = append(artifacts, &azdext.Artifact{ + Kind: azdext.ArtifactKind_ARTIFACT_KIND_ENDPOINT, + Location: ep.URL, + LocationKind: azdext.LocationKind_LOCATION_KIND_REMOTE, + Metadata: map[string]string{ + "agentName": agentName, + "agentVersion": agentVersion, + "label": fmt.Sprintf("Agent endpoint (%s)", ep.Protocol), + "clickable": "false", + }, + }) + } + + // Attach the informational note to the last endpoint only, to avoid repetition. + if len(endpoints) > 0 { + last := artifacts[len(artifacts)-1] + last.Metadata["note"] = "For information on invoking the agent, see " + output.WithLinkFormat( + "https://aka.ms/azd-agents-invoke") + } } return artifacts } -// agentEndpoint constructs the agent endpoint URL from the provided parameters -func (p *AgentServiceTargetProvider) agentEndpoint(projectEndpoint, agentName, agentVersion string) string { - return fmt.Sprintf("%s/agents/%s/versions/%s", projectEndpoint, agentName, agentVersion) +// protocolEndpointInfo holds a displayable protocol label and its invocation URL. +type protocolEndpointInfo struct { + Protocol string + URL string +} + +// protocolPath maps an agent protocol to its URL path suffix. +// Returns empty string for protocols that should not be displayed. +func protocolPath(protocol string) string { + switch agent_api.AgentProtocol(protocol) { + case agent_api.AgentProtocolResponses: + return "openai/responses" + case agent_api.AgentProtocolInvocations: + return "invocations" + default: + return "" + } +} + +// agentInvocationEndpoints builds the list of displayable invocation endpoints +// from the agent's protocols. +func agentInvocationEndpoints( + projectEndpoint string, + agentName string, + protocols []agent_yaml.ProtocolVersionRecord, +) []protocolEndpointInfo { + var endpoints []protocolEndpointInfo + for _, p := range protocols { + path := protocolPath(p.Protocol) + if path == "" { + continue + } + endpoints = append(endpoints, protocolEndpointInfo{ + Protocol: p.Protocol, + URL: fmt.Sprintf( + "%s/agents/%s/endpoint/protocols/%s?api-version=%s", + projectEndpoint, agentName, path, agentAPIVersion), + }) + } + return endpoints } // agentPlaygroundUrl constructs a URL to the agent playground in the Foundry portal @@ -800,9 +861,6 @@ func (p *AgentServiceTargetProvider) createAgent( p.credential, ) - // Use constant API version - const apiVersion = "2025-11-15-preview" - // Extract CreateAgentVersionRequest from CreateAgentRequest versionRequest := &agent_api.CreateAgentVersionRequest{ Description: request.Description, @@ -811,7 +869,7 @@ func (p *AgentServiceTargetProvider) createAgent( } // Create agent version - agentVersionResponse, err := agentClient.CreateAgentVersion(ctx, request.Name, versionRequest, apiVersion) + agentVersionResponse, err := agentClient.CreateAgentVersion(ctx, request.Name, versionRequest, agentAPIVersion) if err != nil { return nil, exterrors.ServiceFromAzure(err, exterrors.OpCreateAgent) } @@ -855,25 +913,29 @@ func (p *AgentServiceTargetProvider) displayAgentInfo(request *agent_api.CreateA fmt.Fprintln(os.Stderr) } -// registerAgentEnvironmentVariables registers agent information as azd environment variables +// registerAgentEnvironmentVariables registers agent information as azd environment variables. +// Per-protocol endpoint vars are set (e.g. AGENT_{KEY}_RESPONSES_ENDPOINT). func (p *AgentServiceTargetProvider) registerAgentEnvironmentVariables( ctx context.Context, azdEnv map[string]string, serviceConfig *azdext.ServiceConfig, agentVersionResponse *agent_api.AgentVersionObject, + protocols []agent_yaml.ProtocolVersionRecord, ) error { + serviceKey := p.getServiceKey(serviceConfig.Name) + envVars := map[string]string{ + fmt.Sprintf("AGENT_%s_NAME", serviceKey): agentVersionResponse.Name, + fmt.Sprintf("AGENT_%s_VERSION", serviceKey): agentVersionResponse.Version, + } - endpoint := p.agentEndpoint( + endpoints := agentInvocationEndpoints( azdEnv["AZURE_AI_PROJECT_ENDPOINT"], agentVersionResponse.Name, - agentVersionResponse.Version, + protocols, ) - - serviceKey := p.getServiceKey(serviceConfig.Name) - envVars := map[string]string{ - fmt.Sprintf("AGENT_%s_NAME", serviceKey): agentVersionResponse.Name, - fmt.Sprintf("AGENT_%s_VERSION", serviceKey): agentVersionResponse.Version, - fmt.Sprintf("AGENT_%s_ENDPOINT", serviceKey): endpoint, + for _, ep := range endpoints { + key := fmt.Sprintf("AGENT_%s_%s_ENDPOINT", serviceKey, strings.ToUpper(ep.Protocol)) + envVars[key] = ep.URL } for key, value := range envVars { diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go index 95ba91aedd3..8d660e572f0 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go @@ -11,6 +11,7 @@ import ( "testing" "azureaiagent/internal/pkg/agents/agent_api" + "azureaiagent/internal/pkg/agents/agent_yaml" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/stretchr/testify/require" @@ -148,6 +149,155 @@ func newContainerTestClient(t *testing.T, containerSrv azdext.ContainerServiceSe return client } +func TestProtocolPath(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + protocol string + expected string + }{ + {"responses", "responses", "openai/responses"}, + {"invocations", "invocations", "invocations"}, + {"activity_protocol excluded", "activity_protocol", ""}, + {"unknown excluded", "unknown_proto", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := protocolPath(tt.protocol) + require.Equal(t, tt.expected, got) + }) + } +} + +func TestAgentInvocationEndpoints(t *testing.T) { + t.Parallel() + + const endpoint = "https://myproject.services.ai.azure.com" + const agentName = "my-agent" + + tests := []struct { + name string + protocols []agent_yaml.ProtocolVersionRecord + expected []protocolEndpointInfo + }{ + { + name: "single responses protocol", + protocols: []agent_yaml.ProtocolVersionRecord{ + {Protocol: "responses", Version: "1.0.0"}, + }, + expected: []protocolEndpointInfo{ + { + Protocol: "responses", + URL: endpoint + "/agents/my-agent/endpoint/protocols/openai/responses?api-version=" + agentAPIVersion, + }, + }, + }, + { + name: "single invocations protocol", + protocols: []agent_yaml.ProtocolVersionRecord{ + {Protocol: "invocations", Version: "1.0.0"}, + }, + expected: []protocolEndpointInfo{ + { + Protocol: "invocations", + URL: endpoint + "/agents/my-agent/endpoint/protocols/invocations?api-version=" + agentAPIVersion, + }, + }, + }, + { + name: "multiple protocols with activity_protocol excluded", + protocols: []agent_yaml.ProtocolVersionRecord{ + {Protocol: "responses", Version: "1.0.0"}, + {Protocol: "activity_protocol", Version: "1.0.0"}, + {Protocol: "invocations", Version: "1.0.0"}, + }, + expected: []protocolEndpointInfo{ + { + Protocol: "responses", + URL: endpoint + "/agents/my-agent/endpoint/protocols/openai/responses?api-version=" + agentAPIVersion, + }, + { + Protocol: "invocations", + URL: endpoint + "/agents/my-agent/endpoint/protocols/invocations?api-version=" + agentAPIVersion, + }, + }, + }, + { + name: "only activity_protocol yields empty", + protocols: []agent_yaml.ProtocolVersionRecord{ + {Protocol: "activity_protocol", Version: "1.0.0"}, + }, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := agentInvocationEndpoints(endpoint, agentName, tt.protocols) + require.Equal(t, tt.expected, got) + }) + } +} + +func TestDeployArtifacts_HostedAgent_ProtocolEndpoints(t *testing.T) { + t.Parallel() + + p := &AgentServiceTargetProvider{} + + protocols := []agent_yaml.ProtocolVersionRecord{ + {Protocol: "responses", Version: "1.0.0"}, + {Protocol: "invocations", Version: "1.0.0"}, + } + + artifacts := p.deployArtifacts( + "test-agent", "1.0.0", + "", // no project resource ID — skip playground + "https://myproject.services.ai.azure.com", + protocols, + ) + + // Should have 2 endpoint artifacts (one per displayable protocol) + require.Len(t, artifacts, 2) + + require.Equal(t, + "https://myproject.services.ai.azure.com/agents/test-agent/endpoint/protocols/openai/responses?api-version="+agentAPIVersion, + artifacts[0].Location) + require.Equal(t, "Agent endpoint (responses)", artifacts[0].Metadata["label"]) + require.Empty(t, artifacts[0].Metadata["note"], "note should only appear on the last endpoint") + + require.Equal(t, + "https://myproject.services.ai.azure.com/agents/test-agent/endpoint/protocols/invocations?api-version="+agentAPIVersion, + artifacts[1].Location) + require.Equal(t, "Agent endpoint (invocations)", artifacts[1].Metadata["label"]) + require.Contains(t, artifacts[1].Metadata["note"], "invoking the agent") +} + +func TestDeployArtifacts_PromptAgent_ResponsesProtocol(t *testing.T) { + t.Parallel() + + p := &AgentServiceTargetProvider{} + + protocols := []agent_yaml.ProtocolVersionRecord{ + {Protocol: "responses", Version: "1.0.0"}, + } + + artifacts := p.deployArtifacts( + "prompt-agent", "2.0.0", + "", // no project resource ID — skip playground + "https://myproject.services.ai.azure.com", + protocols, + ) + + require.Len(t, artifacts, 1) + require.Equal(t, + "https://myproject.services.ai.azure.com/agents/prompt-agent/endpoint/protocols/openai/responses?api-version="+agentAPIVersion, + artifacts[0].Location) + require.Equal(t, "Agent endpoint (responses)", artifacts[0].Metadata["label"]) + require.Contains(t, artifacts[0].Metadata["note"], "invoking the agent") +} + // TestPackage_NoEarlyFailureWithoutACR is a regression test ensuring that // Package for a hosted agent does not fail early when // AZURE_CONTAINER_REGISTRY_ENDPOINT is unset. The ACR endpoint is resolved From c4aea6ad2401a55bdc822737837c1d3972b0b1ab Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 29 Apr 2026 14:26:57 -0700 Subject: [PATCH 2/2] Pr comments Signed-off-by: trangevi --- .../internal/project/service_target_agent.go | 61 +++++-- .../project/service_target_agent_test.go | 154 ++++++++++++++++-- 2 files changed, 186 insertions(+), 29 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index 30902f8acd6..7767332ae7d 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -33,6 +33,20 @@ import ( // agentAPIVersion is the API version used for agent endpoint invocation URLs. const agentAPIVersion = "2025-11-15-preview" +// displayableProtocolEntry defines a protocol that produces user-visible invocation endpoints. +type displayableProtocolEntry struct { + Protocol agent_api.AgentProtocol + URLPath string // path suffix in the invocation URL + EnvSuffix string // suffix used in AGENT_{KEY}_{SUFFIX}_ENDPOINT env vars +} + +// displayableProtocols is the single source of truth for protocols that produce +// user-facing invocation endpoints and env vars. +var displayableProtocols = []displayableProtocolEntry{ + {Protocol: agent_api.AgentProtocolResponses, URLPath: "openai/responses", EnvSuffix: "RESPONSES"}, + {Protocol: agent_api.AgentProtocolInvocations, URLPath: "invocations", EnvSuffix: "INVOCATIONS"}, +} + // Ensure AgentServiceTargetProvider implements ServiceTargetProvider interface var _ azdext.ServiceTargetProvider = &AgentServiceTargetProvider{} @@ -236,13 +250,21 @@ func (p *AgentServiceTargetProvider) Endpoints( // Collect per-protocol endpoint env vars var endpoints []string - for _, suffix := range []string{"RESPONSES", "INVOCATIONS"} { - key := fmt.Sprintf("AGENT_%s_%s_ENDPOINT", serviceKey, suffix) + for _, dp := range displayableProtocols { + key := fmt.Sprintf("AGENT_%s_%s_ENDPOINT", serviceKey, dp.EnvSuffix) if val := azdEnv[key]; val != "" { endpoints = append(endpoints, val) } } + if len(endpoints) == 0 { + return nil, exterrors.Dependency( + exterrors.CodeMissingAgentEnvVars, + fmt.Sprintf("no agent endpoint variables found for service %s", serviceKey), + "run 'azd deploy' to deploy the agent and set these variables", + ) + } + return endpoints, nil } @@ -600,7 +622,7 @@ func (p *AgentServiceTargetProvider) deployPromptAgent( }, nil } - +// deployHostedAgent deploys a container-based hosted agent to the Foundry service. func (p *AgentServiceTargetProvider) deployHostedAgent( ctx context.Context, serviceConfig *azdext.ServiceConfig, @@ -705,7 +727,16 @@ func (p *AgentServiceTargetProvider) deployHostedAgent( // Register agent info in environment progress("Registering agent environment variables") - err = p.registerAgentEnvironmentVariables(ctx, azdEnv, serviceConfig, agentVersionResponse, agentDef.Protocols) + + // Default to "responses" protocol when none specified in agent.yaml. + protocols := agentDef.Protocols + if len(protocols) == 0 { + protocols = []agent_yaml.ProtocolVersionRecord{ + {Protocol: string(agent_api.AgentProtocolResponses), Version: "1.0.0"}, + } + } + + err = p.registerAgentEnvironmentVariables(ctx, azdEnv, serviceConfig, agentVersionResponse, protocols) if err != nil { return nil, err } @@ -715,7 +746,7 @@ func (p *AgentServiceTargetProvider) deployHostedAgent( agentVersionResponse.Version, azdEnv["AZURE_AI_PROJECT_ID"], azdEnv["AZURE_AI_PROJECT_ENDPOINT"], - agentDef.Protocols, + protocols, ) return &azdext.ServiceDeployResult{ @@ -788,14 +819,12 @@ type protocolEndpointInfo struct { // protocolPath maps an agent protocol to its URL path suffix. // Returns empty string for protocols that should not be displayed. func protocolPath(protocol string) string { - switch agent_api.AgentProtocol(protocol) { - case agent_api.AgentProtocolResponses: - return "openai/responses" - case agent_api.AgentProtocolInvocations: - return "invocations" - default: - return "" + for _, dp := range displayableProtocols { + if agent_api.AgentProtocol(protocol) == dp.Protocol { + return dp.URLPath + } } + return "" } // agentInvocationEndpoints builds the list of displayable invocation endpoints @@ -915,6 +944,7 @@ func (p *AgentServiceTargetProvider) displayAgentInfo(request *agent_api.CreateA // registerAgentEnvironmentVariables registers agent information as azd environment variables. // Per-protocol endpoint vars are set (e.g. AGENT_{KEY}_RESPONSES_ENDPOINT). +// The legacy single-endpoint var (AGENT_{KEY}_ENDPOINT) is cleared to avoid stale data. func (p *AgentServiceTargetProvider) registerAgentEnvironmentVariables( ctx context.Context, azdEnv map[string]string, @@ -928,13 +958,18 @@ func (p *AgentServiceTargetProvider) registerAgentEnvironmentVariables( fmt.Sprintf("AGENT_%s_VERSION", serviceKey): agentVersionResponse.Version, } + // Clear legacy single-endpoint var so upgraded environments don't retain stale URLs. + legacyKey := fmt.Sprintf("AGENT_%s_ENDPOINT", serviceKey) + envVars[legacyKey] = "" + endpoints := agentInvocationEndpoints( azdEnv["AZURE_AI_PROJECT_ENDPOINT"], agentVersionResponse.Name, protocols, ) for _, ep := range endpoints { - key := fmt.Sprintf("AGENT_%s_%s_ENDPOINT", serviceKey, strings.ToUpper(ep.Protocol)) + suffix := strings.ToUpper(ep.Protocol) + key := fmt.Sprintf("AGENT_%s_%s_ENDPOINT", serviceKey, suffix) envVars[key] = ep.URL } diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go index 8d660e572f0..9e2d06e6782 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go @@ -149,6 +149,102 @@ func newContainerTestClient(t *testing.T, containerSrv azdext.ContainerServiceSe return client } +// stubEnvServer records SetValue calls for testing registerAgentEnvironmentVariables. +type stubEnvServer struct { + azdext.UnimplementedEnvironmentServiceServer + values map[string]string +} + +func (s *stubEnvServer) SetValue( + _ context.Context, req *azdext.SetEnvRequest, +) (*azdext.EmptyResponse, error) { + if s.values == nil { + s.values = make(map[string]string) + } + s.values[req.Key] = req.Value + return &azdext.EmptyResponse{}, nil +} + +// newEnvTestClient spins up a gRPC server with the given environment +// service stub and returns an AzdClient connected to it. +func newEnvTestClient( + t *testing.T, envSrv azdext.EnvironmentServiceServer, +) *azdext.AzdClient { + t.Helper() + + srv := grpc.NewServer() + azdext.RegisterEnvironmentServiceServer(srv, envSrv) + + lis, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + go func() { _ = srv.Serve(lis) }() + t.Cleanup(func() { + srv.Stop() + _ = lis.Close() + }) + + client, err := azdext.NewAzdClient( + azdext.WithAddress(lis.Addr().String()), + ) + require.NoError(t, err) + t.Cleanup(func() { client.Close() }) + + return client +} + +func TestRegisterAgentEnvironmentVariables(t *testing.T) { + t.Parallel() + + envStub := &stubEnvServer{} + client := newEnvTestClient(t, envStub) + + provider := &AgentServiceTargetProvider{ + azdClient: client, + env: &azdext.Environment{Name: "test-env"}, + } + + azdEnv := map[string]string{ + "AZURE_AI_PROJECT_ENDPOINT": "https://proj.azure.com", + } + protocols := []agent_yaml.ProtocolVersionRecord{ + {Protocol: "responses", Version: "1.0.0"}, + {Protocol: "invocations", Version: "1.0.0"}, + } + agentVersion := &agent_api.AgentVersionObject{ + Name: "my-agent", + Version: "1.0.0", + } + + err := provider.registerAgentEnvironmentVariables( + t.Context(), azdEnv, + &azdext.ServiceConfig{Name: "my-svc"}, + agentVersion, + protocols, + ) + require.NoError(t, err) + + // Verify per-protocol env vars + require.Contains(t, envStub.values, "AGENT_MY_SVC_NAME") + require.Equal(t, "my-agent", envStub.values["AGENT_MY_SVC_NAME"]) + require.Contains(t, envStub.values, "AGENT_MY_SVC_VERSION") + require.Equal(t, "1.0.0", envStub.values["AGENT_MY_SVC_VERSION"]) + + // Per-protocol endpoints + require.Contains(t, envStub.values, "AGENT_MY_SVC_RESPONSES_ENDPOINT") + require.Contains(t, + envStub.values["AGENT_MY_SVC_RESPONSES_ENDPOINT"], + "/agents/my-agent/endpoint/protocols/openai/responses") + require.Contains(t, envStub.values, "AGENT_MY_SVC_INVOCATIONS_ENDPOINT") + require.Contains(t, + envStub.values["AGENT_MY_SVC_INVOCATIONS_ENDPOINT"], + "/agents/my-agent/endpoint/protocols/invocations") + + // Legacy env var cleared + require.Contains(t, envStub.values, "AGENT_MY_SVC_ENDPOINT") + require.Empty(t, envStub.values["AGENT_MY_SVC_ENDPOINT"]) +} + func TestProtocolPath(t *testing.T) { t.Parallel() @@ -176,6 +272,7 @@ func TestAgentInvocationEndpoints(t *testing.T) { const endpoint = "https://myproject.services.ai.azure.com" const agentName = "my-agent" + baseURL := endpoint + "/agents/" + agentName + "/endpoint/protocols/" tests := []struct { name string @@ -190,7 +287,7 @@ func TestAgentInvocationEndpoints(t *testing.T) { expected: []protocolEndpointInfo{ { Protocol: "responses", - URL: endpoint + "/agents/my-agent/endpoint/protocols/openai/responses?api-version=" + agentAPIVersion, + URL: baseURL + "openai/responses?api-version=" + agentAPIVersion, }, }, }, @@ -202,7 +299,7 @@ func TestAgentInvocationEndpoints(t *testing.T) { expected: []protocolEndpointInfo{ { Protocol: "invocations", - URL: endpoint + "/agents/my-agent/endpoint/protocols/invocations?api-version=" + agentAPIVersion, + URL: baseURL + "invocations?api-version=" + agentAPIVersion, }, }, }, @@ -216,11 +313,11 @@ func TestAgentInvocationEndpoints(t *testing.T) { expected: []protocolEndpointInfo{ { Protocol: "responses", - URL: endpoint + "/agents/my-agent/endpoint/protocols/openai/responses?api-version=" + agentAPIVersion, + URL: baseURL + "openai/responses?api-version=" + agentAPIVersion, }, { Protocol: "invocations", - URL: endpoint + "/agents/my-agent/endpoint/protocols/invocations?api-version=" + agentAPIVersion, + URL: baseURL + "invocations?api-version=" + agentAPIVersion, }, }, }, @@ -231,6 +328,11 @@ func TestAgentInvocationEndpoints(t *testing.T) { }, expected: nil, }, + { + name: "nil protocols yields empty", + protocols: nil, + expected: nil, + }, } for _, tt := range tests { @@ -245,6 +347,7 @@ func TestDeployArtifacts_HostedAgent_ProtocolEndpoints(t *testing.T) { t.Parallel() p := &AgentServiceTargetProvider{} + const ep = "https://myproject.services.ai.azure.com" protocols := []agent_yaml.ProtocolVersionRecord{ {Protocol: "responses", Version: "1.0.0"}, @@ -254,22 +357,25 @@ func TestDeployArtifacts_HostedAgent_ProtocolEndpoints(t *testing.T) { artifacts := p.deployArtifacts( "test-agent", "1.0.0", "", // no project resource ID — skip playground - "https://myproject.services.ai.azure.com", + ep, protocols, ) // Should have 2 endpoint artifacts (one per displayable protocol) require.Len(t, artifacts, 2) - require.Equal(t, - "https://myproject.services.ai.azure.com/agents/test-agent/endpoint/protocols/openai/responses?api-version="+agentAPIVersion, - artifacts[0].Location) + wantResponses := ep + + "/agents/test-agent/endpoint/protocols/openai/responses" + + "?api-version=" + agentAPIVersion + require.Equal(t, wantResponses, artifacts[0].Location) require.Equal(t, "Agent endpoint (responses)", artifacts[0].Metadata["label"]) - require.Empty(t, artifacts[0].Metadata["note"], "note should only appear on the last endpoint") + require.Empty(t, artifacts[0].Metadata["note"], + "note should only appear on the last endpoint") - require.Equal(t, - "https://myproject.services.ai.azure.com/agents/test-agent/endpoint/protocols/invocations?api-version="+agentAPIVersion, - artifacts[1].Location) + wantInvocations := ep + + "/agents/test-agent/endpoint/protocols/invocations" + + "?api-version=" + agentAPIVersion + require.Equal(t, wantInvocations, artifacts[1].Location) require.Equal(t, "Agent endpoint (invocations)", artifacts[1].Metadata["label"]) require.Contains(t, artifacts[1].Metadata["note"], "invoking the agent") } @@ -278,6 +384,7 @@ func TestDeployArtifacts_PromptAgent_ResponsesProtocol(t *testing.T) { t.Parallel() p := &AgentServiceTargetProvider{} + const ep = "https://myproject.services.ai.azure.com" protocols := []agent_yaml.ProtocolVersionRecord{ {Protocol: "responses", Version: "1.0.0"}, @@ -286,18 +393,33 @@ func TestDeployArtifacts_PromptAgent_ResponsesProtocol(t *testing.T) { artifacts := p.deployArtifacts( "prompt-agent", "2.0.0", "", // no project resource ID — skip playground - "https://myproject.services.ai.azure.com", + ep, protocols, ) require.Len(t, artifacts, 1) - require.Equal(t, - "https://myproject.services.ai.azure.com/agents/prompt-agent/endpoint/protocols/openai/responses?api-version="+agentAPIVersion, - artifacts[0].Location) + wantURL := ep + + "/agents/prompt-agent/endpoint/protocols/openai/responses" + + "?api-version=" + agentAPIVersion + require.Equal(t, wantURL, artifacts[0].Location) require.Equal(t, "Agent endpoint (responses)", artifacts[0].Metadata["label"]) require.Contains(t, artifacts[0].Metadata["note"], "invoking the agent") } +func TestDeployArtifacts_EmptyProtocols_NoEndpoints(t *testing.T) { + t.Parallel() + + p := &AgentServiceTargetProvider{} + + // When protocols is empty, no endpoint artifacts are produced. + artifacts := p.deployArtifacts( + "agent", "1.0.0", + "", "https://ep.azure.com", + nil, + ) + require.Empty(t, artifacts) +} + // TestPackage_NoEarlyFailureWithoutACR is a regression test ensuring that // Package for a hosted agent does not fail early when // AZURE_CONTAINER_REGISTRY_ENDPOINT is unset. The ACR endpoint is resolved