use std::time::Duration;
use tokio::time::Instant;
use scv_core::ToolError;
use serde::Deserialize;
use serde_json::{Value, json};
pub(crate) fn parse_args<T: for<'de> Deserialize<'de>>(value: &Value) -> Result<T, ToolError> {
serde_json::from_value(value.clone())
.map_err(|error| ToolError::invalid_arguments(format!("invalid arguments: {error}")))
}
pub(crate) fn validate_process_args(value: &str) -> Result<(), ToolError> {
if value.trim().is_empty() {
return Err(ToolError::invalid_arguments(
"command or prompt must be non-empty",
));
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Timeouts {
pub(crate) default: Duration,
pub(crate) max: Duration,
}
impl Timeouts {
pub(crate) fn resolve(self, requested: Option<u64>) -> Result<Duration, ToolError> {
match requested {
None => Ok(self.default.min(self.max)),
Some(0) => Err(ToolError::invalid_arguments(
"timeout_seconds must be positive",
)),
Some(seconds) if seconds > self.max.as_secs() => {
Err(ToolError::invalid_arguments(format!(
"timeout_seconds {seconds} exceeds the configured maximum of {} seconds \
(tools.max_timeout_seconds)",
self.max.as_secs()
)))
}
Some(seconds) => Ok(Duration::from_secs(seconds)),
}
}
pub(crate) fn deadline(
self,
requested: Option<u64>,
no_timeout: bool,
background_job: bool,
) -> Result<Option<Instant>, ToolError> {
if !no_timeout {
return Ok(Some(Instant::now() + self.resolve(requested)?));
}
if requested.is_some() {
return Err(ToolError::invalid_arguments(
"no_timeout cannot be combined with timeout_seconds",
));
}
if !background_job {
return Err(ToolError::invalid_arguments(
"no_timeout is only allowed for background agent jobs; set background: true",
));
}
Ok(None)
}
}
pub(crate) fn timeout_schema(timeouts: Timeouts) -> Value {
json!({
"type":"integer",
"minimum":1,
"maximum":timeouts.max.as_secs(),
"description":format!(
"Seconds before the process is killed. Defaults to {}; at most {}. \
Raise it for long work such as builds, releases, or landing a change.",
timeouts.default.min(timeouts.max).as_secs(),
timeouts.max.as_secs()
)
})
}
pub(crate) fn bounded(value: &str, max_chars: usize) -> String {
let mut output: String = value.chars().take(max_chars).collect();
if value.chars().count() > max_chars {
output.push('…');
}
output
}
#[cfg(test)]
mod tests;