Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fddc3c4
feat(agents): add code deploy (ZIP upload) support for hosted agents
May 11, 2026
346b972
feat(agents): add code deploy mode to 'azd ai agent init --from-code'
May 11, 2026
5b61016
fix(agents): address CI lint errors and align with spec #164 defaults
May 12, 2026
1e5650b
fix(agents): address Copilot review feedback on code deploy
May 12, 2026
026b598
fix(agents): apply agent_endpoint/agent_card via PatchAgent for code …
May 12, 2026
26c8087
fix(agents): resolve CI lint and cspell errors
May 12, 2026
510ed08
style: use map[string]any instead of map[string]interface{}
May 12, 2026
905d3ac
feat(agents): add code deploy support to template init flow
May 12, 2026
a5463b4
fix(agents): improve deploy mode prompt and clean agent.yaml on mode …
May 12, 2026
550b844
fix(agents): guard configureAcrConnection with skipACR check
May 12, 2026
cb8833c
fix(agents): resolve project path when init runs from subdirectory
May 12, 2026
09a94dc
fix(agents): address PR #8146 review feedback (13 items)
May 13, 2026
10d08a8
fix(agents): add TODO for streaming ZIP in zipDeployRequest (C2)
May 13, 2026
d67863b
merge: resolve conflicts with main (prebuilt image support #8104)
May 13, 2026
7ee295e
fix: remove unused isContainerAgent method and accidental files
May 13, 2026
bae1bd6
chore: remove accidentally committed files
May 13, 2026
50577dc
fix(agents): restrict code deploy to supported regions and fix depRes…
May 13, 2026
437bcb1
fix(agents): deduplicate codeDeployRegions and handle empty AZURE_LOC…
May 13, 2026
04b59be
fix(agents): improve deploy failure diagnostics and rename resource p…
May 13, 2026
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
173 changes: 156 additions & 17 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ func (a *InitFromCodeAction) Run(ctx context.Context) error {
}

// Add the agent to the azd project (azure.yaml) services
if err := a.addToProject(ctx, srcDir, localDefinition.Name); err != nil {
isCodeDeploy := localDefinition.CodeConfiguration != nil
if err := a.addToProject(ctx, srcDir, localDefinition.Name, isCodeDeploy); err != nil {
return fmt.Errorf("failed to add agent to azure.yaml: %w", err)
}

Expand Down Expand Up @@ -468,6 +469,42 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context)
// TODO: Prompt user for agent kind
agentKind := agent_yaml.AgentKindHosted

// Prompt user for deploy mode (container vs code)
deployModeChoices := []*azdext.SelectChoice{
{Label: "Code (ZIP upload - no Docker required)", Value: "code"},
{Label: "Container (Dockerfile + ACR)", Value: "container"},
}

var deployMode string
if a.flags.noPrompt {
deployMode = "code" // default to code deploy
} else {
defaultIdx := int32(0)
deployModeResp, err := a.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
Options: &azdext.SelectOptions{
Message: "How would you like to deploy your agent?",
Choices: deployModeChoices,
SelectedIndex: &defaultIdx,
},
})
if err != nil {
if exterrors.IsCancellation(err) {
return nil, exterrors.Cancelled("deploy mode selection was cancelled")
}
return nil, fmt.Errorf("failed to prompt for deploy mode: %w", err)
}
deployMode = deployModeChoices[*deployModeResp.Value].Value
}

// If code deploy, prompt for code configuration details
var codeConfig *agent_yaml.CodeConfiguration
if deployMode == "code" {
codeConfig, err = a.promptCodeConfiguration(ctx)
if err != nil {
return nil, err
}
}

// Prompt user for supported protocols
protocols, err := promptProtocols(ctx, a.azdClient.Prompt(), a.flags.noPrompt, a.flags.protocols)
if err != nil {
Expand Down Expand Up @@ -587,7 +624,8 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context)
Name: agentName,
Kind: agentKind,
},
Protocols: protocols,
Protocols: protocols,
CodeConfiguration: codeConfig,
}

