use crate::agent_backend::{AgentBackend, build_agent_backend, build_agent_backend_with_tee};
use crate::artifacts::{RunArtifacts, SessionDotfileBackups};
use crate::cli::one_shot_session::{
finish_one_shot_after_prompt, resolve_one_shot_request_artifacts,
};
use crate::cli::run_emit::{
RunStartupEmitOpts, emit_command_line, emit_run_logs_line, emit_run_startup_banner,
};
use crate::cli::{AgentStdoutTeeFlags, SharedOpts, WorkflowCliOptions};
use crate::output::agent_stdout_tee_enabled;
#[path = "do_flow_acp.rs"]
mod do_flow_acp;
pub(crate) mod do_flow_prompt;
use do_flow_acp::run_do_acp;
pub use do_flow_prompt::{
combine_do_acp_prompt_header_and_user, combine_do_prompt_file_and_user,
combine_do_raw_header_and_user, prepare_do_prompt_store,
};
#[derive(Debug)]
pub struct DoArgs {
pub request: Option<String>,
}
struct DoRunPrep {
client: AgentBackend,
artifacts: RunArtifacts,
coder: do_flow_prompt::DoCoderRun,
session_dotfile_backups: SessionDotfileBackups,
}
fn new_do_client(
shared: &SharedOpts,
workflow: WorkflowCliOptions,
) -> Result<AgentBackend, String> {
if shared.verbose {
return build_agent_backend(shared, workflow, shared.acp_stdout_markdown_enabled(), "do");
}
let interactive = agent_stdout_tee_enabled();
let emit_markdown = interactive && shared.acp_stdout_markdown_enabled();
let tee = if interactive {
AgentStdoutTeeFlags {
emit_stdout_markdown: emit_markdown,
raw_output: false,
show_thoughts_on_stdout: false,
}
} else {
AgentStdoutTeeFlags {
emit_stdout_markdown: false,
raw_output: true,
show_thoughts_on_stdout: false,
}
};
build_agent_backend_with_tee(shared, workflow, tee)
}
async fn prepare_do_run(
do_args: &DoArgs,
shared: &SharedOpts,
workflow: WorkflowCliOptions,
) -> Result<DoRunPrep, String> {
let mut client = new_do_client(shared, workflow)?;
let (text, artifacts) = resolve_one_shot_request_artifacts(
do_args.request.as_ref(),
"--do",
Some(crate::run_id::RunDirOptions { gc: false }),
)?;
if shared.verbose {
emit_run_startup_banner(
&artifacts,
RunStartupEmitOpts::from_shared(shared, true),
&text,
)?;
} else {
emit_command_line(&artifacts.run_dir, false)?;
}
crate::run_id::maybe_gc_after_run_created(&artifacts.work_dir, &artifacts.run_dir);
client.ensure_authenticated().map_err(|e| e.to_string())?;
client.prompts_log_run_dir = Some(artifacts.run_dir.clone());
let (coder, session_dotfile_backups) =
begin_do_session_overlapping_prompt_prep(&mut client, &artifacts, &text, shared).await?;
Ok(DoRunPrep {
client,
artifacts,
coder,
session_dotfile_backups,
})
}
async fn begin_do_session_overlapping_prompt_prep(
client: &mut AgentBackend,
artifacts: &RunArtifacts,
text: &str,
shared: &SharedOpts,
) -> Result<(do_flow_prompt::DoCoderRun, SessionDotfileBackups), String> {
let begin = client.begin_coder_session(&artifacts.work_dir);
let model = shared.model.canonical();
let git = shared.git;
let coder_backup = async {
let coder = do_flow_prompt::build_do_coder_run(
artifacts,
text,
crate::workflow_context::PromptModelOpts::new(&model, git),
)?;
let session_dotfile_backups =
SessionDotfileBackups::snapshot_after_ensuring_home_config(&artifacts.work_dir)?;
Ok::<_, String>((coder, session_dotfile_backups))
};
let (begin_res, coder_backup_res) = tokio::join!(begin, coder_backup);
begin_res.map_err(|e| e.to_string())?;
coder_backup_res
}
pub async fn run_do(
do_args: DoArgs,
shared: &SharedOpts,
workflow: WorkflowCliOptions,
) -> Result<(), String> {
let interactive = agent_stdout_tee_enabled();
let emit_markdown = interactive && shared.acp_stdout_markdown_enabled();
let dm_only = !shared.verbose;
crate::output::set_do_dm_stdout_opts(crate::output::DoDmStdoutOpts {
enabled: dm_only,
emit_markdown: dm_only && emit_markdown,
});
crate::output::set_heartbeat_stdout_suppressed(dm_only);
let result = run_do_body(do_args, shared, workflow).await;
crate::output::set_do_dm_stdout_opts(crate::output::DoDmStdoutOpts::default());
crate::output::set_heartbeat_stdout_suppressed(false);
result
}
async fn run_do_body(
do_args: DoArgs,
shared: &SharedOpts,
workflow: WorkflowCliOptions,
) -> Result<(), String> {
let mut prep = prepare_do_run(&do_args, shared, workflow).await?;
if shared.verbose {
emit_run_logs_line(&prep.artifacts)?;
}
let acp_res = run_do_acp(&mut prep.client, &prep.artifacts, prep.coder).await;
finish_one_shot_after_prompt(
acp_res,
&prep.artifacts.work_dir,
&prep.session_dotfile_backups,
&prep.artifacts.artifact_result_md(),
)?;
Ok(())
}
#[cfg(test)]
mod do_snapshot_tests {
use super::SessionDotfileBackups;
use crate::malvin_config_path;
use crate::test_utils::with_isolated_home;
#[test]
fn snapshot_do_session_dotfiles_on_empty_workdir() {
let tmp = tempfile::tempdir().expect("tempdir");
SessionDotfileBackups::snapshot(tmp.path()).expect("snapshot");
}
#[test]
fn do_prepare_snapshot_ensures_home_config_exists() {
with_isolated_home(|work| {
let cfg = malvin_config_path(work);
assert!(!cfg.exists());
SessionDotfileBackups::snapshot_after_ensuring_home_config(work).expect("snapshot");
assert!(
cfg.is_file(),
"do session snapshot must ensure ~/.malvin_home/config.toml exists"
);
});
}
}
#[cfg(test)]
mod kiss_static_fn_item_refs {
use super::do_flow_acp::run_do_coder_prompt;
use super::{run_do, run_do_acp};
#[test]
fn kiss_static_fn_item_refs() {
let _ = run_do;
let _ = run_do_acp;
let _ = run_do_coder_prompt;
}
}
#[cfg(test)]
#[allow(unused_imports)]
mod kiss_cov_gate_refs {
use super::*;
#[test]
fn kiss_cov_unit_names() {
let _: Option<DoRunPrep> = None;
let _ = new_do_client;
let _ = prepare_do_run;
let _ = begin_do_session_overlapping_prompt_prep;
}
}
#[cfg(test)]
#[path = "do_flow_kiss_cov_tests.rs"]
mod do_flow_kiss_cov_tests;