feat(agents): support declarative prompt-voice agents - #9364
Conversation
Add azd support for a new 'prompt-voice' agent kind that creates a managed speech-to-speech (voice) agent on Azure AI Foundry. - yaml: new prompt-voice kind + VoiceAgent authoring struct/parsing - map: translate authoring kind prompt-voice -> data-plane kind voice, defaulting the audio pipeline (PCM16@24k, server_vad, whisper-1, DragonHD default voice) and v1 implicit managed model_type - agent_api: VoiceAgentDefinition wire structs + CreateVoiceAgent with Foundry-Features: VoiceAgents=V1Preview preview header - project: voice-aware agent_definition read/write + isolated deployVoiceAgent deploy path (container path unchanged) - init: --kind/--voice flags, voice manifest synthesis, prompt option Scope: prompt-voice + managed model only. BYOM, hosted-voice, tools, avatar, and cascaded models are follow-ups. Draft: needs further end-to-end session testing and optimization.
|
Azure Pipelines: Successfully started running 1 pipeline(s). 21 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
- map_voice_test.go: cover CreateVoiceAgentAPIRequest defaults/overrides, managed enforcement, BYOM rejection, missing-model error, and the isOpenAIVoice/buildVoiceConfig voice-type selection - parse_voice_test.go: cover prompt-voice manifest parsing and ValidateAgentDefinition (ok / missing model.id / self_deployed rejected) - azure.ai.agent.json: add prompt-voice to the kind enum and document the voice service properties (modelType/model/instructions/voice/store) - cspell.yaml: allow BYOM
The azure.ai.projects synthesis copy must stay byte-identical to the azure.ai.agents copy (TestAgentsSynthesisCopyMatches). Mirror the prompt-voice comment update made in the agents synthesizer.
There was a problem hiding this comment.
Pull request overview
Adds managed declarative prompt-voice agents across initialization, configuration, API mapping, and deployment.
Changes:
- Adds voice-agent YAML/schema models and validation.
- Maps voice manifests to Foundry’s preview API contract.
- Adds voice-specific initialization and deployment paths with tests.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
schemas/azure.ai.agent.json |
Adds voice-agent schema fields. |
internal/synthesis/synthesizer.go |
Marks voice agents as non-container. |
internal/project/service_target_agent.go |
Adds voice deployment handling. |
internal/project/agent_definition.go |
Adds inline voice configuration conversion. |
internal/pkg/agents/agent_yaml/yaml.go |
Defines voice authoring models. |
internal/pkg/agents/agent_yaml/parse.go |
Parses and validates voice manifests. |
internal/pkg/agents/agent_yaml/parse_voice_test.go |
Tests voice parsing and validation. |
internal/pkg/agents/agent_yaml/map.go |
Maps voice manifests to API requests. |
internal/pkg/agents/agent_yaml/map_voice_test.go |
Tests voice request mapping. |
internal/pkg/agents/agent_api/operations.go |
Adds the preview create operation. |
internal/pkg/agents/agent_api/models.go |
Defines voice API wire models. |
internal/cmd/init.go |
Adds voice initialization flags and flow. |
internal/cmd/init_from_templates_helpers.go |
Adds the interactive voice option. |
cspell.yaml |
Adds voice-related terminology. |
Resolve four correctness gaps in the declarative prompt-voice flow surfaced in review, keeping every existing hosted/container/workflow path unchanged: - init: validate --kind (and its --image incompatibility) before either the image or prompt-voice synthesis fast path, so `--kind prompt-voice --image` is rejected instead of silently creating a hosted image agent. - init: skipACR now also covers prompt-voice (managed, no container), while a new isHostedAgent decision drives hosted-region filtering. selectFoundryProject gains a distinct filterHostedRegions parameter so a voice agent skips ACR without being constrained to hosted-agent regions. - deploy: resolve an explicit AGENT_DEFINITION_PATH override before the voice/container dispatch (resolveVoiceAgentForDeploy), so an override wins for voice just as it does for the container path. - deploy contract: make Endpoints() and next-step isDeployed voice-aware. Voice agents record only NAME + base ENDPOINT (no agent-version / per-protocol endpoints), so both consumers now treat the base endpoint as the deployment marker instead of reporting a created voice agent as undeployed. Adds unit tests for the skipACR/isHostedAgent split, the override-precedence dispatch, and the voice deployed-marker fallback.
|
Azure Pipelines: Successfully started running 2 pipeline(s). 20 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
Jon Gallant (jongio)
left a comment
There was a problem hiding this comment.
Ran through the voice path end to end against the current head. Build is clean and the new tests pass locally. Three things I'd like to sort out before this ships, plus two nits.
Medium
- The new deployed-agent detection keys off "VERSION empty and ENDPOINT set" rather than the service kind, in both
Endpoints()andnextstep.isDeployed(). Inline comments on both. - Voice create is an unconditional POST to
/voice_agentswith no version model. What's the intended behavior for a secondazd deploy? Inline comment onoperations.go. - Delete has no voice path.
AgentClient.DeleteAgentbuilds%s/agents/%s, andDeleteAction.cleanupEnvVarsclears the threeAGENT_<KEY>_*vars regardless. Soazd ai agent deleteagainst a prompt-voice service hits/agents/<name>, gets a 404, andclassifyDeleteErrorturns that intoCodeAgentNotFound. The user is told the agent doesn't exist while it's still live in the project, and there's no way to tear it down from azd. If delete is intentionally out of scope for this PR, could you add it to the follow-ups list in the description so it doesn't get lost?
Low
- Orphaned comment fragment in
init.go. Inline comment. isOpenAIVoiceclassifies by name shape. Inline comment.
One question on AZURE_VOICE_OVERRIDDEN_HOST: the comment says it exists to bypass the public Foundry APIM while the voice route rolls out. Worth filing an issue to remove it once the route is live, otherwise it tends to stick around forever.
Also, the description still says Draft but the PR is open and review is requested. Probably just needs updating.
- Endpoints()/isDeployed(): gate voice base-endpoint fallback on the service's actual prompt-voice kind instead of the env-var shape, so a partially-failed hosted deploy still surfaces CodeMissingAgentEnvVars - add nextstep isVoiceService helper (mirrors project kind gate; the two stay in separate packages to avoid a project->nextstep import cycle) - CreateVoiceAgent: document create-only redeploy semantics - isOpenAIVoice: classify via explicit OpenAI voice set + Azure Neural locale-prefix pattern instead of a bare '-' check - init.go: drop orphaned comment fragment - tests: cover hosted lingering-endpoint gate and voice name classification
|
Jon Gallant (@jongio) thanks for the review — pushed 438466c addressing the inline threads (replied on each; left them unresolved for you to confirm). For the remaining points from the review body:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Suppressed comments (8)
cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_project_setup.go:148
- [azd-code-reviewer] The interactive existing-project branch has the same coupling: prompt-voice sets
skipACR=true, which now turns on hosted-region filtering and hides otherwise valid voice projects. Use the separate hosted-agent decision here as well.
skipACR, // filterHostedRegions: this path is code/container only (non-voice)
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:440
- [azd-code-reviewer] This voice check ignores
AGENT_DEFINITION_PATH, although deployment now lets that override determine voice/container dispatch. A voice override on a hosted service deploys successfully but endpoint discovery still treats it as hosted and fails on the missing VERSION; next-step state has the same mismatch. Persist or resolve a common effective-kind marker in all three consumers.
if _, isVoice, err := VoiceAgentFromResolvedService(serviceConfig, p.projectPath); err != nil {
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:1646
- [azd-code-reviewer] This resolved name is not pinned to
flags.agentName.runInitFromManifestlater reachesdownloadAgentYaml, which callsresolveInitAgentNameagain, so the interactive voice flow asks for the agent name twice. Pin the result before entering the manifest flow and add a regression test.
resolvedName, err := resolveInitAgentName(ctx, azdClient, flags, "voice-agent")
cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_project_setup.go:74
- [azd-code-reviewer] This still equates skipping ACR with requiring hosted-agent regions. Prompt-voice reaches
configureFoundryProjectwithskipACR=true, so a voice init using--project-idincorrectly filters out projects in regions that do not support hosted agents. Pass a separatefilterHostedRegionsdecision through this helper.
This issue also appears on line 148 of the same file.
skipACR, // filterHostedRegions: this path is code/container only (non-voice)
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:1288
- [azd-code-reviewer] The prompt-voice fast path silently ignores
--model-deployment, even though the shared flag help says it takes precedence over--model. Because this PR supports managed models only, reject this combination instead of creating the defaultgpt-realtimeagent.
if flags.image != "" {
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:2216
- [azd-code-reviewer] Recording
isVoiceAgentdoes not preserve the documented two-question scaffold flow. A synthesized voice manifest has no model resources, so this function continues intoconfigureFoundryProject, whose interactive branch prompts for project choice, subscription, and location during init rather than deferring them toazd provisionas the PR description states.
if kind, err := agentManifestKind(agentManifest); err == nil {
a.isVoiceAgent = kind == agent_yaml.AgentKindPromptVoice
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go:188
- [azd-code-reviewer] The linked REST contract does not have the create-only behavior documented here:
POST /voice_agentscreates an agent or a new version of an existing one, andPOST /voice_agents/{agent_name}provides update semantics. Treating every second deploy as an unavoidable failure makesazd deploynon-repeatable despite API support; implement the existing-agent path rather than deferring it.
// Redeploy semantics: the voice data-plane exposes create-only POST /voice_agents
// with no version/upsert model (unlike hosted agents, which mint a new
// agent-version per deploy). A second `azd deploy` of the same voice service
// therefore re-POSTs with the same name and the service rejects it with a
// non-success status, which this method surfaces as a deploy error rather than
// silently overwriting the existing agent. Idempotent redeploy/update is tracked
// as a follow-up (see the PR "Follow-ups" section); until the service adds an
// update route, redeploy requires deleting the existing voice agent first.
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go:212
- [azd-code-reviewer]
CreateVoiceAgenthas no operation-level test, leaving the required preview header, optional overridden-host header, route, and accepted response statuses unverified. Add a fake-transport test alongsideoperations_test.gothat asserts these request details and response parsing.
// Voice agents are a preview feature; the service rejects the request with
// 403 preview_feature_required unless this opt-in header is present.
req.Raw().Header.Set("Foundry-Features", voiceAgentsPreviewFeature)
if overriddenHost != "" {
req.Raw().Header.Set("x-ms-overridden-host", overriddenHost)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go:338
- [azd-code-reviewer] This makes
--project-idcontainer initialization apply hosted-region filtering even though the equivalent interactive branch below usesdeployMode == "code", and the previous behavior tied both decisions to that same condition. A container project can therefore be rejected when supplied by ID but accepted through interactive selection. Keep the two branches consistent.
filterHostedRegions := true
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go:140
- [azd-code-reviewer] With the preview disabled (the default), an empty interactive directory no longer follows the previous automatic template path; it now opens a one-item selection prompt. This contradicts the PR's claim that existing init flows remain unchanged. Preserve the early
empty -> initModeTemplatereturn when voice preview is off, while showing the prompt when the voice choice is enabled, and update the default-disabled regression test accordingly.
if noPrompt {
if empty {
return initModeTemplate, nil
}
return initModeFromCode, nil
}
cli/azd/extensions/azure.ai.agents/internal/cmd/doctor/checks_agent_status.go:733
- [azd-code-reviewer] This classification omits
AGENT_DEFINITION_PATH, although deploy,Endpoints, and next-step all honor that override. If a hosted or kind-less service is deployed through a prompt-voice override, it has noVERSION, but doctor classifies it as hosted and runs the hosted NAME/VERSION probe, producing a false failure. Pass the process override here as well and add an override regression test.
isVoice, err := agentkind.IsPromptVoice(svc, resp.Project.GetPath(), "")
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Suppressed comments (5)
cli/azd/extensions/azure.ai.agents/schemas/azure.ai.agent.json:58
- [azd-code-reviewer] This schema still accepts
model: { id: "" }, although both manifest validation and request mapping reject an emptymodel.id. Add a non-empty constraint so editor/schema validation matches deployment behavior as the new$commentpromises.
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:1767 - [azd-code-reviewer] The shared
--modelhelp now applies to prompt-voice init too, but it only advertises the hosted-agent default. Prompt-voice synthesis defaults togpt-realtime, soazd ai agent init --helpgives the wrong expectation for this new path. Qualify both defaults here.
"--model-deployment takes precedence.",
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go:29
- [azd-code-reviewer] This new process environment variable is required to discover or select the private-preview flow, but it is only described in the PR text. Add it to the extension's durable documentation with its default-off behavior and accepted values (
1,true, oryes, case-insensitive), so users of the shipped extension can enable the feature correctly.
const promptVoicePreviewEnvVar = "AZD_AI_AGENT_ENABLE_PROMPT_VOICE"
cli/azd/extensions/azure.ai.agents/internal/project/agent_definition.go:1036
- [azd-code-reviewer] Add a round-trip test for the normal inline
azure.yamlpath: create properties withVoiceAgentDefinitionToServiceProperties, then read them throughVoiceAgentFromResolvedServiceorresolveVoiceAgentForDeployand assertmodelType, model, instructions, voice, and store. The current dispatch tests only exerciseAGENT_DEFINITION_PATH, so the init-produced inline representation that deploy normally consumes is unverified.
func VoiceAgentFromResolvedService(
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:1875
- [azd-code-reviewer] This routing override is not documented outside the PR description. Because deployment reads it from azd environment values rather than directly from the process environment, document the expected host format, the default public-route behavior, and that it must be configured with
azd env set AZURE_VOICE_OVERRIDDEN_HOST <host>; otherwise private-preview users are likely to export it and see no effect.
const voiceOverriddenHostEnvKey = "AZURE_VOICE_OVERRIDDEN_HOST"
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:3352
- [azd-code-reviewer] This calls the hosted-agent next-step resolver for a voice-only service. When the project endpoint already exists,
ResolveAfterInittakes its default branch and printsazd ai agent runplus a local invoke command, but prompt-voice has no local runtime and this PR explicitly leaves native run/invoke out of scope. Make the next-step state/resolver voice-aware so voice init emits only supported provision/deploy guidance.
_ = printAllNextIfTerminal(os.Stdout, nextstep.ResolveAfterInit(state, readmeExistsForProject(ctx, a.azdClient)))
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go:33
- [azd-code-reviewer] This required private-preview switch is not documented in the extension README or the environment-variable reference; the PR description is the only place that explains it. Preview users will not have durable guidance on how to enable the otherwise hidden prompt and flags. Document the accepted values and default-off behavior with the extension's environment variables.
const promptVoicePreviewEnvVar = "AZD_AI_AGENT_ENABLE_PROMPT_VOICE"
func promptVoicePreviewEnabled() bool {
value := strings.TrimSpace(os.Getenv(promptVoicePreviewEnvVar))
return strings.EqualFold(value, "1") || strings.EqualFold(value, "true") || strings.EqualFold(value, "yes")
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go:530
- [azd-code-reviewer] A whitespace-only
model.idpasses this required-field check (and the new schema/parser checks) and is then posted as the model name, guaranteeing a service-side failure instead of a local manifest error. Trim the ID before validating and serializing it, and align the schema/parser validation so editor and runtime behavior agree.
func CreateVoiceAgentAPIRequest(voiceAgent VoiceAgent) (*agent_api.CreateAgentRequest, error) {
if voiceAgent.Model == nil || voiceAgent.Model.Id == "" {
return nil, fmt.Errorf("model.id is required for a prompt-voice agent")
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go:555
- [azd-code-reviewer] Whitespace-only
voicevalues pass validation, enter this branch, and are then trimmed to an empty OpenAI voice name bybuildVoiceConfig, so a manifest that is effectively blank sends an invalid request instead of using the documented default. Trim before the emptiness check (or reject blank values during validation).
voiceName := defaultVoiceName
if voiceAgent.Voice != nil && *voiceAgent.Voice != "" {
voiceName = *voiceAgent.Voice
}
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go:33
- [azd-code-reviewer] This new environment variable is missing from the
azure.ai.agentssection ofcli/azd/docs/environment-variables.md, which is the repository's required source of truth for variables read by azd. Document that it gates private-preview prompt-voice initialization, defaults to disabled, and accepts1,true, oryescase-insensitively.
const promptVoicePreviewEnvVar = "AZD_AI_AGENT_ENABLE_PROMPT_VOICE"
func promptVoicePreviewEnabled() bool {
value := strings.TrimSpace(os.Getenv(promptVoicePreviewEnvVar))
return strings.EqualFold(value, "1") || strings.EqualFold(value, "true") || strings.EqualFold(value, "yes")
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:1875
- [azd-code-reviewer]
AZURE_VOICE_OVERRIDDEN_HOSTis a new extension-specific routing control but is absent fromcli/azd/docs/environment-variables.md. Add it to theazure.ai.agentsvariables (or the internal/debug section if unsupported), including that deploy reads it from the active azd environment rather than directly from the process environment.
// voiceOverriddenHostEnvKey optionally routes the /voice_agents call directly to
// a regional data-plane host (bypassing the public Foundry APIM, whose voice
// route may not yet be rolled out). When unset, default endpoint routing is used.
//
//nolint:gosec // env var key name, not a credential
const voiceOverriddenHostEnvKey = "AZURE_VOICE_OVERRIDDEN_HOST"
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Suppressed comments (4)
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go:470
- [azd-code-reviewer] The PR description says the default input transcription model is
whisper-1, while this implementation and its tests sendazure-speech. Align the implementation with the stated contract, or update the PR description ifazure-speechis the intended service value.
// defaultVoiceInputTranscriptionModel enables user-speech transcription events.
// azure-speech is accepted by both realtime and cascaded voice pipelines.
defaultVoiceInputTranscriptionModel = "azure-speech"
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go:975
- [azd-code-reviewer] This hardcoded value changes the existing adopt flow for container deployments. When
usesContaineris true,skipACRis false; before this refactor that also meant no hosted-region filtering, but this now filters those projects and can prevent an otherwise unchanged container sample from being adopted. Preserve the previous non-voice behavior by deriving this value fromskipACR.
filterHostedRegions := true
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go:556
- [azd-code-reviewer] A whitespace-only
voicevalue bypasses the default because it is not equal to"";buildVoiceConfigthen trims it and sends an OpenAI voice with an empty name. Treat a trimmed-empty value like an omitted value so hand-edited manifests do not fail at the service.
voiceName := defaultVoiceName
if voiceAgent.Voice != nil && *voiceAgent.Voice != "" {
voiceName = *voiceAgent.Voice
}
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go:33
- [azd-code-reviewer] This environment variable is the only way to expose prompt-voice initialization, but it is documented only in the PR description. Add a durable private-preview section to the extension README covering the default-off behavior, accepted truthy values, and interactive/non-interactive enablement so the workflow remains discoverable after merge.
const promptVoicePreviewEnvVar = "AZD_AI_AGENT_ENABLE_PROMPT_VOICE"
func promptVoicePreviewEnabled() bool {
value := strings.TrimSpace(os.Getenv(promptVoicePreviewEnvVar))
return strings.EqualFold(value, "1") || strings.EqualFold(value, "true") || strings.EqualFold(value, "yes")
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated 2 comments.
Suppressed comments (6)
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:565
- [azd-code-reviewer] Once this service resolves as voice, an empty voice endpoint must not fall through to the hosted path. If the service was changed from hosted to prompt-voice, stale NAME/VERSION and protocol endpoint variables can make
Endpointsreturn the old hosted endpoint. Handle the voice kind as an exclusive branch and require its NAME+ENDPOINT contract.
if isVoice, err := agentkind.IsPromptVoice(
serviceConfig, projectRoot, agentDefinitionPath,
); err == nil && isVoice && azdEnv[agentEndpointKey] != "" {
return []string{azdEnv[agentEndpointKey]}, nil
}
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:1916
- [azd-code-reviewer] This reads only the persisted azd environment map, so exporting
AZURE_VOICE_OVERRIDDEN_HOSTin the process has no effect. The provider already exposesdependencyEnvValue, which checks the azd environment and thenos.Getenv; use it so this environment variable behaves as advertised.
agentObject, err := agentClient.CreateVoiceAgent(
ctx, request, agent_api.AgentEndpointAPIVersion, azdEnv[voiceOverriddenHostEnvKey],
)
cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go:790
- [azd-code-reviewer] A voice service can retain
AGENT_<KEY>_VERSIONwhen an existing hosted service is changed to prompt-voice, and the voice deploy path never clears it. Returning here then reports the voice service as deployed even when its required ENDPOINT is absent. For voice services, ignore VERSION and require the endpoint marker.
if value != "" {
return true
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go:470
- [azd-code-reviewer] The PR summary says the default input transcription model is
whisper-1, but this request emitsazure-speech. Confirm the intended service contract and either usewhisper-1(with updated tests) or correct the PR description so the documented and deployed pipelines agree.
// defaultVoiceInputTranscriptionModel enables user-speech transcription events.
// azure-speech is accepted by both realtime and cascaded voice pipelines.
defaultVoiceInputTranscriptionModel = "azure-speech"
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go:33
- [azd-code-reviewer] This new preview gate is user-configured but is absent from the environment-variable reference. Document that it defaults to disabled and list the accepted truthy values so users can reliably enable the private-preview flow.
const promptVoicePreviewEnvVar = "AZD_AI_AGENT_ENABLE_PROMPT_VOICE"
func promptVoicePreviewEnabled() bool {
value := strings.TrimSpace(os.Getenv(promptVoicePreviewEnvVar))
return strings.EqualFold(value, "1") || strings.EqualFold(value, "true") || strings.EqualFold(value, "yes")
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:1875
- [azd-code-reviewer] This new routing override is not documented in the environment-variable reference. Add it to the extension/debug variables documentation with the expected hostname format, where it is read from, and an unsupported/private-preview warning; otherwise users cannot safely configure or troubleshoot the temporary route.
// voiceOverriddenHostEnvKey optionally routes the /voice_agents call directly to
// a regional data-plane host (bypassing the public Foundry APIM, whose voice
// route may not yet be rolled out). When unset, default endpoint routing is used.
//
//nolint:gosec // env var key name, not a credential
const voiceOverriddenHostEnvKey = "AZURE_VOICE_OVERRIDDEN_HOST"
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 35 changed files in this pull request and generated no new comments.
Suppressed comments (4)
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go:555
- [azd-code-reviewer] A whitespace-only
voicebypasses the default here, thenbuildVoiceConfigtrims it and emits an OpenAI voice with an empty name. This produces an invalid request instead of the documented default. Trim before deciding whether the override is present.
if voiceAgent.Voice != nil && *voiceAgent.Voice != "" {
voiceName = *voiceAgent.Voice
cli/azd/extensions/azure.ai.agents/internal/project/agent_definition.go:963
- [azd-code-reviewer] The shared kind resolver can classify a legacy on-disk
agent.yamlasprompt-voice, but this line reparses only inline/config properties. When the kind exists only in the service directory, this returnsfound=false, soDeployfalls into the container path and rejects the valid voice definition. After inline resolution misses, load the sameagent.yaml/agent.ymlwithvoiceAgentFromDefinitionFile, and add a dispatch regression for this path.
return VoiceAgentFromResolvedService(svc, projectRoot)
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go:33
- [azd-code-reviewer] This new
os.Getenvfeature gate is not added to the repository's environment-variable reference. Because both related flags are hidden, users invited to the private preview otherwise have no checked-in documentation for enabling it or for the accepted values (1,true, andyes). Add the variable and its default/accepted format to the appropriate azd environment-variable documentation.
const promptVoicePreviewEnvVar = "AZD_AI_AGENT_ENABLE_PROMPT_VOICE"
func promptVoicePreviewEnabled() bool {
value := strings.TrimSpace(os.Getenv(promptVoicePreviewEnvVar))
return strings.EqualFold(value, "1") || strings.EqualFold(value, "true") || strings.EqualFold(value, "yes")
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go:470
- [azd-code-reviewer] The PR summary says the default input transcription model is
whisper-1, but the generated payload usesazure-speech. Since this changes the deployed agent's runtime configuration, align the default and tests with the stated behavior or update the PR description ifazure-speechis intentional.
This issue also appears on line 554 of the same file.
defaultVoiceInputTranscriptionModel = "azure-speech"
Summary
Closes #9336
Adds azd support for a new declarative
prompt-voiceagent kind that creates a managed speech-to-speech (voice) agent on Azure AI Foundry, end-to-end throughazd init→azure.yaml→azd deploy.Scope (intentionally narrow):
prompt-voice+ managed model only — i.e. scaffold (init) and deploy only. BYOM, hosted-voice, tools, avatar, and cascaded models are deliberately left as follow-up PRs. azd-native textinvoke,list, and a Portal Playground link are out of scope for this PR (see Follow-ups).What's included
agent_yaml/yaml.go,parse.go): newprompt-voiceauthoring kind +VoiceAgentstruct/parsing/validation.agent_yaml/map.go): translate authoring kindprompt-voice→ data-plane service kindvoice; default the audio pipeline (PCM16 @ 24 kHz,server_vad,whisper-1, DragonHD default voice); defaultmodel_type: managed; pass throughmodel_type: self_deployedfor BYOM.agent_api/models.go,operations.go):VoiceAgentDefinitionwire structs +CreateVoiceAgentwith the required preview headerFoundry-Features: VoiceAgents=V1Preview.project/agent_definition.go,service_target_agent.go): voice-aware definition read/write + an isolateddeployVoiceAgentdeploy path — the existing hosted/container path is byte-for-byte unchanged.cmd/init.go,init_from_templates_helpers.go): private-preview gated prompt-voice scaffold path, voice manifest synthesis, and a new interactive prompt option when explicitly enabled.azure.ai.agent.jsongains theprompt-voicekind + voice service properties; unit tests cover the map translation, voice-type selection, and manifest parse/validation.Compatibility
All changes are additive — new
casebranches with untoucheddefaults andomitemptyfields. Existingazd init/ invoke / deploy / list flows and the hosted/workflow code paths are unchanged.Testing
Automated
go build ./...,go vet ./..., andcspellclean.map_voice_test.go,parse_voice_test.go,init_test.go,nextstep/state_test.go,voice_deploy_dispatch_test.go) plus the existing suite pass.Live validation
CreateVoiceAgentcode appears in the project list and its storeddefinitionis byte-identical to the service's own managed reference agent (kind:voice/model_type:managed/ audio pipeline / voice config). Connect reachessession.created.How to test end-to-end
Prompt voice init is private-preview gated. Set
AZD_AI_AGENT_ENABLE_PROMPT_VOICE=truebefore running either interactive or no-prompt prompt-voice init.Build the branch binary and install the branch build of the
azure.ai.agentsextension first:1) Interactive (recommended)
azd ai agent initfollows the same prompt flow as other agent kinds (agent name, then the standard Foundry-project selection).--model(defaultgpt-realtime) and--voiceare flags, not prompts — pass them on the same command to override, e.g.azd ai agent init --model gpt-realtime --voice alloy.azd provisionandazd deployadd no voice-specific interaction — they behave exactly like the existing hosted/code experience.infra/is required. Wheninfra/is absent, the Foundry provider synthesizes the embedded ARM template in-memory and resolves the required parameters via the interactive prompts.--infrais optional (only needed to eject IaC to disk).azure.yamlservice:host: azure.ai.agent,kind: prompt-voice,modelType: managed,model: { id: gpt-realtime }. To test BYOM, edit the service tomodelType: self_deployedand setmodel.idto an existing Foundry realtime model deployment beforeazd deploy.2) Non-interactive (CI / scripted)
--no-promptcannot prompt for the three required provision parameters, so eject IaC once and set them explicitly:3) Runtime check (client)
azd-native text
invokeis out of scope for this PR (managed voice agents are Voice Live realtime over WebSocket; the/voice_agentsdata plane has no HTTP text-invoke endpoint). Validate runtime instead with a voice client connecting to the deployed agent — a session reachessession.createdand supports voice/text turns. A read-only control-planeGET /voice_agents/<name>(with headerFoundry-Features: VoiceAgents=V1Preview) is a quick sanity check: expectstate=enabled,definition.model_type=managed.Known gaps
session.audio.output.voicestring coercion at the Voice Live layer, and a demo-clientsession.updateframing issue) — being investigated; not specific to this change.Follow-ups
invoke,list, and Portal Playground link for voice agentsazd ai agent deletefor voice services — the current delete path targets/agents/<name>and returnsCodeAgentNotFound(404) for a voice agent, which lives under/voice_agents/<name>. Voice-aware teardown (and an idempotent redeploy/update path over the create-only/voice_agentsroute) is a follow-up.AZURE_VOICE_OVERRIDDEN_HOST/x-ms-overridden-hostonce the public Foundry APIM voice route is generally rolled out; will be filed as a tracking issue.Follow-ups / known gaps