Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
af7e9d5
feat(agents): add prompt voice telephony bindings
Copilot Sep 1, 2026
321b10b
fix(agents): apply telephony go fix
Copilot Sep 1, 2026
37e4d10
fix(agents): align telephony binding contract
Copilot Sep 1, 2026
c7bf840
fix(agents): compare telephony binding drift safely
Copilot Sep 1, 2026
963ed30
fix(agents): map acs telephony provider name
Copilot Sep 2, 2026
b05e06f
feat(agents): support hosted voice agents
Copilot Aug 27, 2026
48d3547
fix(agents): address hosted voice review feedback
Copilot Aug 27, 2026
ca921bc
fix(agents): reject incompatible hosted voice manifests
Copilot Aug 27, 2026
90d9be6
fix(agents): address follow-up hosted voice review
Copilot Aug 27, 2026
5bf9fce
fix(agents): require string voice metadata
Copilot Aug 27, 2026
fb69170
fix(agents): close hosted voice validation gaps
Copilot Aug 27, 2026
87b65c9
style(agents): wrap hosted voice validation messages
Copilot Aug 27, 2026
7c8280c
fix(agents): reject voice policies in schema
Copilot Aug 27, 2026
24cc358
fix(agents): keep wrapper names distinct
Copilot Aug 27, 2026
b4772b8
test(agents): cover hosted voice deployment
Copilot Aug 27, 2026
fbc540c
style(agents): satisfy hosted voice lint
Copilot Aug 27, 2026
d84ef1f
docs(agents): clarify hosted voice integration
Copilot Aug 28, 2026
b394a50
fix(agents): preserve voice validation after rebase
Copilot Aug 28, 2026
fecc19d
fix(agents): address post-rebase review
Copilot Aug 28, 2026
b6d167f
fix(agents): clear voice target state on version delete
Copilot Aug 28, 2026
6785376
fix(agents): rely on sample azure yaml for hosted voice init
Copilot Sep 2, 2026
0429bfb
fix(agents): update target validation for current agent API
Copilot Sep 2, 2026
f64ae5d
fix(agents): align hosted voice sample docs
Copilot Sep 2, 2026
8b1bf88
fix(agents): use canonical kind during adoption
Copilot Sep 2, 2026
ba3ef60
fix(agents): reject voice wrapper protocols
Copilot Sep 3, 2026
fc6b54f
fix(agents): reject hosted voice fields on hosted agents
Copilot Sep 3, 2026
b2b1b23
chore(agents): rerun hosted voice validation
Copilot Sep 3, 2026
f137e19
feat(agents): accept voice authoring kind
Copilot Sep 3, 2026
6d77ee4
fix(agents): use preferred voice kind in docs
Copilot Sep 3, 2026
173fbcc
fix(agents): handle ACS binding response drift
Copilot Sep 4, 2026
b01fba3
fix(agents): harden telephony binding checks
Copilot Sep 4, 2026
dae1438
fix(agents): address telephony review feedback
Copilot Sep 4, 2026
74a114b
fix(agents): finalize telephony validation
Copilot Sep 4, 2026
a131599
fix(agents): address telephony validation review
Copilot Sep 4, 2026
8b0d4f7
Merge PR 9697 hosted voice agent support
Sep 4, 2026
d114447
Merge PR 9804 prompt voice telephony bindings
Sep 4, 2026
ff97768
Enable voice agents for public preview
Sep 4, 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
131 changes: 131 additions & 0 deletions cli/azd/extensions/azure.ai.agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,65 @@ Details:
> the other inline agent properties such as `codeConfiguration` and
> `environmentVariables`.

## Prompt voice telephony bindings

Prompt voice agents can declare Foundry-side telephony bindings in `azure.yaml`.
This lets `azd deploy` bind an existing phone-provider route to the deployed
agent. Telephony is only supported for `kind: prompt-voice` services.

```yaml
services:
support-voice:
host: azure.ai.agent
kind: prompt-voice
name: support-voice
model:
id: gpt-realtime
telephony:
bindings:
- provider: twilio
identifier: "+14255550123"
connection: telephony-twilio
- provider: acs
identifier: "28:orgid:00000000-0000-0000-0000-000000000001"
connection: telephony-acs
```

