use std::sync::Arc;
use std::time::Duration;
use promptforge_core::CancelHandle;
use promptforge_core::client::GatewayClient;
use promptforge_core::execute::{self, ResolutionContext, RunConfig};
use promptforge_core::parser::Prompt;
use promptforge_core::store::StoreRef;
use rmcp::model::{CallToolResult, ErrorData};
use tokio::sync::oneshot;
use tokio::time::Instant;
use crate::catalog::Entry;
use crate::config::Config;
use crate::progress::{McpObserver, ProgressPump};
use crate::registry::{DuplicateRun, RunRegistry, RunSlot, elapsed_ms};
use crate::result::{NO_TURNS, RunResult};
use super::{PreparedTools, Reporting, bind, run_result, text_error};
struct Launch {
run_id: String,
prompt: Prompt,
name: String,
args: String,
tools: Arc<PreparedTools>,
observer: Arc<McpObserver>,
client: GatewayClient,
started: Instant,
slot: RunSlot,
cancel: CancelHandle,
}
struct Observation {
run_id: String,
observer: Arc<McpObserver>,
pump: Option<ProgressPump>,
}
pub(super) async fn run(
config: &Config,
registry: &Arc<RunRegistry>,
tools: Arc<PreparedTools>,
entry: &Entry,
args: &str,
reporting: Option<Reporting>,
) -> Result<CallToolResult, ErrorData> {
let run_id = new_run_id();
let Some(source) = entry.source() else {
let problem = entry
.problem()
.unwrap_or("the prompt is unavailable")
.to_owned();
return run_result(&RunResult::failed(
run_id,
entry.name(),
problem,
NO_TURNS,
0,
));
};
let (observer, pump) = match reporting {
Some((peer, token)) => {
let (observer, pump) = McpObserver::reporting(peer, token);
(Arc::new(observer), Some(pump))
}
None => (Arc::new(McpObserver::silent()), None),
};
run_observed(
config,
registry,
tools,
entry,
args,
source,
Observation {
run_id,
observer,
pump,
},
)
.await
}
async fn run_observed(
config: &Config,
registry: &Arc<RunRegistry>,
tools: Arc<PreparedTools>,
entry: &Entry,
args: &str,
source: &str,
observation: Observation,
) -> Result<CallToolResult, ErrorData> {
let Observation {
run_id,
observer,
pump,
} = observation;
let prompt = match Prompt::parse(source, &run_id, observer.as_ref()) {
Ok(prompt) => prompt,
Err(error) => {
return preparation_failed(run_id, entry, error.to_string(), observer, pump).await;
}
};
let Some(slot) = registry.admit().await else {
drop(observer);
finish_pump(pump).await;
return Ok(text_error(refused(registry.admission_timeout())));
};
let started = Instant::now();
let client = match bind::gateway_client(&config.gateway) {
Ok(client) => client,
Err(error) => {
drop(slot);
return preparation_failed(run_id, entry, error.to_string(), observer, pump).await;
}
};
let cancel = CancelHandle::new();
let (result_tx, result_rx) = oneshot::channel();
let launch = Launch {
run_id: run_id.clone(),
prompt,
name: entry.name().to_owned(),
args: args.to_owned(),
tools,
observer,
client,
started,
slot,
cancel: cancel.clone(),
};
if let Err(DuplicateRun) = registry.launch(
run_id.clone(),
entry.name().to_owned(),
cancel.clone(),
result_tx,
move || tokio::spawn(execute_run(launch)),
) {
finish_pump(pump).await;
return run_result(&RunResult::failed(
run_id,
entry.name(),
"the run id was already in use".to_owned(),
NO_TURNS,
0,
));
}
let result = registry.settle(&run_id, entry.name(), result_rx).await;
if let Some(pump) = pump {
pump.finish().await;
}
run_result(&result)
}
#[cfg(test)]
pub(super) async fn run_recorded(
config: &Config,
registry: &Arc<RunRegistry>,
tools: Arc<PreparedTools>,
entry: &Entry,
args: &str,
observer: Arc<McpObserver>,
) -> Result<CallToolResult, ErrorData> {
let run_id = new_run_id();
let source = entry
.source()
.expect("the recorded runner fixture must be a healthy entry");
run_observed(
config,
registry,
tools,
entry,
args,
source,
Observation {
run_id,
observer,
pump: None,
},
)
.await
}
async fn execute_run(launch: Launch) -> RunResult {
let Launch {
run_id,
prompt,
name,
args,
tools,
observer,
client,
started,
slot,
cancel,
} = launch;
let store = StoreRef::memory();
let config = RunConfig::new(run_id.as_str())
.observer(Arc::clone(&observer) as Arc<dyn promptforge_core::observe::Observer>)
.client(client)
.cancel(cancel);
let outcome = execute::run(
&prompt,
&args,
ResolutionContext::new(tools.picker(), tools.models()),
tools.tools(),
&store,
config,
)
.await;
let turns = observer.turns();
let elapsed = elapsed_ms(started);
drop(observer);
let result = match outcome {
Ok(value) => RunResult::completed(run_id.clone(), &name, value, turns, elapsed),
Err(error) => RunResult::failed(run_id.clone(), &name, error.to_string(), turns, elapsed),
};
log_terminal_result(&result);
drop(slot);
result
}
async fn preparation_failed(
run_id: String,
entry: &Entry,
message: String,
observer: Arc<McpObserver>,
pump: Option<ProgressPump>,
) -> Result<CallToolResult, ErrorData> {
let turns = observer.turns();
drop(observer);
finish_pump(pump).await;
run_result(&RunResult::failed(run_id, entry.name(), message, turns, 0))
}
async fn finish_pump(pump: Option<ProgressPump>) {
if let Some(pump) = pump {
pump.finish().await;
}
}
fn log_terminal_result(result: &RunResult) {
tracing::info!(
run_id = %result.run_id(),
prompt = %result.prompt(),
status = ?result.status(),
turns = result.turns(),
elapsed_ms = result.elapsed_ms(),
"run reached its terminal state"
);
}
fn refused(waited: Duration) -> String {
format!(
"every run slot is busy and none came free within {}. Retry in a moment.",
humantime::format_duration(waited)
)
}
fn new_run_id() -> String {
format!("{:016x}{:016x}", fastrand::u64(..), fastrand::u64(..))
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use tracing::Level;
use super::{log_terminal_result, new_run_id, refused};
use crate::levels::recording;
use crate::result::RunResult;
#[test]
fn a_run_id_is_thirty_two_hex_digits() {
let id = new_run_id();
assert_eq!(id.len(), 32);
assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
assert_ne!(id, new_run_id(), "two runs do not share an identifier");
}
#[test]
fn a_refusal_names_the_wait_it_spent() {
let message = refused(Duration::from_secs(30));
assert!(message.contains("30s"), "{message}");
}
#[test]
fn terminal_log_carries_the_completed_result_measurements() {
let (levels, _recording) = recording();
let result = RunResult::completed("r1".into(), "echo", "secret".into(), 3, 42);
log_terminal_result(&result);
assert_eq!(levels.operator_visible(), vec![Level::INFO]);
for field in [
"message=run reached its terminal state",
"run_id=r1",
"prompt=echo",
"status=Completed",
"turns=3",
"elapsed_ms=42",
] {
assert!(
levels.said(Level::INFO, field),
"terminal log omitted {field}"
);
}
assert!(
!levels.mentioned(Level::INFO, "secret"),
"terminal logging must exclude the run payload"
);
}
}