use std::collections::HashMap;
use clap::Args;
use crate::agent_backend::{AgentBackend, build_agent_backend};
use crate::artifacts::{RunArtifacts, SessionDotfileBackups};
use crate::cli::cli_request::require_cli_request;
use crate::cli::one_shot_session::{
OneShotCoderGuard, finish_one_shot_after_prompt, finish_one_shot_auth_and_backups,
resolve_one_shot_request_artifacts,
};
use crate::cli::{SharedOpts, WorkflowCliOptions};
use crate::prompts::{INSPIRE_SUMMARIZE_MD, PromptError, PromptStore, render_inspire_mbc2_prompt};
use crate::run_timing::TimingPhase;
#[derive(Args, Debug)]
#[command(override_usage = "malvin inspire [OPTION]... [REQUEST]")]
pub struct InspireArgs {
pub request: Option<String>,
}
struct InspireRunPrep {
client: AgentBackend,
artifacts: RunArtifacts,
prompt: String,
summarize_prompt: String,
session_dotfile_backups: SessionDotfileBackups,
}
fn prepare_inspire_prompt_store() -> Result<PromptStore, String> {
let store = PromptStore::default_store();
store
.validate_exists("mbc2.md")
.map_err(|e: PromptError| e.0)?;
store
.validate_exists(INSPIRE_SUMMARIZE_MD)
.map_err(|e: PromptError| e.0)?;
Ok(store)
}
pub fn build_inspire_render_context(user_prompt: &str) -> HashMap<String, String> {
HashMap::from([("user_prompt".into(), user_prompt.to_string())])
}
pub fn render_inspire_prompt(user_prompt: &str) -> Result<String, String> {
let store = prepare_inspire_prompt_store()?;
let ctx = build_inspire_render_context(user_prompt);
render_inspire_mbc2_prompt(&store, &ctx).map_err(|e| e.0)
}
pub fn render_inspire_summarize_prompt() -> Result<String, String> {
let store = prepare_inspire_prompt_store()?;
store
.render_prompt_only(INSPIRE_SUMMARIZE_MD, &HashMap::new())
.map(|s| s.trim().to_string())
.map_err(|e: PromptError| e.0)
}
fn new_inspire_client(
shared: &SharedOpts,
workflow: WorkflowCliOptions,
) -> Result<AgentBackend, String> {
build_agent_backend(
shared,
workflow,
shared.acp_stdout_markdown_enabled(),
"inspire",
)
}
fn inspire_emit_startup_banner(
inspire: &InspireArgs,
shared: &SharedOpts,
artifacts: &RunArtifacts,
) -> Result<(), String> {
let request = require_cli_request(inspire.request.as_ref(), "inspire")?;
crate::cli::run_emit::emit_run_startup_banner(
artifacts,
crate::cli::run_emit::RunStartupEmitOpts::from_shared(shared, true),
&request,
)
}
async fn prepare_inspire_run(
inspire: &InspireArgs,
shared: &SharedOpts,
workflow: WorkflowCliOptions,
) -> Result<InspireRunPrep, String> {
let mut client = new_inspire_client(shared, workflow)?;
let (text, artifacts) = resolve_one_shot_request_artifacts(
inspire.request.as_ref(),
"inspire",
Some(crate::run_id::RunDirOptions { gc: false }),
)?;
inspire_emit_startup_banner(inspire, shared, &artifacts)?;
crate::run_id::maybe_gc_after_run_created(&artifacts.work_dir, &artifacts.run_dir);
let session_dotfile_backups = finish_one_shot_auth_and_backups(&mut client, &artifacts)?;
let prompt = render_inspire_prompt(&text)?;
let summarize_prompt = render_inspire_summarize_prompt()?;
Ok(InspireRunPrep {
client,
artifacts,
prompt,
summarize_prompt,
session_dotfile_backups,
})
}
pub async fn run_inspire(
inspire: InspireArgs,
shared: &SharedOpts,
workflow: WorkflowCliOptions,
) -> Result<(), String> {
let mut prep = prepare_inspire_run(&inspire, shared, workflow).await?;
prep.client
.begin_coder_session(&prep.artifacts.work_dir)
.await
.map_err(|e| e.to_string())?;
crate::cli::run_emit::emit_run_logs_line(&prep.artifacts)?;
let acp_res = run_inspire_coder_session(
&mut prep.client,
&prep.artifacts,
&prep.prompt,
&prep.summarize_prompt,
)
.await;
finish_one_shot_after_prompt(
acp_res,
&prep.artifacts.work_dir,
&prep.session_dotfile_backups,
&prep.artifacts.artifact_result_md(),
)?;
Ok(())
}
struct InspireCoderPrompt<'a> {
prompt: &'a str,
who: &'a str,
stdout_bracket_label: &'a str,
}
async fn run_inspire_coder_prompt(
client: &mut AgentBackend,
artifacts: &RunArtifacts,
spec: &InspireCoderPrompt<'_>,
) -> Result<(), String> {
client
.run_coder_prompt(
spec.prompt,
&artifacts.log_path(spec.who),
spec.who,
crate::acp::CoderPromptOptions {
llm_phase: Some(TimingPhase::Implement),
do_trace_split: None,
stdout_bracket_label: Some(spec.stdout_bracket_label),
..Default::default()
},
)
.await
.map_err(|e| e.to_string())
}
async fn run_inspire_coder_session(
client: &mut AgentBackend,
artifacts: &RunArtifacts,
prompt: &str,
summarize_prompt: &str,
) -> Result<(), String> {
let guard = OneShotCoderGuard::begin(client, artifacts, "inspire").await?;
let run_res = async {
run_inspire_coder_prompt(
client,
artifacts,
&InspireCoderPrompt {
prompt,
who: "inspire",
stdout_bracket_label: "mbc2.md",
},
)
.await?;
run_inspire_coder_prompt(
client,
artifacts,
&InspireCoderPrompt {
prompt: summarize_prompt,
who: "inspire_summarize",
stdout_bracket_label: INSPIRE_SUMMARIZE_MD,
},
)
.await
}
.await;
guard.finish(client, run_res).await
}
#[cfg(test)]
mod inspire_snapshot_tests {
use super::SessionDotfileBackups;
use crate::malvin_config_path;
use crate::test_utils::with_isolated_home;
#[test]
fn inspire_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(),
"inspire session snapshot must ensure ~/.malvin_home/config.toml exists"
);
});
}
}
#[cfg(test)]
#[path = "inspire_flow_tests.rs"]
mod inspire_flow_tests;
#[cfg(test)]
#[allow(unused_imports)]
mod kiss_cov_gate_refs {
use super::*;
#[test]
fn kiss_cov_unit_names() {
let _: Option<InspireRunPrep> = None;
let _ = new_inspire_client;
let _ = inspire_emit_startup_banner;
let _ = prepare_inspire_prompt_store;
let _ = prepare_inspire_run;
let _ = run_inspire;
let _ = run_inspire_coder_session;
let _ = run_inspire_coder_prompt;
let _ = render_inspire_summarize_prompt;
let _: Option<InspireCoderPrompt<'_>> = None;
}
}