// Add model resource if a model was selected
Expand Down Expand Up @@ -793,41 +831,53 @@ func (a *InitFromCodeAction) writeDefinitionToSrcDir(definition *agent_yaml.Cont
return definitionPath, nil
}

func (a *InitFromCodeAction) addToProject(ctx context.Context, targetDir string, agentName string) error {
func (a *InitFromCodeAction) addToProject(ctx context.Context, targetDir string, agentName string, isCodeDeploy bool) error {
var agentConfig = project.ServiceTargetAgentConfig{}

agentConfig.Container = &project.ContainerSettings{
Resources: &project.ResourceSettings{
Memory: project.DefaultMemory,
Cpu: project.DefaultCpu,
},
if !isCodeDeploy {
agentConfig.Container = &project.ContainerSettings{
Resources: &project.ResourceSettings{
Memory: project.DefaultMemory,
Cpu: project.DefaultCpu,
},
}
}

agentConfig.Deployments = a.deploymentDetails

// Detect startup command from the project source directory
startupCmd, err := resolveStartupCommandForInit(ctx, a.azdClient, a.projectConfig.Path, targetDir, a.flags.noPrompt)
if err != nil {
return err
// Detect startup command from the project source directory (container mode only)
if !isCodeDeploy {
startupCmd, err := resolveStartupCommandForInit(ctx, a.azdClient, a.projectConfig.Path, targetDir, a.flags.noPrompt)
if err != nil {
return err
}
agentConfig.StartupCommand = startupCmd
}
agentConfig.StartupCommand = startupCmd

var agentConfigStruct *structpb.Struct
var err error
if agentConfigStruct, err = project.MarshalStruct(&agentConfig); err != nil {
return fmt.Errorf("failed to marshal agent config: %w", err)
}

language := "python"
if !isCodeDeploy {
language = "docker"
}

serviceConfig := &azdext.ServiceConfig{
Name: strings.ReplaceAll(agentName, " ", ""),
RelativePath: targetDir,
Host: AiAgentHost,
Language: "docker",
Language: language,
Config: agentConfigStruct,
}

// For hosted (container-based) agents, set remoteBuild to true by default
serviceConfig.Docker = &azdext.DockerProjectOptions{
RemoteBuild: true,
// For hosted container-based agents, set remoteBuild to true by default
if !isCodeDeploy {
serviceConfig.Docker = &azdext.DockerProjectOptions{
RemoteBuild: true,
}
}

req := &azdext.AddServiceRequest{Service: serviceConfig}
Expand All @@ -840,6 +890,95 @@ func (a *InitFromCodeAction) addToProject(ctx context.Context, targetDir string,
return nil
}

// promptCodeConfiguration prompts the user for code deploy configuration settings.
func (a *InitFromCodeAction) promptCodeConfiguration(ctx context.Context) (*agent_yaml.CodeConfiguration, error) {
// Prompt for runtime
runtimeChoices := []*azdext.SelectChoice{
{Label: "Python 3.11", Value: "python_3_11"},
Comment thread
v1212 marked this conversation as resolved.
Outdated
{Label: "Python 3.10", Value: "python_3_10"},
}

var runtime string
if a.flags.noPrompt {
runtime = "python_3_11"
} else {
defaultIdx := int32(0)
runtimeResp, err := a.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
Options: &azdext.SelectOptions{
Message: "Select the runtime for your agent",
Choices: runtimeChoices,
SelectedIndex: &defaultIdx,
},
})
if err != nil {
if exterrors.IsCancellation(err) {
return nil, exterrors.Cancelled("runtime selection was cancelled")
}
return nil, fmt.Errorf("failed to prompt for runtime: %w", err)
}
runtime = runtimeChoices[*runtimeResp.Value].Value
}

// Prompt for entry point
defaultEntryPoint := "main.py"
// Try to detect entry point from common patterns
if _, err := os.Stat("app.py"); err == nil {
Comment thread
v1212 marked this conversation as resolved.
Outdated
defaultEntryPoint = "app.py"
}

var entryPoint string
if a.flags.noPrompt {
entryPoint = defaultEntryPoint
} else {
entryPointResp, err := a.azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{
Options: &azdext.PromptOptions{
Message: "Enter the entry point file for your agent",
Comment thread
v1212 marked this conversation as resolved.
Outdated
DefaultValue: defaultEntryPoint,
},
})
if err != nil {
if exterrors.IsCancellation(err) {
return nil, exterrors.Cancelled("entry point prompt was cancelled")
}
return nil, fmt.Errorf("failed to prompt for entry point: %w", err)
}
entryPoint = entryPointResp.Value
}

// Prompt for dependency resolution
depResChoices := []*azdext.SelectChoice{
{Label: "Remote build (server installs dependencies)", Value: "remote_build"},
{Label: "Bundled (pre-install dependencies locally)", Value: "bundled"},
Comment thread
v1212 marked this conversation as resolved.
Outdated
}

var depResolution string
if a.flags.noPrompt {
depResolution = "remote_build"
} else {
defaultIdx := int32(0)
depResResp, err := a.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
Options: &azdext.SelectOptions{
Message: "How should dependencies be resolved?",
Choices: depResChoices,
SelectedIndex: &defaultIdx,
},
})
if err != nil {
if exterrors.IsCancellation(err) {
return nil, exterrors.Cancelled("dependency resolution selection was cancelled")
}
return nil, fmt.Errorf("failed to prompt for dependency resolution: %w", err)
}
depResolution = depResChoices[*depResResp.Value].Value
}

return &agent_yaml.CodeConfiguration{
Runtime: runtime,
EntryPoint: entryPoint,
DependencyResolution: &depResolution,
}, nil
}

