Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions crates/loopal-backend/src/limits.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use loopal_tool_api::{DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_MAX_OUTPUT_LINES};

/// Resource limits applied by `LocalBackend`.
Expand All @@ -15,10 +17,10 @@ pub struct ResourceLimits {
pub max_grep_matches: usize,
/// Maximum HTTP response body size in bytes.
pub max_fetch_bytes: usize,
/// Default shell command timeout (ms).
pub default_timeout_ms: u64,
/// HTTP fetch timeout (seconds).
pub fetch_timeout_secs: u64,
/// Default shell command timeout.
pub default_timeout: Duration,
/// HTTP fetch timeout.
pub fetch_timeout: Duration,
}

impl Default for ResourceLimits {
Expand All @@ -29,9 +31,9 @@ impl Default for ResourceLimits {
max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
max_glob_results: 10_000,
max_grep_matches: 500,
max_fetch_bytes: 5 * 1024 * 1024, // 5 MB
default_timeout_ms: 300_000, // 5 min
fetch_timeout_secs: 30,
max_fetch_bytes: 5 * 1024 * 1024, // 5 MB
default_timeout: Duration::from_secs(300), // 5 min
fetch_timeout: Duration::from_secs(30),
}
}
}
9 changes: 5 additions & 4 deletions crates/loopal-backend/src/local.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! `LocalBackend` — production `Backend` for local filesystem + OS sandbox.
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

use async_trait::async_trait;
use loopal_config::ResolvedPolicy;
Expand Down Expand Up @@ -151,12 +152,12 @@ impl Backend for LocalBackend {
&self.cwd
}

