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