1use std::sync::Arc;
35
36use futures::StreamExt;
37use tokio::sync::{Mutex, Notify, RwLock, broadcast, mpsc};
38use tokio_util::sync::CancellationToken;
39use tracing::{debug, info, warn};
40
41#[cfg(feature = "memory")]
42use adk_core::Memory;
43use adk_core::{Agent, Content, Event, Part};
44use adk_runner::Runner;
45use adk_session::service::SessionService;
46
47use crate::checkpoint::{CheckpointManager, RunState};
48use crate::event_mapping::{RunnerOutput, custom_tool_use_id, map_runner_output, requires_parking};
49use crate::parking::ToolParkingLot;
50use crate::sequence::SequenceCounter;
51use crate::types::{
52 ContentBlock, RuntimeError, SessionEvent, SessionStatus, StopReason, UserEvent,
53};
54use crate::usage::{SessionUsageTracker, UsageReport};
55
56pub struct SessionLoop {
91 session_id: String,
93 event_rx: mpsc::Receiver<UserEvent>,
95 event_tx: broadcast::Sender<SessionEvent>,
97 seq: SequenceCounter,
99 parking: Arc<ToolParkingLot>,
101 checkpoint: Arc<RwLock<CheckpointManager>>,
103 cancel_token: CancellationToken,
105 pause_flag: Arc<Mutex<bool>>,
107 pause_notify: Arc<Notify>,
109 owner: (String, String),
116 status: Arc<RwLock<SessionStatus>>,
122 agent: Arc<dyn Agent>,
124 session_service: Arc<dyn SessionService>,
126 #[cfg(feature = "memory")]
128 memory: Option<Arc<dyn Memory>>,
129 usage_tracker: SessionUsageTracker,
131}
132
133impl SessionLoop {
134 pub fn new(
146 session_id: String,
147 event_rx: mpsc::Receiver<UserEvent>,
148 event_tx: broadcast::Sender<SessionEvent>,
149 parking: Arc<ToolParkingLot>,
150 cancel_token: CancellationToken,
151 agent: Arc<dyn Agent>,
152 session_service: Arc<dyn SessionService>,
153 ) -> Self {
154 let checkpoint = Arc::new(RwLock::new(CheckpointManager::new(session_id.clone())));
155 Self {
156 session_id,
157 event_rx,
158 event_tx,
159 seq: SequenceCounter::default(),
160 parking,
161 checkpoint,
162 cancel_token,
163 pause_flag: Arc::new(Mutex::new(false)),
164 pause_notify: Arc::new(Notify::new()),
165 owner: ("managed".to_string(), "managed_user".to_string()),
166 status: Arc::new(RwLock::new(SessionStatus::Queued)),
167 agent,
168 session_service,
169 #[cfg(feature = "memory")]
170 memory: None,
171 usage_tracker: SessionUsageTracker::new(),
172 }
173 }
174
175 #[cfg(feature = "memory")]
181 #[allow(clippy::too_many_arguments)]
182 pub fn with_pause_controls(
183 session_id: String,
184 event_rx: mpsc::Receiver<UserEvent>,
185 event_tx: broadcast::Sender<SessionEvent>,
186 parking: Arc<ToolParkingLot>,
187 cancel_token: CancellationToken,
188 pause_flag: Arc<Mutex<bool>>,
189 pause_notify: Arc<Notify>,
190 checkpoint: Arc<RwLock<CheckpointManager>>,
191 agent: Arc<dyn Agent>,
192 session_service: Arc<dyn SessionService>,
193 memory: Option<Arc<dyn Memory>>,
194 ) -> Self {
195 Self {
196 session_id,
197 event_rx,
198 event_tx,
199 seq: SequenceCounter::default(),
200 parking,
201 checkpoint,
202 cancel_token,
203 pause_flag,
204 pause_notify,
205 owner: ("managed".to_string(), "managed_user".to_string()),
206 status: Arc::new(RwLock::new(SessionStatus::Queued)),
207 agent,
208 session_service,
209 memory,
210 usage_tracker: SessionUsageTracker::new(),
211 }
212 }
213
214 #[cfg(not(feature = "memory"))]
218 #[allow(clippy::too_many_arguments)]
219 pub fn with_pause_controls(
220 session_id: String,
221 event_rx: mpsc::Receiver<UserEvent>,
222 event_tx: broadcast::Sender<SessionEvent>,
223 parking: Arc<ToolParkingLot>,
224 cancel_token: CancellationToken,
225 pause_flag: Arc<Mutex<bool>>,
226 pause_notify: Arc<Notify>,
227 checkpoint: Arc<RwLock<CheckpointManager>>,
228 agent: Arc<dyn Agent>,
229 session_service: Arc<dyn SessionService>,
230 ) -> Self {
231 Self {
232 session_id,
233 event_rx,
234 event_tx,
235 seq: SequenceCounter::default(),
236 parking,
237 checkpoint,
238 cancel_token,
239 pause_flag,
240 pause_notify,
241 owner: ("managed".to_string(), "managed_user".to_string()),
242 status: Arc::new(RwLock::new(SessionStatus::Queued)),
243 agent,
244 session_service,
245 usage_tracker: SessionUsageTracker::new(),
246 }
247 }
248
249 pub fn with_owner(mut self, app_name: impl Into<String>, user_id: impl Into<String>) -> Self {
261 self.owner = (app_name.into(), user_id.into());
262 self
263 }
264
265 pub fn with_shared_status(mut self, status: Arc<RwLock<SessionStatus>>) -> Self {
279 self.status = status;
280 self
281 }
282
283 pub fn pause_flag(&self) -> Arc<Mutex<bool>> {
285 Arc::clone(&self.pause_flag)
286 }
287
288 pub fn pause_notify(&self) -> Arc<Notify> {
290 Arc::clone(&self.pause_notify)
291 }
292
293 pub async fn run(mut self) -> Result<(), RuntimeError> {
303 info!(session_id = %self.session_id, "session loop started");
304
305 loop {
306 if self.cancel_token.is_cancelled() {
308 debug!(session_id = %self.session_id, "interrupt detected, shutting down");
309 self.emit_idle(Some(StopReason::EndTurn), None).await;
310 break;
311 }
312
313 self.check_pause().await;
315
316 let event = tokio::select! {
318 biased;
319 _ = self.cancel_token.cancelled() => {
320 debug!(session_id = %self.session_id, "interrupted while waiting for event");
321 self.emit_idle(Some(StopReason::EndTurn), None).await;
322 break;
323 }
324 ev = self.event_rx.recv() => {
325 match ev {
326 Some(event) => event,
327 None => {
328 debug!(session_id = %self.session_id, "event channel closed, shutting down");
329 break;
330 }
331 }
332 }
333 };
334
335 match event {
337 UserEvent::Message { content } => {
338 self.process_turn(content).await?;
339 }
340 UserEvent::Interrupt {} => {
341 debug!(session_id = %self.session_id, "user.interrupt received");
342 self.emit_idle(Some(StopReason::EndTurn), None).await;
343 break;
344 }
345 UserEvent::CustomToolResult { custom_tool_use_id, content } => {
346 debug!(
347 session_id = %self.session_id,
348 tool_use_id = %custom_tool_use_id,
349 "delivering custom tool result"
350 );
351 if let Err(e) = self.parking.deliver(&custom_tool_use_id, content).await {
352 warn!(
353 session_id = %self.session_id,
354 error = %e,
355 "failed to deliver custom tool result"
356 );
357 }
358 }
359 UserEvent::ToolConfirmation { tool_use_id, result, deny_message } => {
360 debug!(
361 session_id = %self.session_id,
362 tool_use_id = %tool_use_id,
363 result = ?result,
364 "tool confirmation received, delivering to parking lot"
365 );
366 let content = match result {
371 crate::types::ConfirmationResult::Allow => {
372 vec![ContentBlock::Text {
373 text: serde_json::json!({
374 "confirmation": "approved",
375 "tool_use_id": tool_use_id
376 })
377 .to_string(),
378 }]
379 }
380 crate::types::ConfirmationResult::Deny => {
381 let message = deny_message
382 .unwrap_or_else(|| "Tool execution denied by user".to_string());
383 vec![ContentBlock::Text {
384 text: serde_json::json!({
385 "confirmation": "denied",
386 "tool_use_id": tool_use_id,
387 "reason": message
388 })
389 .to_string(),
390 }]
391 }
392 };
393 if let Err(e) = self.parking.deliver(&tool_use_id, content).await {
394 warn!(
395 session_id = %self.session_id,
396 error = %e,
397 "failed to deliver tool confirmation"
398 );
399 }
400 }
401 UserEvent::ToolResult { tool_use_id, .. } => {
402 debug!(
403 session_id = %self.session_id,
404 tool_use_id = %tool_use_id,
405 "tool result received (self-hosted only, not yet wired)"
406 );
407 }
408 UserEvent::DefineOutcome { criteria } => {
409 debug!(
410 session_id = %self.session_id,
411 criteria = %criteria,
412 "outcome criteria defined"
413 );
414 }
416 }
417 }
418
419 info!(session_id = %self.session_id, "session loop exited");
420 Ok(())
421 }
422
423 async fn process_turn(&mut self, content: Vec<ContentBlock>) -> Result<(), RuntimeError> {
425 *self.status.write().await = SessionStatus::Running;
427 let running_event = SessionEvent::StatusRunning { seq: self.seq.next() };
428 self.emit_event(running_event).await;
429
430 if self.check_interrupt() {
432 self.emit_idle(Some(StopReason::EndTurn), None).await;
433 return Ok(());
434 }
435
436 let user_content = self.build_user_content(&content);
438
439 let runner = self.build_runner()?;
441
442 let event_stream = runner
443 .run_str(&self.owner.1, &self.session_id, user_content)
444 .await
445 .map_err(|e| RuntimeError::internal(format!("runner invocation failed: {e}")))?;
446
447 let mut turn_usage = UsageReport::default();
449 let mut custom_tool_ids = Vec::new();
450
451 futures::pin_mut!(event_stream);
452
453 while let Some(event_result) = event_stream.next().await {
454 if self.check_interrupt() {
456 self.emit_idle(Some(StopReason::EndTurn), None).await;
457 return Ok(());
458 }
459
460 match event_result {
461 Ok(event) => {
462 self.process_runner_event(&event, &mut turn_usage, &mut custom_tool_ids).await;
463 }
464 Err(e) => {
465 warn!(
466 session_id = %self.session_id,
467 error = %e,
468 "runner event stream error"
469 );
470 let error_event = SessionEvent::Error {
471 code: "runner_error".to_string(),
472 message: e.to_string(),
473 seq: self.seq.next(),
474 };
475 self.emit_event(error_event).await;
476 }
477 }
478 }
479
480 let turn_usage_report = if !turn_usage.is_empty() {
483 self.usage_tracker.record_turn(turn_usage.clone());
484 Some(turn_usage)
485 } else {
486 None
487 };
488
489 let stop_reason = if custom_tool_ids.is_empty() {
491 Some(StopReason::EndTurn)
492 } else {
493 Some(StopReason::RequiresAction { event_ids: custom_tool_ids })
494 };
495
496 self.emit_idle(stop_reason, turn_usage_report).await;
498
499 Ok(())
500 }
501
502 fn build_runner(&self) -> Result<Runner, RuntimeError> {
504 #[allow(unused_mut)]
505 let mut builder = Runner::builder()
506 .app_name(self.owner.0.as_str())
507 .agent(Arc::clone(&self.agent))
508 .session_service(Arc::clone(&self.session_service))
509 .cancellation_token(self.cancel_token.clone());
510
511 #[cfg(feature = "memory")]
512 if let Some(ref memory) = self.memory {
513 builder = builder.memory_service(Arc::clone(memory));
514 }
515
516 builder.build().map_err(|e| RuntimeError::internal(format!("failed to build runner: {e}")))
517 }
518
519 fn build_user_content(&self, blocks: &[ContentBlock]) -> Content {
521 let mut parts = Vec::new();
522 for block in blocks {
523 match block {
524 ContentBlock::Text { text } => {
525 parts.push(Part::Text { text: text.clone() });
526 }
527 ContentBlock::Image { source } => {
528 if let Some(url) = source.get("url").and_then(|v| v.as_str()) {
530 parts.push(Part::FileData {
531 mime_type: source
532 .get("media_type")
533 .and_then(|v| v.as_str())
534 .unwrap_or("image/png")
535 .to_string(),
536 file_uri: url.to_string(),
537 annotations: None,
538 });
539 }
540 }
541 ContentBlock::File { file_id } => {
542 parts.push(Part::FileData {
543 mime_type: "application/octet-stream".to_string(),
544 file_uri: file_id.clone(),
545 annotations: None,
546 });
547 }
548 }
549 }
550
551 Content { role: "user".to_string(), parts }
552 }
553
554 async fn process_runner_event(
556 &mut self,
557 event: &Event,
558 turn_usage: &mut UsageReport,
559 custom_tool_ids: &mut Vec<String>,
560 ) {
561 if let Some(ref usage_meta) = event.llm_response.usage_metadata {
563 let report = UsageReport::from_usage_metadata(usage_meta);
564 turn_usage.accumulate(&report);
565 }
566
567 if event.llm_response.partial {
569 return;
570 }
571
572 if let Some(ref content) = event.llm_response.content {
574 for part in &content.parts {
575 match part {
576 Part::Text { text } => {
577 if text.is_empty() {
578 continue;
579 }
580 let output = RunnerOutput::TextContent { text: text.clone() };
581 let session_event = map_runner_output(output, self.seq.next());
582 self.emit_event(session_event).await;
583 }
584 Part::FunctionCall { name, args, id, .. } => {
585 let tool_use_id =
586 id.clone().unwrap_or_else(|| format!("tu_{}", uuid::Uuid::new_v4()));
587
588 let tool_kind = self.classify_tool(name);
590
591 let output = match tool_kind {
592 ToolKind::Custom => {
593 let ctu_id = format!("ctu_{}", uuid::Uuid::new_v4());
594 custom_tool_ids.push(ctu_id.clone());
595 RunnerOutput::CustomToolCall {
596 custom_tool_use_id: ctu_id,
597 name: name.clone(),
598 input: args.clone(),
599 }
600 }
601 ToolKind::Builtin => RunnerOutput::BuiltinToolCall {
602 tool_use_id,
603 name: name.clone(),
604 input: args.clone(),
605 },
606 ToolKind::Mcp => RunnerOutput::McpToolCall {
607 tool_use_id,
608 name: name.clone(),
609 input: args.clone(),
610 },
611 };
612
613 let session_event = map_runner_output(output.clone(), self.seq.next());
614 self.emit_event(session_event).await;
615
616 if requires_parking(&output)
618 && let Some(ctu_id) = custom_tool_use_id(&output)
619 {
620 let ctu_id_owned = ctu_id.to_string();
621 debug!(
622 session_id = %self.session_id,
623 custom_tool_use_id = %ctu_id_owned,
624 "parking for custom tool result"
625 );
626 match self.parking.park(&ctu_id_owned).await {
627 Ok(_result_blocks) => {
628 debug!(
629 session_id = %self.session_id,
630 custom_tool_use_id = %ctu_id_owned,
631 "custom tool result delivered"
632 );
633 }
634 Err(e) => {
635 warn!(
636 session_id = %self.session_id,
637 error = %e,
638 "custom tool park failed or timed out"
639 );
640 }
641 }
642 }
643 }
644 _ => {}
646 }
647 }
648 }
649 }
650
651 fn classify_tool(&self, name: &str) -> ToolKind {
653 const BUILTIN_TOOLS: &[&str] =
655 &["bash", "filesystem", "web_search", "web_fetch", "code_execution"];
656
657 if BUILTIN_TOOLS.contains(&name) {
658 ToolKind::Builtin
659 } else if name.starts_with("mcp_") || name.contains("::") {
660 ToolKind::Mcp
661 } else {
662 ToolKind::Custom
664 }
665 }
666
667 async fn emit_event(&mut self, event: SessionEvent) {
669 let run_state = RunState {
671 seq: self.seq.current(),
672 pending_tool_ids: Vec::new(),
673 status: *self.status.read().await,
674 };
675 self.checkpoint.write().await.checkpoint(event.clone(), run_state);
676
677 let _ = self.event_tx.send(event);
679 }
680
681 async fn emit_idle(&mut self, stop_reason: Option<StopReason>, usage: Option<UsageReport>) {
683 *self.status.write().await = SessionStatus::Idle;
684 let idle_event = SessionEvent::StatusIdle { seq: self.seq.next(), stop_reason, usage };
685 self.emit_event(idle_event).await;
686 }
687
688 fn check_interrupt(&self) -> bool {
692 self.cancel_token.is_cancelled()
693 }
694
695 async fn check_pause(&self) {
697 loop {
698 let is_paused = *self.pause_flag.lock().await;
699 if !is_paused {
700 break;
701 }
702 debug!(session_id = %self.session_id, "session loop paused, waiting for resume");
703 self.pause_notify.notified().await;
704 }
705 }
706}
707
708use crate::event_mapping::ToolKind;
712
713#[cfg(test)]
714mod tests {
715 use std::time::Duration;
716
717 use super::*;
718 use adk_core::{FinishReason, Llm, LlmRequest, LlmResponse, LlmResponseStream};
719 use async_stream::stream;
720 use async_trait::async_trait;
721
722 struct TestLlm {
724 response_text: String,
725 }
726
727 impl TestLlm {
728 fn new(text: &str) -> Self {
729 Self { response_text: text.to_string() }
730 }
731 }
732
733 #[async_trait]
734 impl Llm for TestLlm {
735 fn name(&self) -> &str {
736 "test-llm"
737 }
738
739 async fn generate_content(
740 &self,
741 _request: LlmRequest,
742 _stream: bool,
743 ) -> adk_core::Result<LlmResponseStream> {
744 let text = self.response_text.clone();
745 let s = stream! {
746 yield Ok(LlmResponse {
747 content: Some(Content::new("model").with_text(&text)),
748 partial: false,
749 turn_complete: true,
750 finish_reason: Some(FinishReason::Stop),
751 ..Default::default()
752 });
753 };
754 Ok(Box::pin(s))
755 }
756 }
757
758 fn build_test_agent(llm: impl Llm + 'static) -> Arc<dyn Agent> {
760 let agent =
761 adk_agent::LlmAgentBuilder::new("test-agent").model(Arc::new(llm)).build().unwrap();
762 Arc::new(agent)
763 }
764
765 fn create_test_loop()
767 -> (mpsc::Sender<UserEvent>, broadcast::Receiver<SessionEvent>, CancellationToken, SessionLoop)
768 {
769 let (event_tx, event_rx) = mpsc::channel(64);
770 let (broadcast_tx, broadcast_rx) = broadcast::channel(256);
771 let cancel = CancellationToken::new();
772 let parking = Arc::new(ToolParkingLot::new(Duration::from_secs(5)));
773 let agent = build_test_agent(TestLlm::new("Hello from the agent"));
774 let session_service: Arc<dyn SessionService> =
775 Arc::new(adk_session::InMemorySessionService::new());
776
777 let session_loop = SessionLoop::new(
778 "test_session".to_string(),
779 event_rx,
780 broadcast_tx,
781 parking,
782 cancel.clone(),
783 agent,
784 session_service,
785 );
786
787 (event_tx, broadcast_rx, cancel, session_loop)
788 }
789
790 #[tokio::test]
791 async fn test_basic_message_flow() {
792 let (event_tx, mut broadcast_rx, _cancel, session_loop) = create_test_loop();
793
794 let handle = tokio::spawn(session_loop.run());
795
796 event_tx
798 .send(UserEvent::Message {
799 content: vec![ContentBlock::Text { text: "Hello".to_string() }],
800 })
801 .await
802 .unwrap();
803
804 let ev1 = broadcast_rx.recv().await.unwrap();
806 match ev1 {
807 SessionEvent::StatusRunning { seq } => assert_eq!(seq, 0),
808 other => panic!("expected StatusRunning, got: {other:?}"),
809 }
810
811 let mut got_message = false;
813 let mut got_idle = false;
814 for _ in 0..10 {
815 match tokio::time::timeout(Duration::from_secs(5), broadcast_rx.recv()).await {
816 Ok(Ok(SessionEvent::Message { content, .. })) => {
817 assert!(!content.is_empty());
818 got_message = true;
819 }
820 Ok(Ok(SessionEvent::StatusIdle { stop_reason, .. })) => {
821 assert!(matches!(stop_reason, Some(StopReason::EndTurn)));
822 got_idle = true;
823 break;
824 }
825 Ok(Ok(SessionEvent::Error { message, .. })) => {
826 debug!("got error event: {message}");
828 }
829 Ok(Ok(other)) => {
830 debug!("got other event: {other:?}");
831 }
832 Ok(Err(_)) => break,
833 Err(_) => break,
834 }
835 }
836
837 assert!(got_idle, "expected StatusIdle event");
839
840 drop(event_tx);
842 let result = handle.await.unwrap();
843 assert!(result.is_ok());
844
845 let _ = got_message;
850 }
851
852 #[tokio::test]
853 async fn test_seq_monotonically_increases() {
854 let (event_tx, mut broadcast_rx, _cancel, session_loop) = create_test_loop();
855
856 let handle = tokio::spawn(session_loop.run());
857
858 event_tx
860 .send(UserEvent::Message {
861 content: vec![ContentBlock::Text { text: "First".to_string() }],
862 })
863 .await
864 .unwrap();
865
866 let mut seqs = Vec::new();
868 for _ in 0..10 {
869 match tokio::time::timeout(Duration::from_secs(5), broadcast_rx.recv()).await {
870 Ok(Ok(ev)) => {
871 let seq = match &ev {
872 SessionEvent::StatusRunning { seq } => *seq,
873 SessionEvent::Message { seq, .. } => *seq,
874 SessionEvent::StatusIdle { seq, .. } => *seq,
875 SessionEvent::ToolUse { seq, .. } => *seq,
876 SessionEvent::CustomToolUse { seq, .. } => *seq,
877 SessionEvent::McpToolUse { seq, .. } => *seq,
878 SessionEvent::Error { seq, .. } => *seq,
879 };
880 seqs.push(seq);
881 if matches!(ev, SessionEvent::StatusIdle { .. }) {
882 break;
883 }
884 }
885 _ => break,
886 }
887 }
888
889 assert!(seqs.len() >= 2, "expected at least 2 events");
891 for window in seqs.windows(2) {
892 assert!(
893 window[1] > window[0],
894 "seq must be strictly increasing: {} should be > {}",
895 window[1],
896 window[0]
897 );
898 }
899
900 drop(event_tx);
901 handle.await.unwrap().unwrap();
902 }
903
904 #[tokio::test]
905 async fn test_interrupt_stops_loop() {
906 let (event_tx, mut broadcast_rx, cancel, session_loop) = create_test_loop();
907
908 let handle = tokio::spawn(session_loop.run());
909
910 tokio::time::sleep(Duration::from_millis(10)).await;
912
913 cancel.cancel();
915
916 let ev = broadcast_rx.recv().await.unwrap();
918 match ev {
919 SessionEvent::StatusIdle { stop_reason, .. } => {
920 assert!(matches!(stop_reason, Some(StopReason::EndTurn)));
921 }
922 other => panic!("expected StatusIdle on interrupt, got: {other:?}"),
923 }
924
925 let result = handle.await.unwrap();
927 assert!(result.is_ok());
928
929 drop(event_tx);
930 }
931
932 #[tokio::test]
933 async fn test_user_interrupt_event_stops_loop() {
934 let (event_tx, mut broadcast_rx, _cancel, session_loop) = create_test_loop();
935
936 let handle = tokio::spawn(session_loop.run());
937
938 event_tx.send(UserEvent::Interrupt {}).await.unwrap();
940
941 let ev = broadcast_rx.recv().await.unwrap();
943 match ev {
944 SessionEvent::StatusIdle { stop_reason, .. } => {
945 assert!(matches!(stop_reason, Some(StopReason::EndTurn)));
946 }
947 other => panic!("expected StatusIdle, got: {other:?}"),
948 }
949
950 let result = handle.await.unwrap();
951 assert!(result.is_ok());
952
953 drop(event_tx);
954 }
955
956 #[tokio::test]
957 async fn test_pause_and_resume() {
958 let (event_tx, event_rx) = mpsc::channel(64);
959 let (broadcast_tx, mut broadcast_rx) = broadcast::channel(256);
960 let cancel = CancellationToken::new();
961 let parking = Arc::new(ToolParkingLot::new(Duration::from_secs(5)));
962 let pause_flag = Arc::new(Mutex::new(false));
963 let pause_notify = Arc::new(Notify::new());
964 let agent = build_test_agent(TestLlm::new("resumed response"));
965 let session_service: Arc<dyn SessionService> =
966 Arc::new(adk_session::InMemorySessionService::new());
967
968 #[cfg(feature = "memory")]
969 let session_loop = SessionLoop::with_pause_controls(
970 "pause_test".to_string(),
971 event_rx,
972 broadcast_tx,
973 parking,
974 cancel.clone(),
975 Arc::clone(&pause_flag),
976 Arc::clone(&pause_notify),
977 Arc::new(RwLock::new(CheckpointManager::new("pause_test".to_string()))),
978 agent,
979 session_service,
980 None,
981 );
982 #[cfg(not(feature = "memory"))]
983 let session_loop = SessionLoop::with_pause_controls(
984 "pause_test".to_string(),
985 event_rx,
986 broadcast_tx,
987 parking,
988 cancel.clone(),
989 Arc::clone(&pause_flag),
990 Arc::clone(&pause_notify),
991 Arc::new(RwLock::new(CheckpointManager::new("pause_test".to_string()))),
992 agent,
993 session_service,
994 );
995
996 let handle = tokio::spawn(session_loop.run());
997
998 *pause_flag.lock().await = true;
1000
1001 event_tx
1003 .send(UserEvent::Message {
1004 content: vec![ContentBlock::Text { text: "While paused".to_string() }],
1005 })
1006 .await
1007 .unwrap();
1008
1009 tokio::time::sleep(Duration::from_millis(50)).await;
1011
1012 assert!(broadcast_rx.try_recv().is_err());
1014
1015 *pause_flag.lock().await = false;
1017 pause_notify.notify_one();
1018
1019 let ev1 = tokio::time::timeout(Duration::from_secs(2), broadcast_rx.recv())
1021 .await
1022 .expect("timed out waiting for event after resume")
1023 .unwrap();
1024
1025 match ev1 {
1026 SessionEvent::StatusRunning { .. } => {}
1027 other => panic!("expected StatusRunning after resume, got: {other:?}"),
1028 }
1029
1030 drop(event_tx);
1032 handle.await.unwrap().unwrap();
1033 }
1034
1035 #[tokio::test]
1036 async fn test_channel_close_stops_loop() {
1037 let (event_tx, event_rx) = mpsc::channel(64);
1038 let (broadcast_tx, _broadcast_rx) = broadcast::channel(256);
1039 let cancel = CancellationToken::new();
1040 let parking = Arc::new(ToolParkingLot::new(Duration::from_secs(5)));
1041 let agent = build_test_agent(TestLlm::new("test"));
1042 let session_service: Arc<dyn SessionService> =
1043 Arc::new(adk_session::InMemorySessionService::new());
1044
1045 let session_loop = SessionLoop::new(
1046 "close_test".to_string(),
1047 event_rx,
1048 broadcast_tx,
1049 parking,
1050 cancel,
1051 agent,
1052 session_service,
1053 );
1054
1055 let handle = tokio::spawn(session_loop.run());
1056
1057 drop(event_tx);
1059
1060 let result = handle.await.unwrap();
1062 assert!(result.is_ok());
1063 }
1064
1065 #[tokio::test]
1066 async fn test_custom_tool_result_delivery() {
1067 let (event_tx, event_rx) = mpsc::channel(64);
1068 let (broadcast_tx, _broadcast_rx) = broadcast::channel(256);
1069 let cancel = CancellationToken::new();
1070 let parking = Arc::new(ToolParkingLot::new(Duration::from_secs(5)));
1071 let parking_clone = Arc::clone(&parking);
1072 let agent = build_test_agent(TestLlm::new("test"));
1073 let session_service: Arc<dyn SessionService> =
1074 Arc::new(adk_session::InMemorySessionService::new());
1075
1076 let session_loop = SessionLoop::new(
1077 "parking_test".to_string(),
1078 event_rx,
1079 broadcast_tx,
1080 parking_clone,
1081 cancel,
1082 agent,
1083 session_service,
1084 );
1085
1086 let handle = tokio::spawn(session_loop.run());
1087
1088 let parking_for_park = Arc::clone(&parking);
1090 let park_handle = tokio::spawn(async move { parking_for_park.park("ctu_test_001").await });
1091
1092 tokio::time::sleep(Duration::from_millis(10)).await;
1094
1095 event_tx
1097 .send(UserEvent::CustomToolResult {
1098 custom_tool_use_id: "ctu_test_001".to_string(),
1099 content: vec![ContentBlock::Text { text: "tool output".to_string() }],
1100 })
1101 .await
1102 .unwrap();
1103
1104 let result = tokio::time::timeout(Duration::from_secs(2), park_handle)
1106 .await
1107 .expect("park timed out")
1108 .unwrap()
1109 .unwrap();
1110
1111 assert_eq!(result.len(), 1);
1112 match &result[0] {
1113 ContentBlock::Text { text } => assert_eq!(text, "tool output"),
1114 _ => panic!("expected Text"),
1115 }
1116
1117 drop(event_tx);
1119 handle.await.unwrap().unwrap();
1120 }
1121
1122 #[tokio::test]
1123 async fn test_tool_classification() {
1124 let (event_tx, event_rx) = mpsc::channel(64);
1125 let (broadcast_tx, _) = broadcast::channel(256);
1126 let cancel = CancellationToken::new();
1127 let parking = Arc::new(ToolParkingLot::new(Duration::from_secs(5)));
1128 let agent = build_test_agent(TestLlm::new("test"));
1129 let session_service: Arc<dyn SessionService> =
1130 Arc::new(adk_session::InMemorySessionService::new());
1131
1132 let session_loop = SessionLoop::new(
1133 "classify_test".to_string(),
1134 event_rx,
1135 broadcast_tx,
1136 parking,
1137 cancel,
1138 agent,
1139 session_service,
1140 );
1141
1142 assert!(matches!(session_loop.classify_tool("bash"), ToolKind::Builtin));
1144 assert!(matches!(session_loop.classify_tool("filesystem"), ToolKind::Builtin));
1145 assert!(matches!(session_loop.classify_tool("web_search"), ToolKind::Builtin));
1146 assert!(matches!(session_loop.classify_tool("web_fetch"), ToolKind::Builtin));
1147 assert!(matches!(session_loop.classify_tool("code_execution"), ToolKind::Builtin));
1148
1149 assert!(matches!(session_loop.classify_tool("mcp_file_read"), ToolKind::Mcp));
1151 assert!(matches!(session_loop.classify_tool("server::tool"), ToolKind::Mcp));
1152
1153 assert!(matches!(session_loop.classify_tool("get_weather"), ToolKind::Custom));
1155 assert!(matches!(session_loop.classify_tool("deploy"), ToolKind::Custom));
1156
1157 drop(event_tx);
1158 }
1159}