Skip to content
Merged
Show file tree
Hide file tree
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
May 11, 2026
1f5905d
feat(azure.ai.agents): add AssembleState to nextstep package
May 11, 2026
f040bdf
fix(azure.ai.agents): scope nextstep state to azure.ai.agent services
May 11, 2026
2baedae
feat(azure.ai.agents): add nextstep resolver, OpenAPI extractor, and …
May 11, 2026
fc3928d
fix(azure.ai.agents): apply consensus fixes to nextstep resolver
May 11, 2026
30eae8e
fix(azure.ai.agents): clarify shellEscapeSingleQuoted doc comment
May 11, 2026
3f7b6bc
fix(azure.ai.agents): drop stale line reference in shellEscapeSingleQ…
May 11, 2026
d10f1e0
feat(azure.ai.agents): silent fetchOpenAPISpec + wire cache-only Open…
May 11, 2026
baeb1fc
feat(azure.ai.agents): detect missing env vars during nextstep state …
May 11, 2026
e5100c8
fix(azure.ai.agents): exclude defaulted env refs from missing-vars de…
May 11, 2026
bc33171
feat(azure.ai.agents): wire init success path to nextstep resolver
May 11, 2026
7d210d2
fix(azure.ai.agents): reserve trailing slot in nextstep renderer for …
May 11, 2026
ee09a15
fix(azure.ai.agents/nextstep): trailing collision now keeps the most-…
May 11, 2026
72316b2
feat(azure.ai.agents/run): resolver-driven Next: block on local run
May 11, 2026
8f1c05a
fix(azure.ai.agents): align local OpenAPI cache key + restore "After …
May 11, 2026
bac2857
fix(azure.ai.agents): resolve local agent service once in invocations…
May 11, 2026
5af76df
feat(azure.ai.agents): wire invoke.go success paths to nextstep.Resol…
May 11, 2026
67fca44
fix(azure.ai.agents): TTY-gate Next: emission in invoke success helper
May 11, 2026
d153721
feat(azure.ai.agents): wire invoke failure paths to ResolveAfterInvoke
May 11, 2026
fc0f174
fix(azure.ai.agents): gate invocations-protocol failure Next: by HTTP…
May 11, 2026
116319b
fix(azure.ai.agents/nextstep): match AgentVersionStatus wire values t…
May 11, 2026
c6fc8f9
feat(azure.ai.agents/cmd): wire show.go to nextstep resolver
May 11, 2026
afbc77d
fix(azure.ai.agents/cmd/show): drop dead nil guard + doubled blank line
May 11, 2026
6f70a87
fix(azure.ai.agents): correctness fixes from 2.6 cross-pollinated review
May 11, 2026
39de3b6
fix(azure.ai.agents): correct divergent-name invoke suggestion (G3)
May 11, 2026
72547cd
fix(azure.ai.agents): close symmetric G3 in invoke-success suggestion…
May 11, 2026
0bb147f
chore(azure.ai.agents): doc + test cleanup from 2.6/2.6.3/2.6.4 reviews
May 11, 2026
c247378
fix(azure.ai.agents): include api-version on remote OpenAPI spec fetch
May 11, 2026
eea38c4
feat(azure.ai.agents): add FormatNextForNote renderer for artifact em…
May 11, 2026
7bae8f1
feat(azure.ai.agents): wire deploy-hook Next: block on host artifact
May 11, 2026
366a572
fix(azure.ai.agents): scope deploy-hook Next: block to deployed servi…
May 11, 2026
2dbcb81
feat(azure.ai.agents): scaffold doctor package types and runner
May 11, 2026
b26f1f0
fix(azure.ai.agents): apply 4.1 consensus review fixes for doctor pac…
May 11, 2026
f078a20
fix(azure.ai.agents): align doctor types to design spec
May 11, 2026
46ad791
feat(azure.ai.agents): doctor local checks 1-3 (grpc-extension, azure…
May 11, 2026
23cda19
fix(azure.ai.agents): doctor local checks — 3-of-3 review fix-ups (tr…
May 11, 2026
1884c9a
feat(azure.ai.agents): add doctor local checks 4-6 (agent service, pr…
May 11, 2026
7567546
fix(azure.ai.agents): doctor checks 5-6 — 3-of-3 review fix-ups (skip…
May 11, 2026
d422395
feat(azure.ai.agents): wire azd ai agent doctor command
May 11, 2026
f39e0ac
fix(azure.ai.agents): doctor next-step accuracy + comment correctness…
May 12, 2026
d1d213c
fix(azure.ai.agents): ResolveAfterInit suggests `azd provision` when …
May 12, 2026
da75a17
feat(nextstep): surface {{NAME}} placeholders left over in agent.yaml
May 12, 2026
0d4ae1a
[azure.ai.agents] nextstep: surface every fix-up category after init/…
May 12, 2026
0f29120
[azure.ai.agents] agent_yaml: name the unresolved placeholders in the…
May 12, 2026
56c2c31
Fix premature unresolved-placeholder warning during init
May 12, 2026
e1cad6a
feat(extensions/azure.ai.agents): suggest `azd provision` after init'…
May 12, 2026
69f8a9d
feat(azure.ai.agents): pending-provision reasons foundation (4.11)
May 12, 2026
0e59b65
feat(azure.ai.agents): tag model_deployment pending provision (4.12)
May 12, 2026
94fb58d
fix(azure.ai.agents): route init-from-code trailer through nextstep (…
May 12, 2026
7297296
feat(azure.ai.agents): migrate project trailer to pending-provision t…
May 12, 2026
1623ff0
feat(azure.ai.agents): tag acr + app_insights pending provision (4.14)
May 12, 2026
36a14fe
test(azure.ai.agents): cover "drain last pending-provision tag" path …
May 12, 2026
709e6e8
feat(azure.ai.agents): classify infra vs manual vars via Bicep output…
May 12, 2026
91c87a4
feat(azure.ai.agents): enrich manual-vars Next: block with run follow…
May 13, 2026
ed57cd8
feat(azure.ai.agents): add monitor --follow secondary after invoke --…
May 13, 2026
a93ff7f
feat(azure.ai.agents): align `show` non-Active branches with issue #7…
May 13, 2026
f2528f1
feat(azure.ai.agents): qualify single-agent post-deploy `Next:` block…
May 13, 2026
5f80dd6
feat(azure.ai.agents): add invoke-local secondary to init "everything…
May 13, 2026
e09ef41
feat(azure.ai.agents): add doctor check `local.manual-env-vars` (P5.1…
May 13, 2026
d1ad551
feat(azure.ai.agents): scaffold doctor remote-check pipeline (P5.1 C10)
May 13, 2026
6d36195
feat(azure.ai.agents): add doctor check remote.auth (P5.1 C11)
May 13, 2026
b35ebef
feat(azure.ai.agents): add doctor check remote.foundry-endpoint (P5.1…
May 13, 2026
07970a0
feat(azure.ai.agents): add doctor check remote.rbac (P5.1 C16)
May 13, 2026
18a32c8
feat(azure.ai.agents): add doctor check remote.agent-status (P5.1 C17)
May 14, 2026
a6a9e44
Phase 5 commit C8: run live OpenAPI probe + post-bind Next: emission
May 14, 2026
b8e21b9
feat(azure.ai.agents): add doctor check remote.agent-identity-roles (…
May 14, 2026
e3a8b43
feat(azure.ai.agents): walk agent.manifest.yaml into nextstep.State (…
May 14, 2026
8b7df84
Phase 5 C13: doctor `remote.model-deployments` check
May 14, 2026
4ff95e7
feat(azure.ai.agents): doctor local.toolboxes check (P5.1 C14)
May 14, 2026
7836e82
azd ai agent doctor: add remote.connections check (C15)
May 14, 2026
0fea456
azd ai agent doctor: address 3-reviewer pass on C13/C14/C15
May 14, 2026
a30e2a1
azd ai agent next: prefer README payload placeholder
May 14, 2026
fb2df8a
azd ai agent show: omit next_step for active agents
May 14, 2026
53e53e9
azd ai agent doctor: stream text checks
May 14, 2026
de9dc17
azd ai agents: validate service-relative paths
May 15, 2026
571740e
azd ai agents: remove unused nextstep auth state
May 15, 2026
36ad57e
azd ai agents: centralize nextstep stdout gating
May 15, 2026
c0e3903
azd ai agents: redact doctor auth UPN by default
May 18, 2026
efdac04
azd ai agents: pin AgentVersionIdle wire value in nextstep test
May 18, 2026
f29b4e3
azd ai agents: redesign doctor text-mode report for actionable concis…
May 21, 2026
c52d19a
azd ai agents: apply go fix modernizations and add cspell entries
May 22, 2026
ce3ca24
Move cspell changes to agent specific file
trangevi May 22, 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
15 changes: 15 additions & 0 deletions cli/azd/extensions/azure.ai.agents/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ words:
- CLIENTSECRET
- curr
- dataagent
- envkey
- exterrors
- helloworld
- hostedagent
- hostedagents
- kval
- logstream
- mcpservertoolalwaysrequireapprovalmode
Expand All @@ -56,5 +58,18 @@ words:
- protocolversionrecord
- Qdrant
- Toolsets
- underscoped
- Vnext
- webp
# Doctor / next-step terms
- nextstep
- nextsteps
- undeployed
- unredacted
- UNKN
- inlines
- Remediations
- remediations
- uppercases
- parseable
- azd's
3 changes: 2 additions & 1 deletion cli/azd/extensions/azure.ai.agents/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ require (

require github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817

require golang.org/x/term v0.41.0

require (
dario.cat/mergo v1.0.2 // indirect
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
Expand Down Expand Up @@ -110,7 +112,6 @@ require (
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.14.0 // indirect
)
279 changes: 279 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/doctor.go
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)
Comment thread
antriksh30 marked this conversation as resolved.
return nil // unreachable
Comment thread
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))
Comment thread
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
}
Comment thread
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
}
}
Loading
Loading