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
4 changes: 4 additions & 0 deletions cli/azd/extensions/azure.ai.agents/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release History

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

- [[#8753]](https://github.com/Azure/azure-dev/pull/8753) Add native Activity-protocol support: auto-inject the agent endpoint with `activity` + `BotServiceRbac`, send the `AgentEndpoints=V1Preview` Foundry feature header, and normalize the user-facing `activity` protocol name. Introduce `activity.use_case` (`simple` | `digital_worker`) profiles that gate endpoint injection, blueprint reference, and M365 onboarding; round-trip `use_case` through `azd ai agent init`; set `ENABLE_DIGITAL_WORKER`/`AGENT_NAME` so the starter template provisions the MAIB and Bot Service; and print digital-worker M365 onboarding next steps after `azd deploy`.

## 0.1.40-preview (2026-06-15)

- [[#8641]](https://github.com/Azure/azure-dev/pull/8641) Fix optimize/eval handling for array-valued mutations, resolve `dataset.local_uri` relative to the agent project, and align optimize test schema data with the current API format. Thanks @Zyysurely for the contribution!
Expand Down
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.40-preview
version: 0.1.41-preview
requiredAzdVersion: ">1.25.2"
dependencies:
- id: azure.ai.inspector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,8 @@ func protocolFromAgentYaml(
"agent.yaml declares only non-invocable protocols: %s",
strings.Join(names, ", "),
),
"azd can only invoke agents using the responses or invocations protocols",
"azd can only invoke agents using the responses or invocations protocols. "+
"Use 'azd ai agent endpoint show' to verify other protocol configurations.",
)
case 1:
// Exactly one invocable protocol — but if the agent declares
Expand Down
17 changes: 16 additions & 1 deletion cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ type initFlags struct {
src string
env string
protocols []string
// activityUseCase selects the activity deployment profile written to
// agent.yaml for activity agents: "digital_worker" or "simple". Empty defaults
// to "digital_worker" (the only profile with full init support today).
activityUseCase string
// deploy mode flags for non-interactive code deploy support
deployMode string // "container" or "code"; empty = prompt interactively
runtime string // e.g. "python_3_13", "python_3_14", "dotnet_10"
Expand Down Expand Up @@ -1297,6 +1301,9 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`,
cmd.Flags().StringSliceVar(&flags.protocols, "protocol", nil,
"Protocols supported by the agent (e.g., 'responses', 'invocations'). Can be specified multiple times.")

cmd.Flags().StringVar(&flags.activityUseCase, "activity-use-case", "",
"Activity deployment profile for activity agents: 'digital_worker' (default) or 'simple'.")

cmd.Flags().StringVar(&flags.deployMode, "deploy-mode", "",
"Deployment mode: 'container' (Docker image) or 'code' (ZIP upload). Defaults to 'container' in --no-prompt.")

Expand Down Expand Up @@ -1508,8 +1515,16 @@ func ensureProject(
envName = base + "-dev"
}

// Template defaults to the published starter. AZD_AI_STARTER_TEMPLATE allows
// pointing at a local clone or fork for testing infra changes before they
// land upstream (azd init -t accepts a local directory path).
starterTemplate := "Azure-Samples/azd-ai-starter-basic"
if override := os.Getenv("AZD_AI_STARTER_TEMPLATE"); override != "" {
starterTemplate = override
Comment on lines +1518 to +1523
}

initArgs := []string{
"init", "-t", "Azure-Samples/azd-ai-starter-basic", targetDir,
"init", "-t", starterTemplate, targetDir,
"--environment", envName,
}

Expand Down
110 changes: 108 additions & 2 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,48 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context)
}
}

// Step 2: Model configuration
// Step 2: Model configuration — skipped for activity-only agents which use M365 SDK, not AI models.
if isActivityOnlyProtocols(protocols) {
fmt.Println(output.WithGrayFormat(
"Skipping model configuration: activity agents use the M365 Agents SDK and do not require a model deployment.",
))

// Resolve the activity deployment profile. Only "digital_worker" has full
// init support today; an unset flag defaults to it.
useCase := resolveActivityUseCase(a.flags.activityUseCase)

// Build the definition directly without model prompts.
agentKind := agent_yaml.AgentKindHosted
definition := &agent_yaml.ContainerAgent{
AgentDefinition: agent_yaml.AgentDefinition{
Name: agentName,
Kind: agentKind,
},
Protocols: protocols,
CodeConfiguration: codeConfig,
// activity agents require agent_endpoint.protocols=["activity"] for the Foundry PATCH call.
AgentEndpoint: &agent_yaml.AgentEndpoint{
Protocols: []string{"activity"},
},
Activity: &agent_yaml.ActivityConfig{UseCase: string(useCase)},
}

if useCase == agent_yaml.ActivityUseCaseDigitalWorker {
// From-code init does not scaffold the MAIB + Bot Service infrastructure
// a digital worker requires. Point users to the sample manifest, which
// carries that infra alongside the agent code.
fmt.Println(output.WithWarningFormat(
"Digital-worker agents require Managed Agent Identity Blueprint (MAIB) and Bot Service\n"+
"infrastructure that this from-code init does not generate. To get a complete,\n"+
"deployable digital worker (infra + agent.yaml), initialize from the echo-agent manifest:\n"+
" azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/"+
"samples/python/hosted-agents/bring-your-own/activity/echo-agent/agent.manifest.yaml",
))
}

return definition, nil
}

var modelConfigChoices []*azdext.SelectChoice
if selectedProject != nil {
modelConfigChoices = []*azdext.SelectChoice{
Expand Down Expand Up @@ -857,6 +898,22 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context)
CodeConfiguration: codeConfig,
}

// When the activity protocol is among the selected protocols, set
// agent_endpoint.protocols so the Foundry PATCH call registers the activity
// endpoint correctly, and record the activity deployment profile. Accepts
// both "activity" and "activity_protocol".
for _, p := range protocols {
if isActivityProtocolName(p.Protocol) {
definition.AgentEndpoint = &agent_yaml.AgentEndpoint{
Protocols: []string{"activity"},
}
definition.Activity = &agent_yaml.ActivityConfig{
UseCase: string(resolveActivityUseCase(a.flags.activityUseCase)),
}
break
}
}

// Add model resource if a model was selected
if existingDeployment != nil {
// For existing deployments, store the deployment details directly
Expand Down Expand Up @@ -1215,6 +1272,46 @@ type protocolInfo struct {
var knownProtocols = []protocolInfo{
{Name: "responses", Version: "1.0.0"},
{Name: "invocations", Version: "1.0.0"},
{Name: "activity", Version: "1.0.0"},
}

// isActivityOnlyProtocols reports whether protocols contains only the activity
// protocol. agent.yaml may use the friendly name "activity" or the canonical
// wire name "activity_protocol"; both are recognized. Activity agents use the
// M365 Agents SDK and don't require a model deployment.
func isActivityOnlyProtocols(protocols []agent_yaml.ProtocolVersionRecord) bool {
if len(protocols) == 0 {
return false
}
for _, p := range protocols {
if !isActivityProtocolName(p.Protocol) {
return false
}
}
return true
}

// isActivityProtocolName reports whether the given protocol name refers to the
// activity protocol, accepting both "activity" and "activity_protocol".
func isActivityProtocolName(name string) bool {
switch strings.ToLower(strings.TrimSpace(name)) {
case "activity", "activity_protocol":
return true
default:
return false
}
}

// resolveActivityUseCase normalizes the --activity-use-case flag value to a known
// ActivityUseCase. An empty or unrecognized value defaults to digital_worker,
// the only activity profile with full init support today.
func resolveActivityUseCase(flag string) agent_yaml.ActivityUseCase {
switch strings.ToLower(strings.TrimSpace(flag)) {
case string(agent_yaml.ActivityUseCaseSimple):
return agent_yaml.ActivityUseCaseSimple
default:
return agent_yaml.ActivityUseCaseDigitalWorker
}
}

// promptProtocols asks the user which protocols their agent supports.
Expand All @@ -1231,6 +1328,10 @@ func promptProtocols(
for _, p := range knownProtocols {
versionOf[p.Name] = p.Version
}
// Accept "activity_protocol" as a backward-compatible alias for "activity".
if v, ok := versionOf["activity"]; ok {
versionOf["activity_protocol"] = v
}

// If explicit flag values were provided, use them directly (with dedup).
if len(flagProtocols) > 0 {
Expand All @@ -1251,8 +1352,13 @@ func promptProtocols(
fmt.Sprintf("Use one of the supported protocol values: %s", knownProtocolNames()),
)
}
// Normalize the activity alias to the friendly "activity" name.
recordName := name
if isActivityProtocolName(name) {
recordName = "activity"
}
records = append(records, agent_yaml.ProtocolVersionRecord{
Protocol: name,
Protocol: recordName,
Version: version,
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,101 @@ func TestKnownProtocolNames(t *testing.T) {
if !strings.Contains(result, "invocations") {
t.Errorf("knownProtocolNames() = %q, want to contain 'invocations'", result)
}
if !strings.Contains(result, "activity") {
t.Errorf("knownProtocolNames() = %q, want to contain 'activity'", result)
}
}

// TestPromptProtocols_ActivityProtocol verifies that the "activity_protocol"
// alias can be selected via flag and is normalized to the friendly "activity"
// name with its canonical version.
func TestPromptProtocols_ActivityProtocol(t *testing.T) {
t.Parallel()

got, err := promptProtocols(t.Context(), nil, false, []string{"activity_protocol"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got) != 1 {
t.Fatalf("got %d protocols, want 1", len(got))
}
if got[0].Protocol != "activity" {
t.Errorf("protocol = %q, want %q", got[0].Protocol, "activity")
}
if got[0].Version != "1.0.0" {
t.Errorf("version = %q, want %q", got[0].Version, "1.0.0")
}
}

// TestResolveActivityUseCase verifies the --activity-use-case flag normalization.
func TestResolveActivityUseCase(t *testing.T) {
t.Parallel()

cases := []struct {
in string
want agent_yaml.ActivityUseCase
}{
{"", agent_yaml.ActivityUseCaseDigitalWorker},
{"digital_worker", agent_yaml.ActivityUseCaseDigitalWorker},
{" Digital_Worker ", agent_yaml.ActivityUseCaseDigitalWorker},
{"simple", agent_yaml.ActivityUseCaseSimple},
{"SIMPLE", agent_yaml.ActivityUseCaseSimple},
{"unknown", agent_yaml.ActivityUseCaseDigitalWorker},
}
for _, tc := range cases {
if got := resolveActivityUseCase(tc.in); got != tc.want {
t.Errorf("resolveActivityUseCase(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}

// TestIsActivityOnlyProtocols verifies the helper that gates model-prompt skipping.
func TestIsActivityOnlyProtocols(t *testing.T) {
t.Parallel()

tests := []struct {
name string
protocols []agent_yaml.ProtocolVersionRecord
want bool
}{
{
name: "activity only",
protocols: []agent_yaml.ProtocolVersionRecord{{Protocol: "activity_protocol", Version: "1.0.0"}},
want: true,
},
{
name: "activity only (friendly name)",
protocols: []agent_yaml.ProtocolVersionRecord{{Protocol: "activity", Version: "1.0.0"}},
want: true,
},
{
name: "activity + invocations",
protocols: []agent_yaml.ProtocolVersionRecord{
{Protocol: "activity_protocol", Version: "1.0.0"},
{Protocol: "invocations", Version: "1.0.0"},
},
want: false,
},
{
name: "responses only",
protocols: []agent_yaml.ProtocolVersionRecord{{Protocol: "responses", Version: "1.0.0"}},
want: false,
},
{
name: "nil",
protocols: nil,
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := isActivityOnlyProtocols(tt.protocols); got != tt.want {
t.Errorf("isActivityOnlyProtocols() = %v, want %v", got, tt.want)
}
})
}
}

// fakePromptClient is a lightweight test double for azdext.PromptServiceClient.
Expand Down Expand Up @@ -835,6 +930,21 @@ func TestPromptProtocols_Interactive(t *testing.T) {
wantErr: true,
wantErrContain: "cancelled",
},
{
name: "activity selected interactively",
multiSelectFn: func(_ context.Context, _ *azdext.MultiSelectRequest, _ ...grpc.CallOption) (*azdext.MultiSelectResponse, error) {
return &azdext.MultiSelectResponse{
Values: []*azdext.MultiSelectChoice{
{Value: "responses", Label: "responses", Selected: false},
{Value: "invocations", Label: "invocations", Selected: false},
{Value: "activity", Label: "activity", Selected: true},
},
}, nil
},
wantProtocols: []agent_yaml.ProtocolVersionRecord{
{Protocol: "activity", Version: "1.0.0"},
},
},
{
name: "empty selection returns validation error",
multiSelectFn: func(_ context.Context, _ *azdext.MultiSelectRequest, _ ...grpc.CallOption) (*azdext.MultiSelectResponse, error) {
Expand Down
5 changes: 4 additions & 1 deletion cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ azd creates or reuses a hosted agent session backed by that version.
For agents configured with header-based isolation, pass --user-isolation-key
and --chat-isolation-key on each remote invoke.

Activity protocol is not supported for invocation (it is push-based, not pull-based).
Use 'azd ai agent endpoint show' to view activity protocol configuration.

Use --output raw (or -o raw) to dump the unmodified server response (status
line, headers, and body verbatim) to stdout. Useful for debugging server
behavior and inspecting response headers (for example, the agent version
Expand Down Expand Up @@ -219,7 +222,7 @@ suppressed in raw mode.`,

cmd.Flags().BoolVarP(&flags.local, "local", "l", false, "Invoke on localhost instead of Foundry")
cmd.Flags().StringVarP(&flags.inputFile, "input-file", "f", "", "Path to a file whose contents are sent as the request body")
cmd.Flags().StringVarP(&flags.protocol, "protocol", "p", "", "Protocol to use: responses (default) or invocations")
cmd.Flags().StringVarP(&flags.protocol, "protocol", "p", "", "Protocol to use for invocation: responses (default) or invocations")
cmd.Flags().IntVar(&flags.port, "port", DefaultPort, "Local server port")
cmd.Flags().IntVarP(
&flags.timeout,
Expand Down
Loading