use super::*;
use std::time::Instant;
use tokio::process::Child;
pub(super) fn spawn_agent(args: &mut RunProcessArgs<'_>) -> Result<Child> {
#[cfg(unix)]
args.cmd.process_group(0);
crate::cmd::noninteractive_stdio::configure(&mut args.cmd);
let child = match args.store.with_running_task(args.task_id.as_str(), || {
let child = args.cmd.spawn()?;
if let Some(pid) = child.id() {
let _ = crate::runtime::background::update_agent_pid(args.task_id.as_str(), pid);
}
Ok(child)
}) {
Ok(child) => child,
Err(err) => {
let err = err.context("Failed to spawn agent process");
let stderr = stderr_excerpt(args.task_id)
.or_else(|| Some("unavailable (process did not start)".to_string()));
fail_task_on_agent_spawn(args.store, args.task_id, &err, stderr.as_deref());
return Err(err);
}
};
Ok(child)
}
pub(super) async fn watch_agent(
args: &RunProcessArgs<'_>,
child: &mut Child,
start: Instant,
) -> Result<CompletionInfo> {
let limit = args.timeout_policy.max_duration.min(args.timeout_policy.hard_cap);
let watch = async {
if args.streaming {
crate::runtime::watcher::watch_streaming(
args.agent,
child,
args.task_id,
args.store,
args.log_path,
args.workgroup_id,
args.timeout_policy.idle,
args.max_task_cost,
)
.await
} else {
crate::runtime::watcher::watch_buffered(
args.agent,
child,
args.task_id,
args.store,
args.log_path,
args.output_path.map(std::path::Path::new),
args.workgroup_id,
)
.await
}
};
let result = tokio::time::timeout(limit.saturating_sub(start.elapsed()), watch).await;
settle_result(args, child, start, result).await
}
async fn settle_result(
args: &RunProcessArgs<'_>,
child: &mut Child,
start: Instant,
result: std::result::Result<Result<CompletionInfo>, tokio::time::error::Elapsed>,
) -> Result<CompletionInfo> {
let limit = args.timeout_policy.max_duration.min(args.timeout_policy.hard_cap);
let duration_ms = start.elapsed().as_millis() as i64;
match result {
Ok(Ok(info)) => Ok(info),
Ok(Err(err)) => {
crate::runtime::process::group::force_kill_process_group(child);
let _ = child.kill().await;
let _ = child.wait().await;
settle_execution_failure(args, duration_ms, &err.to_string());
Err(err)
}
Err(_) => {
crate::runtime::process::group::force_kill_process_group(child);
settle_execution_failure(
args, duration_ms, &format!("exceeded hard timeout of {} seconds", limit.as_secs()),
);
Ok(CompletionInfo {
status: TaskStatus::Failed,
tokens: None,
model: None,
cost_usd: None,
exit_code: None,
})
}
}
}
fn settle_execution_failure(args: &RunProcessArgs<'_>, duration_ms: i64, detail: &str) {
let event = TaskEvent {
task_id: args.task_id.clone(),
timestamp: Local::now(),
event_kind: EventKind::Error,
detail: format!("Failed during execution: {detail}"),
metadata: None,
};
let _ = crate::task::lifecycle::complete_task_atomic(
args.store,
TaskCompletionUpdate {
id: args.task_id.as_str(),
status: TaskStatus::Failed,
tokens: None,
duration_ms,
observed_model: None,
attribution_source: None,
cost_usd: None,
exit_code: None,
},
&event,
);
}