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 #[must_use]
42 pub fn with_prompt(self, content: String) -> Self {
43 let backup_path = self.workspace.prompt_backup();
44 Self {
45 prompt_ref: Some(PromptContentReference::from_content(
46 content,
47 &backup_path,
48 "Original user requirements from PROMPT.md",
49 )),
50 ..self
51 }
52 }
53
54 #[must_use]
59 pub fn with_plan(self, content: String) -> Self {
60 let plan_path = Path::new(".agent/PLAN.md");
61 let xml_fallback = Path::new(".agent/tmp/plan.xml");
62 Self {
63 plan_ref: Some(PlanContentReference::from_plan(
64 content,
65 plan_path,
66 Some(xml_fallback),
67 )),
68 ..self
69 }
70 }
71
72 #[must_use]
77 pub fn with_diff(self, content: String, start_commit: &str) -> Self {
78 let is_oversize = content.len() > MAX_INLINE_CONTENT_SIZE;
81 if is_oversize {
82 let tmp_dir = Path::new(".agent/tmp");
83 let diff_rel = tmp_dir.join("diff.txt");
84 if self.workspace.create_dir_all(tmp_dir).is_ok() {
85 let _ = self.workspace.write(&diff_rel, &content);
86 }
87 }
88
89 let diff_abs = self.workspace.absolute(Path::new(".agent/tmp/diff.txt"));
90 Self {
91 diff_ref: Some(DiffContentReference::from_diff(
92 content,
93 start_commit,
94 &diff_abs,
95 )),
96 ..self
97 }
98 }
99
100 #[must_use]
105 pub fn build(self) -> PromptContentReferences {
106 PromptContentReferences {
107 prompt: self.prompt_ref,
108 plan: self.plan_ref,
109 diff: self.diff_ref,
110 }
111 }
112
113 #[must_use]
118 pub fn has_oversize_content(&self) -> bool {
119 let prompt_oversize = self.prompt_ref.as_ref().is_some_and(|r| !r.is_inline());
120 let plan_oversize = self.plan_ref.as_ref().is_some_and(|r| !r.is_inline());
121 let diff_oversize = self.diff_ref.as_ref().is_some_and(|r| !r.is_inline());
122
123 prompt_oversize || plan_oversize || diff_oversize
124 }
125}
126
127pub struct PromptContentReferences {
132 pub prompt: Option<PromptContentReference>,
134 pub plan: Option<PlanContentReference>,
136 pub diff: Option<DiffContentReference>,
138}
139
140impl PromptContentReferences {
141 #[must_use]
145 pub fn prompt_for_template(&self) -> String {
146 self.prompt
147 .as_ref()
148 .map(super::content_reference::PromptContentReference::render_for_template)
149 .unwrap_or_default()
150 }
151
152 #[must_use]
156 pub fn plan_for_template(&self) -> String {
157 self.plan
158 .as_ref()
159 .map(super::content_reference::PlanContentReference::render_for_template)
160 .unwrap_or_default()
161 }
162
163 #[must_use]
167 pub fn diff_for_template(&self) -> String {
168 self.diff
169 .as_ref()
170 .map(super::content_reference::DiffContentReference::render_for_template)
171 .unwrap_or_default()
172 }
173
174 #[must_use]
176 pub fn prompt_is_inline(&self) -> bool {
177 self.prompt
178 .as_ref()
179 .is_some_and(super::content_reference::PromptContentReference::is_inline)
180 }
181
182 #[must_use]
184 pub fn plan_is_inline(&self) -> bool {
185 self.plan
186 .as_ref()
187 .is_some_and(super::content_reference::PlanContentReference::is_inline)
188 }
189
190 #[must_use]
192 pub fn diff_is_inline(&self) -> bool {
193 self.diff
194 .as_ref()
195 .is_some_and(super::content_reference::DiffContentReference::is_inline)
196 }
197}
198
199#[cfg(all(test, feature = "test-utils"))]
200mod tests {
201 use super::*;
202 use crate::prompts::MAX_INLINE_CONTENT_SIZE;
203 use crate::workspace::MemoryWorkspace;
204
205 #[test]
206 fn test_builder_small_content() {
207 let workspace = MemoryWorkspace::new_test().with_file("PROMPT.md", "test");
208
209 let builder = PromptContentBuilder::new(&workspace)
210 .with_prompt("Small prompt".to_string())
211 .with_plan("Small plan".to_string());
212
213 assert!(!builder.has_oversize_content());
214
215 let refs = builder.build();
216 assert_eq!(refs.prompt_for_template(), "Small prompt");
217 assert_eq!(refs.plan_for_template(), "Small plan");
218 }
219
220 #[test]
221 fn test_builder_large_prompt() {
222 let workspace = MemoryWorkspace::new_test().with_file(".agent/PROMPT.md.backup", "backup");
223
224 let large_content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
225 let builder = PromptContentBuilder::new(&workspace).with_prompt(large_content);
226
227 assert!(builder.has_oversize_content());
228
229 let refs = builder.build();
230 let rendered = refs.prompt_for_template();
231 assert!(rendered.contains("PROMPT.md.backup"));
232 assert!(!refs.prompt_is_inline());
233 }
234
235 #[test]
236 fn test_builder_large_plan() {
237 let workspace = MemoryWorkspace::new_test().with_file(".agent/PLAN.md", "plan");
238
239 let large_content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
240 let builder = PromptContentBuilder::new(&workspace).with_plan(large_content);
241
242 assert!(builder.has_oversize_content());
243
244 let refs = builder.build();
245 let rendered = refs.plan_for_template();
246 assert!(rendered.contains(".agent/PLAN.md"));
247 assert!(rendered.contains("plan.xml"));
248 assert!(!refs.plan_is_inline());
249 }
250
251 #[test]
252 fn test_builder_large_diff() {
253 let workspace = MemoryWorkspace::new_test();
254
255 let large_content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
256 let builder = PromptContentBuilder::new(&workspace).with_diff(large_content, "abc123def");
257
258 assert!(builder.has_oversize_content());
259
260 let refs = builder.build();
261 let rendered = refs.diff_for_template();
262 assert!(
263 rendered.contains(".agent/tmp/diff.txt"),
264 "Oversize diff should reference .agent/tmp/diff.txt: {}",
265 &rendered[..rendered.len().min(200)]
266 );
267 assert!(workspace.was_written(".agent/tmp/diff.txt"));
269 assert!(!refs.diff_is_inline());
270 }
271
272 #[test]
273 fn test_builder_no_oversize_when_all_small() {
274 let workspace = MemoryWorkspace::new_test().with_file("PROMPT.md", "test");
275
276 let builder = PromptContentBuilder::new(&workspace)
277 .with_prompt("Small prompt".to_string())
278 .with_plan("Small plan".to_string())
279 .with_diff("Small diff".to_string(), "abc123");
280
281 assert!(!builder.has_oversize_content());
282
283 let refs = builder.build();
284 assert!(refs.prompt_is_inline());
285 assert!(refs.plan_is_inline());
286 assert!(refs.diff_is_inline());
287 }
288
289 #[test]
290 fn test_builder_partial_oversize() {
291 let workspace = MemoryWorkspace::new_test().with_file(".agent/PROMPT.md.backup", "backup");
292
293 let large_prompt = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
295 let builder = PromptContentBuilder::new(&workspace)
296 .with_prompt(large_prompt)
297 .with_plan("Small plan".to_string())
298 .with_diff("Small diff".to_string(), "abc123");
299
300 assert!(builder.has_oversize_content());
301
302 let refs = builder.build();
303 assert!(!refs.prompt_is_inline());
304 assert!(refs.plan_is_inline());
305 assert!(refs.diff_is_inline());
306 }
307
308 #[test]
309 fn test_builder_empty_content() {
310 let workspace = MemoryWorkspace::new_test();
311
312 let refs = PromptContentBuilder::new(&workspace).build();
313
314 assert_eq!(refs.prompt_for_template(), "");
315 assert_eq!(refs.plan_for_template(), "");
316 assert_eq!(refs.diff_for_template(), "");
317 }
318
319 #[test]
320 fn test_refs_inline_checks_with_none() {
321 let refs = PromptContentReferences {
322 prompt: None,
323 plan: None,
324 diff: None,
325 };
326
327 assert!(!refs.prompt_is_inline());
329 assert!(!refs.plan_is_inline());
330 assert!(!refs.diff_is_inline());
331 }
332}