kcode_kennedy_orchestration/
prompts.rs1use 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}