ralph_workflow/prompts/
content_builder.rs1use std::path::Path;
9
10use super::content_reference::{
11 DiffContentReference, PlanContentReference, PromptContentReference,
12};
13use crate::workspace::Workspace;
14
15pub struct PromptContentBuilder<'a> {
20 workspace: &'a dyn Workspace,
21 prompt_ref: Option<PromptContentReference>,
22 plan_ref: Option<PlanContentReference>,
23 diff_ref: Option<DiffContentReference>,
24}
25
26impl<'a> PromptContentBuilder<'a> {
27 pub fn new(workspace: &'a dyn Workspace) -> Self {
29 Self {
30 workspace,
31 prompt_ref: None,
32 plan_ref: None,
33 diff_ref: None,
34 }
35 }
36
37 pub fn with_prompt(mut self, content: String) -> Self {
42 let backup_path = self.workspace.prompt_backup();
43 self.prompt_ref = Some(PromptContentReference::from_content(
44 content,
45 &backup_path,
46 "Original user requirements from PROMPT.md",
47 ));
48 self
49 }
50
51 pub fn with_plan(mut self, content: String) -> Self {
56 let plan_path = Path::new(".agent/PLAN.md");
57 let xml_fallback = Path::new(".agent/tmp/plan.xml");
58 self.plan_ref = Some(PlanContentReference::from_plan(
59 content,
60 plan_path,
61 Some(xml_fallback),
62 ));
63 self
64 }
65
66 pub fn with_diff(mut self, content: String, start_commit: &str) -> Self {
71 self.diff_ref = Some(DiffContentReference::from_diff(content, start_commit));
72 self
73 }
74
75 pub fn build(self) -> PromptContentReferences {
80 PromptContentReferences {
81 prompt: self.prompt_ref,
82 plan: self.plan_ref,
83 diff: self.diff_ref,
84 }
85 }
86
87 pub fn has_oversize_content(&self) -> bool {
92 let prompt_oversize = self.prompt_ref.as_ref().is_some_and(|r| !r.is_inline());
93 let plan_oversize = self.plan_ref.as_ref().is_some_and(|r| !r.is_inline());
94 let diff_oversize = self.diff_ref.as_ref().is_some_and(|r| !r.is_inline());
95
96 prompt_oversize || plan_oversize || diff_oversize
97 }
98}
99
100pub struct PromptContentReferences {
105 pub prompt: Option<PromptContentReference>,
107 pub plan: Option<PlanContentReference>,
109 pub diff: Option<DiffContentReference>,
111}
112
113impl PromptContentReferences {
114 pub fn prompt_for_template(&self) -> String {
118 self.prompt
119 .as_ref()
120 .map(|r| r.render_for_template())
121 .unwrap_or_default()
122 }
123
124 pub fn plan_for_template(&self) -> String {
128 self.plan
129 .as_ref()
130 .map(|r| r.render_for_template())
131 .unwrap_or_default()
132 }
133
134 pub fn diff_for_template(&self) -> String {
138 self.diff
139 .as_ref()
140 .map(|r| r.render_for_template())
141 .unwrap_or_default()
142 }
143
144 pub fn prompt_is_inline(&self) -> bool {
146 self.prompt.as_ref().is_some_and(|r| r.is_inline())
147 }
148
149 pub fn plan_is_inline(&self) -> bool {
151 self.plan.as_ref().is_some_and(|r| r.is_inline())
152 }
153
154 pub fn diff_is_inline(&self) -> bool {
156 self.diff.as_ref().is_some_and(|r| r.is_inline())
157 }
158}
159
160#[cfg(all(test, feature = "test-utils"))]
161mod tests {
162 use super::*;
163 use crate::prompts::MAX_INLINE_CONTENT_SIZE;
164 use crate::workspace::MemoryWorkspace;
165
166 #[test]
167 fn test_builder_small_content() {
168 let workspace = MemoryWorkspace::new_test().with_file("PROMPT.md", "test");
169
170 let builder = PromptContentBuilder::new(&workspace)
171 .with_prompt("Small prompt".to_string())
172 .with_plan("Small plan".to_string());
173
174 assert!(!builder.has_oversize_content());
175
176 let refs = builder.build();
177 assert_eq!(refs.prompt_for_template(), "Small prompt");
178 assert_eq!(refs.plan_for_template(), "Small plan");
179 }
180
181 #[test]
182 fn test_builder_large_prompt() {
183 let workspace = MemoryWorkspace::new_test().with_file(".agent/PROMPT.md.backup", "backup");
184
185 let large_content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
186 let builder = PromptContentBuilder::new(&workspace).with_prompt(large_content);
187
188 assert!(builder.has_oversize_content());
189
190 let refs = builder.build();
191 let rendered = refs.prompt_for_template();
192 assert!(rendered.contains("PROMPT.md.backup"));
193 assert!(!refs.prompt_is_inline());
194 }
195
196 #[test]
197 fn test_builder_large_plan() {
198 let workspace = MemoryWorkspace::new_test().with_file(".agent/PLAN.md", "plan");
199
200 let large_content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
201 let builder = PromptContentBuilder::new(&workspace).with_plan(large_content);
202
203 assert!(builder.has_oversize_content());
204
205 let refs = builder.build();
206 let rendered = refs.plan_for_template();
207 assert!(rendered.contains(".agent/PLAN.md"));
208 assert!(rendered.contains("plan.xml"));
209 assert!(!refs.plan_is_inline());
210 }
211
212 #[test]
213 fn test_builder_large_diff() {
214 let workspace = MemoryWorkspace::new_test();
215
216 let large_content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
217 let builder = PromptContentBuilder::new(&workspace).with_diff(large_content, "abc123def");
218
219 assert!(builder.has_oversize_content());
220
221 let refs = builder.build();
222 let rendered = refs.diff_for_template();
223 assert!(rendered.contains("git diff abc123def..HEAD"));
224 assert!(!refs.diff_is_inline());
225 }
226
227 #[test]
228 fn test_builder_no_oversize_when_all_small() {
229 let workspace = MemoryWorkspace::new_test().with_file("PROMPT.md", "test");
230
231 let builder = PromptContentBuilder::new(&workspace)
232 .with_prompt("Small prompt".to_string())
233 .with_plan("Small plan".to_string())
234 .with_diff("Small diff".to_string(), "abc123");
235
236 assert!(!builder.has_oversize_content());
237
238 let refs = builder.build();
239 assert!(refs.prompt_is_inline());
240 assert!(refs.plan_is_inline());
241 assert!(refs.diff_is_inline());
242 }
243
244 #[test]
245 fn test_builder_partial_oversize() {
246 let workspace = MemoryWorkspace::new_test().with_file(".agent/PROMPT.md.backup", "backup");
247
248 let large_prompt = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
250 let builder = PromptContentBuilder::new(&workspace)
251 .with_prompt(large_prompt)
252 .with_plan("Small plan".to_string())
253 .with_diff("Small diff".to_string(), "abc123");
254
255 assert!(builder.has_oversize_content());
256
257 let refs = builder.build();
258 assert!(!refs.prompt_is_inline());
259 assert!(refs.plan_is_inline());
260 assert!(refs.diff_is_inline());
261 }
262
263 #[test]
264 fn test_builder_empty_content() {
265 let workspace = MemoryWorkspace::new_test();
266
267 let refs = PromptContentBuilder::new(&workspace).build();
268
269 assert_eq!(refs.prompt_for_template(), "");
270 assert_eq!(refs.plan_for_template(), "");
271 assert_eq!(refs.diff_for_template(), "");
272 }
273
274 #[test]
275 fn test_refs_inline_checks_with_none() {
276 let refs = PromptContentReferences {
277 prompt: None,
278 plan: None,
279 diff: None,
280 };
281
282 assert!(!refs.prompt_is_inline());
284 assert!(!refs.plan_is_inline());
285 assert!(!refs.diff_is_inline());
286 }
287}