ralph-workflow 0.7.18

PROMPT-driven multi-agent orchestrator for git repos
Documentation
use super::MainEffectHandler;
use crate::agents::AgentRole;
use crate::files::llm_output_extraction::file_based_extraction::paths as xml_paths;
use crate::phases::development::boundary_domain::{
    build_continuation_context_markdown, derive_development_status, parse_files_changed_lines,
    select_representation_by_inline_budget,
};
use crate::phases::PhaseContext;
use crate::prompts::content_reference::MAX_INLINE_CONTENT_SIZE;
use crate::reducer::effect::{ContinuationContextData, EffectResult};
use crate::reducer::event::{AgentEvent, ErrorEvent, PipelineEvent, WorkspaceIoErrorKind};
use crate::reducer::prompt_inputs::sha256_hex_str;
use crate::reducer::state::{MaterializedPromptInput, PromptInputKind};
use crate::reducer::ui_event::{UIEvent, XmlOutputContext, XmlOutputType};
use crate::workspace::Workspace;
use anyhow::Result;
use std::path::Path;
impl MainEffectHandler {
    pub(in crate::reducer::boundary) fn prepare_development_context(
        ctx: &PhaseContext<'_>,
        iteration: u32,
    ) -> EffectResult {
        let _ = crate::files::create_prompt_backup_with_workspace(ctx.workspace);
        EffectResult::event(PipelineEvent::development_context_prepared(iteration))
    }

    pub(in crate::reducer::boundary) fn invoke_development_agent(
        &mut self,
        ctx: &mut PhaseContext<'_>,
        iteration: u32,
    ) -> Result<EffectResult> {
        self.normalize_agent_chain_for_invocation(ctx, crate::agents::AgentDrain::Development);
        let prompt = ctx
            .workspace
            .read(Path::new(".agent/tmp/development_prompt.txt"))
            .map_err(|_| ErrorEvent::DevelopmentPromptMissing { iteration })?;
        let agent = self
            .state
            .agent_chain
            .current_agent()
            .cloned()
            .unwrap_or_else(|| ctx.developer_agent.to_string());
        let result = self.invoke_agent(
            ctx,
            crate::agents::AgentDrain::Development,
            AgentRole::Developer,
            &agent,
            None,
            prompt,
        )?;
        let result = if result.additional_events.iter().any(|e| {
            matches!(
                e,
                PipelineEvent::Agent(AgentEvent::InvocationSucceeded { .. })
            )
        }) {
            result.with_additional_event(PipelineEvent::development_agent_invoked(iteration))
        } else {
            result
        };
        Ok(result)
    }

    pub(in crate::reducer::boundary) fn archive_development_xml(
        ctx: &PhaseContext<'_>,
        iteration: u32,
    ) -> EffectResult {
        use crate::files::llm_output_extraction::archive_xml_file_with_workspace;
        archive_xml_file_with_workspace(
            ctx.workspace,
            Path::new(xml_paths::DEVELOPMENT_RESULT_XML),
        );
        EffectResult::event(PipelineEvent::development_xml_archived(iteration))
    }

    pub(in crate::reducer::boundary) fn apply_development_outcome(
        &self,
        _ctx: &mut PhaseContext<'_>,
        iteration: u32,
    ) -> Result<EffectResult> {
        self.state
            .development_validated_outcome
            .as_ref()
            .filter(|outcome| outcome.iteration == iteration)
            .ok_or(ErrorEvent::ValidatedDevelopmentOutcomeMissing { iteration })?;
        Ok(EffectResult::event(
            PipelineEvent::development_outcome_applied(iteration),
        ))
    }
}
pub(in crate::reducer::boundary) fn write_continuation_context_to_workspace(
    workspace: &dyn Workspace,
    logger: &crate::logger::Logger,
    data: &ContinuationContextData,
) -> Result<()> {
    let tmp_dir = Path::new(".agent/tmp");
    if !workspace.exists(tmp_dir) {
        workspace.create_dir_all(tmp_dir).map_err(|err| {
            ErrorEvent::WorkspaceCreateDirAllFailed {
                path: tmp_dir.display().to_string(),
                kind: WorkspaceIoErrorKind::from_io_error_kind(err.kind()),
            }
        })?;
    }
    let content = build_continuation_context_markdown(data);
    workspace
        .write(Path::new(".agent/tmp/continuation_context.md"), &content)
        .map_err(|err| ErrorEvent::WorkspaceWriteFailed {
            path: ".agent/tmp/continuation_context.md".to_string(),
            kind: WorkspaceIoErrorKind::from_io_error_kind(err.kind()),
        })?;
    logger.info("Continuation context written to .agent/tmp/continuation_context.md");
    Ok(())
}

