-
Notifications
You must be signed in to change notification settings - Fork 307
Azure AI Agents: add next-step guidance and doctor diagnostics #8198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
trangevi
merged 82 commits into
Azure:main
from
antriksh30:antrikshjain/next-step-implementation
May 22, 2026
Merged
Changes from all commits
Commits
Show all changes
82 commits
Select commit
Hold shift + click to select a range
287a56d
feat(azure.ai.agents): scaffold nextstep package and isTerminal helper
1f5905d
feat(azure.ai.agents): add AssembleState to nextstep package
f040bdf
fix(azure.ai.agents): scope nextstep state to azure.ai.agent services
2baedae
feat(azure.ai.agents): add nextstep resolver, OpenAPI extractor, and …
fc3928d
fix(azure.ai.agents): apply consensus fixes to nextstep resolver
30eae8e
fix(azure.ai.agents): clarify shellEscapeSingleQuoted doc comment
3f7b6bc
fix(azure.ai.agents): drop stale line reference in shellEscapeSingleQ…
d10f1e0
feat(azure.ai.agents): silent fetchOpenAPISpec + wire cache-only Open…
baeb1fc
feat(azure.ai.agents): detect missing env vars during nextstep state …
e5100c8
fix(azure.ai.agents): exclude defaulted env refs from missing-vars de…
bc33171
feat(azure.ai.agents): wire init success path to nextstep resolver
7d210d2
fix(azure.ai.agents): reserve trailing slot in nextstep renderer for …
ee09a15
fix(azure.ai.agents/nextstep): trailing collision now keeps the most-…
72316b2
feat(azure.ai.agents/run): resolver-driven Next: block on local run
8f1c05a
fix(azure.ai.agents): align local OpenAPI cache key + restore "After …
bac2857
fix(azure.ai.agents): resolve local agent service once in invocations…
5af76df
feat(azure.ai.agents): wire invoke.go success paths to nextstep.Resol…
67fca44
fix(azure.ai.agents): TTY-gate Next: emission in invoke success helper
d153721
feat(azure.ai.agents): wire invoke failure paths to ResolveAfterInvoke
fc0f174
fix(azure.ai.agents): gate invocations-protocol failure Next: by HTTP…
116319b
fix(azure.ai.agents/nextstep): match AgentVersionStatus wire values t…
c6fc8f9
feat(azure.ai.agents/cmd): wire show.go to nextstep resolver
afbc77d
fix(azure.ai.agents/cmd/show): drop dead nil guard + doubled blank line
6f70a87
fix(azure.ai.agents): correctness fixes from 2.6 cross-pollinated review
39de3b6
fix(azure.ai.agents): correct divergent-name invoke suggestion (G3)
72547cd
fix(azure.ai.agents): close symmetric G3 in invoke-success suggestion…
0bb147f
chore(azure.ai.agents): doc + test cleanup from 2.6/2.6.3/2.6.4 reviews
c247378
fix(azure.ai.agents): include api-version on remote OpenAPI spec fetch
eea38c4
feat(azure.ai.agents): add FormatNextForNote renderer for artifact em…
7bae8f1
feat(azure.ai.agents): wire deploy-hook Next: block on host artifact
366a572
fix(azure.ai.agents): scope deploy-hook Next: block to deployed servi…
2dbcb81
feat(azure.ai.agents): scaffold doctor package types and runner
b26f1f0
fix(azure.ai.agents): apply 4.1 consensus review fixes for doctor pac…
f078a20
fix(azure.ai.agents): align doctor types to design spec
46ad791
feat(azure.ai.agents): doctor local checks 1-3 (grpc-extension, azure…
23cda19
fix(azure.ai.agents): doctor local checks — 3-of-3 review fix-ups (tr…
1884c9a
feat(azure.ai.agents): add doctor local checks 4-6 (agent service, pr…
7567546
fix(azure.ai.agents): doctor checks 5-6 — 3-of-3 review fix-ups (skip…
d422395
feat(azure.ai.agents): wire azd ai agent doctor command
f39e0ac
fix(azure.ai.agents): doctor next-step accuracy + comment correctness…
d1d213c
fix(azure.ai.agents): ResolveAfterInit suggests `azd provision` when …
da75a17
feat(nextstep): surface {{NAME}} placeholders left over in agent.yaml
0d4ae1a
[azure.ai.agents] nextstep: surface every fix-up category after init/…
0f29120
[azure.ai.agents] agent_yaml: name the unresolved placeholders in the…
56c2c31
Fix premature unresolved-placeholder warning during init
e1cad6a
feat(extensions/azure.ai.agents): suggest `azd provision` after init'…
69f8a9d
feat(azure.ai.agents): pending-provision reasons foundation (4.11)
0e59b65
feat(azure.ai.agents): tag model_deployment pending provision (4.12)
94fb58d
fix(azure.ai.agents): route init-from-code trailer through nextstep (…
7297296
feat(azure.ai.agents): migrate project trailer to pending-provision t…
1623ff0
feat(azure.ai.agents): tag acr + app_insights pending provision (4.14)
36a14fe
test(azure.ai.agents): cover "drain last pending-provision tag" path …
709e6e8
feat(azure.ai.agents): classify infra vs manual vars via Bicep output…
91c87a4
feat(azure.ai.agents): enrich manual-vars Next: block with run follow…
ed57cd8
feat(azure.ai.agents): add monitor --follow secondary after invoke --…
a93ff7f
feat(azure.ai.agents): align `show` non-Active branches with issue #7…
f2528f1
feat(azure.ai.agents): qualify single-agent post-deploy `Next:` block…
5f80dd6
feat(azure.ai.agents): add invoke-local secondary to init "everything…
e09ef41
feat(azure.ai.agents): add doctor check `local.manual-env-vars` (P5.1…
d1ad551
feat(azure.ai.agents): scaffold doctor remote-check pipeline (P5.1 C10)
6d36195
feat(azure.ai.agents): add doctor check remote.auth (P5.1 C11)
b35ebef
feat(azure.ai.agents): add doctor check remote.foundry-endpoint (P5.1…
07970a0
feat(azure.ai.agents): add doctor check remote.rbac (P5.1 C16)
18a32c8
feat(azure.ai.agents): add doctor check remote.agent-status (P5.1 C17)
a6a9e44
Phase 5 commit C8: run live OpenAPI probe + post-bind Next: emission
b8e21b9
feat(azure.ai.agents): add doctor check remote.agent-identity-roles (…
e3a8b43
feat(azure.ai.agents): walk agent.manifest.yaml into nextstep.State (…
8b7df84
Phase 5 C13: doctor `remote.model-deployments` check
4ff95e7
feat(azure.ai.agents): doctor local.toolboxes check (P5.1 C14)
7836e82
azd ai agent doctor: add remote.connections check (C15)
0fea456
azd ai agent doctor: address 3-reviewer pass on C13/C14/C15
a30e2a1
azd ai agent next: prefer README payload placeholder
fb2df8a
azd ai agent show: omit next_step for active agents
53e53e9
azd ai agent doctor: stream text checks
de9dc17
azd ai agents: validate service-relative paths
571740e
azd ai agents: remove unused nextstep auth state
36ad57e
azd ai agents: centralize nextstep stdout gating
c0e3903
azd ai agents: redact doctor auth UPN by default
efdac04
azd ai agents: pin AgentVersionIdle wire value in nextstep test
f29b4e3
azd ai agents: redesign doctor text-mode report for actionable concis…
c52d19a
azd ai agents: apply go fix modernizations and add cspell entries
ce3ca24
Move cspell changes to agent specific file
trangevi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
279 changes: 279 additions & 0 deletions
279
cli/azd/extensions/azure.ai.agents/internal/cmd/doctor.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,279 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| "azureaiagent/internal/cmd/doctor" | ||
| "azureaiagent/internal/cmd/nextstep" | ||
| "azureaiagent/internal/pkg/paths" | ||
| "azureaiagent/internal/version" | ||
|
|
||
| "github.com/azure/azure-dev/cli/azd/pkg/azdext" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| // doctorFlags are the Cobra-bound flags for `azd ai agent doctor`. | ||
| type doctorFlags struct { | ||
| localOnly bool | ||
| unredacted bool | ||
| } | ||
|
|
||
| func newDoctorCommand() *cobra.Command { | ||
| flags := &doctorFlags{} | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "doctor", | ||
| Short: "Diagnose problems with an azd ai agent project.", | ||
| Long: `Diagnose problems with an azd ai agent project. | ||
|
|
||
| Runs a sequence of local and remote checks against the current azd project, | ||
| reporting on each one and (when all checks pass) suggesting the next | ||
| command to run. Use this when you have lost terminal context or hit a | ||
| confusing error and want a complete picture of the project's state. | ||
|
|
||
| Exit codes: | ||
| 0 — at least one check passed and no checks failed | ||
| 1 — any check failed | ||
| 2 — all checks were skipped (e.g. preconditions unmet)`, | ||
| Example: ` # Run the full check suite | ||
| azd ai agent doctor`, | ||
| Args: cobra.NoArgs, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| ctx := azdext.WithAccessToken(cmd.Context()) | ||
| logCleanup := setupDebugLogging(cmd.Flags()) | ||
| defer logCleanup() | ||
|
|
||
| // `--debug` (persistent root flag) also toggles the verbose per-check | ||
| // detail block in the doctor report. | ||
| debug := isDebug(cmd.Flags()) | ||
|
|
||
| // Let `local.grpc-extension` report client creation failures so | ||
| // downstream checks can skip instead of duplicating the error. | ||
| azdClient, clientErr := azdext.NewAzdClient() | ||
| if azdClient != nil { | ||
| defer azdClient.Close() | ||
| } | ||
|
|
||
| deps := doctor.Dependencies{ | ||
| AzdClient: azdClient, | ||
| AzdClientErr: clientErr, | ||
| ExtensionVersion: version.Version, | ||
| AgentAPIVersion: DefaultAgentAPIVersion, | ||
| } | ||
|
|
||
| opts := doctor.Options{ | ||
| LocalOnly: flags.localOnly, | ||
| Unredacted: flags.unredacted, | ||
| } | ||
|
|
||
| report, err := runAndRenderDoctorText(ctx, deps, opts, azdClient, os.Stdout, debug) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Use os.Exit to preserve doctor's 0/1/2 exit-code contract; | ||
| // Cobra/azdext would otherwise collapse all errors to 1. | ||
| // os.Exit skips defers, so do not add cleanup-critical defers here. | ||
| code := doctor.ExitCode(report) | ||
| if code == 0 { | ||
| return nil | ||
| } | ||
| os.Exit(code) | ||
| return nil // unreachable | ||
|
antriksh30 marked this conversation as resolved.
|
||
| }, | ||
| } | ||
|
|
||
| cmd.Flags().BoolVar( | ||
| &flags.localOnly, "local-only", false, | ||
| "Skip remote (network-dependent) checks. "+ | ||
| "Useful when offline, behind a proxy, or for a fast local triage.", | ||
| ) | ||
| cmd.Flags().BoolVar( | ||
| &flags.unredacted, "unredacted", false, | ||
| "Show raw principal IDs, scope ARNs, and UPNs in the report.", | ||
| ) | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| // runAndRenderDoctorText streams human-readable output as checks complete. | ||
| // `debug` switches between the default concise rendering and the verbose | ||
| // per-check Message/Suggestion/Links block. | ||
| func runAndRenderDoctorText( | ||
| ctx context.Context, | ||
| deps doctor.Dependencies, | ||
| opts doctor.Options, | ||
| azdClient *azdext.AzdClient, | ||
| w io.Writer, | ||
| debug bool, | ||
| ) (doctor.Report, error) { | ||
| renderer := newDoctorRenderer(w, debug) | ||
| if err := renderer.writeHeader(); err != nil { | ||
| return doctor.Report{}, err | ||
| } | ||
|
|
||
| report, trailing, err := runDoctorWithObserver( | ||
| ctx, | ||
| deps, | ||
| opts, | ||
| azdClient, | ||
| func(result doctor.Result) error { | ||
| return renderer.writeCheck(result) | ||
| }, | ||
| ) | ||
| if err != nil { | ||
| return report, err | ||
| } | ||
|
|
||
| showNext := len(trailing) > 0 && writerIsTerminal(w) | ||
| if err := renderer.writeFooter(report, trailing, showNext); err != nil { | ||
| return report, err | ||
| } | ||
| return report, nil | ||
| } | ||
|
|
||
| func runDoctorWithObserver( | ||
| ctx context.Context, | ||
| deps doctor.Dependencies, | ||
| opts doctor.Options, | ||
| azdClient *azdext.AzdClient, | ||
| observer doctor.ResultObserver, | ||
| ) (doctor.Report, []nextstep.Suggestion, error) { | ||
| // Keep local checks first so remote checks can inspect their prior | ||
| // results for skip-cascade decisions. | ||
| checks := append(doctor.NewLocalChecks(deps), doctor.NewRemoteChecks(deps)...) | ||
| runner := doctor.Runner{Checks: checks} | ||
| report, err := runner.RunWithObserver(ctx, opts, observer) | ||
| if err != nil { | ||
| return report, nil, err | ||
| } | ||
|
|
||
| // Show trailing Next: only on clean reports; otherwise it competes with | ||
| // the failing check's remediation. | ||
| if doctor.ExitCode(report) != 0 { | ||
| return report, nil, nil | ||
| } | ||
|
|
||
| trailing := resolveDoctorTrailing(ctx, azdClient) | ||
| return report, trailing, nil | ||
| } | ||
|
|
||
| // resolveDoctorTrailing returns the doctor's trailing Next block, or nil on | ||
| // error. It chooses deployed-agent suggestions when any service is deployed; | ||
| // otherwise it reuses the post-init guidance. | ||
| func resolveDoctorTrailing(ctx context.Context, azdClient *azdext.AzdClient) []nextstep.Suggestion { | ||
| if azdClient == nil { | ||
| return nil | ||
| } | ||
|
|
||
| state, _ := nextstep.AssembleStateFromSource(ctx, nextstep.NewSource(azdClient)) | ||
|
antriksh30 marked this conversation as resolved.
|
||
| if len(state.Services) == 0 { | ||
| // Avoid repeating the missing-service guidance already reported by | ||
| // `local.agent-service-detected`. | ||
| return nil | ||
| } | ||
|
antriksh30 marked this conversation as resolved.
|
||
|
|
||
| if anyServiceDeployed(state.Services) { | ||
| // Filter to deployed services so the generated invoke/show commands | ||
| // stay copy-paste correct. | ||
| return nextstep.ResolveAfterDeploy( | ||
| filterDeployedServices(state), | ||
| doctorCachedPayload(ctx, azdClient), | ||
| doctorReadmeExists(ctx, azdClient), | ||
| ) | ||
| } | ||
|
|
||
| return nextstep.ResolveAfterInit(state) | ||
| } | ||
|
|
||
| func anyServiceDeployed(services []nextstep.ServiceState) bool { | ||
| for _, s := range services { | ||
| if s.IsDeployed { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // filterDeployedServices returns a shallow clone with only deployed services. | ||
| func filterDeployedServices(state *nextstep.State) *nextstep.State { | ||
| if state == nil { | ||
| return nil | ||
| } | ||
| clone := *state | ||
| clone.Services = make([]nextstep.ServiceState, 0, len(state.Services)) | ||
| for _, s := range state.Services { | ||
| if s.IsDeployed { | ||
| clone.Services = append(clone.Services, s) | ||
| } | ||
| } | ||
| return &clone | ||
| } | ||
|
|
||
| // doctorCachedPayload returns a remote-cache lookup closure for ResolveAfterDeploy. | ||
| // It returns "" on failure and tries deployed Foundry agent names before | ||
| // falling back to azure.yaml service names. | ||
| func doctorCachedPayload(ctx context.Context, azdClient *azdext.AzdClient) func(string) string { | ||
| var envName string | ||
| if azdClient != nil { | ||
| if envResp, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}); err == nil && | ||
| envResp != nil && envResp.Environment != nil { | ||
| envName = envResp.Environment.Name | ||
| } | ||
| } | ||
|
|
||
| return func(serviceName string) string { | ||
| if azdClient == nil || serviceName == "" { | ||
| return "" | ||
| } | ||
| configPath, err := resolveConfigPath(ctx, azdClient) | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| configDir := filepath.Dir(configPath) | ||
|
|
||
| if envName != "" { | ||
| nameKey := fmt.Sprintf("AGENT_%s_NAME", toServiceKey(serviceName)) | ||
| if v, err := azdClient.Environment().GetValue(ctx, &azdext.GetEnvRequest{ | ||
| EnvName: envName, | ||
| Key: nameKey, | ||
| }); err == nil && v != nil && v.Value != "" && v.Value != serviceName { | ||
| if spec, err := nextstep.ReadCachedOpenAPISpec(configDir, v.Value, "remote"); err == nil { | ||
| if payload := nextstep.ExtractInvokeExample(spec); payload != "" { | ||
| return payload | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| spec, err := nextstep.ReadCachedOpenAPISpec(configDir, serviceName, "remote") | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| return nextstep.ExtractInvokeExample(spec) | ||
| } | ||
| } | ||
|
|
||
| // doctorReadmeExists returns a readmeExists closure for ResolveAfterDeploy. | ||
| // Only canonical "README.md" casing is checked to match rendered guidance. | ||
| func doctorReadmeExists(ctx context.Context, azdClient *azdext.AzdClient) func(string) bool { | ||
| projectRoot := resolveProjectPath(ctx, azdClient) | ||
| return func(relativePath string) bool { | ||
| if projectRoot == "" { | ||
| return false | ||
| } | ||
| readmePath, err := paths.JoinAllowRoot(projectRoot, relativePath, "README.md") | ||
| if err != nil { | ||
| return false | ||
| } | ||
| _, err = os.Stat(readmePath) | ||
| return err == nil | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.