1use async_trait::async_trait;
36use serde::{Deserialize, Serialize};
37use std::sync::Arc;
38use tracing::debug;
39
40use crate::error::McpError;
41use crate::protocol::{McpResource, ResourceContent};
42use crate::server::ResourceHandler;
43
44pub struct SessionResourceHandler<S: SessionStoreRead> {
52 store: Arc<S>,
53 user_filter: Option<String>,
55 max_list_size: usize,
57}
58
59#[async_trait]
61pub trait SessionStoreRead: Send + Sync {
62 async fn list_ids(&self) -> Result<Vec<String>, String>;
64
65 async fn load_json(&self, session_id: &str) -> Result<Option<String>, String>;
67
68 async fn get_metadata(&self, session_id: &str) -> Result<Option<SessionMetadata>, String>;
70
71 async fn get_messages_json(&self, session_id: &str) -> Result<Option<String>, String>;
73
74 async fn get_turn_json(
76 &self,
77 session_id: &str,
78 turn_number: u32,
79 ) -> Result<Option<String>, String>;
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SessionMetadata {
85 pub id: String,
86 pub user_id: Option<String>,
87 pub created_at: u64,
88 pub updated_at: u64,
89 pub message_count: usize,
90 pub turn_count: usize,
91}
92
93impl<S: SessionStoreRead> SessionResourceHandler<S> {
94 pub fn new(store: Arc<S>) -> Self {
96 Self {
97 store,
98 user_filter: None,
99 max_list_size: 100,
100 }
101 }
102
103 pub fn with_user_filter(mut self, user_id: impl Into<String>) -> Self {
105 self.user_filter = Some(user_id.into());
106 self
107 }
108
109 pub fn with_max_list_size(mut self, size: usize) -> Self {
111 self.max_list_size = size;
112 self
113 }
114
115 fn parse_uri(uri: &str) -> Option<SessionUriParts> {
117 let uri = uri.strip_prefix("session://")?;
118
119 if uri == "list" {
120 return Some(SessionUriParts::List);
121 }
122
123 let parts: Vec<&str> = uri.split('/').collect();
124
125 match parts.as_slice() {
126 [session_id] => Some(SessionUriParts::Session(session_id.to_string())),
127 [session_id, "messages"] => Some(SessionUriParts::Messages(session_id.to_string())),
128 [session_id, "turns", turn_num] => {
129 let turn: u32 = turn_num.parse().ok()?;
130 Some(SessionUriParts::Turn(session_id.to_string(), turn))
131 }
132 _ => None,
133 }
134 }
135}
136
137#[derive(Debug)]
138enum SessionUriParts {
139 List,
140 Session(String),
141 Messages(String),
142 Turn(String, u32),
143}
144
145#[async_trait]
146impl<S: SessionStoreRead + 'static> ResourceHandler for SessionResourceHandler<S> {
147 fn list(&self) -> Vec<McpResource> {
148 vec![McpResource {
149 uri: "session://list".to_string(),
150 name: "Sessions".to_string(),
151 description: Some("List all conversation sessions".to_string()),
152 mime_type: Some("application/json".to_string()),
153 }]
154 }
155
156 async fn read(&self, uri: &str) -> Result<ResourceContent, McpError> {
157 debug!(uri = %uri, "Reading session resource");
158
159 let parts = Self::parse_uri(uri)
160 .ok_or_else(|| McpError::ResourceNotFound(format!("Invalid session URI: {}", uri)))?;
161
162 match parts {
163 SessionUriParts::List => {
164 let ids = self.store.list_ids().await.map_err(McpError::Internal)?;
165
166 let mut sessions = Vec::new();
167 for id in ids.into_iter().take(self.max_list_size) {
168 if let Ok(Some(meta)) = self.store.get_metadata(&id).await {
169 if let Some(ref filter) = self.user_filter {
171 if meta.user_id.as_ref() != Some(filter) {
172 continue;
173 }
174 }
175 sessions.push(meta);
176 }
177 }
178
179 let json = serde_json::to_string_pretty(&sessions)
180 .map_err(|e| McpError::Internal(e.to_string()))?;
181
182 Ok(ResourceContent {
183 uri: uri.to_string(),
184 mime_type: Some("application/json".to_string()),
185 text: Some(json),
186 blob: None,
187 })
188 }
189
190 SessionUriParts::Session(session_id) => {
191 let json = self
192 .store
193 .load_json(&session_id)
194 .await
195 .map_err(McpError::Internal)?
196 .ok_or_else(|| {
197 McpError::ResourceNotFound(format!("Session not found: {}", session_id))
198 })?;
199
200 Ok(ResourceContent {
201 uri: uri.to_string(),
202 mime_type: Some("application/json".to_string()),
203 text: Some(json),
204 blob: None,
205 })
206 }
207
208 SessionUriParts::Messages(session_id) => {
209 let json = self
210 .store
211 .get_messages_json(&session_id)
212 .await
213 .map_err(McpError::Internal)?
214 .ok_or_else(|| {
215 McpError::ResourceNotFound(format!("Session not found: {}", session_id))
216 })?;
217
218 Ok(ResourceContent {
219 uri: uri.to_string(),
220 mime_type: Some("application/json".to_string()),
221 text: Some(json),
222 blob: None,
223 })
224 }
225
226 SessionUriParts::Turn(session_id, turn_number) => {
227 let json = self
228 .store
229 .get_turn_json(&session_id, turn_number)
230 .await
231 .map_err(McpError::Internal)?
232 .ok_or_else(|| {
233 McpError::ResourceNotFound(format!(
234 "Turn {} not found in session {}",
235 turn_number, session_id
236 ))
237 })?;
238
239 Ok(ResourceContent {
240 uri: uri.to_string(),
241 mime_type: Some("application/json".to_string()),
242 text: Some(json),
243 blob: None,
244 })
245 }
246 }
247 }
248}
249
250pub struct TraceResourceHandler<T: TraceStoreRead> {
258 store: Arc<T>,
259 agent_filter: Option<String>,
261 success_filter: Option<bool>,
263 max_list_size: usize,
265}
266
267#[async_trait]
269pub trait TraceStoreRead: Send + Sync {
270 fn list_ids(&self) -> Vec<String>;
272
273 fn get_json(&self, task_id: &str) -> Option<String>;
275
276 fn get_metadata(&self, task_id: &str) -> Option<TraceMetadata>;
278
279 fn get_steps_json(&self, task_id: &str) -> Option<String>;
281
282 fn get_summary_json(&self, task_id: &str) -> Option<String>;
284
285 fn filter_by_agent(&self, agent_name: &str) -> Vec<String>;
287
288 fn filter_by_success(&self, success: bool) -> Vec<String>;
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct TraceMetadata {
295 pub task_id: String,
296 pub agent_name: String,
297 pub success: bool,
298 pub total_duration_ms: u64,
299 pub step_count: usize,
300 pub llm_calls: usize,
301 pub tool_calls: usize,
302}
303
304impl<T: TraceStoreRead> TraceResourceHandler<T> {
305 pub fn new(store: Arc<T>) -> Self {
307 Self {
308 store,
309 agent_filter: None,
310 success_filter: None,
311 max_list_size: 100,
312 }
313 }
314
315 pub fn with_agent_filter(mut self, agent_name: impl Into<String>) -> Self {
317 self.agent_filter = Some(agent_name.into());
318 self
319 }
320
321 pub fn with_success_filter(mut self, success: bool) -> Self {
323 self.success_filter = Some(success);
324 self
325 }
326
327 pub fn with_max_list_size(mut self, size: usize) -> Self {
329 self.max_list_size = size;
330 self
331 }
332
333 fn parse_uri(uri: &str) -> Option<TraceUriParts> {
335 let uri = uri.strip_prefix("trace://")?;
336
337 if uri == "list" {
338 return Some(TraceUriParts::List);
339 }
340
341 let parts: Vec<&str> = uri.split('/').collect();
342
343 match parts.as_slice() {
344 [task_id] => Some(TraceUriParts::Trace(task_id.to_string())),
345 [task_id, "steps"] => Some(TraceUriParts::Steps(task_id.to_string())),
346 [task_id, "summary"] => Some(TraceUriParts::Summary(task_id.to_string())),
347 _ => None,
348 }
349 }
350}
351
352#[derive(Debug)]
353enum TraceUriParts {
354 List,
355 Trace(String),
356 Steps(String),
357 Summary(String),
358}
359
360#[async_trait]
361impl<T: TraceStoreRead + 'static> ResourceHandler for TraceResourceHandler<T> {
362 fn list(&self) -> Vec<McpResource> {
363 vec![McpResource {
364 uri: "trace://list".to_string(),
365 name: "Traces".to_string(),
366 description: Some("List all execution traces".to_string()),
367 mime_type: Some("application/json".to_string()),
368 }]
369 }
370
371 async fn read(&self, uri: &str) -> Result<ResourceContent, McpError> {
372 debug!(uri = %uri, "Reading trace resource");
373
374 let parts = Self::parse_uri(uri)
375 .ok_or_else(|| McpError::ResourceNotFound(format!("Invalid trace URI: {}", uri)))?;
376
377 match parts {
378 TraceUriParts::List => {
379 let ids = if let Some(ref agent) = self.agent_filter {
381 self.store.filter_by_agent(agent)
382 } else if let Some(success) = self.success_filter {
383 self.store.filter_by_success(success)
384 } else {
385 self.store.list_ids()
386 };
387
388 let mut traces = Vec::new();
389 for id in ids.into_iter().take(self.max_list_size) {
390 if let Some(meta) = self.store.get_metadata(&id) {
391 traces.push(meta);
392 }
393 }
394
395 let json = serde_json::to_string_pretty(&traces)
396 .map_err(|e| McpError::Internal(e.to_string()))?;
397
398 Ok(ResourceContent {
399 uri: uri.to_string(),
400 mime_type: Some("application/json".to_string()),
401 text: Some(json),
402 blob: None,
403 })
404 }
405
406 TraceUriParts::Trace(task_id) => {
407 let json = self.store.get_json(&task_id).ok_or_else(|| {
408 McpError::ResourceNotFound(format!("Trace not found: {}", task_id))
409 })?;
410
411 Ok(ResourceContent {
412 uri: uri.to_string(),
413 mime_type: Some("application/json".to_string()),
414 text: Some(json),
415 blob: None,
416 })
417 }
418
419 TraceUriParts::Steps(task_id) => {
420 let json = self.store.get_steps_json(&task_id).ok_or_else(|| {
421 McpError::ResourceNotFound(format!("Trace not found: {}", task_id))
422 })?;
423
424 Ok(ResourceContent {
425 uri: uri.to_string(),
426 mime_type: Some("application/json".to_string()),
427 text: Some(json),
428 blob: None,
429 })
430 }
431
432 TraceUriParts::Summary(task_id) => {
433 let json = self.store.get_summary_json(&task_id).ok_or_else(|| {
434 McpError::ResourceNotFound(format!("Trace not found: {}", task_id))
435 })?;
436
437 Ok(ResourceContent {
438 uri: uri.to_string(),
439 mime_type: Some("application/json".to_string()),
440 text: Some(json),
441 blob: None,
442 })
443 }
444 }
445 }
446}
447
448pub struct MemorySessionStoreAdapter {
454 sessions: parking_lot::RwLock<std::collections::HashMap<String, String>>,
455}
456
457impl MemorySessionStoreAdapter {
458 pub fn new() -> Self {
459 Self {
460 sessions: parking_lot::RwLock::new(std::collections::HashMap::new()),
461 }
462 }
463
464 pub fn store(&self, session_id: &str, session_json: &str) {
466 self.sessions
467 .write()
468 .insert(session_id.to_string(), session_json.to_string());
469 }
470
471 pub fn store_session<T: Serialize>(&self, session_id: &str, session: &T) {
473 if let Ok(json) = serde_json::to_string(session) {
474 self.store(session_id, &json);
475 }
476 }
477}
478
479impl Default for MemorySessionStoreAdapter {
480 fn default() -> Self {
481 Self::new()
482 }
483}
484
485#[async_trait]
486impl SessionStoreRead for MemorySessionStoreAdapter {
487 async fn list_ids(&self) -> Result<Vec<String>, String> {
488 Ok(self.sessions.read().keys().cloned().collect())
489 }
490
491 async fn load_json(&self, session_id: &str) -> Result<Option<String>, String> {
492 Ok(self.sessions.read().get(session_id).cloned())
493 }
494
495 async fn get_metadata(&self, session_id: &str) -> Result<Option<SessionMetadata>, String> {
496 let sessions = self.sessions.read();
497 let json = match sessions.get(session_id) {
498 Some(j) => j,
499 None => return Ok(None),
500 };
501
502 let value: serde_json::Value = serde_json::from_str(json).map_err(|e| e.to_string())?;
504
505 Ok(Some(SessionMetadata {
506 id: value["id"].as_str().unwrap_or(session_id).to_string(),
507 user_id: value["user_id"].as_str().map(|s| s.to_string()),
508 created_at: value["created_at"].as_u64().unwrap_or(0),
509 updated_at: value["updated_at"].as_u64().unwrap_or(0),
510 message_count: value["messages"].as_array().map(|a| a.len()).unwrap_or(0),
511 turn_count: value["turns"].as_array().map(|a| a.len()).unwrap_or(0),
512 }))
513 }
514
515 async fn get_messages_json(&self, session_id: &str) -> Result<Option<String>, String> {
516 let sessions = self.sessions.read();
517 let json = match sessions.get(session_id) {
518 Some(j) => j,
519 None => return Ok(None),
520 };
521
522 let value: serde_json::Value = serde_json::from_str(json).map_err(|e| e.to_string())?;
523
524 let messages = &value["messages"];
525 Ok(Some(
526 serde_json::to_string_pretty(messages).map_err(|e| e.to_string())?,
527 ))
528 }
529
530 async fn get_turn_json(
531 &self,
532 session_id: &str,
533 turn_number: u32,
534 ) -> Result<Option<String>, String> {
535 let sessions = self.sessions.read();
536 let json = match sessions.get(session_id) {
537 Some(j) => j,
538 None => return Ok(None),
539 };
540
541 let value: serde_json::Value = serde_json::from_str(json).map_err(|e| e.to_string())?;
542
543 let turns = value["turns"].as_array();
544 let turn = turns.and_then(|t| t.get(turn_number as usize - 1));
545
546 match turn {
547 Some(t) => Ok(Some(
548 serde_json::to_string_pretty(t).map_err(|e| e.to_string())?,
549 )),
550 None => Ok(None),
551 }
552 }
553}
554
555pub struct MemoryTraceStoreAdapter {
557 traces: parking_lot::RwLock<std::collections::HashMap<String, String>>,
558}
559
560impl MemoryTraceStoreAdapter {
561 pub fn new() -> Self {
562 Self {
563 traces: parking_lot::RwLock::new(std::collections::HashMap::new()),
564 }
565 }
566
567 pub fn store(&self, task_id: &str, trace_json: &str) {
569 self.traces
570 .write()
571 .insert(task_id.to_string(), trace_json.to_string());
572 }
573
574 pub fn store_trace<T: Serialize>(&self, task_id: &str, trace: &T) {
576 if let Ok(json) = serde_json::to_string(trace) {
577 self.store(task_id, &json);
578 }
579 }
580}
581
582impl Default for MemoryTraceStoreAdapter {
583 fn default() -> Self {
584 Self::new()
585 }
586}
587
588impl TraceStoreRead for MemoryTraceStoreAdapter {
589 fn list_ids(&self) -> Vec<String> {
590 self.traces.read().keys().cloned().collect()
591 }
592
593 fn get_json(&self, task_id: &str) -> Option<String> {
594 self.traces.read().get(task_id).cloned()
595 }
596
597 fn get_metadata(&self, task_id: &str) -> Option<TraceMetadata> {
598 let traces = self.traces.read();
599 let json = traces.get(task_id)?;
600
601 let value: serde_json::Value = serde_json::from_str(json).ok()?;
602
603 Some(TraceMetadata {
604 task_id: value["task_id"].as_str().unwrap_or(task_id).to_string(),
605 agent_name: value["agent_name"]
606 .as_str()
607 .unwrap_or("unknown")
608 .to_string(),
609 success: value["success"].as_bool().unwrap_or(false),
610 total_duration_ms: value["total_duration_ms"].as_u64().unwrap_or(0),
611 step_count: value["steps"].as_array().map(|a| a.len()).unwrap_or(0),
612 llm_calls: value["steps"]
613 .as_array()
614 .map(|steps| {
615 steps
616 .iter()
617 .filter(|s| s["step_type"] == "llm_call")
618 .count()
619 })
620 .unwrap_or(0),
621 tool_calls: value["steps"]
622 .as_array()
623 .map(|steps| {
624 steps
625 .iter()
626 .filter(|s| s["step_type"] == "tool_call")
627 .count()
628 })
629 .unwrap_or(0),
630 })
631 }
632
633 fn get_steps_json(&self, task_id: &str) -> Option<String> {
634 let traces = self.traces.read();
635 let json = traces.get(task_id)?;
636
637 let value: serde_json::Value = serde_json::from_str(json).ok()?;
638 let steps = &value["steps"];
639
640 serde_json::to_string_pretty(steps).ok()
641 }
642
643 fn get_summary_json(&self, task_id: &str) -> Option<String> {
644 let meta = self.get_metadata(task_id)?;
645 serde_json::to_string_pretty(&meta).ok()
646 }
647
648 fn filter_by_agent(&self, agent_name: &str) -> Vec<String> {
649 self.traces
650 .read()
651 .iter()
652 .filter_map(|(id, json)| {
653 let value: serde_json::Value = serde_json::from_str(json).ok()?;
654 if value["agent_name"].as_str() == Some(agent_name) {
655 Some(id.clone())
656 } else {
657 None
658 }
659 })
660 .collect()
661 }
662
663 fn filter_by_success(&self, success: bool) -> Vec<String> {
664 self.traces
665 .read()
666 .iter()
667 .filter_map(|(id, json)| {
668 let value: serde_json::Value = serde_json::from_str(json).ok()?;
669 if value["success"].as_bool() == Some(success) {
670 Some(id.clone())
671 } else {
672 None
673 }
674 })
675 .collect()
676 }
677}
678
679#[cfg(test)]
684mod tests {
685 use super::*;
686
687 fn sample_session_json() -> String {
688 serde_json::json!({
689 "id": "session-1",
690 "user_id": "user-123",
691 "created_at": 1700000000,
692 "updated_at": 1700001000,
693 "messages": [
694 {"role": "user", "content": "Hello"},
695 {"role": "assistant", "content": "Hi there!"}
696 ],
697 "turns": [
698 {
699 "number": 1,
700 "user_message": {"role": "user", "content": "Hello"},
701 "assistant_response": {"role": "assistant", "content": "Hi there!"}
702 }
703 ]
704 })
705 .to_string()
706 }
707
708 fn sample_trace_json() -> String {
709 serde_json::json!({
710 "task_id": "task-1",
711 "agent_name": "research-agent",
712 "success": true,
713 "total_duration_ms": 1500,
714 "steps": [
715 {"step_type": "llm_call", "duration_ms": 500, "success": true},
716 {"step_type": "tool_call", "duration_ms": 200, "success": true},
717 {"step_type": "llm_call", "duration_ms": 600, "success": true}
718 ],
719 "metadata": {}
720 })
721 .to_string()
722 }
723
724 #[tokio::test]
725 async fn test_session_resource_list() {
726 let store = Arc::new(MemorySessionStoreAdapter::new());
727 store.store("session-1", &sample_session_json());
728 store.store("session-2", &sample_session_json());
729
730 let handler = SessionResourceHandler::new(store);
731 let resources = handler.list();
732
733 assert_eq!(resources.len(), 1);
734 assert_eq!(resources[0].uri, "session://list");
735 }
736
737 #[tokio::test]
738 async fn test_session_resource_read_list() {
739 let store = Arc::new(MemorySessionStoreAdapter::new());
740 store.store("session-1", &sample_session_json());
741
742 let handler = SessionResourceHandler::new(store);
743 let content = handler.read("session://list").await.unwrap();
744
745 assert!(content.text.is_some());
746 let text = content.text.unwrap();
747 assert!(text.contains("session-1"));
748 }
749
750 #[tokio::test]
751 async fn test_session_resource_read_session() {
752 let store = Arc::new(MemorySessionStoreAdapter::new());
753 store.store("session-1", &sample_session_json());
754
755 let handler = SessionResourceHandler::new(store);
756 let content = handler.read("session://session-1").await.unwrap();
757
758 assert!(content.text.is_some());
759 let text = content.text.unwrap();
760 assert!(text.contains("user-123"));
761 }
762
763 #[tokio::test]
764 async fn test_session_resource_read_messages() {
765 let store = Arc::new(MemorySessionStoreAdapter::new());
766 store.store("session-1", &sample_session_json());
767
768 let handler = SessionResourceHandler::new(store);
769 let content = handler.read("session://session-1/messages").await.unwrap();
770
771 assert!(content.text.is_some());
772 let text = content.text.unwrap();
773 assert!(text.contains("Hello"));
774 assert!(text.contains("Hi there!"));
775 }
776
777 #[tokio::test]
778 async fn test_session_resource_read_turn() {
779 let store = Arc::new(MemorySessionStoreAdapter::new());
780 store.store("session-1", &sample_session_json());
781
782 let handler = SessionResourceHandler::new(store);
783 let content = handler.read("session://session-1/turns/1").await.unwrap();
784
785 assert!(content.text.is_some());
786 let text = content.text.unwrap();
787 assert!(text.contains("Hello"));
788 }
789
790 #[tokio::test]
791 async fn test_session_resource_not_found() {
792 let store = Arc::new(MemorySessionStoreAdapter::new());
793 let handler = SessionResourceHandler::new(store);
794
795 let result = handler.read("session://nonexistent").await;
796 assert!(result.is_err());
797 }
798
799 #[tokio::test]
800 async fn test_trace_resource_list() {
801 let store = Arc::new(MemoryTraceStoreAdapter::new());
802 store.store("task-1", &sample_trace_json());
803
804 let handler = TraceResourceHandler::new(store);
805 let resources = handler.list();
806
807 assert_eq!(resources.len(), 1);
808 assert_eq!(resources[0].uri, "trace://list");
809 }
810
811 #[tokio::test]
812 async fn test_trace_resource_read_list() {
813 let store = Arc::new(MemoryTraceStoreAdapter::new());
814 store.store("task-1", &sample_trace_json());
815
816 let handler = TraceResourceHandler::new(store);
817 let content = handler.read("trace://list").await.unwrap();
818
819 assert!(content.text.is_some());
820 let text = content.text.unwrap();
821 assert!(text.contains("task-1"));
822 assert!(text.contains("research-agent"));
823 }
824
825 #[tokio::test]
826 async fn test_trace_resource_read_trace() {
827 let store = Arc::new(MemoryTraceStoreAdapter::new());
828 store.store("task-1", &sample_trace_json());
829
830 let handler = TraceResourceHandler::new(store);
831 let content = handler.read("trace://task-1").await.unwrap();
832
833 assert!(content.text.is_some());
834 let text = content.text.unwrap();
835 assert!(text.contains("research-agent"));
836 assert!(text.contains("1500"));
837 }
838
839 #[tokio::test]
840 async fn test_trace_resource_read_steps() {
841 let store = Arc::new(MemoryTraceStoreAdapter::new());
842 store.store("task-1", &sample_trace_json());
843
844 let handler = TraceResourceHandler::new(store);
845 let content = handler.read("trace://task-1/steps").await.unwrap();
846
847 assert!(content.text.is_some());
848 let text = content.text.unwrap();
849 assert!(text.contains("llm_call"));
850 assert!(text.contains("tool_call"));
851 }
852
853 #[tokio::test]
854 async fn test_trace_resource_read_summary() {
855 let store = Arc::new(MemoryTraceStoreAdapter::new());
856 store.store("task-1", &sample_trace_json());
857
858 let handler = TraceResourceHandler::new(store);
859 let content = handler.read("trace://task-1/summary").await.unwrap();
860
861 assert!(content.text.is_some());
862 let text = content.text.unwrap();
863 assert!(text.contains("llm_calls"));
864 assert!(text.contains("tool_calls"));
865 }
866
867 #[tokio::test]
868 async fn test_trace_filter_by_agent() {
869 let store = Arc::new(MemoryTraceStoreAdapter::new());
870 store.store("task-1", &sample_trace_json());
871 store.store(
872 "task-2",
873 &serde_json::json!({
874 "task_id": "task-2",
875 "agent_name": "other-agent",
876 "success": true,
877 "total_duration_ms": 500,
878 "steps": []
879 })
880 .to_string(),
881 );
882
883 let handler = TraceResourceHandler::new(store).with_agent_filter("research-agent");
884 let content = handler.read("trace://list").await.unwrap();
885
886 let text = content.text.unwrap();
887 assert!(text.contains("task-1"));
888 assert!(!text.contains("task-2"));
889 }
890
891 #[tokio::test]
892 async fn test_trace_filter_by_success() {
893 let store = Arc::new(MemoryTraceStoreAdapter::new());
894 store.store("task-1", &sample_trace_json()); store.store(
896 "task-2",
897 &serde_json::json!({
898 "task_id": "task-2",
899 "agent_name": "agent",
900 "success": false,
901 "total_duration_ms": 500,
902 "steps": []
903 })
904 .to_string(),
905 );
906
907 let handler = TraceResourceHandler::new(store).with_success_filter(false);
908 let content = handler.read("trace://list").await.unwrap();
909
910 let text = content.text.unwrap();
911 assert!(!text.contains("task-1"));
912 assert!(text.contains("task-2"));
913 }
914
915 #[test]
916 fn test_session_uri_parsing() {
917 assert!(matches!(
918 SessionResourceHandler::<MemorySessionStoreAdapter>::parse_uri("session://list"),
919 Some(SessionUriParts::List)
920 ));
921
922 assert!(matches!(
923 SessionResourceHandler::<MemorySessionStoreAdapter>::parse_uri("session://abc"),
924 Some(SessionUriParts::Session(id)) if id == "abc"
925 ));
926
927 assert!(matches!(
928 SessionResourceHandler::<MemorySessionStoreAdapter>::parse_uri("session://abc/messages"),
929 Some(SessionUriParts::Messages(id)) if id == "abc"
930 ));
931
932 assert!(matches!(
933 SessionResourceHandler::<MemorySessionStoreAdapter>::parse_uri("session://abc/turns/1"),
934 Some(SessionUriParts::Turn(id, 1)) if id == "abc"
935 ));
936
937 assert!(
938 SessionResourceHandler::<MemorySessionStoreAdapter>::parse_uri("invalid").is_none()
939 );
940 }
941
942 #[test]
943 fn test_trace_uri_parsing() {
944 assert!(matches!(
945 TraceResourceHandler::<MemoryTraceStoreAdapter>::parse_uri("trace://list"),
946 Some(TraceUriParts::List)
947 ));
948
949 assert!(matches!(
950 TraceResourceHandler::<MemoryTraceStoreAdapter>::parse_uri("trace://task-1"),
951 Some(TraceUriParts::Trace(id)) if id == "task-1"
952 ));
953
954 assert!(matches!(
955 TraceResourceHandler::<MemoryTraceStoreAdapter>::parse_uri("trace://task-1/steps"),
956 Some(TraceUriParts::Steps(id)) if id == "task-1"
957 ));
958
959 assert!(matches!(
960 TraceResourceHandler::<MemoryTraceStoreAdapter>::parse_uri("trace://task-1/summary"),
961 Some(TraceUriParts::Summary(id)) if id == "task-1"
962 ));
963 }
964}