impl MainEffectHandler {
    pub(in crate::reducer::boundary) fn materialize_development_inputs(
        &self,
        ctx: &PhaseContext<'_>,
        iteration: u32,
    ) -> Result<EffectResult> {
        let inline_budget_bytes = MAX_INLINE_CONTENT_SIZE as u64;
        let consumer_signature_sha256 = self.state.agent_chain.consumer_signature_sha256();
        let prompt_input = materialize_development_prompt_input(
            ctx,
            inline_budget_bytes,
            &consumer_signature_sha256,
        )?;
        let plan_input = materialize_development_plan_input(
            ctx,
            inline_budget_bytes,
            consumer_signature_sha256,
        )?;
        let result = EffectResult::event(PipelineEvent::development_inputs_materialized(
            iteration,
            prompt_input.clone(),
            plan_input.clone(),
        ));
        let result = apply_oversize_prompt_events(result, &prompt_input, inline_budget_bytes);
        let result = apply_oversize_plan_events(result, &plan_input, inline_budget_bytes);
        Ok(result)
    }
}

fn materialize_development_prompt_input(
    ctx: &PhaseContext<'_>,
    inline_budget_bytes: u64,
    consumer_signature_sha256: &str,
) -> Result<MaterializedPromptInput, ErrorEvent> {
    let prompt_md = ctx.workspace.read(Path::new("PROMPT.md")).map_err(|err| {
        ErrorEvent::WorkspaceReadFailed {
            path: "PROMPT.md".to_string(),
            kind: WorkspaceIoErrorKind::from_io_error_kind(err.kind()),
        }
    })?;
    let prompt_backup_path = Path::new(".agent/PROMPT.md.backup");
    if prompt_md.len() as u64 > inline_budget_bytes {
        crate::files::create_prompt_backup_with_workspace(ctx.workspace).map_err(|err| {
            ErrorEvent::WorkspaceWriteFailed {
                path: prompt_backup_path.display().to_string(),
                kind: WorkspaceIoErrorKind::from_io_error_kind(err.kind()),
            }
        })?;
        ctx.logger.warn(&format!(
            "PROMPT size ({} KB) exceeds inline limit ({} KB). Referencing: {}",
            (prompt_md.len() as u64) / 1024,
            inline_budget_bytes / 1024,
            prompt_backup_path.display()
        ));
    }
    let (representation, reason) = select_representation_by_inline_budget(
        prompt_md.len() as u64,
        inline_budget_bytes,
        prompt_backup_path,
    );
    Ok(MaterializedPromptInput {
        kind: PromptInputKind::Prompt,
        content_id_sha256: sha256_hex_str(&prompt_md),
        consumer_signature_sha256: consumer_signature_sha256.to_string(),
        original_bytes: prompt_md.len() as u64,
        final_bytes: prompt_md.len() as u64,
        model_budget_bytes: None,
        inline_budget_bytes: Some(inline_budget_bytes),
        representation,
        reason,
    })
}

fn materialize_development_plan_input(
    ctx: &PhaseContext<'_>,
    inline_budget_bytes: u64,
    consumer_signature_sha256: String,
) -> Result<MaterializedPromptInput, ErrorEvent> {
    let plan_path = Path::new(".agent/PLAN.md");
    let plan_md = ctx
        .workspace
        .read(plan_path)
        .map_err(|err| ErrorEvent::WorkspaceReadFailed {
            path: ".agent/PLAN.md".to_string(),
            kind: WorkspaceIoErrorKind::from_io_error_kind(err.kind()),
        })?;
    if plan_md.len() as u64 > inline_budget_bytes {
        ctx.logger.warn(&format!(
            "PLAN size ({} KB) exceeds inline limit ({} KB). Referencing: {}",
            (plan_md.len() as u64) / 1024,
            inline_budget_bytes / 1024,
            plan_path.display()
        ));
    }
    let (representation, reason) = select_representation_by_inline_budget(
        plan_md.len() as u64,
        inline_budget_bytes,
        plan_path,
    );
    Ok(MaterializedPromptInput {
        kind: PromptInputKind::Plan,
        content_id_sha256: sha256_hex_str(&plan_md),
        consumer_signature_sha256,
        original_bytes: plan_md.len() as u64,
        final_bytes: plan_md.len() as u64,
        model_budget_bytes: None,
        inline_budget_bytes: Some(inline_budget_bytes),
        representation,
        reason,
    })
}

fn apply_oversize_prompt_events(
    result: EffectResult,
    prompt_input: &MaterializedPromptInput,
    inline_budget_bytes: u64,
) -> EffectResult {
    if prompt_input.original_bytes > inline_budget_bytes {
        let result = result.with_ui_event(UIEvent::AgentActivity {
            agent: "pipeline".to_string(),
            message: format!(
                "Oversize PROMPT: {} KB > {} KB; using file reference",
                prompt_input.original_bytes / 1024,
                inline_budget_bytes / 1024
            ),
        });
        result.with_additional_event(PipelineEvent::prompt_input_oversize_detected(
            crate::reducer::event::PipelinePhase::Development,
            PromptInputKind::Prompt,
            prompt_input.content_id_sha256.clone(),
            prompt_input.original_bytes,
            inline_budget_bytes,
            "inline-embedding".to_string(),
        ))
    } else {
        result
    }
}

