gemini_adk_fluent_rs/compose/
context.rs1use std::sync::Arc;
6
7use gemini_genai_rs::prelude::Content;
8
9#[derive(Clone)]
11pub struct ContextPolicy {
12 name: &'static str,
13 #[allow(clippy::type_complexity)]
14 filter: Arc<dyn Fn(&[Content]) -> Vec<Content> + Send + Sync>,
15}
16
17impl ContextPolicy {
18 fn new(
19 name: &'static str,
20 f: impl Fn(&[Content]) -> Vec<Content> + Send + Sync + 'static,
21 ) -> Self {
22 Self {
23 name,
24 filter: Arc::new(f),
25 }
26 }
27
28 pub fn apply(&self, history: &[Content]) -> Vec<Content> {
30 (self.filter)(history)
31 }
32
33 pub fn name(&self) -> &str {
35 self.name
36 }
37}
38
39impl std::fmt::Debug for ContextPolicy {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("ContextPolicy")
42 .field("name", &self.name)
43 .finish()
44 }
45}
46
47impl std::ops::Add for ContextPolicy {
50 type Output = ContextPolicyChain;
51
52 fn add(self, rhs: ContextPolicy) -> Self::Output {
53 ContextPolicyChain {
54 policies: vec![self, rhs],
55 }
56 }
57}
58
59#[derive(Clone)]
61pub struct ContextPolicyChain {
62 pub policies: Vec<ContextPolicy>,
64}
65
66impl ContextPolicyChain {
67 pub fn apply(&self, history: &[Content]) -> Vec<Content> {
69 let mut result = Vec::new();
70 for policy in &self.policies {
71 let filtered = policy.apply(history);
72 result.extend(filtered);
74 }
75 result
76 }
77}
78
79impl std::ops::Add<ContextPolicy> for ContextPolicyChain {
80 type Output = ContextPolicyChain;
81
82 fn add(mut self, rhs: ContextPolicy) -> Self::Output {
83 self.policies.push(rhs);
84 self
85 }
86}
87
88pub struct C;
90
91impl C {
92 pub fn window(n: usize) -> ContextPolicy {
94 ContextPolicy::new("window", move |history| {
95 if history.len() > n {
96 history[history.len() - n..].to_vec()
97 } else {
98 history.to_vec()
99 }
100 })
101 }
102
103 pub fn user_only() -> ContextPolicy {
105 use gemini_genai_rs::prelude::Role;
106 ContextPolicy::new("user_only", move |history| {
107 history
108 .iter()
109 .filter(|c| c.role == Some(Role::User))
110 .cloned()
111 .collect()
112 })
113 }
114
115 pub fn custom(f: impl Fn(&[Content]) -> Vec<Content> + Send + Sync + 'static) -> ContextPolicy {
117 ContextPolicy::new("custom", f)
118 }
119
120 pub fn model_only() -> ContextPolicy {
122 use gemini_genai_rs::prelude::Role;
123 ContextPolicy::new("model_only", move |history| {
124 history
125 .iter()
126 .filter(|c| c.role == Some(Role::Model))
127 .cloned()
128 .collect()
129 })
130 }
131
132 pub fn head(n: usize) -> ContextPolicy {
134 ContextPolicy::new("head", move |history| {
135 history.iter().take(n).cloned().collect()
136 })
137 }
138
139 pub fn sample(n: usize) -> ContextPolicy {
141 ContextPolicy::new("sample", move |history| {
142 history
143 .iter()
144 .enumerate()
145 .filter(|(i, _)| i % n == 0)
146 .map(|(_, c)| c.clone())
147 .collect()
148 })
149 }
150
151 pub fn exclude_tools() -> ContextPolicy {
153 use gemini_genai_rs::prelude::Part;
154 ContextPolicy::new("exclude_tools", move |history| {
155 history
156 .iter()
157 .filter(|c| {
158 c.parts.iter().all(|p| {
159 !matches!(p, Part::FunctionCall { .. } | Part::FunctionResponse { .. })
160 })
161 })
162 .cloned()
163 .collect()
164 })
165 }
166
167 pub fn prepend(content: Content) -> ContextPolicy {
169 ContextPolicy::new("prepend", move |history| {
170 let mut result = vec![content.clone()];
171 result.extend(history.iter().cloned());
172 result
173 })
174 }
175
176 pub fn append(content: Content) -> ContextPolicy {
178 ContextPolicy::new("append", move |history| {
179 let mut result = history.to_vec();
180 result.push(content.clone());
181 result
182 })
183 }
184
185 pub fn text_only() -> ContextPolicy {
187 use gemini_genai_rs::prelude::Part;
188 ContextPolicy::new("text_only", move |history| {
189 history
190 .iter()
191 .filter(|c| c.parts.iter().any(|p| matches!(p, Part::Text { .. })))
192 .cloned()
193 .collect()
194 })
195 }
196
197 pub fn filter(f: impl Fn(&Content) -> bool + Send + Sync + 'static) -> ContextPolicy {
199 ContextPolicy::new("filter", move |history| {
200 history.iter().filter(|c| f(c)).cloned().collect()
201 })
202 }
203
204 pub fn map(f: impl Fn(&Content) -> Content + Send + Sync + 'static) -> ContextPolicy {
206 ContextPolicy::new("map", move |history| history.iter().map(&f).collect())
207 }
208
209 pub fn truncate(max_chars: usize) -> ContextPolicy {
211 use gemini_genai_rs::prelude::Part;
212 ContextPolicy::new("truncate", move |history| {
213 let mut total = 0;
214 let mut result = Vec::new();
215 for c in history.iter().rev() {
217 let text_len: usize = c
218 .parts
219 .iter()
220 .filter_map(|p| match p {
221 Part::Text { text } => Some(text.len()),
222 _ => None,
223 })
224 .sum();
225 if total + text_len > max_chars && !result.is_empty() {
226 break;
227 }
228 total += text_len;
229 result.push(c.clone());
230 }
231 result.reverse();
232 result
233 })
234 }
235
236 pub fn last(n: usize) -> ContextPolicy {
239 Self::window(n)
240 }
241
242 pub fn empty() -> ContextPolicy {
244 ContextPolicy::new("empty", |_| Vec::new())
245 }
246
247 pub fn from_state(keys: &[&str]) -> ContextPolicy {
258 let owned_keys: Vec<String> = keys.iter().map(|k| k.to_string()).collect();
259 ContextPolicy::new("from_state", move |history| {
260 let mut result = Vec::new();
265 if !owned_keys.is_empty() {
266 let key_list = owned_keys.join(", ");
267 result.push(Content::user(format!("[Context keys: {}]", key_list)));
268 }
269 result.extend(history.iter().cloned());
270 result
271 })
272 }
273
274 pub fn none() -> ContextPolicy {
276 Self::empty()
277 }
278
279 pub fn recent(n: usize) -> ContextPolicy {
281 Self::window(n)
282 }
283
284 pub fn template(tpl: &str) -> ContextPolicy {
288 let tpl = tpl.to_string();
289 ContextPolicy::new("template", move |history| {
290 let mut result = vec![Content::user(tpl.clone())];
291 result.extend(history.iter().cloned());
292 result
293 })
294 }
295
296 pub fn when(
300 predicate: impl Fn() -> bool + Send + Sync + 'static,
301 inner: ContextPolicy,
302 ) -> ContextPolicy {
303 ContextPolicy::new("when", move |history| {
304 if predicate() {
305 inner.apply(history)
306 } else {
307 history.to_vec()
308 }
309 })
310 }
311
312 pub fn rolling(n: usize) -> ContextPolicy {
314 Self::window(n)
315 }
316
317 pub fn compact() -> ContextPolicy {
319 Self::exclude_tools()
320 }
321
322 pub fn budget(max_tokens: usize) -> ContextPolicy {
326 Self::truncate(max_tokens * 4)
327 }
328
329 pub fn fresh(max_entries: usize) -> ContextPolicy {
331 Self::window(max_entries)
332 }
333
334 pub fn redact(patterns: &[&str]) -> ContextPolicy {
336 use gemini_genai_rs::prelude::Part;
337 let patterns: Vec<String> = patterns.iter().map(|p| p.to_string()).collect();
338 ContextPolicy::new("redact", move |history| {
339 history
340 .iter()
341 .map(|c| {
342 let parts: Vec<Part> = c
343 .parts
344 .iter()
345 .map(|p| match p {
346 Part::Text { text } => {
347 let mut redacted = text.clone();
348 for pattern in &patterns {
349 redacted = redacted.replace(pattern.as_str(), "[REDACTED]");
350 }
351 Part::Text { text: redacted }
352 }
353 other => other.clone(),
354 })
355 .collect();
356 Content {
357 role: c.role,
358 parts,
359 }
360 })
361 .collect()
362 })
363 }
364
365 pub fn summarize(prompt: &str) -> ContextPolicy {
376 let prompt = prompt.to_string();
377 ContextPolicy::new("summarize", move |history| {
378 let mut result = vec![Content::user(format!("[Summarize context: {}]", prompt))];
379 result.extend(history.iter().cloned());
380 result
381 })
382 }
383
384 pub fn relevant(query_key: &str) -> ContextPolicy {
394 let key = query_key.to_string();
395 ContextPolicy::new("relevant", move |history| {
396 let mut result = vec![Content::user(format!(
397 "[Filter context relevant to state key: {}]",
398 key
399 ))];
400 result.extend(history.iter().cloned());
401 result
402 })
403 }
404
405 pub fn extract(keys: &[&str]) -> ContextPolicy {
415 let owned_keys: Vec<String> = keys.iter().map(|k| k.to_string()).collect();
416 ContextPolicy::new("extract", move |history| {
417 let mut result = vec![Content::user(format!(
418 "[Extract from context: {}]",
419 owned_keys.join(", ")
420 ))];
421 result.extend(history.iter().cloned());
422 result
423 })
424 }
425
426 pub fn distill(instruction: &str) -> ContextPolicy {
436 let instruction = instruction.to_string();
437 ContextPolicy::new("distill", move |history| {
438 let mut result = vec![Content::user(format!("[Distill context: {}]", instruction))];
439 result.extend(history.iter().cloned());
440 result
441 })
442 }
443
444 pub fn priority(weights: &[(&str, f64)]) -> ContextPolicy {
455 let owned_weights: Vec<(String, f64)> =
456 weights.iter().map(|(k, v)| (k.to_string(), *v)).collect();
457 ContextPolicy::new("priority", move |history| {
458 let weight_str = owned_weights
459 .iter()
460 .map(|(k, v)| format!("{}={}", k, v))
461 .collect::<Vec<_>>()
462 .join(", ");
463 let mut result = vec![Content::user(format!("[Priority weights: {}]", weight_str))];
464 result.extend(history.iter().cloned());
465 result
466 })
467 }
468
469 pub fn fit(max_tokens: usize) -> ContextPolicy {
480 use gemini_genai_rs::prelude::Part;
481 let max_chars = max_tokens * 4; ContextPolicy::new("fit", move |history| {
483 let mut total = 0;
484 let mut result = Vec::new();
485 for c in history.iter().rev() {
487 let text_len: usize = c
488 .parts
489 .iter()
490 .filter_map(|p| match p {
491 Part::Text { text } => Some(text.len()),
492 _ => None,
493 })
494 .sum();
495 if total + text_len > max_chars && !result.is_empty() {
496 result.push(Content::user(format!(
498 "[Context truncated to fit ~{} token budget; {} earlier messages omitted]",
499 max_tokens,
500 history.len() - result.len()
501 )));
502 break;
503 }
504 total += text_len;
505 result.push(c.clone());
506 }
507 result.reverse();
508 result
509 })
510 }
511
512 pub fn project(fields: &[&str]) -> ContextPolicy {
522 let owned_fields: Vec<String> = fields.iter().map(|f| f.to_string()).collect();
523 ContextPolicy::new("project", move |history| {
524 let mut result = vec![Content::user(format!(
525 "[Project fields: {}]",
526 owned_fields.join(", ")
527 ))];
528 result.extend(history.iter().cloned());
529 result
530 })
531 }
532
533 pub fn select(predicate: impl Fn(&Content) -> bool + Send + Sync + 'static) -> ContextPolicy {
538 ContextPolicy::new("select", move |history| {
539 history.iter().filter(|c| predicate(c)).cloned().collect()
540 })
541 }
542
543 pub fn from_agents(names: &[&str]) -> ContextPolicy {
553 let owned_names: Vec<String> = names.iter().map(|n| n.to_string()).collect();
554 ContextPolicy::new("from_agents", move |history| {
555 history
556 .iter()
557 .filter(|c| {
558 c.parts.iter().any(|p| match p {
559 gemini_genai_rs::prelude::Part::Text { text } => owned_names
560 .iter()
561 .any(|name| text.contains(&format!("[Agent: {}]", name))),
562 _ => false,
563 })
564 })
565 .cloned()
566 .collect()
567 })
568 }
569
570 pub fn exclude_agents(names: &[&str]) -> ContextPolicy {
580 let owned_names: Vec<String> = names.iter().map(|n| n.to_string()).collect();
581 ContextPolicy::new("exclude_agents", move |history| {
582 history
583 .iter()
584 .filter(|c| {
585 !c.parts.iter().any(|p| match p {
586 gemini_genai_rs::prelude::Part::Text { text } => owned_names
587 .iter()
588 .any(|name| text.contains(&format!("[Agent: {}]", name))),
589 _ => false,
590 })
591 })
592 .cloned()
593 .collect()
594 })
595 }
596
597 pub fn notes(key: &str) -> ContextPolicy {
607 let key = key.to_string();
608 ContextPolicy::new("notes", move |history| {
609 let mut result = vec![Content::user(format!(
610 "[Scratchpad from state key: {}]",
611 key
612 ))];
613 result.extend(history.iter().cloned());
614 result
615 })
616 }
617
618 pub fn pipeline_aware() -> ContextPolicy {
629 ContextPolicy::new("pipeline_aware", |history| {
630 let mut result = vec![Content::user(
631 "[Pipeline-aware: adapt context to pipeline position]".to_string(),
632 )];
633 result.extend(history.iter().cloned());
634 result
635 })
636 }
637
638 pub fn dedup() -> ContextPolicy {
640 use gemini_genai_rs::prelude::Part;
641 ContextPolicy::new("dedup", |history| {
642 fn extract_text(c: &Content) -> String {
643 c.parts
644 .iter()
645 .filter_map(|p| match p {
646 Part::Text { text } => Some(text.as_str()),
647 _ => None,
648 })
649 .collect()
650 }
651 let mut result: Vec<Content> = Vec::new();
652 for c in history {
653 let dominated = result.last().is_some_and(|prev| {
654 let prev_text = extract_text(prev);
655 let curr_text = extract_text(c);
656 prev_text == curr_text && !prev_text.is_empty()
657 });
658 if !dominated {
659 result.push(c.clone());
660 }
661 }
662 result
663 })
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670 use gemini_genai_rs::prelude::Content;
671
672 #[test]
673 fn window_limits_messages() {
674 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
675 let result = C::window(2).apply(&history);
676 assert_eq!(result.len(), 2);
677 }
678
679 #[test]
680 fn window_keeps_all_if_under_limit() {
681 let history = vec![Content::user("a")];
682 let result = C::window(5).apply(&history);
683 assert_eq!(result.len(), 1);
684 }
685
686 #[test]
687 fn user_only_filters() {
688 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
689 let result = C::user_only().apply(&history);
690 assert_eq!(result.len(), 2);
691 }
692
693 #[test]
694 fn compose_with_add() {
695 let chain = C::window(10) + C::user_only();
696 assert_eq!(chain.policies.len(), 2);
697 }
698
699 #[test]
700 fn chain_extends_with_add() {
701 let chain = C::window(10) + C::user_only() + C::custom(|h| h.to_vec());
702 assert_eq!(chain.policies.len(), 3);
703 }
704
705 #[test]
706 fn model_only_filters() {
707 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
708 let result = C::model_only().apply(&history);
709 assert_eq!(result.len(), 1);
710 }
711
712 #[test]
713 fn head_keeps_first_n() {
714 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
715 let result = C::head(2).apply(&history);
716 assert_eq!(result.len(), 2);
717 }
718
719 #[test]
720 fn sample_every_nth() {
721 let history = vec![
722 Content::user("a"),
723 Content::model("b"),
724 Content::user("c"),
725 Content::model("d"),
726 ];
727 let result = C::sample(2).apply(&history);
728 assert_eq!(result.len(), 2);
729 }
730
731 #[test]
732 fn empty_returns_nothing() {
733 let history = vec![Content::user("a"), Content::model("b")];
734 let result = C::empty().apply(&history);
735 assert!(result.is_empty());
736 }
737
738 #[test]
739 fn last_is_alias_for_window() {
740 let history = vec![Content::user("a"), Content::model("b"), Content::user("c")];
741 let result = C::last(1).apply(&history);
742 assert_eq!(result.len(), 1);
743 }
744
745 #[test]
746 fn text_only_filters_non_text() {
747 let history = vec![Content::user("text msg")];
748 let result = C::text_only().apply(&history);
749 assert_eq!(result.len(), 1);
750 }
751
752 #[test]
753 fn filter_with_predicate() {
754 use gemini_genai_rs::prelude::Part;
755 let history = vec![
756 Content::user("keep"),
757 Content::user("skip"),
758 Content::user("keep this too"),
759 ];
760 let result = C::filter(|c| {
761 c.parts.iter().any(|p| match p {
762 Part::Text { text } => text.contains("keep"),
763 _ => false,
764 })
765 })
766 .apply(&history);
767 assert_eq!(result.len(), 2);
768 }
769
770 #[test]
771 fn dedup_removes_adjacent_duplicates() {
772 let history = vec![
773 Content::user("hello"),
774 Content::user("hello"),
775 Content::user("world"),
776 Content::user("world"),
777 Content::user("world"),
778 ];
779 let result = C::dedup().apply(&history);
780 assert_eq!(result.len(), 2);
781 }
782
783 #[test]
784 fn prepend_adds_to_front() {
785 let history = vec![Content::user("existing")];
786 let result = C::prepend(Content::model("system")).apply(&history);
787 assert_eq!(result.len(), 2);
788 }
789
790 #[test]
791 fn append_adds_to_back() {
792 let history = vec![Content::user("existing")];
793 let result = C::append(Content::model("suffix")).apply(&history);
794 assert_eq!(result.len(), 2);
795 }
796
797 #[test]
798 fn from_state_prepends_context() {
799 let history = vec![Content::user("hello")];
800 let result = C::from_state(&["user:name", "app:balance"]).apply(&history);
801 assert_eq!(result.len(), 2);
802 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
804 assert!(text.contains("user:name"));
805 assert!(text.contains("app:balance"));
806 } else {
807 panic!("Expected text part");
808 }
809 }
810
811 #[test]
812 fn summarize_prepends_marker() {
813 let history = vec![Content::user("hello"), Content::model("hi")];
814 let result = C::summarize("Focus on action items").apply(&history);
815 assert_eq!(result.len(), 3);
816 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
817 assert!(text.contains("Summarize context"));
818 assert!(text.contains("action items"));
819 } else {
820 panic!("Expected text part");
821 }
822 }
823
824 #[test]
825 fn relevant_prepends_key_marker() {
826 let history = vec![Content::user("hello")];
827 let result = C::relevant("user:topic").apply(&history);
828 assert_eq!(result.len(), 2);
829 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
830 assert!(text.contains("user:topic"));
831 } else {
832 panic!("Expected text part");
833 }
834 }
835
836 #[test]
837 fn extract_prepends_keys_marker() {
838 let history = vec![Content::user("hello")];
839 let result = C::extract(&["name", "order_id"]).apply(&history);
840 assert_eq!(result.len(), 2);
841 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
842 assert!(text.contains("name"));
843 assert!(text.contains("order_id"));
844 } else {
845 panic!("Expected text part");
846 }
847 }
848
849 #[test]
850 fn distill_prepends_instruction_marker() {
851 let history = vec![Content::user("hello")];
852 let result = C::distill("Keep only decisions").apply(&history);
853 assert_eq!(result.len(), 2);
854 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
855 assert!(text.contains("Distill context"));
856 assert!(text.contains("decisions"));
857 } else {
858 panic!("Expected text part");
859 }
860 }
861
862 #[test]
863 fn priority_prepends_weights_marker() {
864 let history = vec![Content::user("hello")];
865 let result = C::priority(&[("user", 1.0), ("model", 0.5)]).apply(&history);
866 assert_eq!(result.len(), 2);
867 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
868 assert!(text.contains("Priority weights"));
869 assert!(text.contains("user=1"));
870 assert!(text.contains("model=0.5"));
871 } else {
872 panic!("Expected text part");
873 }
874 }
875
876 #[test]
877 fn fit_truncates_with_marker() {
878 let long_msg = "a".repeat(500);
880 let history = vec![
881 Content::user(long_msg.clone()),
882 Content::user(long_msg.clone()),
883 Content::user("recent"),
884 ];
885 let result = C::fit(10).apply(&history);
887 assert!(result.len() <= 3);
889 let has_marker = result.iter().any(|c| {
891 c.parts.iter().any(|p| match p {
892 gemini_genai_rs::prelude::Part::Text { text } => text.contains("truncated"),
893 _ => false,
894 })
895 });
896 assert!(has_marker);
897 }
898
899 #[test]
900 fn fit_keeps_all_when_under_budget() {
901 let history = vec![Content::user("hi"), Content::model("hello")];
902 let result = C::fit(1000).apply(&history);
903 assert_eq!(result.len(), 2);
904 }
905
906 #[test]
907 fn project_prepends_fields_marker() {
908 let history = vec![Content::user("hello")];
909 let result = C::project(&["name", "status"]).apply(&history);
910 assert_eq!(result.len(), 2);
911 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
912 assert!(text.contains("Project fields"));
913 assert!(text.contains("name"));
914 assert!(text.contains("status"));
915 } else {
916 panic!("Expected text part");
917 }
918 }
919
920 #[test]
921 fn select_filters_matching() {
922 use gemini_genai_rs::prelude::Role;
923 let history = vec![
924 Content::user("keep"),
925 Content::model("skip"),
926 Content::user("also keep"),
927 ];
928 let result = C::select(|c| c.role == Some(Role::User)).apply(&history);
929 assert_eq!(result.len(), 2);
930 }
931
932 #[test]
933 fn from_agents_filters_by_agent_marker() {
934 let history = vec![
935 Content::user("[Agent: researcher] Found data"),
936 Content::user("[Agent: logger] Debug info"),
937 Content::user("[Agent: researcher] More data"),
938 ];
939 let result = C::from_agents(&["researcher"]).apply(&history);
940 assert_eq!(result.len(), 2);
941 }
942
943 #[test]
944 fn exclude_agents_removes_agent_messages() {
945 let history = vec![
946 Content::user("[Agent: researcher] Found data"),
947 Content::user("[Agent: logger] Debug info"),
948 Content::user("[Agent: researcher] More data"),
949 ];
950 let result = C::exclude_agents(&["logger"]).apply(&history);
951 assert_eq!(result.len(), 2);
952 }
953
954 #[test]
955 fn notes_prepends_scratchpad_marker() {
956 let history = vec![Content::user("hello")];
957 let result = C::notes("session:scratchpad").apply(&history);
958 assert_eq!(result.len(), 2);
959 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
960 assert!(text.contains("Scratchpad"));
961 assert!(text.contains("session:scratchpad"));
962 } else {
963 panic!("Expected text part");
964 }
965 }
966
967 #[test]
968 fn pipeline_aware_prepends_marker() {
969 let history = vec![Content::user("hello")];
970 let result = C::pipeline_aware().apply(&history);
971 assert_eq!(result.len(), 2);
972 if let gemini_genai_rs::prelude::Part::Text { text } = &result[0].parts[0] {
973 assert!(text.contains("Pipeline-aware"));
974 } else {
975 panic!("Expected text part");
976 }
977 }
978}