async fn exec(&self, command: &str, timeout_ms: u64) -> Result<ExecResult, ToolIoError> {
async fn exec(&self, command: &str, timeout: Duration) -> Result<ExecResult, ToolIoError> {
shell::exec_command(
&self.cwd,
self.policy.as_ref(),
command,
timeout_ms,
timeout,
&self.limits,
)
.await
Expand All @@ -165,14 +166,14 @@ impl Backend for LocalBackend {
async fn exec_streaming(
&self,
command: &str,
timeout_ms: u64,
timeout: Duration,
tail: Arc<loopal_tool_api::OutputTail>,
) -> Result<ExecOutcome, ToolIoError> {
shell_stream::exec_command_streaming(
&self.cwd,
self.policy.as_ref(),
command,
timeout_ms,
timeout,
&self.limits,
tail,
)
Expand Down
2 changes: 1 addition & 1 deletion crates/loopal-backend/src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub async fn fetch_url(
}

let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(limits.fetch_timeout_secs))
.timeout(limits.fetch_timeout)
.build()
.map_err(|e| ToolIoError::Network(e.to_string()))?;

Expand Down
6 changes: 3 additions & 3 deletions crates/loopal-backend/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub async fn exec_command(
cwd: &Path,
policy: Option<&ResolvedPolicy>,
command: &str,
timeout_ms: u64,
timeout: Duration,
limits: &ResourceLimits,
) -> Result<ExecResult, ToolIoError> {
let (program, args, env) = build_command(cwd, policy, command);
Expand All @@ -34,9 +34,9 @@ pub async fn exec_command(
}
}

let output = tokio::time::timeout(Duration::from_millis(timeout_ms), cmd.output())
let output = tokio::time::timeout(timeout, cmd.output())
.await
.map_err(|_| ToolIoError::Timeout(timeout_ms))?
.map_err(|_| ToolIoError::Timeout(timeout))?
.map_err(|e| ToolIoError::ExecFailed(format!("spawn failed: {e}")))?;

let stdout_raw = String::from_utf8_lossy(&output.stdout);
Expand Down
6 changes: 3 additions & 3 deletions crates/loopal-backend/src/shell_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub async fn exec_command_streaming(
cwd: &Path,
policy: Option<&ResolvedPolicy>,
command: &str,
timeout_ms: u64,
timeout: Duration,
limits: &ResourceLimits,
tail: Arc<OutputTail>,
) -> Result<ExecOutcome, ToolIoError> {
Expand Down Expand Up @@ -101,7 +101,7 @@ pub async fn exec_command_streaming(
// only runs after pipes close (readers finish → child exited), so on
// timeout the child is still inside child_arc (not taken/dropped).
let child_for_wait = Arc::clone(&child_arc);
let wait_result = tokio::time::timeout(Duration::from_millis(timeout_ms), async {
let wait_result = tokio::time::timeout(timeout, async {
let (r1, r2) = tokio::join!(stdout_task, stderr_task);
let _ = (r1, r2);
let child_opt = child_for_wait.lock().unwrap().take();
Expand All @@ -126,7 +126,7 @@ pub async fn exec_command_streaming(
Err(_timeout) => {
let partial = tail.snapshot();
Ok(ExecOutcome::TimedOut {
timeout_ms,
timeout,
partial_output: partial,
handle: ProcessHandle(Box::new(TimedOutProcessData {
child: child_arc,
Expand Down
5 changes: 3 additions & 2 deletions crates/loopal-error/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::time::Duration;
use thiserror::Error;

#[derive(Debug, Error)]
Expand Down Expand Up @@ -71,8 +72,8 @@ pub enum ToolError {
#[error("Execution failed: {0}")]
ExecutionFailed(String),

#[error("Timeout after {0}ms")]
Timeout(u64),
#[error("Timeout after {0:?}")]
Timeout(Duration),
}

#[derive(Debug, Error)]
Expand Down
5 changes: 3 additions & 2 deletions crates/loopal-error/src/io_error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::time::Duration;
use thiserror::Error;

/// Opaque handle for passing implementation-specific data through error
Expand Down Expand Up @@ -37,8 +38,8 @@ pub enum ToolIoError {
#[error("exec failed: {0}")]
ExecFailed(String),

#[error("timeout after {0}ms")]
Timeout(u64),
#[error("timeout after {0:?}")]
Timeout(Duration),

#[error("network error: {0}")]
Network(String),
Expand Down
6 changes: 3 additions & 3 deletions crates/loopal-error/tests/suite/error_conversion_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn test_loopal_error_from_provider_error() {

#[test]
fn test_loopal_error_from_tool_error() {
let tool_err = ToolError::Timeout(5000);
let tool_err = ToolError::Timeout(std::time::Duration::from_secs(5));
let err: LoopalError = tool_err.into();
assert!(matches!(err, LoopalError::Tool(_)));
}
Expand Down Expand Up @@ -75,8 +75,8 @@ fn test_tool_error_display_execution_failed() {

#[test]
fn test_tool_error_display_timeout() {
let err = ToolError::Timeout(30000);
assert_eq!(format!("{err}"), "Timeout after 30000ms");
let err = ToolError::Timeout(std::time::Duration::from_secs(30));
assert_eq!(format!("{err}"), "Timeout after 30s");
}

// --- ConfigError Display ---
Expand Down
9 changes: 5 additions & 4 deletions crates/loopal-tool-api/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

use async_trait::async_trait;
use loopal_error::{ProcessHandle, ToolIoError};
Expand All @@ -21,7 +22,7 @@ pub enum ExecOutcome {
/// The `handle` carries the child so the caller can register it as a
/// background task.
TimedOut {
timeout_ms: u64,
timeout: Duration,
partial_output: String,
handle: ProcessHandle,
},
Expand Down Expand Up @@ -99,7 +100,7 @@ pub trait Backend: Send + Sync {
// --- Command execution ---

/// Execute a shell command synchronously (with timeout).
async fn exec(&self, command: &str, timeout_ms: u64) -> Result<ExecResult, ToolIoError>;
async fn exec(&self, command: &str, timeout: Duration) -> Result<ExecResult, ToolIoError>;

/// Execute a shell command with streaming output capture.
///
Expand All @@ -112,10 +113,10 @@ pub trait Backend: Send + Sync {
async fn exec_streaming(
&self,
command: &str,
timeout_ms: u64,
timeout: Duration,
_tail: Arc<OutputTail>,
) -> Result<ExecOutcome, ToolIoError> {
self.exec(command, timeout_ms)
self.exec(command, timeout)
.await
.map(ExecOutcome::Completed)
}
Expand Down
14 changes: 11 additions & 3 deletions crates/loopal-tool-api/src/backend_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! These are deliberately simple structs so that tool crates only depend
//! on `loopal-tool-api` (a leaf crate) for their I/O interface.

use std::time::Duration;

/// Result of a file read operation.
#[derive(Debug, Clone)]
pub struct ReadResult {
Expand Down Expand Up @@ -167,9 +169,15 @@ impl TimeoutSecs {
self.0
}

/// Convert to milliseconds, clamped to `max_ms`.
pub fn to_millis_clamped(&self, max_ms: u64) -> u64 {
(self.0.saturating_mul(1000)).min(max_ms)
/// Convert to a `Duration`.
pub const fn to_duration(&self) -> Duration {
Duration::from_secs(self.0)
}

/// Convert to a `Duration`, clamped to `max`.
pub fn to_duration_clamped(&self, max: Duration) -> Duration {
let d = Duration::from_secs(self.0);
if d > max { max } else { d }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async fn test_output_timeout() {

let output = bash
.execute(
json!({"process_id": pid, "block": true, "timeout": 200}),
json!({"process_id": pid, "block": true, "timeout": 1}),
&ctx,
)
.await
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async fn test_bash_background_and_output() {

let output = bash
.execute(
json!({"process_id": pid, "block": true, "timeout": 5000}),
json!({"process_id": pid, "block": true, "timeout": 5}),
&ctx,
)
.await
Expand Down
5 changes: 2 additions & 3 deletions crates/tools/process/bash/src/bg_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub async fn bg_output(
store: &BackgroundTaskStore,
process_id: &str,
block: bool,
timeout_ms: u64,
timeout: Duration,
) -> ToolResult {
let cloned = store.with_task(process_id, |task| {
(
Expand All @@ -26,7 +26,6 @@ pub async fn bg_output(
};

if block {
let deadline = Duration::from_millis(timeout_ms);
let wait = async {
loop {
if *watch_rx.borrow() != TaskStatus::Running {
Expand All @@ -37,7 +36,7 @@ pub async fn bg_output(
}
}
};
if tokio::time::timeout(deadline, wait).await.is_err() {
if tokio::time::timeout(timeout, wait).await.is_err() {
let output = output_buf.lock().unwrap().clone();
return ToolResult::success(format!("{output}\n[Status: Running (timed out waiting)]"));
}
Expand Down
4 changes: 2 additions & 2 deletions crates/tools/process/bash/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ pub fn format_exec_result(output: ExecResult) -> ToolResult {
/// Format a timeout-to-background conversion into a success `ToolResult`.
pub fn format_converted_to_background(
task_id: &str,
timeout_ms: u64,
timeout: std::time::Duration,
partial_output: &str,
) -> ToolResult {
let timeout_secs = timeout_ms / 1000;
let timeout_secs = timeout.as_secs();
let mut msg = format!(
"Command timed out after {timeout_secs}s and was moved to background.\n\
process_id: {task_id}\n\
Expand Down
Loading
Loading