Prerequisites:

- The phone provider account/resource and phone number already exist.
- The Foundry project connection named by `connection` already exists.
- Provider-side callbacks, such as Twilio webhooks or ACS Event Subscriptions,
are configured by the user/admin.

Supported providers and identifiers:

- `twilio`: use a Twilio phone number in E.164 format, such as `+14255550123`.
- `acs`: use `28:orgid:<guid>` for Teams Phone Extensibility Resource Accounts
or `4:+<E.164>` for ACS-purchased numbers. azd maps `acs` to the service
provider value `azure-communication-service`.

Bindings are create-only in this preview. If a remote binding exists and matches
the YAML, deploy continues. If the remote binding has different configuration,
azd fails with a remediation message instead of silently keeping stale routing.

Cleanup: delete telephony bindings before deleting test agents. The service may
leave bindings behind when an agent is deleted, so do not rely on agent deletion
as binding cleanup.

Delete a binding with the agent-scoped telephony API before deleting the agent:

```bash
curl -X DELETE \
-H "Authorization: Bearer $TOKEN" \
-H "Foundry-Features: VoiceAgents=V1Preview" \
"$PROJECT_ENDPOINT/agents/$AGENT_NAME/telephony/$BINDING_ID?api-version=2025-11-15-preview"
```

The binding ID is the service provider plus identifier, for example
`twilio:%2B14255550123` for `+14255550123`, or
`azure-communication-service:28:orgid:<guid>` for ACS/TPE.

### Moderating invocations-protocol traffic

For agents that expose the `invocations` protocol, the RAI policy alone is not
Expand Down Expand Up @@ -304,6 +363,78 @@ keys throughout this block (`invocations_moderation`, `response_mode`,
`input_paths`, `stream_selectors`, `event_type`, and so on). The **values**
(`non_streaming`, `streaming`, `both`, `json`, `text`) are the same in both.

### Hosted voice wrapper (preview)

A hosted voice wrapper keeps Voice Live responsible for VAD, speech-to-text,
and text-to-speech while routing conversation logic to a hosted agent in the
same Foundry project. Hosted Voice samples use the same sample `azure.yaml`
flow as other current Hosted Agent and `invocations_ws` samples:

```powershell
azd ai agent init -m .\path\to\azure.yaml
```

The local path can be replaced with its public GitHub URL after the sample is
published.

When the sample project is already present with its `azure.yaml`, run
`azd ai agent init` from the project directory to reuse the existing azd
configuration before provisioning and deployment.

The sample `azure.yaml` contains both services and references the target by its
service name:

```yaml
services:
ai-project:
host: azure.ai.project

voice-target:
host: azure.ai.agent
project: ./src/voice-target
language: csharp
kind: hosted
name: voice-target
uses:
- ai-project
protocols:
- protocol: invocations_ws
version: 1.0.0
metadata:
voiceLiveCompatible: "true"
bridgeProtocolVersion: "1.0"
container:
resources:
cpu: "1"
memory: 2Gi
codeConfiguration:
runtime: dotnet_10
entryPoint: VoiceHostedAgent.dll
dependencyResolution: bundled

voice-target-voice:
host: azure.ai.agent
kind: voice
name: voice-target-voice
uses:
- ai-project
- voice-target
modelType: hosted_agent
targetAgent:
service: voice-target
version: deployed
store: false
```

The `uses` edge deploys the target before the wrapper. `version: deployed`
pins the wrapper to the target version produced by the current azd environment.
Hosted voice wrappers use the unified Voice API.

The target must be active, declare `invocations_ws/1.0.0`, and include
`voiceLiveCompatible=true` and `bridgeProtocolVersion=1.0` metadata. Model,
instructions, tools, and other conversation controls belong to the target;
the wrapper owns audio, voice, store, avatar, and greeting configuration.

## Session idle timeout

