Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d576207
Add invocations_ws agent init support
Copilot Jul 7, 2026
98934e0
Update agent init protocol help
Copilot Jul 7, 2026
ab39ee8
Honor protocol flags for image agent init
Copilot Jul 14, 2026
4020370
Include fallback regions during agent init
Copilot Jul 14, 2026
c270d14
Support noninteractive voice sample init
Copilot Jul 15, 2026
bc6b69a
Merge main into invocations_ws init support
Copilot Jul 15, 2026
b07c5ab
Add voice live term to spelling dictionary
Copilot Jul 15, 2026
9b1ee53
Remove voice sample env var coupling
Copilot Jul 16, 2026
cba283b
Clarify sample adoption overrides
Copilot Jul 16, 2026
a62af9c
Address sample adoption review feedback
Copilot Jul 16, 2026
3ed8939
Tighten sample adoption model overrides
Copilot Jul 16, 2026
81e4251
Validate Azure context flags before persisting
Copilot Jul 16, 2026
79fdc60
Harden init context flag handling
Copilot Jul 16, 2026
a39bbb1
Address adoption review comments
Copilot Jul 16, 2026
b98706c
Apply protocol overrides to adopted samples
Copilot Jul 16, 2026
98b49d1
Refine adoption override validation
Copilot Jul 16, 2026
e12a9c5
Address remaining adoption review feedback
Copilot Jul 16, 2026
e6bdd12
Support config-nested adopted agent overrides
Copilot Jul 17, 2026
04f83c0
Validate adopted agent override targets
Copilot Jul 17, 2026
7a5b320
Validate no-prompt Azure context flags
Copilot Jul 17, 2026
241952d
Refine image protocol and model resolution
Copilot Jul 17, 2026
9c9ad64
Narrow invocations_ws init PR scope
Copilot Jul 17, 2026
a519b87
Apply go fix suggestions
Copilot Jul 17, 2026
d5db27a
Wrap long init protocol lines
Copilot Jul 17, 2026
7f62eb0
Suppress local invoke hint for websocket agents
Copilot Jul 17, 2026
95cde48
Preserve invocable protocol precedence in next steps
Copilot Jul 17, 2026
ef716a9
Suppress websocket invoke suggestions after run and deploy
Copilot Jul 17, 2026
b72e28f
Handle non-invocable protocols consistently
Copilot Jul 20, 2026
8147335
Fix activity image manifest test cleanup
Copilot Jul 20, 2026
b1bd083
Handle legacy activity protocol in next steps
Copilot Jul 20, 2026
991c215
Load protocols from inline agent config for next steps
Copilot Jul 20, 2026
6ee9c9c
Load next-step protocols from inline service properties
Copilot Jul 20, 2026
d260d0c
Emit explicit invoke protocol for mixed services
Copilot Jul 20, 2026
e610152
Fix multi-protocol local invoke hints
Copilot Jul 20, 2026
aa2a061
Use test context in inline-protocol assemble-state tests
Copilot Jul 20, 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
1 change: 1 addition & 0 deletions cli/azd/extensions/azure.ai.agents/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ words:
- underscoped
- unparseable
- Vnext
- VOICELIVE
- webp
# Doctor / next-step terms
- nextstep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"switzerlandnorth",
"uksouth",
"westus",
"westus2",
"westus3"
]
}
63 changes: 56 additions & 7 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -657,31 +657,81 @@ func preBuiltImageForInit(agentManifest *agent_yaml.AgentManifest, flagImage str
return strings.TrimSpace(ca.Image)
}

func protocolRecordsForImageManifest(flagProtocols []string) ([]agent_yaml.ProtocolVersionRecord, error) {
if len(flagProtocols) == 0 {
return []agent_yaml.ProtocolVersionRecord{{Protocol: "responses", Version: "2.0.0"}}, nil
}
protocols, err := resolveKnownProtocols(flagProtocols)
if err != nil {
return nil, err
}
records := make([]agent_yaml.ProtocolVersionRecord, 0, len(protocols))
for _, p := range protocols {
records = append(records, agent_yaml.ProtocolVersionRecord{Protocol: p.Name, Version: p.Version})
}
return records, nil
}

