Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1044696
feat(ai.agents): generate ready-to-sideload Teams app package on deploy
Copilot Jul 28, 2026
8f8f6da
docs(ai.agents): set CHANGELOG PR number to #9332
Copilot Jul 28, 2026
76dcea0
docs(ai.agents): correct ensureActivityBot comment for package genera…
Copilot Jul 28, 2026
414d649
fix(ai.agents): address PR review on Teams app packaging
Copilot Jul 28, 2026
7cf502f
fix(ai.agents): only remove/overwrite azd-generated Teams packages
Copilot Jul 28, 2026
ad2af3d
feat(ai.agents): add `azd ai agent pack` and `azd ai agent publish`
Copilot Jul 30, 2026
86675fb
fix(ai.agents): align Teams publish scopes with service API
Copilot Aug 3, 2026
6c47dd3
Merge origin/main into activity Teams pack PR
Copilot Aug 3, 2026
d3a1d4c
fix(ai.agents): use deployed agent name for Teams publishing
Copilot Aug 3, 2026
101f0b3
fix(ai.agents): address Teams pack publish review feedback
Copilot Aug 3, 2026
cabba23
Address Teams publish review feedback
Copilot Aug 3, 2026
c55a333
Use Teams title link for published apps
Copilot Aug 3, 2026
995e4a8
Use random temp file for Teams package writes
Copilot Aug 3, 2026
6782c29
Harden Teams package artifact writes
Copilot Aug 3, 2026
2f2a7bb
Fail Teams package commit when marker write fails
Copilot Aug 3, 2026
2317be5
Harden Teams publish responses and package ignores
Copilot Aug 4, 2026
8ee71e4
Apply agent name flag to adopted azure.yaml samples
Copilot Aug 4, 2026
1c58582
Wrap Teams package rollback log line
Copilot Aug 4, 2026
e3d7d28
Address adopted agent names and Teams artifact ignores
Copilot Aug 5, 2026
ea03d71
Track Teams package ownership by digest
Copilot Aug 5, 2026
dcddab6
Keep Teams package marker state consistent
Copilot Aug 6, 2026
6f5cdf1
Apply Teams package ownership to pack output directories
Copilot Aug 6, 2026
fad7afd
Apply adopted agent names only for explicit flag
Copilot Aug 6, 2026
68566b8
Validate Teams publish and adopted agent names earlier
Copilot Aug 7, 2026
a97cf2b
Avoid shared Teams app artifact destinations
Copilot Aug 7, 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,43 @@
`azd deploy` already did the Azure side for you:

- Azure Bot: `{{.BotName}}` (Microsoft Teams channel enabled)
- Bot ID (msaAppId): `{{.MsaAppID}}` <- you will paste this as the bot id
- Bot ID (msaAppId): `{{.MsaAppID}}`
{{- if .PackageFile}}
- Teams app package: `{{.PackageFile}}` (generated next to this guide, ready to sideload)

Two manual steps remain: (A) create a Teams app package, then (B) upload it.
They are the same for any activity-protocol agent.
One step remains: sideload the package to try your agent. Pick ONE of the two ways
below — both are per-user installs and need NO Teams admin.

## Sideload — Teams UI (no extra tooling)

1. In Teams, go to **Apps** -> **Manage your apps** -> **Upload an app**.
2. Select **Upload a custom app**, choose `{{.PackageFile}}`, then **Add**.
3. Select **Open**, then send a message to talk to your agent.

Upload a custom app guide: https://learn.microsoft.com/microsoftteams/platform/concepts/deploy-and-publish/apps-upload

## Or sideload — command line (atk)

The Microsoft 365 Agents Toolkit CLI (atk) installs the same package from a terminal.
`--scope Personal` is a per-user install and needs NO Teams admin:

```sh
npm install -g @microsoft/m365agentstoolkit-cli # one-time; requires Node.js
atk auth login # sign in with your M365 account
atk install --file-path {{.PackageFile}} --scope Personal
```

atk prints a TitleId and a Teams deep link you can open to launch the agent.
atk CLI reference: https://learn.microsoft.com/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli

If **Upload a custom app** is missing or greyed out, custom app upload is turned off for
your tenant, or you want everyone in your org to get it from the org app catalog. Both need
a Teams admin: https://learn.microsoft.com/microsoftteams/platform/concepts/build-and-test/prepare-your-o365-tenant
{{- else}}

azd could not generate the Teams app package automatically, so two manual steps
remain: (A) create a Teams app package, then (B) upload it. They are the same for
any activity-protocol agent.

## A. Create the Teams app package

