From c6319d3e02fcef04e1ac6706fbfe723fde0a1b92 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 2 Mar 2026 18:10:36 +0100 Subject: [PATCH] agent: Propagate model settings to running subagents (#50510) When the model, summarization model, thinking settings, speed, or profile are updated on a thread, apply the same settings to any currently running subagents. Release Notes: - N/A Co-authored-by: Bennet Bo Fenner --- crates/agent/src/thread.rs | 226 ++++++++++++++++++++++++++++++++++++- 1 file changed, 222 insertions(+), 4 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 9a259ecf6a9debaf4afd68f8271e025ae9f19c4f..4560671cc8ad84fb43f07ee711aa72f053e4a2a9 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1277,13 +1277,20 @@ impl Thread { pub fn set_model(&mut self, model: Arc, cx: &mut Context) { let old_usage = self.latest_token_usage(); - self.model = Some(model); + self.model = Some(model.clone()); let new_caps = Self::prompt_capabilities(self.model.as_deref()); let new_usage = self.latest_token_usage(); if old_usage != new_usage { cx.emit(TokenUsageUpdated(new_usage)); } self.prompt_capabilities_tx.send(new_caps).log_err(); + + for subagent in &self.running_subagents { + subagent + .update(cx, |thread, cx| thread.set_model(model.clone(), cx)) + .ok(); + } + cx.notify() } @@ -1296,7 +1303,15 @@ impl Thread { model: Option>, cx: &mut Context, ) { - self.summarization_model = model; + self.summarization_model = model.clone(); + + for subagent in &self.running_subagents { + subagent + .update(cx, |thread, cx| { + thread.set_summarization_model(model.clone(), cx) + }) + .ok(); + } cx.notify() } @@ -1306,6 +1321,12 @@ impl Thread { pub fn set_thinking_enabled(&mut self, enabled: bool, cx: &mut Context) { self.thinking_enabled = enabled; + + for subagent in &self.running_subagents { + subagent + .update(cx, |thread, cx| thread.set_thinking_enabled(enabled, cx)) + .ok(); + } cx.notify(); } @@ -1314,7 +1335,15 @@ impl Thread { } pub fn set_thinking_effort(&mut self, effort: Option, cx: &mut Context) { - self.thinking_effort = effort; + self.thinking_effort = effort.clone(); + + for subagent in &self.running_subagents { + subagent + .update(cx, |thread, cx| { + thread.set_thinking_effort(effort.clone(), cx) + }) + .ok(); + } cx.notify(); } @@ -1324,6 +1353,12 @@ impl Thread { pub fn set_speed(&mut self, speed: Speed, cx: &mut Context) { self.speed = Some(speed); + + for subagent in &self.running_subagents { + subagent + .update(cx, |thread, cx| thread.set_speed(speed, cx)) + .ok(); + } cx.notify(); } @@ -1399,6 +1434,7 @@ impl Thread { self.tools.insert(T::NAME.into(), tool.erase()); } + #[cfg(any(test, feature = "test-support"))] pub fn remove_tool(&mut self, name: &str) -> bool { self.tools.remove(name).is_some() } @@ -1412,12 +1448,18 @@ impl Thread { return; } - self.profile_id = profile_id; + self.profile_id = profile_id.clone(); // Swap to the profile's preferred model when available. if let Some(model) = Self::resolve_profile_model(&self.profile_id, cx) { self.set_model(model, cx); } + + for subagent in &self.running_subagents { + subagent + .update(cx, |thread, cx| thread.set_profile(profile_id.clone(), cx)) + .ok(); + } } pub fn cancel(&mut self, cx: &mut Context) -> Task<()> { @@ -3776,6 +3818,7 @@ mod tests { use super::*; use gpui::TestAppContext; use language_model::LanguageModelToolUseId; + use language_model::fake_provider::FakeLanguageModel; use serde_json::json; use std::sync::Arc; @@ -3813,6 +3856,181 @@ mod tests { }) } + fn setup_parent_with_subagents( + cx: &mut TestAppContext, + parent: &Entity, + count: usize, + ) -> Vec> { + cx.update(|cx| { + let mut subagents = Vec::new(); + for _ in 0..count { + let subagent = cx.new(|cx| Thread::new_subagent(parent, cx)); + parent.update(cx, |thread, _cx| { + thread.register_running_subagent(subagent.downgrade()); + }); + subagents.push(subagent); + } + subagents + }) + } + + #[gpui::test] + async fn test_set_model_propagates_to_subagents(cx: &mut TestAppContext) { + let (parent, _event_stream) = setup_thread_for_test(cx).await; + let subagents = setup_parent_with_subagents(cx, &parent, 2); + + let new_model: Arc = Arc::new(FakeLanguageModel::with_id_and_thinking( + "test-provider", + "new-model", + "New Model", + false, + )); + + cx.update(|cx| { + parent.update(cx, |thread, cx| { + thread.set_model(new_model, cx); + }); + + for subagent in &subagents { + let subagent_model_id = subagent.read(cx).model().unwrap().id(); + assert_eq!( + subagent_model_id.0.as_ref(), + "new-model", + "Subagent model should match parent model after set_model" + ); + } + }); + } + + #[gpui::test] + async fn test_set_summarization_model_propagates_to_subagents(cx: &mut TestAppContext) { + let (parent, _event_stream) = setup_thread_for_test(cx).await; + let subagents = setup_parent_with_subagents(cx, &parent, 2); + + let summary_model: Arc = + Arc::new(FakeLanguageModel::with_id_and_thinking( + "test-provider", + "summary-model", + "Summary Model", + false, + )); + + cx.update(|cx| { + parent.update(cx, |thread, cx| { + thread.set_summarization_model(Some(summary_model), cx); + }); + + for subagent in &subagents { + let subagent_summary_id = subagent.read(cx).summarization_model().unwrap().id(); + assert_eq!( + subagent_summary_id.0.as_ref(), + "summary-model", + "Subagent summarization model should match parent after set_summarization_model" + ); + } + }); + } + + #[gpui::test] + async fn test_set_thinking_enabled_propagates_to_subagents(cx: &mut TestAppContext) { + let (parent, _event_stream) = setup_thread_for_test(cx).await; + let subagents = setup_parent_with_subagents(cx, &parent, 2); + + cx.update(|cx| { + parent.update(cx, |thread, cx| { + thread.set_thinking_enabled(true, cx); + }); + + for subagent in &subagents { + assert!( + subagent.read(cx).thinking_enabled(), + "Subagent thinking should be enabled after parent enables it" + ); + } + + parent.update(cx, |thread, cx| { + thread.set_thinking_enabled(false, cx); + }); + + for subagent in &subagents { + assert!( + !subagent.read(cx).thinking_enabled(), + "Subagent thinking should be disabled after parent disables it" + ); + } + }); + } + + #[gpui::test] + async fn test_set_thinking_effort_propagates_to_subagents(cx: &mut TestAppContext) { + let (parent, _event_stream) = setup_thread_for_test(cx).await; + let subagents = setup_parent_with_subagents(cx, &parent, 2); + + cx.update(|cx| { + parent.update(cx, |thread, cx| { + thread.set_thinking_effort(Some("high".to_string()), cx); + }); + + for subagent in &subagents { + assert_eq!( + subagent.read(cx).thinking_effort().map(|s| s.as_str()), + Some("high"), + "Subagent thinking effort should match parent" + ); + } + + parent.update(cx, |thread, cx| { + thread.set_thinking_effort(None, cx); + }); + + for subagent in &subagents { + assert_eq!( + subagent.read(cx).thinking_effort(), + None, + "Subagent thinking effort should be None after parent clears it" + ); + } + }); + } + + #[gpui::test] + async fn test_set_speed_propagates_to_subagents(cx: &mut TestAppContext) { + let (parent, _event_stream) = setup_thread_for_test(cx).await; + let subagents = setup_parent_with_subagents(cx, &parent, 2); + + cx.update(|cx| { + parent.update(cx, |thread, cx| { + thread.set_speed(Speed::Fast, cx); + }); + + for subagent in &subagents { + assert_eq!( + subagent.read(cx).speed(), + Some(Speed::Fast), + "Subagent speed should match parent after set_speed" + ); + } + }); + } + + #[gpui::test] + async fn test_dropped_subagent_does_not_panic(cx: &mut TestAppContext) { + let (parent, _event_stream) = setup_thread_for_test(cx).await; + let subagents = setup_parent_with_subagents(cx, &parent, 1); + + // Drop the subagent so the WeakEntity can no longer be upgraded + drop(subagents); + + // Should not panic even though the subagent was dropped + cx.update(|cx| { + parent.update(cx, |thread, cx| { + thread.set_thinking_enabled(true, cx); + thread.set_speed(Speed::Fast, cx); + thread.set_thinking_effort(Some("high".to_string()), cx); + }); + }); + } + #[gpui::test] async fn test_handle_tool_use_json_parse_error_adds_tool_use_to_content( cx: &mut TestAppContext,