1use super::effect::{AppEffect, AppEffectHandler, AppEffectResult};
31use std::path::PathBuf;
32use thiserror::Error;
33
34const PLAN_XSD_SCHEMA: &str = include_str!("../files/llm_output_extraction/plan.xsd");
36const DEVELOPMENT_RESULT_XSD_SCHEMA: &str =
37 include_str!("../files/llm_output_extraction/development_result.xsd");
38const DEVELOPMENT_CONTINUATION_RESULT_XSD_SCHEMA: &str =
39 include_str!("../files/llm_output_extraction/development_continuation_result.xsd");
40const ISSUES_XSD_SCHEMA: &str = include_str!("../files/llm_output_extraction/issues.xsd");
41const FIX_RESULT_XSD_SCHEMA: &str = include_str!("../files/llm_output_extraction/fix_result.xsd");
42const COMMIT_MESSAGE_XSD_SCHEMA: &str =
43 include_str!("../files/llm_output_extraction/commit_message.xsd");
44
45use crate::files::context::{VAGUE_ISSUES_LINE, VAGUE_NOTES_LINE, VAGUE_STATUS_LINE};
47
48#[derive(Debug, Error)]
49pub enum AppEffectError {
50 #[error("effect {effect:?} handler failed: {message}")]
51 Handler { effect: AppEffect, message: String },
52 #[error("effect {effect:?} returned unexpected result: {result:?}")]
53 UnexpectedResult {
54 effect: AppEffect,
55 result: AppEffectResult,
56 },
57}
58
59fn execute_expect_ok<H: AppEffectHandler>(
60 handler: &mut H,
61 effect: AppEffect,
62) -> Result<(), AppEffectError> {
63 match handler.execute(effect.clone()) {
64 AppEffectResult::Ok => Ok(()),
65 AppEffectResult::Error(message) => Err(AppEffectError::Handler { effect, message }),
66 other => Err(AppEffectError::UnexpectedResult {
67 effect,
68 result: other,
69 }),
70 }
71}
72
73fn execute_expect_string<H: AppEffectHandler>(
74 handler: &mut H,
75 effect: AppEffect,
76) -> Result<String, AppEffectError> {
77 match handler.execute(effect.clone()) {
78 AppEffectResult::String(value) => Ok(value),
79 AppEffectResult::Error(message) => Err(AppEffectError::Handler { effect, message }),
80 other => Err(AppEffectError::UnexpectedResult {
81 effect,
82 result: other,
83 }),
84 }
85}
86
87fn execute_expect_path<H: AppEffectHandler>(
88 handler: &mut H,
89 effect: AppEffect,
90) -> Result<PathBuf, AppEffectError> {
91 match handler.execute(effect.clone()) {
92 AppEffectResult::Path(path) => Ok(path),
93 AppEffectResult::Error(message) => Err(AppEffectError::Handler { effect, message }),
94 other => Err(AppEffectError::UnexpectedResult {
95 effect,
96 result: other,
97 }),
98 }
99}
100
101fn execute_expect_bool<H: AppEffectHandler>(
102 handler: &mut H,
103 effect: AppEffect,
104) -> Result<bool, AppEffectError> {
105 match handler.execute(effect.clone()) {
106 AppEffectResult::Bool(value) => Ok(value),
107 AppEffectResult::Error(message) => Err(AppEffectError::Handler { effect, message }),
108 other => Err(AppEffectError::UnexpectedResult {
109 effect,
110 result: other,
111 }),
112 }
113}
114
115pub fn handle_reset_start_commit<H: AppEffectHandler>(
141 handler: &mut H,
142 working_dir_override: Option<&PathBuf>,
143) -> Result<String, AppEffectError> {
144 if let Some(dir) = working_dir_override {
145 execute_expect_ok(handler, AppEffect::SetCurrentDir { path: dir.clone() })?;
146 }
147
148 execute_expect_ok(handler, AppEffect::GitRequireRepo)?;
149
150 let repo_root = execute_expect_path(handler, AppEffect::GitGetRepoRoot)?;
151
152 if working_dir_override.is_none() {
153 execute_expect_ok(
154 handler,
155 AppEffect::SetCurrentDir {
156 path: repo_root.clone(),
157 },
158 )?;
159 }
160
161 execute_expect_string(handler, AppEffect::GitResetStartCommit)
162}
163
164pub fn save_start_commit<H: AppEffectHandler>(handler: &mut H) -> Result<String, AppEffectError> {
177 execute_expect_string(handler, AppEffect::GitSaveStartCommit)
178}
179
180pub fn is_on_main_branch<H: AppEffectHandler>(handler: &mut H) -> Result<bool, AppEffectError> {
190 execute_expect_bool(handler, AppEffect::GitIsMainBranch)
191}
192
193pub fn get_head_oid<H: AppEffectHandler>(handler: &mut H) -> Result<String, AppEffectError> {
203 execute_expect_string(handler, AppEffect::GitGetHeadOid)
204}
205
206pub fn require_repo<H: AppEffectHandler>(handler: &mut H) -> Result<(), AppEffectError> {
216 execute_expect_ok(handler, AppEffect::GitRequireRepo)
217}
218
219pub fn get_repo_root<H: AppEffectHandler>(handler: &mut H) -> Result<PathBuf, AppEffectError> {
229 execute_expect_path(handler, AppEffect::GitGetRepoRoot)
230}
231
232pub fn ensure_files_effectful<H: AppEffectHandler>(
260 handler: &mut H,
261 isolation_mode: bool,
262) -> Result<(), AppEffectError> {
263 execute_expect_ok(
264 handler,
265 AppEffect::CreateDir {
266 path: PathBuf::from(".agent/logs"),
267 },
268 )?;
269
270 execute_expect_ok(
271 handler,
272 AppEffect::CreateDir {
273 path: PathBuf::from(".agent/tmp"),
274 },
275 )?;
276
277 let schemas = [
278 (".agent/tmp/plan.xsd", PLAN_XSD_SCHEMA),
279 (
280 ".agent/tmp/development_result.xsd",
281 DEVELOPMENT_RESULT_XSD_SCHEMA,
282 ),
283 (
284 ".agent/tmp/development_continuation_result.xsd",
285 DEVELOPMENT_CONTINUATION_RESULT_XSD_SCHEMA,
286 ),
287 (".agent/tmp/issues.xsd", ISSUES_XSD_SCHEMA),
288 (".agent/tmp/fix_result.xsd", FIX_RESULT_XSD_SCHEMA),
289 (".agent/tmp/commit_message.xsd", COMMIT_MESSAGE_XSD_SCHEMA),
290 ];
291
292 schemas
293 .iter()
294 .map(|(path, content)| {
295 execute_expect_ok(
296 handler,
297 AppEffect::WriteFile {
298 path: PathBuf::from(*path),
299 content: content.to_string(),
300 },
301 )
302 })
303 .collect::<Result<Vec<_>, _>>()?;
304
305 if !isolation_mode {
306 let context_files = [
307 (".agent/STATUS.md", VAGUE_STATUS_LINE),
308 (".agent/NOTES.md", VAGUE_NOTES_LINE),
309 (".agent/ISSUES.md", VAGUE_ISSUES_LINE),
310 ];
311
312 context_files
313 .iter()
314 .map(|(path, line)| {
315 let content = format!("{}\n", line.lines().next().unwrap_or_default().trim());
316 execute_expect_ok(
317 handler,
318 AppEffect::WriteFile {
319 path: PathBuf::from(*path),
320 content,
321 },
322 )
323 })
324 .collect::<Result<Vec<_>, _>>()?;
325 }
326
327 Ok(())
328}
329
330pub fn reset_context_for_isolation_effectful<H: AppEffectHandler>(
353 handler: &mut H,
354) -> Result<(), AppEffectError> {
355 let context_files = [
356 PathBuf::from(".agent/STATUS.md"),
357 PathBuf::from(".agent/NOTES.md"),
358 PathBuf::from(".agent/ISSUES.md"),
359 ];
360
361 context_files
362 .iter()
363 .map(|path| -> Result<(), AppEffectError> {
364 let exists =
365 execute_expect_bool(handler, AppEffect::PathExists { path: path.clone() })?;
366 if exists {
367 execute_expect_ok(handler, AppEffect::DeleteFile { path: path.clone() })?;
368 }
369 Ok(())
370 })
371 .collect::<Result<Vec<_>, _>>()?;
372
373 Ok(())
374}
375
376pub fn check_prompt_exists_effectful<H: AppEffectHandler>(
394 handler: &mut H,
395) -> Result<bool, AppEffectError> {
396 execute_expect_bool(
397 handler,
398 AppEffect::PathExists {
399 path: PathBuf::from("PROMPT.md"),
400 },
401 )
402}
403
404#[cfg(test)]
405mod tests;