Skip to content

refactor: decouple Foundry project selection from model configuration in agent init#8292

Merged
trangevi merged 5 commits into
Azure:mainfrom
v1212:refactor/decouple-project-model-selection
May 21, 2026
Merged

refactor: decouple Foundry project selection from model configuration in agent init#8292
trangevi merged 5 commits into
Azure:mainfrom
v1212:refactor/decouple-project-model-selection

Conversation

@v1212

@v1212 v1212 commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Splits the combined "Deploy new model(s) / Use existing Foundry project" prompt into two distinct sequential steps, making the azd ai agent init UX clearer and more flexible.

Changes

  • Step 1: Project selection — "Use an existing Foundry project" or "Create a new Foundry project"
  • Step 2: Model configuration — Options adapt based on Step 1:
    • Existing project: "Use existing deployment / Deploy from catalog / Skip"
    • New project: "Deploy from catalog / Skip"
  • Bug fix: init_from_code.go now properly sets USE_EXISTING_AI_PROJECT (was never set before)
  • Bug fix: End message now recommends azd up instead of azd deploy when new model deployments need provisioning
  • Fix Deploy new models from catalog doesn't offer to change models for templated manifest #8287: When a manifest specifies a default model, the user can now confirm or choose a different model from the catalog before version/SKU selection

No impact on downstream

The configuration values written to .env and azure.yaml are unchanged — this is purely a UX refactor. azd provision and azd deploy behavior is unaffected.

Addresses feedback from #8265.
Fixes #8287.

Jian Wu added 2 commits May 21, 2026 10:52
Split the combined 'Deploy new model(s) / Use existing Foundry project' prompt
into two distinct steps:
1. Select a Foundry project (existing or new)
2. Configure model(s) with options that adapt based on Step 1

Also fixes:
- init_from_code.go now properly sets USE_EXISTING_AI_PROJECT (was missing)
- End message recommends 'azd up' when new model deployments need provisioning
@github-actions github-actions Bot added the ext-agents azure.ai.agents extension label May 21, 2026
When a manifest template specifies a model (e.g. gpt-4.1-mini), the user
is now prompted to confirm or choose a different model from the catalog
before proceeding to version/SKU/capacity selection. In no-prompt mode,
the manifest model is used without prompting (existing behavior).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the azd ai agent init interactive flow to separate Foundry project selection from model configuration, aiming to make the UX clearer and to allow overriding manifest-specified default models before version/SKU selection.

Changes:

  • Split the init UX into sequential steps: (1) Foundry project selection, then (2) model configuration options that adapt to the project choice.
  • Add a confirmation step when a manifest-specified model exists in the catalog, allowing the user to keep it or select a different catalog model.
  • Update post-init guidance logic (and wire up USE_EXISTING_AI_PROJECT in the code-init path) to better reflect when users should run azd up vs azd deploy.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go Reworks interactive init flow to select Foundry project first, then process models; tweaks next-steps message logic.
cli/azd/extensions/azure.ai.agents/internal/cmd/init_models.go Allows confirming/changing a manifest-specified model before proceeding to deployment version/SKU selection.
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go Applies the same “project first, models second” UX to init-from-code and ensures USE_EXISTING_AI_PROJECT is written.
Comments suppressed due to low confidence (1)

cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:2045

  • The completion message is using len(a.deploymentDetails) == 0 to decide between recommending azd deploy vs azd up, but deploymentDetails is populated for both existing and new model deployments (it always records the selected deployment details). This will incorrectly steer users who selected an existing Foundry project + existing deployment(s) to run azd up even when provisioning isn’t required. Track whether any new resources/deployments need provisioning (or inspect the user’s choice) and base the message on that signal instead of deploymentDetails length.
	}); projectID != nil && projectID.Value != "" && len(a.deploymentDetails) == 0 {
		fmt.Printf("To deploy your agent, use %s.\n",
			color.HiBlueString("azd deploy %s", a.serviceNameOverride))
	} else {
		fmt.Printf(

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/init.go Outdated
Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go
Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go
Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go Outdated

@wbreza wbreza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

The refactor is correctly implemented and backward-compatible. All five branches of the new two-step flow set USE_EXISTING_AI_PROJECT correctly, the .env / azure.yaml contracts are unchanged from pre-PR behavior, the azd up vs azd deploy end-message condition is logically sound, auth flows correctly use UserTenantId, and the #8287 manifest-model fix works in both interactive and --no-prompt modes. Findings below are about consistency and polish, not blocking issues.


🟡 Medium

Missing IsCancellation() check on Step 1 project-selection prompt

  • cli/azd/extensions/azure.ai.agents/internal/cmd/init.go (new Step 1 prompt, ~L81–L89)
  • cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go (new Step 1 prompt, ~L236–L249)

Both new Step 1 prompts wrap errors with exterrors.FromPrompt(...) directly. However, Step 2's model-config prompt in init_from_code.go (~L329) does explicitly check exterrors.IsCancellation(err) first and return exterrors.Cancelled(...). This inconsistency means Ctrl+C at Step 1 will be surfaced/telemetered as a generic prompt failure rather than a clean user cancellation, and the two prompts in the same file behave differently.

Suggested:

resp, err := a.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ /* ... */ })
if err != nil {
    if exterrors.IsCancellation(err) {
        return nil, exterrors.Cancelled("project selection was cancelled")
    }
    return nil, exterrors.FromPrompt(err, "failed to prompt for Foundry project configuration choice")
}

🔵 Low

1. Duplicated Foundry-project-selection block between the two files

  • init.go ~L849–L925 and init_from_code.go ~L230–L299

Roughly 70 lines of near-identical sequence (ensureSubscriptionselectFoundryProject → null-check → setEnvValue → fallback). Future fixes are likely to drift between the two files. Consider extracting a small helper such as promptOrSelectFoundryProject(...) that both callers can use. Non-blocking; would be nice as a follow-up.

2. setEnvValue() errors returned unwrapped

Multiple sites in both files, e.g.:

if err := setEnvValue(ctx, a.azdClient, a.environment.Name, "USE_EXISTING_AI_PROJECT", "true"); err != nil {
    return nil, err
}

If the underlying gRPC call fails, the caller receives a raw error with no context about which env var was being written. Suggest wrapping:

return nil, fmt.Errorf("failed to set USE_EXISTING_AI_PROJECT: %w", err)

3. Error from promptModelFromCatalog in #8287 fix is returned unwrapped

  • init_models.go (new "Choose a different model" branch, ~L452)

When the user picks "Choose a different model" at the manifest-model confirmation prompt, the follow-up promptModelFromCatalog error is returned without context, so it's indistinguishable from a primary model-selection failure. Suggest:

selectedModel, err := a.promptModelFromCatalog(ctx)
if err != nil {
    return nil, fmt.Errorf("failed to select alternative model: %w", err)
}

✅ Explicitly verified clean

  • Backward compatibility — env-var names/values and azure.yaml fields are identical to pre-PR behavior across all five flow branches (existing + use-existing-deployment, existing + deploy-from-catalog, existing + skip, new + deploy-from-catalog, new + skip).
  • End-of-init azd up vs azd deploy message — the new condition projectID != nil && projectID.Value != "" && len(a.deploymentDetails) == 0 is correct; azd deploy only when no new model deployments need provisioning.
  • Auth / securitySubscription.UserTenantId correctly used in credential paths; no new credentials or secrets introduced.
  • #8287 fix — interactive confirm/change works; --no-prompt mode bypasses the prompt and uses the manifest model unchanged, as intended.
  • USE_EXISTING_AI_PROJECT — now set on every branch in both init.go and init_from_code.go, addressing the previously missing set in the init_from_code flow.

Review focused on correctness, backward-compatibility, error handling, security/auth, and tests/UX/maintainability. No critical or high-severity issues found.

…ct-id validation

- Add IsCancellation() check on Step 1 project-selection prompts (init.go, init_from_code.go)
- Wrap setEnvValue errors with context describing which env var failed
- Wrap promptModelFromCatalog error with descriptive context
- Add extractProjectDetails validation for --project-id in init_from_code.go
- Improve error messages when specified foundry project is not found/eligible
@v1212

v1212 commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Code Review Summary

