Skip to main content

navi_core/
context.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{Value, json};
3
4/// Identifies the origin or category of a context packet.
5///
6/// Clients (TUI, Tutor, editors) use these variants so the engine can
7/// prioritize and format injected context without knowing the client's UI.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum ContextSource {
10    /// Content from a file on disk.
11    File,
12    /// Project-level metadata or state.
13    Project,
14    /// A user's text selection in an editor or UI.
15    UserSelection,
16    /// A node from a visual canvas (e.g. NAVI Tutor).
17    CanvasNode,
18    /// A study block from a learning workspace.
19    StudyBlock,
20    /// A focus thread tracking the user's current area of work.
21    FocusThread,
22    /// An excerpt from study material or documentation.
23    MaterialExcerpt,
24    /// A summary from a previous session.
25    SessionSummary,
26    /// A recorded decision or rationale.
27    Decision,
28    /// Results from a memory or knowledge-base search.
29    MemorySearch,
30    /// A custom source identified by an arbitrary string tag.
31    Other(String),
32}
33
34/// A unit of external context injected into the agent's conversation.
35///
36/// Context packets let clients supply information from files, canvas nodes,
37/// study blocks, memory searches, and other sources without the engine
38/// needing to know about the client's data model.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct ContextPacket {
41    /// Optional client-assigned identifier for deduplication or reference.
42    #[serde(default)]
43    pub id: Option<String>,
44    /// The origin category of this packet.
45    pub source: ContextSource,
46    /// Optional short title for display or logging.
47    #[serde(default)]
48    pub title: Option<String>,
49    /// The text content to inject into the conversation.
50    pub content: String,
51    /// Ordering priority; higher values are rendered first in the context block.
52    #[serde(default)]
53    pub priority: i32,
54    /// Arbitrary metadata the client wants to attach (ignored by the engine).
55    #[serde(default = "default_context_metadata")]
56    pub metadata: Value,
57}
58
59fn default_context_metadata() -> Value {
60    json!({})
61}
62
63/// Renders context packets into a text block for injection into the system
64/// prompt, sorted by descending priority.
65///
66/// Returns `None` if the slice is empty.
67pub fn render_context_packets(packets: &[ContextPacket]) -> Option<String> {
68    if packets.is_empty() {
69        return None;
70    }
71
72    let mut ordered = packets.to_vec();
73    ordered.sort_by_key(|b| std::cmp::Reverse(b.priority));
74
75    let mut rendered = String::from("=== External Context Packets ===\n");
76    for packet in ordered {
77        let title = packet.title.as_deref().unwrap_or("untitled");
78        rendered.push_str(&format!(
79            "- source: {:?}; priority: {}; title: {}\n",
80            packet.source, packet.priority, title
81        ));
82        rendered.push_str(packet.content.trim());
83        rendered.push_str("\n\n");
84    }
85
86    Some(rendered)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn renders_context_packets_by_priority() {
95        let low = ContextPacket {
96            id: None,
97            source: ContextSource::StudyBlock,
98            title: Some("low".to_string()),
99            content: "later".to_string(),
100            priority: 1,
101            metadata: json!({}),
102        };
103        let high = ContextPacket {
104            id: None,
105            source: ContextSource::FocusThread,
106            title: Some("high".to_string()),
107            content: "now".to_string(),
108            priority: 10,
109            metadata: json!({}),
110        };
111
112        let rendered = render_context_packets(&[low, high]).expect("rendered");
113        assert!(rendered.find("high").unwrap() < rendered.find("low").unwrap());
114        assert!(rendered.contains("now"));
115        assert!(rendered.contains("later"));
116    }
117}