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 Display for ResponseId {
323 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
324 f.write_str(&self.0)
325 }
326}
327
328impl Default for ResponseId {
329 fn default() -> Self {
330 Self::new()
331 }
332}
333
334impl From<String> for ResponseId {
335 fn from(value: String) -> Self {
336 Self(value)
337 }
338}
339
340impl From<&str> for ResponseId {
341 fn from(value: &str) -> Self {
342 Self(value.to_string())
343 }
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct StoredResponse {
349 pub id: ResponseId,
351
352 pub previous_response_id: Option<ResponseId>,
354
355 pub input: Value,
357
358 pub created_at: DateTime<Utc>,
360
361 pub safety_identifier: Option<String>,
363
364 pub model: Option<String>,
366
367 #[serde(default)]
369 pub conversation_id: Option<String>,
370
371 #[serde(default)]
373 pub raw_response: Value,
374}
375
376impl StoredResponse {
377 pub fn new(previous_response_id: Option<ResponseId>) -> Self {
378 Self {
379 id: ResponseId::new(),
380 previous_response_id,
381 input: Value::Array(vec![]),
382 created_at: Utc::now(),
383 safety_identifier: None,
384 model: None,
385 conversation_id: None,
386 raw_response: Value::Null,
387 }
388 }
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct ResponseChain {
394 pub responses: Vec<StoredResponse>,
396
397 pub metadata: HashMap<String, Value>,
399}
400
401impl Default for ResponseChain {
402 fn default() -> Self {
403 Self::new()
404 }
405}
406
407impl ResponseChain {
408 pub fn new() -> Self {
409 Self {
410 responses: Vec::new(),
411 metadata: HashMap::new(),
412 }
413 }
414
415 pub fn latest_response_id(&self) -> Option<&ResponseId> {
417 self.responses.last().map(|r| &r.id)
418 }
419
420 pub fn add_response(&mut self, response: StoredResponse) {
422 self.responses.push(response);
423 }
424
425 pub fn build_context(&self, max_responses: Option<usize>) -> Vec<(Value, Value)> {
427 let responses = if let Some(max) = max_responses {
428 let start = self.responses.len().saturating_sub(max);
429 &self.responses[start..]
430 } else {
431 &self.responses[..]
432 };
433
434 responses
435 .iter()
436 .map(|r| {
437 let output = r
438 .raw_response
439 .get("output")
440 .cloned()
441 .unwrap_or(Value::Array(vec![]));
442 (r.input.clone(), output)
443 })
444 .collect()
445 }
446}
447
448#[derive(Debug, thiserror::Error)]
450pub enum ResponseStorageError {
451 #[error("Response not found: {0}")]
452 ResponseNotFound(String),
453
454 #[error("Invalid chain: {0}")]
455 InvalidChain(String),
456
457 #[error("Storage error: {0}")]
458 StorageError(String),
459
460 #[error("Serialization error: {0}")]
461 SerializationError(#[from] serde_json::Error),
462}
463
464pub type ResponseResult<T> = Result<T, ResponseStorageError>;
465
466#[async_trait]
468pub trait ResponseStorage: Send + Sync {
469 async fn store_response(&self, response: StoredResponse) -> ResponseResult<ResponseId>;
471
472 async fn get_response(
474 &self,
475 response_id: &ResponseId,
476 ) -> ResponseResult<Option<StoredResponse>>;
477
478 async fn delete_response(&self, response_id: &ResponseId) -> ResponseResult<()>;
480
481 async fn get_response_chain(
492 &self,
493 response_id: &ResponseId,
494 max_depth: Option<usize>,
495 ) -> ResponseResult<ResponseChain> {
496 let mut chain = ResponseChain::new();
497 let mut current_id = Some(response_id.clone());
498 let mut seen = HashSet::new();
499
500 while let Some(ref lookup_id) = current_id {
501 if let Some(limit) = max_depth {
502 if seen.len() >= limit {
503 break;
504 }
505 }
506
507 if !seen.insert(lookup_id.clone()) {
509 return Err(ResponseStorageError::InvalidChain(format!(
510 "cycle detected at response {}",
511 lookup_id.0
512 )));
513 }
514
515 let fetched = self.get_response(lookup_id).await?;
516 match fetched {
517 Some(response) => {
518 current_id.clone_from(&response.previous_response_id);
519 chain.responses.push(response);
520 }
521 None => break,
522 }
523 }
524
525 chain.responses.reverse();
526 Ok(chain)
527 }
528
529 async fn list_identifier_responses(
531 &self,
532 identifier: &str,
533 limit: Option<usize>,
534 ) -> ResponseResult<Vec<StoredResponse>>;
535
536 async fn delete_identifier_responses(&self, identifier: &str) -> ResponseResult<usize>;
538}
539
540impl Default for StoredResponse {
541 fn default() -> Self {
542 Self::new(None)
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use std::collections::HashSet;
549
550 use super::*;
551
552 #[test]
557 fn conversation_id_new_has_conv_prefix() {
558 let id = ConversationId::new();
559 assert!(
560 id.0.starts_with("conv_"),
561 "ConversationId should start with 'conv_', got: {id}"
562 );
563 }
564
565 #[test]
566 fn conversation_id_new_generates_unique_ids() {
567 let ids: HashSet<String> = (0..100).map(|_| ConversationId::new().0).collect();
568 assert_eq!(ids.len(), 100, "all 100 ConversationIds should be unique");
569 }
570
571 #[test]
572 fn conversation_id_new_has_consistent_length() {
573 for _ in 0..10 {
575 let id = ConversationId::new();
576 assert_eq!(
577 id.0.len(),
578 55,
579 "ConversationId should be 55 chars (conv_ + 50 hex), got {} chars: {id}",
580 id.0.len()
581 );
582 }
583 }
584
585 #[test]
586 fn conversation_id_default_works_same_as_new() {
587 let id = ConversationId::default();
588 assert!(
589 id.0.starts_with("conv_"),
590 "Default ConversationId should start with 'conv_', got: {id}"
591 );
592 assert_eq!(id.0.len(), 55, "Default ConversationId should be 55 chars");
593 }
594
595 #[test]
596 fn conversation_id_from_string() {
597 let id = ConversationId::from("my_custom_id".to_string());
598 assert_eq!(id.0, "my_custom_id");
599 }
600
601 #[test]
602 fn conversation_id_from_str() {
603 let id = ConversationId::from("my_custom_id");
604 assert_eq!(id.0, "my_custom_id");
605 }
606
607 #[test]
608 fn conversation_id_display() {
609 let id = ConversationId::from("conv_abc123");
610 assert_eq!(format!("{id}"), "conv_abc123");
611 }
612
613 #[test]
618 fn conversation_item_id_from_string() {
619 let id = ConversationItemId::from("item_123".to_string());
620 assert_eq!(id.0, "item_123");
621 }
622
623 #[test]
624 fn conversation_item_id_from_str() {
625 let id = ConversationItemId::from("item_456");
626 assert_eq!(id.0, "item_456");
627 }
628
629 #[test]
630 fn conversation_item_id_display() {
631 let id = ConversationItemId::from("msg_abc");
632 assert_eq!(format!("{id}"), "msg_abc");
633 }
634
635 #[test]
640 fn response_id_new_generates_valid_ulid() {
641 let id = ResponseId::new();
642 assert_eq!(
644 id.0.len(),
645 26,
646 "ULID string should be 26 chars, got {} chars: {}",
647 id.0.len(),
648 id.0
649 );
650 assert!(
651 id.0.chars().all(|c| c.is_ascii_alphanumeric()),
652 "ULID should contain only alphanumeric characters, got: {}",
653 id.0
654 );
655 }
656
657 #[test]
658 fn response_id_new_generates_unique_ids() {
659 let ids: HashSet<String> = (0..100).map(|_| ResponseId::new().0).collect();
660 assert_eq!(ids.len(), 100, "all 100 ResponseIds should be unique");
661 }
662
663 #[test]
664 fn response_id_default_works_same_as_new() {
665 let id = ResponseId::default();
666 assert_eq!(id.0.len(), 26, "Default ResponseId should be 26-char ULID");
667 }
668
669 #[test]
670 fn response_id_from_string() {
671 let id = ResponseId::from("resp_custom".to_string());
672 assert_eq!(id.0, "resp_custom");
673 }
674
675 #[test]
676 fn response_id_from_str() {
677 let id = ResponseId::from("resp_custom");
678 assert_eq!(id.0, "resp_custom");
679 }
680
681 #[test]
686 fn make_item_id_message_prefix() {
687 let id = make_item_id("message");
688 assert!(
689 id.0.starts_with("msg_"),
690 "message type should produce 'msg_' prefix, got: {id}"
691 );
692 }
693
694 #[test]
695 fn make_item_id_reasoning_prefix() {
696 let id = make_item_id("reasoning");
697 assert!(
698 id.0.starts_with("rs_"),
699 "reasoning type should produce 'rs_' prefix, got: {id}"
700 );
701 }
702
703 #[test]
704 fn make_item_id_mcp_call_prefix() {
705 let id = make_item_id("mcp_call");
706 assert!(
707 id.0.starts_with("mcp_"),
708 "mcp_call type should produce 'mcp_' prefix, got: {id}"
709 );
710 }
711
712 #[test]
713 fn make_item_id_mcp_list_tools_prefix() {
714 let id = make_item_id("mcp_list_tools");
715 assert!(
716 id.0.starts_with("mcpl_"),
717 "mcp_list_tools type should produce 'mcpl_' prefix, got: {id}"
718 );
719 }
720
721 #[test]
722 fn make_item_id_function_call_prefix() {
723 let id = make_item_id("function_call");
724 assert!(
725 id.0.starts_with("fc_"),
726 "function_call type should produce 'fc_' prefix, got: {id}"
727 );
728 }
729
730 #[test]
731 fn make_item_id_unknown_type_uses_first_3_chars() {
732 let id = make_item_id("custom_type");
733 assert!(
734 id.0.starts_with("cus_"),
735 "unknown type 'custom_type' should produce 'cus_' prefix, got: {id}"
736 );
737 }
738
739 #[test]
740 fn make_item_id_empty_type_uses_itm() {
741 let id = make_item_id("");
742 assert!(
743 id.0.starts_with("itm_"),
744 "empty type string should produce 'itm_' prefix, got: {id}"
745 );
746 }
747
748 #[test]
749 fn make_item_id_correct_length() {
750 let test_cases = vec![
752 ("message", "msg_"),
753 ("reasoning", "rs_"),
754 ("mcp_call", "mcp_"),
755 ("mcp_list_tools", "mcpl_"),
756 ("function_call", "fc_"),
757 ];
758
759 for (item_type, prefix) in test_cases {
760 let id = make_item_id(item_type);
761 let expected_len = prefix.len() + 50;
762 assert_eq!(
763 id.0.len(),
764 expected_len,
765 "make_item_id(\"{item_type}\") should be {expected_len} chars ('{prefix}' + 50 hex), got {} chars: {id}",
766 id.0.len()
767 );
768 }
769
770 let id = make_item_id("custom_type");
772 assert_eq!(
773 id.0.len(),
774 54,
775 "unknown type should be 54 chars (3 char prefix + '_' + 50 hex), got {} chars: {id}",
776 id.0.len()
777 );
778
779 let id = make_item_id("");
781 assert_eq!(
782 id.0.len(),
783 54,
784 "empty type should be 54 chars ('itm_' + 50 hex), got {} chars: {id}",
785 id.0.len()
786 );
787 }
788
789 #[test]
794 fn conversation_new_generates_id_if_none_provided() {
795 let conv = Conversation::new(NewConversation {
796 id: None,
797 metadata: None,
798 });
799 assert!(
800 conv.id.0.starts_with("conv_"),
801 "should generate a ConversationId when none provided, got: {}",
802 conv.id
803 );
804 }
805
806 #[test]
807 fn conversation_new_uses_provided_id() {
808 let custom_id = ConversationId::from("my_conv_id");
809 let conv = Conversation::new(NewConversation {
810 id: Some(custom_id.clone()),
811 metadata: None,
812 });
813 assert_eq!(conv.id, custom_id, "should use the provided ConversationId");
814 }
815
816 #[test]
817 fn conversation_with_parts_preserves_all_fields() {
818 let id = ConversationId::from("test_id");
819 let created_at = Utc::now();
820 let mut metadata = ConversationMetadata::new();
821 metadata.insert("key".to_string(), Value::String("value".to_string()));
822
823 let conv = Conversation::with_parts(id.clone(), created_at, Some(metadata.clone()));
824
825 assert_eq!(conv.id, id);
826 assert_eq!(conv.created_at, created_at);
827 assert_eq!(conv.metadata, Some(metadata));
828 }
829
830 #[test]
835 fn stored_response_new_none_has_no_previous() {
836 let resp = StoredResponse::new(None);
837 assert!(
838 resp.previous_response_id.is_none(),
839 "new(None) should have no previous_response_id"
840 );
841 }
842
843 #[test]
844 fn stored_response_new_some_has_correct_previous() {
845 let prev_id = ResponseId::from("prev_123");
846 let resp = StoredResponse::new(Some(prev_id.clone()));
847 assert_eq!(
848 resp.previous_response_id,
849 Some(prev_id),
850 "new(Some(id)) should set previous_response_id"
851 );
852 }
853
854 #[test]
855 fn stored_response_default_works() {
856 let resp = StoredResponse::default();
857 assert!(
858 resp.previous_response_id.is_none(),
859 "default() should have no previous_response_id"
860 );
861 assert_eq!(
862 resp.input,
863 Value::Array(vec![]),
864 "default input should be empty array"
865 );
866 assert_eq!(
867 resp.raw_response,
868 Value::Null,
869 "default raw_response should be Null"
870 );
871 }
872
873 #[test]
878 fn response_chain_new_creates_empty_chain() {
879 let chain = ResponseChain::new();
880 assert!(
881 chain.responses.is_empty(),
882 "new chain should have no responses"
883 );
884 assert!(
885 chain.metadata.is_empty(),
886 "new chain should have no metadata"
887 );
888 }
889
890 #[test]
891 fn response_chain_add_response_appends() {
892 let mut chain = ResponseChain::new();
893 let r1 = StoredResponse::new(None);
894 let r2 = StoredResponse::new(None);
895 let r1_id = r1.id.clone();
896 let r2_id = r2.id.clone();
897
898 chain.add_response(r1);
899 assert_eq!(chain.responses.len(), 1, "chain should have 1 response");
900
901 chain.add_response(r2);
902 assert_eq!(chain.responses.len(), 2, "chain should have 2 responses");
903 assert_eq!(chain.responses[0].id, r1_id, "first response should be r1");
904 assert_eq!(chain.responses[1].id, r2_id, "second response should be r2");
905 }
906
907 #[test]
908 fn response_chain_latest_response_id_returns_last() {
909 let mut chain = ResponseChain::new();
910 let r1 = StoredResponse::new(None);
911 let r2 = StoredResponse::new(None);
912 let r2_id = r2.id.clone();
913
914 chain.add_response(r1);
915 chain.add_response(r2);
916
917 assert_eq!(
918 chain.latest_response_id(),
919 Some(&r2_id),
920 "latest_response_id should return the last response's ID"
921 );
922 }
923
924 #[test]
925 fn response_chain_latest_response_id_returns_none_for_empty() {
926 let chain = ResponseChain::new();
927 assert_eq!(
928 chain.latest_response_id(),
929 None,
930 "latest_response_id should return None for empty chain"
931 );
932 }
933
934 #[test]
935 fn response_chain_build_context_returns_input_output_pairs() {
936 use serde_json::json;
937
938 let mut chain = ResponseChain::new();
939
940 let mut r1 = StoredResponse::new(None);
941 r1.input = Value::String("input1".to_string());
942 r1.raw_response = json!({"output": "output1"});
943
944 let mut r2 = StoredResponse::new(None);
945 r2.input = Value::String("input2".to_string());
946 r2.raw_response = json!({"output": "output2"});
947
948 chain.add_response(r1);
949 chain.add_response(r2);
950
951 let context = chain.build_context(None);
952 assert_eq!(context.len(), 2, "should return 2 pairs");
953 assert_eq!(context[0].0, Value::String("input1".to_string()));
954 assert_eq!(context[0].1, Value::String("output1".to_string()));
955 assert_eq!(context[1].0, Value::String("input2".to_string()));
956 assert_eq!(context[1].1, Value::String("output2".to_string()));
957 }
958
959 #[test]
960 fn response_chain_build_context_with_max_responses_limits_output() {
961 use serde_json::json;
962
963 let mut chain = ResponseChain::new();
964
965 for i in 0..5 {
966 let mut resp = StoredResponse::new(None);
967 resp.input = Value::String(format!("input{i}"));
968 resp.raw_response = json!({"output": format!("output{i}")});
969 chain.add_response(resp);
970 }
971
972 let context = chain.build_context(Some(2));
973 assert_eq!(context.len(), 2, "should return only 2 most recent pairs");
974 assert_eq!(context[0].0, Value::String("input3".to_string()));
976 assert_eq!(context[0].1, Value::String("output3".to_string()));
977 assert_eq!(context[1].0, Value::String("input4".to_string()));
978 assert_eq!(context[1].1, Value::String("output4".to_string()));
979 }
980}