Skip to content

Commit 874a516

Browse files
Antriksh JainCopilot
andcommitted
feat(extensions/azure.ai.agents): suggest azd provision after init's deploy-new path
User-reported MVP bug: after `azd ai agent init` with "Deploy new model(s) from the catalog", the trailer says `azd deploy` (or `azd ai agent run`) instead of `azd provision`. The deploy-new path needs `azd provision` first to create the Foundry project — running locally or deploying without it cannot succeed. Root cause: the post-init resolver uses `AZURE_AI_PROJECT_ENDPOINT` as a "provision finished" marker. That marker is reliable on a green field, but a stale endpoint value carried over from a prior init run (existing-project path), or from a sibling azd environment that already provisioned, leaves HasProjectEndpoint=true. With no missing infra vars in the post-init agent.yaml, the resolver hits the default branch and suggests `azd ai agent run` — misleading the user into running a local invoke against a project that has not been provisioned. Fix: add an explicit `NeedsAIProjectProvision` signal to nextstep.State. The signal is driven by the existing `USE_EXISTING_AI_PROJECT` env var that init.go already writes: - init.go:976 → "false" when user picked "Deploy new model(s)" - init.go:943 → "true" when user picked an existing Foundry project - init.go:954, 881, 864 → "false" on existing-path fallbacks (no project found, no matching models, etc.) — semantically equivalent to "Deploy new" for the resolver's purposes. assembleState now reads USE_EXISTING_AI_PROJECT alongside AZURE_AI_PROJECT_ENDPOINT and sets NeedsAIProjectProvision=(value=="false"). Only the literal string "false" enables the flag; an unset variable (no prior init) or "true" both leave it false, so existing-path behavior is unchanged. ResolveAfterInit's case 1 (the `azd provision` primary) now fires on NeedsAIProjectProvision OR !HasProjectEndpoint OR MissingInfraVars. This makes the deploy-new override explicit: when the user just committed to creating a new Foundry project, suggest `azd provision` regardless of any stale endpoint value lingering in the env. Trade-off accepted: if the user re-runs `azd ai agent init` AFTER a successful provision (when USE_EXISTING_AI_PROJECT=false persists in the env), they'll see `azd provision` suggested again. Provision is idempotent so this is harmless "false noise" rather than a broken suggestion. A future refinement could use Bicep-output signatures to distinguish post-provision from stale-endpoint, but that is out of scope for the MVP bug fix. Rejected alternatives: - Tristate `*bool` field: gross to use in Go; the boolean default correctly handles "unset" and "true" together. - Clearing AZURE_AI_PROJECT_ENDPOINT at init.go:976: bigger blast radius; affects downstream consumers beyond the resolver. - Detecting "provision has run" via secondary Bicep outputs: too template-specific. Tests: - state_test.go: 4 new sub-cases in TestAssembleState covering env var unset / "true" / "false" / unrecognized value. Existing transport-error tests updated for the new env read (errCount bumped from 2→3 and 3→4 to match the additional read). - resolver_test.go: 2 new sub-cases in TestResolveAfterInit covering NeedsAIProjectProvision=true with stale endpoint (override fires) and =false with endpoint set (legacy heuristic drives — anti-regression case). Pre-flight clean: gofmt, vet, build, full extension test suite green (cmd 16.8s, nextstep 5.7s, doctor 4.5s, agent_api 10.8s, agent_yaml 1.5s, etc.), golangci-lint 0 issues, cspell 0 issues. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ce75211 commit 874a516

5 files changed

Lines changed: 156 additions & 7 deletions

File tree

cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ const (
4040
// are deploy-time landmines: the literal `{{NAME}}` would otherwise
4141
// land in the container. They never reach `azd env set` because the
4242
// value lives in agent.yaml itself, not the azd environment.
43-
// - !HasProjectEndpoint OR MissingInfraVars → `azd provision`
43+
// - NeedsAIProjectProvision OR !HasProjectEndpoint OR MissingInfraVars
44+
// → `azd provision`
4445
// The project endpoint is the canonical "provision finished"
4546
// marker — it is set by `azd provision` as a Bicep output, or by
4647
// `azd ai agent init` when the user selects an existing Foundry
@@ -51,7 +52,13 @@ const (
5152
// directly references any AZURE_* variables. MissingInfraVars is
5253
// still consulted to cover the post-provision re-provision case
5354
// (a new ${AZURE_*} reference was added to agent.yaml after the
54-
// last provision run).
55+
// last provision run). NeedsAIProjectProvision adds an explicit
56+
// override for the deploy-new path: USE_EXISTING_AI_PROJECT=false
57+
// means the user just committed to creating a new Foundry project
58+
// via Bicep, so any AZURE_AI_PROJECT_ENDPOINT carried over from a
59+
// prior init or environment is stale and must not let the resolver
60+
// mistake the state for "ready to run or deploy". See
61+
// state.NeedsAIProjectProvision for the env-var contract.
5562
// - MissingManualVars → one `azd env set <KEY> <value>` per missing var
5663
// (up to maxFixupLines)
5764
// - Otherwise → `azd ai agent run`
@@ -87,7 +94,7 @@ func ResolveAfterInit(state *State) []Suggestion {
8794
}
8895

8996
switch {
90-
case !state.HasProjectEndpoint || len(state.MissingInfraVars) > 0:
97+
case state.NeedsAIProjectProvision || !state.HasProjectEndpoint || len(state.MissingInfraVars) > 0:
9198
out = append(out, Suggestion{
9299
Command: "azd provision",
93100
Description: "set up your Foundry project, models, and connections",

cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,38 @@ func TestResolveAfterInit(t *testing.T) {
5959
wantPrimaryHas: "azd provision",
6060
wantTrailing: "azd deploy",
6161
},
62+
{
63+
// User selected "Deploy new model(s)" in init. The Foundry
64+
// project does not exist yet, but a stale
65+
// AZURE_AI_PROJECT_ENDPOINT carried over from a prior init
66+
// or sibling environment sets HasProjectEndpoint=true.
67+
// Without the explicit NeedsAIProjectProvision signal the
68+
// resolver would default to `azd ai agent run` and
69+
// mislead the user into running a local invoke against a
70+
// project that has not been provisioned.
71+
name: "deploy-new chosen but stale endpoint → provision (override)",
72+
state: &State{
73+
HasProjectEndpoint: true,
74+
NeedsAIProjectProvision: true,
75+
},
76+
wantPrimaryHas: "azd provision",
77+
wantTrailing: "azd deploy",
78+
},
79+
{
80+
// Existing-project init path. USE_EXISTING_AI_PROJECT=true
81+
// leaves NeedsAIProjectProvision=false at state assembly,
82+
// so the legacy heuristic continues to drive: endpoint
83+
// set + no missing vars ⇒ `azd ai agent run`. This case
84+
// locks the no-regression contract for the existing
85+
// path.
86+
name: "existing project chosen, all vars set → run locally (no override)",
87+
state: &State{
88+
HasProjectEndpoint: true,
89+
NeedsAIProjectProvision: false,
90+
},
91+
wantPrimaryHas: "azd ai agent run",
92+
wantTrailing: "azd deploy",
93+
},
6294
}
6395

6496
for _, tt := range tests {

cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ const (
3535
// endpoint URL produced by `azd ai agent init`.
3636
projectEndpointVar = "AZURE_AI_PROJECT_ENDPOINT"
3737

38+
// useExistingAIProjectVar records the user's choice in the
39+
// `azd ai agent init` model-configuration step. "true" means the
40+
// user selected an existing Foundry project (init populated
41+
// AZURE_AI_PROJECT_ENDPOINT and related vars immediately from that
42+
// project); "false" means the user opted to create a new Foundry
43+
// project, which requires `azd provision` to run before any
44+
// AZURE_AI_PROJECT_ENDPOINT value reflects reality. The variable
45+
// also drives Bicep's "skip project creation" branch — see
46+
// USE_EXISTING_AI_PROJECT in CHANGELOG.md entry for PR #7843.
47+
useExistingAIProjectVar = "USE_EXISTING_AI_PROJECT"
48+
3849
// azureInfraPrefix tags an env-var name as an azd-infra output rather
3950
// than a user-supplied manual variable. Outputs of `azd provision`
4051
// in the AI Foundry templates uniformly start with this prefix
@@ -215,6 +226,23 @@ func assembleState(ctx context.Context, src Source, opts ...Option) (*State, []e
215226
errs = append(errs, fmt.Errorf("read %s: %w", projectEndpointVar, err))
216227
}
217228
state.HasProjectEndpoint = endpoint != ""
229+
230+
// USE_EXISTING_AI_PROJECT is the explicit signal `azd ai agent
231+
// init` writes to record the user's deploy-vs-existing choice.
232+
// When the user just selected "Deploy new model(s)" (value
233+
// "false"), the Foundry project does not exist yet — any
234+
// AZURE_AI_PROJECT_ENDPOINT value carried over from a prior
235+
// init run or a sibling environment is stale and must not let
236+
// the post-init resolver mistake the state for "ready to run
237+
// or deploy". The flag is only set for the literal string
238+
// "false"; an unset variable (no init yet) or "true" both
239+
// leave the flag false so existing resolver heuristics drive
240+
// the decision.
241+
useExisting, err := src.EnvValue(ctx, envName, useExistingAIProjectVar)
242+
if err != nil {
243+
errs = append(errs, fmt.Errorf("read %s: %w", useExistingAIProjectVar, err))
244+
}
245+
state.NeedsAIProjectProvision = useExisting == "false"
218246
}
219247

220248
project, err := src.Project(ctx)

cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state_test.go

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,73 @@ func TestAssembleState(t *testing.T) {
155155
require.Len(t, state.Services, 1)
156156
assert.False(t, state.Services[0].IsDeployed)
157157
assert.False(t, state.HasProjectEndpoint)
158+
assert.False(t, state.NeedsAIProjectProvision)
159+
},
160+
// One error for AZURE_AI_PROJECT_ENDPOINT + one for USE_EXISTING_AI_PROJECT
161+
// + one per service lookup (AGENT_ECHO_VERSION) = 3.
162+
errCount: 3,
163+
},
164+
{
165+
name: "USE_EXISTING_AI_PROJECT unset: NeedsAIProjectProvision stays false",
166+
src: &fakeSource{
167+
envName: "dev",
168+
project: &azdext.ProjectConfig{Name: "demo"},
169+
values: map[string]string{"dev/AZURE_AI_PROJECT_ENDPOINT": "https://x.services.ai.azure.com"},
170+
},
171+
assert: func(t *testing.T, state *State, _ []error) {
172+
assert.True(t, state.HasProjectEndpoint)
173+
assert.False(t, state.NeedsAIProjectProvision)
174+
},
175+
},
176+
{
177+
name: "USE_EXISTING_AI_PROJECT=true: existing-project path, NeedsAIProjectProvision stays false",
178+
src: &fakeSource{
179+
envName: "dev",
180+
project: &azdext.ProjectConfig{Name: "demo"},
181+
values: map[string]string{
182+
"dev/AZURE_AI_PROJECT_ENDPOINT": "https://x.services.ai.azure.com",
183+
"dev/USE_EXISTING_AI_PROJECT": "true",
184+
},
185+
},
186+
assert: func(t *testing.T, state *State, _ []error) {
187+
assert.True(t, state.HasProjectEndpoint)
188+
assert.False(t, state.NeedsAIProjectProvision)
189+
},
190+
},
191+
{
192+
name: "USE_EXISTING_AI_PROJECT=false: deploy-new path, NeedsAIProjectProvision is true",
193+
src: &fakeSource{
194+
envName: "dev",
195+
project: &azdext.ProjectConfig{Name: "demo"},
196+
values: map[string]string{
197+
// Stale endpoint from a prior init carried over. The
198+
// NeedsAIProjectProvision flag is the explicit signal
199+
// the resolver needs to suggest `azd provision`
200+
// despite the endpoint check independently passing.
201+
"dev/AZURE_AI_PROJECT_ENDPOINT": "https://stale.services.ai.azure.com",
202+
"dev/USE_EXISTING_AI_PROJECT": "false",
203+
},
204+
},
205+
assert: func(t *testing.T, state *State, _ []error) {
206+
assert.True(t, state.HasProjectEndpoint)
207+
assert.True(t, state.NeedsAIProjectProvision)
208+
},
209+
},
210+
{
211+
name: "USE_EXISTING_AI_PROJECT unrecognized value: NeedsAIProjectProvision stays false",
212+
src: &fakeSource{
213+
envName: "dev",
214+
project: &azdext.ProjectConfig{Name: "demo"},
215+
values: map[string]string{
216+
"dev/AZURE_AI_PROJECT_ENDPOINT": "https://x.services.ai.azure.com",
217+
"dev/USE_EXISTING_AI_PROJECT": "maybe",
218+
},
219+
},
220+
assert: func(t *testing.T, state *State, _ []error) {
221+
assert.True(t, state.HasProjectEndpoint)
222+
// Only literal "false" enables the flag.
223+
assert.False(t, state.NeedsAIProjectProvision)
158224
},
159-
// One error for AZURE_AI_PROJECT_ENDPOINT + one per service lookup = 2.
160-
errCount: 2,
161225
},
162226
}
163227

@@ -834,8 +898,9 @@ environment_variables:
834898
}
835899

836900
state, errs := assembleState(context.Background(), src)
837-
// One error for AZURE_AI_PROJECT_ENDPOINT + AGENT_ECHO_VERSION + MY_API_KEY.
838-
assert.Len(t, errs, 3)
901+
// One error each for AZURE_AI_PROJECT_ENDPOINT + USE_EXISTING_AI_PROJECT
902+
// + AGENT_ECHO_VERSION + MY_API_KEY.
903+
assert.Len(t, errs, 4)
839904
assert.Empty(t, state.MissingInfraVars)
840905
assert.Empty(t, state.MissingManualVars)
841906
}

cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/types.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ type State struct {
6565
// (and non-empty) in the active azd environment.
6666
HasProjectEndpoint bool
6767

68+
// NeedsAIProjectProvision is true when `azd ai agent init` recorded
69+
// `USE_EXISTING_AI_PROJECT=false` — i.e., the user selected
70+
// "Deploy new model(s)" rather than picking an existing Foundry
71+
// project. In that mode the Foundry project does not yet exist and
72+
// `azd provision` is required before `azd ai agent run` or
73+
// `azd deploy` can succeed. The flag exists alongside
74+
// HasProjectEndpoint because a stale AZURE_AI_PROJECT_ENDPOINT
75+
// from a prior init or a sibling environment can otherwise satisfy
76+
// the existing "endpoint set ⇒ provisioned" check and mislead the
77+
// post-init trailer into recommending `azd ai agent run`. Treat
78+
// this flag as an OR-contributor to "needs provision" in
79+
// resolvers: when true, suggest `azd provision` even if the
80+
// endpoint check independently passes. The flag is false when the
81+
// variable is unset (no prior init) or "true" (existing path) so
82+
// the existing heuristic continues to drive those cases.
83+
NeedsAIProjectProvision bool
84+
6885
// MissingInfraVars names ${...} references in agent.yaml that map to
6986
// Bicep outputs not yet present in the azd environment (i.e.,
7087
// provision is needed or has been skipped). Named so the resolver can

0 commit comments

Comments
 (0)