A hosted agent's runtime session sandbox is suspended by Foundry after a period
Expand Down
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 @@ -11,6 +11,7 @@ words:
# Voice (prompt-voice) agents
- BYOM
- Nanami
- orgid
- pcma
- pcmu
- webrtc
Expand Down
7 changes: 5 additions & 2 deletions cli/azd/extensions/azure.ai.agents/extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
id: azure.ai.agents
namespace: ai.agent
displayName: Foundry agents (Beta)
description: Ship agents with Microsoft Foundry from your terminal. (Beta)
description: Ship hosted and voice agents with Microsoft Foundry from your terminal. (Beta)
usage: azd ai agent <command> [options]
# NOTE: Make sure version.txt is in sync with this version.
version: 1.0.0-beta.14
Expand All @@ -29,5 +29,8 @@ providers:
description: Deploys agents to the Foundry Agent Service
examples:
- name: init
description: Initialize a new AI agent project.
description: Initialize a new hosted or voice agent project.
usage: azd ai agent init
- name: init-prompt-voice
description: Initialize a new prompt voice agent.
usage: azd ai agent init --kind prompt-voice --agent-name my-voice-agent
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ const agentEndpointHint = "run `azd ai agent show` to see the agent endpoint URL
//
// [1] project name (URL-escaped),
// [2] agent name (URL-escaped),
// [3] protocol tail ("invocations", "a2a", or "openai/responses").
// [3] protocol tail ("invocations", "a2a", "openai/responses", or "voice").
//
// The "openai/v1/responses" tail is also accepted and rebuilt to the canonical
// query-parameter form when invoked.
var agentEndpointPathRegex = regexp.MustCompile(
`^/api/projects/([^/]+)/agents/([^/]+)/endpoint/protocols/` +
`(invocations|a2a|openai/v1/responses|openai/responses)/?$`,
`(invocations|a2a|openai/v1/responses|openai/responses|voice)/?$`,
)

// parsedAgentEndpoint describes a deployed agent invocation endpoint.
Expand All @@ -52,6 +52,7 @@ type parsedAgentEndpoint struct {
//
// https://<acct>.services.ai.azure.com/api/projects/<proj>/agents/<name>/endpoint/protocols/invocations[?api-version=…]
// https://<acct>.services.ai.azure.com/api/projects/<proj>/agents/<name>/endpoint/protocols/openai/responses?api-version=v1
// wss://<acct>.services.ai.azure.com/api/projects/<proj>/agents/<name>/endpoint/protocols/voice?api-version=v1
//
// The host must be a `*.services.ai.azure.com` Foundry host. The path must include the
// protocol-specific suffix; the protocol is derived from the URL.
Expand All @@ -73,10 +74,10 @@ func parseAgentEndpoint(rawURL string) (*parsedAgentEndpoint, error) {
)
}

