Skip to content
Closed
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
17 changes: 17 additions & 0 deletions cli/azd/extensions/azure.ai.agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ Each preview needs its own `alpha.agents.<name>` key or its
`AZD_FOUNDRY_EXT_ENABLE_*` variable. Unrelated keys such as `agents.hosted`
do not turn them on. Ordinary hosted and plain prompt agents need no opt-in.

### Check feature status

`azd ai agent config list-alpha` lists each feature with its name, description,
status in the selected environment, and environment variable, in the layout of
`azd config list-alpha`.

- `-e/--environment` selects another environment. An explicit empty environment is rejected.
- `--output json` sets the output format for scripts.

```bash
azd ai agent config list-alpha
azd ai agent config list-alpha -e dev --output json
```

When a status cannot be read, that entry shows `Unknown` with the error. The
command still lists every feature and then exits with an error.

While a feature is off, its flags, commands, and help sections are hidden from
help. This covers `invoke --long-running`, `invoke --no-wait`, the
`invocations` group, `init --voice`, and the `prompt-voice` choice of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Managed harness means `kind: prompt` with harness type `microsoft_copilot_previe
azure.ai.agents/
├── internal/
│ ├── cmd/
│ │ ├── config.go # config list-alpha command
│ │ ├── root.go # One pre-run check and the help hook
│ │ └── preview_features.go # Annotations, enforcement, help hiding
│ ├── project/
Expand Down Expand Up @@ -101,7 +102,7 @@ The `init` pre-run also binds `WithInitScope` before enforcement or discovery. I

Enforcement has two layers. Each layer has one check.

**Command layer.** Commands and flags carry annotations that name a registered feature. The root `PersistentPreRunE` runs after the SDK pre-run, so `-e` and `AZD_ENVIRONMENT` are already applied. It calls `enforcePreviewFeatures` for the executing command with its args. Whenever the gate would reject, it first runs the command's local argument checks from `validateBeforePreview` so local conflicts are reported before `feature_not_enabled` or an empty environment error. These checks use only flags and args, with no source reads or state changes. The existing debug logging and telemetry steps keep their order. The gate makes no host reads for a command without a preview annotation or a set preview flag. A bound environment error only fails commands that perform a preview check.
**Command layer.** Commands and flags carry annotations that name a registered feature. The root `PersistentPreRunE` runs after the SDK pre-run, so `-e` and `AZD_ENVIRONMENT` are already applied. It calls `enforcePreviewFeatures` for the executing command with its args. Whenever the gate would reject, it first runs the command's local argument checks from `validateBeforePreview` so local conflicts are reported before `feature_not_enabled` or an empty environment error. These checks use only flags and args, with no source reads or state changes. The existing debug logging and telemetry steps keep their order. The gate makes no host reads for a command without a preview annotation or a set preview flag. A bound environment error only fails commands that perform a preview check or carry `rejectEmptyEnvironmentAnnotation`.

| Annotation | Placed on | Effect |
| --- | --- | --- |
Expand Down Expand Up @@ -151,6 +152,14 @@ azd env set AZD_FOUNDRY_EXT_ENABLE_AGENTS_VOICE true

The [README](../../README.md#preview-feature-opt-ins) lists the equivalent commands for all three capabilities.

## Alpha feature listing

`azd ai agent config list-alpha` mirrors `azd config list-alpha`. It prints every feature from `All` as a block with `Name`, `Description`, `Status` (`On` or `Off`), and `Environment variable`. It calls `IsEnabled` once per feature with the environment selected by `-e`, so the status matches what enforcement sees. It prints no source for an opt-in because the API returns only a decision.

An explicit empty environment is rejected before any source is read. The root pre-run rejects it before the project read for telemetry.

The default text output prints `Status: Unknown` and an `Error` line for a feature whose status cannot be read. `--output json` prints `{"features":[...]}` with `name`, `description`, `status`, `envVar`, and an `error` field when the read failed. Both formats list every feature before the command exits with an error that counts the failed reads. The command is read only and needs no authentication.

## Documented enablement methods

The documented ways to opt into a feature are listed below. These opt-ins are additive: any enabling value is sufficient.
Expand Down Expand Up @@ -209,6 +218,6 @@ Disabled capabilities return the structured `feature_not_enabled` compatibility

Tests cover source combinations, all supported boolean spellings and non-enabling values, nested per-feature alpha config, ignored `alpha.all` and unrelated keys, exact environment keys, explicit environment forwarding, optional scopes and sentinel compatibility, genuine failures despite positive sources, malformed responses, context forwarding, and snapshot isolation without input mutation. Registry tests cover declaration rules, the stable copy returned by `All`, and `Lookup`.

Command regressions run through the root command so the central pre-run applies. They cover which parsed flags and values require which feature, inherited command annotations, service gate placement, independent gates, obsolete hosted opt-in isolation, selected-environment forwarding for enforcement and help, help hiding per feature with a process fallback, hidden commands that stay reachable, staged manifests, local invocation, deployed-name fallback, unchanged inputs on rejection, ordinary flows, and retained read access. Help snapshots cover the full surface with every feature on and the default surface with every feature off. A walk over every command with every feature off rejects help text that names a disabled preview.
Command regressions run through the root command so the central pre-run applies. They cover which parsed flags and values require which feature, inherited command annotations, service gate placement, independent gates, obsolete hosted opt-in isolation, selected-environment forwarding for enforcement and help, help hiding per feature with a process fallback, hidden commands that stay reachable, staged manifests, local invocation, deployed-name fallback, unchanged inputs on rejection, ordinary flows, and retained read access. Help snapshots cover the full surface with every feature on and the default surface with every feature off. A walk over every command with every feature off rejects help text that names a disabled preview. Alpha listing tests cover text and JSON output, per-feature read errors, the selected environment, and the output flag allow list.

Run offline tests, build, vet, and scoped lint against the pinned SDK. Tier 0 scenarios cover the hidden and enabled help states.
Run offline tests, build, vet, and scoped lint against the pinned SDK. Tier 0 scenarios cover the hidden and enabled help states and `config list-alpha`.
186 changes: 186 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"errors"
"fmt"
"io"
"strings"

"azureaiagent/internal/featureflags"

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

// Status values match `azd config list-alpha`.
const (
alphaStatusOn = "On"
alphaStatusOff = "Off"
alphaStatusUnknown = "Unknown"
)

// newConfigCommand returns the `azd ai agent config` parent command.
func newConfigCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
extCtx = ensureExtensionContext(extCtx)
cmd := &cobra.Command{
Use: "config",
Short: "Inspect agent extension configuration.",
Long: `Inspect agent extension configuration.

