agent: Mark subagent completions with Subagent intent (#52350) (cherry-pick to preview) (#52358)

zed-zippy[bot] and Ben Brandt created

Cherry-pick of #52350 to preview

----
## Context

Ensure subagent threads build requests with the Subagent intent instead
of UserPrompt. This allows us to properly
attribute this as a tool call for certain providers instead of a user
request.

## Self-Review Checklist

<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX

checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- copilot_chat: Fix subagent requests being marked as user requests.

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>

Change summary

crates/agent/src/tests/mod.rs                       | 5 +++++
crates/agent/src/thread.rs                          | 7 +++++++
crates/cloud_llm_client/src/cloud_llm_client.rs     | 1 +
crates/language_models/src/provider/copilot_chat.rs | 4 +++-
4 files changed, 16 insertions(+), 1 deletion(-)

Detailed changes

crates/agent/src/tests/mod.rs 🔗

@@ -5039,6 +5039,11 @@ async fn test_subagent_thread_inherits_parent_thread_properties(cx: &mut TestApp
             subagent_thread.parent_thread_id(),
             Some(parent_thread.read(cx).id().clone())
         );
+
+        let request = subagent_thread
+            .build_completion_request(CompletionIntent::UserPrompt, cx)
+            .unwrap();
+        assert_eq!(request.intent, Some(CompletionIntent::Subagent));
     });
 }
 

crates/agent/src/thread.rs 🔗

@@ -2646,6 +2646,13 @@ impl Thread {
         completion_intent: CompletionIntent,
         cx: &App,
     ) -> Result<LanguageModelRequest> {
+        let completion_intent =
+            if self.is_subagent() && completion_intent == CompletionIntent::UserPrompt {
+                CompletionIntent::Subagent
+            } else {
+                completion_intent
+            };
+
         let model = self.model().context("No language model configured")?;
         let tools = if let Some(turn) = self.running_turn.as_ref() {
             turn.tools

crates/cloud_llm_client/src/cloud_llm_client.rs 🔗

@@ -193,6 +193,7 @@ pub enum EditPredictionRejectReason {
 #[serde(rename_all = "snake_case")]
 pub enum CompletionIntent {
     UserPrompt,
+    Subagent,
     ToolResults,
     ThreadSummarization,
     ThreadContextSummarization,

crates/language_models/src/provider/copilot_chat.rs 🔗

@@ -357,7 +357,8 @@ impl LanguageModel for CopilotChatLanguageModel {
             | CompletionIntent::TerminalInlineAssist
             | CompletionIntent::GenerateGitCommitMessage => true,
 
-            CompletionIntent::ToolResults
+            CompletionIntent::Subagent
+            | CompletionIntent::ToolResults
             | CompletionIntent::ThreadSummarization
             | CompletionIntent::CreateFile
             | CompletionIntent::EditFile => false,
@@ -1072,6 +1073,7 @@ fn compute_thinking_budget(
 fn intent_to_chat_location(intent: Option<CompletionIntent>) -> ChatLocation {
     match intent {
         Some(CompletionIntent::UserPrompt) => ChatLocation::Agent,
+        Some(CompletionIntent::Subagent) => ChatLocation::Agent,
         Some(CompletionIntent::ToolResults) => ChatLocation::Agent,
         Some(CompletionIntent::ThreadSummarization) => ChatLocation::Panel,
         Some(CompletionIntent::ThreadContextSummarization) => ChatLocation::Panel,