From 304951a18f3fba770c16681a50a83e4fbf6d8c38 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 23 Feb 2026 16:08:33 +0100 Subject: [PATCH] agent: Remove timeout for subagent (#49898) For the last few days, the model has never picked a good timeout value. It seems they are better at choosing timeouts for bash commands but not agents. Given there are some rate limits involved, I think it is hard to estimate and we already have an upper bound of execution which is the context window to keep it from going indefinitely. Release Notes: - N/A --- crates/agent/src/agent.rs | 45 ++--------- crates/agent/src/tests/mod.rs | 90 ---------------------- crates/agent/src/thread.rs | 2 - crates/agent/src/tools/spawn_agent_tool.rs | 23 ++---- crates/eval/src/instance.rs | 1 - 5 files changed, 11 insertions(+), 150 deletions(-) diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 4e8691c0b5017e57fb5af1a6db211c41f7629f49..3906da36056709bdded6ffca92a85a390ec81f44 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -50,7 +50,6 @@ use std::any::Any; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; -use std::time::Duration; use util::ResultExt; use util::rel_path::RelPath; @@ -1586,7 +1585,6 @@ impl NativeThreadEnvironment { parent_thread_entity: Entity, label: String, initial_prompt: String, - timeout: Option, cx: &mut App, ) -> Result> { let parent_thread = parent_thread_entity.read(cx); @@ -1617,7 +1615,6 @@ impl NativeThreadEnvironment { acp_thread, parent_thread_entity, initial_prompt, - timeout, cx, ) } @@ -1627,7 +1624,6 @@ impl NativeThreadEnvironment { parent_thread_entity: Entity, session_id: acp::SessionId, follow_up_prompt: String, - timeout: Option, cx: &mut App, ) -> Result> { let (subagent_thread, acp_thread) = agent.update(cx, |agent, _cx| { @@ -1644,7 +1640,6 @@ impl NativeThreadEnvironment { acp_thread, parent_thread_entity, follow_up_prompt, - timeout, cx, ) } @@ -1655,7 +1650,6 @@ impl NativeThreadEnvironment { acp_thread: Entity, parent_thread_entity: Entity, prompt: String, - timeout: Option, cx: &mut App, ) -> Result> { parent_thread_entity.update(cx, |parent_thread, _cx| { @@ -1666,33 +1660,15 @@ impl NativeThreadEnvironment { acp_thread.send(vec![prompt.into()], cx) }); - let timeout_timer = timeout.map(|d| cx.background_executor().timer(d)); let wait_for_prompt_to_complete = cx .background_spawn(async move { - if let Some(timer) = timeout_timer { - futures::select! { - _ = timer.fuse() => SubagentInitialPromptResult::Timeout, - response = task.fuse() => { - let response = response.log_err().flatten(); - if response.is_some_and(|response| { - response.stop_reason == acp::StopReason::Cancelled - }) - { - SubagentInitialPromptResult::Cancelled - } else { - SubagentInitialPromptResult::Completed - } - }, - } + let response = task.await.log_err().flatten(); + if response + .is_some_and(|response| response.stop_reason == acp::StopReason::Cancelled) + { + SubagentInitialPromptResult::Cancelled } else { - let response = task.await.log_err().flatten(); - if response - .is_some_and(|response| response.stop_reason == acp::StopReason::Cancelled) - { - SubagentInitialPromptResult::Cancelled - } else { - SubagentInitialPromptResult::Completed - } + SubagentInitialPromptResult::Completed } }) .shared(); @@ -1745,7 +1721,6 @@ impl ThreadEnvironment for NativeThreadEnvironment { parent_thread_entity: Entity, label: String, initial_prompt: String, - timeout: Option, cx: &mut App, ) -> Result> { Self::create_subagent_thread( @@ -1753,7 +1728,6 @@ impl ThreadEnvironment for NativeThreadEnvironment { parent_thread_entity, label, initial_prompt, - timeout, cx, ) } @@ -1763,7 +1737,6 @@ impl ThreadEnvironment for NativeThreadEnvironment { parent_thread_entity: Entity, session_id: acp::SessionId, follow_up_prompt: String, - timeout: Option, cx: &mut App, ) -> Result> { Self::resume_subagent_thread( @@ -1771,7 +1744,6 @@ impl ThreadEnvironment for NativeThreadEnvironment { parent_thread_entity, session_id, follow_up_prompt, - timeout, cx, ) } @@ -1780,7 +1752,6 @@ impl ThreadEnvironment for NativeThreadEnvironment { #[derive(Debug, Clone, Copy)] enum SubagentInitialPromptResult { Completed, - Timeout, Cancelled, } @@ -1811,10 +1782,6 @@ impl SubagentHandle for NativeSubagentHandle { .map(|m| m.to_markdown()) .context("No response from subagent") }), - SubagentInitialPromptResult::Timeout => { - thread.update(cx, |thread, cx| thread.cancel(cx)).await; - Err(anyhow!("The time to complete the task was exceeded.")) - } SubagentInitialPromptResult::Cancelled => Err(anyhow!("User cancelled")), }; diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index 3df64794c53e7f173de9adf5f9b8893d56fa70e6..2c412561bae29b87d96e2a4a016283015dfc4e15 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -207,7 +207,6 @@ impl crate::ThreadEnvironment for FakeThreadEnvironment { _parent_thread: Entity, _label: String, _initial_prompt: String, - _timeout_ms: Option, _cx: &mut App, ) -> Result> { Ok(self @@ -253,7 +252,6 @@ impl crate::ThreadEnvironment for MultiTerminalEnvironment { _parent_thread: Entity, _label: String, _initial_prompt: String, - _timeout: Option, _cx: &mut App, ) -> Result> { unimplemented!() @@ -4234,7 +4232,6 @@ async fn test_subagent_tool_call_end_to_end(cx: &mut TestAppContext) { label: "label".to_string(), message: "subagent task prompt".to_string(), session_id: None, - timeout_secs: None, }; let subagent_tool_use = LanguageModelToolUse { id: "subagent_1".into(), @@ -4378,7 +4375,6 @@ async fn test_subagent_tool_call_cancellation_during_task_prompt(cx: &mut TestAp label: "label".to_string(), message: "subagent task prompt".to_string(), session_id: None, - timeout_secs: None, }; let subagent_tool_use = LanguageModelToolUse { id: "subagent_1".into(), @@ -4516,7 +4512,6 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) { label: "initial task".to_string(), message: "do the first task".to_string(), session_id: None, - timeout_secs: None, }; let subagent_tool_use = LanguageModelToolUse { id: "subagent_1".into(), @@ -4578,7 +4573,6 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) { label: "follow-up task".to_string(), message: "do the follow-up task".to_string(), session_id: Some(subagent_session_id.clone()), - timeout_secs: None, }; let resume_tool_use = LanguageModelToolUse { id: "subagent_2".into(), @@ -4836,90 +4830,6 @@ async fn test_parent_cancel_stops_subagent(cx: &mut TestAppContext) { }); } -#[gpui::test] -async fn test_subagent_tool_includes_cancellation_notice_when_timeout_is_exceeded( - cx: &mut TestAppContext, -) { - init_test(cx); - - always_allow_tools(cx); - - cx.update(|cx| { - cx.update_flags(true, vec!["subagents".to_string()]); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - let project_context = cx.new(|_cx| ProjectContext::default()); - let context_server_store = project.read_with(cx, |project, _| project.context_server_store()); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx)); - cx.update(LanguageModelRegistry::test); - let model = Arc::new(FakeLanguageModel::default()); - let thread_store = cx.new(|cx| ThreadStore::new(cx)); - let native_agent = NativeAgent::new( - project.clone(), - thread_store, - Templates::new(), - None, - fs, - &mut cx.to_async(), - ) - .await - .unwrap(); - let parent_thread = cx.new(|cx| { - Thread::new( - project.clone(), - project_context, - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - - let subagent_handle = cx - .update(|cx| { - NativeThreadEnvironment::create_subagent_thread( - native_agent.downgrade(), - parent_thread.clone(), - "some title".to_string(), - "task prompt".to_string(), - Some(Duration::from_secs(1)), - cx, - ) - }) - .expect("Failed to create subagent"); - - let summary_task = subagent_handle.wait_for_output(&cx.to_async()); - - cx.run_until_parked(); - - { - let messages = model.pending_completions().last().unwrap().messages.clone(); - // Ensure that model received a system prompt - assert_eq!(messages[0].role, Role::System); - // Ensure that model received a task prompt - assert_eq!( - messages[1].content, - vec![MessageContent::Text("task prompt".to_string())] - ); - } - - // Don't complete the initial model stream — let the timeout expire instead. - cx.executor().advance_clock(Duration::from_secs(2)); - cx.run_until_parked(); - - model.end_last_completion_stream(); - - let error = summary_task.await.unwrap_err(); - assert_eq!( - error.to_string(), - "The time to complete the task was exceeded." - ); -} - #[gpui::test] async fn test_edit_file_tool_deny_rule_blocks_edit(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index cd5c8f370a71148de508560704ea1dc6b5b5b924..e40ab834a8710cbfd30754e215624fc9fdfa9a6a 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -618,7 +618,6 @@ pub trait ThreadEnvironment { parent_thread: Entity, label: String, initial_prompt: String, - timeout: Option, cx: &mut App, ) -> Result>; @@ -627,7 +626,6 @@ pub trait ThreadEnvironment { _parent_thread: Entity, _session_id: acp::SessionId, _follow_up_prompt: String, - _timeout: Option, _cx: &mut App, ) -> Result> { Err(anyhow::anyhow!( diff --git a/crates/agent/src/tools/spawn_agent_tool.rs b/crates/agent/src/tools/spawn_agent_tool.rs index 61446e1d438a67bd4d7e9abcf3f2a50b8ec6f47f..7392a85cfe256fbfdf53a808925528923dfa93d2 100644 --- a/crates/agent/src/tools/spawn_agent_tool.rs +++ b/crates/agent/src/tools/spawn_agent_tool.rs @@ -5,8 +5,8 @@ use gpui::{App, SharedString, Task, WeakEntity}; use language_model::LanguageModelToolResultContent; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::rc::Rc; use std::sync::Arc; -use std::{rc::Rc, time::Duration}; use crate::{AgentTool, Thread, ThreadEnvironment, ToolCallEventStream}; @@ -37,9 +37,6 @@ pub struct SpawnAgentToolInput { /// Optional session ID of an existing agent session to continue a conversation with. When provided, the message is sent as a follow-up to that session instead of creating a new one. Use this to ask clarifying questions, request changes based on previous output, or retry after errors. #[serde(default)] pub session_id: Option, - /// Optional maximum runtime in seconds. The purpose of this timeout is to prevent the agent from getting stuck in infinite loops, NOT to estimate task duration. Be generous if setting. If not set, the agent runs until it completes or is cancelled. - #[serde(default)] - pub timeout_secs: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] @@ -114,21 +111,11 @@ impl AgentTool for SpawnAgentTool { }; let subagent = if let Some(session_id) = input.session_id { - self.environment.resume_subagent( - parent_thread_entity, - session_id, - input.message, - input.timeout_secs.map(Duration::from_secs), - cx, - ) + self.environment + .resume_subagent(parent_thread_entity, session_id, input.message, cx) } else { - self.environment.create_subagent( - parent_thread_entity, - input.label, - input.message, - input.timeout_secs.map(Duration::from_secs), - cx, - ) + self.environment + .create_subagent(parent_thread_entity, input.label, input.message, cx) }; let subagent = match subagent { Ok(subagent) => subagent, diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 3fc8d23a69c82ebba6e42e7bc0651a16d0ec62d8..cbc5cd1568cf80ad23b9da9dfcaab74730986533 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -685,7 +685,6 @@ impl agent::ThreadEnvironment for EvalThreadEnvironment { _parent_thread: Entity, _label: String, _initial_prompt: String, - _timeout_ms: Option, _cx: &mut App, ) -> Result> { unimplemented!()