1use chrono::{DateTime, Utc};
45use cortexai_core::errors::CrewError;
46use serde::{Deserialize, Serialize};
47use std::collections::HashMap;
48use std::sync::Arc;
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct HandoffMessage {
53 pub role: MessageRole,
55 pub content: String,
57 pub agent_id: Option<String>,
59 pub timestamp: DateTime<Utc>,
61 pub metadata: HashMap<String, serde_json::Value>,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "lowercase")]
68pub enum MessageRole {
69 System,
70 User,
71 Assistant,
72 Tool,
73}
74
75impl HandoffMessage {
76 pub fn user(content: impl Into<String>) -> Self {
78 Self {
79 role: MessageRole::User,
80 content: content.into(),
81 agent_id: None,
82 timestamp: Utc::now(),
83 metadata: HashMap::new(),
84 }
85 }
86
87 pub fn assistant(agent_id: impl Into<String>, content: impl Into<String>) -> Self {
89 Self {
90 role: MessageRole::Assistant,
91 content: content.into(),
92 agent_id: Some(agent_id.into()),
93 timestamp: Utc::now(),
94 metadata: HashMap::new(),
95 }
96 }
97
98 pub fn system(content: impl Into<String>) -> Self {
100 Self {
101 role: MessageRole::System,
102 content: content.into(),
103 agent_id: None,
104 timestamp: Utc::now(),
105 metadata: HashMap::new(),
106 }
107 }
108
109 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
111 self.metadata.insert(key.into(), value);
112 self
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct HandoffContext {
119 pub conversation_id: String,
121 pub messages: Vec<HandoffMessage>,
123 pub current_agent: String,
125 pub agent_stack: Vec<String>,
127 pub handoff_history: Vec<HandoffRecord>,
129 pub data: HashMap<String, serde_json::Value>,
131 pub created_at: DateTime<Utc>,
133 pub updated_at: DateTime<Utc>,
135}
136
137impl HandoffContext {
138 pub fn new(conversation_id: impl Into<String>, entry_agent: impl Into<String>) -> Self {
140 let entry = entry_agent.into();
141 Self {
142 conversation_id: conversation_id.into(),
143 messages: Vec::new(),
144 current_agent: entry.clone(),
145 agent_stack: vec![entry],
146 handoff_history: Vec::new(),
147 data: HashMap::new(),
148 created_at: Utc::now(),
149 updated_at: Utc::now(),
150 }
151 }
152
153 pub fn add_message(&mut self, message: HandoffMessage) {
155 self.messages.push(message);
156 self.updated_at = Utc::now();
157 }
158
159 pub fn user_message(&mut self, content: impl Into<String>) {
161 self.add_message(HandoffMessage::user(content));
162 }
163
164 pub fn agent_message(&mut self, content: impl Into<String>) {
166 self.add_message(HandoffMessage::assistant(&self.current_agent, content));
167 }
168
169 pub fn last_messages(&self, n: usize) -> &[HandoffMessage] {
171 let start = self.messages.len().saturating_sub(n);
172 &self.messages[start..]
173 }
174
175 pub fn messages_from(&self, agent_id: &str) -> Vec<&HandoffMessage> {
177 self.messages
178 .iter()
179 .filter(|m| m.agent_id.as_deref() == Some(agent_id))
180 .collect()
181 }
182
183 pub fn set_data(&mut self, key: impl Into<String>, value: serde_json::Value) {
185 self.data.insert(key.into(), value);
186 self.updated_at = Utc::now();
187 }
188
189 pub fn get_data<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
191 self.data
192 .get(key)
193 .and_then(|v| serde_json::from_value(v.clone()).ok())
194 }
195
196 pub fn summary(&self) -> String {
198 format!(
199 "Conversation {} with {} messages, current agent: {}, {} handoffs",
200 self.conversation_id,
201 self.messages.len(),
202 self.current_agent,
203 self.handoff_history.len()
204 )
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct HandoffRecord {
211 pub from_agent: String,
213 pub to_agent: String,
215 pub reason: String,
217 pub is_return: bool,
219 pub timestamp: DateTime<Utc>,
221 pub message_index: usize,
223}
224
225#[derive(Clone)]
227pub enum HandoffTrigger {
228 Keyword(String),
230 Keywords(Vec<String>),
232 Pattern(String),
234 Custom(Arc<dyn Fn(&HandoffContext, &str) -> bool + Send + Sync>),
236 Always,
238 AgentDecision,
240}
241
242impl std::fmt::Debug for HandoffTrigger {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match self {
245 Self::Keyword(k) => write!(f, "Keyword({})", k),
246 Self::Keywords(ks) => write!(f, "Keywords({:?})", ks),
247 Self::Pattern(p) => write!(f, "Pattern({})", p),
248 Self::Custom(_) => write!(f, "Custom(<fn>)"),
249 Self::Always => write!(f, "Always"),
250 Self::AgentDecision => write!(f, "AgentDecision"),
251 }
252 }
253}
254
255impl HandoffTrigger {
256 pub fn keyword(keyword: impl Into<String>) -> Self {
258 Self::Keyword(keyword.into().to_lowercase())
259 }
260
261 pub fn keywords(keywords: Vec<String>) -> Self {
263 Self::Keywords(keywords.into_iter().map(|k| k.to_lowercase()).collect())
264 }
265
266 pub fn pattern(pattern: impl Into<String>) -> Self {
268 Self::Pattern(pattern.into())
269 }
270
271 pub fn custom<F>(f: F) -> Self
273 where
274 F: Fn(&HandoffContext, &str) -> bool + Send + Sync + 'static,
275 {
276 Self::Custom(Arc::new(f))
277 }
278
279 pub fn matches(&self, context: &HandoffContext, message: &str) -> bool {
281 let lower = message.to_lowercase();
282 match self {
283 Self::Keyword(k) => lower.contains(k),
284 Self::Keywords(ks) => ks.iter().any(|k| lower.contains(k)),
285 Self::Pattern(p) => regex::Regex::new(p)
286 .map(|re| re.is_match(&lower))
287 .unwrap_or(false),
288 Self::Custom(f) => f(context, message),
289 Self::Always => true,
290 Self::AgentDecision => false, }
292 }
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct HandoffInstruction {
298 pub target_agent: String,
300 pub reason: String,
302 pub should_return: bool,
304 pub data: HashMap<String, serde_json::Value>,
306}
307
308impl HandoffInstruction {
309 pub fn to(agent: impl Into<String>) -> Self {
311 Self {
312 target_agent: agent.into(),
313 reason: String::new(),
314 should_return: false,
315 data: HashMap::new(),
316 }
317 }
318
319 pub fn because(mut self, reason: impl Into<String>) -> Self {
321 self.reason = reason.into();
322 self
323 }
324
325 pub fn and_return(mut self) -> Self {
327 self.should_return = true;
328 self
329 }
330
331 pub fn with_data(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
333 self.data.insert(key.into(), value);
334 self
335 }
336}
337
338#[derive(Debug, Clone)]
340pub enum AgentResponse {
341 Message(String),
343 Handoff(HandoffInstruction),
345 Return(String),
347 End(String),
349}
350
351pub type AgentExecutor = Arc<
353 dyn Fn(HandoffContext) -> futures::future::BoxFuture<'static, Result<AgentResponse, CrewError>>
354 + Send
355 + Sync,
356>;
357
358pub struct AgentNode {
360 pub id: String,
362 pub description: String,
364 executor: AgentExecutor,
366 pub system_prompt: Option<String>,
368 pub can_return: bool,
370 pub can_handoff: bool,
372 pub allowed_targets: Vec<String>,
374}
375
376impl std::fmt::Debug for AgentNode {
377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 f.debug_struct("AgentNode")
379 .field("id", &self.id)
380 .field("description", &self.description)
381 .field("can_return", &self.can_return)
382 .field("can_handoff", &self.can_handoff)
383 .finish()
384 }
385}
386
387impl AgentNode {
388 pub fn new<F, Fut>(id: impl Into<String>, executor: F) -> Self
390 where
391 F: Fn(HandoffContext) -> Fut + Send + Sync + 'static,
392 Fut: std::future::Future<Output = Result<AgentResponse, CrewError>> + Send + 'static,
393 {
394 Self {
395 id: id.into(),
396 description: String::new(),
397 executor: Arc::new(move |ctx| Box::pin(executor(ctx))),
398 system_prompt: None,
399 can_return: false,
400 can_handoff: true,
401 allowed_targets: Vec::new(),
402 }
403 }
404
405 pub fn description(mut self, desc: impl Into<String>) -> Self {
407 self.description = desc.into();
408 self
409 }
410
411 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
413 self.system_prompt = Some(prompt.into());
414 self
415 }
416
417 pub fn can_return(mut self) -> Self {
419 self.can_return = true;
420 self
421 }
422
423 pub fn no_handoff(mut self) -> Self {
425 self.can_handoff = false;
426 self
427 }
428
429 pub fn allowed_targets(mut self, targets: Vec<String>) -> Self {
431 self.allowed_targets = targets;
432 self
433 }
434
435 pub async fn execute(&self, context: HandoffContext) -> Result<AgentResponse, CrewError> {
437 (self.executor)(context).await
438 }
439}
440
441#[derive(Debug)]
443pub struct HandoffRule {
444 pub from: String,
446 pub to: String,
448 pub trigger: HandoffTrigger,
450 pub priority: i32,
452}
453
454pub struct HandoffRouter {
456 agents: HashMap<String, AgentNode>,
458 rules: Vec<HandoffRule>,
460 entry_agent: Option<String>,
462 max_handoffs: u32,
464 max_turns: u32,
466}
467
468impl Default for HandoffRouter {
469 fn default() -> Self {
470 Self::new()
471 }
472}
473
474impl HandoffRouter {
475 pub fn new() -> Self {
477 Self {
478 agents: HashMap::new(),
479 rules: Vec::new(),
480 entry_agent: None,
481 max_handoffs: 10,
482 max_turns: 50,
483 }
484 }
485
486 pub fn add_agent(mut self, agent: AgentNode) -> Self {
488 self.agents.insert(agent.id.clone(), agent);
489 self
490 }
491
492 pub fn add_handoff(
494 mut self,
495 from: impl Into<String>,
496 to: impl Into<String>,
497 trigger: HandoffTrigger,
498 ) -> Self {
499 self.rules.push(HandoffRule {
500 from: from.into(),
501 to: to.into(),
502 trigger,
503 priority: 0,
504 });
505 self
506 }
507
508 pub fn add_handoff_priority(
510 mut self,
511 from: impl Into<String>,
512 to: impl Into<String>,
513 trigger: HandoffTrigger,
514 priority: i32,
515 ) -> Self {
516 self.rules.push(HandoffRule {
517 from: from.into(),
518 to: to.into(),
519 trigger,
520 priority,
521 });
522 self
523 }
524
525 pub fn set_entry(mut self, agent_id: impl Into<String>) -> Self {
527 self.entry_agent = Some(agent_id.into());
528 self
529 }
530
531 pub fn max_handoffs(mut self, max: u32) -> Self {
533 self.max_handoffs = max;
534 self
535 }
536
537 pub fn max_turns(mut self, max: u32) -> Self {
539 self.max_turns = max;
540 self
541 }
542
543 pub fn get_agent(&self, id: &str) -> Option<&AgentNode> {
545 self.agents.get(id)
546 }
547
548 pub fn list_agents(&self) -> Vec<&AgentNode> {
550 self.agents.values().collect()
551 }
552
553 fn find_matching_rules(&self, context: &HandoffContext, message: &str) -> Vec<&HandoffRule> {
555 let mut matches: Vec<_> = self
556 .rules
557 .iter()
558 .filter(|r| r.from == context.current_agent && r.trigger.matches(context, message))
559 .collect();
560
561 matches.sort_by(|a, b| b.priority.cmp(&a.priority));
563 matches
564 }
565
566 fn execute_handoff(
568 &self,
569 context: &mut HandoffContext,
570 to: &str,
571 reason: &str,
572 is_return: bool,
573 ) {
574 let record = HandoffRecord {
575 from_agent: context.current_agent.clone(),
576 to_agent: to.to_string(),
577 reason: reason.to_string(),
578 is_return,
579 timestamp: Utc::now(),
580 message_index: context.messages.len(),
581 };
582
583 context.handoff_history.push(record);
584
585 if !is_return {
586 context.agent_stack.push(to.to_string());
587 } else {
588 context.agent_stack.pop();
589 }
590
591 context.current_agent = to.to_string();
592 context.updated_at = Utc::now();
593 }
594
595 pub async fn run(&self, mut context: HandoffContext) -> Result<HandoffResult, CrewError> {
597 let entry = self.entry_agent.as_ref().ok_or_else(|| {
598 CrewError::InvalidConfiguration("No entry agent specified".to_string())
599 })?;
600
601 context.current_agent = entry.clone();
602 if context.agent_stack.is_empty() {
603 context.agent_stack.push(entry.clone());
604 }
605
606 let mut turns = 0;
607 let mut handoffs = 0;
608
609 loop {
610 if turns >= self.max_turns {
612 return Ok(HandoffResult {
613 context,
614 status: HandoffStatus::MaxTurnsReached,
615 final_message: None,
616 });
617 }
618
619 if handoffs >= self.max_handoffs {
620 return Ok(HandoffResult {
621 context,
622 status: HandoffStatus::MaxHandoffsReached,
623 final_message: None,
624 });
625 }
626
627 let agent = self.agents.get(&context.current_agent).ok_or_else(|| {
629 CrewError::TaskNotFound(format!("Agent '{}' not found", context.current_agent))
630 })?;
631
632 let response = agent.execute(context.clone()).await?;
634 turns += 1;
635
636 match response {
637 AgentResponse::Message(msg) => {
638 context.agent_message(&msg);
639
640 let rules = self.find_matching_rules(&context, &msg);
642 if let Some(rule) = rules.first() {
643 self.execute_handoff(
644 &mut context,
645 &rule.to,
646 &format!("Triggered by rule: {:?}", rule.trigger),
647 false,
648 );
649 handoffs += 1;
650 }
651 }
653
654 AgentResponse::Handoff(instruction) => {
655 if !agent.can_handoff {
657 return Err(CrewError::ExecutionFailed(format!(
658 "Agent '{}' is not allowed to hand off",
659 context.current_agent
660 )));
661 }
662
663 if !agent.allowed_targets.is_empty()
664 && !agent.allowed_targets.contains(&instruction.target_agent)
665 {
666 return Err(CrewError::ExecutionFailed(format!(
667 "Agent '{}' cannot hand off to '{}'",
668 context.current_agent, instruction.target_agent
669 )));
670 }
671
672 if !self.agents.contains_key(&instruction.target_agent) {
673 return Err(CrewError::TaskNotFound(format!(
674 "Target agent '{}' not found",
675 instruction.target_agent
676 )));
677 }
678
679 for (key, value) in instruction.data {
681 context.set_data(key, value);
682 }
683
684 self.execute_handoff(
685 &mut context,
686 &instruction.target_agent,
687 &instruction.reason,
688 false,
689 );
690 handoffs += 1;
691 }
692
693 AgentResponse::Return(msg) => {
694 if !agent.can_return {
695 return Err(CrewError::ExecutionFailed(format!(
696 "Agent '{}' is not allowed to return",
697 context.current_agent
698 )));
699 }
700
701 context.agent_message(&msg);
702
703 if context.agent_stack.len() > 1 {
705 let prev = context.agent_stack[context.agent_stack.len() - 2].clone();
706 self.execute_handoff(&mut context, &prev, "Return to caller", true);
707 handoffs += 1;
708 } else {
709 return Ok(HandoffResult {
711 context,
712 status: HandoffStatus::Completed,
713 final_message: Some(msg),
714 });
715 }
716 }
717
718 AgentResponse::End(msg) => {
719 context.agent_message(&msg);
720 return Ok(HandoffResult {
721 context,
722 status: HandoffStatus::Completed,
723 final_message: Some(msg),
724 });
725 }
726 }
727 }
728 }
729
730 pub async fn start(&self, user_message: impl Into<String>) -> Result<HandoffResult, CrewError> {
732 let mut context = HandoffContext::new(
733 uuid::Uuid::new_v4().to_string(),
734 self.entry_agent.as_deref().unwrap_or("default"),
735 );
736 context.user_message(user_message);
737 self.run(context).await
738 }
739}
740
741#[derive(Debug, Clone)]
743pub struct HandoffResult {
744 pub context: HandoffContext,
746 pub status: HandoffStatus,
748 pub final_message: Option<String>,
750}
751
752#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
754pub enum HandoffStatus {
755 Completed,
757 MaxTurnsReached,
759 MaxHandoffsReached,
761 Interrupted,
763}
764
765impl HandoffResult {
766 pub fn summary(&self) -> String {
768 format!(
769 "Status: {:?}, Agents: {:?}, Messages: {}, Handoffs: {}",
770 self.status,
771 self.context.agent_stack,
772 self.context.messages.len(),
773 self.context.handoff_history.len()
774 )
775 }
776
777 pub fn handoffs(&self) -> &[HandoffRecord] {
779 &self.context.handoff_history
780 }
781
782 pub fn messages_from(&self, agent_id: &str) -> Vec<&HandoffMessage> {
784 self.context.messages_from(agent_id)
785 }
786}
787
788pub struct ParallelHandoff {
790 targets: Vec<String>,
792 #[allow(dead_code)]
794 mappings: HashMap<String, HashMap<String, String>>,
795}
796
797impl ParallelHandoff {
798 pub fn new() -> Self {
800 Self {
801 targets: Vec::new(),
802 mappings: HashMap::new(),
803 }
804 }
805
806 pub fn to(mut self, agent: impl Into<String>) -> Self {
808 self.targets.push(agent.into());
809 self
810 }
811
812 pub fn to_all(mut self, agents: Vec<String>) -> Self {
814 self.targets.extend(agents);
815 self
816 }
817}
818
819impl Default for ParallelHandoff {
820 fn default() -> Self {
821 Self::new()
822 }
823}
824
825pub struct HandoffChain {
827 agents: Vec<String>,
829 return_to_origin: bool,
831}
832
833impl HandoffChain {
834 pub fn new() -> Self {
836 Self {
837 agents: Vec::new(),
838 return_to_origin: false,
839 }
840 }
841
842 pub fn then(mut self, agent: impl Into<String>) -> Self {
844 self.agents.push(agent.into());
845 self
846 }
847
848 pub fn and_return(mut self) -> Self {
850 self.return_to_origin = true;
851 self
852 }
853}
854
855impl Default for HandoffChain {
856 fn default() -> Self {
857 Self::new()
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864
865 #[allow(dead_code)]
866 fn create_test_agent(id: &str, response: &'static str) -> AgentNode {
867 AgentNode::new(id, move |_ctx| async move {
868 Ok(AgentResponse::Message(response.to_string()))
869 })
870 .description(format!("Test agent {}", id))
871 }
872
873 fn create_handoff_agent(id: &str, target: &'static str) -> AgentNode {
874 AgentNode::new(id, move |_ctx| async move {
875 Ok(AgentResponse::Handoff(
876 HandoffInstruction::to(target).because("Need specialist"),
877 ))
878 })
879 }
880
881 fn create_ending_agent(id: &str, response: &'static str) -> AgentNode {
882 AgentNode::new(id, move |_ctx| async move {
883 Ok(AgentResponse::End(response.to_string()))
884 })
885 }
886
887 #[allow(dead_code)]
888 fn create_returning_agent(id: &str, response: &'static str) -> AgentNode {
889 AgentNode::new(id, move |_ctx| async move {
890 Ok(AgentResponse::Return(response.to_string()))
891 })
892 .can_return()
893 }
894
895 #[tokio::test]
896 async fn test_simple_conversation() {
897 let router = HandoffRouter::new()
898 .add_agent(create_ending_agent("greeter", "Hello! How can I help?"))
899 .set_entry("greeter");
900
901 let result = router.start("Hi there").await.unwrap();
902
903 assert_eq!(result.status, HandoffStatus::Completed);
904 assert_eq!(
905 result.final_message.as_deref(),
906 Some("Hello! How can I help?")
907 );
908 assert_eq!(result.context.messages.len(), 2); }
910
911 #[tokio::test]
912 async fn test_agent_handoff() {
913 let router = HandoffRouter::new()
914 .add_agent(create_handoff_agent("triage", "sales"))
915 .add_agent(create_ending_agent(
916 "sales",
917 "I can help with your purchase!",
918 ))
919 .set_entry("triage");
920
921 let result = router.start("I want to buy something").await.unwrap();
922
923 assert_eq!(result.status, HandoffStatus::Completed);
924 assert_eq!(result.context.handoff_history.len(), 1);
925 assert_eq!(result.context.handoff_history[0].from_agent, "triage");
926 assert_eq!(result.context.handoff_history[0].to_agent, "sales");
927 }
928
929 #[tokio::test]
930 async fn test_trigger_based_handoff() {
931 let triage = AgentNode::new("triage", |_ctx| async move {
933 Ok(AgentResponse::Message(
934 "I understand you want to buy something. Let me help with that.".to_string(),
935 ))
936 });
937
938 let router = HandoffRouter::new()
939 .add_agent(triage)
940 .add_agent(create_ending_agent("sales", "Sales here!"))
941 .add_handoff("triage", "sales", HandoffTrigger::keyword("buy"))
942 .set_entry("triage");
943
944 let mut context = HandoffContext::new("test", "triage");
946 context.user_message("I want to purchase something");
947
948 let result = router.run(context).await.unwrap();
949
950 assert_eq!(result.context.handoff_history.len(), 1);
951 assert_eq!(result.context.handoff_history[0].to_agent, "sales");
952 }
953
954 #[tokio::test]
955 async fn test_return_handoff() {
956 let triage = AgentNode::new("triage", |ctx| async move {
957 if ctx.handoff_history.is_empty() {
960 Ok(AgentResponse::Handoff(
961 HandoffInstruction::to("specialist").because("Need expert"),
962 ))
963 } else {
964 Ok(AgentResponse::End("Thanks for your patience!".to_string()))
965 }
966 });
967
968 let specialist = AgentNode::new("specialist", |_ctx| async move {
969 Ok(AgentResponse::Return("Here's my analysis".to_string()))
970 })
971 .can_return();
972
973 let router = HandoffRouter::new()
974 .add_agent(triage)
975 .add_agent(specialist)
976 .set_entry("triage");
977
978 let result = router.start("Need help").await.unwrap();
979
980 assert_eq!(result.status, HandoffStatus::Completed);
981 assert_eq!(result.context.handoff_history.len(), 2); assert!(result.context.handoff_history[1].is_return);
983 }
984
985 #[tokio::test]
986 async fn test_max_handoffs_limit() {
987 let agent_a = create_handoff_agent("a", "b");
989 let agent_b = create_handoff_agent("b", "a");
990
991 let router = HandoffRouter::new()
992 .add_agent(agent_a)
993 .add_agent(agent_b)
994 .set_entry("a")
995 .max_handoffs(5);
996
997 let result = router.start("Start").await.unwrap();
998
999 assert_eq!(result.status, HandoffStatus::MaxHandoffsReached);
1000 assert!(result.context.handoff_history.len() <= 5);
1001 }
1002
1003 #[tokio::test]
1004 async fn test_handoff_context_data() {
1005 let mut context = HandoffContext::new("test-123", "agent1");
1006
1007 context.set_data("user_id", serde_json::json!("user-456"));
1008 context.set_data("priority", serde_json::json!(5));
1009
1010 assert_eq!(
1011 context.get_data::<String>("user_id"),
1012 Some("user-456".to_string())
1013 );
1014 assert_eq!(context.get_data::<i32>("priority"), Some(5));
1015 }
1016
1017 #[tokio::test]
1018 async fn test_handoff_trigger_keywords() {
1019 let context = HandoffContext::new("test", "agent");
1020
1021 let trigger = HandoffTrigger::keywords(vec!["buy".to_string(), "purchase".to_string()]);
1022
1023 assert!(trigger.matches(&context, "I want to BUY something"));
1024 assert!(trigger.matches(&context, "Can I purchase this?"));
1025 assert!(!trigger.matches(&context, "Just browsing"));
1026 }
1027
1028 #[tokio::test]
1029 async fn test_handoff_trigger_pattern() {
1030 let context = HandoffContext::new("test", "agent");
1031
1032 let trigger = HandoffTrigger::pattern(r"order\s*#?\d+");
1033
1034 assert!(trigger.matches(&context, "Check order #12345"));
1035 assert!(trigger.matches(&context, "order 67890 status"));
1036 assert!(!trigger.matches(&context, "I want to order"));
1037 }
1038
1039 #[tokio::test]
1040 async fn test_handoff_instruction_builder() {
1041 let instruction = HandoffInstruction::to("support")
1042 .because("Technical issue")
1043 .and_return()
1044 .with_data("ticket_id", serde_json::json!("TKT-123"));
1045
1046 assert_eq!(instruction.target_agent, "support");
1047 assert_eq!(instruction.reason, "Technical issue");
1048 assert!(instruction.should_return);
1049 assert_eq!(
1050 instruction.data.get("ticket_id"),
1051 Some(&serde_json::json!("TKT-123"))
1052 );
1053 }
1054
1055 #[tokio::test]
1056 async fn test_handoff_history_tracking() {
1057 let router = HandoffRouter::new()
1058 .add_agent(create_handoff_agent("a", "b"))
1059 .add_agent(create_handoff_agent("b", "c"))
1060 .add_agent(create_ending_agent("c", "Done!"))
1061 .set_entry("a");
1062
1063 let result = router.start("Go").await.unwrap();
1064
1065 assert_eq!(result.context.handoff_history.len(), 2);
1066 assert_eq!(result.context.handoff_history[0].from_agent, "a");
1067 assert_eq!(result.context.handoff_history[0].to_agent, "b");
1068 assert_eq!(result.context.handoff_history[1].from_agent, "b");
1069 assert_eq!(result.context.handoff_history[1].to_agent, "c");
1070 }
1071
1072 #[tokio::test]
1073 async fn test_message_history() {
1074 let router = HandoffRouter::new()
1075 .add_agent(create_ending_agent("agent", "Response"))
1076 .set_entry("agent");
1077
1078 let result = router.start("User message").await.unwrap();
1079
1080 assert_eq!(result.context.messages.len(), 2);
1081 assert_eq!(result.context.messages[0].role, MessageRole::User);
1082 assert_eq!(result.context.messages[0].content, "User message");
1083 assert_eq!(result.context.messages[1].role, MessageRole::Assistant);
1084 assert_eq!(
1085 result.context.messages[1].agent_id,
1086 Some("agent".to_string())
1087 );
1088 }
1089}