feat: support setting the tool_timeout in the main config file instead of only via an environment variable

This commit is contained in:
Dark-Alex-17 committed 2026-09-08 17:22:21 -06:00
1 parent 695153f88e
commit 13ebe04472
8 files changed
+125 -23

No files matched your search

+5 -7
View File
@@ -143,8 +143,9 @@ pub fn job_function_declarations() -> Vec<FunctionDeclaration> {
directly. Backgroundable tools: external command tools (e.g. execute_command) and \
`mcp_invoke_*` calls; built-in `agent__`/`job__`/`user__`/`todo__`/`memory__`/`skill__` \
tools cannot be backgrounded. The job runs against a snapshot of the current config and \
environment; later changes do not affect it. Process jobs honor COYOTE_TOOL_TIMEOUT; MCP \
jobs have NO timeout — cancel a hung one with `job__cancel`. Jobs do not survive coyote \
environment; later changes do not affect it. Process jobs honor the tool timeout \
(`tool_timeout` config / COYOTE_TOOL_TIMEOUT env var); MCP jobs have NO timeout — cancel \
a hung one with `job__cancel`. Jobs do not survive coyote \
exiting. In graph LLM nodes, jobs are node-local: collect or cancel every job you start \
before the node ends — leftovers are cancelled at node exit.".to_string(),
parameters: JsonSchema {
@@ -963,10 +964,7 @@ fn build_env_snapshot(
args
};
let timeout_secs = env::var("COYOTE_TOOL_TIMEOUT")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(1800);
let timeout_secs = super::tool_timeout_secs(ctx.app.config.tool_timeout);
Ok(JobEnvSnapshot {
cmd_name,
@@ -1105,7 +1103,7 @@ async fn run_process_job(
drain_output_file(&snapshot.output_file, &file_offset, &output_buf);
let output_bytes_captured = output_buf.lock().total_written();
let message = format!(
"Tool call '{}' timed out after {}s and was killed (set COYOTE_TOOL_TIMEOUT to adjust; 0 = unlimited)",
"Tool call '{}' timed out after {}s and was killed (set tool_timeout in config or COYOTE_TOOL_TIMEOUT to adjust; 0 = unlimited)",
snapshot.display_name, snapshot.timeout_secs
);
+47 -6
View File
@@ -1677,7 +1677,14 @@ impl ToolCall {
json!({"tool_call_error": error_msg})
})
}
_ => match run_llm_function(cmd_name, cmd_args, envs, agent_name, quiet) {
_ => match run_llm_function(
cmd_name,
cmd_args,
envs,
agent_name,
ctx.app.config.tool_timeout,
quiet,
) {
Ok(Some(contents)) => serde_json::from_str(&contents)
.ok()
.unwrap_or_else(|| json!({"output": contents})),
@@ -2271,11 +2278,20 @@ fn clamp_metadata_field(object: &mut Value, key: &str) {
object[key] = json!(clamped);
}
pub(crate) fn tool_timeout_secs(config_value: Option<u64>) -> u64 {
env::var(get_env_name("tool_timeout"))
.ok()
.and_then(|v| v.parse::<u64>().ok())
.or(config_value)
.unwrap_or(1800)
}
pub fn run_llm_function(
cmd_name: String,
cmd_args: Vec<String>,
mut envs: HashMap<String, String>,
agent_name: Option<String>,
tool_timeout: Option<u64>,
quiet: bool,
) -> Result<Option<String>> {
let mut bin_dirs: Vec<PathBuf> = vec![];
@@ -2395,10 +2411,7 @@ pub fn run_llm_function(
buf
});
let timeout_secs = env::var("COYOTE_TOOL_TIMEOUT")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(1800);
let timeout_secs = tool_timeout_secs(tool_timeout);
let deadline = (timeout_secs > 0).then(|| Instant::now() + Duration::from_secs(timeout_secs));
let status = loop {
match child.try_wait() {
@@ -2414,7 +2427,7 @@ pub fn run_llm_function(
drop(stdout_thread);
drop(stderr_thread);
let tool_error_message = format!(
"Tool call '{command_name}' timed out after {timeout_secs}s and was killed (set COYOTE_TOOL_TIMEOUT to adjust; 0 = unlimited)"
"Tool call '{command_name}' timed out after {timeout_secs}s and was killed (set tool_timeout in config or COYOTE_TOOL_TIMEOUT to adjust; 0 = unlimited)"
);
emit_tool_warning(
quiet,
@@ -4546,6 +4559,7 @@ mod tests {
],
HashMap::new(),
None,
None,
false,
)
.unwrap()
@@ -4573,6 +4587,7 @@ mod tests {
],
HashMap::new(),
None,
None,
true,
)
.unwrap()
@@ -4589,6 +4604,32 @@ mod tests {
assert_eq!(json["output"], "partial-output\n");
}
#[test]
#[serial]
fn tool_timeout_secs_prefers_env_then_config_then_default() {
let prev = env::var_os("COYOTE_TOOL_TIMEOUT");
unsafe { env::set_var("COYOTE_TOOL_TIMEOUT", "42") };
assert_eq!(tool_timeout_secs(Some(900)), 42);
unsafe { env::set_var("COYOTE_TOOL_TIMEOUT", "abc") };
assert_eq!(tool_timeout_secs(Some(900)), 900);
unsafe { env::set_var("COYOTE_TOOL_TIMEOUT", "0") };
assert_eq!(tool_timeout_secs(Some(900)), 0);
unsafe { env::remove_var("COYOTE_TOOL_TIMEOUT") };
assert_eq!(tool_timeout_secs(Some(900)), 900);
assert_eq!(tool_timeout_secs(None), 1800);
unsafe {
match prev {
Some(v) => env::set_var("COYOTE_TOOL_TIMEOUT", v),
None => env::remove_var("COYOTE_TOOL_TIMEOUT"),
}
}
}
#[test]
fn bin_entry_stem_strips_run_prefix_and_extension() {
assert_eq!(bin_entry_stem("fs_grep"), "fs_grep");