// protocolInfo pairs a protocol name with the default version used when generating agent.yaml.
type protocolInfo struct {
Name string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ const (
CodeCognitiveServicesClientFailed = "cognitiveservices_client_failed"
CodeContainerStartFailed = "container_start_failed"
CodeContainerStartTimeout = "container_start_timeout"
CodeAgentCreateFailed = "agent_create_failed"
)

// Operation names for [ServiceFromAzure] errors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,24 @@ type ImageBasedHostedAgentDefinition struct {
Image string `json:"image"`
}

// CodeConfigurationAPI represents the code_configuration block in the API request
type CodeConfigurationAPI struct {
Runtime string `json:"runtime"`
EntryPoint []string `json:"entry_point"`
DependencyResolution string `json:"dependency_resolution,omitempty"`
}

// CodeBasedHostedAgentDefinition represents a code-deploy hosted agent.
// Uses protocol_versions (not container_protocol_versions).
type CodeBasedHostedAgentDefinition struct {
AgentDefinition
ProtocolVersions []ProtocolVersionRecord `json:"protocol_versions"`
CPU string `json:"cpu"`
Memory string `json:"memory"`
EnvironmentVariables map[string]string `json:"environment_variables,omitempty"`
CodeConfiguration CodeConfigurationAPI `json:"code_configuration"`
Comment thread
trangevi marked this conversation as resolved.
Outdated
Comment thread
v1212 marked this conversation as resolved.
Outdated
}

