ralph_workflow/prompts/
content_builder.rs1use std::path::Path;
9
10use super::content_reference::{
11 DiffContentReference, PlanContentReference, PromptContentReference, MAX_INLINE_CONTENT_SIZE,
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 let is_oversize = content.len() > MAX_INLINE_CONTENT_SIZE;
74 if is_oversize {
75 let tmp_dir = Path::new(".agent/tmp");
76 let diff_rel = tmp_dir.join("diff.txt");
77 if self.workspace.create_dir_all(tmp_dir).is_ok() {
78 let _ = self.workspace.write(&diff_rel, &content);
79 }
80 }
81
82 let diff_abs = self.workspace.absolute(Path::new(".agent/tmp/diff.txt"));
83 self.diff_ref = Some(DiffContentReference::from_diff(
84 content,
85 start_commit,
86 &diff_abs,
87 ));
88 self
89 }
90
91 pub fn build(self) -> PromptContentReferences {
96 PromptContentReferences {
97 prompt: self.prompt_ref,
98 plan: self.plan_ref,
99 diff: self.diff_ref,
100 }
101 }
102
103 pub fn has_oversize_content(&self) -> bool {
108 let prompt_oversize = self.prompt_ref.as_ref().is_some_and(|r| !r.is_inline());
109 let plan_oversize = self.plan_ref.as_ref().is_some_and(|r| !r.is_inline());
110 let diff_oversize = self.diff_ref.as_ref().is_some_and(|r| !r.is_inline());
111
112 prompt_oversize || plan_oversize || diff_oversize
113 }
114}
115
116pub struct PromptContentReferences {
121 pub prompt: Option<PromptContentReference>,
123 pub plan: Option<PlanContentReference>,
125 pub diff: Option<DiffContentReference>,
127}
128
129impl PromptContentReferences {
130 pub fn prompt_for_template(&self) -> String {
134 self.prompt
135 .as_ref()
136 .map(|r| r.render_for_template())
137 .unwrap_or_default()
138 }
139
140 pub fn plan_for_template(&self) -> String {
144 self.plan
145 .as_ref()
146 .map(|r| r.render_for_template())
147 .unwrap_or_default()
148 }
149
150 pub fn diff_for_template(&self) -> String {
154 self.diff
155 .as_ref()
156 .map(|r| r.render_for_template())
157 .unwrap_or_default()
158 }
159
160 pub fn prompt_is_inline(&self) -> bool {
162 self.prompt.as_ref().is_some_and(|r| r.is_inline())
163 }
164
165 pub fn plan_is_inline(&self) -> bool {
167 self.plan.as_ref().is_some_and(|r| r.is_inline())
168 }
169
170 pub fn diff_is_inline(&self) -> bool {
172 self.diff.as_ref().is_some_and(|r| r.is_inline())
173 }
174}
175
176#[cfg(all(test, feature = "test-utils"))]
177mod tests {
178 use super::*;
179 use crate::prompts::MAX_INLINE_CONTENT_SIZE;
180 use crate::workspace::MemoryWorkspace;
181
182 #[test]
183 fn test_builder_small_content() {
184 let workspace = MemoryWorkspace::new_test().with_file("PROMPT.md", "test");
185
186 let builder = PromptContentBuilder::new(&workspace)
187 .with_prompt("Small prompt".to_string())
188 .with_plan("Small plan".to_string());
189
190 assert!(!builder.has_oversize_content());
191
192 let refs = builder.build();
193 assert_eq!(refs.prompt_for_template(), "Small prompt");
194 assert_eq!(refs.plan_for_template(), "Small plan");
195 }
196
197 #[test]
198 fn test_builder_large_prompt() {
199 let workspace = MemoryWorkspace::new_test().with_file(".agent/PROMPT.md.backup", "backup");
200
201 let large_content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
202 let builder = PromptContentBuilder::new(&workspace).with_prompt(large_content);
203
204 assert!(builder.has_oversize_content());
205
206 let refs = builder.build();
207 let rendered = refs.prompt_for_template();
208 assert!(rendered.contains("PROMPT.md.backup"));
209 assert!(!refs.prompt_is_inline());
210 }
211
212 #[test]
213 fn test_builder_large_plan() {
214 let workspace = MemoryWorkspace::new_test().with_file(".agent/PLAN.md", "plan");
215
216 let large_content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
217 let builder = PromptContentBuilder::new(&workspace).with_plan(large_content);
218
219 assert!(builder.has_oversize_content());
220
221 let refs = builder.build();
222 let rendered = refs.plan_for_template();
223 assert!(rendered.contains(".agent/PLAN.md"));
224 assert!(rendered.contains("plan.xml"));
225 assert!(!refs.plan_is_inline());
226 }
227
228 #[test]
229 fn test_builder_large_diff() {
230 let workspace = MemoryWorkspace::new_test();
231
232 let large_content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
233 let builder = PromptContentBuilder::new(&workspace).with_diff(large_content, "abc123def");
234
235 assert!(builder.has_oversize_content());
236
237 let refs = builder.build();
238 let rendered = refs.diff_for_template();
239 assert!(
240 rendered.contains(".agent/tmp/diff.txt"),
241 "Oversize diff should reference .agent/tmp/diff.txt: {}",
242 &rendered[..rendered.len().min(200)]
243 );
244 assert!(workspace.was_written(".agent/tmp/diff.txt"));
246 assert!(!refs.diff_is_inline());
247 }
248
249 #[test]
250 fn test_builder_no_oversize_when_all_small() {
251 let workspace = MemoryWorkspace::new_test().with_file("PROMPT.md", "test");
252
253 let builder = PromptContentBuilder::new(&workspace)
254 .with_prompt("Small prompt".to_string())
255 .with_plan("Small plan".to_string())
256 .with_diff("Small diff".to_string(), "abc123");
257
258 assert!(!builder.has_oversize_content());
259
260 let refs = builder.build();
261 assert!(refs.prompt_is_inline());
262 assert!(refs.plan_is_inline());
263 assert!(refs.diff_is_inline());
264 }
265
266 #[test]
267 fn test_builder_partial_oversize() {
268 let workspace = MemoryWorkspace::new_test().with_file(".agent/PROMPT.md.backup", "backup");
269
270 let large_prompt = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
272 let builder = PromptContentBuilder::new(&workspace)
273 .with_prompt(large_prompt)
274 .with_plan("Small plan".to_string())
275 .with_diff("Small diff".to_string(), "abc123");
276
277 assert!(builder.has_oversize_content());
278
279 let refs = builder.build();
280 assert!(!refs.prompt_is_inline());
281 assert!(refs.plan_is_inline());
282 assert!(refs.diff_is_inline());
283 }
284
285 #[test]
286 fn test_builder_empty_content() {
287 let workspace = MemoryWorkspace::new_test();
288
289 let refs = PromptContentBuilder::new(&workspace).build();
290
291 assert_eq!(refs.prompt_for_template(), "");
292 assert_eq!(refs.plan_for_template(), "");
293 assert_eq!(refs.diff_for_template(), "");
294 }
295
296 #[test]
297 fn test_refs_inline_checks_with_none() {
298 let refs = PromptContentReferences {
299 prompt: None,
300 plan: None,
301 diff: None,
302 };
303
304 assert!(!refs.prompt_is_inline());
306 assert!(!refs.plan_is_inline());
307 assert!(!refs.diff_is_inline());
308 }
309}