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
This commit is contained in:
Ben Brandt authored and GitHub committed 2026-02-23 15:08:33 +00:00
1 parent c2c6a29abc
commit 29bb4743eb
5 files changed
+11 -150

No files matched your search

+6 -39
View File
@@ -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<Thread>,
label: String,
initial_prompt: String,
timeout: Option<Duration>,
cx: &mut App,
) -> Result<Rc<dyn SubagentHandle>> {
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<Thread>,
session_id: acp::SessionId,
follow_up_prompt: String,
timeout: Option<Duration>,
cx: &mut App,
) -> Result<Rc<dyn SubagentHandle>> {
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<acp_thread::AcpThread>,
parent_thread_entity: Entity<Thread>,
prompt: String,
timeout: Option<Duration>,
cx: &mut App,
) -> Result<Rc<dyn SubagentHandle>> {
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<Thread>,
label: String,
initial_prompt: String,
timeout: Option<Duration>,
cx: &mut App,
) -> Result<Rc<dyn SubagentHandle>> {
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<Thread>,
session_id: acp::SessionId,
follow_up_prompt: String,
timeout: Option<Duration>,
cx: &mut App,
) -> Result<Rc<dyn SubagentHandle>> {
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")),
};
-90
View File
@@ -207,7 +207,6 @@ impl crate::ThreadEnvironment for FakeThreadEnvironment {
_parent_thread: Entity<Thread>,
_label: String,
_initial_prompt: String,
_timeout_ms: Option<Duration>,
_cx: &mut App,
) -> Result<Rc<dyn SubagentHandle>> {
Ok(self
@@ -253,7 +252,6 @@ impl crate::ThreadEnvironment for MultiTerminalEnvironment {
_parent_thread: Entity<Thread>,
_label: String,
_initial_prompt: String,
_timeout: Option<Duration>,
_cx: &mut App,
) -> Result<Rc<dyn SubagentHandle>> {
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);
-2
View File
@@ -618,7 +618,6 @@ pub trait ThreadEnvironment {
parent_thread: Entity<Thread>,
label: String,
initial_prompt: String,
timeout: Option<Duration>,
cx: &mut App,
) -> Result<Rc<dyn SubagentHandle>>;
@@ -627,7 +626,6 @@ pub trait ThreadEnvironment {
_parent_thread: Entity<Thread>,
_session_id: acp::SessionId,
_follow_up_prompt: String,
_timeout: Option<Duration>,
_cx: &mut App,
) -> Result<Rc<dyn SubagentHandle>> {
Err(anyhow::anyhow!(
+5 -18
View File
@@ -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<acp::SessionId>,
/// 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<u64>,
}
#[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,
-1
View File
@@ -685,7 +685,6 @@ impl agent::ThreadEnvironment for EvalThreadEnvironment {
_parent_thread: Entity<agent::Thread>,
_label: String,
_initial_prompt: String,
_timeout_ms: Option<Duration>,
_cx: &mut App,
) -> Result<Rc<dyn agent::SubagentHandle>> {
unimplemented!()