1use std::{
12 collections::{HashMap, HashSet},
13 fmt::{Display, Formatter, Write},
14};
15
16use async_trait::async_trait;
17use chrono::{DateTime, Utc};
18use rand::RngCore;
19use serde::{Deserialize, Serialize};
20use serde_json::{Map as JsonMap, Value};
21
22fn random_hex_id() -> String {
29 let mut rng = rand::rng();
30 let mut bytes = [0u8; 25];
31 rng.fill_bytes(&mut bytes);
32 let mut hex_string = String::with_capacity(50);
33 for b in &bytes {
34 let _ = write!(hex_string, "{b:02x}");
36 }
37 hex_string
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
45pub struct ConversationId(pub String);
46
47impl ConversationId {
48 pub fn new() -> Self {
49 Self(format!("conv_{}", random_hex_id()))
50 }
51}
52
53impl Default for ConversationId {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl From<String> for ConversationId {
60 fn from(value: String) -> Self {
61 Self(value)
62 }
63}
64
65impl From<&str> for ConversationId {
66 fn from(value: &str) -> Self {
67 Self(value.to_string())
68 }
69}
70
71impl Display for ConversationId {
72 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73 f.write_str(&self.0)
74 }
75}
76
77pub type ConversationMetadata = JsonMap<String, Value>;
79
80#[derive(Debug, Clone, Serialize, Deserialize, Default)]
82pub struct NewConversation {
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub id: Option<ConversationId>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub metadata: Option<ConversationMetadata>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct Conversation {
93 pub id: ConversationId,
94 pub created_at: DateTime<Utc>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub metadata: Option<ConversationMetadata>,
97}
98
99impl Conversation {
100 pub fn new(new_conversation: NewConversation) -> Self {
101 Self {
102 id: new_conversation.id.unwrap_or_default(),
103 created_at: Utc::now(),
104 metadata: new_conversation.metadata,
105 }
106 }
107
108 pub fn with_parts(
109 id: ConversationId,
110 created_at: DateTime<Utc>,
111 metadata: Option<ConversationMetadata>,
112 ) -> Self {
113 Self {
114 id,
115 created_at,
116 metadata,
117 }
118 }
119}
120
121pub type ConversationResult<T> = Result<T, ConversationStorageError>;
123
124#[derive(Debug, thiserror::Error)]
126pub enum ConversationStorageError {
127 #[error("Conversation not found: {0}")]
128 ConversationNotFound(String),
129
130 #[error("Storage error: {0}")]
131 StorageError(String),
132
133 #[error("Serialization error: {0}")]
134 SerializationError(#[from] serde_json::Error),
135}
136
137#[async_trait]
139pub trait ConversationStorage: Send + Sync + 'static {
140 async fn create_conversation(&self, input: NewConversation)
141 -> ConversationResult<Conversation>;
142
143 async fn get_conversation(
144 &self,
145 id: &ConversationId,
146 ) -> ConversationResult<Option<Conversation>>;
147
148 async fn update_conversation(
149 &self,
150 id: &ConversationId,
151 metadata: Option<ConversationMetadata>,
152 ) -> ConversationResult<Option<Conversation>>;
153
154 async fn delete_conversation(&self, id: &ConversationId) -> ConversationResult<bool>;
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
162pub struct ConversationItemId(pub String);
163
164impl Display for ConversationItemId {
165 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
166 f.write_str(&self.0)
167 }
168}
169
170impl From<String> for ConversationItemId {
171 fn from(value: String) -> Self {
172 Self(value)
173 }
174}
175
176impl From<&str> for ConversationItemId {
177 fn from(value: &str) -> Self {
178 Self(value.to_string())
179 }
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ConversationItem {
184 pub id: ConversationItemId,
185 pub response_id: Option<String>,
186 pub item_type: String,
187 pub role: Option<String>,
188 pub content: Value,
189 pub status: Option<String>,
190 pub created_at: DateTime<Utc>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct NewConversationItem {
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub id: Option<ConversationItemId>,
197 pub response_id: Option<String>,
198 pub item_type: String,
199 pub role: Option<String>,
200 pub content: Value,
201 pub status: Option<String>,
202}
203
204#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
205pub enum SortOrder {
206 Asc,
207 Desc,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct ListParams {
212 pub limit: usize,
213 pub order: SortOrder,
214 pub after: Option<String>, }
216
217pub type ConversationItemResult<T> = Result<T, ConversationItemStorageError>;
218
219#[derive(Debug, thiserror::Error)]
220pub enum ConversationItemStorageError {
221 #[error("Not found: {0}")]
222 NotFound(String),
223
224 #[error("Storage error: {0}")]
225 StorageError(String),
226
227 #[error("Serialization error: {0}")]
228 SerializationError(#[from] serde_json::Error),
229}
230
231#[async_trait]
232pub trait ConversationItemStorage: Send + Sync + 'static {
233 async fn create_item(
234 &self,
235 item: NewConversationItem,
236 ) -> ConversationItemResult<ConversationItem>;
237
238 async fn link_item(
239 &self,
240 conversation_id: &ConversationId,
241 item_id: &ConversationItemId,
242 added_at: DateTime<Utc>,
243 ) -> ConversationItemResult<()>;
244
245 async fn link_items(
249 &self,
250 conversation_id: &ConversationId,
251 items: &[(ConversationItemId, DateTime<Utc>)],
252 ) -> ConversationItemResult<()> {
253 for (item_id, added_at) in items {
254 self.link_item(conversation_id, item_id, *added_at).await?;
255 }
256 Ok(())
257 }
258
259 async fn list_items(
260 &self,
261 conversation_id: &ConversationId,
262 params: ListParams,
263 ) -> ConversationItemResult<Vec<ConversationItem>>;
264
265 async fn get_item(
267 &self,
268 item_id: &ConversationItemId,
269 ) -> ConversationItemResult<Option<ConversationItem>>;
270
271 async fn is_item_linked(
273 &self,
274 conversation_id: &ConversationId,
275 item_id: &ConversationItemId,
276 ) -> ConversationItemResult<bool>;
277
278 async fn delete_item(
280 &self,
281 conversation_id: &ConversationId,
282 item_id: &ConversationItemId,
283 ) -> ConversationItemResult<()>;
284}
285
286pub fn make_item_id(item_type: &str) -> ConversationItemId {
288 let hex_string = random_hex_id();
289
290 let prefix = match item_type {
291 "message" => "msg",
292 "reasoning" => "rs",
293 "mcp_call" => "mcp",
294 "mcp_list_tools" => "mcpl",
295 "function_call" => "fc",
296 other => {
297 let fallback: String = other.chars().take(3).collect();
299 if fallback.is_empty() {
300 return ConversationItemId(format!("itm_{hex_string}"));
301 }
302 return ConversationItemId(format!("{fallback}_{hex_string}"));
303 }
304 };
305 ConversationItemId(format!("{prefix}_{hex_string}"))
306}
307
308#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
314pub struct ResponseId(pub String);
315
316impl ResponseId {
317 pub fn new() -> Self {
318 Self(ulid::Ulid::new().to_string())
319 }
320}
321
322impl Default for ResponseId {
323 fn default() -> Self {
324 Self::new()
325 }
326}
327
328impl From<String> for ResponseId {
329 fn from(value: String) -> Self {
330 Self(value)
331 }
332}
333
334impl From<&str> for ResponseId {
335 fn from(value: &str) -> Self {
336 Self(value.to_string())
337 }
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct StoredResponse {
343 pub id: ResponseId,
345
346 pub previous_response_id: Option<ResponseId>,
348
349 pub input: Value,
351
352 pub created_at: DateTime<Utc>,
354
355 pub safety_identifier: Option<String>,
357
358 pub model: Option<String>,
360
361 #[serde(default)]
363 pub conversation_id: Option<String>,
364
365 #[serde(default)]
367 pub raw_response: Value,
368}
369
370impl StoredResponse {
371 pub fn new(previous_response_id: Option<ResponseId>) -> Self {
372 Self {
373 id: ResponseId::new(),
374 previous_response_id,
375 input: Value::Array(vec![]),
376 created_at: Utc::now(),
377 safety_identifier: None,
378 model: None,
379 conversation_id: None,
380 raw_response: Value::Null,
381 }
382 }
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct ResponseChain {
388 pub responses: Vec<StoredResponse>,
390
391 pub metadata: HashMap<String, Value>,
393}
394
395impl Default for ResponseChain {
396 fn default() -> Self {
397 Self::new()
398 }
399}
400
401impl ResponseChain {
402 pub fn new() -> Self {
403 Self {
404 responses: Vec::new(),
405 metadata: HashMap::new(),
406 }
407 }
408
409 pub fn latest_response_id(&self) -> Option<&ResponseId> {
411 self.responses.last().map(|r| &r.id)
412 }
413
414 pub fn add_response(&mut self, response: StoredResponse) {
416 self.responses.push(response);
417 }
418
419 pub fn build_context(&self, max_responses: Option<usize>) -> Vec<(Value, Value)> {
421 let responses = if let Some(max) = max_responses {
422 let start = self.responses.len().saturating_sub(max);
423 &self.responses[start..]
424 } else {
425 &self.responses[..]
426 };
427
428 responses
429 .iter()
430 .map(|r| {
431 let output = r
432 .raw_response
433 .get("output")
434 .cloned()
435 .unwrap_or(Value::Array(vec![]));
436 (r.input.clone(), output)
437 })
438 .collect()
439 }
440}
441
442#[derive(Debug, thiserror::Error)]
444pub enum ResponseStorageError {
445 #[error("Response not found: {0}")]
446 ResponseNotFound(String),
447
448 #[error("Invalid chain: {0}")]
449 InvalidChain(String),
450
451 #[error("Storage error: {0}")]
452 StorageError(String),
453
454 #[error("Serialization error: {0}")]
455 SerializationError(#[from] serde_json::Error),
456}
457
458pub type ResponseResult<T> = Result<T, ResponseStorageError>;
459
460#[async_trait]
462pub trait ResponseStorage: Send + Sync {
463 async fn store_response(&self, response: StoredResponse) -> ResponseResult<ResponseId>;
465
466 async fn get_response(
468 &self,
469 response_id: &ResponseId,
470 ) -> ResponseResult<Option<StoredResponse>>;
471
472 async fn delete_response(&self, response_id: &ResponseId) -> ResponseResult<()>;
474
475 async fn get_response_chain(
486 &self,
487 response_id: &ResponseId,
488 max_depth: Option<usize>,
489 ) -> ResponseResult<ResponseChain> {
490 let mut chain = ResponseChain::new();
491 let mut current_id = Some(response_id.clone());
492 let mut seen = HashSet::new();
493
494 while let Some(ref lookup_id) = current_id {
495 if let Some(limit) = max_depth {
496 if seen.len() >= limit {
497 break;
498 }
499 }
500
501 if !seen.insert(lookup_id.clone()) {
503 return Err(ResponseStorageError::InvalidChain(format!(
504 "cycle detected at response {}",
505 lookup_id.0
506 )));
507 }
508
509 let fetched = self.get_response(lookup_id).await?;
510 match fetched {
511 Some(response) => {
512 current_id.clone_from(&response.previous_response_id);
513 chain.responses.push(response);
514 }
515 None => break,
516 }
517 }
518
519 chain.responses.reverse();
520 Ok(chain)
521 }
522
523 async fn list_identifier_responses(
525 &self,
526 identifier: &str,
527 limit: Option<usize>,
528 ) -> ResponseResult<Vec<StoredResponse>>;
529
530 async fn delete_identifier_responses(&self, identifier: &str) -> ResponseResult<usize>;
532}
533
534#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
539pub struct ConversationMemoryId(pub String);
540
541impl From<String> for ConversationMemoryId {
542 fn from(value: String) -> Self {
543 Self(value)
544 }
545}
546
547impl From<&str> for ConversationMemoryId {
548 fn from(value: &str) -> Self {
549 Self(value.to_string())
550 }
551}
552
553impl Display for ConversationMemoryId {
554 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
555 f.write_str(&self.0)
556 }
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
560pub enum ConversationMemoryType {
561 #[serde(rename = "ONDEMAND")]
562 OnDemand,
563 #[serde(rename = "LTM")]
564 Ltm,
565 #[serde(rename = "STMO")]
566 Stmo,
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
570pub enum ConversationMemoryStatus {
571 #[serde(rename = "READY")]
572 Ready,
573 #[serde(rename = "RUNNING")]
574 Running,
575 #[serde(rename = "SUCCESS")]
576 Success,
577 #[serde(rename = "FAILED")]
578 Failed,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
589pub struct NewConversationMemory {
590 pub conversation_id: ConversationId,
591 pub conversation_version: Option<i64>,
592 pub response_id: Option<ResponseId>,
593 pub memory_type: ConversationMemoryType,
594 pub status: ConversationMemoryStatus,
595 pub attempt: i64,
596 pub owner_id: Option<String>,
597 pub next_run_at: DateTime<Utc>,
598 pub lease_until: Option<DateTime<Utc>>,
599 pub content: Option<String>,
600 pub memory_config: Option<String>,
601 pub scope_id: Option<String>,
602 pub error_msg: Option<String>,
603}
604
605pub type ConversationMemoryResult<T> = Result<T, ConversationMemoryStorageError>;
606
607#[derive(Debug, thiserror::Error)]
608pub enum ConversationMemoryStorageError {
609 #[error("Storage error: {0}")]
610 StorageError(String),
611
612 #[error("Serialization error: {0}")]
613 SerializationError(#[from] serde_json::Error),
614}
615
616#[async_trait]
617pub trait ConversationMemoryWriter: Send + Sync + 'static {
618 async fn create_memory(
619 &self,
620 input: NewConversationMemory,
621 ) -> ConversationMemoryResult<ConversationMemoryId>;
622}
623
624impl Default for StoredResponse {
625 fn default() -> Self {
626 Self::new(None)
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use std::collections::HashSet;
633
634 use super::*;
635
636 #[test]
641 fn conversation_id_new_has_conv_prefix() {
642 let id = ConversationId::new();
643 assert!(
644 id.0.starts_with("conv_"),
645 "ConversationId should start with 'conv_', got: {id}"
646 );
647 }
648
649 #[test]
650 fn conversation_id_new_generates_unique_ids() {
651 let ids: HashSet<String> = (0..100).map(|_| ConversationId::new().0).collect();
652 assert_eq!(ids.len(), 100, "all 100 ConversationIds should be unique");
653 }
654
655 #[test]
656 fn conversation_id_new_has_consistent_length() {
657 for _ in 0..10 {
659 let id = ConversationId::new();
660 assert_eq!(
661 id.0.len(),
662 55,
663 "ConversationId should be 55 chars (conv_ + 50 hex), got {} chars: {id}",
664 id.0.len()
665 );
666 }
667 }
668
669 #[test]
670 fn conversation_id_default_works_same_as_new() {
671 let id = ConversationId::default();
672 assert!(
673 id.0.starts_with("conv_"),
674 "Default ConversationId should start with 'conv_', got: {id}"
675 );
676 assert_eq!(id.0.len(), 55, "Default ConversationId should be 55 chars");
677 }
678
679 #[test]
680 fn conversation_id_from_string() {
681 let id = ConversationId::from("my_custom_id".to_string());
682 assert_eq!(id.0, "my_custom_id");
683 }
684
685 #[test]
686 fn conversation_id_from_str() {
687 let id = ConversationId::from("my_custom_id");
688 assert_eq!(id.0, "my_custom_id");
689 }
690
691 #[test]
692 fn conversation_id_display() {
693 let id = ConversationId::from("conv_abc123");
694 assert_eq!(format!("{id}"), "conv_abc123");
695 }
696
697 #[test]
702 fn conversation_item_id_from_string() {
703 let id = ConversationItemId::from("item_123".to_string());
704 assert_eq!(id.0, "item_123");
705 }
706
707 #[test]
708 fn conversation_item_id_from_str() {
709 let id = ConversationItemId::from("item_456");
710 assert_eq!(id.0, "item_456");
711 }
712
713 #[test]
714 fn conversation_item_id_display() {
715 let id = ConversationItemId::from("msg_abc");
716 assert_eq!(format!("{id}"), "msg_abc");
717 }
718
719 #[test]
724 fn response_id_new_generates_valid_ulid() {
725 let id = ResponseId::new();
726 assert_eq!(
728 id.0.len(),
729 26,
730 "ULID string should be 26 chars, got {} chars: {}",
731 id.0.len(),
732 id.0
733 );
734 assert!(
735 id.0.chars().all(|c| c.is_ascii_alphanumeric()),
736 "ULID should contain only alphanumeric characters, got: {}",
737 id.0
738 );
739 }
740
741 #[test]
742 fn response_id_new_generates_unique_ids() {
743 let ids: HashSet<String> = (0..100).map(|_| ResponseId::new().0).collect();
744 assert_eq!(ids.len(), 100, "all 100 ResponseIds should be unique");
745 }
746
747 #[test]
748 fn response_id_default_works_same_as_new() {
749 let id = ResponseId::default();
750 assert_eq!(id.0.len(), 26, "Default ResponseId should be 26-char ULID");
751 }
752
753 #[test]
754 fn response_id_from_string() {
755 let id = ResponseId::from("resp_custom".to_string());
756 assert_eq!(id.0, "resp_custom");
757 }
758
759 #[test]
760 fn response_id_from_str() {
761 let id = ResponseId::from("resp_custom");
762 assert_eq!(id.0, "resp_custom");
763 }
764
765 #[test]
770 fn make_item_id_message_prefix() {
771 let id = make_item_id("message");
772 assert!(
773 id.0.starts_with("msg_"),
774 "message type should produce 'msg_' prefix, got: {id}"
775 );
776 }
777
778 #[test]
779 fn make_item_id_reasoning_prefix() {
780 let id = make_item_id("reasoning");
781 assert!(
782 id.0.starts_with("rs_"),
783 "reasoning type should produce 'rs_' prefix, got: {id}"
784 );
785 }
786
787 #[test]
788 fn make_item_id_mcp_call_prefix() {
789 let id = make_item_id("mcp_call");
790 assert!(
791 id.0.starts_with("mcp_"),
792 "mcp_call type should produce 'mcp_' prefix, got: {id}"
793 );
794 }
795
796 #[test]
797 fn make_item_id_mcp_list_tools_prefix() {
798 let id = make_item_id("mcp_list_tools");
799 assert!(
800 id.0.starts_with("mcpl_"),
801 "mcp_list_tools type should produce 'mcpl_' prefix, got: {id}"
802 );
803 }
804
805 #[test]
806 fn make_item_id_function_call_prefix() {
807 let id = make_item_id("function_call");
808 assert!(
809 id.0.starts_with("fc_"),
810 "function_call type should produce 'fc_' prefix, got: {id}"
811 );
812 }
813
814 #[test]
815 fn make_item_id_unknown_type_uses_first_3_chars() {
816 let id = make_item_id("custom_type");
817 assert!(
818 id.0.starts_with("cus_"),
819 "unknown type 'custom_type' should produce 'cus_' prefix, got: {id}"
820 );
821 }
822
823 #[test]
824 fn make_item_id_empty_type_uses_itm() {
825 let id = make_item_id("");
826 assert!(
827 id.0.starts_with("itm_"),
828 "empty type string should produce 'itm_' prefix, got: {id}"
829 );
830 }
831
832 #[test]
833 fn make_item_id_correct_length() {
834 let test_cases = vec![
836 ("message", "msg_"),
837 ("reasoning", "rs_"),
838 ("mcp_call", "mcp_"),
839 ("mcp_list_tools", "mcpl_"),
840 ("function_call", "fc_"),
841 ];
842
843 for (item_type, prefix) in test_cases {
844 let id = make_item_id(item_type);
845 let expected_len = prefix.len() + 50;
846 assert_eq!(
847 id.0.len(),
848 expected_len,
849 "make_item_id(\"{item_type}\") should be {expected_len} chars ('{prefix}' + 50 hex), got {} chars: {id}",
850 id.0.len()
851 );
852 }
853
854 let id = make_item_id("custom_type");
856 assert_eq!(
857 id.0.len(),
858 54,
859 "unknown type should be 54 chars (3 char prefix + '_' + 50 hex), got {} chars: {id}",
860 id.0.len()
861 );
862
863 let id = make_item_id("");
865 assert_eq!(
866 id.0.len(),
867 54,
868 "empty type should be 54 chars ('itm_' + 50 hex), got {} chars: {id}",
869 id.0.len()
870 );
871 }
872
873 #[test]
878 fn conversation_new_generates_id_if_none_provided() {
879 let conv = Conversation::new(NewConversation {
880 id: None,
881 metadata: None,
882 });
883 assert!(
884 conv.id.0.starts_with("conv_"),
885 "should generate a ConversationId when none provided, got: {}",
886 conv.id
887 );
888 }
889
890 #[test]
891 fn conversation_new_uses_provided_id() {
892 let custom_id = ConversationId::from("my_conv_id");
893 let conv = Conversation::new(NewConversation {
894 id: Some(custom_id.clone()),
895 metadata: None,
896 });
897 assert_eq!(conv.id, custom_id, "should use the provided ConversationId");
898 }
899
900 #[test]
901 fn conversation_with_parts_preserves_all_fields() {
902 let id = ConversationId::from("test_id");
903 let created_at = Utc::now();
904 let mut metadata = ConversationMetadata::new();
905 metadata.insert("key".to_string(), Value::String("value".to_string()));
906
907 let conv = Conversation::with_parts(id.clone(), created_at, Some(metadata.clone()));
908
909 assert_eq!(conv.id, id);
910 assert_eq!(conv.created_at, created_at);
911 assert_eq!(conv.metadata, Some(metadata));
912 }
913
914 #[test]
919 fn stored_response_new_none_has_no_previous() {
920 let resp = StoredResponse::new(None);
921 assert!(
922 resp.previous_response_id.is_none(),
923 "new(None) should have no previous_response_id"
924 );
925 }
926
927 #[test]
928 fn stored_response_new_some_has_correct_previous() {
929 let prev_id = ResponseId::from("prev_123");
930 let resp = StoredResponse::new(Some(prev_id.clone()));
931 assert_eq!(
932 resp.previous_response_id,
933 Some(prev_id),
934 "new(Some(id)) should set previous_response_id"
935 );
936 }
937
938 #[test]
939 fn stored_response_default_works() {
940 let resp = StoredResponse::default();
941 assert!(
942 resp.previous_response_id.is_none(),
943 "default() should have no previous_response_id"
944 );
945 assert_eq!(
946 resp.input,
947 Value::Array(vec![]),
948 "default input should be empty array"
949 );
950 assert_eq!(
951 resp.raw_response,
952 Value::Null,
953 "default raw_response should be Null"
954 );
955 }
956
957 #[test]
962 fn response_chain_new_creates_empty_chain() {
963 let chain = ResponseChain::new();
964 assert!(
965 chain.responses.is_empty(),
966 "new chain should have no responses"
967 );
968 assert!(
969 chain.metadata.is_empty(),
970 "new chain should have no metadata"
971 );
972 }
973
974 #[test]
975 fn response_chain_add_response_appends() {
976 let mut chain = ResponseChain::new();
977 let r1 = StoredResponse::new(None);
978 let r2 = StoredResponse::new(None);
979 let r1_id = r1.id.clone();
980 let r2_id = r2.id.clone();
981
982 chain.add_response(r1);
983 assert_eq!(chain.responses.len(), 1, "chain should have 1 response");
984
985 chain.add_response(r2);
986 assert_eq!(chain.responses.len(), 2, "chain should have 2 responses");
987 assert_eq!(chain.responses[0].id, r1_id, "first response should be r1");
988 assert_eq!(chain.responses[1].id, r2_id, "second response should be r2");
989 }
990
991 #[test]
992 fn response_chain_latest_response_id_returns_last() {
993 let mut chain = ResponseChain::new();
994 let r1 = StoredResponse::new(None);
995 let r2 = StoredResponse::new(None);
996 let r2_id = r2.id.clone();
997
998 chain.add_response(r1);
999 chain.add_response(r2);
1000
1001 assert_eq!(
1002 chain.latest_response_id(),
1003 Some(&r2_id),
1004 "latest_response_id should return the last response's ID"
1005 );
1006 }
1007
1008 #[test]
1009 fn response_chain_latest_response_id_returns_none_for_empty() {
1010 let chain = ResponseChain::new();
1011 assert_eq!(
1012 chain.latest_response_id(),
1013 None,
1014 "latest_response_id should return None for empty chain"
1015 );
1016 }
1017
1018 #[test]
1019 fn response_chain_build_context_returns_input_output_pairs() {
1020 use serde_json::json;
1021
1022 let mut chain = ResponseChain::new();
1023
1024 let mut r1 = StoredResponse::new(None);
1025 r1.input = Value::String("input1".to_string());
1026 r1.raw_response = json!({"output": "output1"});
1027
1028 let mut r2 = StoredResponse::new(None);
1029 r2.input = Value::String("input2".to_string());
1030 r2.raw_response = json!({"output": "output2"});
1031
1032 chain.add_response(r1);
1033 chain.add_response(r2);
1034
1035 let context = chain.build_context(None);
1036 assert_eq!(context.len(), 2, "should return 2 pairs");
1037 assert_eq!(context[0].0, Value::String("input1".to_string()));
1038 assert_eq!(context[0].1, Value::String("output1".to_string()));
1039 assert_eq!(context[1].0, Value::String("input2".to_string()));
1040 assert_eq!(context[1].1, Value::String("output2".to_string()));
1041 }
1042
1043 #[test]
1044 fn response_chain_build_context_with_max_responses_limits_output() {
1045 use serde_json::json;
1046
1047 let mut chain = ResponseChain::new();
1048
1049 for i in 0..5 {
1050 let mut resp = StoredResponse::new(None);
1051 resp.input = Value::String(format!("input{i}"));
1052 resp.raw_response = json!({"output": format!("output{i}")});
1053 chain.add_response(resp);
1054 }
1055
1056 let context = chain.build_context(Some(2));
1057 assert_eq!(context.len(), 2, "should return only 2 most recent pairs");
1058 assert_eq!(context[0].0, Value::String("input3".to_string()));
1060 assert_eq!(context[0].1, Value::String("output3".to_string()));
1061 assert_eq!(context[1].0, Value::String("input4".to_string()));
1062 assert_eq!(context[1].1, Value::String("output4".to_string()));
1063 }
1064
1065 #[test]
1070 fn conversation_memory_status_serializes_to_expected_uppercase_values() {
1071 assert_eq!(
1072 serde_json::to_string(&ConversationMemoryStatus::Ready).unwrap(),
1073 "\"READY\""
1074 );
1075 assert_eq!(
1076 serde_json::to_string(&ConversationMemoryStatus::Running).unwrap(),
1077 "\"RUNNING\""
1078 );
1079 assert_eq!(
1080 serde_json::to_string(&ConversationMemoryStatus::Success).unwrap(),
1081 "\"SUCCESS\""
1082 );
1083 assert_eq!(
1084 serde_json::to_string(&ConversationMemoryStatus::Failed).unwrap(),
1085 "\"FAILED\""
1086 );
1087 }
1088
1089 #[test]
1090 fn conversation_memory_type_serializes_to_expected_uppercase_values() {
1091 assert_eq!(
1092 serde_json::to_string(&ConversationMemoryType::OnDemand).unwrap(),
1093 "\"ONDEMAND\""
1094 );
1095 assert_eq!(
1096 serde_json::to_string(&ConversationMemoryType::Ltm).unwrap(),
1097 "\"LTM\""
1098 );
1099 assert_eq!(
1100 serde_json::to_string(&ConversationMemoryType::Stmo).unwrap(),
1101 "\"STMO\""
1102 );
1103 }
1104
1105 #[test]
1106 fn new_conversation_memory_keeps_insert_only_fields() {
1107 let input = NewConversationMemory {
1108 conversation_id: ConversationId::from("conv_123"),
1109 conversation_version: Some(7),
1110 response_id: Some(ResponseId::from("resp_123")),
1111 memory_type: ConversationMemoryType::Ltm,
1112 status: ConversationMemoryStatus::Ready,
1113 attempt: 0,
1114 owner_id: None,
1115 next_run_at: Utc::now(),
1116 lease_until: None,
1117 content: None,
1118 memory_config: None,
1119 scope_id: None,
1120 error_msg: None,
1121 };
1122
1123 assert_eq!(input.attempt, 0);
1124 assert_eq!(input.conversation_id, ConversationId::from("conv_123"));
1125 assert_eq!(input.response_id, Some(ResponseId::from("resp_123")));
1126 assert!(input.lease_until.is_none());
1127 }
1128}