Skip to content

fix(agents): reuse existing azure.yaml agent config on init - #9404

Open
glharper wants to merge 5 commits into
mainfrom
glharper/9154-init-respects-azure-yaml
Open

fix(agents): reuse existing azure.yaml agent config on init#9404
glharper wants to merge 5 commits into
mainfrom
glharper/9154-init-respects-azure-yaml

Conversation

@glharper

@glharper glharper commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

azd ai agent init in a project that already has an azure.yaml re-prompted for agent name, protocols, and deploy mode instead of reading them from the manifest.

Init already detects and reuses an existing agent.manifest.yaml or a bare agent.yaml (the latter added for #7268 — "less to ask and just setup azure.yaml"). But the unified manifest work (d8f3abe) moved the agent definition inline onto the azure.yaml service entry and stopped writing a standalone agent.yaml:

Generate .agentignore. The agent definition is written into the azure.yaml service entry below, not to an on-disk agent.yaml.

The reuse detection was never extended to look there, so a modern project matches neither existing check, falls through to promptInitMode → the from-code path, and re-asks for everything azure.yaml already answers.

Changes

  • detectProjectAgentServices — asks the azd host for the project (Project().Get()), validates every azure.ai.agent service path inside the project boundary, and reuses only definitions that resolve through the production inline / deprecated config: / $ref / on-disk agent.yaml loader. Missing or invalid definitions are debug diagnostics, not successful reuse candidates.
  • runReuseProjectAgentServices — completes init without re-prompting: host-based detection already proves the project exists, so it ensures the selected azd environment exists and hands that exact environment to the shared next-step resolver
  • Wired into RunE after manifest detection but before bare agent.yaml reuse, so a configured disk-backed service is treated as a project no-op rather than being added/replaced from its file

Interactive runs get a confirm (defaulting to yes) matching the sibling detection prompts; declining falls through to today's behavior, so adding a second agent to an existing project still works.

This project already configures "chat" (agent: my-chat-agent). Use it?

Notes

  • Project discovery is the host's. Going through Project().Get() rather than reading azure.yaml directly means init sees the same manifest every other azd command does: resolved by walking up from the working directory, and honoring azure.yml as well as azure.yaml. It also reuses core's parsing instead of carrying a second, hand-rolled view of the service schema.
  • A service is reusable only when its definition is real. project.LoadAgentDefinition validates inline, deprecated config:, $ref, and on-disk definitions. A bare host: azure.ai.agent entry with no definition no longer makes --no-prompt exit successfully.
  • Service source paths stay inside the project. paths.JoinAllowRoot rejects absolute, traversal, and symlink-escaping paths before a service can authorize project reuse.
  • Project ownership wins over bare-file detection. Project reuse runs before the shallow agent.yaml scan. This keeps an existing service's uses: and other fields intact when its definition lives on disk.
  • A project the host can't load means "no detections", not a hard failure. Project().Get() errors on a malformed manifest (it goes through project.Load). Init treats that as nothing detected and falls through to the normal prompts, rather than failing on a file the user hasn't been asked about yet. The cause is logged, so --debug still surfaces a typo.
  • Any agent-defining flag opts out of reuse. --no-prompt makes reuse unconditional, so reusing while the caller passed --deploy-mode, --runtime, --entry-point, --agent-name, --protocol, --model, --model-deployment, --project-id, --image, or --src would silently discard them. Those runs fall through to the normal flow instead. --env and --infra describe the environment and the IaC output rather than the agent, so they stay compatible with reuse.
  • Source paths preserve intent. Explicit --src opts out of project no-op reuse but remains valid for bare agent.yaml reuse, which consumes it. A positional project-root or configured service-directory path may reuse; an unconfigured source directory opts out so it is not silently ignored.
  • Selected-environment guidance stays selected. State is assembled from --environment without changing azd's active environment, and every emitted azd command is qualified as azd --environment "<name>" ...; non-azd cd / edit / README guidance remains unchanged.
  • Adoption (runInitFromAzureYaml) is deliberately not reused here: it refuses when a project manifest already exists, since merging a sample's services into an existing azure.yaml is tracked separately as [ext-agents]: azd ai agent init -m <azure.yaml> should merge into an existing project's azure.yaml #8884. This change is about respecting the manifest already present, not merging a new one in.

Testing

  • go test ./... -count=1 (all extension packages)
  • go build ./...
  • golangci-lint run ./internal/cmd/... — 0 issues
  • gofmt -s -l ./internal — clean
  • cspell lint on the changed files — 0 issues

New coverage: inline / config: / disk definitions, missing definitions rejected, unsafe absolute / traversal / symlink service paths rejected, deterministic ordering, non-agent hosts ignored, project reuse before disk reuse from a service subdirectory, the project-vs-bare agent-defining flag matrix, positional root / configured service / unconfigured source classification, selected-environment state, and environment-qualified next-step commands.

Fixes #9154

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fed9e97b-e79b-4889-ac76-0d9a428599cd
Copilot AI balanced review requested due to automatic review settings August 3, 2026 17:21
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

📋 Prioritization Note

Thanks for the contribution! The linked issue isn't in the current milestone yet.
Thank you for logging this issue; our team is reviewing it. If you need urgent prioritization, tag @RickWinter and @kristenwomack to let us know.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
21 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

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

Adds reuse of inline agent configuration from an existing project manifest during azd ai agent init.

Changes:

  • Detects inline and legacy nested agent definitions.
  • Reuses existing configuration without prompting.
  • Adds manifest parsing and formatting tests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
internal/cmd/init.go Integrates existing-project reuse detection.
internal/cmd/init_reuse_project_agent.go Implements detection and reuse.
internal/cmd/init_reuse_project_agent_test.go Tests manifest discovery and parsing.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/init.go Outdated
@github-actions github-actions Bot added the ext-agents azure.ai.agents extension label Aug 3, 2026

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two things worth a second look before this merges.

The new azure.yaml reader re-implements parsing the extension already gets from the azd host, and it reads from --src rather than the project root. Details inline on init_reuse_project_agent.go.

The reuse gate only opts out on --agent-name, but --no-prompt makes reuse unconditional, so a scripted run in a repo that already declares an agent silently ignores every other agent-defining flag instead of adding a second agent. Details inline on init.go.

Two smaller ones inline: a validatePostInit call that can't do anything, and a swallowed YAML parse error.

// A manifest that cannot be parsed yields no services rather than an error: the
// caller treats "nothing detected" as "fall through to the normal init prompts",
// which is the safe outcome for a malformed file.
func findProjectAgentServices(path string) ([]projectAgentService, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This re-implements reading agent services out of azure.yaml, which the extension already gets from the host. helpers.go around line 689 iterates azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) and filters on s.Host == AiAgentHost, and adoptedAgentNameConfig in init_adopt.go already resolves the agent name from both the inline additionalProperties shape and the deprecated config: nested shape.

Going through the host would also drop the azure.yml candidate list and fix the project-root lookup already raised below, since the host resolves the project by walking up from the cwd, and azdcontext handles azure.yml with azure.yaml taking precedence (which azdext.GetProjectDir on its own does not).

The tradeoff: Project().Get() probably hard-fails on a malformed azure.yaml where this returns no services and falls through to the normal init flow. Was that the reason for parsing directly, or was standalone parsing just simpler here?

@glharper glharper Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good call — switched to the host in 00f592d. findProjectManifest and the hand-rolled service struct are gone; detection is now azdClient.Project().Get(ctx, ...) with adoptedAgentNameConfig resolving the name from both shapes.

To your question: it was just simpler, not a considered tradeoff — so thanks for pushing. Your hunch about Project().Get() is right, though: it goes through lazyProjectConfigproject.Load, so a malformed manifest is a hard error rather than an empty project. That doesn't force the choice, because the extension can decide what the error means here — any failure to load is treated as "no detections" and init falls through to the normal prompts, same as before. The difference is it's no longer silent (see the reply on the swallowed-error thread).

Dropping the private reader also fixed the --src bug from the sibling thread and the azure.yml gap for free, since the host walks up from the cwd and azdcontext already handles both filenames with azure.yaml taking precedence.

One consequence worth flagging: ProjectConfig.Path is filepath.Dir(projectFilePath), so the actual filename isn't recoverable from the response. Rather than reintroduce candidate-list guessing just to render it, the prompt is now file-agnostic: This project already configures "chat" (agent: my-chat-agent). Use it?

// agent, so it opts out of reuse and falls through to the normal
// flow rather than silently adopting whatever azure.yaml already
// declares.
if flags.manifestPointer == "" && !manifestDetectedButDeclined && flags.agentName == "" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The --agent-name opt-out makes sense, but the other agent-defining flags don't get the same treatment. --deploy-mode, --runtime, --entry-point, --protocols, --model, --model-deployment, --project-resource-id, and --dep-resolution are all silently ignored once this block fires.

That's a behavior change for scripts. promptInitMode returns initModeFromCode under --no-prompt in a non-empty directory, so today azd ai agent init --no-prompt --deploy-mode code --runtime python_3_13 --entry-point app.py in a repo that already declares an agent runs the from-code path and honors those flags. After this, useExisting := flags.noPrompt makes reuse unconditional and the command no-ops.

Either extend the opt-out to any agent-defining flag, or fail when reuse would discard flags the caller explicitly passed.

@glharper glharper Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed — that scripted no-op is the worse failure, since nothing tells the caller their flags were dropped. Fixed in 00f592d by extending the opt-out rather than erroring: passing any flag that describes the agent to set up falls through to the normal flow.

New agentDefiningFlagsSet covers --agent-name, --deploy-mode, --runtime, --entry-point, --dep-resolution, --model, --model-deployment, --project-id, --image, --protocol, and --src. Your example now runs the from-code path and honors the flags exactly as it does today.

--env and --infra are deliberately excluded — they describe the environment and the IaC output, not the agent, and both stay meaningful on a reuse run.

One subtlety I got wrong on the first pass and then caught: --src has to be tested via cmd.Flags().Changed("src"), not flags.src != "". applyPositionalArg folds a positional directory into the same field, so testing the field made azd ai agent init . skip reuse and re-prompt — reintroducing #9154 through the documented positional form. There's now a regression test driving the real applyPositionalArg path for it, plus a note on the helper so it doesn't come back.


// Advisory only, matching the other reuse paths. The deploy-mode specific
// checks need a CodeConfiguration, which is not re-parsed here.
validatePostInit(srcDir, nil)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

validatePostInit returns immediately when codeConfig is nil (init_validate.go:23), so this call can never do anything. Worth deleting rather than leaving a no-op behind a comment explaining why it's a no-op.

@glharper glharper Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and deleted in 00f592dvalidatePostInit returns immediately on a nil codeConfig (init_validate.go:23), so the call could never do anything. The srcDir parameter it was the only consumer of is gone from runReuseProjectAgentServices too.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/init_reuse_project_agent.go Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 4, 2026 13:46

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

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

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

  • This fallback misses an azure.yml project when the command is run from a subdirectory. azdext.GetProjectDir only walks upward for azure.yaml (cli/azd/pkg/azdext/project.go:16-35), so checkDir becomes . and the subsequent shallow scan never reaches the parent manifest. Resolve the root using both supported project filenames and add a nested-directory azure.yml test.
				if errors.Is(projectErr, azdext.ErrProjectNotFound) {
					checkDir = "."

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

  • This line is not gofmt-formatted, so the repository's formatting check will fail. Indent it consistently with the surrounding block.
checkDir, projectErr := azdext.GetProjectDir()

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

  • No test executes this new RunE branch or runReuseProjectAgentServices; the added tests only validate YAML scanning and display formatting. Add command-level coverage for interactive accept/decline, no-prompt reuse, explicit-option opt-out, environment creation/reuse, and invocation from a nested project directory so the orchestration—not just its parser—is verified.
							if err := runReuseProjectAgentServices(
								ctx, flags, azdClient, checkDir, displayPath, agentServices,

cli/azd/extensions/azure.ai.agents/internal/cmd/init_reuse_project_agent.go:157

  • ensureProject prints Found existing azd project ... Adding agent to it. for an existing project (init.go:1876-1879), but this reuse path intentionally writes no service. Avoid that helper's mutation-oriented status message here, or refactor it so this path can report that the existing configuration is being reused rather than claiming an agent was added.
	if _, err := ensureProject(ctx, flags, azdClient, "."); err != nil {

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

Addresses review feedback on #9404.

Detection went through its own azure.yaml reader rooted at --src, which is the
agent source directory rather than the project root, so a project found by
walking up from the cwd was missed and init re-prompted anyway. It also carried
a private azure.yaml/azure.yml candidate list and a hand-rolled service struct.

Ask the host instead: Project().Get() resolves the manifest the same way every
other azd command does (walking up from the cwd, honoring azure.yml), and
adoptedAgentNameConfig already reads the agent name from both the inline and the
deprecated config: shapes. A project the host cannot load still yields no
detections so init falls through to its normal prompts, but the cause is now
logged instead of silently swallowed.

Reuse also opted out only on --agent-name. Since --no-prompt makes reuse
unconditional, a scripted run passing --deploy-mode/--runtime/--entry-point or
any other agent-defining flag would silently no-op; every such flag now opts
out. --src counts only when passed explicitly, because applyPositionalArg folds
a positional path into the same field and `azd ai agent init .` must keep
reusing.

Also drops a validatePostInit call that could never run, since it returns
immediately when its codeConfig argument is nil.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1d5b46e6-fc04-48bb-9a2c-156105966b48
Copilot AI review requested due to automatic review settings August 4, 2026 14:22

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

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

Suppressed comments (2)

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

  • A positional directory is also an explicit source selection: applyPositionalArg maps azd ai agent init ./agents/new to flags.src, just like --src. Because srcExplicit only checks Cobra's flag state, this invocation is treated as having no agent-defining input; in --no-prompt mode it silently reuses the existing service instead of initializing the supplied source directory. Preserve reuse only when the positional path resolves to the active project root (the init . case), and treat other positional source paths as opt-outs. [azd-code-reviewer]
		srcExplicit ||

cli/azd/extensions/azure.ai.agents/internal/cmd/init_reuse_project_agent.go:114

  • ensureProject's existing-project branch always prints Found existing azd project ... Adding agent to it. (init.go:1882-1885), but this reuse path deliberately writes no service. Every successful reuse therefore emits a false mutation status and may show add-agent-specific infra guidance. Use a read-only project check here, or split the generic project lookup from the add-agent messaging. [azd-code-reviewer]
	if _, err := ensureProject(ctx, flags, azdClient, "."); err != nil {
		return err
	}

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One leftover from the rework, not a blocker.

runReuseProjectAgentServices still calls ensureProject, but that call can't do anything on this path anymore. Detection now runs through the host, so reaching the reuse branch already proves Project().Get() succeeded, which makes the scaffold branch inside ensureProject unreachable and its return value discarded.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/init_reuse_project_agent.go Outdated
Host-based detection already proves Project().Get succeeded before the reuse
path runs. Calling ensureProject again could not reach its scaffold branch,
discarded the returned project, and printed the false status that an agent was
being added even though reuse intentionally writes no service. Remove the
second round-trip and document the precondition.

Also distinguish a positional project-root path from a positional agent source
directory. `init .` at the project root can reuse its configured agent, while
`init ./agents/new` must fall through and honor the selected source instead of
silently ignoring it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1d5b46e6-fc04-48bb-9a2c-156105966b48
Copilot AI review requested due to automatic review settings August 4, 2026 19:45

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

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment on lines +1341 to +1342
if flags.manifestPointer == "" && !manifestDetectedButDeclined &&
!agentDefiningFlagsSet(flags, cmd.Flags().Changed("src")) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 1ece346.

Project reuse now runs before the bare agent.yaml scan. More importantly, the project detector validates each configured service through project.LoadAgentDefinition, so a disk-backed service is recognized from its azure.yaml ownership (inline / config / $ref / disk all use the production loader) before the shallow file scan can call runReuseDefinition.

The regression test creates projectRoot/src/chat/agent.yaml, returns the owning service from a real local gRPC project server, changes cwd to src/chat, and proves project detection resolves the configured service and disk agent name. It also verifies positional . from that configured service directory remains project reuse, while an unconfigured source directory still opts out.

The bare path now shares the agent-defining flag guard as well, except explicit --src remains allowed there because runReuseDefinition actually consumes it.


agentName, _ := adoptedAgentNameConfig(svc)
if agentName == "" {
agentName = serviceName

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the service has no real agent definition, this fallback still marks it for reuse. With --no-prompt, init can exit successfully without creating the missing definition. Let's verify the definition before reusing the service.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 1ece346.

A host: azure.ai.agent entry is no longer enough by itself. projectAgentServicesFrom now calls the production project.LoadAgentDefinition loader before adding a reuse candidate. That validates:

  • unified inline definitions
  • deprecated config: definitions
  • local $ref includes
  • legacy on-disk agent.yaml / agent.yml

Missing or invalid definitions are skipped and logged under --debug; if no valid services remain, init falls through instead of returning successful reuse under --no-prompt. The display name is taken from the configured shape, then the loaded definition, and only falls back to the service key after a real definition was proven.

Service paths are also validated with the repository's symlink-aware paths.JoinAllowRoot before definition loading. Tests cover missing definitions and absolute, traversal, and symlink-escaping paths.


fmt.Println(color.HiBlackString("Reusing the agent configuration already in this project."))

state, _ := nextstep.AssembleState(ctx, azdClient)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This assembles state from the current environment, even when flags.env selected a different one. The next-step hints can therefore describe the wrong environment. Let's pass the selected environment into state assembly without changing the project default.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 1ece346 without selecting or changing the project's active environment.

nextstep.WithEnvironment(env.Name) now makes state assembly read the exact environment found/created by init. The selected name is retained in State.EnvironmentName, and ResolveAfterInit qualifies every actionable azd command as:

azd --environment "<name>" ...

Non-azd guidance such as cd, edit instructions, and README pointers is left unchanged. This means both the inspected state and copied commands target the selected environment even when another environment remains current.

Tests prove the override bypasses a failing CurrentEnvName, reads the selected environment's values, and qualifies azd env set, azd provision, and trailing azd deploy commands.

Reuse only project services whose definitions resolve successfully through the
production inline, config, ref, or disk loader. Validate service source paths at
the project boundary so absolute, traversal, and symlink escapes cannot authorize
reuse.

Run project-owned reuse before bare agent.yaml reuse so a configured disk-backed
service is not added or replaced. Share caller-intent guards while retaining
explicit --src support for the bare-file path that consumes it.

Assemble next-step state from the environment selected for init without changing
the active environment, and qualify emitted azd commands so copied guidance
continues targeting that environment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1d5b46e6-fc04-48bb-9a2c-156105966b48
Copilot AI review requested due to automatic review settings August 5, 2026 20:31

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

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

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.

[Issue] azd ai agent init on existing project does not respect the azure.yaml

5 participants