Skip to main content

rvoip_vapi/
agent.rs

1//! High-level caller-to-Vapi attachment and paired lifecycle supervision.
2
3use std::fmt;
4use std::sync::{Arc, Mutex as StdMutex, Weak};
5
6use rvoip_core::adapter::{ConnectionAdapter, EndReason, OriginateRequest};
7use rvoip_core::connection::{Direction, Transport};
8use rvoip_core::error::{Result as RvoipResult, RvoipError};
9use rvoip_core::events::Event;
10use rvoip_core::ids::{BridgeId, ConnectionId};
11use rvoip_core::Orchestrator;
12use tokio::sync::{broadcast, watch};
13
14use crate::adapter::VapiAdapter;
15use crate::error::Result;
16use crate::events::VapiEvent;
17use crate::types::{VapiCallOptions, VapiPeerFailurePolicy};
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub enum VapiAgentOutcome {
22    CallerEnded,
23    AgentEnded,
24    EventStreamClosed,
25}
26
27/// A bridged caller/Vapi pair.
28///
29/// The supervisor remains alive independently of this handle so dropping the
30/// handle cannot silently disable symmetric teardown.
31pub struct VapiAgentCall {
32    orchestrator: Weak<Orchestrator>,
33    adapter: Arc<VapiAdapter>,
34    caller_connection_id: ConnectionId,
35    vapi_connection_id: ConnectionId,
36    bridge_id: BridgeId,
37    events: broadcast::Sender<VapiEvent>,
38    initial_events: StdMutex<Option<broadcast::Receiver<VapiEvent>>>,
39    completion: watch::Receiver<Option<VapiAgentOutcome>>,
40}
41
42impl VapiAgentCall {
43    pub fn caller_connection_id(&self) -> &ConnectionId {
44        &self.caller_connection_id
45    }
46
47    pub fn vapi_connection_id(&self) -> &ConnectionId {
48        &self.vapi_connection_id
49    }
50
51    pub fn bridge_id(&self) -> &BridgeId {
52        &self.bridge_id
53    }
54
55    pub fn subscribe_events(&self) -> broadcast::Receiver<VapiEvent> {
56        self.initial_events
57            .lock()
58            .unwrap_or_else(|poisoned| poisoned.into_inner())
59            .take()
60            .unwrap_or_else(|| self.events.subscribe())
61    }
62
63    pub async fn say(
64        &self,
65        content: impl Into<String>,
66        end_call_after_spoken: bool,
67        interrupt_assistant: bool,
68    ) -> Result<()> {
69        self.adapter
70            .say(
71                &self.vapi_connection_id,
72                content,
73                end_call_after_spoken,
74                interrupt_assistant,
75            )
76            .await
77    }
78
79    pub async fn add_message(
80        &self,
81        role: impl Into<String>,
82        content: impl Into<String>,
83        trigger_response: bool,
84    ) -> Result<()> {
85        self.adapter
86            .add_message(&self.vapi_connection_id, role, content, trigger_response)
87            .await
88    }
89
90    pub async fn mute_assistant(&self) -> Result<()> {
91        self.adapter.mute_assistant(&self.vapi_connection_id).await
92    }
93
94    pub async fn unmute_assistant(&self) -> Result<()> {
95        self.adapter
96            .unmute_assistant(&self.vapi_connection_id)
97            .await
98    }
99
100    /// End the Vapi leg. Under the default peer policy the supervisor then
101    /// ends the paired caller leg as well.
102    pub async fn end(&self) -> RvoipResult<()> {
103        if let Some(orchestrator) = self.orchestrator.upgrade() {
104            orchestrator
105                .end_connection(self.vapi_connection_id.clone(), EndReason::Normal)
106                .await
107        } else {
108            self.adapter
109                .end(self.vapi_connection_id.clone(), EndReason::Normal)
110                .await
111        }
112    }
113
114    pub async fn wait(&mut self) -> VapiAgentOutcome {
115        loop {
116            if let Some(outcome) = *self.completion.borrow() {
117                return outcome;
118            }
119            if self.completion.changed().await.is_err() {
120                return VapiAgentOutcome::EventStreamClosed;
121            }
122        }
123    }
124}
125
126impl fmt::Debug for VapiAgentCall {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        formatter
129            .debug_struct("VapiAgentCall")
130            .field("caller_connection_id", &self.caller_connection_id)
131            .field("vapi_connection_id", &self.vapi_connection_id)
132            .field("bridge_id", &self.bridge_id)
133            .finish()
134    }
135}
136
137impl VapiAdapter {
138    /// Originate a Vapi WebSocket call and bridge it to an existing caller.
139    ///
140    /// The adapter registers itself when no Vapi adapter is registered. A
141    /// different pre-existing Vapi adapter is rejected to keep route ownership
142    /// exact.
143    pub async fn attach_agent(
144        self: &Arc<Self>,
145        orchestrator: &Arc<Orchestrator>,
146        caller_connection_id: ConnectionId,
147        options: VapiCallOptions,
148    ) -> RvoipResult<VapiAgentCall> {
149        options.validate().map_err(RvoipError::from)?;
150        self.ensure_registered(orchestrator)?;
151
152        let session_id = orchestrator
153            .session_of(&caller_connection_id)
154            .ok_or_else(|| RvoipError::ConnectionNotFound(caller_connection_id.clone()))?;
155        let participant_id = {
156            let session = orchestrator
157                .session(&session_id)
158                .ok_or_else(|| RvoipError::SessionNotFound(session_id.clone()))?;
159            let session = session
160                .read()
161                .map_err(|_| RvoipError::InvalidState("session lock is poisoned"))?;
162            session
163                .connections
164                .get(&caller_connection_id)
165                .map(|connection| connection.participant_id.clone())
166                .ok_or_else(|| RvoipError::ConnectionNotFound(caller_connection_id.clone()))?
167        };
168
169        let mut core_events = orchestrator.subscribe_events();
170        let request = OriginateRequest::new(
171            session_id,
172            participant_id,
173            "vapi.websocket",
174            Direction::Outbound,
175            options.audio_format.capabilities(),
176        )
177        .with_transport(Transport::Vapi)
178        .with_context(options.clone());
179        let handle = orchestrator.originate_connection(request).await?;
180        let vapi_connection_id = handle.connection.id.clone();
181        let initial_events = self.subscribe_call_events(&vapi_connection_id)?;
182        let call_events = self.call_event_sender(&vapi_connection_id)?;
183
184        let bridge_id = match orchestrator
185            .bridge_connections(caller_connection_id.clone(), vapi_connection_id.clone())
186            .await
187        {
188            Ok(bridge_id) => bridge_id,
189            Err(error) => {
190                let _ = orchestrator
191                    .end_connection(
192                        vapi_connection_id,
193                        EndReason::Failed {
194                            detail: "Vapi attachment bridge failed".into(),
195                        },
196                    )
197                    .await;
198                return Err(error);
199            }
200        };
201
202        let (completion_tx, completion) = watch::channel(None);
203        let supervisor_orchestrator = Arc::clone(orchestrator);
204        let supervisor_adapter = Arc::clone(self);
205        let supervisor_caller = caller_connection_id.clone();
206        let supervisor_vapi = vapi_connection_id.clone();
207        let supervisor_bridge = bridge_id.clone();
208        tokio::spawn(async move {
209            let outcome = supervise_pair(
210                &supervisor_orchestrator,
211                &supervisor_adapter,
212                &mut core_events,
213                &supervisor_caller,
214                &supervisor_vapi,
215                &supervisor_bridge,
216                options.peer_failure_policy,
217            )
218            .await;
219            let _ = completion_tx.send(Some(outcome));
220        });
221
222        Ok(VapiAgentCall {
223            orchestrator: Arc::downgrade(orchestrator),
224            adapter: Arc::clone(self),
225            caller_connection_id,
226            vapi_connection_id,
227            bridge_id,
228            events: call_events,
229            initial_events: StdMutex::new(Some(initial_events)),
230            completion,
231        })
232    }
233
234    fn ensure_registered(self: &Arc<Self>, orchestrator: &Arc<Orchestrator>) -> RvoipResult<()> {
235        match orchestrator.adapter(Transport::Vapi) {
236            Ok(registered) => {
237                let this: Arc<dyn ConnectionAdapter> =
238                    Arc::clone(self) as Arc<dyn ConnectionAdapter>;
239                if Arc::ptr_eq(&registered, &this) {
240                    Ok(())
241                } else {
242                    Err(RvoipError::AdapterAlreadyRegistered(Transport::Vapi))
243                }
244            }
245            Err(RvoipError::NoAdapterForTransport(Transport::Vapi)) => {
246                orchestrator.register(Arc::clone(self) as Arc<dyn ConnectionAdapter>)
247            }
248            Err(error) => Err(error),
249        }
250    }
251}
252
253async fn supervise_pair(
254    orchestrator: &Arc<Orchestrator>,
255    adapter: &Arc<VapiAdapter>,
256    events: &mut broadcast::Receiver<Event>,
257    caller: &ConnectionId,
258    vapi: &ConnectionId,
259    bridge: &BridgeId,
260    peer_policy: VapiPeerFailurePolicy,
261) -> VapiAgentOutcome {
262    loop {
263        let event = match events.recv().await {
264            Ok(event) => event,
265            Err(broadcast::error::RecvError::Lagged(_)) => {
266                if orchestrator.session_of(caller).is_none() {
267                    let _ = orchestrator.unbridge_connections(bridge.clone()).await;
268                    let _ = adapter.end(vapi.clone(), EndReason::BridgeTorn).await;
269                    return VapiAgentOutcome::CallerEnded;
270                }
271                if !adapter.is_connection_live(vapi) {
272                    let _ = orchestrator.unbridge_connections(bridge.clone()).await;
273                    if peer_policy == VapiPeerFailurePolicy::EndCaller {
274                        let _ = orchestrator
275                            .end_connection(caller.clone(), EndReason::BridgeTorn)
276                            .await;
277                    }
278                    return VapiAgentOutcome::AgentEnded;
279                }
280                continue;
281            }
282            Err(broadcast::error::RecvError::Closed) => {
283                let _ = orchestrator.unbridge_connections(bridge.clone()).await;
284                let _ = adapter.end(vapi.clone(), EndReason::BridgeTorn).await;
285                if peer_policy == VapiPeerFailurePolicy::EndCaller {
286                    let _ = orchestrator
287                        .end_connection(caller.clone(), EndReason::BridgeTorn)
288                        .await;
289                }
290                return VapiAgentOutcome::EventStreamClosed;
291            }
292        };
293        let terminal_connection = match event {
294            Event::ConnectionEnded { connection_id, .. }
295            | Event::ConnectionFailed { connection_id, .. } => Some(connection_id),
296            _ => None,
297        };
298        let Some(terminal_connection) = terminal_connection else {
299            continue;
300        };
301        if terminal_connection == *caller {
302            let _ = orchestrator.unbridge_connections(bridge.clone()).await;
303            let _ = orchestrator
304                .end_connection(vapi.clone(), EndReason::BridgeTorn)
305                .await;
306            return VapiAgentOutcome::CallerEnded;
307        }
308        if terminal_connection == *vapi {
309            let _ = orchestrator.unbridge_connections(bridge.clone()).await;
310            if peer_policy == VapiPeerFailurePolicy::EndCaller {
311                let _ = orchestrator
312                    .end_connection(caller.clone(), EndReason::BridgeTorn)
313                    .await;
314            }
315            return VapiAgentOutcome::AgentEnded;
316        }
317    }
318}