// CreateAgentVersionRequest represents a request to create an agent version
type CreateAgentVersionRequest struct {
Description *string `json:"description,omitempty"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,114 @@ func (c *AgentClient) CreateAgentVersion(ctx context.Context, agentName string,
return &agentVersion, nil
}

// CreateAgentFromZip creates a hosted agent via multipart/form-data ZIP code deploy.
// POST /agents?api-version=...
func (c *AgentClient) CreateAgentFromZip(
ctx context.Context,
agentName string,
metadata *CreateAgentVersionRequest,
zipData []byte,
sha256Hex string,
apiVersion string,
) (*AgentObject, error) {
reqURL := fmt.Sprintf("%s/agents?api-version=%s", c.endpoint, apiVersion)
// For create, include "name" in the metadata JSON (spec requirement)
createMeta := &CreateAgentRequest{
Name: agentName,
CreateAgentVersionRequest: *metadata,
}
return c.zipDeployRequest(ctx, reqURL, agentName, createMeta, zipData, sha256Hex)
}

// UpdateAgentFromZip updates an existing hosted agent via multipart/form-data ZIP code deploy.
// POST /agents/{name}?api-version=...
func (c *AgentClient) UpdateAgentFromZip(
ctx context.Context,
agentName string,
metadata *CreateAgentVersionRequest,
zipData []byte,
sha256Hex string,
apiVersion string,
) (*AgentObject, error) {
reqURL := fmt.Sprintf("%s/agents/%s?api-version=%s", c.endpoint, agentName, apiVersion)
return c.zipDeployRequest(ctx, reqURL, "", metadata, zipData, sha256Hex)
}

// zipDeployRequest performs the multipart ZIP deploy request (shared by create and update).
func (c *AgentClient) zipDeployRequest(
ctx context.Context,
reqURL string,
agentName string, // if non-empty, sent as x-ms-agent-name header (create only)
metadata any,
zipData []byte,
sha256Hex string,
) (*AgentObject, error) {
Comment thread
v1212 marked this conversation as resolved.
// Build multipart body
var body bytes.Buffer
boundary := "----azd-code-deploy-boundary"

// Part 1: metadata (JSON)
metadataJSON, err := json.Marshal(metadata)
if err != nil {
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
}

body.WriteString("--" + boundary + "\r\n")
body.WriteString("Content-Disposition: form-data; name=\"metadata\"\r\n")
body.WriteString("Content-Type: application/json\r\n\r\n")
body.Write(metadataJSON)
body.WriteString("\r\n")

// Part 2: code (ZIP)
body.WriteString("--" + boundary + "\r\n")
body.WriteString("Content-Disposition: form-data; name=\"code\"; filename=\"agent.zip\"\r\n")
body.WriteString("Content-Type: application/zip\r\n\r\n")
body.Write(zipData)
body.WriteString("\r\n")

// End boundary
body.WriteString("--" + boundary + "--\r\n")

req, err := runtime.NewRequest(ctx, http.MethodPost, reqURL)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

contentType := "multipart/form-data; boundary=" + boundary
if err := req.SetBody(streaming.NopCloser(bytes.NewReader(body.Bytes())), contentType); err != nil {
return nil, fmt.Errorf("failed to set request body: %w", err)
}
Comment thread
v1212 marked this conversation as resolved.

// Required headers
req.Raw().Header.Set("Foundry-Features", "CodeAgents=V1Preview,HostedAgents=V1Preview")
req.Raw().Header.Set("x-ms-code-zip-sha256", sha256Hex)
if agentName != "" {
req.Raw().Header.Set("x-ms-agent-name", agentName)
}

resp, err := c.pipeline.Do(req)
if err != nil {
return nil, fmt.Errorf("HTTP request failed: %w", err)
}
defer resp.Body.Close()

if !runtime.HasStatusCode(resp, http.StatusOK, http.StatusCreated) {
return nil, runtime.NewResponseError(resp)
}

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

var agentObj AgentObject
if err := json.Unmarshal(respBody, &agentObj); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}

return &agentObj, nil
}

// GetAgentVersion retrieves a specific version of an agent
func (c *AgentClient) GetAgentVersion(ctx context.Context, agentName, agentVersion, apiVersion string) (*AgentVersionObject, error) {
url := fmt.Sprintf("%s/agents/%s/versions/%s?api-version=%s", c.endpoint, agentName, agentVersion, apiVersion)
Expand Down
Loading
Loading