Skip to content
Merged
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
13 changes: 10 additions & 3 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -3412,14 +3412,21 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
// Emit the sibling Foundry resource services (project + deployments,
// connections, toolboxes) and wire the agent's uses: to them. A selected
// existing project contributes its endpoint so provision reuses it.
if err := emitResourceServices(
emittedConnections, err := emitResourceServices(
ctx, a.azdClient, a.serviceNameOverride,
projectNameHint(ctx, a.azdClient, a.environment.Name, a.selectedFoundryProject),
a.selectedFoundryProject.Endpoint(),
resourceDeployments, resourceConnections, resourceToolboxes,
); err != nil {
)
if err != nil {
return err
}
recordPendingConnectionProvision(
ctx,
a.azdClient,
a.environment.Name,
emittedConnections,
)

printAgentAddedMessage(agentDef.Name)

Expand Down Expand Up @@ -3491,7 +3498,7 @@ func (a *InitAction) addVoiceAgentToProject(
// project. Voice init emits no deployment/connection/toolbox siblings; managed
// models are service-hosted, and BYOM model deployments are referenced from
// azure.yaml and must already exist.
if err := emitResourceServices(
if _, err := emitResourceServices(
Comment thread
huimiu marked this conversation as resolved.
ctx, a.azdClient, a.serviceNameOverride,
projectNameHint(ctx, a.azdClient, a.environment.Name, a.selectedFoundryProject),
a.selectedFoundryProject.Endpoint(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ func (a *InitFromCodeAction) addToProject(
// Emit the sibling azure.ai.project service carrying the model deployments
// and wire the agent's uses: to it. A selected existing project contributes
// its endpoint so provision reuses it instead of creating a new project.
if err := emitResourceServices(
if _, err := emitResourceServices(
Comment thread
huimiu marked this conversation as resolved.
ctx, a.azdClient, agentServiceName,
projectNameHint(ctx, a.azdClient, a.environment.Name, a.selectedFoundryProject),
a.selectedFoundryProject.Endpoint(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ func TestResolveAfterInit(t *testing.T) {
wantPrimaryHas: "azd provision",
wantTrailing: "azd deploy",
},
{
name: "new connection in existing project → provision",
state: &State{
HasProjectEndpoint: true,
PendingProvisionReasons: []string{"connection"},
},
wantPrimaryHas: "azd provision",
wantTrailing: "azd deploy",
},
{
name: "provision needed with missing Azure context → env set before provision",
state: &State{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cmd
import (
"context"
"fmt"
"log"
"slices"
"strings"

Expand Down Expand Up @@ -45,6 +46,7 @@ const (
pendingReasonModelDeployment = "model_deployment"
pendingReasonACR = "acr"
pendingReasonAppInsights = "app_insights"
pendingReasonConnection = "connection"
)

// parsePendingProvisionReasons splits the comma-separated env-var
Expand Down Expand Up @@ -107,6 +109,31 @@ func addPendingProvisionReason(
})
}

// recordPendingConnectionProvision marks connections that need
// provision after init writes a connection service.
// A signal write failure only produces a warning.
func recordPendingConnectionProvision(
ctx context.Context,
azdClient *azdext.AzdClient,
envName string,
emitted int,
) {
if emitted <= 0 {
return
}
if _, err := addPendingProvisionReason(
ctx,
azdClient,
envName,
pendingReasonConnection,
); err != nil {
log.Printf(
"warning: could not record pending connection provision: %v",
err,
)
}
}

// removePendingProvisionReason drops a reason tag from the
// AI_AGENT_PENDING_PROVISION env var. Idempotent: removing a tag
// that was not present is a no-op (no write performed). Used when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,89 @@ func TestRemovePendingProvisionReason(t *testing.T) {
})
}

func TestRecordPendingConnectionProvision(t *testing.T) {
t.Parallel()

t.Run("emitted writes connection reason", func(t *testing.T) {
t.Parallel()

envServer := &testEnvironmentServiceServer{
environments: map[string]*azdext.Environment{
"test-env": {Name: "test-env"},
},
}
azdClient := newTestAzdClient(t, envServer, &testWorkflowServiceServer{})

recordPendingConnectionProvision(
context.Background(), azdClient, "test-env", 1)
require.Equal(
t,
pendingReasonConnection,
envServer.values["test-env"][pendingProvisionEnvVar],
)
})

t.Run("zero emitted is no-op", func(t *testing.T) {
t.Parallel()

envServer := &testEnvironmentServiceServer{
environments: map[string]*azdext.Environment{
"test-env": {Name: "test-env"},
},
}
azdClient := newTestAzdClient(t, envServer, &testWorkflowServiceServer{})

recordPendingConnectionProvision(
context.Background(), azdClient, "test-env", 0)
_, hit := envServer.values["test-env"][pendingProvisionEnvVar]
require.False(t, hit)
})

t.Run("zero emitted preserves existing reason", func(t *testing.T) {
t.Parallel()

envServer := &testEnvironmentServiceServer{
environments: map[string]*azdext.Environment{
"test-env": {Name: "test-env"},
},
values: map[string]map[string]string{
"test-env": {pendingProvisionEnvVar: "connection"},
},
}
azdClient := newTestAzdClient(t, envServer, &testWorkflowServiceServer{})

recordPendingConnectionProvision(
context.Background(), azdClient, "test-env", 0)
require.Equal(
t,
"connection",
envServer.values["test-env"][pendingProvisionEnvVar],
)
})

t.Run("sorts with existing reasons", func(t *testing.T) {
t.Parallel()

envServer := &testEnvironmentServiceServer{
environments: map[string]*azdext.Environment{
"test-env": {Name: "test-env"},
},
values: map[string]map[string]string{
"test-env": {pendingProvisionEnvVar: "project"},
},
}
azdClient := newTestAzdClient(t, envServer, &testWorkflowServiceServer{})

recordPendingConnectionProvision(
context.Background(), azdClient, "test-env", 2)
require.Equal(
t,
"connection,project",
envServer.values["test-env"][pendingProvisionEnvVar],
)
})
}

func TestClearPendingProvisionReasons(t *testing.T) {
t.Parallel()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ func emitResourceServices(
deployments []project.Deployment,
connections []project.Connection,
toolboxes []project.Toolbox,
) error {
) (int, error) {
var agentUses []string
emittedConnections := 0

// Track every azure.yaml service key we emit so two resource names that
// sanitize to the same key (e.g. "my conn" and "myconn") fail fast instead
Expand Down Expand Up @@ -93,14 +94,14 @@ func emitResourceServices(
Deployments: deployments,
})
if err != nil {
return fmt.Errorf("marshaling project service config: %w", err)
return 0, fmt.Errorf("marshaling project service config: %w", err)
}
projectServiceName := resolveProjectServiceKey(ctx, azdClient, projectName, agentServiceName)
if err := reserveServiceName(usedNames, projectServiceName, "project service"); err != nil {
return err
return 0, err
}
if err := addResourceService(ctx, azdClient, projectServiceName, AiProjectHost, projectCfg, nil); err != nil {
return err
return 0, err
}
agentUses = append(agentUses, projectServiceName)

Expand All @@ -119,16 +120,17 @@ func emitResourceServices(
continue
}
if err := reserveServiceName(usedNames, connName, fmt.Sprintf("connection %q", conn.Name)); err != nil {
return err
return 0, err
}
connCfg, err := project.MarshalStruct(&conn)
if err != nil {
return fmt.Errorf("marshaling connection service %q config: %w", connName, err)
return 0, fmt.Errorf("marshaling connection service %q config: %w", connName, err)
}
if err := addResourceService(ctx, azdClient, connName, AiConnectionHost, connCfg, siblingUses); err != nil {
return err
return 0, err
}
agentUses = append(agentUses, connName)
emittedConnections++
}

for i := range toolboxes {
Expand All @@ -142,26 +144,26 @@ func emitResourceServices(
continue
}
if err := reserveServiceName(usedNames, toolboxName, fmt.Sprintf("toolbox %q", toolbox.Name)); err != nil {
return err
return 0, err
}
toolboxCfg, err := project.MarshalStruct(&toolbox)
if err != nil {
return fmt.Errorf("marshaling toolbox service %q config: %w", toolboxName, err)
return 0, fmt.Errorf("marshaling toolbox service %q config: %w", toolboxName, err)
}
if err := addResourceService(ctx, azdClient, toolboxName, AiToolboxHost, toolboxCfg, siblingUses); err != nil {
return err
return 0, err
}
agentUses = append(agentUses, toolboxName)
}

// Wire the agent service to its resource siblings so azd walks them first.
if len(agentUses) > 0 && agentServiceName != "" {
if err := setServiceUses(ctx, azdClient, agentServiceName, agentUses); err != nil {
return err
return 0, err
}
}

return nil
return emittedConnections, nil
}

// resolveProjectServiceKey picks the azure.yaml service key for the single
Expand Down
Loading
Loading