1use std::sync::Arc;
6
7use async_trait::async_trait;
8use gemini_adk_rs::error::AgentError;
9use gemini_adk_rs::llm::LlmRequest;
10use gemini_adk_rs::middleware::Middleware;
11use gemini_genai_rs::prelude::Content;
12
13#[derive(Clone)]
15pub struct ContextPolicy {
16 name: &'static str,
17 #[allow(clippy::type_complexity)]
18 filter: Arc<dyn Fn(&[Content]) -> Vec<Content> + Send + Sync>,
19}
20
21impl ContextPolicy {
22 fn new(
23 name: &'static str,
24 f: impl Fn(&[Content]) -> Vec<Content> + Send + Sync + 'static,
25 ) -> Self {
26 Self {
27 name,
28 filter: Arc::new(f),
29 }
30 }
31
32 pub fn apply(&self, history: &[Content]) -> Vec<Content> {
34 (self.filter)(history)
35 }
36
37 pub fn name(&self) -> &str {
39 self.name
40 }
41}
42
43impl std::fmt::Debug for ContextPolicy {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("ContextPolicy")
46 .field("name", &self.name)
47 .finish()
48 }
49}
50
51impl std::ops::Add for ContextPolicy {
54 type Output = ContextPolicyChain;
55
56 fn add(self, rhs: ContextPolicy) -> Self::Output {
57 ContextPolicyChain {
58 policies: vec![self, rhs],
59 }
60 }
61}
62
63#[derive(Clone)]
65pub struct ContextPolicyChain {
66 pub policies: Vec<ContextPolicy>,
68}
69
70impl ContextPolicyChain {
71 pub fn apply(&self, history: &[Content]) -> Vec<Content> {
76 let mut current = history.to_vec();
77 for policy in &self.policies {
78 current = policy.apply(¤t);
79 }
80 current
81 }
82
83 pub fn into_middleware(self) -> Arc<dyn Middleware> {
86 Arc::new(ContextMiddleware { chain: self })
87 }
88}
89
90impl From<ContextPolicy> for ContextPolicyChain {
93 fn from(policy: ContextPolicy) -> Self {
94 ContextPolicyChain {
95 policies: vec![policy],
96 }
97 }
98}
99
100struct ContextMiddleware {
103 chain: ContextPolicyChain,
104}
105
106#[async_trait]
107impl Middleware for ContextMiddleware {
108 fn name(&self) -> &str {
109 "context"
110 }
111
112 async fn transform_request(&self, request: &mut LlmRequest) -> Result<(), AgentError> {
113 request.contents = self.chain.apply(&request.contents);
114 Ok(())
115 }
116}
117
118impl std::ops::Add<ContextPolicy> for ContextPolicyChain {
119 type Output = ContextPolicyChain;
120
121 fn add(mut self, rhs: ContextPolicy) -> Self::Output {
122 self.policies.push(rhs);
123 self
124 }
125}
126
127pub struct C;
129
130impl C {
131 pub fn window(n: usize) -> ContextPolicy {
133 ContextPolicy::new("window", move |history| {
134 if history.len() > n {
135 history[history.len() - n..].to_vec()
136 } else {
137 history.to_vec()
138 }
139 })
140 }
141
142 pub fn user_only() -> ContextPolicy {
144 use gemini_genai_rs::prelude::Role;
145 ContextPolicy::new("user_only", move |history| {
146 history
147 .iter()
148 .filter(|c| c.role == Some(Role::User))
149 .cloned()
150 .collect()
151 })
152 }
153
154 pub fn custom(f: impl Fn(&[Content]) -> Vec<Content> + Send + Sync + 'static) -> ContextPolicy {
156 ContextPolicy::new("custom", f)
157 }
158
159 pub fn model_only() -> ContextPolicy {
161 use gemini_genai_rs::prelude::Role;
162 ContextPolicy::new("model_only", move |history| {
163 history
164 .iter()
165 .filter(|c| c.role == Some(Role::Model))
166 .cloned()
167 .collect()
168 })
169 }
170
171 pub fn head(n: usize) -> ContextPolicy {
173 ContextPolicy::new("head", move |history| {
174 history.iter().take(n).cloned().collect()
175 })
176 }
177
178 pub fn sample(n: usize) -> ContextPolicy {
180 ContextPolicy::new("sample", move |history| {
181 history
182 .iter()
183 .enumerate()
184 .filter(|(i, _)| i % n == 0)
185 .map(|(_, c)| c.clone())
186 .collect()
187 })
188 }
189
190 pub fn exclude_tools() -> ContextPolicy {
192 use gemini_genai_rs::prelude::Part;
193 ContextPolicy::new("exclude_tools", move |history| {
194 history
195 .iter()
196 .filter(|c| {
197 c.parts.iter().all(|p| {
198 !matches!(p, Part::FunctionCall { .. } | Part::FunctionResponse { .. })
199 })
200 })
201 .cloned()
202 .collect()
203 })
204 }
205
206 pub fn prepend(content: Content) -> ContextPolicy {
208 ContextPolicy::new("prepend", move |history| {
209 let mut result = vec![content.clone()];
210 result.extend(history.iter().cloned());
211 result
212 })
213 }
214
215 pub fn append(content: Content) -> ContextPolicy {
217 ContextPolicy::new("append", move |history| {
218 let mut result = history.to_vec();
219 result.push(content.clone());
220 result
221 })
222 }
223
224 pub fn text_only() -> ContextPolicy {
226 use gemini_genai_rs::prelude::Part;
227 ContextPolicy::new("text_only", move |history| {
228 history
229 .iter()
230 .filter(|c| c.parts.iter().any(|p| matches!(p, Part::Text { .. })))
231 .cloned()
232 .collect()
233 })
234 }
235
236 pub fn filter(f: impl Fn(&Content) -> bool + Send + Sync + 'static) -> ContextPolicy {
238 ContextPolicy::new("filter", move |history| {
239 history.iter().filter(|c| f(c)).cloned().collect()
240 })
241 }
242
243 pub fn map(f: impl Fn(&Content) -> Content + Send + Sync + 'static) -> ContextPolicy {
245 ContextPolicy::new("map", move |history| history.iter().map(&f).collect())
246 }
247
248 pub fn truncate(max_chars: usize) -> ContextPolicy {
250 use gemini_genai_rs::prelude::Part;
251 ContextPolicy::new("truncate", move |history| {
252 let mut total = 0;
253 let mut result = Vec::new();
254 for c in history.iter().rev() {
256 let text_len: usize = c
257 .parts
258 .iter()
259 .filter_map(|p| match p {
260 Part::Text { text } => Some(text.len()),
261 _ => None,
262 })
263 .sum();
264 if total + text_len > max_chars && !result.is_empty() {
265 break;
266 }
267 total += text_len;
268 result.push(c.clone());
269 }
270 result.reverse();
271 result
272 })
273 }
274
275 pub fn last(n: usize) -> ContextPolicy {
278 Self::window(n)
279 }
280
281 pub fn empty() -> ContextPolicy {
283 ContextPolicy::new("empty", |_| Vec::new())
284 }
285
286 pub fn from_state(keys: &[&str]) -> ContextPolicy {
297 let owned_keys: Vec<String> = keys.iter().map(|k| k.to_string()).collect();
298 ContextPolicy::new("from_state", move |history| {
299 let mut result = Vec::new();
304 if !owned_keys.is_empty() {
305 let key_list = owned_keys.join(", ");
306 result.push(Content::user(format!("[Context keys: {}]", key_list)));
307 }
308 result.extend(history.iter().cloned());
309 result
310 })
311 }
312
313 pub fn none() -> ContextPolicy {
315 Self::empty()
316 }
317
318 pub fn recent(n: usize) -> ContextPolicy {
320 Self::window(n)
321 }
322
323 pub fn template(tpl: &str) -> ContextPolicy {
327 let tpl = tpl.to_string();
328 ContextPolicy::new("template", move |history| {
329 let mut result = vec![Content::user(tpl.clone())];
330 result.extend(history.iter().cloned());
331 result
332 })
333 }
334
335 pub fn when(
339 predicate: impl Fn() -> bool + Send + Sync + 'static,
340 inner: ContextPolicy,
341 ) -> ContextPolicy {
342 ContextPolicy::new("when", move |history| {
343 if predicate() {
344 inner.apply(history)
345 } else {
346 history.to_vec()
347 }
348 })
349 }
350
351 pub fn rolling(n: usize) -> ContextPolicy {
353 Self::window(n)
354 }
355
356 pub fn compact() -> ContextPolicy {
358 Self::exclude_tools()
359 }
360
361 pub fn budget(max_tokens: usize) -> ContextPolicy {
365 Self::truncate(max_tokens * 4)
366 }
367
368 pub fn fresh(max_entries: usize) -> ContextPolicy {
370 Self::window(max_entries)
371 }
372
373 pub fn redact(patterns: &[&str]) -> ContextPolicy {
375 use gemini_genai_rs::prelude::Part;
376 let patterns: Vec<String> = patterns.iter().map(|p| p.to_string()).collect();
377 ContextPolicy::new("redact", move |history| {
378 history
379 .iter()
380 .map(|c| {
381 let parts: Vec<Part> = c
382 .parts
383 .iter()
384 .map(|p| match p {
385 Part::Text { text } => {
386 let mut redacted = text.clone();
387 for pattern in &patterns {
388 redacted = redacted.replace(pattern.as_str(), "[REDACTED]");
389 }
390 Part::Text { text: redacted }
391 }
392 other => other.clone(),
393 })
394 .collect();
395 Content {
396 role: c.role,
397 parts,
398 }
399 })
400 .collect()
401 })
402 }
403
404 pub fn summarize(prompt: &str) -> ContextPolicy {
415 let prompt = prompt.to_string();
416 ContextPolicy::new("summarize", move |history| {
417 let mut result = vec![Content::user(format!("[Summarize context: {}]", prompt))];
418 result.extend(history.iter().cloned());
419 result
420 })
421 }
422
423 pub fn relevant(query_key: &str) -> ContextPolicy {
433 let key = query_key.to_string();
434 ContextPolicy::new("relevant", move |history| {
435 let mut result = vec![Content::user(format!(
436 "[Filter context relevant to state key: {}]",
437 key
438 ))];
439 result.extend(history.iter().cloned());
440 result
441 })
442 }
443
444 pub fn extract(keys: &[&str]) -> ContextPolicy {
454 let owned_keys: Vec<String> = keys.iter().map(|k| k.to_string()).collect();
455 ContextPolicy::new("extract", move |history| {
456 let mut result = vec![Content::user(format!(
457 "[Extract from context: {}]",
458 owned_keys.join(", ")
459 ))];
460 result.extend(history.iter().cloned());
461 result
462 })
463 }
464
465 pub fn distill(instruction: &str) -> ContextPolicy {
475 let instruction = instruction.to_string();
476 ContextPolicy::new("distill", move |history| {
477 let mut result = vec![Content::user(format!("[Distill context: {}]", instruction))];
478 result.extend(history.iter().cloned());
479 result
480 })
481 }
482
483 pub fn priority(weights: &[(&str, f64)]) -> ContextPolicy {
494 let owned_weights: Vec<(String, f64)> =
495 weights.iter().map(|(k, v)| (k.to_string(), *v)).collect();
496 ContextPolicy::new("priority", move |history| {
497 let weight_str = owned_weights
498 .iter()
499 .map(|(k, v)| format!("{}={}", k, v))
500 .collect::<Vec<_>>()
501 .join(", ");
502 let mut result = vec![Content::user(format!("[Priority weights: {}]", weight_str))];
503 result.extend(history.iter().cloned());
504 result
505 })
506 }
507
508 pub fn fit(max_tokens: usize) -> ContextPolicy {
519 use gemini_genai_rs::prelude::Part;
520 let max_chars = max_tokens * 4; ContextPolicy::new("fit", move |history| {
522 let mut total = 0;
523 let mut result = Vec::new();
524 for c in history.iter().rev() {
526 let text_len: usize = c
527 .parts
528 .iter()
529 .filter_map(|p| match p {
530 Part::Text { text } => Some(text.len()),
531 _ => None,
532 })
533 .sum();
534 if total + text_len > max_chars && !result.is_empty() {
535 result.push(Content::user(format!(
537 "[Context truncated to fit ~{} token budget; {} earlier messages omitted]",
538 max_tokens,
539 history.len() - result.len()
540 )));
541 break;
542 }
543 total += text_len;
544 result.push(c.clone());
545 }
546 result.reverse();
547 result
548 })
549 }
550
551 pub fn project(fields: &[&str]) -> ContextPolicy {
561 let owned_fields: Vec<String> = fields.iter().map(|f| f.to_string()).collect();
562 ContextPolicy::new("project", move |history| {
563 let mut result = vec![Content::user(format!(
564 "[Project fields: {}]",
565 owned_fields.join(", ")
566 ))];
567 result.extend(history.iter().cloned());
568 result
569 })
570 }
571
572 pub fn select(predicate: impl Fn(&Content) -> bool + Send + Sync + 'static) -> ContextPolicy {
577 ContextPolicy::new("select", move |history| {
578 history.iter().filter(|c| predicate(c)).cloned().collect()
579 })
580 }
581
582 pub fn from_agents(names: &[&str]) -> ContextPolicy {
592 let owned_names: Vec<String> = names.iter().map(|n| n.to_string()).collect();
593 ContextPolicy::new("from_agents", move |history| {
594 history
595 .iter()
596 .filter(|c| {
597 c.parts.iter().any(|p| match p {
598 gemini_genai_rs::prelude::Part::Text { text } => owned_names
599 .iter()
600 .any(|name| text.contains(&format!("[Agent: {}]", name))),
601 _ => false,
602 })
603 })
604 .cloned()
605 .collect()
606 })
607 }
608
609 pub fn exclude_agents(names: &[&str]) -> ContextPolicy {
619 let owned_names: Vec<String> = names.iter().map(|n| n.to_string()).collect();
620 ContextPolicy::new("exclude_agents", move |history| {
621 history
622 .iter()
623 .filter(|c| {
624 !c.parts.iter().any(|p| match p {
625 gemini_genai_rs::prelude::Part::Text { text } => owned_names
626 .iter()
627 .any(|name| text.contains(&format!("[Agent: {}]", name))),
628 _ => false,
629 })
630 })
631 .cloned()
632 .collect()
633 })
634 }
635
636 pub fn notes(key: &str) -> ContextPolicy {
646 let key = key.to_string();
647 ContextPolicy::new("notes", move |history| {
648 let mut result = vec![Content::user(format!(
649 "[Scratchpad from state key: {}]",
650 key
651 ))];
652 result.extend(history.iter().cloned());
653 result
654 })
655 }
656
657 pub fn pipeline_aware() -> ContextPolicy {
668 ContextPolicy::new("pipeline_aware", |history| {
669 let mut result = vec![Content::user(
670 "[Pipeline-aware: adapt context to pipeline position]".to_string(),
671 )];
672 result.extend(history.iter().cloned());
673 result
674 })
675 }
676
677 pub fn dedup() -> ContextPolicy {
679 use gemini_genai_rs::prelude::Part;
680 ContextPolicy::new("dedup", |history| {
681 fn extract_text(c: &Content) -> String {
682 c.parts
683 .iter()
684 .filter_map(|p| match p {
685 Part::Text { text } => Some(text.as_str()),
686 _ => None,
687 })
688 .collect()
689 }
690 let mut result: Vec<Content> = Vec::new();
691 for c in history {
692 let dominated = result.last().is_some_and(|prev| {
693 let prev_text = extract_text(prev);
694 let curr_text = extract_text(c);
695 prev_text == curr_text && !prev_text.is_empty()
696 });
697 if !dominated {
698 result.push(c.clone());
699 }
700 }
701 result
702 })
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use gemini_genai_rs::prelude::Content;
710
711 #[test]
712 fn window_limits_messages() {
713 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
714 let result = C::window(2).apply(&history);
715 assert_eq!(result.len(), 2);
716 }
717
718 #[test]
719 fn window_keeps_all_if_under_limit() {
720 let history = vec![Content::user("a")];
721 let result = C::window(5).apply(&history);
722 assert_eq!(result.len(), 1);
723 }
724
725 #[test]
726 fn user_only_filters() {
727 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
728 let result = C::user_only().apply(&history);
729 assert_eq!(result.len(), 2);
730 }
731
732 #[test]
733 fn compose_with_add() {
734 let chain = C::window(10) + C::user_only();
735 assert_eq!(chain.policies.len(), 2);
736 }
737
738 #[test]
739 fn chain_extends_with_add() {
740 let chain = C::window(10) + C::user_only() + C::custom(|h| h.to_vec());
741 assert_eq!(chain.policies.len(), 3);
742 }
743
744 #[test]
745 fn model_only_filters() {
746 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
747 let result = C::model_only().apply(&history);
748 assert_eq!(result.len(), 1);
749 }
750
751 #[test]
752 fn head_keeps_first_n() {
753 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
754 let result = C::head(2).apply(&history);
755 assert_eq!(result.len(), 2);
756 }
757
758 #[test]
759 fn sample_every_nth() {
760 let history = vec![
761 Content::user("a"),
762 Content::model("b"),
763 Content::user("c"),
764 Content::model("d"),
765 ];
766 let result = C::sample(2).apply(&history);
767 assert_eq!(result.len(), 2);
768 }
769
770 #[test]
771 fn empty_returns_nothing() {
772 let history = vec![Content::user("a"), Content::model("b")];
773 let result = C::empty().apply(&history);
774 assert!(result.is_empty());
775 }
776
777 #[test]
778 fn last_is_alias_for_window() {
779 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
780 let result = C::last(1).apply(&history);
781 assert_eq!(result.len(), 1);
782 }
783
784 #[test]
785 fn text_only_filters_non_text() {
786 let history = vec![Content::user("text msg")];
787 let result = C::text_only().apply(&history);
788 assert_eq!(result.len(), 1);
789 }
790
791 #[test]
792 fn filter_with_predicate() {
793 use gemini_genai_rs::prelude::Part;
794 let history = vec![
795 Content::user("keep"),
796 Content::user("skip"),
797 Content::user("keep this too"),
798 ];
799 let result = C::filter(|c| {
800 c.parts.iter().any(|p| match p {
801 Part::Text { text } => text.contains("keep"),
802 _ => false,
803 })
804 })
805 .apply(&history);
806 assert_eq!(result.len(), 2);
807 }
808
809 #[test]
810 fn dedup_removes_adjacent_duplicates() {
811 let history = vec![
812 Content::user("hello"),
813 Content::user("hello"),
814 Content::user("world"),
815 Content::user("world"),
816 Content::user("world"),
817 ];
818 let result = C::dedup().apply(&history);
819 assert_eq!(result.len(), 2);
820 }
821
822 #[test]
823 fn prepend_adds_to_front() {
824 let history = vec![Content::user("existing")];
825 let result = C::prepend(Content::model("system")).apply(&history);
826 assert_eq!(result.len(), 2);
827 }
828
829 #[test]
830 fn append_adds_to_back() {
831 let history = vec![Content::user("existing")];
832 let result = C::append(Content::model("suffix")).apply(&history);
833 assert_eq!(result.len(), 2);
834 }
835
836 #[test]
837 fn from_state_prepends_context() {
838 let history = vec![Content::user("hello")];
839 let result = C::from_state(&["user:name", "app:balance"]).apply(&history);
840 assert_eq!(result.len(), 2);
841 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
843 assert!(text.contains("user:name"));
844 assert!(text.contains("app:balance"));
845 } else {
846 panic!("Expected text part");
847 }
848 }
849
850 #[test]
851 fn summarize_prepends_marker() {
852 let history = vec![Content::user("hello"), Content::model("hi")];
853 let result = C::summarize("Focus on action items").apply(&history);
854 assert_eq!(result.len(), 3);
855 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
856 assert!(text.contains("Summarize context"));
857 assert!(text.contains("action items"));
858 } else {
859 panic!("Expected text part");
860 }
861 }
862
863 #[test]
864 fn relevant_prepends_key_marker() {
865 let history = vec![Content::user("hello")];
866 let result = C::relevant("user:topic").apply(&history);
867 assert_eq!(result.len(), 2);
868 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
869 assert!(text.contains("user:topic"));
870 } else {
871 panic!("Expected text part");
872 }
873 }
874
875 #[test]
876 fn extract_prepends_keys_marker() {
877 let history = vec![Content::user("hello")];
878 let result = C::extract(&["name", "order_id"]).apply(&history);
879 assert_eq!(result.len(), 2);
880 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
881 assert!(text.contains("name"));
882 assert!(text.contains("order_id"));
883 } else {
884 panic!("Expected text part");
885 }
886 }
887
888 #[test]
889 fn distill_prepends_instruction_marker() {
890 let history = vec![Content::user("hello")];
891 let result = C::distill("Keep only decisions").apply(&history);
892 assert_eq!(result.len(), 2);
893 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
894 assert!(text.contains("Distill context"));
895 assert!(text.contains("decisions"));
896 } else {
897 panic!("Expected text part");
898 }
899 }
900
901 #[test]
902 fn priority_prepends_weights_marker() {
903 let history = vec![Content::user("hello")];
904 let result = C::priority(&[("user", 1.0), ("model", 0.5)]).apply(&history);
905 assert_eq!(result.len(), 2);
906 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
907 assert!(text.contains("Priority weights"));
908 assert!(text.contains("user=1"));
909 assert!(text.contains("model=0.5"));
910 } else {
911 panic!("Expected text part");
912 }
913 }
914
915 #[test]
916 fn fit_truncates_with_marker() {
917 let long_msg = "a".repeat(500);
919 let history = vec![
920 Content::user(long_msg.clone()),
921 Content::user(long_msg.clone()),
922 Content::user("recent"),
923 ];
924 let result = C::fit(10).apply(&history);
926 assert!(result.len() <= 3);
928 let has_marker = result.iter().any(|c| {
930 c.parts.iter().any(|p| match p {
931 gemini_genai_rs::prelude::Part::Text { text } => text.contains("truncated"),
932 _ => false,
933 })
934 });
935 assert!(has_marker);
936 }
937
938 #[test]
939 fn fit_keeps_all_when_under_budget() {
940 let history = vec![Content::user("hi"), Content::model("hello")];
941 let result = C::fit(1000).apply(&history);
942 assert_eq!(result.len(), 2);
943 }
944
945 #[test]
946 fn project_prepends_fields_marker() {
947 let history = vec![Content::user("hello")];
948 let result = C::project(&["name", "status"]).apply(&history);
949 assert_eq!(result.len(), 2);
950 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
951 assert!(text.contains("Project fields"));
952 assert!(text.contains("name"));
953 assert!(text.contains("status"));
954 } else {
955 panic!("Expected text part");
956 }
957 }
958
959 #[test]
960 fn select_filters_matching() {
961 use gemini_genai_rs::prelude::Role;
962 let history = vec![
963 Content::user("keep"),
964 Content::model("skip"),
965 Content::user("also keep"),
966 ];
967 let result = C::select(|c| c.role == Some(Role::User)).apply(&history);
968 assert_eq!(result.len(), 2);
969 }
970
971 #[test]
972 fn from_agents_filters_by_agent_marker() {
973 let history = vec![
974 Content::user("[Agent: researcher] Found data"),
975 Content::user("[Agent: logger] Debug info"),
976 Content::user("[Agent: researcher] More data"),
977 ];
978 let result = C::from_agents(&["researcher"]).apply(&history);
979 assert_eq!(result.len(), 2);
980 }
981
982 #[test]
983 fn exclude_agents_removes_agent_messages() {
984 let history = vec![
985 Content::user("[Agent: researcher] Found data"),
986 Content::user("[Agent: logger] Debug info"),
987 Content::user("[Agent: researcher] More data"),
988 ];
989 let result = C::exclude_agents(&["logger"]).apply(&history);
990 assert_eq!(result.len(), 2);
991 }
992
993 #[test]
994 fn notes_prepends_scratchpad_marker() {
995 let history = vec![Content::user("hello")];
996 let result = C::notes("session:scratchpad").apply(&history);
997 assert_eq!(result.len(), 2);
998 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
999 assert!(text.contains("Scratchpad"));
1000 assert!(text.contains("session:scratchpad"));
1001 } else {
1002 panic!("Expected text part");
1003 }
1004 }
1005
1006 #[test]
1007 fn pipeline_aware_prepends_marker() {
1008 let history = vec![Content::user("hello")];
1009 let result = C::pipeline_aware().apply(&history);
1010 assert_eq!(result.len(), 2);
1011 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
1012 assert!(text.contains("Pipeline-aware"));
1013 } else {
1014 panic!("Expected text part");
1015 }
1016 }
1017}