1pub mod assembler;
27pub mod fs_provider;
28pub mod recent_workspace_provider;
29pub mod ripgrep_provider;
30pub mod skill_catalog_provider;
31pub mod static_provider;
32
33pub use assembler::{
34 ContextAssembler, ContextAssembly, ContextAssemblyPolicy, ContextBudget, ContextSourcePolicy,
35};
36pub use fs_provider::{FileSystemContextConfig, FileSystemContextProvider};
37pub use recent_workspace_provider::RecentWorkspaceFilesContextProvider;
38pub use ripgrep_provider::{RipgrepContextConfig, RipgrepContextProvider};
39pub use skill_catalog_provider::SkillCatalogContextProvider;
40pub use static_provider::StaticContextProvider;
41
42use serde::{Deserialize, Serialize};
43use std::collections::HashMap;
44
45pub const CONTEXT_PROVENANCE_METADATA: &str = "a3s.context.provenance";
46pub const CONTEXT_PRIORITY_METADATA: &str = "a3s.context.priority";
47pub const CONTEXT_TRUST_METADATA: &str = "a3s.context.trust";
48pub const CONTEXT_FRESHNESS_METADATA: &str = "a3s.context.freshness";
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
52pub enum ContextType {
53 Memory,
55 #[default]
57 Resource,
58 Skill,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
64pub enum ContextDepth {
65 Abstract,
67 #[default]
69 Overview,
70 Full,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ContextQuery {
77 pub query: String,
79
80 #[serde(default)]
82 pub context_types: Vec<ContextType>,
83
84 #[serde(default)]
86 pub depth: ContextDepth,
87
88 #[serde(default = "default_max_results")]
90 pub max_results: usize,
91
92 #[serde(default = "default_max_tokens")]
94 pub max_tokens: usize,
95
96 #[serde(default)]
98 pub session_id: Option<String>,
99
100 #[serde(default)]
102 pub params: HashMap<String, serde_json::Value>,
103}
104
105fn default_max_results() -> usize {
106 10
107}
108
109fn default_max_tokens() -> usize {
110 4000
111}
112
113impl ContextQuery {
114 pub fn new(query: impl Into<String>) -> Self {
116 Self {
117 query: query.into(),
118 context_types: vec![ContextType::Resource],
119 depth: ContextDepth::default(),
120 max_results: default_max_results(),
121 max_tokens: default_max_tokens(),
122 session_id: None,
123 params: HashMap::new(),
124 }
125 }
126
127 pub fn with_types(mut self, types: impl IntoIterator<Item = ContextType>) -> Self {
129 self.context_types = types.into_iter().collect();
130 self
131 }
132
133 pub fn with_depth(mut self, depth: ContextDepth) -> Self {
135 self.depth = depth;
136 self
137 }
138
139 pub fn with_max_results(mut self, max: usize) -> Self {
141 self.max_results = max;
142 self
143 }
144
145 pub fn with_max_tokens(mut self, max: usize) -> Self {
147 self.max_tokens = max;
148 self
149 }
150
151 pub fn with_session_id(mut self, id: impl Into<String>) -> Self {
153 self.session_id = Some(id.into());
154 self
155 }
156
157 pub fn with_param(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
159 self.params.insert(key.into(), value);
160 self
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct ContextItem {
167 pub id: String,
169
170 pub context_type: ContextType,
172
173 pub content: String,
175
176 #[serde(default)]
178 pub token_count: usize,
179
180 #[serde(default)]
182 pub relevance: f32,
183
184 #[serde(default)]
186 pub source: Option<String>,
187
188 #[serde(default)]
190 pub metadata: HashMap<String, serde_json::Value>,
191}
192
193impl ContextItem {
194 pub fn new(
196 id: impl Into<String>,
197 context_type: ContextType,
198 content: impl Into<String>,
199 ) -> Self {
200 Self {
201 id: id.into(),
202 context_type,
203 content: content.into(),
204 token_count: 0,
205 relevance: 0.0,
206 source: None,
207 metadata: HashMap::new(),
208 }
209 }
210
211 pub fn with_token_count(mut self, count: usize) -> Self {
213 self.token_count = count;
214 self
215 }
216
217 pub fn with_relevance(mut self, score: f32) -> Self {
219 self.relevance = score.clamp(0.0, 1.0);
220 self
221 }
222
223 pub fn with_source(mut self, source: impl Into<String>) -> Self {
225 self.source = Some(source.into());
226 self
227 }
228
229 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
231 self.metadata.insert(key.into(), value);
232 self
233 }
234
235 pub fn with_provenance(mut self, provenance: impl Into<String>) -> Self {
237 self.metadata.insert(
238 CONTEXT_PROVENANCE_METADATA.to_string(),
239 serde_json::Value::String(provenance.into()),
240 );
241 self
242 }
243
244 pub fn with_priority(mut self, priority: f32) -> Self {
246 self.metadata.insert(
247 CONTEXT_PRIORITY_METADATA.to_string(),
248 serde_json::json!(priority.clamp(0.0, 1.0)),
249 );
250 self
251 }
252
253 pub fn with_trust(mut self, trust: f32) -> Self {
255 self.metadata.insert(
256 CONTEXT_TRUST_METADATA.to_string(),
257 serde_json::json!(trust.clamp(0.0, 1.0)),
258 );
259 self
260 }
261
262 pub fn with_freshness(mut self, freshness: f32) -> Self {
264 self.metadata.insert(
265 CONTEXT_FRESHNESS_METADATA.to_string(),
266 serde_json::json!(freshness.clamp(0.0, 1.0)),
267 );
268 self
269 }
270
271 pub fn provenance(&self) -> Option<&str> {
272 self.metadata
273 .get(CONTEXT_PROVENANCE_METADATA)
274 .and_then(serde_json::Value::as_str)
275 }
276
277 pub fn priority(&self) -> f32 {
278 metadata_score(self.metadata.get(CONTEXT_PRIORITY_METADATA))
279 }
280
281 pub fn trust(&self) -> f32 {
282 metadata_score(self.metadata.get(CONTEXT_TRUST_METADATA))
283 }
284
285 pub fn freshness(&self) -> f32 {
286 metadata_score(self.metadata.get(CONTEXT_FRESHNESS_METADATA))
287 }
288
289 pub fn to_xml(&self) -> String {
291 let source_attr = self
292 .source
293 .as_ref()
294 .map(|s| format!(" source=\"{}\"", s))
295 .unwrap_or_default();
296 let type_str = match self.context_type {
297 ContextType::Memory => "Memory",
298 ContextType::Resource => "Resource",
299 ContextType::Skill => "Skill",
300 };
301 format!(
302 "<context{} type=\"{}\">\n{}\n</context>",
303 source_attr, type_str, self.content
304 )
305 }
306}
307
308fn metadata_score(value: Option<&serde_json::Value>) -> f32 {
309 value
310 .and_then(serde_json::Value::as_f64)
311 .map(|score| (score as f32).clamp(0.0, 1.0))
312 .unwrap_or(0.0)
313}
314
315#[derive(Debug, Clone, Default, Serialize, Deserialize)]
317pub struct ContextResult {
318 pub items: Vec<ContextItem>,
320
321 pub total_tokens: usize,
323
324 pub provider: String,
326
327 pub truncated: bool,
329}
330
331impl ContextResult {
332 pub fn new(provider: impl Into<String>) -> Self {
334 Self {
335 items: Vec::new(),
336 total_tokens: 0,
337 provider: provider.into(),
338 truncated: false,
339 }
340 }
341
342 pub fn add_item(&mut self, item: ContextItem) {
344 self.total_tokens += item.token_count;
345 self.items.push(item);
346 }
347
348 pub fn is_empty(&self) -> bool {
350 self.items.is_empty()
351 }
352
353 pub fn to_xml(&self) -> String {
355 self.items
356 .iter()
357 .map(|item| item.to_xml())
358 .collect::<Vec<_>>()
359 .join("\n\n")
360 }
361}
362
363#[async_trait::async_trait]
365pub trait ContextProvider: Send + Sync {
366 fn name(&self) -> &str;
368
369 async fn query(&self, query: &ContextQuery) -> anyhow::Result<ContextResult>;
371
372 async fn on_turn_complete(
377 &self,
378 _session_id: &str,
379 _prompt: &str,
380 _response: &str,
381 ) -> anyhow::Result<()> {
382 Ok(())
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
395 fn test_context_type_default() {
396 let ct: ContextType = Default::default();
397 assert_eq!(ct, ContextType::Resource);
398 }
399
400 #[test]
401 fn test_context_type_serialization() {
402 let ct = ContextType::Memory;
403 let json = serde_json::to_string(&ct).unwrap();
404 assert_eq!(json, "\"Memory\"");
405
406 let parsed: ContextType = serde_json::from_str(&json).unwrap();
407 assert_eq!(parsed, ContextType::Memory);
408 }
409
410 #[test]
411 fn test_context_type_all_variants() {
412 let types = vec![
413 ContextType::Memory,
414 ContextType::Resource,
415 ContextType::Skill,
416 ];
417 for ct in types {
418 let json = serde_json::to_string(&ct).unwrap();
419 let parsed: ContextType = serde_json::from_str(&json).unwrap();
420 assert_eq!(parsed, ct);
421 }
422 }
423
424 #[test]
429 fn test_context_depth_default() {
430 let cd: ContextDepth = Default::default();
431 assert_eq!(cd, ContextDepth::Overview);
432 }
433
434 #[test]
435 fn test_context_depth_serialization() {
436 let cd = ContextDepth::Full;
437 let json = serde_json::to_string(&cd).unwrap();
438 assert_eq!(json, "\"Full\"");
439
440 let parsed: ContextDepth = serde_json::from_str(&json).unwrap();
441 assert_eq!(parsed, ContextDepth::Full);
442 }
443
444 #[test]
445 fn test_context_depth_all_variants() {
446 let depths = vec![
447 ContextDepth::Abstract,
448 ContextDepth::Overview,
449 ContextDepth::Full,
450 ];
451 for cd in depths {
452 let json = serde_json::to_string(&cd).unwrap();
453 let parsed: ContextDepth = serde_json::from_str(&json).unwrap();
454 assert_eq!(parsed, cd);
455 }
456 }
457
458 #[test]
463 fn test_context_query_new() {
464 let query = ContextQuery::new("test query");
465 assert_eq!(query.query, "test query");
466 assert_eq!(query.context_types, vec![ContextType::Resource]);
467 assert_eq!(query.depth, ContextDepth::Overview);
468 assert_eq!(query.max_results, 10);
469 assert_eq!(query.max_tokens, 4000);
470 assert!(query.session_id.is_none());
471 assert!(query.params.is_empty());
472 }
473
474 #[test]
475 fn test_context_query_builder() {
476 let query = ContextQuery::new("test")
477 .with_types([ContextType::Memory, ContextType::Skill])
478 .with_depth(ContextDepth::Full)
479 .with_max_results(5)
480 .with_max_tokens(2000)
481 .with_session_id("sess-123")
482 .with_param("custom", serde_json::json!("value"));
483
484 assert_eq!(query.context_types.len(), 2);
485 assert!(query.context_types.contains(&ContextType::Memory));
486 assert!(query.context_types.contains(&ContextType::Skill));
487 assert_eq!(query.depth, ContextDepth::Full);
488 assert_eq!(query.max_results, 5);
489 assert_eq!(query.max_tokens, 2000);
490 assert_eq!(query.session_id, Some("sess-123".to_string()));
491 assert_eq!(
492 query.params.get("custom"),
493 Some(&serde_json::json!("value"))
494 );
495 }
496
497 #[test]
498 fn test_context_query_serialization() {
499 let query = ContextQuery::new("search term")
500 .with_types([ContextType::Resource])
501 .with_session_id("sess-456");
502
503 let json = serde_json::to_string(&query).unwrap();
504 let parsed: ContextQuery = serde_json::from_str(&json).unwrap();
505
506 assert_eq!(parsed.query, "search term");
507 assert_eq!(parsed.session_id, Some("sess-456".to_string()));
508 }
509
510 #[test]
511 fn test_context_query_deserialization_with_defaults() {
512 let json = r#"{"query": "minimal query"}"#;
513 let query: ContextQuery = serde_json::from_str(json).unwrap();
514
515 assert_eq!(query.query, "minimal query");
516 assert!(query.context_types.is_empty()); assert_eq!(query.depth, ContextDepth::Overview);
518 assert_eq!(query.max_results, 10);
519 assert_eq!(query.max_tokens, 4000);
520 }
521
522 #[test]
527 fn test_context_item_new() {
528 let item = ContextItem::new("item-1", ContextType::Resource, "Some content");
529 assert_eq!(item.id, "item-1");
530 assert_eq!(item.context_type, ContextType::Resource);
531 assert_eq!(item.content, "Some content");
532 assert_eq!(item.token_count, 0);
533 assert_eq!(item.relevance, 0.0);
534 assert!(item.source.is_none());
535 assert!(item.metadata.is_empty());
536 }
537
538 #[test]
539 fn test_context_item_builder() {
540 let item = ContextItem::new("item-2", ContextType::Memory, "Memory content")
541 .with_token_count(150)
542 .with_relevance(0.85)
543 .with_source("viking://memory/session-123")
544 .with_provenance("memory")
545 .with_priority(0.7)
546 .with_trust(1.2)
547 .with_freshness(-1.0)
548 .with_metadata("key", serde_json::json!("value"));
549
550 assert_eq!(item.token_count, 150);
551 assert!((item.relevance - 0.85).abs() < f32::EPSILON);
552 assert_eq!(item.source, Some("viking://memory/session-123".to_string()));
553 assert_eq!(item.provenance(), Some("memory"));
554 assert!((item.priority() - 0.7).abs() < f32::EPSILON);
555 assert!((item.trust() - 1.0).abs() < f32::EPSILON);
556 assert!(item.freshness().abs() < f32::EPSILON);
557 assert_eq!(item.metadata.get("key"), Some(&serde_json::json!("value")));
558 }
559
560 #[test]
561 fn test_context_item_relevance_clamping() {
562 let item1 = ContextItem::new("id", ContextType::Resource, "").with_relevance(1.5);
563 assert!((item1.relevance - 1.0).abs() < f32::EPSILON);
564
565 let item2 = ContextItem::new("id", ContextType::Resource, "").with_relevance(-0.5);
566 assert!(item2.relevance.abs() < f32::EPSILON);
567 }
568
569 #[test]
570 fn test_context_item_to_xml_without_source() {
571 let item = ContextItem::new("id", ContextType::Resource, "Content here");
572 let xml = item.to_xml();
573 assert_eq!(xml, "<context type=\"Resource\">\nContent here\n</context>");
574 }
575
576 #[test]
577 fn test_context_item_to_xml_with_source() {
578 let item = ContextItem::new("id", ContextType::Memory, "Memory content")
579 .with_source("viking://docs/auth");
580 let xml = item.to_xml();
581 assert_eq!(
582 xml,
583 "<context source=\"viking://docs/auth\" type=\"Memory\">\nMemory content\n</context>"
584 );
585 }
586
587 #[test]
588 fn test_context_item_to_xml_all_types() {
589 let memory = ContextItem::new("m", ContextType::Memory, "m").to_xml();
590 assert!(memory.contains("type=\"Memory\""));
591
592 let resource = ContextItem::new("r", ContextType::Resource, "r").to_xml();
593 assert!(resource.contains("type=\"Resource\""));
594
595 let skill = ContextItem::new("s", ContextType::Skill, "s").to_xml();
596 assert!(skill.contains("type=\"Skill\""));
597 }
598
599 #[test]
600 fn test_context_item_serialization() {
601 let item = ContextItem::new("item-3", ContextType::Skill, "Skill instructions")
602 .with_token_count(200)
603 .with_relevance(0.9)
604 .with_source("viking://skills/code-review");
605
606 let json = serde_json::to_string(&item).unwrap();
607 let parsed: ContextItem = serde_json::from_str(&json).unwrap();
608
609 assert_eq!(parsed.id, "item-3");
610 assert_eq!(parsed.context_type, ContextType::Skill);
611 assert_eq!(parsed.content, "Skill instructions");
612 assert_eq!(parsed.token_count, 200);
613 }
614
615 #[test]
620 fn test_context_result_new() {
621 let result = ContextResult::new("test-provider");
622 assert!(result.items.is_empty());
623 assert_eq!(result.total_tokens, 0);
624 assert_eq!(result.provider, "test-provider");
625 assert!(!result.truncated);
626 }
627
628 #[test]
629 fn test_context_result_add_item() {
630 let mut result = ContextResult::new("provider");
631 let item = ContextItem::new("id", ContextType::Resource, "content").with_token_count(100);
632 result.add_item(item);
633
634 assert_eq!(result.items.len(), 1);
635 assert_eq!(result.total_tokens, 100);
636 }
637
638 #[test]
639 fn test_context_result_add_multiple_items() {
640 let mut result = ContextResult::new("provider");
641 result.add_item(ContextItem::new("1", ContextType::Resource, "a").with_token_count(50));
642 result.add_item(ContextItem::new("2", ContextType::Memory, "b").with_token_count(75));
643 result.add_item(ContextItem::new("3", ContextType::Skill, "c").with_token_count(25));
644
645 assert_eq!(result.items.len(), 3);
646 assert_eq!(result.total_tokens, 150);
647 }
648
649 #[test]
650 fn test_context_result_is_empty() {
651 let empty = ContextResult::new("provider");
652 assert!(empty.is_empty());
653
654 let mut non_empty = ContextResult::new("provider");
655 non_empty.add_item(ContextItem::new("id", ContextType::Resource, "content"));
656 assert!(!non_empty.is_empty());
657 }
658
659 #[test]
660 fn test_context_result_to_xml() {
661 let mut result = ContextResult::new("provider");
662 result.add_item(
663 ContextItem::new("1", ContextType::Resource, "First content").with_source("source://1"),
664 );
665 result.add_item(ContextItem::new("2", ContextType::Memory, "Second content"));
666
667 let xml = result.to_xml();
668 assert!(xml.contains("<context source=\"source://1\" type=\"Resource\">"));
669 assert!(xml.contains("First content"));
670 assert!(xml.contains("<context type=\"Memory\">"));
671 assert!(xml.contains("Second content"));
672 }
673
674 #[test]
675 fn test_context_result_to_xml_empty() {
676 let result = ContextResult::new("provider");
677 let xml = result.to_xml();
678 assert!(xml.is_empty());
679 }
680
681 #[test]
682 fn test_context_result_serialization() {
683 let mut result = ContextResult::new("test-provider");
684 result.truncated = true;
685 result.add_item(ContextItem::new("id", ContextType::Resource, "content"));
686
687 let json = serde_json::to_string(&result).unwrap();
688 let parsed: ContextResult = serde_json::from_str(&json).unwrap();
689
690 assert_eq!(parsed.provider, "test-provider");
691 assert!(parsed.truncated);
692 assert_eq!(parsed.items.len(), 1);
693 }
694
695 #[test]
696 fn test_context_result_default() {
697 let result: ContextResult = Default::default();
698 assert!(result.items.is_empty());
699 assert_eq!(result.total_tokens, 0);
700 assert!(result.provider.is_empty());
701 assert!(!result.truncated);
702 }
703
704 struct MockContextProvider {
709 name: String,
710 items: Vec<ContextItem>,
711 }
712
713 impl MockContextProvider {
714 fn new(name: &str) -> Self {
715 Self {
716 name: name.to_string(),
717 items: Vec::new(),
718 }
719 }
720
721 fn with_items(mut self, items: Vec<ContextItem>) -> Self {
722 self.items = items;
723 self
724 }
725 }
726
727 #[async_trait::async_trait]
728 impl ContextProvider for MockContextProvider {
729 fn name(&self) -> &str {
730 &self.name
731 }
732
733 async fn query(&self, _query: &ContextQuery) -> anyhow::Result<ContextResult> {
734 let mut result = ContextResult::new(&self.name);
735 for item in &self.items {
736 result.add_item(item.clone());
737 }
738 Ok(result)
739 }
740 }
741
742 #[tokio::test]
743 async fn test_mock_context_provider() {
744 let provider = MockContextProvider::new("mock").with_items(vec![ContextItem::new(
745 "1",
746 ContextType::Resource,
747 "content",
748 )]);
749
750 assert_eq!(provider.name(), "mock");
751
752 let query = ContextQuery::new("test");
753 let result = provider.query(&query).await.unwrap();
754
755 assert_eq!(result.provider, "mock");
756 assert_eq!(result.items.len(), 1);
757 }
758
759 #[tokio::test]
760 async fn test_context_provider_on_turn_complete_default() {
761 let provider = MockContextProvider::new("mock");
762
763 let result = provider
765 .on_turn_complete("session-1", "prompt", "response")
766 .await;
767 assert!(result.is_ok());
768 }
769
770 struct MockMemoryProvider {
771 memories: std::sync::Arc<tokio::sync::RwLock<Vec<(String, String, String)>>>,
772 }
773
774 impl MockMemoryProvider {
775 fn new() -> Self {
776 Self {
777 memories: std::sync::Arc::new(tokio::sync::RwLock::new(Vec::new())),
778 }
779 }
780 }
781
782 #[async_trait::async_trait]
783 impl ContextProvider for MockMemoryProvider {
784 fn name(&self) -> &str {
785 "memory-provider"
786 }
787
788 async fn query(&self, _query: &ContextQuery) -> anyhow::Result<ContextResult> {
789 Ok(ContextResult::new("memory-provider"))
790 }
791
792 async fn on_turn_complete(
793 &self,
794 session_id: &str,
795 prompt: &str,
796 response: &str,
797 ) -> anyhow::Result<()> {
798 let mut memories = self.memories.write().await;
799 memories.push((
800 session_id.to_string(),
801 prompt.to_string(),
802 response.to_string(),
803 ));
804 Ok(())
805 }
806 }
807
808 #[tokio::test]
809 async fn test_context_provider_on_turn_complete_custom() {
810 let provider = MockMemoryProvider::new();
811
812 provider
813 .on_turn_complete("sess-1", "What is Rust?", "Rust is a systems language.")
814 .await
815 .unwrap();
816
817 let memories = provider.memories.read().await;
818 assert_eq!(memories.len(), 1);
819 assert_eq!(memories[0].0, "sess-1");
820 assert_eq!(memories[0].1, "What is Rust?");
821 assert_eq!(memories[0].2, "Rust is a systems language.");
822 }
823
824 #[tokio::test]
829 async fn test_multiple_providers_query() {
830 let provider1 = MockContextProvider::new("provider-1").with_items(vec![ContextItem::new(
831 "p1-1",
832 ContextType::Resource,
833 "Resource from P1",
834 )]);
835
836 let provider2 = MockContextProvider::new("provider-2").with_items(vec![
837 ContextItem::new("p2-1", ContextType::Memory, "Memory from P2"),
838 ContextItem::new("p2-2", ContextType::Skill, "Skill from P2"),
839 ]);
840
841 let providers: Vec<&dyn ContextProvider> = vec![&provider1, &provider2];
842 let query = ContextQuery::new("test");
843
844 let mut all_items = Vec::new();
845 for provider in providers {
846 let result = provider.query(&query).await.unwrap();
847 all_items.extend(result.items);
848 }
849
850 assert_eq!(all_items.len(), 3);
851 assert!(all_items.iter().any(|i| i.id == "p1-1"));
852 assert!(all_items.iter().any(|i| i.id == "p2-1"));
853 assert!(all_items.iter().any(|i| i.id == "p2-2"));
854 }
855
856 #[test]
857 fn test_context_result_xml_formatting_complex() {
858 let mut result = ContextResult::new("openviking");
859 result.add_item(
860 ContextItem::new(
861 "doc-1",
862 ContextType::Resource,
863 "Authentication uses JWT tokens stored in httpOnly cookies.",
864 )
865 .with_source("viking://docs/auth")
866 .with_token_count(50),
867 );
868 result.add_item(
869 ContextItem::new(
870 "mem-1",
871 ContextType::Memory,
872 "User prefers TypeScript over JavaScript.",
873 )
874 .with_token_count(30),
875 );
876
877 let xml = result.to_xml();
878
879 assert!(xml.contains("<context source=\"viking://docs/auth\" type=\"Resource\">"));
881 assert!(xml.contains("Authentication uses JWT tokens"));
882 assert!(xml.contains("<context type=\"Memory\">"));
883 assert!(xml.contains("User prefers TypeScript"));
884
885 assert!(xml.contains("</context>\n\n<context"));
887 }
888}