Expand Down Expand Up @@ -70,30 +103,4 @@ Upload a custom app guide: https://learn.microsoft.com/microsoftteams/platform/c
If **Upload a custom app** is missing or greyed out, custom app upload is turned off for
your tenant, or you want everyone in your org to get it from the org app catalog. Both need
a Teams admin: https://learn.microsoft.com/microsoftteams/platform/concepts/build-and-test/prepare-your-o365-tenant

## C. Optional — do both from the command line

Steps A and B can be scripted. This is a convenience path for repeat runs; it needs extra
tooling and does NOT bypass the tenant custom-app-upload setting above.

Package: put the manifest.json from section A (its Bot ID is already filled in) next to your
two icons, then zip the three files at the root:

```sh
zip -j {{.AgentName}}-teams-app.zip manifest.json color.png outline.png # bash
```
```powershell
Compress-Archive manifest.json,color.png,outline.png {{.AgentName}}-teams-app.zip # PowerShell
```

Sideload for yourself with the Microsoft 365 Agents Toolkit CLI (atk). `--scope Personal` is a
per-user install and needs NO Teams admin:

```sh
npm install -g @microsoft/m365agentstoolkit-cli # one-time; requires Node.js
atk auth login # sign in with your M365 account
atk install --file-path {{.AgentName}}-teams-app.zip --scope Personal
```

atk prints a TitleId and a Teams deep link you can open to launch the agent.
atk CLI reference: https://learn.microsoft.com/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli
{{- end}}
25 changes: 16 additions & 9 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type initFlags struct {
model string
manifestPointer string
agentName string
agentNameExplicit bool
src string
env string
protocols []string
Expand Down Expand Up @@ -1144,6 +1145,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`,
}

ctx := azdext.WithAccessToken(cmd.Context())
flags.agentNameExplicit = cmd.Flags().Changed("agent-name")

azdClient, err := azdext.NewAzdClient()
if err != nil {
Expand Down Expand Up @@ -1425,21 +1427,26 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`,
)
}

// Resolve the agent name BEFORE creating the project
// folder so the folder and agent identity use the same
// name. Use the azure.yaml project name as the default,
// falling back to the template title.
// Resolve --agent-name only when the user explicitly
// provided it. Unified azure.yaml adoption can contain
// multiple agent services, so an interactive/default
// single name must not be treated as an override.
defaultName := foundryProjectName(content)
if defaultName == "" {
defaultName = folderNameStrippingParenSuffix(selectedTemplate.Title)
}