fn apply_oversize_plan_events(
    result: EffectResult,
    plan_input: &MaterializedPromptInput,
    inline_budget_bytes: u64,
) -> EffectResult {
    if plan_input.original_bytes > inline_budget_bytes {
        let result = result.with_ui_event(UIEvent::AgentActivity {
            agent: "pipeline".to_string(),
            message: format!(
                "Oversize PLAN: {} KB > {} KB; using file reference",
                plan_input.original_bytes / 1024,
                inline_budget_bytes / 1024
            ),
        });
        result.with_additional_event(PipelineEvent::prompt_input_oversize_detected(
            crate::reducer::event::PipelinePhase::Development,
            PromptInputKind::Plan,
            plan_input.content_id_sha256.clone(),
            plan_input.original_bytes,
            inline_budget_bytes,
            "inline-embedding".to_string(),
        ))
    } else {
        result
    }
}

const DEVELOPMENT_XSD_ERROR_PATH: &str = ".agent/tmp/development_xsd_error.txt";

impl MainEffectHandler {
    pub(in crate::reducer::boundary) fn extract_development_xml(
        &self,
        ctx: &PhaseContext<'_>,
        iteration: u32,
    ) -> EffectResult {
        let xml_path = Path::new(xml_paths::DEVELOPMENT_RESULT_XML);
        let initial_event = UIEvent::IterationProgress {
            current: iteration,
            total: self.state.total_iterations,
        };

        match ctx.workspace.read(xml_path) {
            Ok(content) => EffectResult::with_ui(
                PipelineEvent::development_xml_extracted(iteration),
                vec![
                    initial_event,
                    UIEvent::XmlOutput {
                        xml_type: XmlOutputType::DevelopmentResult,
                        content,
                        context: Some(XmlOutputContext {
                            iteration: Some(iteration),
                            pass: None,
                            snippets: Vec::new(),
                        }),
                    },
                ],
            ),
            Err(_) => EffectResult::with_ui(
                PipelineEvent::development_xml_missing(
                    iteration,
                    self.state.continuation.invalid_output_attempts,
                ),
                vec![initial_event],
            ),
        }
    }

    pub(in crate::reducer::boundary) fn validate_development_xml(
        &self,
        ctx: &PhaseContext<'_>,
        iteration: u32,
    ) -> EffectResult {
        let Ok(xml) = ctx
            .workspace
            .read(Path::new(xml_paths::DEVELOPMENT_RESULT_XML))
        else {
            return EffectResult::event(PipelineEvent::development_output_validation_failed(
                iteration,
                self.state.continuation.invalid_output_attempts,
            ));
        };

        let validation_result =
            dispatch_development_xml_validation(&xml, self.state.continuation.is_continuation());

        apply_development_validation_result(
            ctx,
            validation_result,
            iteration,
            self.state.continuation.invalid_output_attempts,
        )
    }
}

fn dispatch_development_xml_validation(
    xml: &str,
    is_continuation: bool,
) -> Result<
    crate::files::llm_output_extraction::DevelopmentResultElements,
    crate::files::llm_output_extraction::xsd_validation::XsdValidationError,
> {
    use crate::files::llm_output_extraction::{
        validate_continuation_development_result_xml, validate_development_result_xml,
    };
    if is_continuation {
        validate_continuation_development_result_xml(xml)
    } else {
        validate_development_result_xml(xml)
    }
}

fn apply_development_validation_result(
    ctx: &PhaseContext<'_>,
    validation_result: Result<
        crate::files::llm_output_extraction::DevelopmentResultElements,
        crate::files::llm_output_extraction::xsd_validation::XsdValidationError,
    >,
    iteration: u32,
    invalid_output_attempts: u32,
) -> EffectResult {
    match validation_result {
        Ok(elements) => {
            let _ = ctx
                .workspace
                .remove_if_exists(Path::new(DEVELOPMENT_XSD_ERROR_PATH));
            let status = derive_development_status(elements.is_completed(), elements.is_partial());
            let files_changed = parse_files_changed_lines(elements.files_changed.as_deref());
            EffectResult::event(PipelineEvent::development_xml_validated(
                iteration,
                status,
                elements.summary.clone(),
                files_changed,
                elements.next_steps,
            ))
        }
        Err(err) => {
            let _ = ctx.workspace.write(
                Path::new(DEVELOPMENT_XSD_ERROR_PATH),
                &err.format_for_ai_retry(),
            );
            EffectResult::event(PipelineEvent::development_output_validation_failed(
                iteration,
                invalid_output_attempts,
            ))
        }
    }
}