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