ralph_core/
wave_prompt.rs1use crate::config::HatConfig;
7use crate::event_reader::Event;
8
9#[derive(Debug)]
11pub struct WaveWorkerContext {
12 pub wave_id: String,
14 pub wave_index: u32,
16 pub wave_total: u32,
18 pub result_topics: Vec<String>,
20}
21
22pub fn build_wave_worker_prompt(hat: &HatConfig, event: &Event, ctx: &WaveWorkerContext) -> String {
31 let mut prompt = String::new();
32
33 if !hat.instructions.trim().is_empty() {
35 prompt.push_str("# Instructions\n\n");
36 prompt.push_str(&hat.instructions);
37 if !hat.instructions.ends_with('\n') {
38 prompt.push('\n');
39 }
40 prompt.push('\n');
41 }
42
43 prompt.push_str("# Wave Context\n\n");
45 prompt.push_str(&format!(
46 "You are worker **{}/{}** in wave `{}`.\n\
47 Each worker in this wave processes one task independently and in parallel.\n\
48 Focus exclusively on your assigned task below.\n\n",
49 ctx.wave_index + 1,
50 ctx.wave_total,
51 ctx.wave_id,
52 ));
53
54 prompt.push_str("# Your Task\n\n");
56 if let Some(ref payload) = event.payload {
57 prompt.push_str(payload);
58 }
59 prompt.push_str("\n\n");
60
61 if !ctx.result_topics.is_empty() {
63 prompt.push_str("# Publishing Results\n\n");
64 prompt.push_str("When your work is complete, publish your results using `ralph emit`:\n\n");
65 for topic in &ctx.result_topics {
66 prompt.push_str(&format!(
67 "```bash\nralph emit {} \"<your result payload>\"\n```\n\n",
68 topic
69 ));
70 }
71 }
72
73 prompt.push_str("# Constraints\n\n");
75 prompt.push_str(
76 "- **DO NOT** use `ralph wave emit` — nested wave dispatch is prohibited.\n\
77 - Focus exclusively on your assigned task. Do not attempt work assigned to other workers.\n\
78 - Publish exactly one result event when complete.\n",
79 );
80
81 prompt
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 fn make_hat_config() -> HatConfig {
89 let yaml = r#"
90 name: "Reviewer"
91 triggers: ["review.file"]
92 publishes: ["review.done"]
93 instructions: "Review the file for bugs and style issues."
94 "#;
95 serde_yaml::from_str(yaml).unwrap()
96 }
97
98 fn make_event(payload: &str) -> Event {
99 Event {
100 topic: "review.file".to_string(),
101 payload: Some(payload.to_string()),
102 ts: "2025-01-01T00:00:00Z".to_string(),
103 wave_id: Some("w-test1234".to_string()),
104 wave_index: Some(0),
105 wave_total: Some(3),
106 }
107 }
108
109 #[test]
110 fn test_build_wave_worker_prompt_contains_all_sections() {
111 let hat = make_hat_config();
112 let event = make_event("src/main.rs");
113 let ctx = WaveWorkerContext {
114 wave_id: "w-test1234".to_string(),
115 wave_index: 0,
116 wave_total: 3,
117 result_topics: vec!["review.done".to_string()],
118 };
119
120 let prompt = build_wave_worker_prompt(&hat, &event, &ctx);
121
122 assert!(prompt.contains("# Instructions"));
123 assert!(prompt.contains("Review the file for bugs"));
124 assert!(prompt.contains("# Wave Context"));
125 assert!(prompt.contains("worker **1/3**"));
126 assert!(prompt.contains("w-test1234"));
127 assert!(prompt.contains("# Your Task"));
128 assert!(prompt.contains("src/main.rs"));
129 assert!(prompt.contains("# Publishing Results"));
130 assert!(prompt.contains("ralph emit review.done"));
131 assert!(prompt.contains("# Constraints"));
132 assert!(prompt.contains("DO NOT"));
133 }
134
135 #[test]
136 fn test_worker_index_is_1_based_in_display() {
137 let hat = make_hat_config();
138 let event = make_event("file.rs");
139 let ctx = WaveWorkerContext {
140 wave_id: "w-abc".to_string(),
141 wave_index: 2,
142 wave_total: 5,
143 result_topics: vec![],
144 };
145
146 let prompt = build_wave_worker_prompt(&hat, &event, &ctx);
147 assert!(prompt.contains("worker **3/5**"));
148 }
149
150 #[test]
151 fn test_empty_instructions_omitted() {
152 let yaml = r#"
153 name: "Reviewer"
154 triggers: ["review.file"]
155 publishes: ["review.done"]
156 instructions: ""
157 "#;
158 let hat: HatConfig = serde_yaml::from_str(yaml).unwrap();
159 let event = make_event("payload");
160 let ctx = WaveWorkerContext {
161 wave_id: "w-abc".to_string(),
162 wave_index: 0,
163 wave_total: 1,
164 result_topics: vec![],
165 };
166
167 let prompt = build_wave_worker_prompt(&hat, &event, &ctx);
168 assert!(!prompt.contains("# Instructions"));
169 }
170
171 #[test]
172 fn test_no_result_topics_skips_publishing_section() {
173 let hat = make_hat_config();
174 let event = make_event("payload");
175 let ctx = WaveWorkerContext {
176 wave_id: "w-abc".to_string(),
177 wave_index: 0,
178 wave_total: 1,
179 result_topics: vec![],
180 };
181
182 let prompt = build_wave_worker_prompt(&hat, &event, &ctx);
183 assert!(!prompt.contains("# Publishing Results"));
184 }
185}