Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 40 additions & 0 deletions cli/azd/extensions/azure.ai.agents/.agentignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Files excluded from agent code deployment packaging.
# Uses .gitignore syntax.
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename

# azd tooling files
agent.yaml
agent.manifest.yaml
azure.yaml
.agentignore

# Security / secrets
.env
.env.*
.azure/
.git/

# Python
__pycache__/
.venv/
venv/
*.pyc
*.pyo
.mypy_cache/
.pytest_cache/

# .NET
bin/
obj/
*.user
*.suo
.vs/

# Node
node_modules/

# Docker (not used in code deploy)
Dockerfile
.dockerignore
16 changes: 16 additions & 0 deletions cli/azd/extensions/azure.ai.agents/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Release History

## Unreleased

- Prompt (kind: prompt) agents now support a convention-over-configuration deploy pipeline. `azd up` resolves an internal dependency graph before publishing the agent and validates the whole graph first so a failure never leaves a half-wired agent:
- A sibling `instructions.md` supplies the agent's instructions when none are declared inline (inline wins).
- A non-empty `files/` folder is uploaded to a vector store and wired into an auto-added `file_search` tool (content-hash dedupe; existing `file_search` tools are merged, not duplicated).
- A non-empty `skills/` folder registers each `SKILL.md` bundle into a Foundry toolbox version and attaches its MCP endpoint as an `mcp` tool; an explicit `toolbox:` reference attaches an existing toolbox instead.
- A `connections:` block resolves through a precedence ladder (use existing, create-if-missing with Entra default, auto-fill target from provisioning outputs, or provision/fail-fast), and each tool's required role is surfaced for assignment.
- The model deployment is create-if-missing, and container-only fields (`image`, `protocols`, `code_configuration`, …) are rejected for prompt agents.
- The manifest parser recognizes `skill` and `file` resource kinds.
- `azd ai agent init` now scaffolds the prompt-agent authoring layout: an `instructions.md` sidecar (instructions are written there instead of inline in `agent.yaml`) plus an empty `skills/` folder so the deploy conventions are discoverable from a fresh init.
- **Breaking:** the `agent.yaml` discriminator for prompt agents is now `kind: prompt` (was `kind: managed`). Existing `agent.yaml` files must be updated; the scaffolded schema annotation now points at `PromptAgent.yaml`. The `--kind managed` init flag value is still accepted as a backward-compatible alias for `--kind prompt`.
- Fixed a bug where only `SKILL.md` was uploaded when registering a skill under `skills/<name>/` — any other files in the bundle (e.g. `references/`, `assets/`, `scripts/`, at any nesting depth) were silently dropped. Skill registration now uploads the entire bundle via multipart upload instead of sending just the parsed `SKILL.md` body inline.
- Fixed a bug where a toolbox attached to a prompt agent (via a `skills/` folder or a `toolbox:` reference) was wired into the agent's `mcp` tool without a `project_connection_id`, leaving the agent with no credential to reach the toolbox MCP endpoint so its skills were never invoked. Deploy now creates (or updates) a `RemoteTool` project connection — via the Microsoft.CognitiveServices control plane, since the data-plane connections API is read-only — that fronts the toolbox endpoint and sets it as the tool's `project_connection_id`.
- Fixed a bug where `azd up` re-prompted for an Azure region for a prompt agent even after an existing Foundry project was selected during init. Selecting an existing project now seeds `AZURE_LOCATION` from the project's region (in addition to `AZURE_AI_DEPLOYMENTS_LOCATION`), so the model is deployed to the project's region without a redundant prompt.
- `azd ai agent show` now lists the toolbox tools attached to a prompt agent — each `mcp` tool's server URL and its backing `project_connection_id` — so the toolbox created during deploy is discoverable without inspecting the deployed definition. Also fixed the `Harness` field, which previously printed the harness API base URL instead of the actual execution harness (e.g. `GitHub Copilot (ghcp)`), and added a `Project Endpoint` row showing where the agent is served.

## 0.1.41-preview (2026-06-19)

