gemini_adk_fluent_rs/compose/
context.rs1use std::sync::Arc;
7
8use async_trait::async_trait;
9use gemini_adk_rs::error::AgentError;
10use gemini_adk_rs::llm::LlmRequest;
11use gemini_adk_rs::middleware::Middleware;
12use gemini_genai_rs::prelude::Content;
13
14#[derive(Clone)]
16pub struct ContextPolicy {
17 name: &'static str,
18 #[allow(clippy::type_complexity)]
19 filter: Arc<dyn Fn(&[Content]) -> Vec<Content> + Send + Sync>,
20}
21
22impl ContextPolicy {
23 fn new(
24 name: &'static str,
25 f: impl Fn(&[Content]) -> Vec<Content> + Send + Sync + 'static,
26 ) -> Self {
27 Self {
28 name,
29 filter: Arc::new(f),
30 }
31 }
32
33 pub fn apply(&self, history: &[Content]) -> Vec<Content> {
35 (self.filter)(history)
36 }
37
38 pub fn name(&self) -> &str {
40 self.name
41 }
42}
43
44impl std::fmt::Debug for ContextPolicy {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("ContextPolicy")
47 .field("name", &self.name)
48 .finish()
49 }
50}
51
52impl std::ops::Shr for ContextPolicy {
55 type Output = ContextComposite;
56
57 fn shr(self, rhs: ContextPolicy) -> Self::Output {
58 ContextComposite {
59 policies: vec![self, rhs],
60 }
61 }
62}
63
64#[derive(Clone)]
66#[non_exhaustive]
67pub struct ContextComposite {
68 pub policies: Vec<ContextPolicy>,
70}
71
72impl ContextComposite {
73 pub fn apply(&self, history: &[Content]) -> Vec<Content> {
78 let mut current = history.to_vec();
79 for policy in &self.policies {
80 current = policy.apply(¤t);
81 }
82 current
83 }
84
85 pub fn into_middleware(self) -> Arc<dyn Middleware> {
88 Arc::new(ContextMiddleware { chain: self })
89 }
90}
91
92impl From<ContextPolicy> for ContextComposite {
95 fn from(policy: ContextPolicy) -> Self {
96 ContextComposite {
97 policies: vec![policy],
98 }
99 }
100}
101
102struct ContextMiddleware {
105 chain: ContextComposite,
106}
107
108#[async_trait]
109impl Middleware for ContextMiddleware {
110 fn name(&self) -> &str {
111 "context"
112 }
113
114 async fn transform_request(&self, request: &mut LlmRequest) -> Result<(), AgentError> {
115 request.contents = self.chain.apply(&request.contents);
116 Ok(())
117 }
118}
119
120impl std::ops::Shr<ContextPolicy> for ContextComposite {
121 type Output = ContextComposite;
122
123 fn shr(mut self, rhs: ContextPolicy) -> Self::Output {
124 self.policies.push(rhs);
125 self
126 }
127}
128
129pub struct C;
131
132impl C {
133 pub fn window(n: usize) -> ContextPolicy {
135 ContextPolicy::new("window", move |history| {
136 if history.len() > n {
137 history[history.len() - n..].to_vec()
138 } else {
139 history.to_vec()
140 }
141 })
142 }
143
144 pub fn user_only() -> ContextPolicy {
146 use gemini_genai_rs::prelude::Role;
147 ContextPolicy::new("user_only", move |history| {
148 history
149 .iter()
150 .filter(|c| c.role == Some(Role::User))
151 .cloned()
152 .collect()
153 })
154 }
155
156 pub fn custom(f: impl Fn(&[Content]) -> Vec<Content> + Send + Sync + 'static) -> ContextPolicy {
158 ContextPolicy::new("custom", f)
159 }
160
161 pub fn model_only() -> ContextPolicy {
163 use gemini_genai_rs::prelude::Role;
164 ContextPolicy::new("model_only", move |history| {
165 history
166 .iter()
167 .filter(|c| c.role == Some(Role::Model))
168 .cloned()
169 .collect()
170 })
171 }
172
173 pub fn head(n: usize) -> ContextPolicy {
175 ContextPolicy::new("head", move |history| {
176 history.iter().take(n).cloned().collect()
177 })
178 }
179
180 pub fn sample(n: usize) -> ContextPolicy {
182 ContextPolicy::new("sample", move |history| {
183 history
184 .iter()
185 .enumerate()
186 .filter(|(i, _)| i % n == 0)
187 .map(|(_, c)| c.clone())
188 .collect()
189 })
190 }
191
192 pub fn exclude_tools() -> ContextPolicy {
194 use gemini_genai_rs::prelude::Part;
195 ContextPolicy::new("exclude_tools", move |history| {
196 history
197 .iter()
198 .filter(|c| {
199 c.parts.iter().all(|p| {
200 !matches!(p, Part::FunctionCall { .. } | Part::FunctionResponse { .. })
201 })
202 })
203 .cloned()
204 .collect()
205 })
206 }
207
208 pub fn prepend(content: Content) -> ContextPolicy {
210 ContextPolicy::new("prepend", move |history| {
211 let mut result = vec![content.clone()];
212 result.extend(history.iter().cloned());
213 result
214 })
215 }
216
217 pub fn append(content: Content) -> ContextPolicy {
219 ContextPolicy::new("append", move |history| {
220 let mut result = history.to_vec();
221 result.push(content.clone());
222 result
223 })
224 }
225
226 pub fn text_only() -> ContextPolicy {
228 use gemini_genai_rs::prelude::Part;
229 ContextPolicy::new("text_only", move |history| {
230 history
231 .iter()
232 .filter(|c| c.parts.iter().any(|p| matches!(p, Part::Text { .. })))
233 .cloned()
234 .collect()
235 })
236 }
237
238 pub fn filter(f: impl Fn(&Content) -> bool + Send + Sync + 'static) -> ContextPolicy {
240 ContextPolicy::new("filter", move |history| {
241 history.iter().filter(|c| f(c)).cloned().collect()
242 })
243 }
244
245 pub fn map(f: impl Fn(&Content) -> Content + Send + Sync + 'static) -> ContextPolicy {
247 ContextPolicy::new("map", move |history| history.iter().map(&f).collect())
248 }
249
250 pub fn truncate(max_chars: usize) -> ContextPolicy {
252 use gemini_genai_rs::prelude::Part;
253 ContextPolicy::new("truncate", move |history| {
254 let mut total = 0;
255 let mut result = Vec::new();
256 for c in history.iter().rev() {
258 let text_len: usize = c
259 .parts
260 .iter()
261 .filter_map(|p| match p {
262 Part::Text { text } => Some(text.len()),
263 _ => None,
264 })
265 .sum();
266 if total + text_len > max_chars && !result.is_empty() {
267 break;
268 }
269 total += text_len;
270 result.push(c.clone());
271 }
272 result.reverse();
273 result
274 })
275 }
276
277 pub fn empty() -> ContextPolicy {
279 ContextPolicy::new("empty", |_| Vec::new())
280 }
281
282 pub fn from_state(keys: &[&str]) -> ContextPolicy {
295 let owned_keys: Vec<String> = keys.iter().map(std::string::ToString::to_string).collect();
296 ContextPolicy::new("from_state", move |history| {
297 let mut result = Vec::new();
302 if !owned_keys.is_empty() {
303 let key_list = owned_keys.join(", ");
304 result.push(Content::user(format!("[Context keys: {key_list}]")));
305 }
306 result.extend(history.iter().cloned());
307 result
308 })
309 }
310
311 pub fn template(tpl: &str) -> ContextPolicy {
315 let tpl = tpl.to_string();
316 ContextPolicy::new("template", move |history| {
317 let mut result = vec![Content::user(tpl.clone())];
318 result.extend(history.iter().cloned());
319 result
320 })
321 }
322
323 pub fn when(
327 predicate: impl Fn() -> bool + Send + Sync + 'static,
328 inner: ContextPolicy,
329 ) -> ContextPolicy {
330 ContextPolicy::new("when", move |history| {
331 if predicate() {
332 inner.apply(history)
333 } else {
334 history.to_vec()
335 }
336 })
337 }
338
339 pub fn redact(patterns: &[&str]) -> ContextPolicy {
341 use gemini_genai_rs::prelude::Part;
342 let patterns: Vec<String> = patterns
343 .iter()
344 .map(std::string::ToString::to_string)
345 .collect();
346 ContextPolicy::new("redact", move |history| {
347 history
348 .iter()
349 .map(|c| {
350 let parts: Vec<Part> = c
351 .parts
352 .iter()
353 .map(|p| match p {
354 Part::Text { text } => {
355 let mut redacted = text.clone();
356 for pattern in &patterns {
357 redacted = redacted.replace(pattern.as_str(), "[REDACTED]");
358 }
359 Part::Text { text: redacted }
360 }
361 other => other.clone(),
362 })
363 .collect();
364 Content {
365 role: c.role,
366 parts,
367 }
368 })
369 .collect()
370 })
371 }
372
373 pub fn summarize(prompt: &str) -> ContextPolicy {
386 let prompt = prompt.to_string();
387 ContextPolicy::new("summarize", move |history| {
388 let mut result = vec![Content::user(format!("[Summarize context: {prompt}]"))];
389 result.extend(history.iter().cloned());
390 result
391 })
392 }
393
394 pub fn relevant(query_key: &str) -> ContextPolicy {
406 let key = query_key.to_string();
407 ContextPolicy::new("relevant", move |history| {
408 let mut result = vec![Content::user(format!(
409 "[Filter context relevant to state key: {key}]"
410 ))];
411 result.extend(history.iter().cloned());
412 result
413 })
414 }
415
416 pub fn extract(keys: &[&str]) -> ContextPolicy {
428 let owned_keys: Vec<String> = keys.iter().map(std::string::ToString::to_string).collect();
429 ContextPolicy::new("extract", move |history| {
430 let mut result = vec![Content::user(format!(
431 "[Extract from context: {}]",
432 owned_keys.join(", ")
433 ))];
434 result.extend(history.iter().cloned());
435 result
436 })
437 }
438
439 pub fn distill(instruction: &str) -> ContextPolicy {
451 let instruction = instruction.to_string();
452 ContextPolicy::new("distill", move |history| {
453 let mut result = vec![Content::user(format!("[Distill context: {instruction}]"))];
454 result.extend(history.iter().cloned());
455 result
456 })
457 }
458
459 pub fn priority(weights: &[(&str, f64)]) -> ContextPolicy {
472 let owned_weights: Vec<(String, f64)> =
473 weights.iter().map(|(k, v)| (k.to_string(), *v)).collect();
474 ContextPolicy::new("priority", move |history| {
475 let weight_str = owned_weights
476 .iter()
477 .map(|(k, v)| format!("{k}={v}"))
478 .collect::<Vec<_>>()
479 .join(", ");
480 let mut result = vec![Content::user(format!("[Priority weights: {weight_str}]"))];
481 result.extend(history.iter().cloned());
482 result
483 })
484 }
485
486 pub fn fit(max_tokens: usize) -> ContextPolicy {
499 use gemini_genai_rs::prelude::Part;
500 let max_chars = max_tokens * 4; ContextPolicy::new("fit", move |history| {
502 let mut total = 0;
503 let mut result = Vec::new();
504 for c in history.iter().rev() {
506 let text_len: usize = c
507 .parts
508 .iter()
509 .filter_map(|p| match p {
510 Part::Text { text } => Some(text.len()),
511 _ => None,
512 })
513 .sum();
514 if total + text_len > max_chars && !result.is_empty() {
515 result.push(Content::user(format!(
517 "[Context truncated to fit ~{} token budget; {} earlier messages omitted]",
518 max_tokens,
519 history.len() - result.len()
520 )));
521 break;
522 }
523 total += text_len;
524 result.push(c.clone());
525 }
526 result.reverse();
527 result
528 })
529 }
530
531 pub fn project(fields: &[&str]) -> ContextPolicy {
543 let owned_fields: Vec<String> = fields
544 .iter()
545 .map(std::string::ToString::to_string)
546 .collect();
547 ContextPolicy::new("project", move |history| {
548 let mut result = vec![Content::user(format!(
549 "[Project fields: {}]",
550 owned_fields.join(", ")
551 ))];
552 result.extend(history.iter().cloned());
553 result
554 })
555 }
556
557 pub fn select(predicate: impl Fn(&Content) -> bool + Send + Sync + 'static) -> ContextPolicy {
562 ContextPolicy::new("select", move |history| {
563 history.iter().filter(|c| predicate(c)).cloned().collect()
564 })
565 }
566
567 pub fn from_agents(names: &[&str]) -> ContextPolicy {
579 let owned_names: Vec<String> = names.iter().map(std::string::ToString::to_string).collect();
580 ContextPolicy::new("from_agents", move |history| {
581 history
582 .iter()
583 .filter(|c| {
584 c.parts.iter().any(|p| match p {
585 gemini_genai_rs::prelude::Part::Text { text } => owned_names
586 .iter()
587 .any(|name| text.contains(&format!("[Agent: {name}]"))),
588 _ => false,
589 })
590 })
591 .cloned()
592 .collect()
593 })
594 }
595
596 pub fn exclude_agents(names: &[&str]) -> ContextPolicy {
608 let owned_names: Vec<String> = names.iter().map(std::string::ToString::to_string).collect();
609 ContextPolicy::new("exclude_agents", move |history| {
610 history
611 .iter()
612 .filter(|c| {
613 !c.parts.iter().any(|p| match p {
614 gemini_genai_rs::prelude::Part::Text { text } => owned_names
615 .iter()
616 .any(|name| text.contains(&format!("[Agent: {name}]"))),
617 _ => false,
618 })
619 })
620 .cloned()
621 .collect()
622 })
623 }
624
625 pub fn notes(key: &str) -> ContextPolicy {
637 let key = key.to_string();
638 ContextPolicy::new("notes", move |history| {
639 let mut result = vec![Content::user(format!("[Scratchpad from state key: {key}]"))];
640 result.extend(history.iter().cloned());
641 result
642 })
643 }
644
645 pub fn pipeline_aware() -> ContextPolicy {
658 ContextPolicy::new("pipeline_aware", |history| {
659 let mut result = vec![Content::user(
660 "[Pipeline-aware: adapt context to pipeline position]".to_string(),
661 )];
662 result.extend(history.iter().cloned());
663 result
664 })
665 }
666
667 pub fn dedup() -> ContextPolicy {
669 use gemini_genai_rs::prelude::Part;
670 ContextPolicy::new("dedup", |history| {
671 fn extract_text(c: &Content) -> String {
672 c.parts
673 .iter()
674 .filter_map(|p| match p {
675 Part::Text { text } => Some(text.as_str()),
676 _ => None,
677 })
678 .collect()
679 }
680 let mut result: Vec<Content> = Vec::new();
681 for c in history {
682 let dominated = result.last().is_some_and(|prev| {
683 let prev_text = extract_text(prev);
684 let curr_text = extract_text(c);
685 prev_text == curr_text && !prev_text.is_empty()
686 });
687 if !dominated {
688 result.push(c.clone());
689 }
690 }
691 result
692 })
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use gemini_genai_rs::prelude::Content;
700
701 #[test]
702 fn window_limits_messages() {
703 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
704 let result = C::window(2).apply(&history);
705 assert_eq!(result.len(), 2);
706 }
707
708 #[test]
709 fn window_keeps_all_if_under_limit() {
710 let history = vec![Content::user("a")];
711 let result = C::window(5).apply(&history);
712 assert_eq!(result.len(), 1);
713 }
714
715 #[test]
716 fn user_only_filters() {
717 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
718 let result = C::user_only().apply(&history);
719 assert_eq!(result.len(), 2);
720 }
721
722 #[test]
723 fn compose_with_add() {
724 let chain = C::window(10) >> C::user_only();
725 assert_eq!(chain.policies.len(), 2);
726 }
727
728 #[test]
729 fn chain_extends_with_add() {
730 let chain = C::window(10) >> C::user_only() >> C::custom(<[Content]>::to_vec);
731 assert_eq!(chain.policies.len(), 3);
732 }
733
734 #[test]
735 fn model_only_filters() {
736 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
737 let result = C::model_only().apply(&history);
738 assert_eq!(result.len(), 1);
739 }
740
741 #[test]
742 fn head_keeps_first_n() {
743 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
744 let result = C::head(2).apply(&history);
745 assert_eq!(result.len(), 2);
746 }
747
748 #[test]
749 fn sample_every_nth() {
750 let history = vec![
751 Content::user("a"),
752 Content::model("b"),
753 Content::user("c"),
754 Content::model("d"),
755 ];
756 let result = C::sample(2).apply(&history);
757 assert_eq!(result.len(), 2);
758 }
759
760 #[test]
761 fn empty_returns_nothing() {
762 let history = vec![Content::user("a"), Content::model("b")];
763 let result = C::empty().apply(&history);
764 assert!(result.is_empty());
765 }
766
767 #[test]
768 fn last_is_alias_for_window() {
769 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
770 let result = C::window(1).apply(&history);
771 assert_eq!(result.len(), 1);
772 }
773
774 #[test]
775 fn text_only_filters_non_text() {
776 let history = vec![Content::user("text msg")];
777 let result = C::text_only().apply(&history);
778 assert_eq!(result.len(), 1);
779 }
780
781 #[test]
782 fn filter_with_predicate() {
783 use gemini_genai_rs::prelude::Part;
784 let history = vec![
785 Content::user("keep"),
786 Content::user("skip"),
787 Content::user("keep this too"),
788 ];
789 let result = C::filter(|c| {
790 c.parts.iter().any(|p| match p {
791 Part::Text { text } => text.contains("keep"),
792 _ => false,
793 })
794 })
795 .apply(&history);
796 assert_eq!(result.len(), 2);
797 }
798
799 #[test]
800 fn dedup_removes_adjacent_duplicates() {
801 let history = vec![
802 Content::user("hello"),
803 Content::user("hello"),
804 Content::user("world"),
805 Content::user("world"),
806 Content::user("world"),
807 ];
808 let result = C::dedup().apply(&history);
809 assert_eq!(result.len(), 2);
810 }
811
812 #[test]
813 fn prepend_adds_to_front() {
814 let history = vec![Content::user("existing")];
815 let result = C::prepend(Content::model("system")).apply(&history);
816 assert_eq!(result.len(), 2);
817 }
818
819 #[test]
820 fn append_adds_to_back() {
821 let history = vec![Content::user("existing")];
822 let result = C::append(Content::model("suffix")).apply(&history);
823 assert_eq!(result.len(), 2);
824 }
825
826 #[test]
827 fn from_state_prepends_context() {
828 let history = vec![Content::user("hello")];
829 let result = C::from_state(&["user:name", "app:balance"]).apply(&history);
830 assert_eq!(result.len(), 2);
831 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
833 assert!(text.contains("user:name"));
834 assert!(text.contains("app:balance"));
835 } else {
836 panic!("Expected text part");
837 }
838 }
839
840 #[test]
841 fn summarize_prepends_marker() {
842 let history = vec![Content::user("hello"), Content::model("hi")];
843 let result = C::summarize("Focus on action items").apply(&history);
844 assert_eq!(result.len(), 3);
845 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
846 assert!(text.contains("Summarize context"));
847 assert!(text.contains("action items"));
848 } else {
849 panic!("Expected text part");
850 }
851 }
852
853 #[test]
854 fn relevant_prepends_key_marker() {
855 let history = vec![Content::user("hello")];
856 let result = C::relevant("user:topic").apply(&history);
857 assert_eq!(result.len(), 2);
858 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
859 assert!(text.contains("user:topic"));
860 } else {
861 panic!("Expected text part");
862 }
863 }
864
865 #[test]
866 fn extract_prepends_keys_marker() {
867 let history = vec![Content::user("hello")];
868 let result = C::extract(&["name", "order_id"]).apply(&history);
869 assert_eq!(result.len(), 2);
870 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
871 assert!(text.contains("name"));
872 assert!(text.contains("order_id"));
873 } else {
874 panic!("Expected text part");
875 }
876 }
877
878 #[test]
879 fn distill_prepends_instruction_marker() {
880 let history = vec![Content::user("hello")];
881 let result = C::distill("Keep only decisions").apply(&history);
882 assert_eq!(result.len(), 2);
883 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
884 assert!(text.contains("Distill context"));
885 assert!(text.contains("decisions"));
886 } else {
887 panic!("Expected text part");
888 }
889 }
890
891 #[test]
892 fn priority_prepends_weights_marker() {
893 let history = vec![Content::user("hello")];
894 let result = C::priority(&[("user", 1.0), ("model", 0.5)]).apply(&history);
895 assert_eq!(result.len(), 2);
896 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
897 assert!(text.contains("Priority weights"));
898 assert!(text.contains("user=1"));
899 assert!(text.contains("model=0.5"));
900 } else {
901 panic!("Expected text part");
902 }
903 }
904
905 #[test]
906 fn fit_truncates_with_marker() {
907 let long_msg = "a".repeat(500);
909 let history = vec![
910 Content::user(long_msg.clone()),
911 Content::user(long_msg.clone()),
912 Content::user("recent"),
913 ];
914 let result = C::fit(10).apply(&history);
916 assert!(result.len() <= 3);
918 let has_marker = result.iter().any(|c| {
920 c.parts.iter().any(|p| match p {
921 gemini_genai_rs::prelude::Part::Text { text } => text.contains("truncated"),
922 _ => false,
923 })
924 });
925 assert!(has_marker);
926 }
927
928 #[test]
929 fn fit_keeps_all_when_under_budget() {
930 let history = vec![Content::user("hi"), Content::model("hello")];
931 let result = C::fit(1000).apply(&history);
932 assert_eq!(result.len(), 2);
933 }
934
935 #[test]
936 fn project_prepends_fields_marker() {
937 let history = vec![Content::user("hello")];
938 let result = C::project(&["name", "status"]).apply(&history);
939 assert_eq!(result.len(), 2);
940 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
941 assert!(text.contains("Project fields"));
942 assert!(text.contains("name"));
943 assert!(text.contains("status"));
944 } else {
945 panic!("Expected text part");
946 }
947 }
948
949 #[test]
950 fn select_filters_matching() {
951 use gemini_genai_rs::prelude::Role;
952 let history = vec![
953 Content::user("keep"),
954 Content::model("skip"),
955 Content::user("also keep"),
956 ];
957 let result = C::select(|c| c.role == Some(Role::User)).apply(&history);
958 assert_eq!(result.len(), 2);
959 }
960
961 #[test]
962 fn from_agents_filters_by_agent_marker() {
963 let history = vec![
964 Content::user("[Agent: researcher] Found data"),
965 Content::user("[Agent: logger] Debug info"),
966 Content::user("[Agent: researcher] More data"),
967 ];
968 let result = C::from_agents(&["researcher"]).apply(&history);
969 assert_eq!(result.len(), 2);
970 }
971
972 #[test]
973 fn exclude_agents_removes_agent_messages() {
974 let history = vec![
975 Content::user("[Agent: researcher] Found data"),
976 Content::user("[Agent: logger] Debug info"),
977 Content::user("[Agent: researcher] More data"),
978 ];
979 let result = C::exclude_agents(&["logger"]).apply(&history);
980 assert_eq!(result.len(), 2);
981 }
982
983 #[test]
984 fn notes_prepends_scratchpad_marker() {
985 let history = vec![Content::user("hello")];
986 let result = C::notes("session:scratchpad").apply(&history);
987 assert_eq!(result.len(), 2);
988 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
989 assert!(text.contains("Scratchpad"));
990 assert!(text.contains("session:scratchpad"));
991 } else {
992 panic!("Expected text part");
993 }
994 }
995
996 #[test]
997 fn pipeline_aware_prepends_marker() {
998 let history = vec![Content::user("hello")];
999 let result = C::pipeline_aware().apply(&history);
1000 assert_eq!(result.len(), 2);
1001 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
1002 assert!(text.contains("Pipeline-aware"));
1003 } else {
1004 panic!("Expected text part");
1005 }
1006 }
1007}