1use crate::config::{RealtimeConfig, SessionUpdateConfig, ToolDefinition};
7use crate::error::{RealtimeError, Result};
8use crate::events::{ServerEvent, ToolCall, ToolResponse};
9use crate::model::BoxedModel;
10use crate::session::ContextMutationOutcome;
11use async_trait::async_trait;
12use std::collections::HashMap;
13use std::sync::Arc;
14use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
15use tokio::sync::RwLock;
16
17#[derive(Debug, Clone, Default, PartialEq)]
19pub enum RunnerState {
20 #[default]
22 Idle,
23 Generating,
25 ExecutingTool,
27 PendingResumption {
39 config: Box<crate::config::RealtimeConfig>,
41 bridge_message: Option<String>,
43 attempts: u8,
45 },
46}
47
48#[async_trait]
50pub trait ToolHandler: Send + Sync {
51 async fn execute(&self, call: &ToolCall) -> Result<serde_json::Value>;
53}
54
55pub struct FnToolHandler<F>
57where
58 F: Fn(&ToolCall) -> Result<serde_json::Value> + Send + Sync,
59{
60 handler: F,
61}
62
63impl<F> FnToolHandler<F>
64where
65 F: Fn(&ToolCall) -> Result<serde_json::Value> + Send + Sync,
66{
67 pub fn new(handler: F) -> Self {
69 Self { handler }
70 }
71}
72
73#[async_trait]
74impl<F> ToolHandler for FnToolHandler<F>
75where
76 F: Fn(&ToolCall) -> Result<serde_json::Value> + Send + Sync,
77{
78 async fn execute(&self, call: &ToolCall) -> Result<serde_json::Value> {
79 (self.handler)(call)
80 }
81}
82
83#[allow(dead_code)]
85pub struct AsyncToolHandler<F, Fut>
86where
87 F: Fn(ToolCall) -> Fut + Send + Sync,
88 Fut: std::future::Future<Output = Result<serde_json::Value>> + Send,
89{
90 handler: F,
91}
92
93impl<F, Fut> AsyncToolHandler<F, Fut>
94where
95 F: Fn(ToolCall) -> Fut + Send + Sync,
96 Fut: std::future::Future<Output = Result<serde_json::Value>> + Send,
97{
98 pub fn new(handler: F) -> Self {
100 Self { handler }
101 }
102}
103
104#[async_trait]
106pub trait EventHandler: Send + Sync {
107 async fn on_audio(&self, _audio: &[u8], _item_id: &str) -> Result<()> {
109 Ok(())
110 }
111
112 async fn on_text(&self, _text: &str, _item_id: &str) -> Result<()> {
114 Ok(())
115 }
116
117 async fn on_transcript(&self, _transcript: &str, _item_id: &str) -> Result<()> {
119 Ok(())
120 }
121
122 async fn on_speech_started(&self, _audio_start_ms: u64) -> Result<()> {
124 Ok(())
125 }
126
127 async fn on_speech_stopped(&self, _audio_end_ms: u64) -> Result<()> {
129 Ok(())
130 }
131
132 async fn on_response_done(&self) -> Result<()> {
134 Ok(())
135 }
136
137 async fn on_disconnect(&self) -> Result<()> {
155 Ok(())
156 }
157
158 async fn on_error(&self, _error: &RealtimeError) -> Result<()> {
159 Ok(())
160 }
161}
162
163#[derive(Debug, Clone, Default)]
165pub struct NoOpEventHandler;
166
167#[async_trait]
168impl EventHandler for NoOpEventHandler {}
169
170#[derive(Debug, Clone)]
172struct PendingToolCall {
173 call_id: String,
174 name: String,
175 arguments: String,
176}
177
178#[derive(Clone)]
180pub struct RunnerConfig {
181 pub auto_execute_tools: bool,
183 pub auto_respond_tools: bool,
185 pub max_concurrent_tools: usize,
187}
188
189impl Default for RunnerConfig {
190 fn default() -> Self {
191 Self { auto_execute_tools: true, auto_respond_tools: true, max_concurrent_tools: 4 }
192 }
193}
194
195pub struct RealtimeRunnerBuilder {
197 model: Option<BoxedModel>,
198 config: RealtimeConfig,
199 runner_config: RunnerConfig,
200 tools: HashMap<String, (ToolDefinition, Arc<dyn ToolHandler>)>,
201 event_handler: Option<Arc<dyn EventHandler>>,
202}
203
204impl Default for RealtimeRunnerBuilder {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl RealtimeRunnerBuilder {
211 pub fn new() -> Self {
213 Self {
214 model: None,
215 config: RealtimeConfig::default(),
216 runner_config: RunnerConfig::default(),
217 tools: HashMap::new(),
218 event_handler: None,
219 }
220 }
221
222 pub fn model(mut self, model: BoxedModel) -> Self {
224 self.model = Some(model);
225 self
226 }
227
228 pub fn config(mut self, config: RealtimeConfig) -> Self {
230 self.config = config;
231 self
232 }
233
234 pub fn runner_config(mut self, config: RunnerConfig) -> Self {
236 self.runner_config = config;
237 self
238 }
239
240 pub fn instruction(mut self, instruction: impl Into<String>) -> Self {
242 self.config.instruction = Some(instruction.into());
243 self
244 }
245
246 pub fn voice(mut self, voice: impl Into<String>) -> Self {
248 self.config.voice = Some(voice.into());
249 self
250 }
251
252 pub fn tool(mut self, definition: ToolDefinition, handler: impl ToolHandler + 'static) -> Self {
254 let name = definition.name.clone();
255 self.tools.insert(name, (definition, Arc::new(handler)));
256 self
257 }
258
259 pub fn tool_fn<F>(self, definition: ToolDefinition, handler: F) -> Self
261 where
262 F: Fn(&ToolCall) -> Result<serde_json::Value> + Send + Sync + 'static,
263 {
264 self.tool(definition, FnToolHandler::new(handler))
265 }
266
267 pub fn tool_arc(mut self, definition: ToolDefinition, handler: Arc<dyn ToolHandler>) -> Self {
272 let name = definition.name.clone();
273 self.tools.insert(name, (definition, handler));
274 self
275 }
276
277 pub fn event_handler(mut self, handler: impl EventHandler + 'static) -> Self {
279 self.event_handler = Some(Arc::new(handler));
280 self
281 }
282
283 pub fn event_handler_arc(mut self, handler: Arc<dyn EventHandler>) -> Self {
288 self.event_handler = Some(handler);
289 self
290 }
291
292 pub fn build(self) -> Result<RealtimeRunner> {
294 let model = self.model.ok_or_else(|| RealtimeError::config("Model is required"))?;
295
296 let mut config = self.config;
298 if !self.tools.is_empty() {
299 let tool_defs: Vec<ToolDefinition> =
300 self.tools.values().map(|(def, _)| def.clone()).collect();
301 config.tools = Some(tool_defs);
302 }
303
304 let max_concurrent_tools = self.runner_config.max_concurrent_tools.max(1);
305
306 Ok(RealtimeRunner {
307 model,
308 config: Arc::new(RwLock::new(config)),
309 runner_config: self.runner_config,
310 tools: self.tools,
311 event_handler: self.event_handler.unwrap_or_else(|| Arc::new(NoOpEventHandler)),
312 session: Arc::new(RwLock::new(None)),
313 state: Arc::new(RwLock::new(RunnerState::Idle)),
314 pending_tool_response: AtomicBool::new(false),
315 tool_permits: Arc::new(tokio::sync::Semaphore::new(max_concurrent_tools)),
316 outstanding_tools: Arc::new(AtomicUsize::new(0)),
317 response_closed_awaiting_tools: Arc::new(AtomicBool::new(false)),
318 })
319 }
320}
321
322pub struct RealtimeRunner {
360 model: BoxedModel,
361 config: Arc<RwLock<RealtimeConfig>>,
362 runner_config: RunnerConfig,
363 tools: HashMap<String, (ToolDefinition, Arc<dyn ToolHandler>)>,
364 event_handler: Arc<dyn EventHandler>,
365 session: Arc<RwLock<Option<Arc<dyn crate::session::RealtimeSession>>>>,
366 state: Arc<RwLock<RunnerState>>,
367 pending_tool_response: AtomicBool,
370 tool_permits: Arc<tokio::sync::Semaphore>,
373 outstanding_tools: Arc<AtomicUsize>,
375 response_closed_awaiting_tools: Arc<AtomicBool>,
378}
379
380impl RealtimeRunner {
381 async fn session_handle(&self) -> Result<Arc<dyn crate::session::RealtimeSession>> {
383 let guard = self.session.read().await;
384 guard.as_ref().cloned().ok_or_else(|| RealtimeError::connection("Not connected"))
385 }
386
387 pub fn builder() -> RealtimeRunnerBuilder {
389 RealtimeRunnerBuilder::new()
390 }
391
392 pub async fn connect(&self) -> Result<()> {
394 let config = self.config.read().await.clone();
395 let session = self.model.connect(config).await?;
396 let mut guard = self.session.write().await;
397 *guard = Some(session.into());
398 Ok(())
399 }
400
401 pub async fn is_connected(&self) -> bool {
403 let guard = self.session.read().await;
404 guard.as_ref().map(|s| s.is_connected()).unwrap_or(false)
405 }
406
407 pub async fn session_id(&self) -> Option<String> {
409 let guard = self.session.read().await;
410 guard.as_ref().map(|s| s.session_id().to_string())
411 }
412
413 pub async fn send_client_event(&self, event: crate::events::ClientEvent) -> Result<()> {
421 match event {
422 crate::events::ClientEvent::UpdateSession { instructions, tools } => {
423 let update_config = SessionUpdateConfig(crate::config::RealtimeConfig {
424 instruction: instructions,
425 tools,
426 ..Default::default()
427 });
428 self.update_session(update_config).await
429 }
430 other => {
431 let session = self.session_handle().await?;
432 session.send_event(other).await
433 }
434 }
435 }
436
437 fn merge_config(base: &mut RealtimeConfig, update: &SessionUpdateConfig) {
450 if let Some(instruction) = &update.0.instruction {
451 base.instruction = Some(instruction.clone());
452 }
453 if let Some(tools) = &update.0.tools {
454 base.tools = Some(tools.clone());
455 }
456 if let Some(voice) = &update.0.voice {
457 base.voice = Some(voice.clone());
458 }
459 if let Some(temp) = update.0.temperature {
460 base.temperature = Some(temp);
461 }
462 if let Some(extra) = &update.0.extra {
463 base.extra = Some(extra.clone());
464 }
465 }
466
467 pub async fn update_session(&self, config: SessionUpdateConfig) -> Result<()> {
484 self.update_session_with_bridge(config, None).await
485 }
486
487 pub async fn update_session_with_bridge(
500 &self,
501 config: SessionUpdateConfig,
502 bridge_message: Option<String>,
503 ) -> Result<()> {
504 let mut full_config = self.config.write().await;
508 Self::merge_config(&mut full_config, &config);
509
510 let cloned_config = full_config.clone();
511 drop(full_config); let session = self.session_handle().await?;
515
516 match session.mutate_context(cloned_config).await? {
518 ContextMutationOutcome::Applied => {
521 tracing::info!("Context mutated natively mid-flight.");
522
523 if let Some(msg) = bridge_message {
526 let event = crate::events::ClientEvent::Message {
527 role: "user".to_string(),
528 parts: vec![adk_core::types::Part::Text { text: msg }],
529 };
530 session.send_event(event).await?;
531 }
532 Ok(())
533 }
534
535 ContextMutationOutcome::RequiresResumption(new_config) => {
538 drop(session); let mut state_guard = self.state.write().await;
542
543 if *state_guard == RunnerState::Idle {
544 drop(state_guard); tracing::info!("Runner is idle. Executing resumption immediately.");
547
548 if let Err(e) =
549 self.execute_resumption((*new_config).clone(), bridge_message.clone()).await
550 {
551 tracing::error!("Immediate resumption failed: {}. Queueing for retry.", e);
552 let mut fallback_state = self.state.write().await;
555 *fallback_state = RunnerState::PendingResumption {
556 config: Box::new(*new_config),
557 bridge_message,
558 attempts: 1,
559 };
560 return Err(e);
561 }
562 } else {
563 if let RunnerState::PendingResumption { .. } = *state_guard {
566 tracing::warn!(
567 "Runner already had a pending resumption. Overwriting with last-write-wins policy."
568 );
569 } else {
570 tracing::info!("Runner is busy ({:?}). Queueing resumption.", *state_guard);
571 }
572
573 *state_guard = RunnerState::PendingResumption {
575 config: new_config,
576 bridge_message,
577 attempts: 0,
578 };
579 }
580 Ok(())
581 }
582 }
583 }
584
585 async fn execute_resumption(
587 &self,
588 new_config: crate::config::RealtimeConfig,
589 bridge_message: Option<String>,
590 ) -> Result<()> {
591 tracing::warn!("Executing transport resumption with new configuration.");
592
593 let old_session = {
595 let mut write_guard = self.session.write().await;
596 write_guard.take()
597 };
598
599 if let Some(session) = old_session
602 && let Err(e) = session.close().await
603 {
604 tracing::warn!("Failed to cleanly close old session during resumption: {}", e);
605 }
606
607 let new_session = self.model.connect(new_config).await?;
611
612 {
614 let mut write_guard = self.session.write().await;
615 *write_guard = Some(new_session.into());
616 }
617
618 if let Some(msg) = bridge_message {
621 self.inject_bridge_message(msg).await?;
622 }
623
624 tracing::info!("Resumption complete. New transport established.");
625 Ok(())
626 }
627
628 async fn inject_bridge_message(&self, msg: String) -> Result<()> {
634 tracing::info!("Injecting bridge message post-resumption.");
635 let event = crate::events::ClientEvent::Message {
636 role: "user".to_string(),
637 parts: vec![adk_core::types::Part::Text { text: msg }],
638 };
639 let session = self.session_handle().await?;
640 session.send_event(event).await
641 }
642
643 pub async fn send_audio_chunk(&self, audio: &crate::audio::AudioChunk) -> Result<()> {
649 let session = self.session_handle().await?;
650 session.send_audio(audio).await
651 }
652
653 pub async fn send_audio(&self, audio_base64: &str) -> Result<()> {
660 let session = self.session_handle().await?;
661 session.send_audio_base64(audio_base64).await
662 }
663
664 pub async fn send_text(&self, text: &str) -> Result<()> {
666 let session = self.session_handle().await?;
667 session.send_text(text).await
668 }
669
670 pub async fn send_video_frame(&self, mime_type: &str, data_base64: &str) -> Result<()> {
674 let session = self.session_handle().await?;
675 session.send_video_frame(mime_type, data_base64).await
676 }
677
678 pub async fn commit_audio(&self) -> Result<()> {
680 let session = self.session_handle().await?;
681 session.commit_audio().await
682 }
683
684 pub async fn create_response(&self) -> Result<()> {
686 let session = self.session_handle().await?;
687 session.create_response().await
688 }
689
690 pub async fn interrupt(&self) -> Result<()> {
692 let session = self.session_handle().await?;
693 session.interrupt().await
694 }
695
696 pub async fn disconnect_reason(&self) -> Option<crate::session::DisconnectReason> {
722 self.session_handle().await.ok().and_then(|session| session.disconnect_reason())
723 }
724
725 pub async fn next_event(&self) -> Option<Result<ServerEvent>> {
726 let session = match self.session_handle().await {
727 Ok(session) => session,
728 Err(_) => {
729 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
730 return None;
731 }
732 };
733
734 tokio::task::yield_now().await;
736 session.next_event().await
737 }
738
739 pub async fn send_tool_response(&self, response: ToolResponse) -> Result<()> {
756 let session = self.session_handle().await?;
757 session.send_tool_response(response).await
758 }
759
760 pub async fn dispatch_tool_call(
769 &self,
770 call_id: &str,
771 name: &str,
772 arguments: &str,
773 ) -> Result<()> {
774 self.execute_tool_call(call_id, name, arguments).await
775 }
776
777 pub async fn send_tool_result(&self, call_id: &str, output: serde_json::Value) -> Result<()> {
791 if !self.runner_config.auto_respond_tools {
792 return Ok(());
793 }
794
795 if let Ok(session) = self.session_handle().await {
796 session.send_tool_output(ToolResponse { call_id: call_id.to_string(), output }).await?;
797 self.pending_tool_response.store(true, Ordering::Release);
798 }
799 Ok(())
800 }
801
802 pub async fn run(&self) -> Result<()> {
804 use futures::stream::{FuturesUnordered, StreamExt};
805
806 let mut running_tools = FuturesUnordered::new();
810
811 loop {
812 let session = self.session_handle().await?;
813 let old_session_id = session.session_id().to_string();
814
815 let event = if running_tools.is_empty() {
818 session.next_event().await
819 } else {
820 tokio::select! {
821 biased;
822 Some(finished) = running_tools.next() => {
823 let () = finished?;
824 continue;
825 }
826 event = session.next_event() => event,
827 }
828 };
829
830 match event {
831 Some(Ok(event)) => {
832 if let Some(call) = self.handle_event(event).await? {
833 self.outstanding_tools.fetch_add(1, Ordering::AcqRel);
834 running_tools.push(self.run_tool_call(call));
835 }
836 }
837 Some(Err(e)) => {
838 self.event_handler.on_error(&e).await?;
839 return Err(e);
840 }
841 None => {
842 let current_session_id = self.session_id().await;
844 if let Some(id) = current_session_id
845 && id != old_session_id
846 {
847 continue;
849 }
850 while let Some(finished) = running_tools.next().await {
853 finished?;
854 }
855 self.event_handler.on_disconnect().await?;
858 break;
859 }
860 }
861 }
862 Ok(())
863 }
864
865 async fn run_tool_call(&self, call: PendingToolCall) -> Result<()> {
870 let permit = Arc::clone(&self.tool_permits).acquire_owned().await;
871 let result = self.execute_tool_call(&call.call_id, &call.name, &call.arguments).await;
872 drop(permit);
873
874 if self.outstanding_tools.fetch_sub(1, Ordering::AcqRel) == 1
879 && self.response_closed_awaiting_tools.swap(false, Ordering::AcqRel)
880 {
881 self.respond_after_tools().await?;
882 }
883 result
884 }
885
886 async fn handle_event(&self, event: ServerEvent) -> Result<Option<PendingToolCall>> {
888 match &event {
890 ServerEvent::ResponseCreated { .. } => {
891 let mut state = self.state.write().await;
892 if let RunnerState::Idle = *state {
893 *state = RunnerState::Generating;
894 }
895 }
896 ServerEvent::FunctionCallDone { .. } => {
897 let mut state = self.state.write().await;
898 if let RunnerState::Generating | RunnerState::Idle = *state {
899 *state = RunnerState::ExecutingTool;
900 }
901 }
902 _ => {}
903 }
904
905 match event {
906 ServerEvent::AudioDelta { delta, item_id, .. } => {
907 self.event_handler.on_audio(&delta, &item_id).await?;
908 }
909 ServerEvent::TextDelta { delta, item_id, .. } => {
910 self.event_handler.on_text(&delta, &item_id).await?;
911 }
912 ServerEvent::TranscriptDelta { delta, item_id, .. } => {
913 self.event_handler.on_transcript(&delta, &item_id).await?;
914 }
915 ServerEvent::SpeechStarted { audio_start_ms, .. } => {
916 self.event_handler.on_speech_started(audio_start_ms).await?;
917 }
918 ServerEvent::SpeechStopped { audio_end_ms, .. } => {
919 self.event_handler.on_speech_stopped(audio_end_ms).await?;
920 }
921 ServerEvent::ResponseDone { .. } => {
922 self.event_handler.on_response_done().await?;
923 self.respond_after_tools().await?;
926 self.check_resumption_queue().await?;
927 }
928 ServerEvent::FunctionCallDone { call_id, name, arguments, .. }
929 if self.runner_config.auto_execute_tools =>
930 {
931 return Ok(Some(PendingToolCall { call_id, name, arguments }));
934 }
935 ServerEvent::SessionUpdated { session, .. } => {
936 if let Some(token) = session.get("resumeToken").and_then(|t| t.as_str()) {
938 tracing::info!(
939 "Received Gemini sessionResumption token, saving for future reconnects."
940 );
941 let mut config = self.config.write().await;
942 let mut extra = config.extra.clone().unwrap_or_else(|| serde_json::json!({}));
943 extra["resumeToken"] = serde_json::Value::String(token.to_string());
944 config.extra = Some(extra);
945 }
946 }
947 ServerEvent::Error { error, .. } => {
948 let err = RealtimeError::server(error.code.unwrap_or_default(), error.message);
949 self.event_handler.on_error(&err).await?;
950 }
951 _ => {
952 }
954 }
955 Ok(None)
956 }
957
958 async fn check_resumption_queue(&self) -> Result<()> {
960 let mut state = self.state.write().await;
962
963 let pending =
965 if let RunnerState::PendingResumption { config, bridge_message, attempts } = &*state {
966 Some((config.clone(), bridge_message.clone(), *attempts))
967 } else {
968 None
969 };
970
971 if let Some((config, bridge_message, attempts)) = pending {
972 tracing::info!(
973 "Executing queued resumption after turn completion. (Attempt {})",
974 attempts + 1
975 );
976
977 *state = RunnerState::Idle;
979
980 drop(state);
982
983 if let Err(e) = self.execute_resumption((*config).clone(), bridge_message.clone()).await
985 {
986 tracing::error!("Resumption failed: {}.", e);
987
988 let mut fallback_state = self.state.write().await;
991
992 if attempts + 1 >= 3 {
994 tracing::error!(
995 "Maximum resumption attempts reached (3). Dropping queued mutation to prevent infinite loop."
996 );
997 *fallback_state = RunnerState::Idle;
998 } else {
999 tracing::info!("Restoring pending queue state for retry.");
1000 *fallback_state = RunnerState::PendingResumption {
1001 config,
1002 bridge_message,
1003 attempts: attempts + 1,
1004 };
1005 }
1006
1007 let _ = self.event_handler.on_error(&e).await;
1010 }
1011 } else {
1012 *state = RunnerState::Idle;
1014 }
1015 Ok(())
1016 }
1017
1018 async fn execute_tool_call(&self, call_id: &str, name: &str, arguments: &str) -> Result<()> {
1020 let handler = self.tools.get(name).map(|(_, h)| h.clone());
1021
1022 let result = if let Some(handler) = handler {
1023 let args: serde_json::Value = serde_json::from_str(arguments)
1024 .unwrap_or(serde_json::Value::Object(Default::default()));
1025
1026 let call =
1027 ToolCall { call_id: call_id.to_string(), name: name.to_string(), arguments: args };
1028
1029 match handler.execute(&call).await {
1030 Ok(value) => value,
1031 Err(e) => serde_json::json!({
1032 "error": e.to_string()
1033 }),
1034 }
1035 } else {
1036 serde_json::json!({
1037 "error": format!("Unknown tool: {}", name)
1038 })
1039 };
1040
1041 if self.runner_config.auto_respond_tools {
1042 let response = ToolResponse { call_id: call_id.to_string(), output: result };
1043
1044 if let Ok(session) = self.session_handle().await {
1045 session.send_tool_output(response).await?;
1051 self.pending_tool_response.store(true, Ordering::Release);
1052 }
1053 }
1054
1055 Ok(())
1056 }
1057
1058 pub async fn instruction(&self) -> Option<String> {
1063 self.config.read().await.instruction.clone()
1064 }
1065
1066 pub async fn prepend_instruction_context(&self, block: &str) {
1079 if block.is_empty() {
1080 return;
1081 }
1082
1083 let mut config = self.config.write().await;
1084 config.instruction = Some(match config.instruction.take() {
1085 Some(existing) if !existing.is_empty() => format!("{block}\n\n{existing}"),
1086 _ => block.to_string(),
1087 });
1088 }
1089
1090 pub async fn respond_after_tools(&self) -> Result<()> {
1099 if self.outstanding_tools.load(Ordering::Acquire) > 0 {
1104 self.response_closed_awaiting_tools.store(true, Ordering::Release);
1105 return Ok(());
1106 }
1107
1108 if self.pending_tool_response.swap(false, Ordering::AcqRel)
1109 && let Ok(session) = self.session_handle().await
1110 {
1111 session.create_response().await?;
1112 }
1113 Ok(())
1114 }
1115
1116 pub async fn close(&self) -> Result<()> {
1118 if let Ok(session) = self.session_handle().await {
1119 session.close().await?;
1120 }
1121 Ok(())
1122 }
1123}
1124
1125#[cfg(test)]
1126mod runner_tests {
1127 use super::*;
1128 use crate::audio::{AudioChunk, AudioFormat};
1129 use crate::events::{ClientEvent, ToolResponse};
1130 use crate::model::RealtimeModel;
1131 use crate::session::{BoxedSession, ContextMutationOutcome, RealtimeSession};
1132 use std::pin::Pin;
1133 use std::sync::atomic::AtomicUsize;
1134
1135 struct MockModel;
1137
1138 #[async_trait]
1139 impl RealtimeModel for MockModel {
1140 fn provider(&self) -> &str {
1141 "mock"
1142 }
1143 fn model_id(&self) -> &str {
1144 "mock"
1145 }
1146 fn supported_input_formats(&self) -> Vec<AudioFormat> {
1147 vec![]
1148 }
1149 fn supported_output_formats(&self) -> Vec<AudioFormat> {
1150 vec![]
1151 }
1152 fn available_voices(&self) -> Vec<&str> {
1153 vec![]
1154 }
1155 async fn connect(&self, _config: RealtimeConfig) -> Result<BoxedSession> {
1156 Err(RealtimeError::connection("mock model does not connect"))
1157 }
1158 }
1159
1160 #[derive(Default)]
1162 struct Counts {
1163 raw_audio: AtomicUsize,
1164 base64_audio: AtomicUsize,
1165 last_audio: parking_lot::Mutex<Option<AudioChunk>>,
1166 tool_output: AtomicUsize,
1167 tool_response: AtomicUsize,
1168 create_response: AtomicUsize,
1169 }
1170
1171 struct RecordingSession {
1172 counts: Arc<Counts>,
1173 }
1174
1175 #[async_trait]
1176 impl RealtimeSession for RecordingSession {
1177 fn session_id(&self) -> &str {
1178 "mock-session"
1179 }
1180 fn is_connected(&self) -> bool {
1181 true
1182 }
1183 async fn send_audio(&self, audio: &AudioChunk) -> Result<()> {
1184 self.counts.raw_audio.fetch_add(1, Ordering::SeqCst);
1185 *self.counts.last_audio.lock() = Some(audio.clone());
1186 Ok(())
1187 }
1188 async fn send_audio_base64(&self, _audio: &str) -> Result<()> {
1189 self.counts.base64_audio.fetch_add(1, Ordering::SeqCst);
1190 Ok(())
1191 }
1192 async fn send_text(&self, _text: &str) -> Result<()> {
1193 Ok(())
1194 }
1195 async fn send_tool_response(&self, _response: ToolResponse) -> Result<()> {
1196 self.counts.tool_response.fetch_add(1, Ordering::SeqCst);
1197 Ok(())
1198 }
1199 async fn send_tool_output(&self, _response: ToolResponse) -> Result<()> {
1200 self.counts.tool_output.fetch_add(1, Ordering::SeqCst);
1201 Ok(())
1202 }
1203 async fn commit_audio(&self) -> Result<()> {
1204 Ok(())
1205 }
1206 async fn clear_audio(&self) -> Result<()> {
1207 Ok(())
1208 }
1209 async fn create_response(&self) -> Result<()> {
1210 self.counts.create_response.fetch_add(1, Ordering::SeqCst);
1211 Ok(())
1212 }
1213 async fn interrupt(&self) -> Result<()> {
1214 Ok(())
1215 }
1216 async fn send_event(&self, _event: ClientEvent) -> Result<()> {
1217 Ok(())
1218 }
1219 async fn next_event(&self) -> Option<Result<ServerEvent>> {
1220 None
1221 }
1222 fn events(&self) -> Pin<Box<dyn futures::Stream<Item = Result<ServerEvent>> + Send + '_>> {
1223 Box::pin(futures::stream::empty())
1224 }
1225 async fn close(&self) -> Result<()> {
1226 Ok(())
1227 }
1228 async fn mutate_context(&self, _config: RealtimeConfig) -> Result<ContextMutationOutcome> {
1229 Ok(ContextMutationOutcome::Applied)
1230 }
1231 }
1232
1233 fn tool_def(name: &str) -> ToolDefinition {
1234 ToolDefinition { name: name.into(), description: None, parameters: None }
1235 }
1236
1237 fn ok_tool() -> FnToolHandler<impl Fn(&ToolCall) -> Result<serde_json::Value> + Send + Sync> {
1238 FnToolHandler::new(|_call: &ToolCall| Ok(serde_json::json!({ "ok": true })))
1239 }
1240
1241 fn function_call(call_id: &str, name: &str) -> ServerEvent {
1242 ServerEvent::FunctionCallDone {
1243 event_id: "evt".into(),
1244 response_id: "resp".into(),
1245 item_id: "item".into(),
1246 output_index: 0,
1247 call_id: call_id.into(),
1248 name: name.into(),
1249 arguments: "{}".into(),
1250 }
1251 }
1252
1253 fn response_done() -> ServerEvent {
1254 ServerEvent::ResponseDone { event_id: "evt".into(), response: serde_json::json!({}) }
1255 }
1256
1257 async fn runner_with_session(counts: Arc<Counts>) -> RealtimeRunner {
1258 let runner =
1259 RealtimeRunner::builder().model(Arc::new(MockModel) as BoxedModel).build().unwrap();
1260 let session = Arc::new(RecordingSession { counts }) as Arc<dyn RealtimeSession>;
1261
1262 *runner.session.write().await = Some(session);
1265 runner
1266 }
1267
1268 #[tokio::test]
1269 async fn send_audio_chunk_preserves_bytes_and_format_on_raw_path() {
1270 let counts = Arc::new(Counts::default());
1271 let runner = runner_with_session(Arc::clone(&counts)).await;
1272 let chunk = AudioChunk::pcm16_24khz(vec![1, 2, 3, 4]);
1273
1274 runner.send_audio_chunk(&chunk).await.unwrap();
1275
1276 assert_eq!(counts.raw_audio.load(Ordering::SeqCst), 1);
1277 assert_eq!(counts.base64_audio.load(Ordering::SeqCst), 0);
1278 let recorded = counts.last_audio.lock();
1279 let recorded = recorded.as_ref().unwrap();
1280 assert_eq!(recorded.data, chunk.data);
1281 assert_eq!(recorded.format, chunk.format);
1282 }
1283
1284 #[tokio::test]
1285 async fn send_audio_keeps_base64_compatibility_path() {
1286 let counts = Arc::new(Counts::default());
1287 let runner = runner_with_session(Arc::clone(&counts)).await;
1288
1289 runner.send_audio("AQIDBA==").await.unwrap();
1290
1291 assert_eq!(counts.raw_audio.load(Ordering::SeqCst), 0);
1292 assert_eq!(counts.base64_audio.load(Ordering::SeqCst), 1);
1293 }
1294
1295 #[tokio::test]
1299 async fn parallel_tool_calls_trigger_a_single_response() {
1300 let counts = Arc::new(Counts::default());
1301 let runner = RealtimeRunner::builder()
1302 .model(Arc::new(MockModel) as BoxedModel)
1303 .tool(tool_def("get_weather"), ok_tool())
1304 .tool(tool_def("get_time"), ok_tool())
1305 .build()
1306 .unwrap();
1307 *runner.session.write().await =
1308 Some(Arc::new(RecordingSession { counts: counts.clone() }) as Arc<dyn RealtimeSession>);
1309
1310 let scripted = Arc::new(ScriptedSession::new(
1314 counts.clone(),
1315 vec![
1316 function_call("c1", "get_weather"),
1317 function_call("c2", "get_time"),
1318 response_done(),
1319 ],
1320 ));
1321 *runner.session.write().await = Some(scripted as Arc<dyn RealtimeSession>);
1322 runner.run().await.unwrap();
1323
1324 assert_eq!(counts.tool_output.load(Ordering::SeqCst), 2, "both outputs sent");
1325 assert_eq!(counts.create_response.load(Ordering::SeqCst), 1, "exactly one response");
1326 assert_eq!(
1327 counts.tool_response.load(Ordering::SeqCst),
1328 0,
1329 "auto path must use send_tool_output, not the output+create combo"
1330 );
1331
1332 runner.handle_event(response_done()).await.unwrap();
1334 assert_eq!(counts.create_response.load(Ordering::SeqCst), 1, "no extra response");
1335 }
1336
1337 #[tokio::test]
1339 async fn plain_response_triggers_no_auto_response() {
1340 let counts = Arc::new(Counts::default());
1341 let runner =
1342 RealtimeRunner::builder().model(Arc::new(MockModel) as BoxedModel).build().unwrap();
1343 *runner.session.write().await =
1344 Some(Arc::new(RecordingSession { counts: counts.clone() }) as Arc<dyn RealtimeSession>);
1345
1346 runner.handle_event(response_done()).await.unwrap();
1347 assert_eq!(counts.create_response.load(Ordering::SeqCst), 0);
1348 assert_eq!(counts.tool_output.load(Ordering::SeqCst), 0);
1349 }
1350
1351 struct ScriptedSession {
1360 counts: Arc<Counts>,
1361 events: parking_lot::Mutex<std::collections::VecDeque<ServerEvent>>,
1362 }
1363
1364 impl ScriptedSession {
1365 fn new(counts: Arc<Counts>, events: Vec<ServerEvent>) -> Self {
1366 Self { counts, events: parking_lot::Mutex::new(events.into()) }
1367 }
1368 }
1369
1370 #[async_trait]
1371 impl RealtimeSession for ScriptedSession {
1372 fn session_id(&self) -> &str {
1373 "scripted-session"
1374 }
1375 fn is_connected(&self) -> bool {
1376 true
1377 }
1378 async fn send_audio(&self, _audio: &AudioChunk) -> Result<()> {
1379 Ok(())
1380 }
1381 async fn send_audio_base64(&self, _audio: &str) -> Result<()> {
1382 Ok(())
1383 }
1384 async fn send_text(&self, _text: &str) -> Result<()> {
1385 Ok(())
1386 }
1387 async fn send_tool_response(&self, _response: ToolResponse) -> Result<()> {
1388 self.counts.tool_response.fetch_add(1, Ordering::SeqCst);
1389 Ok(())
1390 }
1391 async fn send_tool_output(&self, _response: ToolResponse) -> Result<()> {
1392 self.counts.tool_output.fetch_add(1, Ordering::SeqCst);
1393 Ok(())
1394 }
1395 async fn commit_audio(&self) -> Result<()> {
1396 Ok(())
1397 }
1398 async fn clear_audio(&self) -> Result<()> {
1399 Ok(())
1400 }
1401 async fn create_response(&self) -> Result<()> {
1402 self.counts.create_response.fetch_add(1, Ordering::SeqCst);
1403 Ok(())
1404 }
1405 async fn interrupt(&self) -> Result<()> {
1406 Ok(())
1407 }
1408 async fn send_event(&self, _event: ClientEvent) -> Result<()> {
1409 Ok(())
1410 }
1411 async fn next_event(&self) -> Option<Result<ServerEvent>> {
1412 let event = self.events.lock().pop_front();
1413 event.map(Ok)
1414 }
1415 fn events(&self) -> Pin<Box<dyn futures::Stream<Item = Result<ServerEvent>> + Send + '_>> {
1416 Box::pin(futures::stream::empty())
1417 }
1418 async fn close(&self) -> Result<()> {
1419 Ok(())
1420 }
1421 async fn mutate_context(&self, _config: RealtimeConfig) -> Result<ContextMutationOutcome> {
1422 Ok(ContextMutationOutcome::Applied)
1423 }
1424 }
1425
1426 struct ConcurrencyProbe {
1428 in_flight: Arc<AtomicUsize>,
1429 peak: Arc<AtomicUsize>,
1430 barrier: Option<Arc<tokio::sync::Barrier>>,
1431 }
1432
1433 #[async_trait]
1434 impl ToolHandler for ConcurrencyProbe {
1435 async fn execute(&self, _call: &ToolCall) -> Result<serde_json::Value> {
1436 let now = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
1437 self.peak.fetch_max(now, Ordering::SeqCst);
1438
1439 match &self.barrier {
1440 Some(barrier) => {
1443 barrier.wait().await;
1444 }
1445 None => {
1446 tokio::task::yield_now().await;
1447 }
1448 }
1449
1450 self.in_flight.fetch_sub(1, Ordering::SeqCst);
1451 Ok(serde_json::json!({ "ok": true }))
1452 }
1453 }
1454
1455 fn audio_delta() -> ServerEvent {
1456 ServerEvent::AudioDelta {
1457 event_id: "evt".into(),
1458 response_id: "resp".into(),
1459 item_id: "item".into(),
1460 output_index: 0,
1461 content_index: 0,
1462 delta: vec![1, 2, 3],
1463 }
1464 }
1465
1466 #[tokio::test]
1467 async fn tool_calls_overlap_up_to_the_configured_bound() {
1468 let counts = Arc::new(Counts::default());
1469 let in_flight = Arc::new(AtomicUsize::new(0));
1470 let peak = Arc::new(AtomicUsize::new(0));
1471 let barrier = Arc::new(tokio::sync::Barrier::new(3));
1473
1474 let probe = || ConcurrencyProbe {
1475 in_flight: Arc::clone(&in_flight),
1476 peak: Arc::clone(&peak),
1477 barrier: Some(Arc::clone(&barrier)),
1478 };
1479
1480 let runner = RealtimeRunner::builder()
1481 .model(Arc::new(MockModel) as BoxedModel)
1482 .runner_config(RunnerConfig {
1483 auto_execute_tools: true,
1484 auto_respond_tools: true,
1485 max_concurrent_tools: 3,
1486 })
1487 .tool(tool_def("a"), probe())
1488 .tool(tool_def("b"), probe())
1489 .tool(tool_def("c"), probe())
1490 .build()
1491 .unwrap();
1492
1493 let scripted = Arc::new(ScriptedSession::new(
1494 Arc::clone(&counts),
1495 vec![function_call("c1", "a"), function_call("c2", "b"), function_call("c3", "c")],
1496 ));
1497 *runner.session.write().await = Some(scripted as Arc<dyn RealtimeSession>);
1498
1499 tokio::time::timeout(std::time::Duration::from_secs(5), runner.run())
1500 .await
1501 .expect("serial dispatch cannot satisfy a three-way barrier")
1502 .unwrap();
1503
1504 assert_eq!(peak.load(Ordering::SeqCst), 3, "all three tools must overlap");
1505 assert_eq!(counts.tool_output.load(Ordering::SeqCst), 3, "every output is sent");
1506 }
1507
1508 #[tokio::test]
1509 async fn the_bound_caps_how_many_tools_overlap() {
1510 let counts = Arc::new(Counts::default());
1511 let in_flight = Arc::new(AtomicUsize::new(0));
1512 let peak = Arc::new(AtomicUsize::new(0));
1513
1514 let probe = || ConcurrencyProbe {
1515 in_flight: Arc::clone(&in_flight),
1516 peak: Arc::clone(&peak),
1517 barrier: None,
1518 };
1519
1520 let runner = RealtimeRunner::builder()
1521 .model(Arc::new(MockModel) as BoxedModel)
1522 .runner_config(RunnerConfig {
1523 auto_execute_tools: true,
1524 auto_respond_tools: true,
1525 max_concurrent_tools: 2,
1526 })
1527 .tool(tool_def("a"), probe())
1528 .tool(tool_def("b"), probe())
1529 .tool(tool_def("c"), probe())
1530 .tool(tool_def("d"), probe())
1531 .build()
1532 .unwrap();
1533
1534 let scripted = Arc::new(ScriptedSession::new(
1535 Arc::clone(&counts),
1536 vec![
1537 function_call("c1", "a"),
1538 function_call("c2", "b"),
1539 function_call("c3", "c"),
1540 function_call("c4", "d"),
1541 ],
1542 ));
1543 *runner.session.write().await = Some(scripted as Arc<dyn RealtimeSession>);
1544
1545 runner.run().await.unwrap();
1546
1547 assert!(
1548 peak.load(Ordering::SeqCst) <= 2,
1549 "the bound was exceeded: peak {}",
1550 peak.load(Ordering::SeqCst)
1551 );
1552 assert_eq!(counts.tool_output.load(Ordering::SeqCst), 4, "all four still complete");
1553 }
1554
1555 struct WaitsForAudio {
1557 audio_seen: Arc<tokio::sync::Notify>,
1558 }
1559
1560 #[async_trait]
1561 impl ToolHandler for WaitsForAudio {
1562 async fn execute(&self, _call: &ToolCall) -> Result<serde_json::Value> {
1563 self.audio_seen.notified().await;
1564 Ok(serde_json::json!({ "ok": true }))
1565 }
1566 }
1567
1568 struct AudioSignaller {
1570 audio_seen: Arc<tokio::sync::Notify>,
1571 audio_events: Arc<AtomicUsize>,
1572 }
1573
1574 #[async_trait]
1575 impl EventHandler for AudioSignaller {
1576 async fn on_audio(&self, _audio: &[u8], _item_id: &str) -> Result<()> {
1577 self.audio_events.fetch_add(1, Ordering::SeqCst);
1578 self.audio_seen.notify_waiters();
1579 Ok(())
1580 }
1581 }
1582
1583 #[tokio::test]
1584 async fn audio_keeps_flowing_while_a_tool_runs() {
1585 let counts = Arc::new(Counts::default());
1586 let audio_seen = Arc::new(tokio::sync::Notify::new());
1587 let audio_events = Arc::new(AtomicUsize::new(0));
1588
1589 let runner = RealtimeRunner::builder()
1590 .model(Arc::new(MockModel) as BoxedModel)
1591 .tool(tool_def("slow"), WaitsForAudio { audio_seen: Arc::clone(&audio_seen) })
1592 .event_handler(AudioSignaller {
1593 audio_seen: Arc::clone(&audio_seen),
1594 audio_events: Arc::clone(&audio_events),
1595 })
1596 .build()
1597 .unwrap();
1598
1599 let scripted = Arc::new(ScriptedSession::new(
1602 Arc::clone(&counts),
1603 vec![function_call("c1", "slow"), audio_delta(), response_done()],
1604 ));
1605 *runner.session.write().await = Some(scripted as Arc<dyn RealtimeSession>);
1606
1607 tokio::time::timeout(std::time::Duration::from_secs(5), runner.run())
1608 .await
1609 .expect("a tool awaiting a later event deadlocks when dispatch blocks intake")
1610 .unwrap();
1611
1612 assert_eq!(audio_events.load(Ordering::SeqCst), 1, "the audio delta was handled");
1613 assert_eq!(counts.tool_output.load(Ordering::SeqCst), 1, "the tool still reported output");
1614 }
1615
1616 #[tokio::test]
1617 async fn the_follow_up_response_waits_for_tools_that_outlive_the_response() {
1618 let counts = Arc::new(Counts::default());
1619 let audio_seen = Arc::new(tokio::sync::Notify::new());
1620 let audio_events = Arc::new(AtomicUsize::new(0));
1621
1622 let runner = RealtimeRunner::builder()
1623 .model(Arc::new(MockModel) as BoxedModel)
1624 .tool(tool_def("slow"), WaitsForAudio { audio_seen: Arc::clone(&audio_seen) })
1625 .event_handler(AudioSignaller {
1626 audio_seen: Arc::clone(&audio_seen),
1627 audio_events: Arc::clone(&audio_events),
1628 })
1629 .build()
1630 .unwrap();
1631
1632 let scripted = Arc::new(ScriptedSession::new(
1636 Arc::clone(&counts),
1637 vec![function_call("c1", "slow"), response_done(), audio_delta()],
1638 ));
1639 *runner.session.write().await = Some(scripted as Arc<dyn RealtimeSession>);
1640
1641 tokio::time::timeout(std::time::Duration::from_secs(5), runner.run())
1642 .await
1643 .expect("run must finish")
1644 .unwrap();
1645
1646 assert_eq!(counts.tool_output.load(Ordering::SeqCst), 1, "the output was sent");
1647 assert_eq!(
1648 counts.create_response.load(Ordering::SeqCst),
1649 1,
1650 "exactly one follow-up response, issued after the last tool finished"
1651 );
1652 }
1653
1654 #[derive(Default)]
1656 struct DisconnectWatcher {
1657 disconnects: Arc<AtomicUsize>,
1658 }
1659
1660 #[async_trait]
1661 impl EventHandler for DisconnectWatcher {
1662 async fn on_disconnect(&self) -> Result<()> {
1663 self.disconnects.fetch_add(1, Ordering::SeqCst);
1664 Ok(())
1665 }
1666 }
1667
1668 #[tokio::test]
1669 async fn a_terminal_disconnect_is_surfaced_once() {
1670 let counts = Arc::new(Counts::default());
1671 let disconnects = Arc::new(AtomicUsize::new(0));
1672
1673 let runner = RealtimeRunner::builder()
1674 .model(Arc::new(MockModel) as BoxedModel)
1675 .event_handler(DisconnectWatcher { disconnects: Arc::clone(&disconnects) })
1676 .build()
1677 .unwrap();
1678
1679 let scripted = Arc::new(ScriptedSession::new(Arc::clone(&counts), vec![response_done()]));
1680 *runner.session.write().await = Some(scripted as Arc<dyn RealtimeSession>);
1681
1682 runner.run().await.unwrap();
1685
1686 assert_eq!(
1687 disconnects.load(Ordering::SeqCst),
1688 1,
1689 "transport loss must be reported exactly once"
1690 );
1691 }
1692}