1use std::sync::Arc;
4use std::sync::Mutex;
5use std::sync::atomic::AtomicBool;
6use std::sync::atomic::Ordering;
7
8use ferrin_spec::DynRealtimeModel;
9use ferrin_spec::JsonValue;
10use ferrin_spec::ToolCallId;
11use ferrin_spec::ToolName;
12use ferrin_spec::realtime_model::ClientSecret;
13use ferrin_spec::realtime_model::ConversationItem;
14use ferrin_spec::realtime_model::ConversationRole;
15use ferrin_spec::realtime_model::RealtimeClientEvent;
16use ferrin_spec::realtime_model::RealtimeServerEvent;
17use ferrin_spec::realtime_model::RealtimeSessionConfig;
18use ferrin_tool::Tool;
19use ferrin_tool::ToolContext;
20use ferrin_tool::ToolSet;
21use ferrin_tool::execute_to_completion;
22use futures_util::SinkExt;
23use futures_util::StreamExt;
24use http::HeaderValue;
25use http::header::SEC_WEBSOCKET_PROTOCOL;
26use http::header::USER_AGENT;
27use tokio::net::TcpStream;
28use tokio::sync::mpsc;
29use tokio::task::JoinSet;
30use tokio_tungstenite::MaybeTlsStream;
31use tokio_tungstenite::WebSocketStream;
32use tokio_tungstenite::connect_async_tls_with_config;
33use tokio_tungstenite::tungstenite::Message;
34use tokio_tungstenite::tungstenite::client::IntoClientRequest;
35use tokio_util::sync::CancellationToken;
36
37use super::RealtimeEvent;
38use super::tools::ToolTurn;
39use crate::error::Error;
40
41type Socket = WebSocketStream<MaybeTlsStream<TcpStream>>;
42
43const OUTBOUND_BUFFER: usize = 64;
45
46struct Shared {
48 model: Arc<dyn DynRealtimeModel>,
49 outbound: mpsc::Sender<JsonValue>,
50 cancellation: CancellationToken,
51 closed: AtomicBool,
52 tool_turn: Mutex<ToolTurn>,
53}
54
55impl Shared {
56 fn tool_turn(&self) -> std::sync::MutexGuard<'_, ToolTurn> {
57 self.tool_turn
58 .lock()
59 .unwrap_or_else(std::sync::PoisonError::into_inner)
60 }
61}
62
63#[derive(Clone)]
67pub struct RealtimeHandle {
68 shared: Arc<Shared>,
69}
70
71impl std::fmt::Debug for RealtimeHandle {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 f.debug_struct("RealtimeHandle")
74 .field("provider", self.shared.model.provider())
75 .field("model_id", self.shared.model.model_id())
76 .field("closed", &self.is_closed())
77 .finish()
78 }
79}
80
81impl RealtimeHandle {
82 pub async fn send(&self, event: RealtimeClientEvent) -> Result<(), Error> {
89 let raw = self.shared.model.serialize_client_event(event).await?;
90 self.send_raw(raw).await
91 }
92
93 pub async fn send_raw(&self, raw: JsonValue) -> Result<(), Error> {
99 self.shared
100 .outbound
101 .send(raw)
102 .await
103 .map_err(|_| closed_error())
104 }
105
106 pub async fn send_text(&self, text: impl Into<String>) -> Result<(), Error> {
112 self.send(RealtimeClientEvent::ConversationItemCreate {
113 item: ConversationItem::TextMessage {
114 role: ConversationRole::User,
115 text: text.into(),
116 },
117 })
118 .await?;
119 self.send(RealtimeClientEvent::ResponseCreate { options: None })
120 .await
121 }
122
123 pub async fn add_tool_output(&self, call_id: &str, output: &JsonValue) -> Result<(), Error> {
130 let name = self
131 .shared
132 .tool_turn()
133 .name(call_id)
134 .map(|name| name.as_str().to_owned());
135 self.send(RealtimeClientEvent::ConversationItemCreate {
136 item: ConversationItem::FunctionCallOutput {
137 call_id: call_id.to_owned(),
138 name,
139 output: output.to_string(),
140 },
141 })
142 .await?;
143 let request_response = self.shared.tool_turn().output_submitted(call_id);
144 if request_response {
145 self.send(RealtimeClientEvent::ResponseCreate { options: None })
146 .await?;
147 }
148 Ok(())
149 }
150
151 pub async fn request_response(&self) -> Result<(), Error> {
157 self.send(RealtimeClientEvent::ResponseCreate { options: None })
158 .await
159 }
160
161 pub fn close(&self) {
163 self.shared.cancellation.cancel();
164 }
165
166 #[must_use]
168 pub fn is_closed(&self) -> bool {
169 self.shared.closed.load(Ordering::Acquire)
170 }
171}
172
173fn closed_error() -> Error {
174 Error::message("realtime session is closed")
175}
176
177pub(super) struct StartOptions {
179 pub(super) model: Arc<dyn DynRealtimeModel>,
180 pub(super) secret: ClientSecret,
181 pub(super) config: RealtimeSessionConfig,
182 pub(super) tools: Arc<ToolSet>,
183 pub(super) tools_context: Option<JsonValue>,
184 pub(super) cancellation: CancellationToken,
185 pub(super) events: mpsc::Sender<RealtimeEvent>,
186}
187
188pub(super) async fn start(
191 options: StartOptions,
192 tasks: &mut JoinSet<()>,
193) -> Result<RealtimeHandle, Error> {
194 let StartOptions {
195 model,
196 secret,
197 config,
198 tools,
199 tools_context,
200 cancellation,
201 events,
202 } = options;
203 let ws_config = model.websocket_config(&secret.token, &secret.url);
204 let mut request = ws_config
205 .url
206 .as_str()
207 .into_client_request()
208 .map_err(Error::other)?;
209 if !ws_config.protocols.is_empty() {
210 let value = HeaderValue::from_str(&ws_config.protocols.join(", "))
211 .map_err(|error| Error::invalid_argument("protocols", error.to_string()))?;
212 request.headers_mut().insert(SEC_WEBSOCKET_PROTOCOL, value);
213 }
214 request
215 .headers_mut()
216 .insert(USER_AGENT, HeaderValue::from_static(crate::USER_AGENT));
217
218 let connect = connect_async_tls_with_config(request, None, false, None);
219 let (mut socket, _response) = tokio::select! {
220 result = connect => result.map_err(Error::other)?,
221 () = cancellation.cancelled() => return Err(Error::Cancelled),
222 };
223
224 let initial = model
225 .serialize_client_event(RealtimeClientEvent::SessionUpdate {
226 config: Box::new(config),
227 })
228 .await?;
229 socket
230 .send(Message::text(initial.to_string()))
231 .await
232 .map_err(Error::other)?;
233
234 let (outbound_tx, outbound_rx) = mpsc::channel(OUTBOUND_BUFFER);
235 let shared = Arc::new(Shared {
236 model,
237 outbound: outbound_tx,
238 cancellation,
239 closed: AtomicBool::new(false),
240 tool_turn: Mutex::new(ToolTurn::default()),
241 });
242 let handle = RealtimeHandle {
243 shared: Arc::clone(&shared),
244 };
245 tasks.spawn(run(Connection {
246 socket,
247 outbound: outbound_rx,
248 shared,
249 events,
250 tools,
251 tools_context,
252 }));
253 Ok(handle)
254}
255
256struct Connection {
257 socket: Socket,
258 outbound: mpsc::Receiver<JsonValue>,
259 shared: Arc<Shared>,
260 events: mpsc::Sender<RealtimeEvent>,
261 tools: Arc<ToolSet>,
262 tools_context: Option<JsonValue>,
263}
264
265async fn run(mut connection: Connection) {
266 let mut tool_tasks: JoinSet<()> = JoinSet::new();
267 loop {
268 let stop = tokio::select! {
269 biased;
270 () = connection.shared.cancellation.cancelled() => {
271 let _ = connection.socket.send(Message::Close(None)).await;
272 true
273 }
274 outbound = connection.outbound.recv() => match outbound {
275 Some(raw) => connection.send_raw(raw).await.is_err(),
276 None => {
277 let _ = connection.socket.send(Message::Close(None)).await;
278 true
279 }
280 },
281 frame = connection.socket.next() => match frame {
282 Some(Ok(Message::Text(text))) => {
283 connection.handle_text(text.as_str(), &mut tool_tasks).await
284 }
285 Some(Ok(Message::Binary(bytes))) => match std::str::from_utf8(&bytes) {
286 Ok(text) => connection.handle_text(text, &mut tool_tasks).await,
287 Err(_) => false,
288 },
289 Some(Ok(Message::Close(_))) | None => true,
290 Some(Ok(_)) => false,
291 Some(Err(error)) => {
292 connection.emit(Err(Error::other(error))).await;
293 true
294 }
295 },
296 Some(joined) = tool_tasks.join_next(), if !tool_tasks.is_empty() => {
297 if let Err(error) = joined
298 && !error.is_cancelled()
299 {
300 connection.emit(Err(Error::other(error))).await;
301 }
302 false
303 }
304 };
305 if stop {
306 break;
307 }
308 }
309 tool_tasks.shutdown().await;
310 connection.shared.closed.store(true, Ordering::Release);
311 connection.shared.cancellation.cancel();
312}
313
314impl Connection {
315 async fn emit(&self, item: RealtimeEvent) -> bool {
318 self.events.send(item).await.is_ok()
319 }
320
321 async fn send_raw(&mut self, raw: JsonValue) -> Result<(), ()> {
322 match self.socket.send(Message::text(raw.to_string())).await {
323 Ok(()) => Ok(()),
324 Err(error) => {
325 self.emit(Err(Error::other(error))).await;
326 Err(())
327 }
328 }
329 }
330
331 async fn handle_text(&mut self, text: &str, tool_tasks: &mut JoinSet<()>) -> bool {
333 let raw: JsonValue = match serde_json::from_str(text) {
334 Ok(raw) => raw,
335 Err(error) => {
336 tracing::debug!(error = %error, "ignoring non-JSON realtime message");
337 return false;
338 }
339 };
340 if let Some(reply) = self.shared.model.health_check_response(&raw)
341 && self.send_raw(reply).await.is_err()
342 {
343 return true;
344 }
345 let events = match self.shared.model.parse_server_event(raw) {
346 Ok(events) => events,
347 Err(error) => return !self.emit(Err(Error::from(error))).await,
348 };
349 for event in events {
350 if !self.handle_event(event, tool_tasks).await {
351 return true;
352 }
353 }
354 false
355 }
356
357 async fn handle_event(
360 &mut self,
361 event: RealtimeServerEvent,
362 tool_tasks: &mut JoinSet<()>,
363 ) -> bool {
364 let follow_up = match &event {
365 RealtimeServerEvent::FunctionCallArgumentsDone {
366 call_id,
367 name,
368 arguments,
369 ..
370 } => {
371 let name = ToolName::new(name.clone());
372 self.shared.tool_turn().call_started(call_id, &name);
373 Some(ToolCall {
374 call_id: call_id.clone(),
375 name,
376 arguments: arguments.clone(),
377 })
378 }
379 _ => None,
380 };
381 let response_done = matches!(event, RealtimeServerEvent::ResponseDone { .. });
382
383 if !self.emit(Ok(event)).await {
384 return false;
385 }
386 if let Some(call) = follow_up
387 && !self.start_tool_call(call, tool_tasks).await
388 {
389 return false;
390 }
391 if response_done && self.shared.tool_turn().response_done() {
392 let handle = RealtimeHandle {
393 shared: Arc::clone(&self.shared),
394 };
395 if let Err(error) = handle.request_response().await {
396 return self.emit(Err(error)).await;
397 }
398 }
399 true
400 }
401
402 async fn start_tool_call(&self, call: ToolCall, tool_tasks: &mut JoinSet<()>) -> bool {
405 let Some(tool) = self.tools.get(call.name.as_str()).map(Arc::clone) else {
406 let available = self.tools.names().cloned().collect();
407 return self
408 .emit(Err(Error::no_such_tool(call.name, available)))
409 .await;
410 };
411 if !tool.is_executable() {
412 return true;
415 }
416 let input: JsonValue = match serde_json::from_str(&call.arguments) {
417 Ok(input) => input,
418 Err(error) => {
419 return self
420 .emit(Err(Error::invalid_tool_input(
421 call.name,
422 call.arguments,
423 error,
424 )))
425 .await;
426 }
427 };
428 let input = match tool.validate_input(&call.name, input) {
429 Ok(input) => input,
430 Err(error) => {
431 return self
432 .emit(Err(Error::invalid_tool_input(
433 call.name,
434 call.arguments,
435 error,
436 )))
437 .await;
438 }
439 };
440 let ctx = ToolContext::new(ToolCallId::new(call.call_id.clone()))
441 .with_cancellation(self.shared.cancellation.child_token())
442 .with_tools_context(self.tools_context.clone());
443 let handle = RealtimeHandle {
444 shared: Arc::clone(&self.shared),
445 };
446 let events = self.events.clone();
447 tool_tasks.spawn(execute_tool_call(
448 tool,
449 input,
450 ctx,
451 call.call_id,
452 handle,
453 events,
454 ));
455 true
456 }
457}
458
459struct ToolCall {
460 call_id: String,
461 name: ToolName,
462 arguments: String,
463}
464
465async fn execute_tool_call(
466 tool: Arc<Tool>,
467 input: JsonValue,
468 ctx: ToolContext,
469 call_id: String,
470 handle: RealtimeHandle,
471 events: mpsc::Sender<RealtimeEvent>,
472) {
473 let Some(stream) = tool.execute(input, ctx) else {
474 return;
475 };
476 let result = match execute_to_completion(stream, |_| {}).await {
477 Ok(output) => handle.add_tool_output(&call_id, &output).await,
478 Err(error) => Err(Error::from(error)),
479 };
480 if let Err(error) = result {
481 let _ = events.send(Err(error)).await;
482 }
483}