Skip to main content

kcode_kennedy_orchestration/
prompts.rs

1use std::{collections::HashMap, path::Path};
2
3use anyhow::Context;
4use chrono::{DateTime, Datelike, Timelike, Utc};
5use kcode_kennedy_sessions::RuntimeModel;
6
7const PROMPT_FILES: [(&str, &str); 12] = [
8    ("identity", "KennedyIdentity.txt"),
9    ("conversationSession", "ConversationSession.txt"),
10    ("freeTimeSession", "SelfTimeSession.txt"),
11    ("wakeupSession", "WakeupSession.txt"),
12    ("historyIngressSession", "HistoryIngressSession.txt"),
13    ("audioIngressSession", "AudioIngressSession.txt"),
14    ("telegramSession", "TelegramSession.txt"),
15    ("telegramGroupSession", "TelegramGroupSession.txt"),
16    ("codexHarness", "CodexHarness.txt"),
17    ("kmapBasics", "KmapBasics.txt"),
18    ("readTools", "ReadTools.txt"),
19    ("writeTools", "WriteTools.txt"),
20];
21
22#[derive(Clone, Debug)]
23pub struct Manuals(HashMap<String, String>);
24
25impl Manuals {
26    pub fn load(directory: &Path) -> anyhow::Result<Self> {
27        let mut manuals = HashMap::new();
28        for (key, filename) in PROMPT_FILES {
29            let path = directory.join(filename);
30            let text = std::fs::read_to_string(&path)
31                .with_context(|| format!("reading system prompt {}", path.display()))?;
32            let text = text.trim().to_owned();
33            anyhow::ensure!(
34                !text.is_empty(),
35                "system prompt {} is empty",
36                path.display()
37            );
38            manuals.insert(key.to_owned(), text);
39        }
40        Ok(Self(manuals))
41    }
42
43    pub fn compose_conversation(
44        &self,
45        runtime: &RuntimeModel,
46        session_type: &str,
47        session_context: &str,
48    ) -> anyhow::Result<String> {
49        let (session_key, writes) = match session_type {
50            "free-time" => ("freeTimeSession", true),
51            "wakeup" => ("wakeupSession", true),
52            _ => ("conversationSession", false),
53        };
54        self.compose(runtime, session_key, writes, session_context, session_type)
55    }
56
57    pub fn compose_ingress(
58        &self,
59        runtime: &RuntimeModel,
60        source_session_type: &str,
61    ) -> anyhow::Result<String> {
62        let session_key = if source_session_type == "audio" {
63            "audioIngressSession"
64        } else {
65            "historyIngressSession"
66        };
67        self.compose(runtime, session_key, true, "", source_session_type)
68    }
69
70    fn compose(
71        &self,
72        runtime: &RuntimeModel,
73        session_key: &str,
74        writes: bool,
75        session_context: &str,
76        channel: &str,
77    ) -> anyhow::Result<String> {
78        let mut sections = vec![
79            section("Kennedy's identity", self.required("identity")?),
80            section("Session type", self.required(session_key)?),
81        ];
82        if matches!(channel, "telegram" | "telegram-group") {
83            sections.push(section(
84                "Telegram session",
85                self.required("telegramSession")?,
86            ));
87        }
88        if channel == "telegram-group" {
89            sections.push(section(
90                "Telegram group session",
91                self.required("telegramGroupSession")?,
92            ));
93        }
94        sections.extend([
95            section("Kmap basics", self.required("kmapBasics")?),
96            section(
97                "Critical Kmap and context tools",
98                self.required("readTools")?,
99            ),
100        ]);
101        if writes {
102            sections.push(section("Write tools", self.required("writeTools")?));
103        }
104        sections.push(section("Codex harness", self.required("codexHarness")?));
105        if !session_context.trim().is_empty() {
106            sections.push(section("Self-time schedule", session_context.trim()));
107        }
108        sections.push(section(
109            "Current runtime",
110            &runtime_description(runtime, Utc::now()),
111        ));
112        Ok(sections.join("\n\n"))
113    }
114
115    fn required(&self, key: &str) -> anyhow::Result<&str> {
116        self.0
117            .get(key)
118            .map(String::as_str)
119            .with_context(|| format!("missing system prompt section {key}"))
120    }
121}
122
123fn section(title: &str, content: &str) -> String {
124    format!("{title}\n\n{content}")
125}
126
127pub fn runtime_description(runtime: &RuntimeModel, current_time: DateTime<Utc>) -> String {
128    format!(
129        "You are currently running on {} with {} thinking mode. The current date and time is {}.",
130        runtime.model,
131        runtime.reasoning_effort,
132        human_utc_datetime(current_time)
133    )
134}
135
136pub fn human_utc_datetime(value: DateTime<Utc>) -> String {
137    let day = value.day();
138    let suffix = match day % 100 {
139        11..=13 => "th",
140        _ => match day % 10 {
141            1 => "st",
142            2 => "nd",
143            3 => "rd",
144            _ => "th",
145        },
146    };
147    let hour = match value.hour() % 12 {
148        0 => 12,
149        hour => hour,
150    };
151    let period = if value.hour() < 12 { "am" } else { "pm" };
152    format!(
153        "{} {day}{suffix}, {}, {hour}:{:02}{period} UTC",
154        value.format("%B"),
155        value.year(),
156        value.minute()
157    )
158}
159
160#[cfg(test)]
161mod tests {
162    use std::collections::HashMap;
163
164    use chrono::TimeZone;
165
166    use super::*;
167
168    fn testing_manuals() -> Manuals {
169        Manuals(
170            PROMPT_FILES
171                .into_iter()
172                .map(|(key, _)| (key.to_owned(), format!("[{key}]")))
173                .collect::<HashMap<_, _>>(),
174        )
175    }
176
177    fn testing_runtime() -> RuntimeModel {
178        RuntimeModel {
179            model: "gpt-5.6-sol".into(),
180            reasoning_effort: "xhigh".into(),
181            context_window_tokens: 1_000_000,
182        }
183    }
184
185    #[test]
186    fn human_time_uses_ordinals_and_unambiguous_twelve_hour_clock() {
187        assert_eq!(
188            human_utc_datetime(Utc.with_ymd_and_hms(2026, 7, 6, 4, 23, 0).unwrap()),
189            "July 6th, 2026, 4:23am UTC"
190        );
191        assert_eq!(
192            human_utc_datetime(Utc.with_ymd_and_hms(2026, 7, 11, 16, 34, 0).unwrap()),
193            "July 11th, 2026, 4:34pm UTC"
194        );
195        assert_eq!(
196            human_utc_datetime(Utc.with_ymd_and_hms(2026, 7, 22, 0, 5, 0).unwrap()),
197            "July 22nd, 2026, 12:05am UTC"
198        );
199    }
200
201    #[test]
202    fn telegram_layers_are_scoped_to_telegram_channels() {
203        let manuals = testing_manuals();
204        let runtime = testing_runtime();
205        let browser = manuals
206            .compose_conversation(&runtime, "conversation", "")
207            .unwrap();
208        assert!(!browser.contains("[telegramSession]"));
209        assert!(!browser.contains("[telegramGroupSession]"));
210
211        let private = manuals
212            .compose_conversation(&runtime, "telegram", "")
213            .unwrap();
214        assert!(private.contains("[telegramSession]"));
215        assert!(!private.contains("[telegramGroupSession]"));
216
217        let group = manuals
218            .compose_conversation(&runtime, "telegram-group", "")
219            .unwrap();
220        assert!(group.contains("[telegramSession]"));
221        assert!(group.contains("[telegramGroupSession]"));
222
223        let group_ingress = manuals.compose_ingress(&runtime, "telegram-group").unwrap();
224        assert!(group_ingress.contains("[telegramSession]"));
225        assert!(group_ingress.contains("[telegramGroupSession]"));
226
227        let audio = manuals.compose_ingress(&runtime, "audio").unwrap();
228        assert!(!audio.contains("[telegramSession]"));
229        assert!(!audio.contains("[telegramGroupSession]"));
230    }
231
232    #[test]
233    fn wakeup_sessions_have_their_own_autonomous_write_prompt() {
234        let prompt = testing_manuals()
235            .compose_conversation(&testing_runtime(), "wakeup", "")
236            .unwrap();
237        assert!(prompt.contains("[wakeupSession]"));
238        assert!(prompt.contains("[writeTools]"));
239        assert!(!prompt.contains("[conversationSession]"));
240        assert!(!prompt.contains("[telegramSession]"));
241    }
242}