The refactor is correctly implemented and backward-compatible. All five branches of the new two-step flow set USE_EXISTING_AI_PROJECT correctly, the .env / azure.yaml contracts are unchanged from pre-PR behavior, the azd up vs azd deploy end-message condition is logically sound, auth flows correctly use UserTenantId, and the #8287 manifest-model fix works in both interactive and --no-prompt modes. Findings below are about consistency and polish, not blocking issues.

🟡 Medium

Missing IsCancellation() check on Step 1 project-selection prompt

  • cli/azd/extensions/azure.ai.agents/internal/cmd/init.go (new Step 1 prompt, ~L81–L89)
  • cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go (new Step 1 prompt, ~L236–L249)

Both new Step 1 prompts wrap errors with exterrors.FromPrompt(...) directly. However, Step 2's model-config prompt in init_from_code.go (~L329) does explicitly check exterrors.IsCancellation(err) first and return exterrors.Cancelled(...). This inconsistency means Ctrl+C at Step 1 will be surfaced/telemetered as a generic prompt failure rather than a clean user cancellation, and the two prompts in the same file behave differently.

Suggested:

resp, err := a.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ /* ... */ })
if err != nil {
    if exterrors.IsCancellation(err) {
        return nil, exterrors.Cancelled("project selection was cancelled")
    }
    return nil, exterrors.FromPrompt(err, "failed to prompt for Foundry project configuration choice")
}

🔵 Low

1. Duplicated Foundry-project-selection block between the two files

  • init.go ~L849–L925 and init_from_code.go ~L230–L299

Roughly 70 lines of near-identical sequence (ensureSubscriptionselectFoundryProject → null-check → setEnvValue → fallback). Future fixes are likely to drift between the two files. Consider extracting a small helper such as promptOrSelectFoundryProject(...) that both callers can use. Non-blocking; would be nice as a follow-up.

2. setEnvValue() errors returned unwrapped

Multiple sites in both files, e.g.:

if err := setEnvValue(ctx, a.azdClient, a.environment.Name, "USE_EXISTING_AI_PROJECT", "true"); err != nil {
    return nil, err
}

If the underlying gRPC call fails, the caller receives a raw error with no context about which env var was being written. Suggest wrapping:

return nil, fmt.Errorf("failed to set USE_EXISTING_AI_PROJECT: %w", err)

3. Error from promptModelFromCatalog in #8287 fix is returned unwrapped

  • init_models.go (new "Choose a different model" branch, ~L452)

When the user picks "Choose a different model" at the manifest-model confirmation prompt, the follow-up promptModelFromCatalog error is returned without context, so it's indistinguishable from a primary model-selection failure. Suggest:

selectedModel, err := a.promptModelFromCatalog(ctx)
if err != nil {
    return nil, fmt.Errorf("failed to select alternative model: %w", err)
}

✅ Explicitly verified clean

  • Backward compatibility — env-var names/values and azure.yaml fields are identical to pre-PR behavior across all five flow branches (existing + use-existing-deployment, existing + deploy-from-catalog, existing + skip, new + deploy-from-catalog, new + skip).
  • End-of-init azd up vs azd deploy message — the new condition projectID != nil && projectID.Value != "" && len(a.deploymentDetails) == 0 is correct; azd deploy only when no new model deployments need provisioning.
  • Auth / securitySubscription.UserTenantId correctly used in credential paths; no new credentials or secrets introduced.
  • Deploy new models from catalog doesn't offer to change models for templated manifest #8287 fix — interactive confirm/change works; --no-prompt mode bypasses the prompt and uses the manifest model unchanged, as intended.
  • USE_EXISTING_AI_PROJECT — now set on every branch in both init.go and init_from_code.go, addressing the previously missing set in the init_from_code flow.

Review focused on correctness, backward-compatibility, error handling, security/auth, and tests/UX/maintainability. No critical or high-severity issues found.

thanks for the review and comments, quick improvements are made as

  1. Missing IsCancellation() check on Step 1 project-selection prompt
  • added checking on cancellation and returning differently
  1. Duplicated Foundry-project-selection block between the two files
  1. setEnvValue() errors returned unwrapped and 4. Error from promptModelFromCatalog in Deploy new models from catalog doesn't offer to change models for templated manifest #8287 fix is returned unwrapped
  • wrapped errors as suggested

…needs provisioning

