use std::{
io::{self, IsTerminal, Read, Write},
process::ExitCode,
};
use basis::{
Bound, Event, JsonlWriter, RunConfig, RunOutcome, ShellAccess, provider,
run::{EventSink, FnSink},
};
use mentra::ModelSelector;
use crate::{cli::RunArgs, exit::exit_code};
pub(crate) async fn execute_run(args: RunArgs) -> Result<ExitCode, String> {
let workspace = match args.workspace {
Some(path) => path,
None => {
std::env::current_dir().map_err(|error| format!("no working directory: {error}"))?
}
};
let mut config = RunConfig::new(workspace, prompt_from(args.prompt)?);
if let Some(name) = &args.provider {
config = config.with_provider(provider::parse(name).map_err(|error| error.to_string())?);
}
if let Some(base_url) = args.base_url {
config = config.with_base_url(base_url);
}
if let Some(model) = args.model {
config = config.with_model(ModelSelector::Id(model));
}
config = config.with_shell(ShellAccess::from_flag(!args.no_shell));
if let Some(effort) = args.effort {
config = config.with_effort(effort.into());
}
if let Some(deadline) = args.deadline {
config = config.with_deadline(deadline.duration());
}
if let Some(tool_budget) = args.tool_budget {
config = config.with_tool_budget(tool_budget);
}
if let Some(token_budget) = args.token_budget {
config = config.with_token_budget(token_budget);
}
let approver = args.approve.approver();
if args.json {
let report = basis::run_with_approver(config, JsonlWriter::new(io::stdout()), approver)
.await
.map_err(|error| error.to_string())?;
return Ok(ExitCode::from(exit_code(&report)));
}
let report = basis::run_with_approver(config, prose_sink(), approver)
.await
.map_err(|error| error.to_string())?;
if let RunOutcome::Error { message } = &report.outcome {
let what = match report.stopped_by {
Some(_) => "run stopped",
None => "run failed",
};
eprintln!("basis: {what}: {message}");
}
eprintln!("{}", next_hint(&report.outcome, report.stopped_by));
Ok(ExitCode::from(exit_code(&report)))
}
fn next_hint(outcome: &RunOutcome, stopped_by: Option<Bound>) -> &'static str {
if stopped_by.is_some() {
return "next: retry with `basis spawn <PROMPT>` using a narrower prompt or a larger bound";
}
match outcome {
RunOutcome::Ok => {
"next: use `basis fingerprint` to inspect workspace changes or `basis spawn <PROMPT>` for another task"
}
RunOutcome::Error { .. } => {
"next: retry with `basis spawn <PROMPT>` after addressing the reported failure"
}
}
}
pub(crate) fn prompt_from(argument: String) -> Result<String, String> {
match argument.as_str() {
"-" => read_prompt(io::stdin().lock()),
_ => Ok(argument),
}
}
fn read_prompt(mut source: impl Read) -> Result<String, String> {
let mut prompt = String::new();
source
.read_to_string(&mut prompt)
.map_err(|error| format!("could not read the prompt from stdin: {error}"))?;
if prompt.trim().is_empty() {
return Err(
"no prompt on stdin: `-` reads one from stdin, and nothing arrived".to_string(),
);
}
Ok(prompt)
}
fn prose_sink() -> impl EventSink {
let quiet = !io::stderr().is_terminal();
FnSink::new(move |event| {
match event {
Event::RunStarted {
model,
context_files,
..
} => {
if !quiet {
eprintln!("basis: {model}, {} context file(s)", context_files.len());
}
}
Event::AssistantDelta { text } => {
print!("{text}");
io::stdout().flush()?;
}
Event::ToolStarted { tool_name, .. } => {
if !quiet {
eprintln!(" · {tool_name}");
}
}
Event::ToolCompleted {
tool_call_id,
tool_name,
summary,
is_error,
} if is_error => {
let label = if tool_name.is_empty() {
&tool_call_id
} else {
&tool_name
};
eprintln!(" ! {label}: {summary}");
}
Event::Notice { message, .. } => eprintln!("basis: {message}"),
Event::Error { message, .. } => eprintln!("basis: {message}"),
Event::RunFinished { .. } => println!(),
_ => {}
}
Ok(())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_dash_reads_the_prompt_from_stdin() {
let prompt = read_prompt(&b"fix the failing test\nthen push\n"[..])
.expect("a prompt arrived on stdin");
assert_eq!(
prompt, "fix the failing test\nthen push\n",
"a multi-line prompt must arrive whole, not truncated at the first newline"
);
}
#[test]
fn an_empty_stdin_says_where_the_prompt_was_looked_for() {
let reason = read_prompt(&b" \n"[..]).expect_err("whitespace is not a prompt");
assert!(
reason.contains("stdin"),
"the reason must name stdin: {reason}"
);
}
#[test]
fn a_prompt_that_is_not_a_dash_is_taken_as_written() {
assert_eq!(
prompt_from("fix the failing test".to_string()).expect("a literal prompt"),
"fix the failing test"
);
}
#[test]
fn a_success_hint_names_two_commands_that_exist() {
assert_eq!(
next_hint(&RunOutcome::Ok, None),
"next: use `basis fingerprint` to inspect workspace changes or `basis spawn <PROMPT>` for another task"
);
}
#[test]
fn a_bound_hint_suggests_changing_the_bound_or_the_work() {
assert_eq!(
next_hint(
&RunOutcome::Error {
message: "deadline exceeded".to_string(),
},
Some(Bound::Deadline),
),
"next: retry with `basis spawn <PROMPT>` using a narrower prompt or a larger bound"
);
}
#[test]
fn a_failure_hint_does_not_guess_the_remedy() {
assert_eq!(
next_hint(
&RunOutcome::Error {
message: "provider failed".to_string(),
},
None,
),
"next: retry with `basis spawn <PROMPT>` after addressing the reported failure"
);
}
}