Use 'azd config' to change azd user configuration.`,
Example: ` # Display the agent features in alpha stage
azd ai agent config list-alpha`,
}
cmd.AddCommand(newConfigListAlphaCommand(extCtx))
return cmd
}

// AlphaFeatureItem is the JSON contract for one agent alpha feature.
// Fields added later are additive. Consumers must ignore unknown fields.
type AlphaFeatureItem struct {
// Name is the feature ID used under alpha in user config.
Name string `json:"name"`
// Description is a short summary of the feature.
Description string `json:"description,omitempty"`
// Status is On or Off for the selected environment, or Unknown when
// Error is set.
Status string `json:"status"`
// EnvVar is the environment variable that turns the feature on.
EnvVar string `json:"envVar"`
// Error is set when the status could not be read.
Error string `json:"error,omitempty"`
}

type alphaFeatureListResponse struct {
Features []AlphaFeatureItem `json:"features"`
}

type alphaFeatureReader func(context.Context, featureflags.Feature) (bool, error)

func newConfigListAlphaCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list-alpha",
Short: "Display the list of agent features in alpha stage.",
Long: `Display the list of agent features in alpha stage with the status of each
in the selected environment and the environment variable that turns it on.

Use -e/--environment to read another azd environment. When a status cannot be
read, the entry reports the error and the command exits with an error after
listing every feature.`,
Example: ` # Display the agent features in alpha stage
azd ai agent config list-alpha

# Display the features for another environment as JSON
azd ai agent config list-alpha -e dev --output json

# Turn on a specific alpha feature
azd config set alpha.<feature-name> on`,
Annotations: map[string]string{rejectEmptyEnvironmentAnnotation: "true"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := azdext.NewAzdClient()
if err != nil {
err = fmt.Errorf("connecting to azd to read alpha features: %w", err)
} else {
defer client.Close()
}
environmentName := featureflags.Environment(ctx)
read := func(ctx context.Context, feature featureflags.Feature) (bool, error) {
if err != nil {
return false, err
}
return featureflags.IsEnabled(ctx, client, feature, environmentName)
}
return runConfigListAlpha(ctx, cmd.OutOrStdout(), extCtx.OutputFormat, read)
},
}
azdext.RegisterFlagOptions(cmd, azdext.FlagOptions{
Name: "output",
AllowedValues: []string{"json", "text"},
Default: "text",
})
return cmd
}

// runConfigListAlpha reads every registered feature, writes the list, and
// then reports whether any status could not be read.
func runConfigListAlpha(ctx context.Context, out io.Writer, format string, read alphaFeatureReader) error {
features := featureflags.All()
items := make([]AlphaFeatureItem, 0, len(features))
var failures []error
for _, feature := range features {
item := AlphaFeatureItem{
Name: feature.ID,
Description: feature.Description,
Status: alphaStatusOff,
EnvVar: feature.EnvVar,
}
enabled, err := read(ctx, feature)
switch {
case err != nil:
item.Status = alphaStatusUnknown
item.Error = err.Error()
if errors.Is(err, context.Canceled) {
failures = append([]error{err}, failures...)
} else {
failures = append(failures, err)
}
case enabled:
item.Status = alphaStatusOn
}
items = append(items, item)
}

var err error
if format == "json" {
err = writeJSON(out, alphaFeatureListResponse{Features: items})
} else {
err = writeAlphaFeatures(out, items)
}
if err != nil {
return err
}
if len(failures) > 0 {
return &alphaReadError{total: len(items), causes: failures}
}
return nil
}

// alphaReadError reports how many statuses could not be read. The listing
// already shows each message, so Error stays short while Unwrap keeps the
// read errors for classification. Cancellation is listed first.
type alphaReadError struct {
total int
causes []error
}

func (e *alphaReadError) Error() string {
return fmt.Sprintf("could not read the status of %d of %d alpha features", len(e.causes), e.total)
}

func (e *alphaReadError) Unwrap() []error { return e.causes }

// writeAlphaFeatures writes one block per feature in the layout of
// `azd config list-alpha`, plus the environment variable.
func writeAlphaFeatures(out io.Writer, items []AlphaFeatureItem) error {
blocks := make([]string, 0, len(items))
for _, item := range items {
lines := []string{
"Name: " + item.Name,
"Description: " + item.Description,
"Status: " + item.Status,
"Environment variable: " + item.EnvVar,
}
if item.Error != "" {
lines = append(lines, "Error: "+item.Error)
}
blocks = append(blocks, strings.Join(lines, "\n"))
}
_, err := fmt.Fprintln(out, strings.Join(blocks, "\n\n"))
return err
}
Loading
Loading