Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package cmd
import (
"context"
"fmt"
"os"
"strconv"

"azureaiagent/internal/pkg/agents/agent_api"

Expand All @@ -17,6 +19,9 @@ import (
// DefaultAgentAPIVersion is the default API version for agent operations.
const DefaultAgentAPIVersion = "2025-05-15-preview"

// VNextAgentAPIVersion is the API version for VNext (ADC) hosted agent operations.
const VNextAgentAPIVersion = "2025-11-15-preview"

// AgentContext holds the common properties of a hosted agent.
type AgentContext struct {
ProjectEndpoint string
Expand Down Expand Up @@ -105,3 +110,29 @@ func newAgentCredential() (azcore.TokenCredential, error) {
}
return credential, nil
}

// resolveAgentAPIVersion returns the appropriate API version based on the enableHostedAgentVNext setting.
// Checks the azd environment first, then falls back to the OS environment variable.
func resolveAgentAPIVersion(ctx context.Context, azdClient *azdext.AzdClient) string {
// Try azd environment first
envResponse, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{})
if err == nil && envResponse.Environment != nil {
v, err := azdClient.Environment().GetValue(ctx, &azdext.GetEnvRequest{
EnvName: envResponse.Environment.Name,
Key: "enableHostedAgentVNext",
})
if err == nil && v.Value != "" {
if enabled, err := strconv.ParseBool(v.Value); err == nil && enabled {
return VNextAgentAPIVersion
}
}
}

// Fall back to OS environment variable
vnextValue := os.Getenv("enableHostedAgentVNext")
if enabled, err := strconv.ParseBool(vnextValue); err == nil && enabled {
return VNextAgentAPIVersion
}

return DefaultAgentAPIVersion
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func resolveConversationID(
forceNew bool,
endpoint string,
bearerToken string,
apiVersion string,
) (string, error) {
if explicit != "" {
return explicit, nil
Expand All @@ -138,7 +139,7 @@ func resolveConversationID(
}

// Create and persist a new conversation for multi-turn memory.
newConvID, err := createConversation(ctx, endpoint, bearerToken)
newConvID, err := createConversation(ctx, endpoint, bearerToken, apiVersion)
if err != nil {
return "", fmt.Errorf("failed to create conversation: %w", err)
}
Expand Down
11 changes: 8 additions & 3 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,17 @@ func (a *InvokeAction) invokeRemote(ctx context.Context) error {

msg := a.flags.message

// Determine API version based on VNext flag
apiVersion := resolveAgentAPIVersion(ctx, azdClient)

// Build request body — uses streaming to receive the full agent response.
body := map[string]interface{}{
"input": msg,
"agent": map[string]string{
"name": name,
"type": "agent_reference",
},
"store": true,
"stream": true,
}

Expand Down Expand Up @@ -228,6 +232,7 @@ func (a *InvokeAction) invokeRemote(ctx context.Context) error {
a.flags.newConversation,
endpoint,
token.Token,
apiVersion,
)
if err != nil {
return err
Expand All @@ -245,7 +250,7 @@ func (a *InvokeAction) invokeRemote(ctx context.Context) error {
return fmt.Errorf("failed to marshal request: %w", err)
}

url := fmt.Sprintf("%s/openai/responses?api-version=%s", endpoint, DefaultAgentAPIVersion)
url := fmt.Sprintf("%s/openai/responses?api-version=%s", endpoint, apiVersion)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
Expand Down Expand Up @@ -275,8 +280,8 @@ func (a *InvokeAction) invokeRemote(ctx context.Context) error {
}

// createConversation creates a new Foundry conversation for multi-turn memory.
func createConversation(ctx context.Context, endpoint string, bearerToken string) (string, error) {
url := fmt.Sprintf("%s/openai/conversations?api-version=%s", endpoint, DefaultAgentAPIVersion)
func createConversation(ctx context.Context, endpoint string, bearerToken string, apiVersion string) (string, error) {
url := fmt.Sprintf("%s/openai/conversations?api-version=%s", endpoint, apiVersion)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader([]byte("{}")))
if err != nil {
return "", err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,35 @@ type UpdateAgentRequest struct {
CreateAgentVersionRequest
}

// AgentVersionStatus represents the provisioning status of an agent version (VNext)
type AgentVersionStatus string

const (
AgentVersionStatusCreating AgentVersionStatus = "creating"
AgentVersionStatusActive AgentVersionStatus = "active"
AgentVersionStatusFailed AgentVersionStatus = "failed"
AgentVersionStatusDeleting AgentVersionStatus = "deleting"
AgentVersionStatusDeleted AgentVersionStatus = "deleted"
)

// AgentVersionError represents error details when an agent version fails provisioning
type AgentVersionError struct {
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}

// AgentVersionObject represents an agent version
type AgentVersionObject struct {
Object string `json:"object"`
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Description *string `json:"description,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt int64 `json:"created_at"`
Definition interface{} `json:"definition"` // Can be any of the agent definition types
Object string `json:"object"`
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Status AgentVersionStatus `json:"status,omitempty"`
Description *string `json:"description,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt int64 `json:"created_at"`
Definition interface{} `json:"definition"` // Can be any of the agent definition types
Error *AgentVersionError `json:"error,omitempty"`
}

// AgentObject represents an agent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package project
import (
"context"
"encoding/base64"
"errors"
"fmt"
"math"
"os"
Expand All @@ -19,6 +20,7 @@ import (
"azureaiagent/internal/pkg/agents/agent_yaml"
"azureaiagent/internal/pkg/azure"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2"
Expand Down Expand Up @@ -680,6 +682,111 @@ func (p *AgentServiceTargetProvider) deployHostedAgent(
// Display agent information
p.displayAgentInfo(request)

vnextEnabled := isVnextEnabled(azdEnv)

if vnextEnabled {
// VNext (ADC) flow: auto-provisioning via version status polling
return p.deployHostedAgentVNext(ctx, serviceConfig, progress, request, azdEnv)
}

// Legacy flow: explicit container start/stop
return p.deployHostedAgentLegacy(ctx, serviceConfig, foundryAgentConfig, progress, request, azdEnv)
}

// deployHostedAgentVNext handles VNext (ADC) deployment where provisioning is automatic.
// Creates the agent or a new version, then polls the version status until active.
func (p *AgentServiceTargetProvider) deployHostedAgentVNext(
ctx context.Context,
serviceConfig *azdext.ServiceConfig,
progress azdext.ProgressReporter,
request *agent_api.CreateAgentRequest,
azdEnv map[string]string,
) (*azdext.ServiceDeployResult, error) {
const apiVersion = "2025-11-15-preview"

agentClient := agent_api.NewAgentClient(
azdEnv["AZURE_AI_PROJECT_ENDPOINT"],
p.credential,
)

// Check if agent already exists (tolerate 404)
progress("Checking if agent exists")
existing, err := agentClient.GetAgent(ctx, request.Name, apiVersion)

var agentVersionResponse *agent_api.AgentVersionObject

if err != nil {
// Check if this is a 404 (agent not found)
var respErr *azcore.ResponseError
if errors.As(err, &respErr) && respErr.StatusCode == 404 {
// Agent does not exist — create it (POST /agents)
progress("Creating agent")
fmt.Fprintf(os.Stderr, "Agent '%s' does not exist, creating...\n", request.Name)

agentObj, createErr := agentClient.CreateAgent(ctx, request, apiVersion)
if createErr != nil {
return nil, exterrors.ServiceFromAzure(createErr, exterrors.OpCreateAgent)
}

agentVersionResponse = &agentObj.Versions.Latest
fmt.Fprintf(os.Stderr, "Agent '%s' created (version %s)\n", agentObj.Name, agentVersionResponse.Version)
} else {
return nil, exterrors.ServiceFromAzure(err, "check-agent-exists")
}
} else {
// Agent exists — create a new version (POST /agents/{name}/versions)
prevVersion := existing.Versions.Latest.Version
fmt.Fprintf(os.Stderr, "Agent '%s' exists (version %s), deploying new version...\n", request.Name, prevVersion)

progress("Creating new agent version")
versionRequest := &agent_api.CreateAgentVersionRequest{
Description: request.Description,
Metadata: request.Metadata,
Definition: request.Definition,
}
agentVersionResponse, err = agentClient.CreateAgentVersion(ctx, request.Name, versionRequest, apiVersion)
if err != nil {
return nil, exterrors.ServiceFromAzure(err, exterrors.OpCreateAgent)
}
fmt.Fprintf(os.Stderr, "Agent version '%s' (version %s) created\n",
agentVersionResponse.Name, agentVersionResponse.Version)
}

// Wait for the version to become active (VNext auto-provisions)
progress("Waiting for agent to become active")
err = p.waitForAgentVersionActive(ctx, agentClient, agentVersionResponse.Name, agentVersionResponse.Version, apiVersion)
if err != nil {
return nil, err
}

// Register agent info in environment
progress("Registering agent environment variables")
err = p.registerAgentEnvironmentVariables(ctx, azdEnv, serviceConfig, agentVersionResponse)
if err != nil {
return nil, err
}

artifacts := p.deployArtifacts(
agentVersionResponse.Name,
agentVersionResponse.Version,
azdEnv["AZURE_AI_PROJECT_ID"],
azdEnv["AZURE_AI_PROJECT_ENDPOINT"],
)

return &azdext.ServiceDeployResult{
Artifacts: artifacts,
}, nil
}

// deployHostedAgentLegacy handles the legacy deployment flow with explicit container start/stop.
func (p *AgentServiceTargetProvider) deployHostedAgentLegacy(
ctx context.Context,
serviceConfig *azdext.ServiceConfig,
foundryAgentConfig *ServiceTargetAgentConfig,
progress azdext.ProgressReporter,
request *agent_api.CreateAgentRequest,
azdEnv map[string]string,
) (*azdext.ServiceDeployResult, error) {
// Step 4: Create agent
progress("Creating agent")
agentVersionResponse, err := p.createAgent(ctx, request, azdEnv)
Expand Down Expand Up @@ -1146,3 +1253,69 @@ func applyVnextMetadata(request *agent_api.CreateAgentRequest, azdEnv map[string
request.Metadata["enableVnextExperience"] = "true"
}
}

// isVnextEnabled checks enableHostedAgentVNext from azdEnv then OS env.
func isVnextEnabled(azdEnv map[string]string) bool {
vnextValue := azdEnv["enableHostedAgentVNext"]
if vnextValue == "" {
vnextValue = os.Getenv("enableHostedAgentVNext")
}
enabled, err := strconv.ParseBool(vnextValue)
return err == nil && enabled
}

// waitForAgentVersionActive polls the agent version status until it becomes active or fails.
// VNext auto-provisions the agent when a version is created, so we poll the version status
// rather than using explicit container start/stop operations.
func (p *AgentServiceTargetProvider) waitForAgentVersionActive(
ctx context.Context,
agentClient *agent_api.AgentClient,
agentName string,
agentVersion string,
apiVersion string,
) error {
const (
pollInterval = 5 * time.Second
maxIterations = 36 // ~3 minutes
)

fmt.Fprintf(os.Stderr, "Waiting for agent '%s' version '%s' to become active...\n", agentName, agentVersion)

for i := 0; i < maxIterations; i++ {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(pollInterval):
}

ver, err := agentClient.GetAgentVersion(ctx, agentName, agentVersion, apiVersion)
if err != nil {
return exterrors.ServiceFromAzure(err, "poll-agent-version-status")
}

switch ver.Status {
case agent_api.AgentVersionStatusActive:
fmt.Fprintf(os.Stderr, "Agent '%s' version '%s' is active and ready\n", agentName, agentVersion)
return nil
case agent_api.AgentVersionStatusFailed:
errorMsg := "provisioning failed"
if ver.Error != nil {
errorMsg = fmt.Sprintf("provisioning failed: [%s] %s", ver.Error.Code, ver.Error.Message)
}
return exterrors.Internal(
exterrors.CodeContainerStartFailed,
fmt.Sprintf("agent '%s' version '%s' %s", agentName, agentVersion, errorMsg),
)
case agent_api.AgentVersionStatusCreating:
fmt.Fprintf(os.Stderr, "Agent version status: %s\n", ver.Status)
default:
fmt.Fprintf(os.Stderr, "Agent version status: %s (unexpected)\n", ver.Status)
}
}

return exterrors.Internal(
exterrors.CodeContainerStartTimeout,
fmt.Sprintf("timeout waiting for agent '%s' version '%s' to become active after %d seconds",
agentName, agentVersion, int(maxIterations*pollInterval/time.Second)),
)
}
Loading
Loading