use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use locode_host::{ExecRequest, Host};
use locode_tools::{Tool, ToolCtx, ToolError, ToolKind, ToolOutput};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
const DEFAULT_TIMEOUT_MS: u64 = 120_000;
const MAX_TIMEOUT_MS: u64 = 300_000;
pub(crate) struct GrokRunTerminalCmd {
host: Arc<Host>,
}
impl GrokRunTerminalCmd {
pub(crate) fn new(host: Arc<Host>) -> Self {
Self { host }
}
}
#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct RunTerminalCmdArgs {
#[schemars(description = "The bash command to run.")]
command: String,
#[schemars(
description = "Optional timeout in milliseconds (max 300000). Default: 120000 (2 minutes)."
)]
#[serde(default)]
timeout: Option<u64>,
#[schemars(
description = "One sentence explanation as to why this command needs to be run and how it contributes to the goal."
)]
#[allow(dead_code)] description: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct RunTerminalCmdOutput {
exit_code: i64,
timed_out: bool,
truncated: bool,
#[serde(skip)]
output: String,
}
impl ToolOutput for RunTerminalCmdOutput {
fn to_prompt_text(&self) -> String {
let status = if self.timed_out {
"killed (timeout)".to_string()
} else if self.exit_code < 0 {
"killed".to_string()
} else {
self.exit_code.to_string()
};
let marker = if self.truncated {
" [output truncated]"
} else {
""
};
format!("exit: {status}{marker}\n{}", self.output)
}
}
#[async_trait]
impl Tool for GrokRunTerminalCmd {
type Args = RunTerminalCmdArgs;
type Output = RunTerminalCmdOutput;
fn kind(&self) -> ToolKind {
ToolKind::Shell
}
#[allow(clippy::unnecessary_literal_bound)] fn description(&self) -> &str {
"Run a bash command in the workspace shell and return its combined output and exit code."
}
async fn run(
&self,
ctx: &ToolCtx,
args: RunTerminalCmdArgs,
) -> Result<Self::Output, ToolError> {
let timeout_ms = args
.timeout
.unwrap_or(DEFAULT_TIMEOUT_MS)
.min(MAX_TIMEOUT_MS);
let request = ExecRequest {
command: args.command,
cwd: ctx.cwd.clone(),
timeout: Some(Duration::from_millis(timeout_ms)),
env: Vec::new(),
};
let out = self
.host
.exec(request, &ctx.cancel)
.await
.map_err(|e| ToolError::Respond(e.to_string()))?;
Ok(RunTerminalCmdOutput {
exit_code: out.exit_code.map_or(-1, i64::from),
timed_out: out.timed_out,
truncated: out.truncated,
output: out.combined,
})
}
}