if !strings.EqualFold(u.Scheme, "https") {
if !strings.EqualFold(u.Scheme, "https") && !strings.EqualFold(u.Scheme, "wss") {
return nil, exterrors.Validation(
exterrors.CodeInvalidParameter,
"--agent-endpoint must use https",
"--agent-endpoint must use https or wss",
agentEndpointHint,
)
}
Expand Down Expand Up @@ -139,6 +140,22 @@ func parseAgentEndpoint(rawURL string) (*parsedAgentEndpoint, error) {
protocol = agent_api.AgentProtocolA2A
case "openai/responses", "openai/v1/responses":
protocol = agent_api.AgentProtocolResponses
case "voice":
protocol = agent_api.AgentProtocolVoice
}
if protocol == agent_api.AgentProtocolVoice && !strings.EqualFold(u.Scheme, "wss") {
return nil, exterrors.Validation(
exterrors.CodeInvalidParameter,
"--agent-endpoint voice URLs must use wss",
agentEndpointHint,
)
}
if protocol != agent_api.AgentProtocolVoice && !strings.EqualFold(u.Scheme, "https") {
return nil, exterrors.Validation(
exterrors.CodeInvalidParameter,
"--agent-endpoint HTTP protocol URLs must use https",
agentEndpointHint,
)
}

// Reject an explicit but empty api-version query parameter; the default fallback would
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ func TestParseAgentEndpoint(t *testing.T) {
wantAgent: "hello",
wantProto: agent_api.AgentProtocolA2A,
},
{
name: "voice websocket with api-version",
raw: "wss://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/voice?api-version=v1",
wantProj: "https://acct.services.ai.azure.com/api/projects/proj",
wantAgent: "hello",
wantProto: agent_api.AgentProtocolVoice,
wantAPIVer: "v1",
},
{
name: "empty url",
raw: "",
Expand All @@ -84,7 +92,19 @@ func TestParseAgentEndpoint(t *testing.T) {
name: "http scheme rejected",
raw: "http://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/invocations",
wantErr: true,
errContains: "https",
errContains: "https or wss",
},
{
name: "wss rejected for HTTP protocol endpoint",
raw: "wss://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/invocations",
wantErr: true,
errContains: "must use https",
},
{
name: "https rejected for voice protocol endpoint",
raw: "https://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/voice",
wantErr: true,
errContains: "must use wss",
},
{
name: "non-foundry host rejected",
Expand Down
8 changes: 6 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 @@ -37,8 +37,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 a hosted or voice agent.",
Long: `Delete a hosted or voice agent and all of its versions.

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

Expand Down Expand Up @@ -253,6 +253,8 @@ func (a *DeleteAction) cleanupEnvVars(
fmt.Sprintf("AGENT_%s_NAME", serviceKey),
fmt.Sprintf("AGENT_%s_VERSION", serviceKey),
fmt.Sprintf("AGENT_%s_ENDPOINT", serviceKey),
fmt.Sprintf("AGENT_%s_TARGET_NAME", serviceKey),
fmt.Sprintf("AGENT_%s_TARGET_VERSION", serviceKey),
envkey.AgentProjectEndpoint(serviceName),
}
for _, protocol := range project.DisplayableProtocolEnvSuffixes() {
Expand Down Expand Up @@ -292,6 +294,8 @@ func (a *DeleteAction) clearDeletedVersionMarker(
keys := []string{
versionKey,
fmt.Sprintf("AGENT_%s_ENDPOINT", serviceKey),
fmt.Sprintf("AGENT_%s_TARGET_NAME", serviceKey),
fmt.Sprintf("AGENT_%s_TARGET_VERSION", serviceKey),
}
for _, protocol := range project.DisplayableProtocolEnvSuffixes() {
keys = append(keys, fmt.Sprintf("AGENT_%s_%s_ENDPOINT", serviceKey, protocol.Suffix))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func TestDeleteMarkerCleanup(t *testing.T) {
"AGENT_MY_AGENT_NAME",
"AGENT_MY_AGENT_VERSION",
"AGENT_MY_AGENT_ENDPOINT",
"AGENT_MY_AGENT_TARGET_NAME",
"AGENT_MY_AGENT_TARGET_VERSION",
"AGENT_MY_AGENT_PROJECT_ENDPOINT",
"AGENT_MY_AGENT_RESPONSES_ENDPOINT",
"AGENT_MY_AGENT_INVOCATIONS_ENDPOINT",
Expand Down Expand Up @@ -82,6 +84,8 @@ func TestDeleteMarkerCleanup(t *testing.T) {
for _, key := range []string{
"AGENT_MY_AGENT_VERSION",
"AGENT_MY_AGENT_ENDPOINT",
"AGENT_MY_AGENT_TARGET_NAME",
"AGENT_MY_AGENT_TARGET_VERSION",
"AGENT_MY_AGENT_RESPONSES_ENDPOINT",
"AGENT_MY_AGENT_INVOCATIONS_ENDPOINT",
"AGENT_MY_AGENT_INVOCATIONS_WS_ENDPOINT",
Expand Down
7 changes: 4 additions & 3 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,13 @@ func newAgentDeployCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
flags := &agentDeployFlags{}
cmd := &cobra.Command{
Use: "deploy [path]",
Short: "Deploy an agent directly from agent.yaml.",
Long: `Deploy an agent definition to the configured Foundry project.
Short: "Deploy a hosted or voice agent directly from agent.yaml.",
Long: `Deploy a hosted or voice agent definition to the configured Foundry project.

The path defaults to ./agent.yaml. A hosted agent uploads source code from the
definition directory unless --code specifies another path. If toolbox.yaml is
present next to agent.yaml, it is deployed first through the toolbox extension.`,
present next to agent.yaml, it is deployed first through the toolbox extension.
Prompt voice and hosted voice definitions deploy through the unified Voice API.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
path := "agent.yaml"
Expand Down
Loading
Loading