- [[#8731]](https://github.com/Azure/azure-dev/pull/8731) Improve the post-deploy `Next:` guidance with a stacked layout that puts each command on its own line above its description, adds a blank line between suggestions, and highlights `azd` commands. The new layout applies across deploy, `azd ai agent show`, `init`, and `doctor`. Thanks @therealjohn for the contribution!
Expand Down
6 changes: 6 additions & 0 deletions cli/azd/extensions/azure.ai.agents/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,9 @@ words:
- Bhadauria
# Test infrastructure
- recordproxy
# Managed agent (Foundry PES / vienna harness) terms
- vienna
- azureml
- cognitiveservices
- fdp
- PES
2 changes: 1 addition & 1 deletion cli/azd/extensions/azure.ai.agents/extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ displayName: Foundry agents (Preview)
description: Ship agents with Microsoft Foundry from your terminal. (Preview)
usage: azd ai agent <command> [options]
# NOTE: Make sure version.txt is in sync with this version.
version: 0.1.41-preview
version: 0.1.46-preview
requiredAzdVersion: ">1.25.2"
dependencies:
- id: azure.ai.inspector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ func parseAgentEndpoint(rawURL string) (*parsedAgentEndpoint, error) {
)
}

if !strings.EqualFold(u.Scheme, "https") {
bypass := foundryEndpointValidationBypassed()

if !strings.EqualFold(u.Scheme, "https") &&
!(bypass && strings.EqualFold(u.Scheme, "http")) {
Comment on lines +77 to +78
return nil, exterrors.Validation(
exterrors.CodeInvalidParameter,
"--agent-endpoint must use https",
Expand All @@ -81,7 +84,14 @@ func parseAgentEndpoint(rawURL string) (*parsedAgentEndpoint, error) {
}

host := strings.ToLower(u.Hostname())
if host == "" || !isFoundryHost(host) {
if host == "" {
return nil, exterrors.Validation(
exterrors.CodeInvalidParameter,
"--agent-endpoint host must not be empty",
agentEndpointHint,
)
}
if !bypass && !isFoundryHost(host) {
return nil, exterrors.Validation(
exterrors.CodeInvalidParameter,
fmt.Sprintf("--agent-endpoint host %q is not a Foundry host (*%s)", u.Hostname(), agentEndpointHostHint),
Expand All @@ -91,7 +101,8 @@ func parseAgentEndpoint(rawURL string) (*parsedAgentEndpoint, error) {

// Reject explicit ports — Foundry endpoints always use the default HTTPS port,
// and silently dropping a non-default port would route requests to a different origin.
if u.Port() != "" {
// The override path allows ports (e.g. http://localhost:5000) for local backends.
if !bypass && u.Port() != "" {
return nil, exterrors.Validation(
exterrors.CodeInvalidParameter,
fmt.Sprintf("--agent-endpoint host %q must not include a port", u.Host),
Expand Down
95 changes: 93 additions & 2 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func newDeleteCommand(extCtx *azdext.ExtensionContext) *cobra.Command {

cmd := &cobra.Command{
Use: "delete [name]",
Short: "Delete a hosted agent.",
Long: `Delete a hosted agent and all of its versions.
Short: "Delete an agent.",
Long: `Delete an agent and all of its versions.

If --version is specified, only that version is deleted (the agent itself remains).

Expand Down Expand Up @@ -99,6 +99,15 @@ func (a *DeleteAction) Run(ctx context.Context) error {
}
defer azdClient.Close()

// Prompt (kind=managed) agents are azd services on the harness. They are
// torn down with the rest of the project via `azd down`, so redirect
// rather than calling the Foundry agent-delete path that would fail.
if pctx, isPrompt, pErr := resolvePromptAgentService(
ctx, azdClient, a.flags.name, a.flags.noPrompt,
); pErr == nil && isPrompt {
return a.runPromptDelete(ctx, azdClient, pctx)
}

info, err := resolveAgentServiceFromProject(ctx, azdClient, a.flags.name, a.flags.noPrompt)
if err != nil {
return err
Expand Down Expand Up @@ -266,3 +275,85 @@ func classifyDeleteError(err error, agentName string) error {
}
return exterrors.ServiceFromAzure(err, exterrors.OpDeleteAgent)
}

// runPromptDelete deletes a prompt (kind=managed) agent from the harness. It
// is dispatched from Run() when the resolved azure.ai.agent service carries a
// promptAgent config block. The agent is removed from the harness directly;
// to tear down the whole project (infra included) use `azd down`.
//
// Versioning is not supported for prompt agents today — the backend does not
// expose a per-version delete on the v2.0 surface — so --version is rejected
// with a typed validation error rather than silently ignored.
func (a *DeleteAction) runPromptDelete(
ctx context.Context,
azdClient *azdext.AzdClient,
pctx *promptServiceContext,
) error {
if a.flags.version != "" {
return exterrors.Validation(
exterrors.CodeInvalidParameter,
"--version is not supported for prompt agents",
"prompt agents do not expose per-version delete; omit --version to delete the agent",
)
}

agentName := pctx.AgentName()
if agentName == "" {
return exterrors.Validation(
exterrors.CodeInvalidAgentName,
"agent name is required but could not be resolved",
"set 'name' in agent.yaml or pass the agent name as a positional argument",
)
}

// Confirmation prompt (skip in --no-prompt mode).
if !a.flags.noPrompt {
message := fmt.Sprintf("Delete prompt agent %q from the harness?", agentName)
if a.flags.force {
message = fmt.Sprintf(
"Force-delete prompt agent %q? This will terminate all active sessions.",
agentName,
)
}
defaultValue := false
resp, promptErr := azdClient.Prompt().Confirm(ctx, &azdext.ConfirmRequest{
Options: &azdext.ConfirmOptions{
Message: message,
DefaultValue: &defaultValue,
},
})
if promptErr != nil {
if exterrors.IsCancellation(promptErr) {
return exterrors.Cancelled("delete cancelled")
}
return fmt.Errorf("prompting for confirmation: %w", promptErr)
}
if resp.Value == nil || !*resp.Value {
return exterrors.Cancelled("delete cancelled by user")
}
}

client, err := pctx.newClient()
if err != nil {
return err
}

result, err := client.DeleteAgent(ctx, agentName, pctx.Settings.EffectiveAPIVersion(), a.flags.force)
if err != nil {
return classifyDeleteError(err, agentName)
}

switch a.flags.output {
case "json":
data, jsonErr := json.MarshalIndent(result, "", " ")
if jsonErr != nil {
return fmt.Errorf("failed to marshal response: %w", jsonErr)
}
fmt.Println(string(data))
default:
fmt.Printf("Prompt agent %q deleted from the harness.\n", agentName)
fmt.Println("To also tear down the project infrastructure, run `azd down`.")
}

return nil
}
57 changes: 57 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"fmt"

"azureaiagent/internal/exterrors"

"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/spf13/cobra"
)

// newDeployCommand creates `azd ai agent deploy`, which now exists only to
// redirect users to the standard azd lifecycle.
//
// Prompt agents are first-class azd services (host: azure.ai.agent) created on
// the harness by the service-target provider during `azd up` / `azd deploy`,
// exactly like hosted agents. The previous standalone harness-deploy behavior
// has been removed in favor of that unified flow.
func newDeployCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
extCtx = ensureExtensionContext(extCtx)

cmd := &cobra.Command{
Use: "deploy [name]",
Short: "Deprecated: use `azd up` or `azd deploy`.",
Hidden: true,
Long: `Deprecated. Prompt and hosted agents both deploy through the standard azd
lifecycle now.

Run 'azd up' to provision infrastructure and create the agent, or 'azd deploy'
to (re)deploy the agent once infrastructure exists.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := azdext.WithAccessToken(cmd.Context())
return (&DeployAction{}).Run(ctx)
},
}

return cmd
}

// DeployAction implements the deprecated deploy redirect.
type DeployAction struct{}

func (a *DeployAction) Run(_ context.Context) error {
return exterrors.Validation(
exterrors.CodeInvalidParameter,
"`azd ai agent deploy` has been replaced by the standard azd lifecycle",
fmt.Sprintf(
"run %q to provision and deploy, or %q to (re)deploy an existing project",
"azd up", "azd deploy",
),
)
}
17 changes: 16 additions & 1 deletion cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,11 @@ type AgentServiceInfo struct {
AgentName string // deployed agent name from env
Version string // deployed agent version from env
AgentEndpoint string // full AGENT_{SVC}_ENDPOINT URL (includes name + version)
// ServiceDir is the absolute path to the service's source directory
// (project.Path joined with svc.RelativePath). It points at the folder
// that contains the service's agent.yaml, when one was scaffolded by
// `azd ai agent init`. May be empty if the resolver could not compute it.
ServiceDir string
}

// promptForAgentService prompts the user to select one of multiple azure.ai.agent services.
Expand Down Expand Up @@ -657,13 +662,23 @@ func resolveAgentService(
// resolveAgentServiceFromProject finds the azure.ai.agent service in azure.yaml
// and resolves its deployed agent name and version from the azd environment.
func resolveAgentServiceFromProject(ctx context.Context, azdClient *azdext.AzdClient, name string, noPrompt bool) (*AgentServiceInfo, error) {
svc, _, err := resolveAgentService(ctx, azdClient, name, noPrompt)
svc, project, err := resolveAgentService(ctx, azdClient, name, noPrompt)
if err != nil {
return nil, err
}

info := &AgentServiceInfo{ServiceName: svc.Name}

// Best-effort: compute the on-disk service directory so callers can find
// the agent.yaml that backs the service. Errors here are intentionally
// not fatal — older azure.yaml entries (or services in unusual layouts)
// may not resolve cleanly, and the rest of the resolver remains useful.
if project != nil {
if dir, joinErr := paths.JoinAllowRoot(project.Path, svc.RelativePath); joinErr == nil {
info.ServiceDir = dir
}
}

// Resolve agent name and version from azd environment
envResponse, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{})
if err != nil {
Expand Down
Loading
Loading