kcode_kennedy_orchestration/
prompts.rs1use chrono::{DateTime, Datelike, Timelike, Utc};
2use kcode_kennedy_prompts::SystemPrompts;
3use kcode_kennedy_sessions::RuntimeModel;
4
5#[derive(Clone, Debug)]
6pub struct Manuals(SystemPrompts);
7
8impl Manuals {
9 pub fn open() -> Self {
10 Self(kcode_kennedy_prompts::open())
11 }
12
13 pub fn compose_conversation(
14 &self,
15 runtime: &RuntimeModel,
16 session_type: &str,
17 session_context: &str,
18 ) -> String {
19 let (session_prompt, writes) = match session_type {
20 "free-time" => (self.0.self_time_session(), true),
21 "wakeup" => (self.0.wakeup_session(), true),
22 _ => (self.0.conversation_session(), false),
23 };
24 self.compose(
25 runtime,
26 session_prompt,
27 writes,
28 session_context,
29 session_type,
30 )
31 }
32
33 pub fn compose_ingress(&self, runtime: &RuntimeModel, source_session_type: &str) -> String {
34 let session_prompt = if source_session_type == "audio" {
35 self.0.audio_ingress_session()
36 } else {
37 self.0.history_ingress_session()
38 };
39 self.compose(runtime, session_prompt, true, "", source_session_type)
40 }
41
42 fn compose(
43 &self,
44 runtime: &RuntimeModel,
45 session_prompt: &str,
46 writes: bool,
47 session_context: &str,
48 channel: &str,
49 ) -> String {
50 let mut sections = vec![
51 section("Kennedy's identity", self.0.identity()),
52 section("Session type", session_prompt),
53 ];
54 if matches!(channel, "telegram" | "telegram-group") {
55 sections.push(section("Telegram session", self.0.telegram_session()));
56 }
57 if channel == "telegram-group" {
58 sections.push(section(
59 "Telegram group session",
60 self.0.telegram_group_session(),
61 ));
62 }
63 sections.extend([
64 section("Kmap basics", self.0.kmap_basics()),
65 section("Critical Kmap and context tools", self.0.read_tools()),
66 ]);
67 if writes {
68 sections.push(section("Write tools", self.0.write_tools()));
69 }
70 sections.push(section("Codex harness", self.0.codex_harness()));
71 if !session_context.trim().is_empty() {
72 sections.push(section("Self-time schedule", session_context.trim()));
73 }
74 sections.push(section(
75 "Current runtime",
76 &runtime_description(runtime, Utc::now()),
77 ));
78 sections.join("\n\n")
79 }
80}
81
82fn section(title: &str, content: &str) -> String {
83 format!("{title}\n\n{content}")
84}
85
86pub fn runtime_description(runtime: &RuntimeModel, current_time: DateTime<Utc>) -> String {
87 format!(
88 "You are currently running on {} with {} thinking mode. The current date and time is {}.",
89 runtime.model,
90 runtime.reasoning_effort,
91 human_utc_datetime(current_time)
92 )
93}
94
95pub fn human_utc_datetime(value: DateTime<Utc>) -> String {
96 let day = value.day();
97 let suffix = match day % 100 {
98 11..=13 => "th",
99 _ => match day % 10 {
100 1 => "st",
101 2 => "nd",
102 3 => "rd",
103 _ => "th",
104 },
105 };
106 let hour = match value.hour() % 12 {
107 0 => 12,
108 hour => hour,
109 };
110 let period = if value.hour() < 12 { "am" } else { "pm" };
111 format!(
112 "{} {day}{suffix}, {}, {hour}:{:02}{period} UTC",
113 value.format("%B"),
114 value.year(),
115 value.minute()
116 )
117}
118
119#[cfg(test)]
120mod tests {
121 use chrono::TimeZone;
122
123 use super::*;
124
125 fn testing_manuals() -> Manuals {
126 Manuals::open()
127 }
128
129 fn testing_runtime() -> RuntimeModel {
130 RuntimeModel {
131 model: "gpt-5.6-sol".into(),
132 reasoning_effort: "xhigh".into(),
133 context_window_tokens: 1_000_000,
134 }
135 }
136
137 #[test]
138 fn human_time_uses_ordinals_and_unambiguous_twelve_hour_clock() {
139 assert_eq!(
140 human_utc_datetime(Utc.with_ymd_and_hms(2026, 7, 6, 4, 23, 0).unwrap()),
141 "July 6th, 2026, 4:23am UTC"
142 );
143 assert_eq!(
144 human_utc_datetime(Utc.with_ymd_and_hms(2026, 7, 11, 16, 34, 0).unwrap()),
145 "July 11th, 2026, 4:34pm UTC"
146 );
147 assert_eq!(
148 human_utc_datetime(Utc.with_ymd_and_hms(2026, 7, 22, 0, 5, 0).unwrap()),
149 "July 22nd, 2026, 12:05am UTC"
150 );
151 }
152
153 #[test]
154 fn telegram_layers_are_scoped_to_telegram_channels() {
155 let manuals = testing_manuals();
156 let runtime = testing_runtime();
157 let browser = manuals.compose_conversation(&runtime, "conversation", "");
158 assert!(!browser.contains("This session belongs to a Telegram conversation."));
159 assert!(!browser.contains("This is a Telegram group session"));
160
161 let private = manuals.compose_conversation(&runtime, "telegram", "");
162 assert!(private.contains("This session belongs to a Telegram conversation."));
163 assert!(!private.contains("This is a Telegram group session"));
164
165 let group = manuals.compose_conversation(&runtime, "telegram-group", "");
166 assert!(group.contains("This session belongs to a Telegram conversation."));
167 assert!(group.contains("This is a Telegram group session"));
168
169 let group_ingress = manuals.compose_ingress(&runtime, "telegram-group");
170 assert!(group_ingress.contains("This session belongs to a Telegram conversation."));
171 assert!(group_ingress.contains("This is a Telegram group session"));
172
173 let audio = manuals.compose_ingress(&runtime, "audio");
174 assert!(!audio.contains("This session belongs to a Telegram conversation."));
175 assert!(!audio.contains("This is a Telegram group session"));
176 }
177
178 #[test]
179 fn wakeup_sessions_have_their_own_autonomous_write_prompt() {
180 let prompt = testing_manuals().compose_conversation(&testing_runtime(), "wakeup", "");
181 assert!(prompt.contains("You are in a wakeup session"));
182 assert!(prompt.contains("ConnectNodes"));
183 assert!(!prompt.contains("This is a conversation session with a user."));
184 assert!(!prompt.contains("This session belongs to a Telegram conversation."));
185 }
186}