func resolveKnownProtocols(flagProtocols []string) ([]protocolInfo, error) {
versionOf := make(map[string]string, len(knownProtocols))
for _, p := range knownProtocols {
versionOf[p.Name] = p.Version
}

seen := make(map[string]bool, len(flagProtocols))
protocols := make([]protocolInfo, 0, len(flagProtocols))
for _, name := range flagProtocols {
if seen[name] {
continue
}
seen[name] = true

version, ok := versionOf[name]
if !ok {
return nil, exterrors.Validation(
exterrors.CodeInvalidAgentManifest,
fmt.Sprintf("unknown protocol %q; supported values: %s", name, knownProtocolNames()),
"choose one of the supported protocols or omit --protocol to use the default",
)
}
protocols = append(protocols, protocolInfo{Name: name, Version: version})
}

return protocols, nil
}

// synthesizeImageManifestFile writes a minimal hosted container agent manifest to a
// temporary file for the bring-your-own-image flow (--image without --manifest).
// Routing through the manifest path lets init skip template/language selection and code
// scaffolding, since a pre-built image needs none of those. The returned cleanup removes
// the temp directory; callers should defer it. The image is intentionally not embedded
// in the temporary manifest; init writes --image to the generated azure.yaml service's
// top-level image field.
func synthesizeImageManifestFile(agentName, image string) (string, func(), error) {
func synthesizeImageManifestFile(agentName, image string, flagProtocols []string) (string, func(), error) {
noop := func() {}
protocols, err := protocolRecordsForImageManifest(flagProtocols)
if err != nil {
return "", noop, err
}

tmpDir, err := os.MkdirTemp("", "azd-agent-image-")
if err != nil {
return "", noop, fmt.Errorf("creating temp directory for synthesized manifest: %w", err)
}
cleanup := func() { _ = os.RemoveAll(tmpDir) }

protocolDocs := make([]map[string]any, 0, len(protocols))
for _, p := range protocols {
protocolDocs = append(protocolDocs, map[string]any{"protocol": p.Protocol, "version": p.Version})
}

Comment thread
v1212 marked this conversation as resolved.
doc := map[string]any{
"name": agentName,
"template": map[string]any{
"kind": string(agent_yaml.AgentKindHosted),
"name": agentName,
"description": fmt.Sprintf("Hosted container agent using pre-built image %s", image),
"protocols": []map[string]any{
{"protocol": "responses", "version": "2.0.0"},
},
"protocols": protocolDocs,
},
}

Expand Down Expand Up @@ -923,7 +973,6 @@ func runInitFromManifest(
if err != nil {
return err
}

// Create credential with whatever tenant is available (may be empty → default tenant)
credential, err := azidentity.NewAzureDeveloperCLICredential(
&azidentity.AzureDeveloperCLICredentialOptions{
Expand Down Expand Up @@ -1114,7 +1163,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`,
"pass --agent-name <name> (or provide --manifest with the agent definition)",
)
}
manifestPath, cleanup, err := synthesizeImageManifestFile(flags.agentName, flags.image)
manifestPath, cleanup, err := synthesizeImageManifestFile(flags.agentName, flags.image, flags.protocols)
if err != nil {
return err
}
Expand Down Expand Up @@ -1481,7 +1530,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`,
"Directory to download the agent definition to (defaults to 'src/<agent-id>')")

cmd.Flags().StringSliceVar(&flags.protocols, "protocol", nil,
"Protocols supported by the agent (e.g., 'responses', 'invocations'). Can be specified multiple times.")
fmt.Sprintf("Protocols supported by the agent (%s). Can be specified multiple times.", knownProtocolNames()))

cmd.Flags().StringVar(&flags.deployMode, "deploy-mode", "",
"Deployment mode: 'container' (Docker image) or 'code' (ZIP upload). Defaults to 'code' for Python/.NET projects in --no-prompt.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context)
}
a.azureContext = azureContext
}

// TODO: Prompt user for agent kind
agentKind := agent_yaml.AgentKindHosted

Expand Down Expand Up @@ -906,6 +905,7 @@ type protocolInfo struct {
var knownProtocols = []protocolInfo{
{Name: "responses", Version: "2.0.0"},
{Name: "invocations", Version: "1.0.0"},
{Name: "invocations_ws", Version: "2.0.0"},
Comment thread
v1212 marked this conversation as resolved.
// "activity" is the canonical protocol name (legacy alias: "activity_protocol").
// The version selects the platform's internal container route ("v1"/"1.0.0" ->
// /api/messages, "2.0.0" -> /activity/messages), but that hop is Bot Service ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -597,6 +598,13 @@ func TestPromptProtocols_FlagValues(t *testing.T) {
{Protocol: "invocations", Version: "1.0.0"},
},
},
{
name: "invocations_ws only",
flagProtocols: []string{"invocations_ws"},
wantProtocols: []agent_yaml.ProtocolVersionRecord{
{Protocol: "invocations_ws", Version: "2.0.0"},
},
},
{
name: "both protocols",
flagProtocols: []string{"responses", "invocations"},
Expand Down Expand Up @@ -696,6 +704,9 @@ func TestKnownProtocolNames(t *testing.T) {
if !strings.Contains(result, "invocations") {
t.Errorf("knownProtocolNames() = %q, want to contain 'invocations'", result)
}
if !strings.Contains(result, "invocations_ws") {
t.Errorf("knownProtocolNames() = %q, want to contain 'invocations_ws'", result)
}
if !strings.Contains(result, "activity") {
t.Errorf("knownProtocolNames() = %q, want to contain 'activity'", result)
}
Expand Down Expand Up @@ -736,6 +747,7 @@ func TestPromptProtocols_Interactive(t *testing.T) {
Values: []*azdext.MultiSelectChoice{
{Value: "responses", Label: "responses", Selected: true},
{Value: "invocations", Label: "invocations", Selected: true},
{Value: "invocations_ws", Label: "invocations_ws", Selected: false},
},
}, nil
},
Expand All @@ -751,13 +763,33 @@ func TestPromptProtocols_Interactive(t *testing.T) {
Values: []*azdext.MultiSelectChoice{
{Value: "responses", Label: "responses", Selected: true},
{Value: "invocations", Label: "invocations", Selected: false},
{Value: "invocations_ws", Label: "invocations_ws", Selected: false},
},
}, nil
},
wantProtocols: []agent_yaml.ProtocolVersionRecord{
{Protocol: "responses", Version: "2.0.0"},
},
},
{
name: "websocket protocol selected",
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: "invocations_ws", Label: "invocations_ws", Selected: true},
},
}, nil
},
wantProtocols: []agent_yaml.ProtocolVersionRecord{
{Protocol: "invocations_ws", Version: "2.0.0"},
},
},
{
name: "user cancellation",
multiSelectFn: func(_ context.Context, _ *azdext.MultiSelectRequest, _ ...grpc.CallOption) (*azdext.MultiSelectResponse, error) {
Expand All @@ -773,6 +805,7 @@ func TestPromptProtocols_Interactive(t *testing.T) {
Values: []*azdext.MultiSelectChoice{
{Value: "responses", Label: "responses", Selected: false},
{Value: "invocations", Label: "invocations", Selected: false},
{Value: "invocations_ws", Label: "invocations_ws", Selected: false},
},
}, nil
},
Expand Down Expand Up @@ -819,6 +852,33 @@ func TestPromptProtocols_Interactive(t *testing.T) {
}
}

