Skip to main content

ralph_core/
wave_prompt.rs

1//! Wave worker prompt builder.
2//!
3//! Constructs focused prompts for individual wave worker instances,
4//! providing task context and constraints to keep workers on track.
5
6use crate::config::HatConfig;
7use crate::event_reader::Event;
8
9/// Context for a wave worker instance.
10#[derive(Debug)]
11pub struct WaveWorkerContext {
12    /// Wave correlation ID (e.g., "w-1a2b3c4d").
13    pub wave_id: String,
14    /// 0-based index of this worker within the wave.
15    pub wave_index: u32,
16    /// Total number of workers in this wave.
17    pub wave_total: u32,
18    /// Topics this worker should publish results to.
19    pub result_topics: Vec<String>,
20}
21
22/// Builds a focused prompt for a wave worker instance.
23///
24/// The prompt contains:
25/// 1. Hat instructions (what the worker does)
26/// 2. Wave context (worker identity within the wave)
27/// 3. Task payload (the specific work item)
28/// 4. Publishing guide (how to emit results)
29/// 5. Constraints (nested wave prohibition, focus directive)
30pub fn build_wave_worker_prompt(hat: &HatConfig, event: &Event, ctx: &WaveWorkerContext) -> String {
31    let mut prompt = String::new();
32
33    // 1. Instructions
34    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    // 2. Wave context
44    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    // 3. Task payload
55    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    // 4. Publishing results
62    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    // 5. Constraints
74    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}