refactor(voice): move ConversationRelay logic onto ConversationRelayProvider - #116
Open
xinghaohuang91 wants to merge 2 commits into
Open
Conversation
…ConversationRelayProvider VoiceChannel owned every piece of ConversationRelay-specific behavior directly: the WebSocket setup/prompt/interrupt protocol loop, outbound call initiation, and the ConversationRelay callback webhook (TwiML building was already extracted to twiml.py in an earlier PR). Move all of it onto ConversationRelayProvider — VoiceChannel now only builds the provider and delegates through thin wrapper methods. Moved as-is — method bodies are unchanged apart from the necessary self.X -> self.channel.X renames for functionality VoiceChannel still owns (TAC, conversation bookkeeping, Calls API client). _merge_call_options/_build_call_kwargs move in full since they're only used by outbound, and outbound support itself is provider-specific — no reason to keep them on VoiceChannel for a hypothetical second provider. Also renames handle_conversation_relay_callback -> handle_twilio_provider_callback: this webhook is Twilio's ConversationRelay-specific mechanism, but Media Streams has its own independent statusCallback equivalent, so the channel-facing name shouldn't bake in "ConversationRelay" — VoiceProvider declares the neutral no-op default, ConversationRelayProvider implements it. Left on VoiceChannel unchanged: Calls-API webhook handling (on_call_status/ on_amd/on_recording and their event handlers), _get_twilio_client, end_call, get_conversation_session_by_call_sid, process_webhook — all genuinely provider-agnostic. No public API changes. Tests updated only at the mechanical level: private provider methods are now reached via channel._provider, and the channel argument no longer needs threading through since VoiceProvider now stores it at construction. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
xinghaohuang91
requested review from
ryanrishi,
ryanrouleau and
wenzhu1587
as code owners
August 26, 2026 20:39
There was a problem hiding this comment.
Pull request overview
Refactors voice transport behavior into a pluggable provider architecture while retaining generic lifecycle management in VoiceChannel.
Changes:
- Moves ConversationRelay WebSocket, TwiML, callback, and outbound-call logic into
ConversationRelayProvider. - Adds provider delegation methods to
VoiceChannel. - Updates server integration and tests for the provider boundary.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/tac/channels/voice/channel.py |
Delegates transport behavior to the provider. |
src/tac/channels/voice/conversation_relay.py |
Implements ConversationRelay provider behavior. |
src/tac/channels/voice/provider.py |
Defines the provider interface and defaults. |
src/tac/channels/voice/twiml.py |
Avoids a runtime configuration import cycle. |
src/tac/server/fastapi_server.py |
Uses the provider-neutral callback method. |
tests/test_voice_channel.py |
Updates voice tests for relocated internals. |
tests/test_relay_only_mode.py |
Updates relay-only callback and interrupt tests. |
Suppressed comments (1)
src/tac/channels/voice/channel.py:205
- Renaming this method removes the existing public
VoiceChannel.handle_conversation_relay_callbackentry point. Custom server integrations that post the<Connect action>payload through that documented, unfiltered method will now fail withAttributeError, contrary to the stated no-public-API-change goal. Keep the old name as a compatibility alias (optionally deprecated) while the neutral name becomes preferred.
async def handle_twilio_provider_callback(
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…erConfig Nothing read the VoiceProvider.memory_mode property except VoiceChannel's own constructor plumbing it through. Moving the field onto the config lets VoiceChannel.__init__ call super().__init__() before create_provider(), removing the two-step ordering workaround.
ryanrouleau
approved these changes
Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
VoiceChannelowned every piece of ConversationRelay-specific behavior directly: the WebSocket setup/prompt/interrupt protocol loop, outbound call initiation, and the ConversationRelay callback webhook. Moves all of it ontoConversationRelayProvider;VoiceChannelnow only builds the provider (config.create_provider(self, tac.config)) and delegates through thin wrapper methods.self.X→self.channel.Xrenames for functionalityVoiceChannelstill owns (TAC, conversation bookkeeping, Calls API client). Verified via a line-by-line diff against the pre-move code._merge_call_options/_build_call_kwargsmove in full — they're only used by outbound, and outbound support itself is provider-specific, so there's no reason to keep them onVoiceChannelfor a hypothetical second provider that may not support outbound at all.handle_conversation_relay_callback→handle_twilio_provider_callback: this webhook is Twilio's ConversationRelay-specific mechanism, but Media Streams has its own independentstatusCallbackequivalent, so the channel-facing name shouldn't bake in "ConversationRelay".VoiceProviderdeclares the neutral no-op default;ConversationRelayProviderimplements it.VoiceProvider.__init__(channel)now storesself.channeland gets its ownself.logger(named after the concrete provider's module, same convention asBaseChannel) — no more threadingchannelthrough every method call.VoiceChannelunchanged: Calls-API webhook handling (on_call_status/on_amd/on_recording+ their event handlers),_get_twilio_client,end_call,get_conversation_session_by_call_sid,process_webhook— all genuinely provider-agnostic.channel._provider, no morechannelargument to thread through.Test plan
make lint/make type-check/make checkall passvoice_streaming.pyexample — full flow (WS setup → CO conversation lookup → memory retrieval → hangup → WS cleanup → ConversationRelay callback → conversation ended) works, logs correctly attributed totac.channels.voice.conversation_relayvstac.channels.voice.channel