Skip to main content

telos_agent/agent/prompt/builtins/
context.rs

1use async_trait::async_trait;
2use std::path::PathBuf;
3use std::sync::{Arc, Mutex};
4
5use crate::agent::prompt::{PromptSection, PromptStability};
6use crate::integrations::mcp::manager::McpManager;
7use crate::knowledge::memory::MemoryStatus;
8use crate::knowledge::memory::index::{MemoryQuery, MemorySort, MemoryStore};
9use crate::knowledge::memory::profile::ProfileManager;
10use crate::knowledge::skills::SkillRegistry;
11
12// ── Date ──────────────────────────────────────────────────
13
14pub struct DateSection;
15
16#[async_trait]
17impl PromptSection for DateSection {
18    fn name(&self) -> &str {
19        "date"
20    }
21    fn stability(&self) -> PromptStability {
22        // A session rarely spans midnight; the date is effectively constant.
23        PromptStability::Static
24    }
25
26    async fn render(&self, _ctx: &()) -> String {
27        let date = time::OffsetDateTime::now_utc().date();
28        format!("Today's date is {}.", date)
29    }
30}
31
32// ── CWD ───────────────────────────────────────────────────
33
34pub struct CwdSection {
35    cwd: PathBuf,
36}
37
38impl CwdSection {
39    pub fn new(cwd: PathBuf) -> Self {
40        Self { cwd }
41    }
42}
43
44#[async_trait]
45impl PromptSection for CwdSection {
46    fn name(&self) -> &str {
47        "cwd"
48    }
49    fn stability(&self) -> PromptStability {
50        // The working directory doesn't change during a session.
51        PromptStability::Static
52    }
53
54    async fn render(&self, _ctx: &()) -> String {
55        format!("Working directory: {}", self.cwd.display())
56    }
57}
58
59// ── Skills ────────────────────────────────────────────────
60
61pub struct SkillsSection {
62    registry: Arc<SkillRegistry>,
63}
64
65impl SkillsSection {
66    pub fn new(registry: Arc<SkillRegistry>) -> Self {
67        Self { registry }
68    }
69}
70
71#[async_trait]
72impl PromptSection for SkillsSection {
73    fn name(&self) -> &str {
74        "skills"
75    }
76    fn stability(&self) -> PromptStability {
77        PromptStability::Static
78    }
79
80    async fn render(&self, _ctx: &()) -> String {
81        self.registry.render_for_prompt()
82    }
83}
84
85// ── Git Status ────────────────────────────────────────────
86
87pub struct GitStatusSection;
88
89#[async_trait]
90impl PromptSection for GitStatusSection {
91    fn name(&self) -> &str {
92        "git_status"
93    }
94    fn stability(&self) -> PromptStability {
95        // Rendered once at session start; runtime changes appear in tool results.
96        PromptStability::Static
97    }
98
99    async fn render(&self, _ctx: &()) -> String {
100        let mut command = std::process::Command::new("git");
101        command.args(["status", "--short"]);
102        hide_console_window(&mut command);
103        match command.output() {
104            Ok(output) if output.status.success() => {
105                let stdout = String::from_utf8_lossy(&output.stdout);
106                if stdout.trim().is_empty() {
107                    "Git: clean working tree.".into()
108                } else {
109                    format!("## Git Status\n```\n{}\n```", stdout.trim())
110                }
111            }
112            _ => String::new(),
113        }
114    }
115}
116
117#[cfg_attr(not(windows), allow(unused_variables))]
118fn hide_console_window(command: &mut std::process::Command) {
119    #[cfg(windows)]
120    {
121        use std::os::windows::process::CommandExt;
122
123        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
124        command.creation_flags(CREATE_NO_WINDOW);
125    }
126}
127
128// ── Memory ────────────────────────────────────────────────
129
130pub struct MemorySection {
131    store: Arc<Mutex<MemoryStore>>,
132}
133
134impl MemorySection {
135    pub fn new(store: Arc<Mutex<MemoryStore>>) -> Self {
136        Self { store }
137    }
138}
139
140#[async_trait]
141impl PromptSection for MemorySection {
142    fn name(&self) -> &str {
143        "memory"
144    }
145    fn stability(&self) -> PromptStability {
146        // Rendered once at session start; memories are injected as
147        // <system-reminder> tags by the runtime when they change.
148        PromptStability::Static
149    }
150
151    async fn render(&self, _ctx: &()) -> String {
152        let store = self.store.clone();
153        match tokio::task::spawn_blocking(move || {
154            let store = store.lock().unwrap();
155            let mut memories = store.query(MemoryQuery {
156                limit: Some(8),
157                sort: MemorySort::Relevance,
158                ..MemoryQuery::default()
159            });
160            memories.retain(|entry| entry.status != MemoryStatus::Deprecated);
161            memories.truncate(5);
162            if memories.is_empty() {
163                return String::new();
164            }
165            let mut lines = vec!["## Relevant Memories".to_string()];
166            for entry in &memories {
167                lines.push(format!(
168                    "- **{}** ({:?}, {:?}): {}",
169                    entry.name, entry.category, entry.status, entry.description
170                ));
171                let preview: String = entry.body.chars().take(200).collect();
172                if !preview.is_empty() {
173                    lines.push(format!("  {}", preview));
174                }
175            }
176            lines.join("\n")
177        })
178        .await
179        {
180            Ok(result) => result,
181            Err(e) => {
182                tracing::warn!("MemorySection::render failed: {e}");
183                String::new()
184            }
185        }
186    }
187}
188
189// ── Profile ────────────────────────────────────────────────
190
191pub struct ProfileSection {
192    profile_manager: Arc<ProfileManager>,
193}
194
195impl ProfileSection {
196    pub fn new(profile_manager: Arc<ProfileManager>) -> Self {
197        Self { profile_manager }
198    }
199}
200
201#[async_trait]
202impl PromptSection for ProfileSection {
203    fn name(&self) -> &str {
204        "profile"
205    }
206    fn stability(&self) -> PromptStability {
207        PromptStability::Dynamic
208    }
209
210    async fn render(&self, _ctx: &()) -> String {
211        self.profile_manager.render_all()
212    }
213}
214
215// ── MCP ────────────────────────────────────────────────────
216
217/// Renders a list of tools provided by connected MCP servers.
218pub struct McpSection {
219    manager: Arc<McpManager>,
220}
221
222impl McpSection {
223    pub fn new(manager: Arc<McpManager>) -> Self {
224        Self { manager }
225    }
226}
227
228#[async_trait]
229impl PromptSection for McpSection {
230    fn name(&self) -> &str {
231        "mcp"
232    }
233    fn stability(&self) -> PromptStability {
234        PromptStability::Dynamic
235    }
236
237    async fn render(&self, _ctx: &()) -> String {
238        let tools = self.manager.all_tools().await;
239        if tools.is_empty() {
240            return String::new();
241        }
242        let mut lines = vec!["## MCP Tools".to_string()];
243        for (server_id, tool) in &tools {
244            lines.push(format!("- **mcp__{}__{}**: {}", server_id, tool.name, tool.description));
245        }
246        lines.join("\n")
247    }
248}