resolvedName, err := resolveInitAgentName(ctx, azdClient, flags, defaultName)
if err != nil {
if exterrors.IsCancellation(err) {
return exterrors.Cancelled("initialization was cancelled")
resolvedName := defaultName
if flags.agentNameExplicit {
var err error
resolvedName, err = resolveInitAgentName(ctx, azdClient, flags, defaultName)
if err != nil {
if exterrors.IsCancellation(err) {
return exterrors.Cancelled("initialization was cancelled")
}
return err
}
return err
flags.agentName = resolvedName
}

if flags.src == "" && resolvedName != "" {
Expand Down
156 changes: 154 additions & 2 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,8 @@ func updateAzureYamlDeployments(
type adoptedAgentNameResolver func(context.Context, string) (string, error)

func adoptedAgentNameConflictSuggestion() string {
return "To create a separate agent, update the agent service's `name` in the adopted azure.yaml before deploying.\n"
return "To create a separate agent, re-run init with --agent-name <unique-name> for single-agent samples, " +
"or update each agent service's `name` in the adopted azure.yaml.\n"
}

// confirmAdoptedAgentNameConflicts checks every agent definition embedded in an
Expand Down Expand Up @@ -734,6 +735,64 @@ func updateAdoptedAgentNames(
return nil
}

func applyAdoptedAgentNameOverride(
ctx context.Context,
azdClient *azdext.AzdClient,
agentName string,
) error {
agentName, err := validateInitAgentName(agentName)
if err != nil {
return err
}

resp, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{})
if err != nil {
return fmt.Errorf("reading adopted project for agent name override: %w", err)
}

var serviceName string
var configPath string
for name, svc := range resp.GetProject().GetServices() {
if svc.GetHost() != AiAgentHost {
continue
}
currentName, path := adoptedAgentNameConfig(svc)
if currentName == "" {
continue
}
if serviceName != "" {
return exterrors.Validation(
exterrors.CodeConflictingArguments,
"--agent-name cannot be applied to an adopted azure.yaml with multiple agent services",
"update each agent service's `name` in azure.yaml after init, or use a sample with a single agent service",
)
}
serviceName = name
configPath = path
}
if serviceName == "" {
Comment thread
v1212 marked this conversation as resolved.
return exterrors.Validation(
exterrors.CodeConflictingArguments,
"--agent-name could not be applied because the adopted azure.yaml has no named agent service",
"update the agent service's `name` in azure.yaml after init, or use a sample with a named agent service",
)
}

value, err := structpb.NewValue(agentName)
if err != nil {
return fmt.Errorf("encoding agent name override for agent service %q: %w", serviceName, err)
}
if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{
ServiceName: serviceName,
Path: configPath,
Value: value,
}); err != nil {
return fmt.Errorf("updating agent name in adopted azure.yaml for service %q: %w", serviceName, err)
}

return nil
}

// adoptedAgentNameConfig returns the Foundry agent name and its service-relative
// config path for the unified inline shape or deprecated config-nested shape.
func adoptedAgentNameConfig(svc *azdext.ServiceConfig) (string, string) {
Expand Down Expand Up @@ -832,7 +891,16 @@ func runInitFromAzureYaml(
httpClient *http.Client,
content []byte,
) error {
targetDir, folderDisplay := adoptTargetDir(flags, foundryProjectName(content))
projectName := foundryProjectName(content)
agentNameOverride, err := adoptedAgentNameOverride(flags)
if err != nil {
return err
}
if agentNameOverride != "" {
projectName = agentNameOverride
}

targetDir, folderDisplay := adoptTargetDir(flags, projectName)

// Adoption is a fresh-project operation: it lays down the project-root
// azure.yaml. When the target already contains an azd project manifest we
Expand All @@ -848,6 +916,11 @@ func runInitFromAzureYaml(
"'azd ai agent init -m <agent.manifest.yaml>'",
)
}
if agentNameOverride != "" {
if err := validateAdoptedAgentNameOverride(content); err != nil {
return err
}
}

// Stage the sample as a local template directory (azure.yaml at its root
// alongside referenced files) that azd-core can adopt with `azd init -t`.
Expand All @@ -870,6 +943,11 @@ func runInitFromAzureYaml(
if err := ensureFoundryProviderDeclared(ctx, azdClient); err != nil {
return err
}
if agentNameOverride != "" {
if err := applyAdoptedAgentNameOverride(ctx, azdClient, agentNameOverride); err != nil {
return err
}
Comment thread
v1212 marked this conversation as resolved.
}

// --- Interactive Azure context setup (subscription, Foundry project) ---
// The scaffolding created an environment; load it and run the same Foundry
Expand Down Expand Up @@ -1002,6 +1080,80 @@ func runInitFromAzureYaml(
return nil
}

type adoptedAgentNameOverrideAzureYaml struct {
Services map[string]adoptedAgentNameOverrideService `yaml:"services"`
}

type adoptedAgentNameOverrideService struct {
Host string `yaml:"host"`
Kind string `yaml:"kind"`
Name string `yaml:"name"`
Config struct {
Kind string `yaml:"kind"`
Name string `yaml:"name"`
} `yaml:"config"`
}

func validateAdoptedAgentNameOverride(content []byte) error {
var doc adoptedAgentNameOverrideAzureYaml
if err := yaml.Unmarshal(content, &doc); err != nil {
return fmt.Errorf("parsing adopted azure.yaml for agent name override: %w", err)
}

namedAgents := 0
for _, svc := range doc.Services {
if svc.Host != AiAgentHost {
continue
}
if adoptedAgentNameOverrideServiceName(svc) == "" {
continue
}
namedAgents++
if namedAgents > 1 {
return exterrors.Validation(
exterrors.CodeConflictingArguments,
"--agent-name cannot be applied to an adopted azure.yaml with multiple agent services",
"update each agent service's `name` in azure.yaml after init, or use a sample with a single agent service",
)
}
}
if namedAgents == 0 {
return exterrors.Validation(
exterrors.CodeConflictingArguments,
"--agent-name could not be applied because the adopted azure.yaml has no named agent service",
"update the agent service's `name` in azure.yaml after init, or use a sample with a named agent service",
)
}

return nil
}

func adoptedAgentNameOverrideServiceName(svc adoptedAgentNameOverrideService) string {
if strings.TrimSpace(svc.Kind) != "" {
return strings.TrimSpace(svc.Name)
}
if strings.TrimSpace(svc.Config.Kind) != "" {
return strings.TrimSpace(svc.Config.Name)
}
return ""
}

func adoptedAgentNameOverride(flags *initFlags) (string, error) {
if !flags.agentNameExplicit {
return "", nil
}
agentNameOverride := strings.TrimSpace(flags.agentName)
if agentNameOverride == "" {
return "", nil
}
validatedName, err := validateInitAgentName(agentNameOverride)
if err != nil {
return "", err
}
flags.agentName = validatedName
return validatedName, nil
}

// adoptTargetDir resolves the directory the adopted project is created in and
// the display path for the "created folder" next-step hint. An explicit --src
// (or positional directory) wins; otherwise a new folder named after the
Expand Down
Loading
Loading