func TestPromptProtocols_ChoicesIncludeInvocationsWsWithoutChangingDefault(t *testing.T) {
t.Parallel()

client := &fakePromptClient{multiSelectFn: func(
_ context.Context,
in *azdext.MultiSelectRequest,
_ ...grpc.CallOption,
) (*azdext.MultiSelectResponse, error) {
choices := in.Options.Choices
require.GreaterOrEqual(t, len(choices), 3)
require.Equal(t, "responses", choices[0].Value)
require.True(t, choices[0].Selected)
require.Equal(t, "invocations", choices[1].Value)
require.False(t, choices[1].Selected)
require.Equal(t, "invocations_ws", choices[2].Value)
require.False(t, choices[2].Selected)

return &azdext.MultiSelectResponse{Values: choices}, nil
}}

got, err := promptProtocols(t.Context(), client, false, nil)
require.NoError(t, err)
require.Equal(t, []agent_yaml.ProtocolVersionRecord{
{Protocol: "responses", Version: "2.0.0"},
}, got)
}

func TestPromptDeployMode_FlagOverride(t *testing.T) {
t.Parallel()

Expand Down
61 changes: 60 additions & 1 deletion cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ func TestSynthesizeImageManifestFile(t *testing.T) {
const image = "myacr.azurecr.io/agents/my-agent@sha256:" +
"76a9463463acf11d4068e8468fb232a3de0709177b6b35de95de6a34b33fa686"

manifestPath, cleanup, err := synthesizeImageManifestFile(agentName, image)
manifestPath, cleanup, err := synthesizeImageManifestFile(agentName, image, nil)
require.NoError(t, err)
require.NotNil(t, cleanup)
require.FileExists(t, manifestPath)
Expand All @@ -343,6 +343,65 @@ func TestSynthesizeImageManifestFile(t *testing.T) {
require.NoFileExists(t, manifestPath)
}

func TestSynthesizeImageManifestFile_UsesFlagProtocols(t *testing.T) {
t.Parallel()

const agentName = "my-agent"
const image = "myacr.azurecr.io/agents/my-agent:v1"

manifestPath, cleanup, err := synthesizeImageManifestFile(agentName, image, []string{"invocations_ws"})
require.NoError(t, err)
defer cleanup()

content, err := os.ReadFile(manifestPath)
require.NoError(t, err)
template, err := agent_yaml.ExtractAgentDefinition(content)
require.NoError(t, err)

containerAgent, ok := template.(agent_yaml.ContainerAgent)
require.True(t, ok, "synthesized template should be a ContainerAgent, got %T", template)
require.Len(t, containerAgent.Protocols, 1)
require.Equal(t, "invocations_ws", containerAgent.Protocols[0].Protocol)
require.Equal(t, "2.0.0", containerAgent.Protocols[0].Version)
}

func TestSynthesizeImageManifestFile_RejectsUnknownProtocol(t *testing.T) {
t.Parallel()

manifestPath, cleanup, err := synthesizeImageManifestFile(
"my-agent",
"myacr.azurecr.io/agents/my-agent:v1",
[]string{"unknown"},
)
require.Error(t, err)
require.Empty(t, manifestPath)
cleanup()
require.Contains(t, err.Error(), "unknown protocol")
}

func TestSynthesizeImageManifestFile_AcceptsActivityProtocol(t *testing.T) {
t.Parallel()

manifestPath, cleanup, err := synthesizeImageManifestFile(
"my-agent",
"myacr.azurecr.io/agents/my-agent:v1",
[]string{"activity"},
)
require.NoError(t, err)
defer cleanup()

content, err := os.ReadFile(manifestPath)
require.NoError(t, err)
template, err := agent_yaml.ExtractAgentDefinition(content)
require.NoError(t, err)

containerAgent, ok := template.(agent_yaml.ContainerAgent)
require.True(t, ok, "synthesized template should be a ContainerAgent, got %T", template)
require.Equal(t, []agent_yaml.ProtocolVersionRecord{
{Protocol: "activity", Version: "2.0.0"},
}, containerAgent.Protocols)
}

func TestAddToProjectPreBuiltImageWritesServiceImage(t *testing.T) {
const image = "myacr.azurecr.io/agents/my-agent:v1"
server := &recordingProjectServer{}
Expand Down
Loading
Loading