The end-of-init message now checks needsProvision to avoid recommending
'azd deploy' when a new model deployment still needs to be provisioned,
aligning with the logic in init.go.
@trangevi

Copy link
Copy Markdown
Member

/check-enforcer override

@trangevi
trangevi enabled auto-merge (squash) May 21, 2026 18:18
@trangevi
trangevi merged commit 3e1e1ae into Azure:main May 21, 2026
26 of 27 checks passed

@wbreza wbreza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review on 56d16a7

Thanks for the quick turnaround on the prior feedback. Status update plus one important correction to my earlier review.


✅ Prior feedback addressed cleanly

  • M1 — IsCancellation() on Step 1 prompts: Fixed in both init.go (~L1195) and init_from_code.go (~L315). Both Step 1 and Step 2 prompts now have consistent cancellation semantics across the two files. 👍
  • L2 — setEnvValue error wrapping: Wrapped with fmt.Errorf("...: %w", err) at all flagged sites in both files. 👍
  • L3 — promptModelFromCatalog error wrapping: Wrapped via exterrors.FromPrompt(...) in the new helper. 👍
  • L1 — Duplicated project-selection block: Punted as agreed; non-blocking follow-up.

✨ Bonus improvement

The new extractProjectDetails validator for --project-id in init_from_code.go is a nice add. It validates the ARM resource ID format via regex, returns exterrors.Validation with CodeInvalidProjectResourceId and an actionable suggestion that includes the expected format template. Good telemetry posture.


🟠 Correction to my prior review — Copilot bot reviewer was right

I want to retract the "explicitly verified clean" claim I made about the azd up vs azd deploy end-of-init message. On a deeper trace of a.deploymentDetails, the Copilot bot reviewer's concern is correct, and this is a real (likely Medium) bug.

The condition at cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:~2045:

}); projectID != nil && projectID.Value != "" && len(a.deploymentDetails) == 0 {
    // → recommend `azd deploy`
} else {
    // → recommend `azd up`
}

Why this is wrong: a.deploymentDetails is populated by ProcessModels (init_models.go ~L796: deploymentDetails = append(deploymentDetails, *modelDeployment)) for both code paths:

  • Existing-deployment path (init_models.go ~L197–L212): the selected existing Deployment is returned and appended → deploymentDetails non-empty.
  • New-deployment path (init_models.go ~L309–L320): the new Deployment is returned and appended → deploymentDetails non-empty.

So the only way len(a.deploymentDetails) == 0 is true is when the user skipped model configuration entirely. The current logic therefore steers users who selected existing project + existing deployment (no new provisioning needed!) to run azd up — exactly backwards.

Truth table of the four interactive paths:

Project Model config Needs new provisioning? Current message Correct message
Existing Use existing deployment No azd up azd deploy
Existing Deploy from catalog Yes (new model) azd up azd up
Existing Skip No azd deploy azd deploy
New Deploy from catalog Yes (project + model) azd up azd up
New Skip Yes (project) azd up azd up

The first row is the regression — and it's also the most common "existing-everything" UX path where the user just wants to wire up an agent against pre-existing infra.

Suggested fix: Instead of inferring from len(a.deploymentDetails), track an explicit signal of whether any new resource is being introduced. Two options:

  1. Add a boolean a.needsProvisioning bool set to true when:

    • the user chooses "Create a new Foundry project" (Step 1), OR
    • the user chooses "Deploy a new model from the catalog" (Step 2)

    Then condition on if projectID != nil && projectID.Value != "" && !a.needsProvisioning.

  2. Or mark Deployment entries with an IsExisting bool field at creation time in init_models.go (~L197 vs ~L309), and condition on whether deploymentDetails contains any non-existing entries.

Option 1 is simpler and decouples the UX message from the data structure.


Recap

  • M1 / L2 / L3 / extractProjectDetails: ✅ clean
  • L1 duplication: deferred (fine)
  • 🟠 End-of-init message azd up/azd deploy logic: this is a real bug, my prior "verified clean" was incorrect — apologies for the noise. Worth fixing before merge.

No other new findings introduced by 56d16a7.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ext-agents azure.ai.agents extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deploy new models from catalog doesn't offer to change models for templated manifest

4 participants