diff --git a/crates/agent/src/edit_agent.rs b/crates/agent/src/edit_agent.rs
index ecd0b20f674c0a8efdfca3b28cce5780a882cedb..0c234b8cb152e46ad3be29f21639e5a20a1ffb86 100644
--- a/crates/agent/src/edit_agent.rs
+++ b/crates/agent/src/edit_agent.rs
@@ -732,6 +732,10 @@ impl EditAgent {
stop: Vec::new(),
temperature: None,
thinking_allowed: true,
+ // Bypass the rate limiter for nested requests (edit agent requests spawned
+ // from within a tool call) to avoid deadlocks when multiple subagents try
+ // to use edit_file simultaneously.
+ bypass_rate_limit: true,
};
Ok(self.model.stream_completion_text(request, cx).await?.stream)
diff --git a/crates/agent/src/tests/edit_file_thread_test.rs b/crates/agent/src/tests/edit_file_thread_test.rs
new file mode 100644
index 0000000000000000000000000000000000000000..67a0aa07255f15445ae236e2042864acba9833c4
--- /dev/null
+++ b/crates/agent/src/tests/edit_file_thread_test.rs
@@ -0,0 +1,618 @@
+use super::*;
+use acp_thread::UserMessageId;
+use action_log::ActionLog;
+use fs::FakeFs;
+use language_model::{
+ LanguageModelCompletionEvent, LanguageModelToolUse, MessageContent, StopReason,
+ fake_provider::FakeLanguageModel,
+};
+use prompt_store::ProjectContext;
+use serde_json::json;
+use std::{collections::BTreeMap, sync::Arc, time::Duration};
+use util::path;
+
+#[gpui::test]
+async fn test_edit_file_tool_in_thread_context(cx: &mut TestAppContext) {
+ // This test verifies that the edit_file tool works correctly when invoked
+ // through the full thread flow (model sends ToolUse event -> tool runs -> result sent back).
+ // This is different from tests that call tool.run() directly.
+ super::init_test(cx);
+ super::always_allow_tools(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "src": {
+ "main.rs": "fn main() {\n println!(\"Hello, world!\");\n}\n"
+ }
+ }),
+ )
+ .await;
+
+ let project = project::Project::test(fs.clone(), [path!("/project").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| crate::ContextServerRegistry::new(context_server_store.clone(), cx));
+ let model = Arc::new(FakeLanguageModel::default());
+ let fake_model = model.as_fake();
+
+ let thread = cx.new(|cx| {
+ let mut thread = crate::Thread::new(
+ project.clone(),
+ project_context,
+ context_server_registry,
+ crate::Templates::new(),
+ Some(model.clone()),
+ cx,
+ );
+ // Add just the tools we need for this test
+ let language_registry = project.read(cx).languages().clone();
+ thread.add_tool(crate::ReadFileTool::new(
+ cx.weak_entity(),
+ project.clone(),
+ thread.action_log().clone(),
+ ));
+ thread.add_tool(crate::EditFileTool::new(
+ project.clone(),
+ cx.weak_entity(),
+ language_registry,
+ crate::Templates::new(),
+ ));
+ thread
+ });
+
+ // First, read the file so the thread knows about its contents
+ let _events = thread
+ .update(cx, |thread, cx| {
+ thread.send(UserMessageId::new(), ["Read the file src/main.rs"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ // Model calls read_file tool
+ let read_tool_use = LanguageModelToolUse {
+ id: "read_tool_1".into(),
+ name: "read_file".into(),
+ raw_input: json!({"path": "project/src/main.rs"}).to_string(),
+ input: json!({"path": "project/src/main.rs"}),
+ is_input_complete: true,
+ thought_signature: None,
+ };
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(read_tool_use));
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::ToolUse));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // Wait for the read tool to complete and model to be called again
+ while fake_model.pending_completions().is_empty() {
+ cx.run_until_parked();
+ }
+
+ // Model responds after seeing the file content, then calls edit_file
+ fake_model.send_last_completion_stream_text_chunk("I'll edit the file now.");
+ let edit_tool_use = LanguageModelToolUse {
+ id: "edit_tool_1".into(),
+ name: "edit_file".into(),
+ raw_input: json!({
+ "display_description": "Change greeting message",
+ "path": "project/src/main.rs",
+ "mode": "edit"
+ })
+ .to_string(),
+ input: json!({
+ "display_description": "Change greeting message",
+ "path": "project/src/main.rs",
+ "mode": "edit"
+ }),
+ is_input_complete: true,
+ thought_signature: None,
+ };
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(edit_tool_use));
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::ToolUse));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // The edit_file tool creates an EditAgent which makes its own model request.
+ // We need to respond to that request with the edit instructions.
+ // Wait for the edit agent's completion request
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ while fake_model.pending_completions().is_empty() {
+ if std::time::Instant::now() >= deadline {
+ panic!(
+ "Timed out waiting for edit agent completion request. Pending: {}",
+ fake_model.pending_completions().len()
+ );
+ }
+ cx.run_until_parked();
+ cx.background_executor
+ .timer(Duration::from_millis(10))
+ .await;
+ }
+
+ // Send the edit agent's response with the XML format it expects
+ let edit_response = "println!(\"Hello, world!\");\nprintln!(\"Hello, Zed!\");";
+ fake_model.send_last_completion_stream_text_chunk(edit_response);
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // Wait for the edit to complete and the thread to call the model again with tool results
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ while fake_model.pending_completions().is_empty() {
+ if std::time::Instant::now() >= deadline {
+ panic!("Timed out waiting for model to be called after edit completion");
+ }
+ cx.run_until_parked();
+ cx.background_executor
+ .timer(Duration::from_millis(10))
+ .await;
+ }
+
+ // Verify the file was edited
+ let file_content = fs
+ .load(path!("/project/src/main.rs").as_ref())
+ .await
+ .expect("file should exist");
+ assert!(
+ file_content.contains("Hello, Zed!"),
+ "File should have been edited. Content: {}",
+ file_content
+ );
+ assert!(
+ !file_content.contains("Hello, world!"),
+ "Old content should be replaced. Content: {}",
+ file_content
+ );
+
+ // Verify the tool result was sent back to the model
+ let pending = fake_model.pending_completions();
+ assert!(
+ !pending.is_empty(),
+ "Model should have been called with tool result"
+ );
+
+ let last_request = pending.last().unwrap();
+ let has_tool_result = last_request.messages.iter().any(|m| {
+ m.content
+ .iter()
+ .any(|c| matches!(c, language_model::MessageContent::ToolResult(_)))
+ });
+ assert!(
+ has_tool_result,
+ "Tool result should be in the messages sent back to the model"
+ );
+
+ // Complete the turn
+ fake_model.send_last_completion_stream_text_chunk("I've updated the greeting message.");
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // Verify the thread completed successfully
+ thread.update(cx, |thread, _cx| {
+ assert!(
+ thread.is_turn_complete(),
+ "Thread should be complete after the turn ends"
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_subagent_uses_read_file_tool(cx: &mut TestAppContext) {
+ // This test verifies that subagents can successfully use the read_file tool
+ // through the full thread flow, and that tools are properly rebound to use
+ // the subagent's thread ID instead of the parent's.
+ super::init_test(cx);
+ super::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!("/project"),
+ json!({
+ "src": {
+ "lib.rs": "pub fn hello() -> &'static str {\n \"Hello from lib!\"\n}\n"
+ }
+ }),
+ )
+ .await;
+
+ let project = project::Project::test(fs.clone(), [path!("/project").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| crate::ContextServerRegistry::new(context_server_store.clone(), cx));
+ let model = Arc::new(FakeLanguageModel::default());
+ let fake_model = model.as_fake();
+
+ // Create subagent context
+ let subagent_context = crate::SubagentContext {
+ parent_thread_id: agent_client_protocol::SessionId::new("parent-id"),
+ tool_use_id: language_model::LanguageModelToolUseId::from("subagent-tool-use-id"),
+ depth: 1,
+ summary_prompt: "Summarize what you found".to_string(),
+ context_low_prompt: "Context low".to_string(),
+ };
+
+ // Create parent tools that will be passed to the subagent
+ // This simulates how the subagent_tool passes tools to new_subagent
+ let parent_tools: BTreeMap> = {
+ let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ // Create a "fake" parent thread reference - this should get rebound
+ let fake_parent_thread = cx.new(|cx| {
+ crate::Thread::new(
+ project.clone(),
+ cx.new(|_cx| ProjectContext::default()),
+ cx.new(|cx| crate::ContextServerRegistry::new(context_server_store.clone(), cx)),
+ crate::Templates::new(),
+ Some(model.clone()),
+ cx,
+ )
+ });
+ let mut tools: BTreeMap> =
+ BTreeMap::new();
+ tools.insert(
+ "read_file".into(),
+ crate::ReadFileTool::new(fake_parent_thread.downgrade(), project.clone(), action_log)
+ .erase(),
+ );
+ tools
+ };
+
+ // Create subagent - tools should be rebound to use subagent's thread
+ let subagent = cx.new(|cx| {
+ crate::Thread::new_subagent(
+ project.clone(),
+ project_context,
+ context_server_registry,
+ crate::Templates::new(),
+ model.clone(),
+ subagent_context,
+ parent_tools,
+ cx,
+ )
+ });
+
+ // Get the subagent's thread ID
+ let _subagent_thread_id = subagent.read_with(cx, |thread, _| thread.id().to_string());
+
+ // Verify the subagent has the read_file tool
+ subagent.read_with(cx, |thread, _| {
+ assert!(
+ thread.has_registered_tool("read_file"),
+ "subagent should have read_file tool"
+ );
+ });
+
+ // Submit a user message to the subagent
+ subagent
+ .update(cx, |thread, cx| {
+ thread.submit_user_message("Read the file src/lib.rs", cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ // Simulate the model calling the read_file tool
+ let read_tool_use = LanguageModelToolUse {
+ id: "read_tool_1".into(),
+ name: "read_file".into(),
+ raw_input: json!({"path": "project/src/lib.rs"}).to_string(),
+ input: json!({"path": "project/src/lib.rs"}),
+ is_input_complete: true,
+ thought_signature: None,
+ };
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(read_tool_use));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // Wait for the tool to complete and the model to be called again with tool results
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ while fake_model.pending_completions().is_empty() {
+ if std::time::Instant::now() >= deadline {
+ panic!("Timed out waiting for model to be called after read_file tool completion");
+ }
+ cx.run_until_parked();
+ cx.background_executor
+ .timer(Duration::from_millis(10))
+ .await;
+ }
+
+ // Verify the tool result was sent back to the model
+ let pending = fake_model.pending_completions();
+ assert!(
+ !pending.is_empty(),
+ "Model should have been called with tool result"
+ );
+
+ let last_request = pending.last().unwrap();
+ let tool_result = last_request.messages.iter().find_map(|m| {
+ m.content.iter().find_map(|c| match c {
+ MessageContent::ToolResult(result) => Some(result),
+ _ => None,
+ })
+ });
+ assert!(
+ tool_result.is_some(),
+ "Tool result should be in the messages sent back to the model"
+ );
+
+ // Verify the tool result contains the file content
+ let result = tool_result.unwrap();
+ let result_text = match &result.content {
+ language_model::LanguageModelToolResultContent::Text(text) => text.to_string(),
+ _ => panic!("expected text content in tool result"),
+ };
+ assert!(
+ result_text.contains("Hello from lib!"),
+ "Tool result should contain file content, got: {}",
+ result_text
+ );
+
+ // Verify the subagent is ready for more input (tool completed, model called again)
+ // This test verifies the subagent can successfully use read_file tool.
+ // The summary flow is tested separately in test_subagent_returns_summary_on_completion.
+}
+
+#[gpui::test]
+async fn test_subagent_uses_edit_file_tool(cx: &mut TestAppContext) {
+ // This test verifies that subagents can successfully use the edit_file tool
+ // through the full thread flow, including the edit agent's model request.
+ // It also verifies that the edit agent uses the subagent's thread ID, not the parent's.
+ super::init_test(cx);
+ super::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!("/project"),
+ json!({
+ "src": {
+ "config.rs": "pub const VERSION: &str = \"1.0.0\";\n"
+ }
+ }),
+ )
+ .await;
+
+ let project = project::Project::test(fs.clone(), [path!("/project").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| crate::ContextServerRegistry::new(context_server_store.clone(), cx));
+ let model = Arc::new(FakeLanguageModel::default());
+ let fake_model = model.as_fake();
+
+ // Create a "parent" thread to simulate the real scenario where tools are inherited
+ let parent_thread = cx.new(|cx| {
+ crate::Thread::new(
+ project.clone(),
+ cx.new(|_cx| ProjectContext::default()),
+ cx.new(|cx| crate::ContextServerRegistry::new(context_server_store.clone(), cx)),
+ crate::Templates::new(),
+ Some(model.clone()),
+ cx,
+ )
+ });
+ let parent_thread_id = parent_thread.read_with(cx, |thread, _| thread.id().to_string());
+
+ // Create parent tools that reference the parent thread
+ let parent_tools: BTreeMap> = {
+ let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let language_registry = project.read_with(cx, |p, _| p.languages().clone());
+ let mut tools: BTreeMap> =
+ BTreeMap::new();
+ tools.insert(
+ "read_file".into(),
+ crate::ReadFileTool::new(parent_thread.downgrade(), project.clone(), action_log)
+ .erase(),
+ );
+ tools.insert(
+ "edit_file".into(),
+ crate::EditFileTool::new(
+ project.clone(),
+ parent_thread.downgrade(),
+ language_registry,
+ crate::Templates::new(),
+ )
+ .erase(),
+ );
+ tools
+ };
+
+ // Create subagent context
+ let subagent_context = crate::SubagentContext {
+ parent_thread_id: agent_client_protocol::SessionId::new("parent-id"),
+ tool_use_id: language_model::LanguageModelToolUseId::from("subagent-tool-use-id"),
+ depth: 1,
+ summary_prompt: "Summarize what you changed".to_string(),
+ context_low_prompt: "Context low".to_string(),
+ };
+
+ // Create subagent - tools should be rebound to use subagent's thread
+ let subagent = cx.new(|cx| {
+ crate::Thread::new_subagent(
+ project.clone(),
+ project_context,
+ context_server_registry,
+ crate::Templates::new(),
+ model.clone(),
+ subagent_context,
+ parent_tools,
+ cx,
+ )
+ });
+
+ // Get the subagent's thread ID - it should be different from parent
+ let subagent_thread_id = subagent.read_with(cx, |thread, _| thread.id().to_string());
+ assert_ne!(
+ parent_thread_id, subagent_thread_id,
+ "Subagent should have a different thread ID than parent"
+ );
+
+ // Verify the subagent has the tools
+ subagent.read_with(cx, |thread, _| {
+ assert!(
+ thread.has_registered_tool("read_file"),
+ "subagent should have read_file tool"
+ );
+ assert!(
+ thread.has_registered_tool("edit_file"),
+ "subagent should have edit_file tool"
+ );
+ });
+
+ // Submit a user message to the subagent
+ subagent
+ .update(cx, |thread, cx| {
+ thread.submit_user_message("Update the version in config.rs to 2.0.0", cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ // First, model calls read_file to see the current content
+ let read_tool_use = LanguageModelToolUse {
+ id: "read_tool_1".into(),
+ name: "read_file".into(),
+ raw_input: json!({"path": "project/src/config.rs"}).to_string(),
+ input: json!({"path": "project/src/config.rs"}),
+ is_input_complete: true,
+ thought_signature: None,
+ };
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(read_tool_use));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // Wait for the read tool to complete and model to be called again
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ while fake_model.pending_completions().is_empty() {
+ if std::time::Instant::now() >= deadline {
+ panic!("Timed out waiting for model to be called after read_file tool");
+ }
+ cx.run_until_parked();
+ cx.background_executor
+ .timer(Duration::from_millis(10))
+ .await;
+ }
+
+ // Model responds and calls edit_file
+ fake_model.send_last_completion_stream_text_chunk("I'll update the version now.");
+ let edit_tool_use = LanguageModelToolUse {
+ id: "edit_tool_1".into(),
+ name: "edit_file".into(),
+ raw_input: json!({
+ "display_description": "Update version to 2.0.0",
+ "path": "project/src/config.rs",
+ "mode": "edit"
+ })
+ .to_string(),
+ input: json!({
+ "display_description": "Update version to 2.0.0",
+ "path": "project/src/config.rs",
+ "mode": "edit"
+ }),
+ is_input_complete: true,
+ thought_signature: None,
+ };
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(edit_tool_use));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // The edit_file tool creates an EditAgent which makes its own model request.
+ // Wait for that request.
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ while fake_model.pending_completions().is_empty() {
+ if std::time::Instant::now() >= deadline {
+ panic!(
+ "Timed out waiting for edit agent completion request in subagent. Pending: {}",
+ fake_model.pending_completions().len()
+ );
+ }
+ cx.run_until_parked();
+ cx.background_executor
+ .timer(Duration::from_millis(10))
+ .await;
+ }
+
+ // Verify the edit agent's request uses the SUBAGENT's thread ID, not the parent's
+ let pending = fake_model.pending_completions();
+ let edit_agent_request = pending.last().unwrap();
+ let edit_agent_thread_id = edit_agent_request.thread_id.as_ref().unwrap();
+ std::assert_eq!(
+ edit_agent_thread_id,
+ &subagent_thread_id,
+ "Edit agent should use subagent's thread ID, not parent's. Got: {}, expected: {}",
+ edit_agent_thread_id,
+ subagent_thread_id
+ );
+ std::assert_ne!(
+ edit_agent_thread_id,
+ &parent_thread_id,
+ "Edit agent should NOT use parent's thread ID"
+ );
+
+ // Send the edit agent's response with the XML format it expects
+ let edit_response = "pub const VERSION: &str = \"1.0.0\";\npub const VERSION: &str = \"2.0.0\";";
+ fake_model.send_last_completion_stream_text_chunk(edit_response);
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // Wait for the edit to complete and the thread to call the model again with tool results
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
+ while fake_model.pending_completions().is_empty() {
+ if std::time::Instant::now() >= deadline {
+ panic!("Timed out waiting for model to be called after edit completion in subagent");
+ }
+ cx.run_until_parked();
+ cx.background_executor
+ .timer(Duration::from_millis(10))
+ .await;
+ }
+
+ // Verify the file was edited
+ let file_content = fs
+ .load(path!("/project/src/config.rs").as_ref())
+ .await
+ .expect("file should exist");
+ assert!(
+ file_content.contains("2.0.0"),
+ "File should have been edited to contain new version. Content: {}",
+ file_content
+ );
+ assert!(
+ !file_content.contains("1.0.0"),
+ "Old version should be replaced. Content: {}",
+ file_content
+ );
+
+ // Verify the tool result was sent back to the model
+ let pending = fake_model.pending_completions();
+ assert!(
+ !pending.is_empty(),
+ "Model should have been called with tool result"
+ );
+
+ let last_request = pending.last().unwrap();
+ let has_tool_result = last_request.messages.iter().any(|m| {
+ m.content
+ .iter()
+ .any(|c| matches!(c, MessageContent::ToolResult(_)))
+ });
+ assert!(
+ has_tool_result,
+ "Tool result should be in the messages sent back to the model"
+ );
+}
diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs
index 762b4d9a1393f96116e77d3e265b53633f014e7a..a4706f6a752b0ae2fd251320106da998819b0b47 100644
--- a/crates/agent/src/tests/mod.rs
+++ b/crates/agent/src/tests/mod.rs
@@ -52,6 +52,7 @@ use std::{
};
use util::path;
+mod edit_file_thread_test;
mod test_tools;
use test_tools::*;
diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs
index b1f868a4e42e9e7ddc8ddbf866986f72360e35fc..d58e3cb0c4a14489b8e6f5321e4c7a5178ebe766 100644
--- a/crates/agent/src/thread.rs
+++ b/crates/agent/src/thread.rs
@@ -836,6 +836,19 @@ impl Thread {
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
let (prompt_capabilities_tx, prompt_capabilities_rx) =
watch::channel(Self::prompt_capabilities(Some(model.as_ref())));
+
+ // Rebind tools that hold thread references to use this subagent's thread
+ // instead of the parent's thread. This is critical for tools like EditFileTool
+ // that make model requests using the thread's ID.
+ let weak_self = cx.weak_entity();
+ let tools: BTreeMap> = parent_tools
+ .into_iter()
+ .map(|(name, tool)| {
+ let rebound = tool.rebind_thread(weak_self.clone()).unwrap_or(tool);
+ (name, rebound)
+ })
+ .collect();
+
Self {
id: acp::SessionId::new(uuid::Uuid::new_v4().to_string()),
prompt_id: PromptId::new(),
@@ -849,7 +862,7 @@ impl Thread {
running_turn: None,
queued_messages: Vec::new(),
pending_message: None,
- tools: parent_tools,
+ tools,
request_token_usage: HashMap::default(),
cumulative_token_usage: TokenUsage::default(),
initial_project_snapshot: Task::ready(None).shared(),
@@ -2274,6 +2287,7 @@ impl Thread {
stop: Vec::new(),
temperature: AgentSettings::temperature_for_model(model, cx),
thinking_allowed: true,
+ bypass_rate_limit: false,
};
log::debug!("Completion request built successfully");
@@ -2690,6 +2704,15 @@ where
fn erase(self) -> Arc {
Arc::new(Erased(Arc::new(self)))
}
+
+ /// Create a new instance of this tool bound to a different thread.
+ /// This is used when creating subagents, so that tools like EditFileTool
+ /// that hold a thread reference will use the subagent's thread instead
+ /// of the parent's thread.
+ /// Returns None if the tool doesn't need rebinding (most tools).
+ fn rebind_thread(&self, _new_thread: WeakEntity) -> Option> {
+ None
+ }
}
pub struct Erased(T);
@@ -2721,6 +2744,14 @@ pub trait AnyAgentTool {
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Result<()>;
+ /// Create a new instance of this tool bound to a different thread.
+ /// This is used when creating subagents, so that tools like EditFileTool
+ /// that hold a thread reference will use the subagent's thread instead
+ /// of the parent's thread.
+ /// Returns None if the tool doesn't need rebinding (most tools).
+ fn rebind_thread(&self, _new_thread: WeakEntity) -> Option> {
+ None
+ }
}
impl AnyAgentTool for Erased>
@@ -2784,6 +2815,10 @@ where
let output = serde_json::from_value(output)?;
self.0.replay(input, output, event_stream, cx)
}
+
+ fn rebind_thread(&self, new_thread: WeakEntity) -> Option> {
+ self.0.rebind_thread(new_thread)
+ }
}
#[derive(Clone)]
diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs
index 4f8288c24382d373a41fd15b779970676ad09fae..bc7e5b5289937d6212c662f97238e43ea185684d 100644
--- a/crates/agent/src/tools/edit_file_tool.rs
+++ b/crates/agent/src/tools/edit_file_tool.rs
@@ -144,6 +144,15 @@ impl EditFileTool {
}
}
+ pub fn with_thread(&self, new_thread: WeakEntity) -> Self {
+ Self {
+ project: self.project.clone(),
+ thread: new_thread,
+ language_registry: self.language_registry.clone(),
+ templates: self.templates.clone(),
+ }
+ }
+
fn authorize(
&self,
input: &EditFileToolInput,
@@ -398,7 +407,6 @@ impl AgentTool for EditFileTool {
})
.await;
-
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
edit_agent.edit(
buffer.clone(),
@@ -575,6 +583,13 @@ impl AgentTool for EditFileTool {
}));
Ok(())
}
+
+ fn rebind_thread(
+ &self,
+ new_thread: gpui::WeakEntity,
+ ) -> Option> {
+ Some(self.with_thread(new_thread).erase())
+ }
}
/// Validate that the file path is valid, meaning:
diff --git a/crates/agent/src/tools/read_file_tool.rs b/crates/agent/src/tools/read_file_tool.rs
index bc7647739035a41b91c481d2f25b5fbd0f7856c7..8b13452e9357921a1f7a43a51a3364594b481c42 100644
--- a/crates/agent/src/tools/read_file_tool.rs
+++ b/crates/agent/src/tools/read_file_tool.rs
@@ -65,6 +65,14 @@ impl ReadFileTool {
action_log,
}
}
+
+ pub fn with_thread(&self, new_thread: WeakEntity) -> Self {
+ Self {
+ thread: new_thread,
+ project: self.project.clone(),
+ action_log: self.action_log.clone(),
+ }
+ }
}
impl AgentTool for ReadFileTool {
@@ -308,6 +316,13 @@ impl AgentTool for ReadFileTool {
result
})
}
+
+ fn rebind_thread(
+ &self,
+ new_thread: WeakEntity,
+ ) -> Option> {
+ Some(self.with_thread(new_thread).erase())
+ }
}
#[cfg(test)]
diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs
index 6d860bea9974b209289fd57276351e96ed744b20..5cfd161e9fb01cfde6fefe65ef7b3a5fbd89a6f7 100644
--- a/crates/agent_ui/src/buffer_codegen.rs
+++ b/crates/agent_ui/src/buffer_codegen.rs
@@ -544,6 +544,7 @@ impl CodegenAlternative {
temperature,
messages,
thinking_allowed: false,
+ bypass_rate_limit: false,
}
}))
}
@@ -622,6 +623,7 @@ impl CodegenAlternative {
temperature,
messages: vec![request_message],
thinking_allowed: false,
+ bypass_rate_limit: false,
}
}))
}
diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs
index 58a73131e2d20d0776b2cdc49a0b395834b5008f..04dafa588dd3960eb435ef7d9225217d2dfb3354 100644
--- a/crates/agent_ui/src/terminal_inline_assistant.rs
+++ b/crates/agent_ui/src/terminal_inline_assistant.rs
@@ -275,6 +275,7 @@ impl TerminalInlineAssistant {
stop: Vec::new(),
temperature,
thinking_allowed: false,
+ bypass_rate_limit: false,
}
}))
}
diff --git a/crates/assistant_text_thread/src/text_thread.rs b/crates/assistant_text_thread/src/text_thread.rs
index 034314e349a306040fc0cd37dbc3ad9a5ea6e81b..042169eb93b51b681e88091cf994d95fa7b88436 100644
--- a/crates/assistant_text_thread/src/text_thread.rs
+++ b/crates/assistant_text_thread/src/text_thread.rs
@@ -2269,6 +2269,7 @@ impl TextThread {
stop: Vec::new(),
temperature: model.and_then(|model| AgentSettings::temperature_for_model(model, cx)),
thinking_allowed: true,
+ bypass_rate_limit: false,
};
for message in self.messages(cx) {
if message.status != MessageStatus::Done {
diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs
index 17b5adfc4aa9621ac4638f873c30e62ab6244107..1cb5e9a10c3c8814f154643b590e280af724188a 100644
--- a/crates/eval/src/instance.rs
+++ b/crates/eval/src/instance.rs
@@ -563,6 +563,7 @@ impl ExampleInstance {
tool_choice: None,
stop: Vec::new(),
thinking_allowed: true,
+ bypass_rate_limit: false,
};
let model = model.clone();
diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs
index 3f2f406136bdc3a8d9a813e37e264e97633bd214..1628358c602fd5a2ff67a178331ec2172f3d7a67 100644
--- a/crates/git_ui/src/git_panel.rs
+++ b/crates/git_ui/src/git_panel.rs
@@ -2691,6 +2691,7 @@ impl GitPanel {
stop: Vec::new(),
temperature,
thinking_allowed: false,
+ bypass_rate_limit: false,
};
let stream = model.stream_completion_text(request, cx);
diff --git a/crates/language_model/src/rate_limiter.rs b/crates/language_model/src/rate_limiter.rs
index 790be05ac069b8f394e442cbcb6383f611326a69..f4aea8177666e406f8c22f2c92fd0c6f9b4619a8 100644
--- a/crates/language_model/src/rate_limiter.rs
+++ b/crates/language_model/src/rate_limiter.rs
@@ -16,7 +16,7 @@ pub struct RateLimiter {
pub struct RateLimitGuard {
inner: T,
- _guard: SemaphoreGuardArc,
+ _guard: Option,
}
impl Stream for RateLimitGuard
@@ -68,6 +68,36 @@ impl RateLimiter {
async move {
let guard = guard.await;
let inner = future.await?;
+ Ok(RateLimitGuard {
+ inner,
+ _guard: Some(guard),
+ })
+ }
+ }
+
+ /// Like `stream`, but conditionally bypasses the rate limiter based on the flag.
+ /// Used for nested requests (like edit agent requests) that are already "part of"
+ /// a rate-limited request to avoid deadlocks.
+ pub fn stream_with_bypass<'a, Fut, T>(
+ &self,
+ future: Fut,
+ bypass: bool,
+ ) -> impl 'a
+ + Future<
+ Output = Result + use, LanguageModelCompletionError>,
+ >
+ where
+ Fut: 'a + Future