1use std::collections::HashMap;
4use std::sync::Arc;
5
6use tokio_util::sync::CancellationToken;
7
8use gemini_genai_rs::prelude::{ConnectBuilder, SessionConfig, SessionEvent, SessionPhase};
9use gemini_genai_rs::session::{SessionError, SessionHandle, SessionWriter};
10
11use crate::error::AgentError;
12use crate::state::State;
13use crate::tool::ToolDispatcher;
14
15use super::background_tool::{BackgroundToolTracker, ToolExecutionMode};
16use super::callbacks::EventCallbacks;
17use super::computed::ComputedRegistry;
18use super::context_writer::{DeferredWriter, PendingContext};
19use super::extractor::TurnExtractor;
20use super::handle::LiveHandle;
21use super::needs::{NeedsFulfillment, RepairConfig};
22use super::persistence::SessionPersistence;
23use super::phase::PhaseMachine;
24use super::processor::{ControlPlaneConfig, spawn_event_processor, spawn_telemetry_lane};
25use super::session_signals::SessionSignals;
26use super::soft_turn::SoftTurnDetector;
27use super::steering::{ContextDelivery, SteeringMode};
28use super::telemetry::SessionTelemetry;
29use super::temporal::TemporalRegistry;
30use super::watcher::WatcherRegistry;
31
32pub struct LiveSessionBuilder {
42 config: SessionConfig,
43 callbacks: EventCallbacks,
44 dispatcher: Option<Arc<ToolDispatcher>>,
45 extractors: Vec<Arc<dyn TurnExtractor>>,
46 computed: Option<ComputedRegistry>,
47 phase_machine: Option<PhaseMachine>,
48 watchers: Option<WatcherRegistry>,
49 temporal: Option<TemporalRegistry>,
50 greeting: Option<String>,
51 state: Option<State>,
52 execution_modes: HashMap<String, ToolExecutionMode>,
53 soft_turn_timeout: Option<std::time::Duration>,
55 steering_mode: SteeringMode,
56 context_delivery: ContextDelivery,
57 delivery: super::processor::DeliveryConfig,
58 repair_config: Option<RepairConfig>,
59 persistence: Option<Arc<dyn SessionPersistence>>,
60 session_id: Option<String>,
61 tool_advisory: bool,
62 telemetry_interval: Option<std::time::Duration>,
63 middleware: Vec<Arc<dyn crate::middleware::Middleware>>,
64 flow: Option<crate::flow::FlowMonitor>,
65 redactor: Option<Arc<super::redaction::TranscriptRedactor>>,
66}
67
68impl LiveSessionBuilder {
69 pub fn new(config: SessionConfig) -> Self {
71 Self {
72 config,
73 callbacks: EventCallbacks::default(),
74 dispatcher: None,
75 extractors: Vec::new(),
76 computed: None,
77 phase_machine: None,
78 watchers: None,
79 temporal: None,
80 greeting: None,
81 state: None,
82 execution_modes: HashMap::new(),
83 soft_turn_timeout: None,
84 steering_mode: SteeringMode::default(),
85 context_delivery: ContextDelivery::default(),
86 delivery: super::processor::DeliveryConfig::default(),
87 repair_config: None,
88 persistence: None,
89 session_id: None,
90 tool_advisory: true,
91 telemetry_interval: None,
92 middleware: Vec::new(),
93 flow: None,
94 redactor: None,
95 }
96 }
97
98 pub fn redaction(mut self, redactor: super::redaction::TranscriptRedactor) -> Self {
105 self.redactor = redactor.is_active().then(|| Arc::new(redactor));
106 self
107 }
108
109 pub fn middleware(mut self, layer: Arc<dyn crate::middleware::Middleware>) -> Self {
115 self.middleware.push(layer);
116 self
117 }
118
119 pub fn flow_monitor(mut self, monitor: crate::flow::FlowMonitor) -> Self {
121 self.flow = Some(monitor);
122 self
123 }
124
125 pub fn state(mut self, state: State) -> Self {
131 self.state = Some(state);
132 self
133 }
134
135 pub fn greeting(mut self, prompt: impl Into<String>) -> Self {
137 self.greeting = Some(prompt.into());
138 self
139 }
140
141 pub fn dispatcher(mut self, dispatcher: ToolDispatcher) -> Self {
143 for tool in dispatcher.to_tool_declarations() {
145 self.config = self.config.add_tool(tool);
146 }
147 self.dispatcher = Some(Arc::new(dispatcher));
148 self
149 }
150
151 pub fn callbacks(mut self, callbacks: EventCallbacks) -> Self {
153 self.callbacks = callbacks;
154 self
155 }
156
157 pub fn extractor(mut self, extractor: Arc<dyn TurnExtractor>) -> Self {
159 self.extractors.push(extractor);
160 self
161 }
162
163 pub fn computed(mut self, registry: ComputedRegistry) -> Self {
165 self.computed = Some(registry);
166 self
167 }
168
169 pub fn phase_machine(mut self, machine: PhaseMachine) -> Self {
171 self.phase_machine = Some(machine);
172 self
173 }
174
175 pub fn watchers(mut self, registry: WatcherRegistry) -> Self {
177 self.watchers = Some(registry);
178 self
179 }
180
181 pub fn temporal(mut self, registry: TemporalRegistry) -> Self {
183 self.temporal = Some(registry);
184 self
185 }
186
187 pub fn tool_execution_mode(
192 mut self,
193 tool_name: impl Into<String>,
194 mode: ToolExecutionMode,
195 ) -> Self {
196 self.execution_modes.insert(tool_name.into(), mode);
197 self
198 }
199
200 pub fn soft_turn_timeout(mut self, timeout: std::time::Duration) -> Self {
207 self.soft_turn_timeout = Some(timeout);
208 self
209 }
210
211 pub fn steering_mode(mut self, mode: SteeringMode) -> Self {
213 self.steering_mode = mode;
214 self
215 }
216
217 pub fn context_delivery(mut self, mode: ContextDelivery) -> Self {
222 self.context_delivery = mode;
223 self
224 }
225
226 pub fn delivery(mut self, delivery: super::processor::DeliveryConfig) -> Self {
233 self.delivery = delivery;
234 self
235 }
236
237 pub fn repair(mut self, config: RepairConfig) -> Self {
242 self.repair_config = Some(config);
243 self
244 }
245
246 pub fn persistence(mut self, backend: Arc<dyn SessionPersistence>) -> Self {
248 self.persistence = Some(backend);
249 self
250 }
251
252 pub fn session_id(mut self, id: impl Into<String>) -> Self {
254 self.session_id = Some(id.into());
255 self
256 }
257
258 pub fn tool_advisory(mut self, enabled: bool) -> Self {
260 self.tool_advisory = enabled;
261 self
262 }
263
264 pub fn telemetry_interval(mut self, interval: std::time::Duration) -> Self {
269 self.telemetry_interval = Some(interval);
270 self
271 }
272
273 pub async fn connect(self) -> Result<LiveHandle, AgentError> {
286 let mut plan = self.into_plan()?;
287
288 let config = plan.config.take().expect("plan always carries a config");
291 let session = ConnectBuilder::new(config)
292 .connect()
293 .await
294 .map_err(AgentError::Session)?;
295
296 let mut events = session.subscribe();
306 tokio::select! {
307 () = session.wait_for_phase(SessionPhase::Active) => {}
308 failure = wait_for_connect_failure(&mut events) => {
309 return Err(AgentError::Session(SessionError::SetupFailed(
310 gemini_genai_rs::session::SetupError::ServerRejected {
311 code: None,
312 message: failure,
313 },
314 )));
315 }
316 }
317
318 let runtime = build_runtime(plan, session);
319 spawn_lanes(runtime).await
320 }
321
322 pub(crate) fn into_plan(self) -> Result<SessionPlan, AgentError> {
329 if let Some(ref pm) = self.phase_machine {
331 pm.validate()?;
332 }
333 if let Some(ref computed) = self.computed {
334 computed.validate()?;
335 }
336
337 let mut config = self.config;
339 for (tool_name, mode) in &self.execution_modes {
340 if matches!(
341 mode,
342 super::background_tool::ToolExecutionMode::Background { .. }
343 ) {
344 for tool in &mut config.tools {
345 if let Some(ref mut decls) = tool.function_declarations {
346 for decl in decls {
347 if decl.name == *tool_name {
348 decl.behavior = Some(
349 gemini_genai_rs::prelude::FunctionCallingBehavior::NonBlocking,
350 );
351 }
352 }
353 }
354 }
355 }
356 }
357
358 let base_instruction = config.system_instruction.as_ref().map(|content| {
363 content
364 .parts
365 .iter()
366 .filter_map(|part| match part {
367 gemini_genai_rs::prelude::Part::Text { text } => Some(text.as_str()),
368 _ => None,
369 })
370 .collect::<Vec<_>>()
371 .join("\n")
372 });
373
374 Ok(SessionPlan {
375 config: Some(config),
376 base_instruction,
377 callbacks: self.callbacks,
378 dispatcher: self.dispatcher,
379 extractors: self.extractors,
380 computed: self.computed,
381 phase_machine: self.phase_machine,
382 watchers: self.watchers,
383 temporal: self.temporal,
384 greeting: self.greeting,
385 state: self.state,
386 execution_modes: self.execution_modes,
387 soft_turn_timeout: self.soft_turn_timeout,
388 steering_mode: self.steering_mode,
389 context_delivery: self.context_delivery,
390 delivery: self.delivery,
391 repair_config: self.repair_config,
392 persistence: self.persistence,
393 session_id: self.session_id,
394 tool_advisory: self.tool_advisory,
395 telemetry_interval: self.telemetry_interval,
396 middleware: self.middleware,
397 flow: self.flow,
398 redactor: self.redactor,
399 })
400 }
401}
402
403pub(crate) struct SessionPlan {
411 config: Option<SessionConfig>,
414 base_instruction: Option<String>,
417 callbacks: EventCallbacks,
418 dispatcher: Option<Arc<ToolDispatcher>>,
419 extractors: Vec<Arc<dyn TurnExtractor>>,
420 computed: Option<ComputedRegistry>,
421 phase_machine: Option<PhaseMachine>,
422 watchers: Option<WatcherRegistry>,
423 temporal: Option<TemporalRegistry>,
424 greeting: Option<String>,
425 state: Option<State>,
426 execution_modes: HashMap<String, ToolExecutionMode>,
427 soft_turn_timeout: Option<std::time::Duration>,
428 steering_mode: SteeringMode,
429 context_delivery: ContextDelivery,
430 delivery: super::processor::DeliveryConfig,
431 repair_config: Option<RepairConfig>,
432 persistence: Option<Arc<dyn SessionPersistence>>,
433 session_id: Option<String>,
434 tool_advisory: bool,
435 telemetry_interval: Option<std::time::Duration>,
436 middleware: Vec<Arc<dyn crate::middleware::Middleware>>,
437 flow: Option<crate::flow::FlowMonitor>,
438 redactor: Option<Arc<super::redaction::TranscriptRedactor>>,
439}
440
441pub(crate) struct SessionRuntime {
449 session: SessionHandle,
450 callbacks: Arc<EventCallbacks>,
451 dispatcher: Option<Arc<ToolDispatcher>>,
452 extractors: Vec<Arc<dyn TurnExtractor>>,
453 computed: Option<ComputedRegistry>,
454 phase_machine: Option<tokio::sync::Mutex<PhaseMachine>>,
455 watchers: Option<WatcherRegistry>,
456 temporal: Option<Arc<TemporalRegistry>>,
457 greeting: Option<String>,
458 state: State,
459 execution_modes: HashMap<String, ToolExecutionMode>,
460 background_tracker: Arc<BackgroundToolTracker>,
461 telemetry: Arc<SessionTelemetry>,
462 telemetry_interval: Option<std::time::Duration>,
463 control_plane: ControlPlaneConfig,
464 pending_context: Option<Arc<PendingContext>>,
465 writer: Arc<dyn SessionWriter>,
467 user_writer: Arc<dyn SessionWriter>,
469 event_rx: tokio::sync::broadcast::Receiver<gemini_genai_rs::prelude::SessionEvent>,
470 telem_rx: tokio::sync::broadcast::Receiver<gemini_genai_rs::prelude::SessionEvent>,
471 on_usage_cb: Option<super::callbacks::UsageCallback>,
472 live_event_tx: tokio::sync::broadcast::Sender<super::events::LiveEvent>,
473 telem_cancel: CancellationToken,
474 flow_monitor: Option<crate::flow::SharedFlowMonitor>,
475}
476
477pub(crate) fn build_runtime(plan: SessionPlan, session: SessionHandle) -> SessionRuntime {
482 let flow_monitor = plan.flow.map(crate::flow::FlowMonitor::into_shared);
485 let mut callbacks = plan.callbacks;
486 let on_usage_cb = callbacks.on_usage.take();
487 let callbacks = Arc::new(callbacks);
488 let raw_writer: Arc<dyn SessionWriter> = Arc::new(session.clone());
489 let state = plan.state.unwrap_or_default();
490
491 let event_rx = session.subscribe();
493 let telem_rx = session.subscribe();
494
495 if let Some(ref pm) = plan.phase_machine {
497 let _ = state.session().set("phase", pm.current());
498 if let Some(phase) = pm.current_phase()
499 && !phase.needs.is_empty()
500 {
501 let _ = state.set("session:phase_needs", phase.needs.clone());
502 }
503 }
504
505 let phase_machine_mutex = plan.phase_machine.map(tokio::sync::Mutex::new);
506 let temporal_arc = plan.temporal.map(Arc::new);
507 let background_tracker = Arc::new(BackgroundToolTracker::new());
508
509 let telemetry = Arc::new(SessionTelemetry::new());
511 let telem_cancel = CancellationToken::new();
512
513 let mut control_plane = ControlPlaneConfig {
515 soft_turn: plan.soft_turn_timeout.map(SoftTurnDetector::new),
516 steering_mode: plan.steering_mode,
517 context_delivery: plan.context_delivery,
518 delivery: plan.delivery,
519 needs_fulfillment: plan.repair_config.map(NeedsFulfillment::new),
520 persistence: plan.persistence,
521 session_id: plan.session_id,
522 tool_advisory: plan.tool_advisory,
523 base_instruction: plan.base_instruction,
524 pending_context: None, middleware: {
526 let mut chain = crate::middleware::MiddlewareChain::new();
527 for layer in plan.middleware {
528 chain.add(layer);
529 }
530 Arc::new(chain)
531 },
532 flow: flow_monitor.clone(),
533 redactor: plan.redactor,
534 };
535
536 let pending_context = if plan.context_delivery == ContextDelivery::Deferred {
541 Some(Arc::new(PendingContext::new()))
542 } else {
543 None
544 };
545
546 let (writer, user_writer) = if let Some(ref pending) = pending_context {
548 let deferred: Arc<dyn SessionWriter> =
549 Arc::new(DeferredWriter::new(raw_writer.clone(), pending.clone()));
550 (raw_writer, deferred)
554 } else {
555 (raw_writer.clone(), raw_writer)
556 };
557
558 control_plane.pending_context = pending_context.clone();
560
561 use super::events::LiveEvent;
563 use tokio::sync::broadcast;
564 let (live_event_tx, _) = broadcast::channel::<LiveEvent>(4096);
565
566 SessionRuntime {
567 session,
568 callbacks,
569 dispatcher: plan.dispatcher,
570 extractors: plan.extractors,
571 computed: plan.computed,
572 phase_machine: phase_machine_mutex,
573 watchers: plan.watchers,
574 temporal: temporal_arc,
575 greeting: plan.greeting,
576 state,
577 execution_modes: plan.execution_modes,
578 background_tracker,
579 telemetry,
580 telemetry_interval: plan.telemetry_interval,
581 control_plane,
582 pending_context,
583 writer,
584 user_writer,
585 event_rx,
586 telem_rx,
587 on_usage_cb,
588 live_event_tx,
589 telem_cancel,
590 flow_monitor,
591 }
592}
593
594async fn wait_for_connect_failure(
608 events: &mut tokio::sync::broadcast::Receiver<SessionEvent>,
609) -> String {
610 let mut last_error: Option<String> = None;
611 loop {
612 match events.recv().await {
613 Ok(SessionEvent::Error(err)) => last_error = Some(err.to_string()),
614 Ok(SessionEvent::Disconnected(reason)) => {
615 let reason = reason.unwrap_or_else(|| "connection closed".to_string());
616 return match last_error {
617 Some(err) => format!("{reason} ({err})"),
618 None => reason,
619 };
620 }
621 Ok(_) => {}
622 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
624 return last_error.unwrap_or_else(|| "session ended during setup".to_string());
625 }
626 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
627 }
628 }
629}
630
631pub(crate) async fn spawn_lanes(rt: SessionRuntime) -> Result<LiveHandle, AgentError> {
632 use super::events::LiveEvent;
633
634 let session_signals = SessionSignals::new(rt.state.clone());
636 let _telem_handle = spawn_telemetry_lane(
637 rt.telem_rx,
638 session_signals,
639 rt.telemetry.clone(),
640 rt.telem_cancel.clone(),
641 rt.on_usage_cb,
642 );
643
644 let greeting_writer = rt.user_writer.clone();
646 let (fast_handle, ctrl_handle, ctrl_tx) = spawn_event_processor(
647 rt.event_rx,
648 rt.callbacks,
649 rt.dispatcher,
650 rt.writer,
651 rt.extractors,
652 rt.state.clone(),
653 rt.computed,
654 rt.phase_machine,
655 rt.watchers,
656 rt.temporal,
657 Some(rt.background_tracker.clone()),
658 rt.execution_modes,
659 rt.control_plane,
660 rt.live_event_tx.clone(),
661 );
662
663 if let Some(interval) = rt.telemetry_interval {
665 let telem_tx = rt.live_event_tx.clone();
666 let telem_ref = rt.telemetry.clone();
667 tokio::spawn(async move {
668 let mut tick = tokio::time::interval(interval);
669 let mut prev_turns = 0u64;
670 loop {
671 tick.tick().await;
672 let snap = telem_ref.snapshot();
673 if let Some(obj) = snap.as_object() {
674 let tc = obj
675 .get("turn_count")
676 .or_else(|| obj.get("response_count"))
677 .and_then(serde_json::Value::as_u64)
678 .unwrap_or(0);
679 if tc > prev_turns {
680 let latency = obj
681 .get("last_response_latency_ms")
682 .and_then(serde_json::Value::as_u64)
683 .unwrap_or(0) as u32;
684 let prompt = obj
685 .get("prompt_token_count")
686 .and_then(serde_json::Value::as_u64)
687 .unwrap_or(0) as u32;
688 let response = obj
689 .get("response_token_count")
690 .and_then(serde_json::Value::as_u64)
691 .unwrap_or(0) as u32;
692 let _ = telem_tx.send(LiveEvent::TurnMetrics {
693 turn: tc as u32,
694 latency_ms: latency,
695 prompt_tokens: prompt,
696 response_tokens: response,
697 });
698 prev_turns = tc;
699 }
700 }
701 if telem_tx.send(LiveEvent::Telemetry(snap)).is_err() {
702 break;
703 }
704 }
705 });
706 }
707
708 if let Some(greeting) = rt.greeting {
710 greeting_writer
711 .send_text(greeting)
712 .await
713 .map_err(AgentError::Session)?;
714 }
715
716 Ok(LiveHandle::new(
717 rt.session,
718 rt.user_writer,
719 fast_handle,
720 ctrl_handle,
721 rt.state,
722 rt.telemetry,
723 rt.live_event_tx,
724 rt.pending_context,
725 rt.flow_monitor,
726 rt.background_tracker,
727 rt.telem_cancel,
728 )
729 .with_control_sender(ctrl_tx))
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735
736 #[test]
737 fn builder_creates_with_defaults() {
738 let config = SessionConfig::new("test-key");
739 let builder = LiveSessionBuilder::new(config);
740 assert!(builder.dispatcher.is_none());
741 assert!(builder.computed.is_none());
742 assert!(builder.phase_machine.is_none());
743 assert!(builder.watchers.is_none());
744 assert!(builder.temporal.is_none());
745 }
746
747 #[test]
748 fn into_plan_derives_defaults() {
749 let config = SessionConfig::new("test-key");
750 let plan = LiveSessionBuilder::new(config)
751 .into_plan()
752 .expect("default builder should produce a plan");
753
754 assert!(plan.config.is_some());
756 assert!(plan.dispatcher.is_none());
758 assert!(plan.phase_machine.is_none());
759 assert!(plan.persistence.is_none());
760 assert!(plan.session_id.is_none());
761 assert!(plan.greeting.is_none());
762 assert!(plan.soft_turn_timeout.is_none());
763 assert!(plan.telemetry_interval.is_none());
764 assert!(plan.repair_config.is_none());
765 assert!(plan.flow.is_none());
766 assert!(plan.execution_modes.is_empty());
767 assert!(plan.middleware.is_empty());
768 assert_eq!(plan.steering_mode, SteeringMode::default());
769 assert_eq!(plan.context_delivery, ContextDelivery::default());
770 assert!(plan.tool_advisory);
772 }
773
774 #[test]
775 fn into_plan_carries_persistence_and_session_id() {
776 let config = SessionConfig::new("test-key");
777 let plan = LiveSessionBuilder::new(config)
778 .session_id("user-123-session-456")
779 .into_plan()
780 .expect("plan derivation should succeed");
781
782 assert_eq!(plan.session_id.as_deref(), Some("user-123-session-456"));
783 }
784
785 #[test]
786 fn into_plan_carries_steering_and_context_delivery() {
787 let config = SessionConfig::new("test-key");
788 let plan = LiveSessionBuilder::new(config)
789 .steering_mode(SteeringMode::ContextInjection)
790 .context_delivery(ContextDelivery::Deferred)
791 .tool_advisory(false)
792 .into_plan()
793 .expect("plan derivation should succeed");
794
795 assert_eq!(plan.steering_mode, SteeringMode::ContextInjection);
796 assert_eq!(plan.context_delivery, ContextDelivery::Deferred);
797 assert!(!plan.tool_advisory);
798 }
799
800 #[test]
801 fn into_plan_carries_greeting_and_telemetry_interval() {
802 let config = SessionConfig::new("test-key");
803 let plan = LiveSessionBuilder::new(config)
804 .greeting("Hello there")
805 .telemetry_interval(std::time::Duration::from_secs(5))
806 .soft_turn_timeout(std::time::Duration::from_secs(2))
807 .into_plan()
808 .expect("plan derivation should succeed");
809
810 assert_eq!(plan.greeting.as_deref(), Some("Hello there"));
811 assert_eq!(
812 plan.telemetry_interval,
813 Some(std::time::Duration::from_secs(5))
814 );
815 assert_eq!(
816 plan.soft_turn_timeout,
817 Some(std::time::Duration::from_secs(2))
818 );
819 }
820
821 #[test]
822 fn into_plan_validates_phase_machine() {
823 let config = SessionConfig::new("test-key");
826 let pm = PhaseMachine::new("nonexistent");
827 let result = LiveSessionBuilder::new(config)
828 .phase_machine(pm)
829 .into_plan();
830 assert!(result.is_err(), "invalid phase machine should fail to plan");
831 }
832
833 #[test]
834 fn into_plan_carries_valid_phase_machine_and_seeds_nothing() {
835 let config = SessionConfig::new("test-key");
838 let mut pm = PhaseMachine::new("start");
839 pm.add_phase(crate::live::phase::Phase::new("start", "Start phase"));
840 let plan = LiveSessionBuilder::new(config)
841 .phase_machine(pm)
842 .into_plan()
843 .expect("valid phase machine should plan");
844
845 assert!(plan.phase_machine.is_some());
846 }
847
848 #[test]
849 fn into_plan_applies_non_blocking_to_background_tools() {
850 use gemini_genai_rs::prelude::{FunctionCallingBehavior, FunctionDeclaration, Tool};
851
852 let decl = FunctionDeclaration {
853 name: "search_kb".into(),
854 description: "Search".into(),
855 parameters: None,
856 behavior: None,
857 };
858 let config = SessionConfig::new("test-key").add_tool(Tool::functions(vec![decl]));
859
860 let plan = LiveSessionBuilder::new(config)
861 .tool_execution_mode(
862 "search_kb",
863 ToolExecutionMode::Background {
864 formatter: None,
865 scheduling: None,
866 },
867 )
868 .into_plan()
869 .expect("plan derivation should succeed");
870
871 let cfg = plan.config.expect("config carried");
872 let decl = cfg.tools[0]
873 .function_declarations
874 .as_ref()
875 .unwrap()
876 .iter()
877 .find(|d| d.name == "search_kb")
878 .unwrap();
879 assert_eq!(decl.behavior, Some(FunctionCallingBehavior::NonBlocking));
880 }
881}