[agentserver] Add typed Voice event relay - #48544
Conversation
|
Thank you for your contribution knit (@knitvoger)! We will review the pull request and get back to you soon. |
|
Azure Pipelines: Successfully started running 1 pipeline(s). 7 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.
Pull request overview
Adds a preview typed Voice Live Bridge relay to the Invocations package.
Changes:
- Adds typed Voice protocol models, validation, callbacks, and serialized sends.
- Adds a runnable full-duplex voice-agent sample and documentation.
- Adds comprehensive codec, host, session, and sample tests.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
.chronus/changes/wujin-voice_submodule-2026-7-12-16-36-34.md |
Records the feature. |
sdk/agentserver/azure-ai-agentserver-invocations/MANIFEST.in |
Packages sample support files. |
sdk/agentserver/azure-ai-agentserver-invocations/README.md |
Documents the Voice API and sample. |
sdk/agentserver/azure-ai-agentserver-invocations/api.md |
Adds APIView declarations. |
sdk/agentserver/azure-ai-agentserver-invocations/api.metadata.yml |
Updates APIView metadata. |
sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml |
Adds the Azure Core dependency. |
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/__init__.py |
Exports the public Voice API. |
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_codec.py |
Implements frame validation and serialization. |
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_models.py |
Defines immutable protocol models. |
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_session.py |
Implements serialized outbound sends. |
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_voice_host.py |
Implements callback dispatch and transport handling. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/basic_voice_agent/README.md |
Documents sample setup and deployment. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/basic_voice_agent/agent.manifest.yaml |
Configures Bridge compatibility. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/basic_voice_agent/basic_voice_agent.py |
Demonstrates application-owned lifecycle handling. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/basic_voice_agent/requirements.txt |
Declares the sample dependency. |
sdk/agentserver/azure-ai-agentserver-invocations/tests/voice/test_basic_voice_agent.py |
Tests sample lifecycle behavior. |
sdk/agentserver/azure-ai-agentserver-invocations/tests/voice/test_voice_codec.py |
Tests codec and model behavior. |
sdk/agentserver/azure-ai-agentserver-invocations/tests/voice/test_voice_host.py |
Tests WebSocket dispatch and admission. |
sdk/agentserver/azure-ai-agentserver-invocations/tests/voice/test_voice_session.py |
Tests session construction and send serialization. |
Suppressed comments (3)
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_voice_host.py:235
- This non-text-frame close also returns normally, causing the shared wrapper to record a normal 1000 close instead of 1003. Surface the actual terminal code to the wrapper after sending the close frame.
frame = raw_message.get("text")
if frame is None:
await websocket.close(code=1003, reason="Voice messages must be text frames")
return
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_voice_host.py:240
- After sending the codec-selected close code (including 1009 for oversized frames), returning makes
_invoke_user_handlerreport 1000. RaiseWebSocketDisconnectwithexc.close_codeso operational diagnostics match the close sent to the peer.
try:
event = decode_inbound_message(frame)
except VoiceProtocolError as exc:
await websocket.close(code=exc.close_code, reason="Invalid Voice message")
return
sdk/agentserver/azure-ai-agentserver-invocations/api.md:596
- This APIView artifact says
reconnectdefaults toFalse, butSessionStart.reconnectis required in_models.py. Reconcile the source signature and regenerate the API artifacts so reviewers and consumers see the actual public constructor contract.
def __init__(
id: str,
ts: str,
protocol_version: str,
reconnect: bool = False,
response_timeouts: ResponseTimeouts,
greeting: str | None,
no_input_timeout_ms: int | None,
caller: Mapping
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (3)
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_voice_host.py:275
- A direct
CancelledErrorwhilewebsocket.accept()is pending bypasses this assignment and escapes_ws_endpointbefore either finalization path runs. That drops the required first-terminal close diagnostic for handshake cancellations (the wrapped-cancellation test only verifies identity propagation). Record the cancelled 1011 outcome here and re-raise the same cancellation.
accept_error, voice_session, close_code, handler_exc, pending_error = await self._run_voice_connection_context(
websocket,
session_id,
)
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_voice_host.py:351
- These resets happen before
_complete_voice_endpoint, soon_connection_terminating,on_disconnect, endpoint error logs, and the structured close event all run without the Foundry request context or extracted W3C context. In particular, an async disconnect callback that performs a Foundry call loses the required call ID even though normal Voice callbacks receive it. Keep the contexts attached through application callback and diagnostic finalization, while retaining the context-free transport-close task.
platform_token.var.reset(platform_token)
finally:
if trace_token is not None:
trace_token.var.reset(trace_token)
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_voice_host.py:442
- A failure from
on_connection_terminatingis omitted from the structured outcome, so an otherwise normal peer disconnect is emitted witherror_code=Noneeven though application cleanup failed. The adjacent disconnect-callback path correctly reportsinternal_error; includetermination_errorin the same classification so close telemetry does not record this failure as success.
error_code="internal_error" if disconnect_error is not None else error_code,
This comment has been minimized.
This comment has been minimized.
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 (1)
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_voice_host.py:828
SessionEndis documented as Bridge-initiated session termination, but after dispatching it the receive loop remains active and theSessionstays writable. If the peer does not immediately disconnect, later frames and application sends are still accepted andon_connection_terminatingnever runs. Close the send gate when this event is received and return after its callback so normal endpoint finalization closes the transport.
callback = self._voice_callbacks.get(event.type)
if callback is not None:
await _await_with_cancellation_guard(callback(session, cast(InboundVoiceMessage, event)))
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 23 changed files in this pull request and generated no new comments.
Suppressed comments (1)
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_models.py:242
SessionStartis a public constructor, but a caller-supplieddictis stored by reference here. That contradicts the documented immutable caller context: mutating the original or a nested value changes an already-created frozen event. Deep-freeze this field in__post_init__, as the outboundvoicemappings already do.
caller: Mapping[str, Any] | None = None
[Pilot] PR Pipeline Failure AnalysisA CI pipeline failed on this pull request. Here is an automated analysis of what went wrong and how to get the build green. What failedTwo validation checks failed in the
All other packages (activity, core, optimization, responses) passed pylint cleanly. Recommended next steps
Raw pipeline analysis (azsdk ci analyze)
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (1)
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_voice_host.py:330
- Detach this token through the OpenTelemetry context API rather than reaching into
Token.var. The token belongs to the configured runtime, so assuming acontextvars.Tokencan fail with another runtime and skip context cleanup. This package already uses_otel_context.detach(...)withValueErrorhandling inazure/ai/agentserver/invocations/_invocation.py:592-595(and Core does the same in_tracing.py:509-513).
if trace_token is not None:
trace_token.var.reset(trace_token)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 22 changed files in this pull request and generated no new comments.
Suppressed comments (1)
sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/voice/_voice_host.py:330
attach()returns an OpenTelemetry runtime token, which is opaque and is only guaranteed to work withopentelemetry.context.detach(). Accessing.varassumes the defaultcontextvarsruntime and can raise during cleanup (and leak the Voice trace/baggage context) when another supported runtime is configured. Detach through the OpenTelemetry API instead.
trace_token.var.reset(trace_token)
Why
Enable existing text-in/text-out hosted agents to participate in managed Voice Live
calls without implementing audio, STT, TTS, VAD, or a private WebSocket protocol.
The platform-side Bridge owns media processing and turn-taking; the SDK exposes the
typed text/control surface used by hosted agents.
What
Add the preview
azure.ai.agentserver.invocations.voicesubmodule to the existingazure-ai-agentserver-invocationsPython package. It implements Voice Live BridgeProtocol
1.0over/invocations_ws, including:Sessioncontext;Voice ships inside Invocations; it is not a separate distribution or top-level namespace.
How
azure.ai.agentserver.invocations.voice.platform identity, W3C context extraction, and first-terminal close diagnostics.
the relay retains no dedupe ledger or cross-message protocol state.
generation tasks, cancellation, history, and reconnect restoration.
admission_timeout_msremains Bridge-owned, avoiding a competing SDK timer.azure-ai-agentserver-core>=2.1.0b1.Opt-in rides existing untyped agent metadata (
voiceLiveCompatible,invocations_ws,bridgeProtocolVersion), so no TypeSpec / service-model change is required.Spec: voice_live_bridge/spec.md · Design: hosted_text_agent_and_voice_live_bridge.md
Architecture and responsibility boundaries
Voice is deliberately a thin typed transport adapter (a "plumber"), not a conversation, response, or application-task runtime.
SDK-owned responsibilities
/invocations_wsroute wiring while preserving supported StarletteHostandMountauthority.Session, Foundry request context, and W3C trace context.Application-owned responsibilities
on_connection_terminatingis a cooperative synchronous notification that must return promptly;on_disconnectobserves an actual peer disconnect. The SDK does not supervise or force-stop application work.Bridge and infrastructure responsibilities
Explicit non-goals
The Voice relay retains no response lifecycle, conversation history, reconnect/replay/retry state, application-task supervisor, or cross-message dedupe ledger.