Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:30:30.411Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

thread.rs

8582 lines · 331.3 KB · rust
1use crate::{
2    ApplyCodeActionTool, CodeActionStore, ContextServerRegistry, CopyPathTool, CreateDirectoryTool,
3    CreateThreadTool, DbLanguageModel, DbThread, DeletePathTool, DiagnosticsTool, EditFileTool,
4    FetchTool, FindPathTool, FindReferencesTool, GetCodeActionsTool, GoToDefinitionTool, GrepTool,
5    ListAgentsAndModelsTool, ListDirectoryTool, MovePathTool, ProjectSnapshot, ReadFileTool,
6    RenameTool, SandboxedTerminalTool, SpawnAgentTool, SystemPromptTemplate, Template, Templates,
7    TerminalTool, ToolPermissionDecision, WebSearchTool, WriteFileTool,
8    decide_permission_from_settings,
9};
10use acp_thread::{ClientUserMessageId, MentionUri};
11use action_log::ActionLog;
12use agent_settings::UserAgentsMd;
13
14use crate::sandboxing::{
15    SandboxRequest, ThreadSandbox, ThreadSandboxGrants, sandbox_git_dirs,
16    sandbox_worktree_writable_paths, sandboxing_available_for_project,
17    sandboxing_enabled_for_project,
18};
19use agent_client_protocol::schema::v1 as acp;
20use agent_settings::{
21    AgentProfileId, AgentProfileSettings, AgentSettings, AutoCompactThreshold, COMPACTION_PROMPT,
22    SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, builtin_profiles,
23};
24use anyhow::{Context as _, Result, anyhow};
25use chrono::{DateTime, Local, Utc};
26use client::UserStore;
27use cloud_api_types::Plan;
28use collections::{HashMap, HashSet, IndexMap};
29use fs::Fs;
30use futures::{
31    FutureExt,
32    channel::{mpsc, oneshot},
33    future::Shared,
34    stream::FuturesUnordered,
35};
36use futures::{StreamExt, stream};
37use gpui::{
38    App, AppContext, AsyncApp, Context, Entity, EventEmitter, ReadGlobal as _, SharedString, Task,
39    WeakEntity,
40};
41use heck::ToSnakeCase as _;
42use language_model::{
43    CompletionIntent, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
44    LanguageModelId, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry,
45    LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
46    LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
47    LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, SelectedModel, Speed,
48    StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
49};
50use project::{Project, trusted_worktrees::TrustedWorktrees};
51use prompt_store::ProjectContext;
52use schemars::{JsonSchema, Schema};
53use serde::de::DeserializeOwned;
54use serde::{Deserialize, Serialize};
55use settings::{
56    LanguageModelSelection, Settings, SettingsStore, ToolPermissionMode, update_settings_file,
57};
58use std::fmt::Write;
59use std::{cell::RefCell, ops::ControlFlow};
60use std::{
61    collections::BTreeMap,
62    marker::PhantomData,
63    ops::RangeInclusive,
64    path::{Path, PathBuf},
65    rc::Rc,
66    sync::Arc,
67    time::{Duration, Instant},
68};
69use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock, paths::PathStyle};
70use uuid::Uuid;
71
72const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user";
73pub const MAX_TOOL_NAME_LENGTH: usize = 64;
74pub const MAX_SUBAGENT_DEPTH: u8 = 1;
75
76pub(crate) fn provider_compatible_tool_name(tool_name: &str) -> String {
77    let mut sanitized = String::new();
78    for character in tool_name.chars() {
79        if sanitized.len() >= MAX_TOOL_NAME_LENGTH {
80            break;
81        }
82
83        if character.is_ascii_alphanumeric() || character == '_' || character == '-' {
84            sanitized.push(character);
85        } else {
86            sanitized.push('_');
87        }
88    }
89
90    if sanitized.is_empty() {
91        sanitized.push_str("tool");
92    }
93
94    sanitized
95}
96
97#[derive(Clone, Debug, Eq, PartialEq)]
98pub struct SandboxStatusKey {
99    pub settings_sandbox: ThreadSandbox,
100    pub thread_sandbox: ThreadSandbox,
101    pub baseline_writable_paths: Vec<PathBuf>,
102    pub git_paths: Vec<PathBuf>,
103}
104
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct VerifiedSandboxStatus {
107    pub settings_sandbox: ThreadSandbox,
108    pub thread_sandbox: ThreadSandbox,
109    pub baseline_writable_paths: Vec<PathBuf>,
110}
111
112pub enum SandboxStatusRefresh {
113    Ready(VerifiedSandboxStatus),
114    Pending(Task<VerifiedSandboxStatus>),
115}
116
117/// Auto-compaction is only available for models whose context window is at least
118/// this large. For smaller models there isn't enough headroom for a compaction
119/// pass to be worthwhile, so we leave the thread uncompacted and let the UI warn
120/// the user instead.
121pub const MIN_COMPACTION_CONTEXT_WINDOW: u64 = 80_000;
122
123// Using the heuristic that 1 token is about 4 bytes, keep the last 80K bytes of user-message content (~20k tokens).
124const COMPACTION_RETAINED_USER_MESSAGES_BYTE_BUDGET: usize = 80_000;
125
126/// Returned when a turn is attempted but no language model has been selected.
127#[derive(Debug)]
128pub struct NoModelConfiguredError;
129
130impl std::fmt::Display for NoModelConfiguredError {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        write!(f, "no language model configured")
133    }
134}
135
136impl std::error::Error for NoModelConfiguredError {}
137
138/// Context passed to a subagent thread for lifecycle management
139#[derive(Clone, Debug, Serialize, Deserialize)]
140pub struct SubagentContext {
141    /// ID of the parent thread
142    pub parent_thread_id: acp::SessionId,
143
144    /// Current depth level (0 = root agent, 1 = first-level subagent, etc.)
145    pub depth: u8,
146}
147
148/// The ID of the user prompt that initiated a request.
149///
150/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key).
151#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
152pub struct PromptId(Arc<str>);
153
154impl PromptId {
155    pub fn new() -> Self {
156        Self(Uuid::new_v4().to_string().into())
157    }
158}
159
160impl std::fmt::Display for PromptId {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        write!(f, "{}", self.0)
163    }
164}
165
166pub(crate) const MAX_RETRY_ATTEMPTS: u8 = 4;
167pub(crate) const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
168
169#[derive(Debug, Clone)]
170enum RetryStrategy {
171    ExponentialBackoff {
172        initial_delay: Duration,
173        max_attempts: u8,
174    },
175    Fixed {
176        delay: Duration,
177        max_attempts: u8,
178    },
179}
180
181#[derive(Debug, PartialEq, Serialize, Deserialize)]
182pub enum Message {
183    User(UserMessage),
184    Agent(AgentMessage),
185    Resume,
186    Compaction(CompactionInfo),
187}
188
189#[derive(Debug, PartialEq, Serialize, Deserialize)]
190pub enum CompactionInfo {
191    Summary(SharedString),
192    ProviderNative {
193        provider: LanguageModelProviderId,
194        items: Vec<serde_json::Value>,
195    },
196}
197
198impl CompactionInfo {
199    fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
200        match self {
201            Self::Summary(summary) => vec![LanguageModelRequestMessage {
202                role: Role::User,
203                content: vec![format!(
204                    "The previous conversation was compacted. Use this summary as context:\n\n{}",
205                    summary
206                )
207                .into()],
208                cache: false,
209                reasoning_details: None,
210            }],
211            Self::ProviderNative { .. } => Vec::new(),
212        }
213    }
214}
215
216impl Message {
217    pub fn as_agent_message(&self) -> Option<&AgentMessage> {
218        match self {
219            Message::Agent(agent_message) => Some(agent_message),
220            _ => None,
221        }
222    }
223
224    pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
225        match self {
226            Message::User(message) => {
227                if message.content.is_empty() {
228                    vec![]
229                } else {
230                    vec![message.to_request()]
231                }
232            }
233            Message::Agent(message) => message.to_request(),
234            Message::Compaction(info) => info.to_request(),
235            Message::Resume => vec![LanguageModelRequestMessage {
236                role: Role::User,
237                content: vec!["Continue where you left off".into()],
238                cache: false,
239                reasoning_details: None,
240            }],
241        }
242    }
243
244    pub fn to_markdown(&self) -> String {
245        match self {
246            Message::User(message) => message.to_markdown(),
247            Message::Agent(message) => message.to_markdown(),
248            Message::Resume => "[resume]\n".into(),
249            Message::Compaction(_) => "--- Context Compacted ---\n".into(),
250        }
251    }
252
253    pub fn role(&self) -> Role {
254        match self {
255            Message::User(_) | Message::Resume | Message::Compaction(_) => Role::User,
256            Message::Agent(_) => Role::Assistant,
257        }
258    }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262pub struct UserMessage {
263    pub id: ClientUserMessageId,
264    pub content: Arc<[UserMessageContent]>,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub enum UserMessageContent {
269    Text(String),
270    Mention {
271        uri: MentionUri,
272        content: SharedString,
273    },
274    Image(LanguageModelImage),
275}
276
277impl UserMessage {
278    pub fn to_markdown(&self) -> String {
279        let mut markdown = String::new();
280
281        for content in &*self.content {
282            match content {
283                UserMessageContent::Text(text) => {
284                    markdown.push_str(text);
285                    markdown.push('\n');
286                }
287                UserMessageContent::Image(_) => {
288                    markdown.push_str("<image />\n");
289                }
290                UserMessageContent::Mention { uri, content } => {
291                    if !content.is_empty() {
292                        let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content);
293                    } else {
294                        let _ = writeln!(&mut markdown, "{}", uri.as_link());
295                    }
296                }
297            }
298        }
299
300        markdown
301    }
302
303    fn to_request(&self) -> LanguageModelRequestMessage {
304        let mut message = LanguageModelRequestMessage {
305            role: Role::User,
306            content: Vec::with_capacity(self.content.len()),
307            cache: false,
308            reasoning_details: None,
309        };
310
311        const OPEN_CONTEXT: &str = "<context>\n\
312            The following items were attached by the user. \
313            They are up-to-date and don't need to be re-read.\n\n";
314
315        const OPEN_FILES_TAG: &str = "<files>";
316        const OPEN_DIRECTORIES_TAG: &str = "<directories>";
317        const OPEN_SYMBOLS_TAG: &str = "<symbols>";
318        const OPEN_SELECTIONS_TAG: &str = "<selections>";
319        const OPEN_THREADS_TAG: &str = "<threads>";
320        const OPEN_FETCH_TAG: &str = "<fetched_urls>";
321        const OPEN_RULES_TAG: &str =
322            "<rules>\nThe user has specified the following rules that should be applied:\n";
323        const OPEN_DIAGNOSTICS_TAG: &str = "<diagnostics>";
324        const OPEN_DIFFS_TAG: &str = "<diffs>";
325        const MERGE_CONFLICT_TAG: &str = "<merge_conflicts>";
326        const OPEN_SKILLS_TAG: &str =
327            "<skills>\nThe user has attached the following agent skills:\n";
328
329        let mut file_context = OPEN_FILES_TAG.to_string();
330        let mut directory_context = OPEN_DIRECTORIES_TAG.to_string();
331        let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
332        let mut selection_context = OPEN_SELECTIONS_TAG.to_string();
333        let mut thread_context = OPEN_THREADS_TAG.to_string();
334        let mut fetch_context = OPEN_FETCH_TAG.to_string();
335        let mut rules_context = OPEN_RULES_TAG.to_string();
336        let mut diagnostics_context = OPEN_DIAGNOSTICS_TAG.to_string();
337        let mut diffs_context = OPEN_DIFFS_TAG.to_string();
338        let mut merge_conflict_context = MERGE_CONFLICT_TAG.to_string();
339        let mut skills_context = OPEN_SKILLS_TAG.to_string();
340
341        for chunk in &*self.content {
342            let chunk = match chunk {
343                UserMessageContent::Text(text) => {
344                    language_model::MessageContent::Text(text.clone())
345                }
346                UserMessageContent::Image(value) => {
347                    language_model::MessageContent::Image(value.clone())
348                }
349                UserMessageContent::Mention { uri, content } => {
350                    match uri {
351                        MentionUri::File { abs_path } => {
352                            write!(
353                                &mut file_context,
354                                "\n{}",
355                                MarkdownCodeBlock {
356                                    tag: &codeblock_tag(abs_path, None),
357                                    text: content,
358                                }
359                            )
360                            .ok();
361                        }
362                        MentionUri::PastedImage { .. } => {
363                            debug_panic!("pasted image URI should not be used in mention content")
364                        }
365                        MentionUri::Directory { .. } => {
366                            write!(&mut directory_context, "\n{}\n", content).ok();
367                        }
368                        MentionUri::Symbol {
369                            abs_path: path,
370                            line_range,
371                            ..
372                        } => {
373                            write!(
374                                &mut symbol_context,
375                                "\n{}",
376                                MarkdownCodeBlock {
377                                    tag: &codeblock_tag(path, Some(line_range)),
378                                    text: content
379                                }
380                            )
381                            .ok();
382                        }
383                        MentionUri::Selection {
384                            abs_path: path,
385                            line_range,
386                            ..
387                        } => {
388                            write!(
389                                &mut selection_context,
390                                "\n{}",
391                                MarkdownCodeBlock {
392                                    tag: &codeblock_tag(
393                                        path.as_deref().unwrap_or("Untitled".as_ref()),
394                                        Some(line_range)
395                                    ),
396                                    text: content
397                                }
398                            )
399                            .ok();
400                        }
401                        MentionUri::Thread { .. } => {
402                            write!(&mut thread_context, "\n{}\n", content).ok();
403                        }
404                        MentionUri::Rule { .. } => {
405                            // Deprecated: keeps legacy rule mentions as context.
406                            write!(
407                                &mut rules_context,
408                                "\n{}",
409                                MarkdownCodeBlock {
410                                    tag: "",
411                                    text: content
412                                }
413                            )
414                            .ok();
415                        }
416                        MentionUri::Fetch { url } => {
417                            write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
418                        }
419                        MentionUri::Diagnostics { .. } => {
420                            write!(&mut diagnostics_context, "\n{}\n", content).ok();
421                        }
422                        MentionUri::TerminalSelection { .. } => {
423                            write!(
424                                &mut selection_context,
425                                "\n{}",
426                                MarkdownCodeBlock {
427                                    tag: "console",
428                                    text: content
429                                }
430                            )
431                            .ok();
432                        }
433                        MentionUri::GitDiff { base_ref } => {
434                            write!(
435                                &mut diffs_context,
436                                "\nBranch diff against {}:\n{}",
437                                base_ref,
438                                MarkdownCodeBlock {
439                                    tag: "diff",
440                                    text: content
441                                }
442                            )
443                            .ok();
444                        }
445                        MentionUri::MergeConflict { file_path } => {
446                            write!(
447                                &mut merge_conflict_context,
448                                "\nMerge conflict in {}:\n{}",
449                                file_path,
450                                MarkdownCodeBlock {
451                                    tag: "diff",
452                                    text: content
453                                }
454                            )
455                            .ok();
456                        }
457                        MentionUri::Skill { name, source, .. } => {
458                            let label = format!("{} ({})", name, source);
459                            write!(&mut skills_context, "\nSkill: {}\n{}\n", label, content).ok();
460                        }
461                    }
462
463                    language_model::MessageContent::Text(uri.as_link().to_string())
464                }
465            };
466
467            message.content.push(chunk);
468        }
469
470        let len_before_context = message.content.len();
471
472        if file_context.len() > OPEN_FILES_TAG.len() {
473            file_context.push_str("</files>\n");
474            message
475                .content
476                .push(language_model::MessageContent::Text(file_context));
477        }
478
479        if directory_context.len() > OPEN_DIRECTORIES_TAG.len() {
480            directory_context.push_str("</directories>\n");
481            message
482                .content
483                .push(language_model::MessageContent::Text(directory_context));
484        }
485
486        if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
487            symbol_context.push_str("</symbols>\n");
488            message
489                .content
490                .push(language_model::MessageContent::Text(symbol_context));
491        }
492
493        if selection_context.len() > OPEN_SELECTIONS_TAG.len() {
494            selection_context.push_str("</selections>\n");
495            message
496                .content
497                .push(language_model::MessageContent::Text(selection_context));
498        }
499
500        if diffs_context.len() > OPEN_DIFFS_TAG.len() {
501            diffs_context.push_str("</diffs>\n");
502            message
503                .content
504                .push(language_model::MessageContent::Text(diffs_context));
505        }
506
507        if thread_context.len() > OPEN_THREADS_TAG.len() {
508            thread_context.push_str("</threads>\n");
509            message
510                .content
511                .push(language_model::MessageContent::Text(thread_context));
512        }
513
514        if fetch_context.len() > OPEN_FETCH_TAG.len() {
515            fetch_context.push_str("</fetched_urls>\n");
516            message
517                .content
518                .push(language_model::MessageContent::Text(fetch_context));
519        }
520
521        if rules_context.len() > OPEN_RULES_TAG.len() {
522            rules_context.push_str("</user_rules>\n");
523            message
524                .content
525                .push(language_model::MessageContent::Text(rules_context));
526        }
527
528        if diagnostics_context.len() > OPEN_DIAGNOSTICS_TAG.len() {
529            diagnostics_context.push_str("</diagnostics>\n");
530            message
531                .content
532                .push(language_model::MessageContent::Text(diagnostics_context));
533        }
534
535        if skills_context.len() > OPEN_SKILLS_TAG.len() {
536            skills_context.push_str("</skills>\n");
537            message
538                .content
539                .push(language_model::MessageContent::Text(skills_context));
540        }
541
542        if merge_conflict_context.len() > MERGE_CONFLICT_TAG.len() {
543            merge_conflict_context.push_str("</merge_conflicts>\n");
544            message
545                .content
546                .push(language_model::MessageContent::Text(merge_conflict_context));
547        }
548
549        if message.content.len() > len_before_context {
550            message.content.insert(
551                len_before_context,
552                language_model::MessageContent::Text(OPEN_CONTEXT.into()),
553            );
554            message
555                .content
556                .push(language_model::MessageContent::Text("</context>".into()));
557        }
558
559        message
560    }
561}
562
563fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive<u32>>) -> String {
564    let mut result = String::new();
565
566    if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
567        let _ = write!(result, "{} ", extension);
568    }
569
570    let _ = write!(result, "{}", full_path.display());
571
572    if let Some(range) = line_range {
573        if range.start() == range.end() {
574            let _ = write!(result, ":{}", range.start() + 1);
575        } else {
576            let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1);
577        }
578    }
579
580    result
581}
582
583impl AgentMessage {
584    pub fn to_markdown(&self) -> String {
585        let mut markdown = String::new();
586
587        for content in &self.content {
588            match content {
589                AgentMessageContent::Text(text) => {
590                    markdown.push_str(text);
591                    markdown.push('\n');
592                }
593                AgentMessageContent::Thinking { text, .. } => {
594                    markdown.push_str("<think>");
595                    markdown.push_str(text);
596                    markdown.push_str("</think>\n");
597                }
598                AgentMessageContent::RedactedThinking(_) => {
599                    markdown.push_str("<redacted_thinking />\n")
600                }
601                AgentMessageContent::ToolUse(tool_use) => {
602                    markdown.push_str(&format!(
603                        "**Tool Use**: {} (ID: {})\n",
604                        tool_use.name, tool_use.id
605                    ));
606                    markdown.push_str(&format!(
607                        "{}\n",
608                        MarkdownCodeBlock {
609                            tag: "json",
610                            text: &format!("{:#}", tool_use.input.to_display_json())
611                        }
612                    ));
613                }
614            }
615        }
616
617        for tool_result in self.tool_results.values() {
618            markdown.push_str(&format!(
619                "**Tool Result**: {} (ID: {})\n\n",
620                tool_result.tool_name, tool_result.tool_use_id
621            ));
622            if tool_result.is_error {
623                markdown.push_str("**ERROR:**\n");
624            }
625
626            for part in &tool_result.content {
627                match part {
628                    LanguageModelToolResultContent::Text(text) => {
629                        writeln!(markdown, "{text}\n").ok();
630                    }
631                    LanguageModelToolResultContent::Image(_) => {
632                        writeln!(markdown, "<image />\n").ok();
633                    }
634                }
635            }
636
637            if let Some(output) = tool_result.output.as_ref() {
638                writeln!(
639                    markdown,
640                    "**Debug Output**:\n\n```json\n{}\n```\n",
641                    serde_json::to_string_pretty(output).unwrap()
642                )
643                .unwrap();
644            }
645        }
646
647        markdown
648    }
649
650    pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
651        let mut assistant_message = LanguageModelRequestMessage {
652            role: Role::Assistant,
653            content: Vec::with_capacity(self.content.len()),
654            cache: false,
655            reasoning_details: self.reasoning_details.clone(),
656        };
657        for chunk in &self.content {
658            match chunk {
659                AgentMessageContent::Text(text) => {
660                    assistant_message
661                        .content
662                        .push(language_model::MessageContent::Text(text.clone()));
663                }
664                AgentMessageContent::Thinking { text, signature } => {
665                    assistant_message
666                        .content
667                        .push(language_model::MessageContent::Thinking {
668                            text: text.clone(),
669                            signature: signature.clone(),
670                        });
671                }
672                AgentMessageContent::RedactedThinking(value) => {
673                    assistant_message.content.push(
674                        language_model::MessageContent::RedactedThinking(value.clone()),
675                    );
676                }
677                AgentMessageContent::ToolUse(tool_use) => {
678                    if self.tool_results.contains_key(&tool_use.id) {
679                        assistant_message
680                            .content
681                            .push(language_model::MessageContent::ToolUse(tool_use.clone()));
682                    }
683                }
684            };
685        }
686
687        let mut user_message = LanguageModelRequestMessage {
688            role: Role::User,
689            content: Vec::new(),
690            cache: false,
691            reasoning_details: None,
692        };
693
694        for tool_result in self.tool_results.values() {
695            let mut tool_result = tool_result.clone();
696            // Surprisingly, the API fails if we return an empty string here.
697            // It thinks we are sending a tool use without a tool result.
698            if tool_result.is_content_empty() {
699                tool_result.content = vec!["<Tool returned an empty string>".into()];
700            }
701            user_message
702                .content
703                .push(language_model::MessageContent::ToolResult(tool_result));
704        }
705
706        let mut messages = Vec::new();
707        if !assistant_message.content.is_empty() {
708            messages.push(assistant_message);
709        }
710        if !user_message.content.is_empty() {
711            messages.push(user_message);
712        }
713        messages
714    }
715}
716
717#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718pub struct AgentMessage {
719    pub(crate) content: Vec<AgentMessageContent>,
720    pub(crate) tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
721    pub(crate) reasoning_details: Option<Arc<serde_json::Value>>,
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
725pub enum AgentMessageContent {
726    Text(String),
727    Thinking {
728        text: String,
729        signature: Option<String>,
730    },
731    RedactedThinking(String),
732    ToolUse(LanguageModelToolUse),
733}
734
735pub trait TerminalHandle {
736    fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId>;
737    fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse>;
738    fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>>;
739    fn kill(&self, cx: &AsyncApp) -> Result<()>;
740    fn was_stopped_by_user(&self, cx: &AsyncApp) -> Result<bool>;
741}
742
743pub trait SubagentHandle {
744    /// The session ID of this subagent thread
745    fn id(&self) -> acp::SessionId;
746    /// The current number of entries in the thread.
747    /// Useful for knowing where the next turn will begin
748    fn num_entries(&self, cx: &App) -> usize;
749    /// Runs a turn for a given message and returns both the response and the index of that output message.
750    fn send(&self, message: String, cx: &AsyncApp) -> Task<Result<String>>;
751}
752
753pub trait ThreadEnvironment {
754    fn create_terminal(
755        &self,
756        command: String,
757        extra_env: Vec<acp::EnvVariable>,
758        cwd: Option<PathBuf>,
759        output_byte_limit: Option<u64>,
760        sandbox_wrap: Option<acp_thread::SandboxWrap>,
761        cx: &mut AsyncApp,
762    ) -> Task<Result<Rc<dyn TerminalHandle>>>;
763
764    fn create_subagent(&self, label: String, cx: &mut App) -> Result<Rc<dyn SubagentHandle>>;
765
766    fn resume_subagent(
767        &self,
768        _session_id: acp::SessionId,
769        _cx: &mut App,
770    ) -> Result<Rc<dyn SubagentHandle>> {
771        Err(anyhow::anyhow!(
772            "Resuming subagent sessions is not supported"
773        ))
774    }
775
776    /// Creates an independent sibling thread visible in the agent sidebar.
777    /// Unlike subagents, sibling threads are first-class threads that persist
778    /// and run in parallel without reporting results back to the parent.
779    fn create_sibling_thread(
780        &self,
781        request: SiblingThreadRequest,
782        cx: &mut AsyncApp,
783    ) -> Task<Result<SiblingThreadInfo>> {
784        let _ = request;
785        let _ = cx;
786        Task::ready(Err(anyhow::anyhow!(
787            "Creating sibling threads is not supported in this environment"
788        )))
789    }
790
791    /// Lists the agents and models available for use with `create_sibling_thread`.
792    fn list_available_agents(&self, cx: &mut App) -> Result<AvailableAgents> {
793        let _ = cx;
794        Err(anyhow::anyhow!(
795            "Listing available agents is not supported in this environment"
796        ))
797    }
798}
799
800/// A request to create a new sibling thread.
801#[derive(Debug, Clone)]
802pub struct SiblingThreadRequest {
803    /// A short title for the new thread, shown in the sidebar.
804    pub title: SharedString,
805    /// The initial prompt to send to the new thread.
806    pub prompt: String,
807    /// Optional agent ID to use. Defaults to the native Omega Agent executor.
808    pub agent_id: Option<String>,
809    /// Optional model override, as `provider/model-id`.
810    /// Defaults to the user's configured default model for the agent.
811    pub model: Option<String>,
812    /// Whether to create the thread in a new git worktree workspace.
813    pub use_new_worktree: bool,
814    /// Optional worktree directory name. When `None`, the UI generates a
815    /// random non-colliding name (matching the manual "Create worktree"
816    /// flow). Only relevant when `use_new_worktree` is true.
817    pub worktree_name: Option<String>,
818    /// Git ref (branch, tag, or commit) to base the new worktree on.
819    /// Only relevant when `use_new_worktree` is true.
820    pub base_ref: Option<String>,
821}
822
823/// Information returned when a sibling thread is successfully created.
824#[derive(Debug, Clone)]
825pub struct SiblingThreadInfo {
826    /// The title assigned to the thread.
827    pub title: SharedString,
828    /// The agent ID used for the thread.
829    pub agent_id: String,
830    /// The model ID used for the thread, if known.
831    pub model: Option<String>,
832    /// An optional, non-fatal heads-up about the created thread that the
833    /// caller should relay or take into account (e.g., the project had an
834    /// unusual worktree layout that affected how the new worktree was set
835    /// up). Empty when nothing noteworthy happened.
836    pub warning: Option<String>,
837}
838
839/// A list of agents and, for each, the models available for use.
840#[derive(Debug, Clone, Serialize, Deserialize)]
841pub struct AvailableAgents {
842    pub agents: Vec<AvailableAgent>,
843}
844
845#[derive(Debug, Clone, Serialize, Deserialize)]
846pub struct AvailableAgent {
847    /// Identifier used when creating a thread.
848    pub id: String,
849    /// Human-readable name shown in the UI.
850    pub name: SharedString,
851    /// Whether this is Zed's built-in native agent.
852    pub is_native: bool,
853    /// Models available for this agent. May be empty if models are not
854    /// enumerated up front (e.g., external agents that choose their own).
855    pub models: Vec<AvailableModel>,
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize)]
859pub struct AvailableModel {
860    /// Identifier to pass as the `model` field when creating a thread.
861    pub id: String,
862    /// Human-readable name.
863    pub name: SharedString,
864    /// Whether this is the default model for the agent.
865    pub is_default: bool,
866}
867
868#[derive(Debug)]
869pub enum ThreadEvent {
870    UserMessage(UserMessage),
871    AgentText(String),
872    AgentThinking(String),
873    ToolCall(acp::ToolCall),
874    ToolCallUpdate(acp_thread::ToolCallUpdate),
875    ToolCallAuthorization(ToolCallAuthorization),
876    ToolCallAuthorizationResolved {
877        tool_call_id: acp::ToolCallId,
878        outcome: acp_thread::SelectedPermissionOutcome,
879    },
880    SubagentSpawned(acp::SessionId),
881    Retry(acp_thread::RetryStatus),
882    ContextCompaction(acp_thread::ContextCompaction),
883    ContextCompactionUpdate(acp_thread::ContextCompactionUpdate),
884    Stop(acp::StopReason),
885}
886
887#[derive(Debug)]
888pub struct NewTerminal {
889    pub command: String,
890    pub output_byte_limit: Option<u64>,
891    pub cwd: Option<PathBuf>,
892    pub response: oneshot::Sender<Result<Entity<acp_thread::Terminal>>>,
893}
894
895#[derive(Debug, Clone)]
896pub struct ToolPermissionContext {
897    pub tool_name: String,
898    pub input_values: Vec<String>,
899    pub scope: ToolPermissionScope,
900}
901
902#[derive(Debug, Clone, Copy, PartialEq, Eq)]
903pub enum ToolPermissionScope {
904    ToolInput,
905    SymlinkTarget,
906    AgentSkills,
907}
908
909impl ToolPermissionContext {
910    pub fn new(tool_name: impl Into<String>, input_values: Vec<String>) -> Self {
911        Self {
912            tool_name: tool_name.into(),
913            input_values,
914            scope: ToolPermissionScope::ToolInput,
915        }
916    }
917
918    pub fn symlink_target(tool_name: impl Into<String>, target_paths: Vec<String>) -> Self {
919        Self {
920            tool_name: tool_name.into(),
921            input_values: target_paths,
922            scope: ToolPermissionScope::SymlinkTarget,
923        }
924    }
925
926    pub fn for_agent_skills(mut self) -> Self {
927        self.scope = ToolPermissionScope::AgentSkills;
928        self
929    }
930
931    /// Builds the permission options for this tool context.
932    ///
933    /// This is the canonical source for permission option generation.
934    /// Tests should use this function rather than manually constructing options.
935    ///
936    /// # Shell Compatibility for Terminal Tool
937    ///
938    /// For the terminal tool, "Always allow" options are only shown when the user's
939    /// shell supports POSIX-like command chaining syntax (`&&`, `||`, `;`, `|`).
940    ///
941    /// **Why this matters:** When a user sets up an "always allow" pattern like `^cargo`,
942    /// we need to parse the command to extract all sub-commands and verify that EVERY
943    /// sub-command matches the pattern. Otherwise, an attacker could craft a command like
944    /// `cargo build && rm -rf /` that would bypass the security check.
945    ///
946    /// **Supported shells:** Posix (sh, bash, dash, zsh), Fish 3.0+, PowerShell 7+/Pwsh,
947    /// Cmd, Xonsh, Csh, Tcsh
948    ///
949    /// **Unsupported shells:** Nushell (uses `and`/`or` keywords), Elvish (uses `and`/`or`
950    /// keywords), Rc (Plan 9 shell - no `&&`/`||` operators)
951    ///
952    /// For unsupported shells, we hide the "Always allow" UI options entirely, and if
953    /// the user has `always_allow` rules configured in settings, `ToolPermissionDecision::from_input`
954    /// will return a `Deny` with an explanatory error message.
955    pub fn build_permission_options(&self) -> acp_thread::PermissionOptions {
956        use crate::pattern_extraction::*;
957        use util::shell::ShellKind;
958
959        let tool_name = &self.tool_name;
960        let input_values = &self.input_values;
961        if self.scope == ToolPermissionScope::SymlinkTarget {
962            return acp_thread::PermissionOptions::Flat(vec![
963                acp::PermissionOption::new(
964                    acp::PermissionOptionId::new("allow"),
965                    "Yes",
966                    acp::PermissionOptionKind::AllowOnce,
967                ),
968                acp::PermissionOption::new(
969                    acp::PermissionOptionId::new("deny"),
970                    "No",
971                    acp::PermissionOptionKind::RejectOnce,
972                ),
973            ]);
974        }
975
976        // Skills always prompt, so offer only once-only allow/deny.
977        if self.scope == ToolPermissionScope::AgentSkills {
978            return acp_thread::PermissionOptions::Flat(vec![
979                acp::PermissionOption::new(
980                    acp::PermissionOptionId::new("allow"),
981                    "Allow",
982                    acp::PermissionOptionKind::AllowOnce,
983                ),
984                acp::PermissionOption::new(
985                    acp::PermissionOptionId::new("deny"),
986                    "Deny",
987                    acp::PermissionOptionKind::RejectOnce,
988                ),
989            ]);
990        }
991
992        // Check if the user's shell supports POSIX-like command chaining.
993        // See the doc comment above for the full explanation of why this is needed.
994        let shell_supports_always_allow = if tool_name == TerminalTool::NAME {
995            ShellKind::system().supports_posix_chaining()
996        } else {
997            true
998        };
999
1000        // For terminal commands with multiple pipeline commands, use DropdownWithPatterns
1001        // to let users individually select which command patterns to always allow.
1002        if tool_name == TerminalTool::NAME && shell_supports_always_allow {
1003            if let Some(input) = input_values.first() {
1004                let all_patterns = extract_all_terminal_patterns(input);
1005                if all_patterns.len() > 1 {
1006                    let mut choices = Vec::new();
1007                    choices.push(acp_thread::PermissionOptionChoice {
1008                        allow: acp::PermissionOption::new(
1009                            acp::PermissionOptionId::new(format!("always_allow:{}", tool_name)),
1010                            format!("Always for {}", tool_name.replace('_', " ")),
1011                            acp::PermissionOptionKind::AllowAlways,
1012                        ),
1013                        deny: acp::PermissionOption::new(
1014                            acp::PermissionOptionId::new(format!("always_deny:{}", tool_name)),
1015                            format!("Always for {}", tool_name.replace('_', " ")),
1016                            acp::PermissionOptionKind::RejectAlways,
1017                        ),
1018                        sub_patterns: vec![],
1019                    });
1020                    choices.push(acp_thread::PermissionOptionChoice {
1021                        allow: acp::PermissionOption::new(
1022                            acp::PermissionOptionId::new("allow"),
1023                            "Only this time",
1024                            acp::PermissionOptionKind::AllowOnce,
1025                        ),
1026                        deny: acp::PermissionOption::new(
1027                            acp::PermissionOptionId::new("deny"),
1028                            "Only this time",
1029                            acp::PermissionOptionKind::RejectOnce,
1030                        ),
1031                        sub_patterns: vec![],
1032                    });
1033                    return acp_thread::PermissionOptions::DropdownWithPatterns {
1034                        choices,
1035                        patterns: all_patterns,
1036                        tool_name: tool_name.clone(),
1037                    };
1038                }
1039            }
1040        }
1041
1042        let extract_for_value = |value: &str| -> (Option<String>, Option<String>) {
1043            if tool_name == TerminalTool::NAME {
1044                (
1045                    extract_terminal_pattern(value),
1046                    extract_terminal_pattern_display(value),
1047                )
1048            } else if tool_name == CopyPathTool::NAME
1049                || tool_name == MovePathTool::NAME
1050                || tool_name == EditFileTool::NAME
1051                || tool_name == WriteFileTool::NAME
1052                || tool_name == DeletePathTool::NAME
1053                || tool_name == CreateDirectoryTool::NAME
1054            {
1055                (
1056                    extract_path_pattern(value),
1057                    extract_path_pattern_display(value),
1058                )
1059            } else if tool_name == FetchTool::NAME {
1060                (
1061                    extract_url_pattern(value),
1062                    extract_url_pattern_display(value),
1063                )
1064            } else {
1065                (None, None)
1066            }
1067        };
1068
1069        // Extract patterns from all input values. Only offer a pattern-specific
1070        // "always allow/deny" button when every value produces the same pattern.
1071        let (pattern, pattern_display) = match input_values.as_slice() {
1072            [single] => extract_for_value(single),
1073            _ => {
1074                let mut iter = input_values.iter().map(|v| extract_for_value(v));
1075                match iter.next() {
1076                    Some(first) => {
1077                        if iter.all(|pair| pair.0 == first.0) {
1078                            first
1079                        } else {
1080                            (None, None)
1081                        }
1082                    }
1083                    None => (None, None),
1084                }
1085            }
1086        };
1087
1088        let mut choices = Vec::new();
1089
1090        let mut push_choice =
1091            |label: String, allow_id, deny_id, allow_kind, deny_kind, sub_patterns: Vec<String>| {
1092                choices.push(acp_thread::PermissionOptionChoice {
1093                    allow: acp::PermissionOption::new(
1094                        acp::PermissionOptionId::new(allow_id),
1095                        label.clone(),
1096                        allow_kind,
1097                    ),
1098                    deny: acp::PermissionOption::new(
1099                        acp::PermissionOptionId::new(deny_id),
1100                        label,
1101                        deny_kind,
1102                    ),
1103                    sub_patterns,
1104                });
1105            };
1106
1107        if shell_supports_always_allow {
1108            push_choice(
1109                format!("Always for {}", tool_name.replace('_', " ")),
1110                format!("always_allow:{}", tool_name),
1111                format!("always_deny:{}", tool_name),
1112                acp::PermissionOptionKind::AllowAlways,
1113                acp::PermissionOptionKind::RejectAlways,
1114                vec![],
1115            );
1116
1117            if let (Some(pattern), Some(display)) = (pattern, pattern_display) {
1118                let button_text = if tool_name == TerminalTool::NAME {
1119                    format!("Always for `{}` commands", display)
1120                } else {
1121                    format!("Always for `{}`", display)
1122                };
1123                push_choice(
1124                    button_text,
1125                    format!("always_allow:{}", tool_name),
1126                    format!("always_deny:{}", tool_name),
1127                    acp::PermissionOptionKind::AllowAlways,
1128                    acp::PermissionOptionKind::RejectAlways,
1129                    vec![pattern],
1130                );
1131            }
1132        }
1133
1134        push_choice(
1135            "Only this time".to_string(),
1136            "allow".to_string(),
1137            "deny".to_string(),
1138            acp::PermissionOptionKind::AllowOnce,
1139            acp::PermissionOptionKind::RejectOnce,
1140            vec![],
1141        );
1142
1143        acp_thread::PermissionOptions::Dropdown(choices)
1144    }
1145}
1146
1147#[derive(Debug)]
1148pub struct ToolCallAuthorization {
1149    pub tool_call: acp::ToolCallUpdate,
1150    pub options: acp_thread::PermissionOptions,
1151    pub response: oneshot::Sender<acp_thread::SelectedPermissionOutcome>,
1152    pub context: Option<ToolPermissionContext>,
1153    pub kind: acp_thread::AuthorizationKind,
1154}
1155
1156fn auto_resolve_permission_outcome(
1157    options: &acp_thread::PermissionOptions,
1158    is_allow: bool,
1159) -> Result<acp_thread::SelectedPermissionOutcome> {
1160    let kind = if is_allow {
1161        acp::PermissionOptionKind::AllowOnce
1162    } else {
1163        acp::PermissionOptionKind::RejectOnce
1164    };
1165    let option = options
1166        .first_option_of_kind(kind)
1167        .ok_or_else(|| anyhow!("permission prompt has no auto-resolution option"))?;
1168
1169    Ok(acp_thread::SelectedPermissionOutcome::new(
1170        option.option_id.clone(),
1171        option.kind,
1172    ))
1173}
1174
1175#[derive(Debug, thiserror::Error)]
1176enum CompletionError {
1177    #[error("max tokens")]
1178    MaxTokens,
1179    #[error("refusal")]
1180    Refusal,
1181    #[error(transparent)]
1182    Other(#[from] anyhow::Error),
1183}
1184
1185pub enum ThreadModel {
1186    Ready(Arc<dyn LanguageModel>),
1187    Unresolved(SelectedModel),
1188    Unset,
1189}
1190
1191impl ThreadModel {
1192    fn as_model(&self) -> Option<&Arc<dyn LanguageModel>> {
1193        match self {
1194            Self::Ready(model) => Some(model),
1195            Self::Unresolved(_) | Self::Unset => None,
1196        }
1197    }
1198}
1199
1200impl From<&ThreadModel> for Option<DbLanguageModel> {
1201    fn from(model: &ThreadModel) -> Self {
1202        match model {
1203            ThreadModel::Ready(model) => Some(DbLanguageModel {
1204                provider: model.provider_id().to_string(),
1205                model: model.id().0.to_string(),
1206            }),
1207            ThreadModel::Unresolved(selection) => Some(DbLanguageModel {
1208                provider: selection.provider.0.to_string(),
1209                model: selection.model.0.to_string(),
1210            }),
1211            ThreadModel::Unset => None,
1212        }
1213    }
1214}
1215
1216pub struct Thread {
1217    id: acp::SessionId,
1218    prompt_id: PromptId,
1219    updated_at: DateTime<Utc>,
1220    title: Option<SharedString>,
1221    pending_title_generation: Option<Task<()>>,
1222    title_generation_error: Option<SharedString>,
1223    pending_summary_generation: Option<Shared<Task<Option<SharedString>>>>,
1224    summary: Option<SharedString>,
1225    messages: Vec<Arc<Message>>,
1226    user_store: Entity<UserStore>,
1227    /// Holds the task that handles agent interaction until the end of the turn.
1228    /// Survives across multiple requests as the model performs tool calls and
1229    /// we run tools, report their results.
1230    running_turn: Option<RunningTurn>,
1231    /// When set, the current turn ends at the next message boundary instead of
1232    /// running to completion. The UI sets this to deliver a "steering" queued
1233    /// message mid-task; by default queued messages wait for the turn to finish.
1234    end_turn_at_next_boundary: bool,
1235    pending_message: Option<AgentMessage>,
1236    pub(crate) tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
1237    request_token_usage: HashMap<ClientUserMessageId, language_model::TokenUsage>,
1238    cumulative_token_usage: TokenUsage,
1239    /// The per-field maximum usage snapshot already added to
1240    /// `cumulative_token_usage` for the in-flight completion request. Reset at
1241    /// the start of each request.
1242    current_request_token_usage: TokenUsage,
1243    pending_compaction_telemetry: Option<CompactionTelemetry>,
1244    #[allow(unused)]
1245    initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
1246    pub(crate) context_server_registry: Entity<ContextServerRegistry>,
1247    profile_id: AgentProfileId,
1248    /// Whether `profile_id` was downgraded to `minimal` at thread start because
1249    /// the workspace is restricted. Used purely to surface a warning in the UI.
1250    profile_downgraded_for_restricted_workspace: bool,
1251    project_context: Entity<ProjectContext>,
1252    pub(crate) templates: Arc<Templates>,
1253    model: ThreadModel,
1254    summarization_model: Option<Arc<dyn LanguageModel>>,
1255    thinking_enabled: bool,
1256    thinking_effort: Option<String>,
1257    speed: Option<Speed>,
1258    prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
1259    pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
1260    pub(crate) project: Entity<Project>,
1261    pub(crate) action_log: Entity<ActionLog>,
1262    /// If this is a subagent thread, contains context about the parent
1263    subagent_context: Option<SubagentContext>,
1264    /// The user's unsent prompt text, persisted so it can be restored when reloading the thread.
1265    draft_prompt: Option<Vec<acp::ContentBlock>>,
1266    ui_scroll_position: Option<gpui::ListOffset>,
1267    /// Weak references to running subagent threads for cancellation propagation
1268    running_subagents: Vec<WeakEntity<Thread>>,
1269    inherits_parent_model_settings: bool,
1270    sandboxed_terminal_temp_dir: Option<PathBuf>,
1271    /// Sandbox permissions the user approved "for the rest of the thread".
1272    /// Shared with each tool call's event stream so repeated requests for
1273    /// already-granted permissions skip the approval prompt.
1274    /// Never persisted — lives and dies with this thread.
1275    sandbox_grants: Rc<RefCell<ThreadSandboxGrants>>,
1276}
1277
1278impl Thread {
1279    fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
1280        let image = model.map_or(true, |model| model.supports_images());
1281        acp::PromptCapabilities::new()
1282            .image(image)
1283            .embedded_context(true)
1284    }
1285
1286    pub fn new_subagent(parent_thread: &Entity<Thread>, cx: &mut Context<Self>) -> Self {
1287        let project = parent_thread.read(cx).project.clone();
1288        let project_context = parent_thread.read(cx).project_context.clone();
1289        let context_server_registry = parent_thread.read(cx).context_server_registry.clone();
1290        let templates = parent_thread.read(cx).templates.clone();
1291        let model = parent_thread.read(cx).model().cloned();
1292        let parent_action_log = parent_thread.read(cx).action_log().clone();
1293        let action_log =
1294            cx.new(|_cx| ActionLog::new(project.clone()).with_linked_action_log(parent_action_log));
1295        let mut thread = Self::new_internal(
1296            project,
1297            project_context,
1298            context_server_registry,
1299            templates,
1300            model,
1301            action_log,
1302            cx,
1303        );
1304        thread.subagent_context = Some(SubagentContext {
1305            parent_thread_id: parent_thread.read(cx).id().clone(),
1306            depth: parent_thread.read(cx).depth() + 1,
1307        });
1308        thread.inherit_parent_settings(parent_thread, cx);
1309        if let Some(subagent_model) = AgentSettings::get_global(cx).subagent_model.clone() {
1310            thread.inherits_parent_model_settings = false;
1311            thread.apply_model_selection(&subagent_model, cx);
1312        }
1313        thread
1314    }
1315
1316    pub fn new(
1317        project: Entity<Project>,
1318        project_context: Entity<ProjectContext>,
1319        context_server_registry: Entity<ContextServerRegistry>,
1320        templates: Arc<Templates>,
1321        model: Option<Arc<dyn LanguageModel>>,
1322        cx: &mut Context<Self>,
1323    ) -> Self {
1324        Self::new_internal(
1325            project.clone(),
1326            project_context,
1327            context_server_registry,
1328            templates,
1329            model,
1330            cx.new(|_cx| ActionLog::new(project)),
1331            cx,
1332        )
1333    }
1334
1335    fn new_internal(
1336        project: Entity<Project>,
1337        project_context: Entity<ProjectContext>,
1338        context_server_registry: Entity<ContextServerRegistry>,
1339        templates: Arc<Templates>,
1340        model: Option<Arc<dyn LanguageModel>>,
1341        action_log: Entity<ActionLog>,
1342        cx: &mut Context<Self>,
1343    ) -> Self {
1344        let settings = AgentSettings::get_global(cx);
1345        let (profile_id, profile_downgraded_for_restricted_workspace) =
1346            Self::profile_for_restricted_workspace(settings.default_profile.clone(), &project, cx);
1347        let enable_thinking = settings
1348            .default_model
1349            .as_ref()
1350            .is_some_and(|model| model.enable_thinking);
1351        let thinking_effort = settings
1352            .default_model
1353            .as_ref()
1354            .and_then(|model| model.effort.clone());
1355        let speed = settings
1356            .default_model
1357            .as_ref()
1358            .and_then(|model| model.speed);
1359        let (prompt_capabilities_tx, prompt_capabilities_rx) =
1360            watch::channel(Self::prompt_capabilities(model.as_deref()));
1361        let model = match model {
1362            Some(model) => ThreadModel::Ready(model),
1363            None => Self::user_configured_model_selection(cx)
1364                .map_or(ThreadModel::Unset, ThreadModel::Unresolved),
1365        };
1366        Self {
1367            id: acp::SessionId::new(uuid::Uuid::new_v4().to_string()),
1368            prompt_id: PromptId::new(),
1369            updated_at: Utc::now(),
1370            title: None,
1371            pending_title_generation: None,
1372            title_generation_error: None,
1373            pending_summary_generation: None,
1374            summary: None,
1375            messages: Vec::new(),
1376            user_store: project.read(cx).user_store(),
1377            running_turn: None,
1378            end_turn_at_next_boundary: false,
1379            pending_message: None,
1380            tools: BTreeMap::default(),
1381            request_token_usage: HashMap::default(),
1382            cumulative_token_usage: TokenUsage::default(),
1383            current_request_token_usage: TokenUsage::default(),
1384            pending_compaction_telemetry: None,
1385            initial_project_snapshot: {
1386                let project_snapshot = Self::project_snapshot(project.clone(), cx);
1387                cx.foreground_executor()
1388                    .spawn(async move { Some(project_snapshot.await) })
1389                    .shared()
1390            },
1391            context_server_registry,
1392            profile_id,
1393            profile_downgraded_for_restricted_workspace,
1394            project_context,
1395            templates,
1396            model,
1397            summarization_model: None,
1398            thinking_enabled: enable_thinking,
1399            speed,
1400            thinking_effort,
1401            prompt_capabilities_tx,
1402            prompt_capabilities_rx,
1403            project,
1404            action_log,
1405            subagent_context: None,
1406            draft_prompt: None,
1407            ui_scroll_position: None,
1408            running_subagents: Vec::new(),
1409            inherits_parent_model_settings: true,
1410            sandboxed_terminal_temp_dir: None,
1411            sandbox_grants: Rc::new(RefCell::new(ThreadSandboxGrants::default())),
1412        }
1413    }
1414
1415    /// Copies runtime-mutable settings from the parent thread so that
1416    /// subagents start with the same configuration the user selected.
1417    /// Every property that `set_*` propagates to `running_subagents`
1418    /// should be inherited here as well.
1419    fn inherit_parent_settings(&mut self, parent_thread: &Entity<Thread>, cx: &mut Context<Self>) {
1420        let parent = parent_thread.read(cx);
1421        self.speed = parent.speed;
1422        self.thinking_enabled = parent.thinking_enabled;
1423        self.thinking_effort = parent.thinking_effort.clone();
1424        self.summarization_model = parent.summarization_model.clone();
1425        self.profile_id = parent.profile_id.clone();
1426        self.profile_downgraded_for_restricted_workspace =
1427            parent.profile_downgraded_for_restricted_workspace;
1428    }
1429
1430    fn apply_model_selection(
1431        &mut self,
1432        selection: &LanguageModelSelection,
1433        cx: &mut Context<Self>,
1434    ) {
1435        let Some(model) = Self::resolve_model_from_selection(selection, cx) else {
1436            log::warn!(
1437                "failed to resolve configured subagent model: {}/{}",
1438                selection.provider.0,
1439                selection.model
1440            );
1441            return;
1442        };
1443
1444        self.thinking_enabled = selection.enable_thinking && model.supports_thinking();
1445        self.thinking_effort = selection.effort.clone();
1446        self.speed = selection.speed.filter(|_| model.supports_fast_mode());
1447        self.prompt_capabilities_tx
1448            .send(Self::prompt_capabilities(Some(model.as_ref())))
1449            .log_err();
1450        self.model = ThreadModel::Ready(model);
1451    }
1452
1453    pub fn id(&self) -> &acp::SessionId {
1454        &self.id
1455    }
1456
1457    // Only used by Seatbelt-style sandboxes (macOS); Linux relies on bwrap's
1458    // tmpfs `/tmp` and Windows on the WSL bwrap tmpfs, so neither needs a
1459    // per-thread temp directory.
1460    #[cfg(not(any(target_os = "linux", target_os = "windows")))]
1461    pub(crate) fn sandboxed_terminal_temp_dir(
1462        &mut self,
1463        cx: &mut Context<Self>,
1464    ) -> Result<PathBuf> {
1465        if let Some(temp_dir) = &self.sandboxed_terminal_temp_dir {
1466            std::fs::create_dir_all(temp_dir).with_context(|| {
1467                format!(
1468                    "failed to recreate sandboxed terminal temp directory {}",
1469                    temp_dir.display()
1470                )
1471            })?;
1472            return Ok(temp_dir.clone());
1473        }
1474
1475        let temp_dir = tempfile::Builder::new()
1476            .prefix("omega-agent-terminal-")
1477            .tempdir()
1478            .context("failed to create sandboxed terminal temp directory")?;
1479        let temp_dir = temp_dir.keep();
1480        self.sandboxed_terminal_temp_dir = Some(temp_dir.clone());
1481        cx.notify();
1482        Ok(temp_dir)
1483    }
1484
1485    pub fn replay(
1486        &mut self,
1487        cx: &mut Context<Self>,
1488    ) -> mpsc::UnboundedReceiver<Result<ThreadEvent>> {
1489        let (tx, rx) = mpsc::unbounded();
1490        let stream = ThreadEventStream(tx);
1491        for (message_ix, message) in self.messages.iter().enumerate() {
1492            match &**message {
1493                Message::User(user_message) => stream.send_user_message(user_message),
1494                Message::Agent(assistant_message) => {
1495                    for content in &assistant_message.content {
1496                        match content {
1497                            AgentMessageContent::Text(text) => stream.send_text(text),
1498                            AgentMessageContent::Thinking { text, .. } => {
1499                                stream.send_thinking(text)
1500                            }
1501                            AgentMessageContent::RedactedThinking(_) => {}
1502                            AgentMessageContent::ToolUse(tool_use) => {
1503                                self.replay_tool_call(
1504                                    tool_use,
1505                                    assistant_message.tool_results.get(&tool_use.id),
1506                                    message_ix,
1507                                    &stream,
1508                                    cx,
1509                                );
1510                            }
1511                        }
1512                    }
1513                }
1514                Message::Resume => {}
1515                Message::Compaction(info) => {
1516                    let compaction_id = acp_thread::ContextCompactionId(
1517                        format!("replay-compaction-{message_ix}").into(),
1518                    );
1519                    match info {
1520                        CompactionInfo::Summary(summary) => {
1521                            stream.send_context_compaction(
1522                                compaction_id.clone(),
1523                                acp_thread::ContextCompactionStatus::Completed,
1524                            );
1525                            stream.send_context_compaction_update(compaction_id.clone(), summary);
1526                        }
1527                        CompactionInfo::ProviderNative { .. } => {
1528                            stream.send_context_compaction(
1529                                compaction_id,
1530                                acp_thread::ContextCompactionStatus::Completed,
1531                            );
1532                        }
1533                    }
1534                }
1535            }
1536        }
1537        rx
1538    }
1539
1540    fn replay_tool_call(
1541        &self,
1542        tool_use: &LanguageModelToolUse,
1543        tool_result: Option<&LanguageModelToolResult>,
1544        owning_message_ix: usize,
1545        stream: &ThreadEventStream,
1546        cx: &mut Context<Self>,
1547    ) {
1548        // A tool call left only with the canceled sentinel produced nothing useful
1549        // (the sentinel is model-facing only, and is inserted exactly when a tool
1550        // had no real result). Don't replay it into the UI at all.
1551        if tool_result.is_some_and(Self::is_canceled_tool_result) {
1552            return;
1553        }
1554
1555        let tool_call_id = scoped_tool_call_id(owning_message_ix, &tool_use.id);
1556        let output = tool_result
1557            .as_ref()
1558            .and_then(|result| result.output.clone());
1559        let replay_content = tool_result.and_then(Self::tool_result_content_for_replay);
1560        let status = tool_result
1561            .as_ref()
1562            .map_or(acp::ToolCallStatus::Failed, |result| {
1563                if result.is_error {
1564                    acp::ToolCallStatus::Failed
1565                } else {
1566                    acp::ToolCallStatus::Completed
1567                }
1568            });
1569
1570        // Recorded tool calls use the model-facing name, so a terminal call is
1571        // always keyed as `terminal` and resolves to the non-sandboxed
1572        // `TerminalTool` here, even if it originally ran under
1573        // `SandboxedTerminalTool`. That's safe because both variants share the
1574        // same `replay` behavior; replay only reconstructs UI state and never
1575        // re-runs the command or re-applies sandbox policy.
1576        let tool = self.tools.get(tool_use.name.as_ref()).cloned().or_else(|| {
1577            self.context_server_registry
1578                .read(cx)
1579                .servers()
1580                .find_map(|(_, tools)| {
1581                    if let Some(tool) = tools.get(tool_use.name.as_ref()) {
1582                        Some(tool.clone())
1583                    } else {
1584                        None
1585                    }
1586                })
1587        });
1588
1589        let Some(tool) = tool else {
1590            // Tool not found (e.g., MCP server not connected after restart),
1591            // but still display the saved result if available.
1592            // We need to send both ToolCall and ToolCallUpdate events because the UI
1593            // only converts raw_output to displayable content in update_fields, not from_acp.
1594            stream
1595                .0
1596                .unbounded_send(Ok(ThreadEvent::ToolCall(
1597                    acp::ToolCall::new(tool_call_id.clone(), tool_use.name.to_string())
1598                        .status(status)
1599                        .raw_input(tool_use.input.to_display_json()),
1600                )))
1601                .ok();
1602            let mut fields = acp::ToolCallUpdateFields::new()
1603                .status(status)
1604                .raw_output(output);
1605            if let Some(content) = replay_content {
1606                fields = fields.content(content);
1607            }
1608            stream.update_tool_call_fields(&tool_call_id, fields, None);
1609            return;
1610        };
1611
1612        let Ok(input) = tool_use.input.clone().into_json() else {
1613            return;
1614        };
1615        let title = tool.initial_title(input.clone(), cx);
1616        let kind = tool.kind();
1617        stream.send_tool_call(&tool_call_id, &tool_use.name, title, kind, input.clone());
1618
1619        if let Some(content) = replay_content {
1620            stream.update_tool_call_fields(
1621                &tool_call_id,
1622                acp::ToolCallUpdateFields::new().content(content),
1623                None,
1624            );
1625        }
1626
1627        if let Some(output) = output.clone() {
1628            // For replay, we use a dummy cancellation receiver since the tool already completed
1629            let (_cancellation_tx, cancellation_rx) = watch::channel(false);
1630            let tool_event_stream = ToolCallEventStream::new(
1631                tool_use.id.clone(),
1632                tool_call_id.clone(),
1633                stream.clone(),
1634                Some(self.project.read(cx).fs().clone()),
1635                cancellation_rx,
1636                self.sandbox_grants.clone(),
1637                Some(cx.weak_entity()),
1638            );
1639            tool.replay(input, output, tool_event_stream, cx).log_err();
1640        }
1641
1642        stream.update_tool_call_fields(
1643            &tool_call_id,
1644            acp::ToolCallUpdateFields::new()
1645                .status(status)
1646                .raw_output(output),
1647            None,
1648        );
1649    }
1650
1651    /// A canceled tool result carries only the model-facing `TOOL_CANCELED_MESSAGE`
1652    /// sentinel (inserted exactly when a tool had no real result). It's never
1653    /// meaningful to the user, so we detect it to skip replaying the tool call.
1654    fn is_canceled_tool_result(tool_result: &LanguageModelToolResult) -> bool {
1655        tool_result.is_error
1656            && matches!(
1657                tool_result.content.as_slice(),
1658                [LanguageModelToolResultContent::Text(text)]
1659                    if text.as_ref() == TOOL_CANCELED_MESSAGE
1660            )
1661    }
1662
1663    fn tool_result_content_for_replay(
1664        tool_result: &LanguageModelToolResult,
1665    ) -> Option<Vec<acp::ToolCallContent>> {
1666        let has_image = tool_result
1667            .content
1668            .iter()
1669            .any(|part| matches!(part, LanguageModelToolResultContent::Image(_)));
1670        if !has_image && tool_result.output.is_some() {
1671            return None;
1672        }
1673
1674        let content = tool_result
1675            .content
1676            .iter()
1677            .filter_map(|part| match part {
1678                LanguageModelToolResultContent::Text(text) => {
1679                    if text.is_empty() {
1680                        None
1681                    } else {
1682                        Some(acp::ToolCallContent::Content(acp::Content::new(
1683                            acp::ContentBlock::Text(acp::TextContent::new(text.to_string())),
1684                        )))
1685                    }
1686                }
1687                LanguageModelToolResultContent::Image(image) => Some(
1688                    acp::ToolCallContent::Content(acp::Content::new(acp::ContentBlock::Image(
1689                        acp::ImageContent::new(image.source.clone(), "image/png"),
1690                    ))),
1691                ),
1692            })
1693            .collect::<Vec<_>>();
1694
1695        if content.is_empty() {
1696            None
1697        } else {
1698            Some(content)
1699        }
1700    }
1701
1702    pub fn from_db(
1703        id: acp::SessionId,
1704        db_thread: DbThread,
1705        project: Entity<Project>,
1706        project_context: Entity<ProjectContext>,
1707        context_server_registry: Entity<ContextServerRegistry>,
1708        templates: Arc<Templates>,
1709        cx: &mut Context<Self>,
1710    ) -> Self {
1711        let settings = AgentSettings::get_global(cx);
1712        let profile_id = db_thread
1713            .profile
1714            .unwrap_or_else(|| settings.default_profile.clone());
1715
1716        let saved_selection = db_thread.model.map(|model| SelectedModel {
1717            provider: model.provider.into(),
1718            model: model.model.into(),
1719        });
1720
1721        let resolved_saved_model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
1722            saved_selection
1723                .as_ref()
1724                .and_then(|selection| registry.select_model(selection, cx))
1725                .map(|configured| configured.model)
1726        });
1727
1728        let model = match (resolved_saved_model, saved_selection) {
1729            (Some(model), _) => ThreadModel::Ready(model),
1730            (None, Some(selection)) => ThreadModel::Unresolved(selection),
1731            (None, None) => Self::resolve_profile_model(&profile_id, cx)
1732                .or_else(|| {
1733                    LanguageModelRegistry::global(cx).update(cx, |registry, _cx| {
1734                        registry.default_model().map(|model| model.model)
1735                    })
1736                })
1737                .map_or(ThreadModel::Unset, ThreadModel::Ready),
1738        };
1739
1740        let (prompt_capabilities_tx, prompt_capabilities_rx) = watch::channel(
1741            Self::prompt_capabilities(model.as_model().map(|model| model.as_ref())),
1742        );
1743
1744        let action_log = cx.new(|_| ActionLog::new(project.clone()));
1745
1746        Self {
1747            id,
1748            prompt_id: PromptId::new(),
1749            title: if db_thread.title.is_empty() {
1750                None
1751            } else {
1752                Some(db_thread.title.clone())
1753            },
1754            pending_title_generation: None,
1755            title_generation_error: None,
1756            pending_summary_generation: None,
1757            summary: db_thread.detailed_summary,
1758            messages: db_thread.messages,
1759            user_store: project.read(cx).user_store(),
1760            running_turn: None,
1761            end_turn_at_next_boundary: false,
1762            pending_message: None,
1763            tools: BTreeMap::default(),
1764            request_token_usage: db_thread.request_token_usage.clone(),
1765            cumulative_token_usage: db_thread.cumulative_token_usage,
1766            current_request_token_usage: TokenUsage::default(),
1767            pending_compaction_telemetry: None,
1768            initial_project_snapshot: Task::ready(db_thread.initial_project_snapshot).shared(),
1769            context_server_registry,
1770            profile_id,
1771            profile_downgraded_for_restricted_workspace: false,
1772            project_context,
1773            templates,
1774            model,
1775            summarization_model: None,
1776            thinking_enabled: db_thread.thinking_enabled,
1777            thinking_effort: db_thread.thinking_effort,
1778            speed: db_thread.speed,
1779            project,
1780            action_log,
1781            updated_at: db_thread.updated_at,
1782            prompt_capabilities_tx,
1783            prompt_capabilities_rx,
1784            subagent_context: db_thread.subagent_context,
1785            draft_prompt: db_thread.draft_prompt,
1786            ui_scroll_position: db_thread.ui_scroll_position.map(|sp| gpui::ListOffset {
1787                item_ix: sp.item_ix,
1788                offset_in_item: gpui::px(sp.offset_in_item),
1789            }),
1790            running_subagents: Vec::new(),
1791            inherits_parent_model_settings: true,
1792            sandboxed_terminal_temp_dir: db_thread.sandboxed_terminal_temp_dir,
1793            sandbox_grants: Rc::new(RefCell::new(ThreadSandboxGrants::from_db(
1794                &db_thread.sandbox_grants,
1795            ))),
1796        }
1797    }
1798
1799    pub fn sandbox_status(&self, cx: &App) -> Option<(ThreadSandbox, ThreadSandbox)> {
1800        if !self.sandboxing_available(cx) {
1801            return None;
1802        }
1803        let persistent = AgentSettings::get_global(cx).sandbox_permissions.clone();
1804        let git_dirs = sandbox_git_dirs(self.project.read(cx), cx);
1805        let grants = self.sandbox_grants.borrow();
1806        let settings = crate::sandboxing::settings_thread_sandbox(&persistent)
1807            .with_protected_paths(git_dirs.clone());
1808        let thread = grants.thread_sandbox().with_protected_paths(git_dirs);
1809        Some((settings, thread))
1810    }
1811
1812    pub fn refresh_verified_sandbox_status(
1813        &self,
1814        cx: &mut Context<Self>,
1815    ) -> Option<(SandboxStatusKey, SandboxStatusRefresh)> {
1816        if !self.sandboxing_available(cx) {
1817            return None;
1818        }
1819
1820        let persistent = AgentSettings::get_global(cx).sandbox_permissions.clone();
1821        let settings_sandbox = crate::sandboxing::settings_thread_sandbox(&persistent);
1822        let grants = self.sandbox_grants.borrow();
1823        let thread_sandbox = grants.thread_sandbox();
1824        drop(grants);
1825
1826        let project = self.project.read(cx);
1827        let baseline_writable_paths = sandbox_worktree_writable_paths(project, cx);
1828        let git_paths = sandbox_git_dirs(project, cx);
1829
1830        let key = SandboxStatusKey {
1831            settings_sandbox: settings_sandbox.clone(),
1832            thread_sandbox: thread_sandbox.clone(),
1833            baseline_writable_paths: baseline_writable_paths.clone(),
1834            git_paths: git_paths.clone(),
1835        };
1836
1837        Some((
1838            key,
1839            SandboxStatusRefresh::Ready(VerifiedSandboxStatus {
1840                settings_sandbox: settings_sandbox.with_protected_paths(git_paths.clone()),
1841                thread_sandbox: thread_sandbox.with_protected_paths(git_paths),
1842                baseline_writable_paths,
1843            }),
1844        ))
1845    }
1846
1847    /// Whether agent terminal commands are sandboxed for this thread's project,
1848    /// so the UI can decide whether to surface the sandbox status at all.
1849    pub fn sandboxing_enabled(&self, cx: &App) -> bool {
1850        sandboxing_enabled_for_project(self.project.read(cx), cx)
1851    }
1852
1853    /// Whether sandboxing is *applicable* for this thread's project (feature on,
1854    /// local project, supported platform), regardless of whether it's been
1855    /// turned off in settings. The UI shows the sandbox indicator whenever this
1856    /// is true, drawing it struck-out when sandboxing is disabled.
1857    pub fn sandboxing_available(&self, cx: &App) -> bool {
1858        sandboxing_available_for_project(self.project.read(cx), cx)
1859    }
1860
1861    /// The directory subtrees the sandbox always grants write access to for this
1862    /// thread's project (its worktree roots), derived from the same source the
1863    /// terminal tool uses when it actually builds the sandbox.
1864    pub fn sandbox_baseline_writable_paths(&self, cx: &App) -> Vec<PathBuf> {
1865        crate::sandboxing::sandbox_worktree_writable_paths(self.project.read(cx), cx)
1866    }
1867
1868    pub fn to_db(&self, cx: &App) -> Task<DbThread> {
1869        let initial_project_snapshot = self.initial_project_snapshot.clone();
1870        let mut thread = DbThread {
1871            title: self.title().unwrap_or_default(),
1872            messages: self.messages.clone(),
1873            updated_at: self.updated_at,
1874            detailed_summary: self.summary.clone(),
1875            initial_project_snapshot: None,
1876            cumulative_token_usage: self.cumulative_token_usage,
1877            request_token_usage: self.request_token_usage.clone(),
1878            model: (&self.model).into(),
1879            profile: Some(self.profile_id.clone()),
1880            subagent_context: self.subagent_context.clone(),
1881            speed: self.speed,
1882            thinking_enabled: self.thinking_enabled,
1883            thinking_effort: self.thinking_effort.clone(),
1884            draft_prompt: self.draft_prompt.clone(),
1885            ui_scroll_position: self.ui_scroll_position.map(|lo| {
1886                crate::db::SerializedScrollPosition {
1887                    item_ix: lo.item_ix,
1888                    offset_in_item: lo.offset_in_item.as_f32(),
1889                }
1890            }),
1891            sandboxed_terminal_temp_dir: self.sandboxed_terminal_temp_dir.clone(),
1892            sandbox_grants: self.sandbox_grants.borrow().to_db(),
1893        };
1894
1895        cx.background_spawn(async move {
1896            let initial_project_snapshot = initial_project_snapshot.await;
1897            thread.initial_project_snapshot = initial_project_snapshot;
1898            thread
1899        })
1900    }
1901
1902    /// Create a snapshot of the current project state including git information and unsaved buffers.
1903    fn project_snapshot(
1904        project: Entity<Project>,
1905        cx: &mut Context<Self>,
1906    ) -> Task<Arc<ProjectSnapshot>> {
1907        let task = project::telemetry_snapshot::TelemetrySnapshot::new(&project, cx);
1908        cx.spawn(async move |_, _| {
1909            let snapshot = task.await;
1910
1911            Arc::new(ProjectSnapshot {
1912                worktree_snapshots: snapshot.worktree_snapshots,
1913                timestamp: Utc::now(),
1914            })
1915        })
1916    }
1917
1918    pub fn project_context(&self) -> &Entity<ProjectContext> {
1919        &self.project_context
1920    }
1921
1922    pub fn project(&self) -> &Entity<Project> {
1923        &self.project
1924    }
1925
1926    pub fn action_log(&self) -> &Entity<ActionLog> {
1927        &self.action_log
1928    }
1929
1930    pub fn is_empty(&self) -> bool {
1931        self.messages.is_empty() && self.title.is_none()
1932    }
1933
1934    pub fn draft_prompt(&self) -> Option<&[acp::ContentBlock]> {
1935        self.draft_prompt.as_deref()
1936    }
1937
1938    pub fn set_draft_prompt(&mut self, prompt: Option<Vec<acp::ContentBlock>>) {
1939        self.draft_prompt = prompt;
1940    }
1941
1942    pub fn ui_scroll_position(&self) -> Option<gpui::ListOffset> {
1943        self.ui_scroll_position
1944    }
1945
1946    pub fn set_ui_scroll_position(&mut self, position: Option<gpui::ListOffset>) {
1947        self.ui_scroll_position = position;
1948    }
1949
1950    pub fn model(&self) -> Option<&Arc<dyn LanguageModel>> {
1951        self.model.as_model()
1952    }
1953
1954    pub fn thread_model(&self) -> &ThreadModel {
1955        &self.model
1956    }
1957
1958    pub(crate) fn ensure_model(
1959        &mut self,
1960        default_model: Option<&Arc<dyn LanguageModel>>,
1961        cx: &mut Context<Self>,
1962    ) {
1963        let resolved = match &self.model {
1964            ThreadModel::Ready(_) => return,
1965            ThreadModel::Unresolved(selection) => {
1966                LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
1967                    registry
1968                        .select_model(selection, cx)
1969                        .map(|configured| configured.model)
1970                })
1971            }
1972            ThreadModel::Unset => default_model.cloned(),
1973        };
1974
1975        if let Some(model) = resolved {
1976            self.set_model(model, cx);
1977        }
1978    }
1979
1980    pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
1981        let old_usage = self.latest_token_usage();
1982        self.model = ThreadModel::Ready(model.clone());
1983        let new_caps = Self::prompt_capabilities(self.model.as_model().map(|model| model.as_ref()));
1984        let new_usage = self.latest_token_usage();
1985        if old_usage != new_usage {
1986            cx.emit(TokenUsageUpdated(new_usage));
1987        }
1988        self.prompt_capabilities_tx.send(new_caps).log_err();
1989        cx.emit(ModelChanged);
1990
1991        for subagent in &self.running_subagents {
1992            subagent
1993                .update(cx, |thread, cx| {
1994                    if thread.inherits_parent_model_settings {
1995                        thread.set_model(model.clone(), cx);
1996                    }
1997                })
1998                .ok();
1999        }
2000
2001        cx.notify()
2002    }
2003
2004    pub fn summarization_model(&self) -> Option<&Arc<dyn LanguageModel>> {
2005        self.summarization_model.as_ref()
2006    }
2007
2008    pub fn set_summarization_model(
2009        &mut self,
2010        model: Option<Arc<dyn LanguageModel>>,
2011        cx: &mut Context<Self>,
2012    ) {
2013        self.summarization_model = model.clone();
2014
2015        for subagent in &self.running_subagents {
2016            subagent
2017                .update(cx, |thread, cx| {
2018                    thread.set_summarization_model(model.clone(), cx)
2019                })
2020                .ok();
2021        }
2022        cx.notify()
2023    }
2024
2025    pub fn thinking_enabled(&self) -> bool {
2026        self.thinking_enabled
2027    }
2028
2029    pub fn set_thinking_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
2030        self.thinking_enabled = enabled;
2031
2032        for subagent in &self.running_subagents {
2033            subagent
2034                .update(cx, |thread, cx| {
2035                    if thread.inherits_parent_model_settings {
2036                        thread.set_thinking_enabled(enabled, cx);
2037                    }
2038                })
2039                .ok();
2040        }
2041        cx.notify();
2042    }
2043
2044    pub fn thinking_effort(&self) -> Option<&String> {
2045        self.thinking_effort.as_ref()
2046    }
2047
2048    pub fn set_thinking_effort(&mut self, effort: Option<String>, cx: &mut Context<Self>) {
2049        self.thinking_effort = effort.clone();
2050
2051        for subagent in &self.running_subagents {
2052            subagent
2053                .update(cx, |thread, cx| {
2054                    if thread.inherits_parent_model_settings {
2055                        thread.set_thinking_effort(effort.clone(), cx)
2056                    }
2057                })
2058                .ok();
2059        }
2060        cx.notify();
2061    }
2062
2063    pub fn speed(&self) -> Option<Speed> {
2064        self.speed
2065    }
2066
2067    pub fn set_speed(&mut self, speed: Speed, cx: &mut Context<Self>) {
2068        self.speed = Some(speed);
2069
2070        for subagent in &self.running_subagents {
2071            subagent
2072                .update(cx, |thread, cx| {
2073                    if thread.inherits_parent_model_settings {
2074                        thread.set_speed(speed, cx);
2075                    }
2076                })
2077                .ok();
2078        }
2079        cx.notify();
2080    }
2081
2082    pub fn last_message(&self) -> Option<&Message> {
2083        self.messages.last().map(std::ops::Deref::deref)
2084    }
2085
2086    #[cfg(any(test, feature = "test-support"))]
2087    pub fn last_received_or_pending_message(&self) -> Option<Arc<Message>> {
2088        if let Some(message) = self.pending_message.clone() {
2089            Some(Arc::new(Message::Agent(message)))
2090        } else {
2091            self.messages.last().cloned()
2092        }
2093    }
2094
2095    pub fn add_default_tools(
2096        &mut self,
2097        environment: Rc<dyn ThreadEnvironment>,
2098        cx: &mut Context<Self>,
2099    ) {
2100        // Only update the agent location for the root thread, not for subagents.
2101        let update_agent_location = self.parent_thread_id().is_none();
2102
2103        let language_registry = self.project.read(cx).languages().clone();
2104        self.add_tool(CopyPathTool::new(self.project.clone()));
2105        self.add_tool(CreateDirectoryTool::new(self.project.clone()));
2106        self.add_tool(DeletePathTool::new(
2107            self.project.clone(),
2108            self.action_log.clone(),
2109        ));
2110        self.add_tool(EditFileTool::new(
2111            self.project.clone(),
2112            cx.weak_entity(),
2113            self.action_log.clone(),
2114            language_registry.clone(),
2115        ));
2116        self.add_tool(WriteFileTool::new(
2117            self.project.clone(),
2118            cx.weak_entity(),
2119            self.action_log.clone(),
2120            language_registry,
2121        ));
2122        self.add_tool(FetchTool::new(self.project.read(cx).client().http_client()));
2123        self.add_tool(FindPathTool::new(self.project.clone()));
2124        self.add_tool(GrepTool::new(self.project.clone()));
2125        self.add_tool(ListDirectoryTool::new(self.project.clone()));
2126        self.add_tool(MovePathTool::new(self.project.clone()));
2127        self.add_tool(ReadFileTool::new(
2128            self.project.clone(),
2129            self.action_log.clone(),
2130            update_agent_location,
2131        ));
2132        // Register terminal tool variants; `enabled_tools` exposes the one
2133        // matching the current sandbox state to the model as `terminal`.
2134        self.add_tool(TerminalTool::new(self.project.clone(), environment.clone()));
2135        self.add_tool(SandboxedTerminalTool::new(
2136            self.project.clone(),
2137            environment.clone(),
2138        ));
2139        self.add_tool(WebSearchTool);
2140
2141        self.add_tool(DiagnosticsTool::new(self.project.clone()));
2142
2143        let code_action_store: CodeActionStore = cx.new(|_cx| None);
2144        self.add_tool(FindReferencesTool::new(self.project.clone()));
2145        self.add_tool(GetCodeActionsTool::new(
2146            self.project.clone(),
2147            code_action_store.clone(),
2148        ));
2149        self.add_tool(ApplyCodeActionTool::new(
2150            self.project.clone(),
2151            code_action_store,
2152        ));
2153        self.add_tool(GoToDefinitionTool::new(self.project.clone()));
2154        self.add_tool(RenameTool::new(self.project.clone()));
2155
2156        if self.depth() < MAX_SUBAGENT_DEPTH {
2157            self.add_tool(SpawnAgentTool::new(environment.clone()));
2158        }
2159
2160        // Sibling-thread tools are exposed at every depth: a subagent should
2161        // still be able to kick off independent sibling work on behalf of the
2162        // user, even when it can no longer nest further subagents. Visibility
2163        // to the model is gated by `CreateThreadToolFeatureFlag` in
2164        // `Thread::enabled_tools`.
2165        self.add_tool(CreateThreadTool::new(environment.clone()));
2166        self.add_tool(ListAgentsAndModelsTool::new(environment));
2167    }
2168
2169    pub fn add_tool<T: AgentTool>(&mut self, tool: T) {
2170        debug_assert!(
2171            !self.tools.contains_key(T::NAME),
2172            "Duplicate tool name: {}",
2173            T::NAME,
2174        );
2175        self.tools.insert(T::NAME.into(), tool.erase());
2176    }
2177
2178    #[cfg(any(test, feature = "test-support"))]
2179    pub fn remove_tool(&mut self, name: &str) -> bool {
2180        self.tools.remove(name).is_some()
2181    }
2182
2183    pub fn profile(&self) -> &AgentProfileId {
2184        &self.profile_id
2185    }
2186
2187    /// Whether this thread's profile was downgraded to `minimal` at thread start
2188    /// because the workspace is restricted.
2189    pub fn profile_was_downgraded(&self) -> bool {
2190        self.profile_downgraded_for_restricted_workspace
2191    }
2192
2193    /// Computes the profile a thread should start with, given the user's chosen
2194    /// profile. In a restricted workspace, the built-in `write`/`ask` profiles
2195    /// are downgraded to `minimal` — but only when both the chosen profile and
2196    /// `minimal` are unmodified, shipped defaults, so we never override a user's
2197    /// custom or customized profiles.
2198    ///
2199    /// Returns the (possibly downgraded) profile and whether a downgrade
2200    /// happened.
2201    fn profile_for_restricted_workspace(
2202        profile_id: AgentProfileId,
2203        project: &Entity<Project>,
2204        cx: &App,
2205    ) -> (AgentProfileId, bool) {
2206        let is_write_or_ask = profile_id.as_str() == builtin_profiles::WRITE
2207            || profile_id.as_str() == builtin_profiles::ASK;
2208        let minimal = AgentProfileId(builtin_profiles::MINIMAL.into());
2209        if is_write_or_ask
2210            && TrustedWorktrees::has_restricted_worktrees(&project.read(cx).worktree_store(), cx)
2211            && AgentProfileSettings::is_unmodified_default(&profile_id, cx)
2212            && AgentProfileSettings::is_unmodified_default(&minimal, cx)
2213        {
2214            (minimal, true)
2215        } else {
2216            (profile_id, false)
2217        }
2218    }
2219
2220    pub fn set_profile(&mut self, profile_id: AgentProfileId, cx: &mut Context<Self>) {
2221        // An explicit selection means any earlier automatic downgrade no longer
2222        // applies, even if the user re-selects the same profile.
2223        self.profile_downgraded_for_restricted_workspace = false;
2224
2225        if self.profile_id == profile_id {
2226            return;
2227        }
2228
2229        self.profile_id = profile_id.clone();
2230
2231        // Swap to the profile's preferred model when available.
2232        if let Some(model) = Self::resolve_profile_model(&self.profile_id, cx) {
2233            self.set_model(model, cx);
2234        }
2235
2236        for subagent in &self.running_subagents {
2237            subagent
2238                .update(cx, |thread, cx| thread.set_profile(profile_id.clone(), cx))
2239                .ok();
2240        }
2241    }
2242
2243    pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<()> {
2244        for subagent in self.running_subagents.drain(..) {
2245            if let Some(subagent) = subagent.upgrade() {
2246                subagent.update(cx, |thread, cx| thread.cancel(cx)).detach();
2247            }
2248        }
2249
2250        let Some(running_turn) = self.running_turn.take() else {
2251            self.flush_pending_message(cx);
2252            return Task::ready(());
2253        };
2254
2255        let turn_task = running_turn.cancel();
2256
2257        cx.spawn(async move |this, cx| {
2258            turn_task.await;
2259            this.update(cx, |this, cx| {
2260                this.flush_pending_message(cx);
2261            })
2262            .ok();
2263        })
2264    }
2265
2266    pub fn set_end_turn_at_next_boundary(&mut self, end_at_boundary: bool) {
2267        self.end_turn_at_next_boundary = end_at_boundary;
2268    }
2269
2270    pub fn end_turn_at_next_boundary(&self) -> bool {
2271        self.end_turn_at_next_boundary
2272    }
2273
2274    fn accumulate_token_usage(&mut self, update: language_model::TokenUsage) {
2275        let previous_accounted_usage = self.current_request_token_usage;
2276        let current_accounted_usage = TokenUsage {
2277            input_tokens: previous_accounted_usage
2278                .input_tokens
2279                .max(update.input_tokens),
2280            output_tokens: previous_accounted_usage
2281                .output_tokens
2282                .max(update.output_tokens),
2283            cache_creation_input_tokens: previous_accounted_usage
2284                .cache_creation_input_tokens
2285                .max(update.cache_creation_input_tokens),
2286            cache_read_input_tokens: previous_accounted_usage
2287                .cache_read_input_tokens
2288                .max(update.cache_read_input_tokens),
2289        };
2290        self.current_request_token_usage = current_accounted_usage;
2291        self.cumulative_token_usage = self.cumulative_token_usage
2292            + TokenUsage {
2293                input_tokens: current_accounted_usage
2294                    .input_tokens
2295                    .saturating_sub(previous_accounted_usage.input_tokens),
2296                output_tokens: current_accounted_usage
2297                    .output_tokens
2298                    .saturating_sub(previous_accounted_usage.output_tokens),
2299                cache_creation_input_tokens: current_accounted_usage
2300                    .cache_creation_input_tokens
2301                    .saturating_sub(previous_accounted_usage.cache_creation_input_tokens),
2302                cache_read_input_tokens: current_accounted_usage
2303                    .cache_read_input_tokens
2304                    .saturating_sub(previous_accounted_usage.cache_read_input_tokens),
2305            };
2306    }
2307
2308    fn update_token_usage(&mut self, update: language_model::TokenUsage, cx: &mut Context<Self>) {
2309        self.accumulate_token_usage(update);
2310
2311        let Some(last_user_message) = self.last_user_message() else {
2312            return;
2313        };
2314
2315        self.request_token_usage
2316            .insert(last_user_message.id.clone(), update);
2317        cx.emit(TokenUsageUpdated(self.latest_token_usage()));
2318        cx.notify();
2319    }
2320
2321    /// Records that the last request overflowed the model's context window so
2322    /// the token usage indicator reports `Exceeded` instead of the stale usage
2323    /// from the last successful request. Providers don't report usage for
2324    /// failed requests, so we synthesize one from the reported overflow (when
2325    /// available) or the model's context size.
2326    fn mark_token_limit_exceeded(&mut self, tokens: Option<u64>, cx: &mut Context<Self>) {
2327        let Some(model) = self.model() else {
2328            return;
2329        };
2330        let input_tokens = tokens.unwrap_or(0).max(model.max_token_count());
2331        let Some(last_user_message) = self.last_user_message() else {
2332            return;
2333        };
2334
2335        self.request_token_usage.insert(
2336            last_user_message.id.clone(),
2337            language_model::TokenUsage {
2338                input_tokens,
2339                ..Default::default()
2340            },
2341        );
2342        cx.emit(TokenUsageUpdated(self.latest_token_usage()));
2343        cx.notify();
2344    }
2345
2346    pub fn truncate(
2347        &mut self,
2348        client_user_message_id: ClientUserMessageId,
2349        cx: &mut Context<Self>,
2350    ) -> Result<()> {
2351        self.cancel(cx).detach();
2352        // Clear pending message since cancel will try to flush it asynchronously,
2353        // and we don't want that content to be added after we truncate
2354        self.pending_message.take();
2355        let Some(position) = self.messages.iter().position(|msg| {
2356            matches!(&**msg, Message::User(UserMessage { id, .. }) if id == &client_user_message_id)
2357        }) else {
2358            return Err(anyhow!("Message not found"));
2359        };
2360
2361        for message in self.messages.drain(position..) {
2362            match &*message {
2363                Message::User(message) => {
2364                    self.request_token_usage.remove(&message.id);
2365                }
2366                Message::Agent(_) | Message::Resume | Message::Compaction(_) => {}
2367            }
2368        }
2369        self.clear_summary();
2370        cx.notify();
2371        Ok(())
2372    }
2373
2374    pub fn latest_request_token_usage(&self) -> Option<language_model::TokenUsage> {
2375        let last_user_message = self.last_user_message()?;
2376        let tokens = self.request_token_usage.get(&last_user_message.id)?;
2377        Some(*tokens)
2378    }
2379
2380    pub fn cumulative_token_usage(&self) -> language_model::TokenUsage {
2381        self.cumulative_token_usage
2382    }
2383
2384    pub fn latest_token_usage(&self) -> Option<acp_thread::TokenUsage> {
2385        let usage = self.latest_request_token_usage()?;
2386        let model = self.model()?;
2387        let input_tokens = total_input_tokens(usage);
2388
2389        Some(acp_thread::TokenUsage {
2390            max_tokens: model.max_token_count(),
2391            max_output_tokens: model.max_output_tokens(),
2392            used_tokens: usage.total_tokens(),
2393            input_tokens,
2394            output_tokens: usage.output_tokens,
2395        })
2396    }
2397
2398    /// Get the total input token count as of the message before the given message.
2399    ///
2400    /// Returns `None` if:
2401    /// - `target_id` is the first message (no previous message)
2402    /// - The previous message hasn't received a response yet (no usage data)
2403    /// - `target_id` is not found in the messages
2404    pub fn tokens_before_message(&self, target_id: &ClientUserMessageId) -> Option<u64> {
2405        let mut previous_user_message_id: Option<&ClientUserMessageId> = None;
2406
2407        for message in &self.messages {
2408            if let Message::User(user_msg) = &**message {
2409                if &user_msg.id == target_id {
2410                    let prev_id = previous_user_message_id?;
2411                    let usage = self.request_token_usage.get(prev_id)?;
2412                    return Some(total_input_tokens(*usage));
2413                }
2414                previous_user_message_id = Some(&user_msg.id);
2415            }
2416        }
2417        None
2418    }
2419
2420    /// Look up the active profile and resolve its preferred model if one is configured.
2421    fn resolve_profile_model(
2422        profile_id: &AgentProfileId,
2423        cx: &mut Context<Self>,
2424    ) -> Option<Arc<dyn LanguageModel>> {
2425        let selection = AgentSettings::get_global(cx)
2426            .profiles
2427            .get(profile_id)?
2428            .default_model
2429            .clone()?;
2430        Self::resolve_model_from_selection(&selection, cx)
2431    }
2432
2433    fn user_configured_model_selection(cx: &App) -> Option<SelectedModel> {
2434        let selection = SettingsStore::global(cx)
2435            .raw_user_settings()?
2436            .content
2437            .agent
2438            .as_ref()?
2439            .default_model
2440            .as_ref()?;
2441        Some(SelectedModel {
2442            provider: LanguageModelProviderId::from(selection.provider.0.clone()),
2443            model: LanguageModelId::from(selection.model.clone()),
2444        })
2445    }
2446
2447    /// Translate a stored model selection into the configured model from the registry.
2448    fn resolve_model_from_selection(
2449        selection: &LanguageModelSelection,
2450        cx: &mut Context<Self>,
2451    ) -> Option<Arc<dyn LanguageModel>> {
2452        let selected = SelectedModel {
2453            provider: LanguageModelProviderId::from(selection.provider.0.clone()),
2454            model: LanguageModelId::from(selection.model.clone()),
2455        };
2456        LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
2457            registry
2458                .select_model(&selected, cx)
2459                .map(|configured| configured.model)
2460        })
2461    }
2462
2463    pub fn resume(
2464        &mut self,
2465        cx: &mut Context<Self>,
2466    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
2467        self.messages.push(Arc::new(Message::Resume));
2468        cx.notify();
2469
2470        log::debug!("Total messages in thread: {}", self.messages.len());
2471        self.run_turn(cx)
2472    }
2473
2474    /// Sending a message results in the model streaming a response, which could include tool calls.
2475    /// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent.
2476    /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
2477    pub fn send<T>(
2478        &mut self,
2479        id: ClientUserMessageId,
2480        content: impl IntoIterator<Item = T>,
2481        cx: &mut Context<Self>,
2482    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>
2483    where
2484        T: Into<UserMessageContent>,
2485    {
2486        let content = content.into_iter().map(Into::into).collect::<Arc<_>>();
2487        log::debug!("Thread::send content: {:?}", content);
2488
2489        self.messages
2490            .push(Arc::new(Message::User(UserMessage { id, content })));
2491        cx.notify();
2492
2493        self.send_existing(cx)
2494    }
2495
2496    pub fn send_existing(
2497        &mut self,
2498        cx: &mut Context<Self>,
2499    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
2500        let model = self
2501            .model()
2502            .ok_or_else(|| anyhow!(NoModelConfiguredError))?;
2503
2504        log::info!("Thread::send called with model: {}", model.name().0);
2505        self.advance_prompt_id();
2506
2507        log::debug!("Total messages in thread: {}", self.messages.len());
2508        self.run_turn(cx)
2509    }
2510
2511    /// Force a manual context compaction using the summary strategy,
2512    /// regardless of the current token usage or context window size.
2513    pub fn compact(
2514        &mut self,
2515        id: ClientUserMessageId,
2516        cx: &mut Context<Self>,
2517    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
2518        let model = self
2519            .compaction_model(cx)
2520            .ok_or_else(|| anyhow!(NoModelConfiguredError))?;
2521
2522        // Flush any pending message and cancel an in-flight turn before we
2523        // start, mirroring `run_turn` so a stray completion can't race with the
2524        // compaction we're about to perform.
2525        self.flush_pending_message(cx);
2526        self.cancel(cx).detach();
2527
2528        let compaction = self.forced_compaction_target_ix().map(|request_end_ix| {
2529            self.advance_prompt_id();
2530            let request = self.build_compaction_request(request_end_ix, &model, cx);
2531            self.current_request_token_usage = TokenUsage::default();
2532            (model.clone(), request)
2533        });
2534
2535        if compaction.is_some() {
2536            self.pending_compaction_telemetry =
2537                self.build_compaction_telemetry("manual", &model, cx);
2538        }
2539
2540        self.clear_summary();
2541        cx.notify();
2542
2543        let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>();
2544        let event_stream = ThreadEventStream(events_tx);
2545        let (cancellation_tx, mut cancellation_rx) = watch::channel(false);
2546        let task = cx.spawn({
2547            let event_stream = event_stream.clone();
2548            async move |this, cx| {
2549                let result = if let Some((model, request)) = compaction {
2550                    Self::stream_compaction(
2551                        &this,
2552                        &event_stream,
2553                        cancellation_rx.clone(),
2554                        model,
2555                        request,
2556                        CompactionInsertion::Manual { marker_id: id },
2557                        cx,
2558                    )
2559                    .await
2560                } else {
2561                    Ok(ControlFlow::Continue(()))
2562                };
2563
2564                // If we were cancelled, `cancel()` already took `running_turn`
2565                // (possibly for a new turn), so leave it alone.
2566                if *cancellation_rx.borrow() {
2567                    this.update(cx, |this, _| {
2568                        this.emit_compaction_telemetry_outcome("canceled", None)
2569                    })
2570                    .log_err();
2571                    return;
2572                }
2573
2574                match result {
2575                    // On success, the telemetry event is deferred until the next
2576                    // completion reports usage (see `handle_completion_event`),
2577                    // so we leave `pending_compaction_telemetry` in place here.
2578                    Ok(_) => event_stream.send_stop(acp::StopReason::EndTurn),
2579                    Err(error) => {
2580                        log::error!("Manual compaction failed: {:?}", error);
2581                        this.update(cx, |this, _| {
2582                            this.emit_compaction_telemetry_outcome(
2583                                "failed",
2584                                Some(error.to_string()),
2585                            )
2586                        })
2587                        .log_err();
2588                        event_stream.send_error(error);
2589                    }
2590                }
2591
2592                _ = this.update(cx, |this, _| this.running_turn.take());
2593            }
2594        });
2595        self.running_turn = Some(RunningTurn::new(
2596            event_stream,
2597            BTreeMap::default(),
2598            cancellation_tx,
2599            task,
2600        ));
2601
2602        Ok(events_rx)
2603    }
2604
2605    pub fn push_acp_user_block(
2606        &mut self,
2607        id: ClientUserMessageId,
2608        blocks: impl IntoIterator<Item = acp::ContentBlock>,
2609        path_style: PathStyle,
2610        cx: &mut Context<Self>,
2611    ) {
2612        let content = blocks
2613            .into_iter()
2614            .map(|block| UserMessageContent::from_content_block(block, path_style))
2615            .collect::<Arc<_>>();
2616        self.messages
2617            .push(Arc::new(Message::User(UserMessage { id, content })));
2618        cx.notify();
2619    }
2620
2621    pub fn push_acp_agent_block(&mut self, block: acp::ContentBlock, cx: &mut Context<Self>) {
2622        let text = match block {
2623            acp::ContentBlock::Text(text_content) => text_content.text,
2624            acp::ContentBlock::Image(_) => "[image]".to_string(),
2625            acp::ContentBlock::Audio(_) => "[audio]".to_string(),
2626            acp::ContentBlock::ResourceLink(resource_link) => resource_link.uri,
2627            acp::ContentBlock::Resource(resource) => match resource.resource {
2628                acp::EmbeddedResourceResource::TextResourceContents(resource) => resource.uri,
2629                acp::EmbeddedResourceResource::BlobResourceContents(resource) => resource.uri,
2630                _ => "[resource]".to_string(),
2631            },
2632            _ => "[unknown]".to_string(),
2633        };
2634
2635        self.messages.push(Arc::new(Message::Agent(AgentMessage {
2636            content: vec![AgentMessageContent::Text(text)],
2637            ..Default::default()
2638        })));
2639        cx.notify();
2640    }
2641
2642    fn run_turn(
2643        &mut self,
2644        cx: &mut Context<Self>,
2645    ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
2646        // Flush the old pending message synchronously before cancelling,
2647        // to avoid a race where the detached cancel task might flush the NEW
2648        // turn's pending message instead of the old one.
2649        self.flush_pending_message(cx);
2650        self.cancel(cx).detach();
2651
2652        let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>();
2653        let event_stream = ThreadEventStream(events_tx);
2654        let message_ix = self.messages.len().saturating_sub(1);
2655        self.clear_summary();
2656        let tools = self.enabled_tools(cx);
2657        let (cancellation_tx, mut cancellation_rx) = watch::channel(false);
2658        let task = cx.spawn({
2659            let event_stream = event_stream.clone();
2660            async move |this, cx| {
2661                log::debug!("Starting agent turn execution");
2662
2663                let turn_result =
2664                    Self::run_turn_internal(&this, &event_stream, cancellation_rx.clone(), cx)
2665                        .await;
2666
2667                // Check if we were cancelled - if so, cancel() already took running_turn
2668                // and we shouldn't touch it (it might be a NEW turn now)
2669                let was_cancelled = *cancellation_rx.borrow();
2670                if was_cancelled {
2671                    log::debug!("Turn was cancelled, skipping cleanup");
2672                    return;
2673                }
2674
2675                _ = this.update(cx, |this, cx| this.flush_pending_message(cx));
2676
2677                match turn_result {
2678                    Ok(()) => {
2679                        log::debug!("Turn execution completed");
2680                        event_stream.send_stop(acp::StopReason::EndTurn);
2681                    }
2682                    Err(error) => {
2683                        log::error!("Turn execution failed: {:?}", error);
2684                        match error.downcast::<CompletionError>() {
2685                            Ok(CompletionError::Refusal) => {
2686                                event_stream.send_stop(acp::StopReason::Refusal);
2687                                _ = this.update(cx, |this, _| this.messages.truncate(message_ix));
2688                            }
2689                            Ok(CompletionError::MaxTokens) => {
2690                                event_stream.send_stop(acp::StopReason::MaxTokens);
2691                            }
2692                            Ok(CompletionError::Other(error)) | Err(error) => {
2693                                event_stream.send_error(error);
2694                            }
2695                        }
2696                    }
2697                }
2698
2699                _ = this.update(cx, |this, _| this.running_turn.take());
2700            }
2701        });
2702        self.running_turn = Some(RunningTurn::new(event_stream, tools, cancellation_tx, task));
2703        Ok(events_rx)
2704    }
2705
2706    async fn run_turn_internal(
2707        this: &WeakEntity<Self>,
2708        event_stream: &ThreadEventStream,
2709        mut cancellation_rx: watch::Receiver<bool>,
2710        cx: &mut AsyncApp,
2711    ) -> Result<()> {
2712        let mut attempt = 0;
2713        let mut intent = CompletionIntent::UserPrompt;
2714        // Set when a refusal fallback occurs so subsequent iterations use the fallback model.
2715        let mut refusal_fallback_model: Option<Arc<dyn LanguageModel>> = None;
2716        loop {
2717            match Self::perform_compaction_if_needed(
2718                this,
2719                event_stream,
2720                cancellation_rx.clone(),
2721                cx,
2722            )
2723            .await
2724            {
2725                // On success the telemetry event is deferred until the
2726                // completion below reports usage, so we can record an
2727                // accurate post-compaction context size (see
2728                // `handle_completion_event`).
2729                Ok(ControlFlow::Continue(())) => {}
2730                Ok(ControlFlow::Break(())) => {
2731                    this.update(cx, |this, _| {
2732                        this.emit_compaction_telemetry_outcome("canceled", None)
2733                    })?;
2734                    return Ok(());
2735                }
2736                Err(error) => {
2737                    log::error!("Compaction failed: {}", error);
2738                    let error_message = error.to_string();
2739                    match error.downcast::<LanguageModelCompletionError>() {
2740                        Ok(error) => {
2741                            attempt += 1;
2742                            match Self::retry_completion_error(
2743                                this,
2744                                event_stream,
2745                                &mut cancellation_rx,
2746                                error,
2747                                attempt,
2748                                cx,
2749                            )
2750                            .await
2751                            {
2752                                Ok(ControlFlow::Break(())) => {
2753                                    this.update(cx, |this, _| {
2754                                        this.emit_compaction_telemetry_outcome("canceled", None)
2755                                    })?;
2756                                    return Ok(());
2757                                }
2758                                Ok(ControlFlow::Continue(())) => {
2759                                    this.update(cx, |this, _| {
2760                                        if let Some(telemetry) =
2761                                            this.pending_compaction_telemetry.as_mut()
2762                                        {
2763                                            telemetry.retries += 1;
2764                                        }
2765                                    })?;
2766                                    continue;
2767                                }
2768                                Err(retry_error) => {
2769                                    this.update(cx, |this, _| {
2770                                        this.emit_compaction_telemetry_outcome(
2771                                            "failed",
2772                                            Some(error_message),
2773                                        )
2774                                    })?;
2775                                    return Err(retry_error);
2776                                }
2777                            }
2778                        }
2779                        Err(error) => {
2780                            this.update(cx, |this, _| {
2781                                this.emit_compaction_telemetry_outcome(
2782                                    "failed",
2783                                    Some(error_message),
2784                                )
2785                            })?;
2786                            return Err(error);
2787                        }
2788                    }
2789                }
2790            }
2791
2792            // Re-read the model and refresh tools on each iteration so that
2793            // mid-turn changes (e.g. the user switches model, toggles tools,
2794            // or changes profile) take effect between tool-call rounds.
2795            // If a refusal fallback is active, use that model instead.
2796            let (model, request) = this.update(cx, |this, cx| {
2797                let model = refusal_fallback_model
2798                    .clone()
2799                    .or_else(|| this.model().cloned())
2800                    .ok_or_else(|| anyhow!(NoModelConfiguredError))?;
2801                this.refresh_turn_tools(cx);
2802                let request = this.build_completion_request(intent, cx)?;
2803                this.current_request_token_usage = TokenUsage::default();
2804                anyhow::Ok((model, request))
2805            })??;
2806
2807            telemetry::event!(
2808                "Agent Thread Completion",
2809                thread_id = this.read_with(cx, |this, _| this.id.to_string())?,
2810                parent_thread_id = this.read_with(cx, |this, _| this
2811                    .parent_thread_id()
2812                    .map(|id| id.to_string()))?,
2813                prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?,
2814                model = model.telemetry_id(),
2815                model_provider = model.provider_id().to_string(),
2816                attempt
2817            );
2818
2819            log::debug!("Calling model.stream_completion, attempt {}", attempt);
2820
2821            let (mut events, mut error) = match model.stream_completion(request, cx).await {
2822                Ok(events) => (events.fuse(), None),
2823                Err(err) => (stream::empty().boxed().fuse(), Some(err)),
2824            };
2825            let mut tool_results: FuturesUnordered<Task<(usize, LanguageModelToolResult)>> =
2826                FuturesUnordered::new();
2827            let mut early_tool_results: Vec<(usize, LanguageModelToolResult)> = Vec::new();
2828            let mut cancelled = false;
2829            let mut had_refusal = false;
2830            loop {
2831                // Race between getting the first event, tool completion, and cancellation.
2832                let first_event = futures::select! {
2833                    event = events.next().fuse() => event,
2834                    tool_result = futures::StreamExt::select_next_some(&mut tool_results) => {
2835                        let (owning_message_ix, tool_result) = tool_result;
2836                        let is_error = tool_result.is_error;
2837                        let is_still_streaming = this
2838                            .read_with(cx, |this, _cx| {
2839                                this.running_turn
2840                                    .as_ref()
2841                                    .and_then(|turn| turn.streaming_tool_inputs.get(&tool_result.tool_use_id))
2842                                    .map_or(false, |inputs| !inputs.has_received_final())
2843                            })
2844                            .unwrap_or(false);
2845
2846                        early_tool_results.push((owning_message_ix, tool_result));
2847
2848                        // Only break if the tool errored and we are still
2849                        // streaming the input of the tool. If the tool errored
2850                        // but we are no longer streaming its input (i.e. there
2851                        // are parallel tool calls) we want to continue
2852                        // processing those tool inputs.
2853                        if is_error && is_still_streaming {
2854                            break;
2855                        }
2856                        continue;
2857                    }
2858                    _ = cancellation_rx.changed().fuse() => {
2859                        if *cancellation_rx.borrow() {
2860                            cancelled = true;
2861                            break;
2862                        }
2863                        continue;
2864                    }
2865                };
2866                let Some(first_event) = first_event else {
2867                    break;
2868                };
2869
2870                // Collect all immediately available events to process as a batch
2871                let mut batch = vec![first_event];
2872                while let Some(event) = events.next().now_or_never().flatten() {
2873                    batch.push(event);
2874                }
2875
2876                // Process the batch in a single update
2877                let batch_result = this.update(cx, |this, cx| {
2878                    let mut batch_tool_results = Vec::new();
2879                    let mut batch_error = None;
2880
2881                    for event in batch {
2882                        log::trace!("Received completion event: {:?}", event);
2883                        match event {
2884                            Ok(event) => {
2885                                match this.handle_completion_event(
2886                                    event,
2887                                    event_stream,
2888                                    cancellation_rx.clone(),
2889                                    cx,
2890                                ) {
2891                                    Ok(Some(task)) => batch_tool_results.push(task),
2892                                    Ok(None) => {}
2893                                    Err(err) => {
2894                                        batch_error = Some(err);
2895                                        break;
2896                                    }
2897                                }
2898                            }
2899                            Err(err) => {
2900                                batch_error = Some(err.into());
2901                                break;
2902                            }
2903                        }
2904                    }
2905
2906                    cx.notify();
2907                    (batch_tool_results, batch_error)
2908                })?;
2909
2910                tool_results.extend(batch_result.0);
2911                if let Some(err) = batch_result.1 {
2912                    let is_refusal = err
2913                        .downcast_ref::<CompletionError>()
2914                        .is_some_and(|e| matches!(e, CompletionError::Refusal));
2915                    if is_refusal {
2916                        log::info!("Model refused request; checking for fallback model");
2917                        had_refusal = true;
2918                        break;
2919                    }
2920                    error = Some(err.downcast()?);
2921                    break;
2922                }
2923            }
2924
2925            // Drop the stream to release the rate limit permit before tool execution.
2926            // The stream holds a semaphore guard that limits concurrent requests.
2927            // Without this, the permit would be held during potentially long-running
2928            // tool execution, which could cause deadlocks when tools spawn subagents
2929            // that need their own permits.
2930            drop(events);
2931
2932            // Drop streaming tool input senders that never received their final input.
2933            // This prevents deadlock when the LLM stream ends (e.g. because of an error)
2934            // before sending a tool use with `is_input_complete: true`.
2935            this.update(cx, |this, _cx| {
2936                if let Some(running_turn) = this.running_turn.as_mut() {
2937                    if running_turn.streaming_tool_inputs.is_empty() {
2938                        return;
2939                    }
2940                    log::warn!("Dropping partial tool inputs because the stream ended");
2941                    running_turn.streaming_tool_inputs.drain();
2942                }
2943            })?;
2944
2945            if had_refusal {
2946                let maybe_fallback = this.update(cx, |this, cx| -> Option<Arc<dyn LanguageModel>> {
2947                    let current_model = refusal_fallback_model.as_ref().or(this.model())?;
2948                    let fallback_id = match current_model.refusal_fallback_model_id() {
2949                        Some(id) => id,
2950                        None => {
2951                            log::info!(
2952                                "Refusal fallback: no fallback configured for model {} (provider {})",
2953                                current_model.id().0,
2954                                current_model.provider_id()
2955                            );
2956                            return None;
2957                        }
2958                    };
2959                    let provider_id = current_model.provider_id();
2960                    let found = LanguageModelRegistry::global(cx)
2961                        .read(cx)
2962                        .available_models(cx)
2963                        .find(|m| {
2964                            m.provider_id() == provider_id && m.id().0.as_ref() == fallback_id
2965                        });
2966                    if found.is_none() {
2967                        log::info!(
2968                            "Refusal fallback: fallback model {}/{} not found in available models",
2969                            provider_id,
2970                            fallback_id
2971                        );
2972                    }
2973                    found
2974                })?;
2975
2976                if let Some(fallback) = maybe_fallback {
2977                    log::info!("Refusal fallback: retrying with {}", fallback.id().0);
2978                    let fallback_name = fallback.name().0.clone();
2979                    this.update(cx, |this, cx| {
2980                        this.pending_message = None;
2981                        this.set_model(fallback.clone(), cx);
2982                    })?;
2983                    event_stream.send_retry(acp_thread::RetryStatus {
2984                        last_error: "Safety filter triggered".into(),
2985                        attempt: 1,
2986                        max_attempts: 1,
2987                        started_at: Instant::now(),
2988                        duration: Duration::MAX,
2989                        meta: Some(acp_thread::meta_with_refusal_fallback(&fallback_name)),
2990                    });
2991                    refusal_fallback_model = Some(fallback);
2992                    continue;
2993                }
2994                log::info!("Request refused with no fallback model available");
2995                return Err(CompletionError::Refusal.into());
2996            }
2997
2998            let end_turn = tool_results.is_empty() && early_tool_results.is_empty();
2999
3000            for (owning_message_ix, tool_result) in early_tool_results {
3001                Self::process_tool_result(this, event_stream, cx, owning_message_ix, tool_result)?;
3002            }
3003            while let Some((owning_message_ix, tool_result)) = tool_results.next().await {
3004                Self::process_tool_result(this, event_stream, cx, owning_message_ix, tool_result)?;
3005            }
3006
3007            this.update(cx, |this, cx| {
3008                this.flush_pending_message(cx);
3009                if this.title.is_none() {
3010                    this.generate_title(cx);
3011                }
3012            })?;
3013
3014            if cancelled {
3015                log::debug!("Turn cancelled by user, exiting");
3016                return Ok(());
3017            }
3018
3019            if let Some(error) = error {
3020                attempt += 1;
3021                match Self::retry_completion_error(
3022                    this,
3023                    event_stream,
3024                    &mut cancellation_rx,
3025                    error,
3026                    attempt,
3027                    cx,
3028                )
3029                .await?
3030                {
3031                    ControlFlow::Break(_) => return Ok(()),
3032                    ControlFlow::Continue(_) => {}
3033                }
3034                this.update(cx, |this, _cx| {
3035                    if let Some(Message::Agent(message)) = this.last_message() {
3036                        if message.tool_results.is_empty() {
3037                            intent = CompletionIntent::UserPrompt;
3038                            this.messages.push(Arc::new(Message::Resume));
3039                        }
3040                    }
3041                })?;
3042            } else if end_turn {
3043                return Ok(());
3044            } else {
3045                let end_at_boundary =
3046                    this.update(cx, |this, _| this.end_turn_at_next_boundary())?;
3047                if end_at_boundary {
3048                    log::debug!("Steering message queued, ending turn at message boundary");
3049                    return Ok(());
3050                }
3051                intent = CompletionIntent::ToolResults;
3052                attempt = 0;
3053            }
3054        }
3055    }
3056
3057    /// Computes the retry status for a failed completion, notifies listeners,
3058    /// and waits out the backoff delay (or returns early if the turn is
3059    /// cancelled while waiting). Returns an error if the completion is not
3060    /// retryable or retries are exhausted.
3061    async fn retry_completion_error(
3062        this: &WeakEntity<Self>,
3063        event_stream: &ThreadEventStream,
3064        cancellation_rx: &mut watch::Receiver<bool>,
3065        error: LanguageModelCompletionError,
3066        attempt: u8,
3067        cx: &mut AsyncApp,
3068    ) -> Result<ControlFlow<()>> {
3069        let retry = this.update(cx, |this, cx| {
3070            let user_store = this.user_store.read(cx);
3071            this.handle_completion_error(error, attempt, user_store.plan(), cx)
3072        })??;
3073        let timer = cx.background_executor().timer(retry.duration);
3074        event_stream.send_retry(retry);
3075        futures::select! {
3076            _ = timer.fuse() => {}
3077            _ = cancellation_rx.changed().fuse() => {
3078                if *cancellation_rx.borrow() {
3079                    log::debug!("Turn cancelled during retry delay, exiting");
3080                    return Ok(ControlFlow::Break(()));
3081                }
3082            }
3083        }
3084        Ok(ControlFlow::Continue(()))
3085    }
3086
3087    async fn perform_compaction_if_needed(
3088        this: &WeakEntity<Self>,
3089        event_stream: &ThreadEventStream,
3090        cancellation_rx: watch::Receiver<bool>,
3091        cx: &mut AsyncApp,
3092    ) -> Result<ControlFlow<()>> {
3093        let Some((model, request, insertion_ix)) = this.update(cx, |this, cx| {
3094            let insertion_ix = this.compaction_message_target_ix(cx)?;
3095            let model = this.compaction_model(cx)?;
3096            let request = this.build_compaction_request(insertion_ix, &model, cx);
3097            this.current_request_token_usage = TokenUsage::default();
3098            // Preserve telemetry across retries so the retry count keeps
3099            // accumulating rather than resetting on each attempt.
3100            if this.pending_compaction_telemetry.is_none() {
3101                this.pending_compaction_telemetry =
3102                    this.build_compaction_telemetry("auto", &model, cx);
3103            }
3104            Some((model, request, insertion_ix))
3105        })?
3106        else {
3107            return Ok(ControlFlow::Continue(()));
3108        };
3109
3110        Self::stream_compaction(
3111            this,
3112            event_stream,
3113            cancellation_rx,
3114            model,
3115            request,
3116            CompactionInsertion::Auto { insertion_ix },
3117            cx,
3118        )
3119        .await
3120    }
3121
3122    async fn stream_compaction(
3123        this: &WeakEntity<Self>,
3124        event_stream: &ThreadEventStream,
3125        mut cancellation_rx: watch::Receiver<bool>,
3126        model: Arc<dyn LanguageModel>,
3127        request: LanguageModelRequest,
3128        insertion: CompactionInsertion,
3129        cx: &mut AsyncApp,
3130    ) -> Result<ControlFlow<()>> {
3131        log::debug!("Running compaction");
3132        let compaction_id = acp_thread::ContextCompactionId(Uuid::new_v4().to_string().into());
3133        event_stream.send_context_compaction(
3134            compaction_id.clone(),
3135            acp_thread::ContextCompactionStatus::InProgress,
3136        );
3137        let stream = futures::select! {
3138            result = model.stream_completion(request, cx).fuse() => result,
3139            _ = cancellation_rx.changed().fuse() => {
3140                if *cancellation_rx.borrow() {
3141                    log::debug!("Compaction cancelled before request started");
3142                    return Ok(ControlFlow::Break(()));
3143                }
3144                return Ok(ControlFlow::Continue(()));
3145            }
3146        };
3147        let mut stream = stream?;
3148
3149        let mut summary = String::new();
3150        loop {
3151            let event = futures::select! {
3152                event = stream.next().fuse() => event,
3153                _ = cancellation_rx.changed().fuse() => {
3154                    if *cancellation_rx.borrow() {
3155                        log::debug!("Compaction cancelled while summarizing");
3156                        return Ok(ControlFlow::Break(()));
3157                    }
3158                    continue;
3159                }
3160            };
3161
3162            let Some(event) = event else {
3163                break;
3164            };
3165
3166            match event? {
3167                LanguageModelCompletionEvent::Text(text) => {
3168                    summary.push_str(&text);
3169                    event_stream.send_context_compaction_update(compaction_id.clone(), &text);
3170                }
3171                LanguageModelCompletionEvent::UsageUpdate(usage) => {
3172                    this.update(cx, |this, _cx| {
3173                        this.accumulate_token_usage(usage);
3174                    })?;
3175                }
3176                LanguageModelCompletionEvent::Stop(_)
3177                | LanguageModelCompletionEvent::Started
3178                | LanguageModelCompletionEvent::Queued { .. }
3179                | LanguageModelCompletionEvent::Thinking { .. }
3180                | LanguageModelCompletionEvent::RedactedThinking { .. }
3181                | LanguageModelCompletionEvent::ReasoningDetails(_)
3182                | LanguageModelCompletionEvent::ToolUse(_)
3183                | LanguageModelCompletionEvent::ToolUseJsonParseError { .. }
3184                | LanguageModelCompletionEvent::StartMessage { .. }
3185                | LanguageModelCompletionEvent::Compaction(_) => {}
3186            }
3187        }
3188
3189        if *cancellation_rx.borrow() {
3190            log::debug!("Compaction cancelled after summarizing");
3191            return Ok(ControlFlow::Break(()));
3192        }
3193
3194        let summary = summary.trim().to_string();
3195        if summary.is_empty() {
3196            log::warn!("Compaction produced an empty summary");
3197            return Err(anyhow::anyhow!("Compaction produced an empty summary"));
3198        }
3199
3200        log::debug!("Compaction succeeded:\n{summary}");
3201        event_stream.update_context_compaction_status(
3202            compaction_id,
3203            acp_thread::ContextCompactionStatus::Completed,
3204        );
3205
3206        this.update(cx, |this, cx| {
3207            let compaction = Arc::new(Message::Compaction(CompactionInfo::Summary(summary.into())));
3208            match insertion {
3209                CompactionInsertion::Auto { insertion_ix } => {
3210                    if insertion_ix <= this.messages.len() {
3211                        this.messages.insert(insertion_ix, compaction);
3212                    } else {
3213                        this.messages.push(compaction);
3214                    }
3215                }
3216                CompactionInsertion::Manual { marker_id } => {
3217                    this.messages.push(Arc::new(Message::User(UserMessage {
3218                        id: marker_id,
3219                        content: Arc::from([]),
3220                    })));
3221                    this.messages.push(compaction);
3222                }
3223            }
3224            cx.notify();
3225        })?;
3226
3227        Ok(ControlFlow::Continue(()))
3228    }
3229
3230    fn process_tool_result(
3231        this: &WeakEntity<Thread>,
3232        event_stream: &ThreadEventStream,
3233        cx: &mut AsyncApp,
3234        owning_message_ix: usize,
3235        tool_result: LanguageModelToolResult,
3236    ) -> Result<(), anyhow::Error> {
3237        log::debug!("Tool finished {:?}", tool_result);
3238
3239        event_stream.update_tool_call_fields(
3240            &scoped_tool_call_id(owning_message_ix, &tool_result.tool_use_id),
3241            acp::ToolCallUpdateFields::new()
3242                .status(if tool_result.is_error {
3243                    acp::ToolCallStatus::Failed
3244                } else {
3245                    acp::ToolCallStatus::Completed
3246                })
3247                .raw_output(tool_result.output.clone()),
3248            None,
3249        );
3250        this.update(cx, |this, _cx| {
3251            this.pending_message()
3252                .tool_results
3253                .insert(tool_result.tool_use_id.clone(), tool_result)
3254        })?;
3255        Ok(())
3256    }
3257
3258    fn handle_completion_error(
3259        &mut self,
3260        error: LanguageModelCompletionError,
3261        attempt: u8,
3262        plan: Option<Plan>,
3263        cx: &mut Context<Self>,
3264    ) -> Result<acp_thread::RetryStatus> {
3265        if let LanguageModelCompletionError::PromptTooLarge { tokens } = &error {
3266            self.mark_token_limit_exceeded(*tokens, cx);
3267        }
3268
3269        let Some(model) = self.model() else {
3270            return Err(anyhow!(error));
3271        };
3272
3273        let auto_retry = if model.provider_id() == ZED_CLOUD_PROVIDER_ID {
3274            plan.is_some()
3275        } else {
3276            true
3277        };
3278
3279        if !auto_retry {
3280            return Err(anyhow!(error));
3281        }
3282
3283        let Some(strategy) = Self::retry_strategy_for(&error) else {
3284            return Err(anyhow!(error));
3285        };
3286
3287        let max_attempts = match &strategy {
3288            RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
3289            RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
3290        };
3291
3292        if attempt > max_attempts {
3293            return Err(anyhow!(error));
3294        }
3295
3296        let delay = match &strategy {
3297            RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
3298                let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
3299                Duration::from_secs(delay_secs)
3300            }
3301            RetryStrategy::Fixed { delay, .. } => *delay,
3302        };
3303        log::debug!("Retry attempt {attempt} with delay {delay:?}");
3304
3305        Ok(acp_thread::RetryStatus {
3306            last_error: error.to_string().into(),
3307            attempt: attempt as usize,
3308            max_attempts: max_attempts as usize,
3309            started_at: Instant::now(),
3310            duration: delay,
3311            meta: None,
3312        })
3313    }
3314
3315    /// A helper method that's called on every streamed completion event.
3316    /// Returns an optional tool result task, which the main agentic loop will
3317    /// send back to the model when it resolves.
3318    fn handle_completion_event(
3319        &mut self,
3320        event: LanguageModelCompletionEvent,
3321        event_stream: &ThreadEventStream,
3322        cancellation_rx: watch::Receiver<bool>,
3323        cx: &mut Context<Self>,
3324    ) -> Result<Option<Task<(usize, LanguageModelToolResult)>>> {
3325        log::trace!("Handling streamed completion event: {:?}", event);
3326        use LanguageModelCompletionEvent::*;
3327
3328        match event {
3329            StartMessage { .. } => {
3330                self.flush_pending_message(cx);
3331                self.pending_message = Some(AgentMessage::default());
3332            }
3333            Text(new_text) => self.handle_text_event(new_text, event_stream),
3334            Thinking { text, signature } => {
3335                self.handle_thinking_event(text, signature, event_stream)
3336            }
3337            RedactedThinking { data } => self.handle_redacted_thinking_event(data),
3338            ReasoningDetails(details) => {
3339                let last_message = self.pending_message();
3340                // Store the last non-empty reasoning_details (overwrites earlier ones)
3341                // This ensures we keep the encrypted reasoning with signatures, not the early text reasoning
3342                if let serde_json::Value::Array(arr) = &details {
3343                    if !arr.is_empty() {
3344                        last_message.reasoning_details = Some(Arc::new(details));
3345                    }
3346                } else {
3347                    last_message.reasoning_details = Some(Arc::new(details));
3348                }
3349            }
3350            ToolUse(tool_use) => {
3351                return Ok(self.handle_tool_use_event(tool_use, event_stream, cancellation_rx, cx));
3352            }
3353            ToolUseJsonParseError {
3354                id,
3355                tool_name,
3356                raw_input,
3357                json_parse_error,
3358            } => {
3359                return Ok(self.handle_tool_use_json_parse_error_event(
3360                    id,
3361                    tool_name,
3362                    raw_input,
3363                    json_parse_error,
3364                    event_stream,
3365                    cancellation_rx,
3366                    cx,
3367                ));
3368            }
3369            UsageUpdate(usage) => {
3370                telemetry::event!(
3371                    "Agent Thread Completion Usage Updated",
3372                    thread_id = self.id.to_string(),
3373                    parent_thread_id = self.parent_thread_id().map(|id| id.to_string()),
3374                    prompt_id = self.prompt_id.to_string(),
3375                    model = self.model().map(|m| m.telemetry_id()),
3376                    model_provider = self.model().map(|m| m.provider_id().to_string()),
3377                    input_tokens = usage.input_tokens,
3378                    output_tokens = usage.output_tokens,
3379                    cache_creation_input_tokens = usage.cache_creation_input_tokens,
3380                    cache_read_input_tokens = usage.cache_read_input_tokens,
3381                );
3382                // A successful compaction defers its telemetry until the first
3383                // completion that follows it, so `tokens_after` reflects the
3384                // real post-compaction context size.
3385                if let Some(telemetry) = self.pending_compaction_telemetry.take() {
3386                    telemetry.emit("succeeded", None, Some(total_input_tokens(usage)));
3387                }
3388                self.update_token_usage(usage, cx);
3389            }
3390            Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()),
3391            Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()),
3392            Stop(StopReason::ToolUse | StopReason::EndTurn) => {}
3393            Started | Queued { .. } | Compaction(_) => {}
3394        }
3395
3396        Ok(None)
3397    }
3398
3399    fn handle_text_event(&mut self, new_text: String, event_stream: &ThreadEventStream) {
3400        event_stream.send_text(&new_text);
3401
3402        let last_message = self.pending_message();
3403        if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() {
3404            text.push_str(&new_text);
3405        } else {
3406            last_message
3407                .content
3408                .push(AgentMessageContent::Text(new_text));
3409        }
3410    }
3411
3412    fn handle_thinking_event(
3413        &mut self,
3414        new_text: String,
3415        new_signature: Option<String>,
3416        event_stream: &ThreadEventStream,
3417    ) {
3418        event_stream.send_thinking(&new_text);
3419
3420        let last_message = self.pending_message();
3421        if let Some(AgentMessageContent::Thinking { text, signature }) =
3422            last_message.content.last_mut()
3423        {
3424            text.push_str(&new_text);
3425            *signature = new_signature.or(signature.take());
3426        } else {
3427            last_message.content.push(AgentMessageContent::Thinking {
3428                text: new_text,
3429                signature: new_signature,
3430            });
3431        }
3432    }
3433
3434    fn handle_redacted_thinking_event(&mut self, data: String) {
3435        let last_message = self.pending_message();
3436        last_message
3437            .content
3438            .push(AgentMessageContent::RedactedThinking(data));
3439    }
3440
3441    fn handle_tool_use_event(
3442        &mut self,
3443        tool_use: LanguageModelToolUse,
3444        event_stream: &ThreadEventStream,
3445        cancellation_rx: watch::Receiver<bool>,
3446        cx: &mut Context<Self>,
3447    ) -> Option<Task<(usize, LanguageModelToolResult)>> {
3448        cx.notify();
3449
3450        let owning_message_ix = self.messages.len();
3451        let tool = self.tool(tool_use.name.as_ref());
3452        let mut title = SharedString::from(&tool_use.name);
3453        let mut kind = acp::ToolKind::Other;
3454        if let Some(tool) = tool.as_ref() {
3455            if let Ok(input) = tool_use.input.clone().into_json() {
3456                title = tool.initial_title(input, cx);
3457            }
3458            kind = tool.kind();
3459        }
3460
3461        self.send_or_update_tool_use(&tool_use, title, kind, owning_message_ix, event_stream);
3462
3463        let Some(tool) = tool else {
3464            let content = format!("No tool named {} exists", tool_use.name);
3465            return Some(Task::ready((
3466                owning_message_ix,
3467                LanguageModelToolResult {
3468                    content: vec![LanguageModelToolResultContent::Text(Arc::from(content))],
3469                    tool_use_id: tool_use.id,
3470                    tool_name: tool_use.name,
3471                    is_error: true,
3472                    output: None,
3473                },
3474            )));
3475        };
3476
3477        // Agent tools are JSON-schema tools. Custom text-tool deltas are rejected
3478        // before considering partial-vs-complete input for these local tools.
3479        let input = match tool_use.input.clone().into_json() {
3480            Ok(input) => input,
3481            Err(error) => {
3482                return Some(Task::ready((
3483                    owning_message_ix,
3484                    LanguageModelToolResult {
3485                        content: vec![LanguageModelToolResultContent::Text(Arc::from(
3486                            error.to_string(),
3487                        ))],
3488                        tool_use_id: tool_use.id,
3489                        tool_name: tool_use.name,
3490                        is_error: true,
3491                        output: None,
3492                    },
3493                )));
3494            }
3495        };
3496
3497        if !tool_use.is_input_complete {
3498            if tool.supports_input_streaming() {
3499                let running_turn = self.running_turn.as_mut()?;
3500                if let Some(sender) = running_turn.streaming_tool_inputs.get_mut(&tool_use.id) {
3501                    sender.send_partial(input);
3502                    return None;
3503                }
3504
3505                let (mut sender, tool_input) = ToolInputSender::channel();
3506                sender.send_partial(input);
3507                running_turn
3508                    .streaming_tool_inputs
3509                    .insert(tool_use.id.clone(), sender);
3510
3511                let tool = tool.clone();
3512                log::debug!("Running streaming tool {}", tool_use.name);
3513                return Some(self.run_tool(
3514                    tool,
3515                    tool_input,
3516                    tool_use.id,
3517                    tool_use.name,
3518                    owning_message_ix,
3519                    event_stream,
3520                    cancellation_rx,
3521                    cx,
3522                ));
3523            } else {
3524                return None;
3525            }
3526        }
3527
3528        if let Some(mut sender) = self
3529            .running_turn
3530            .as_mut()?
3531            .streaming_tool_inputs
3532            .remove(&tool_use.id)
3533        {
3534            sender.send_full(input);
3535            return None;
3536        }
3537
3538        log::debug!("Running tool {}", tool_use.name);
3539        let tool_input = ToolInput::ready(input);
3540        Some(self.run_tool(
3541            tool,
3542            tool_input,
3543            tool_use.id,
3544            tool_use.name,
3545            owning_message_ix,
3546            event_stream,
3547            cancellation_rx,
3548            cx,
3549        ))
3550    }
3551
3552    fn run_tool(
3553        &self,
3554        tool: Arc<dyn AnyAgentTool>,
3555        tool_input: ToolInput<serde_json::Value>,
3556        tool_use_id: LanguageModelToolUseId,
3557        tool_name: Arc<str>,
3558        owning_message_ix: usize,
3559        event_stream: &ThreadEventStream,
3560        cancellation_rx: watch::Receiver<bool>,
3561        cx: &mut Context<Self>,
3562    ) -> Task<(usize, LanguageModelToolResult)> {
3563        // A workspace can become restricted after a thread has already started.
3564        // Tools that aren't allowed in restricted workspaces must never run in
3565        // that state, even though they were exposed to the model earlier.
3566        if !tool.allow_in_restricted_mode()
3567            && TrustedWorktrees::has_restricted_worktrees(
3568                &self.project.read(cx).worktree_store(),
3569                cx,
3570            )
3571        {
3572            return Task::ready((
3573                owning_message_ix,
3574                LanguageModelToolResult {
3575                    tool_use_id,
3576                    tool_name,
3577                    is_error: true,
3578                    content: vec![LanguageModelToolResultContent::Text(Arc::from(
3579                        "workspace has become restricted",
3580                    ))],
3581                    output: None,
3582                },
3583            ));
3584        }
3585
3586        let fs = self.project.read(cx).fs().clone();
3587        let tool_call_id = scoped_tool_call_id(owning_message_ix, &tool_use_id);
3588        let tool_event_stream = ToolCallEventStream::new(
3589            tool_use_id.clone(),
3590            tool_call_id,
3591            event_stream.clone(),
3592            Some(fs),
3593            cancellation_rx,
3594            self.sandbox_grants.clone(),
3595            Some(cx.weak_entity()),
3596        );
3597        tool_event_stream.update_fields(
3598            acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress),
3599        );
3600        let supports_images = self.model().is_some_and(|model| model.supports_images());
3601        let tool_result = tool.run(tool_input, tool_event_stream, cx);
3602        cx.foreground_executor().spawn(async move {
3603            let (is_error, output) = match tool_result.await {
3604                Ok(mut output) => {
3605                    let contains_image = output
3606                        .llm_output
3607                        .iter()
3608                        .any(|part| matches!(part, LanguageModelToolResultContent::Image(_)));
3609                    if contains_image && !supports_images {
3610                        // Replace each image part with an inline placeholder so
3611                        // any accompanying text is still presented to the model.
3612                        // If there's nothing else in the output, surface an error
3613                        // to match the pre-multi-part behavior for image-only
3614                        // tool results.
3615                        let placeholder = LanguageModelToolResultContent::Text(Arc::from(
3616                            "[Tool responded with an image, but this model doesn't support images]",
3617                        ));
3618                        let has_non_image = output
3619                            .llm_output
3620                            .iter()
3621                            .any(|part| !matches!(part, LanguageModelToolResultContent::Image(_)));
3622                        if has_non_image {
3623                            output.llm_output = output
3624                                .llm_output
3625                                .into_iter()
3626                                .map(|part| match part {
3627                                    LanguageModelToolResultContent::Image(_) => placeholder.clone(),
3628                                    other => other,
3629                                })
3630                                .collect();
3631                            (false, output)
3632                        } else {
3633                            let output = anyhow::anyhow!(
3634                                "Attempted to read an image, but this model doesn't support it.",
3635                            )
3636                            .into();
3637                            (true, output)
3638                        }
3639                    } else {
3640                        (false, output)
3641                    }
3642                }
3643                Err(output) => (true, output),
3644            };
3645
3646            (
3647                owning_message_ix,
3648                LanguageModelToolResult {
3649                    tool_use_id,
3650                    tool_name,
3651                    is_error,
3652                    content: output.llm_output,
3653                    output: Some(output.raw_output),
3654                },
3655            )
3656        })
3657    }
3658
3659    fn handle_tool_use_json_parse_error_event(
3660        &mut self,
3661        tool_use_id: LanguageModelToolUseId,
3662        tool_name: Arc<str>,
3663        raw_input: Arc<str>,
3664        json_parse_error: String,
3665        event_stream: &ThreadEventStream,
3666        cancellation_rx: watch::Receiver<bool>,
3667        cx: &mut Context<Self>,
3668    ) -> Option<Task<(usize, LanguageModelToolResult)>> {
3669        let owning_message_ix = self.messages.len();
3670        let tool_use = LanguageModelToolUse {
3671            id: tool_use_id,
3672            name: tool_name,
3673            raw_input: raw_input.to_string(),
3674            input: language_model::LanguageModelToolUseInput::Json(serde_json::json!({})),
3675            is_input_complete: true,
3676            thought_signature: None,
3677        };
3678        self.send_or_update_tool_use(
3679            &tool_use,
3680            SharedString::from(&tool_use.name),
3681            acp::ToolKind::Other,
3682            owning_message_ix,
3683            event_stream,
3684        );
3685
3686        let tool = self.tool(tool_use.name.as_ref());
3687
3688        let Some(tool) = tool else {
3689            let content = format!("No tool named {} exists", tool_use.name);
3690            return Some(Task::ready((
3691                owning_message_ix,
3692                LanguageModelToolResult {
3693                    content: vec![LanguageModelToolResultContent::Text(Arc::from(content))],
3694                    tool_use_id: tool_use.id,
3695                    tool_name: tool_use.name,
3696                    is_error: true,
3697                    output: None,
3698                },
3699            )));
3700        };
3701
3702        let error_message = format!("Error parsing input JSON: {json_parse_error}");
3703
3704        if tool.supports_input_streaming()
3705            && let Some(mut sender) = self
3706                .running_turn
3707                .as_mut()?
3708                .streaming_tool_inputs
3709                .remove(&tool_use.id)
3710        {
3711            sender.send_invalid_json(error_message);
3712            return None;
3713        }
3714
3715        log::debug!("Running tool {}. Received invalid JSON", tool_use.name);
3716        let tool_input = ToolInput::invalid_json(error_message);
3717        Some(self.run_tool(
3718            tool,
3719            tool_input,
3720            tool_use.id,
3721            tool_use.name,
3722            owning_message_ix,
3723            event_stream,
3724            cancellation_rx,
3725            cx,
3726        ))
3727    }
3728
3729    fn send_or_update_tool_use(
3730        &mut self,
3731        tool_use: &LanguageModelToolUse,
3732        title: SharedString,
3733        kind: acp::ToolKind,
3734        owning_message_ix: usize,
3735        event_stream: &ThreadEventStream,
3736    ) {
3737        let tool_call_id = scoped_tool_call_id(owning_message_ix, &tool_use.id);
3738
3739        // Ensure the last message ends in the current tool use
3740        let last_message = self.pending_message();
3741
3742        let has_tool_use = last_message.content.iter_mut().rev().any(|content| {
3743            if let AgentMessageContent::ToolUse(last_tool_use) = content {
3744                if last_tool_use.id == tool_use.id {
3745                    *last_tool_use = tool_use.clone();
3746                    return true;
3747                }
3748            }
3749            false
3750        });
3751
3752        if !has_tool_use {
3753            event_stream.send_tool_call(
3754                &tool_call_id,
3755                &tool_use.name,
3756                title,
3757                kind,
3758                tool_use.input.to_display_json(),
3759            );
3760            last_message
3761                .content
3762                .push(AgentMessageContent::ToolUse(tool_use.clone()));
3763        } else {
3764            event_stream.update_tool_call_fields(
3765                &tool_call_id,
3766                acp::ToolCallUpdateFields::new()
3767                    .title(title.as_str())
3768                    .kind(kind)
3769                    .raw_input(tool_use.input.to_display_json()),
3770                None,
3771            );
3772        }
3773    }
3774
3775    pub fn title(&self) -> Option<SharedString> {
3776        self.title.clone()
3777    }
3778
3779    pub fn is_generating_summary(&self) -> bool {
3780        self.pending_summary_generation.is_some()
3781    }
3782
3783    pub fn is_generating_title(&self) -> bool {
3784        self.pending_title_generation.is_some()
3785    }
3786
3787    pub fn has_failed_title_generation(&self) -> bool {
3788        self.title_generation_error.is_some()
3789    }
3790
3791    pub fn title_generation_error(&self) -> Option<SharedString> {
3792        self.title_generation_error.clone()
3793    }
3794
3795    pub fn can_generate_title(&self) -> bool {
3796        self.pending_title_generation.is_none() && self.summarization_model.is_some()
3797    }
3798
3799    pub fn summary(&mut self, cx: &mut Context<Self>) -> Shared<Task<Option<SharedString>>> {
3800        if let Some(summary) = self.summary.as_ref() {
3801            return Task::ready(Some(summary.clone())).shared();
3802        }
3803        if let Some(task) = self.pending_summary_generation.clone() {
3804            return task;
3805        }
3806        let Some(model) = self.summarization_model.clone() else {
3807            log::error!("No summarization model available");
3808            return Task::ready(None).shared();
3809        };
3810        let mut request = LanguageModelRequest {
3811            intent: Some(CompletionIntent::ThreadContextSummarization),
3812            temperature: AgentSettings::temperature_for_model(&model, cx),
3813            ..Default::default()
3814        };
3815
3816        self.extend_request_history_until(&mut request.messages, self.messages.len());
3817
3818        request.messages.push(LanguageModelRequestMessage {
3819            role: Role::User,
3820            content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
3821            cache: false,
3822            reasoning_details: None,
3823        });
3824
3825        let task = cx
3826            .spawn(async move |this, cx| {
3827                let mut summary = String::new();
3828                let mut messages = model.stream_completion(request, cx).await.log_err()?;
3829                while let Some(event) = messages.next().await {
3830                    let event = event.log_err()?;
3831                    let text = match event {
3832                        LanguageModelCompletionEvent::Text(text) => text,
3833                        _ => continue,
3834                    };
3835
3836                    let mut lines = text.lines();
3837                    summary.extend(lines.next());
3838                }
3839
3840                log::debug!("Setting summary: {}", summary);
3841                let summary = SharedString::from(summary);
3842
3843                this.update(cx, |this, cx| {
3844                    this.summary = Some(summary.clone());
3845                    this.pending_summary_generation = None;
3846                    cx.notify()
3847                })
3848                .ok()?;
3849
3850                Some(summary)
3851            })
3852            .shared();
3853        self.pending_summary_generation = Some(task.clone());
3854        task
3855    }
3856
3857    pub fn generate_title(&mut self, cx: &mut Context<Self>) {
3858        if !self.can_generate_title() {
3859            return;
3860        }
3861        let Some(model) = self.summarization_model.clone() else {
3862            return;
3863        };
3864        self.spawn_title_generation(model, None, cx);
3865    }
3866
3867    pub fn regenerate_title(&mut self, cx: &mut Context<Self>) -> bool {
3868        self.regenerate_title_with_callback(cx, |_title, _cx| {})
3869    }
3870
3871    pub fn regenerate_title_with_callback(
3872        &mut self,
3873        cx: &mut Context<Self>,
3874        on_generated_title: impl FnOnce(SharedString, &mut Context<Self>) + 'static,
3875    ) -> bool {
3876        if self.pending_title_generation.is_some() {
3877            return false;
3878        }
3879
3880        let Some(model) = self.summarization_model.clone() else {
3881            return false;
3882        };
3883
3884        self.spawn_title_generation(model, Some(Box::new(on_generated_title)), cx);
3885
3886        true
3887    }
3888
3889    fn spawn_title_generation(
3890        &mut self,
3891        model: Arc<dyn LanguageModel>,
3892        on_generated_title: Option<Box<dyn FnOnce(SharedString, &mut Context<Self>)>>,
3893        cx: &mut Context<Self>,
3894    ) {
3895        self.title_generation_error = None;
3896        log::debug!("Generating title with model: {:?}", model.name());
3897
3898        let temperature = AgentSettings::temperature_for_model(&model, cx);
3899        let request = build_thread_title_request(&self.messages, temperature);
3900
3901        let title_generation = cx.spawn(async move |_this, cx| {
3902            stream_thread_title(model, request, cx)
3903                .await
3904                .context("failed to generate thread title")
3905                .map(SharedString::from)
3906        });
3907
3908        self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
3909            let title = title_generation.await;
3910            _ = this.update(cx, |this, cx| {
3911                this.pending_title_generation = None;
3912                match title {
3913                    Ok(title) => {
3914                        this.set_title(title.clone(), cx);
3915                        if let Some(on_generated_title) = on_generated_title {
3916                            on_generated_title(title, cx);
3917                        }
3918                    }
3919                    Err(error) => {
3920                        let error = format!("{error:#}");
3921                        log::error!("{error}");
3922                        this.title_generation_error = Some(error.into());
3923                        cx.emit(TitleUpdated);
3924                        cx.notify();
3925                    }
3926                }
3927            });
3928        }));
3929        cx.notify();
3930    }
3931
3932    pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
3933        self.pending_title_generation = None;
3934        self.title_generation_error = None;
3935        if Some(&title) != self.title.as_ref() {
3936            self.title = Some(title);
3937            cx.emit(TitleUpdated);
3938            cx.notify();
3939        }
3940    }
3941
3942    fn clear_summary(&mut self) {
3943        self.summary = None;
3944        self.pending_summary_generation = None;
3945    }
3946
3947    fn last_user_message(&self) -> Option<&UserMessage> {
3948        self.messages
3949            .iter()
3950            .rev()
3951            .find_map(|message| match &**message {
3952                Message::User(user_message) => Some(user_message),
3953                Message::Agent(_) | Message::Resume | Message::Compaction(_) => None,
3954            })
3955    }
3956
3957    fn pending_message(&mut self) -> &mut AgentMessage {
3958        self.pending_message.get_or_insert_default()
3959    }
3960
3961    fn flush_pending_message(&mut self, cx: &mut Context<Self>) {
3962        let Some(mut message) = self.pending_message.take() else {
3963            return;
3964        };
3965
3966        if message.content.is_empty() {
3967            return;
3968        }
3969
3970        for content in &message.content {
3971            let AgentMessageContent::ToolUse(tool_use) = content else {
3972                continue;
3973            };
3974
3975            if !message.tool_results.contains_key(&tool_use.id) {
3976                message.tool_results.insert(
3977                    tool_use.id.clone(),
3978                    LanguageModelToolResult {
3979                        tool_use_id: tool_use.id.clone(),
3980                        tool_name: tool_use.name.clone(),
3981                        is_error: true,
3982                        content: vec![LanguageModelToolResultContent::Text(
3983                            TOOL_CANCELED_MESSAGE.into(),
3984                        )],
3985                        output: None,
3986                    },
3987                );
3988            }
3989        }
3990
3991        self.messages.push(Arc::new(Message::Agent(message)));
3992        self.updated_at = Utc::now();
3993        self.clear_summary();
3994        cx.notify()
3995    }
3996
3997    pub(crate) fn build_completion_request(
3998        &self,
3999        completion_intent: CompletionIntent,
4000        cx: &App,
4001    ) -> Result<LanguageModelRequest> {
4002        let completion_intent =
4003            if self.is_subagent() && completion_intent == CompletionIntent::UserPrompt {
4004                CompletionIntent::Subagent
4005            } else {
4006                completion_intent
4007            };
4008
4009        let model = self
4010            .model()
4011            .ok_or_else(|| anyhow!(NoModelConfiguredError))?;
4012        let sandboxing_enabled = crate::sandboxing::sandboxing_enabled(cx);
4013        let tools = if let Some(turn) = self.running_turn.as_ref() {
4014            turn.tools
4015                .iter()
4016                .filter_map(|(tool_name, tool)| {
4017                    log::trace!("Including tool: {}", tool_name);
4018                    let mut description = tool.description().to_string();
4019                    let mut schema = tool.input_schema(model.tool_input_format()).log_err()?;
4020                    // TEMPORARY (sandboxing feature flag): with the flag off,
4021                    // the fetch and create_directory descriptions/schemas must
4022                    // not advertise sandbox-dependent behavior (host grants,
4023                    // out-of-project creation via the `reason` field), since
4024                    // the corresponding runtime paths are disabled. Restore
4025                    // the pre-sandboxing model-facing surface here rather than
4026                    // forking the tools; delete this when the flag is removed
4027                    // again.
4028                    if !sandboxing_enabled {
4029                        if tool_name.as_ref() == FetchTool::NAME {
4030                            description =
4031                                "Fetches a URL and returns the content as Markdown.".to_string();
4032                        } else if tool_name.as_ref() == CreateDirectoryTool::NAME {
4033                            description = "Creates a new directory at the specified path within \
4034                                the project. Returns confirmation that the directory was \
4035                                created.\n\nThis tool creates a directory and all necessary \
4036                                parent directories. It should be used whenever you need to \
4037                                create new directories within the project.\nThe only supported \
4038                                path outside the project is `~/.agents/skills` or a descendant, \
4039                                for global agent skills."
4040                                .to_string();
4041                            if let Some(properties) = schema
4042                                .get_mut("properties")
4043                                .and_then(|value| value.as_object_mut())
4044                            {
4045                                properties.remove("reason");
4046                            }
4047                        }
4048                    }
4049                    Some(LanguageModelRequestTool::function(
4050                        tool_name.to_string(),
4051                        description,
4052                        schema,
4053                        tool.supports_input_streaming(),
4054                    ))
4055                })
4056                .collect::<Vec<_>>()
4057        } else {
4058            Vec::new()
4059        };
4060
4061        log::debug!("Building completion request");
4062        log::debug!("Completion intent: {:?}", completion_intent);
4063
4064        let available_tools: Vec<_> = self
4065            .running_turn
4066            .as_ref()
4067            .map(|turn| turn.tools.keys().cloned().collect())
4068            .unwrap_or_default();
4069
4070        log::debug!("Request includes {} tools", available_tools.len());
4071        let messages = self.build_request_messages(available_tools, cx);
4072        log::debug!("Request will include {} messages", messages.len());
4073
4074        let request = LanguageModelRequest {
4075            thread_id: Some(self.id.to_string()),
4076            prompt_id: Some(self.prompt_id.to_string()),
4077            intent: Some(completion_intent),
4078            messages,
4079            tools,
4080            tool_choice: None,
4081            stop: Vec::new(),
4082            temperature: AgentSettings::temperature_for_model(model, cx),
4083            // Models that can't run with thinking disabled ignore the
4084            // toggle state, which may be stale from a previously selected
4085            // model that could.
4086            thinking_allowed: self.thinking_enabled || !model.supports_disabling_thinking(),
4087            thinking_effort: self.thinking_effort.clone(),
4088            speed: self.speed(),
4089            compact_at_tokens: None,
4090        };
4091
4092        log::debug!("Completion request built successfully");
4093        Ok(request)
4094    }
4095
4096    fn enabled_tools(&self, cx: &App) -> BTreeMap<SharedString, Arc<dyn AnyAgentTool>> {
4097        let Some(model) = self.model() else {
4098            return BTreeMap::new();
4099        };
4100        let Some(profile) = AgentSettings::get_global(cx).profiles.get(&self.profile_id) else {
4101            return BTreeMap::new();
4102        };
4103        // Terminal variants are configured by users under the canonical
4104        // `terminal` name. Expose the one matching the current sandbox state
4105        // to the model under that name.
4106        let use_sandboxed_terminal = sandboxing_enabled_for_project(self.project.read(cx), cx);
4107
4108        // Tools that aren't allowed in restricted workspaces must never be
4109        // provided to the model while the workspace is restricted, regardless
4110        // of what the active profile enables.
4111        let is_restricted =
4112            TrustedWorktrees::has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx);
4113
4114        let mut tools = self
4115            .tools
4116            .iter()
4117            .filter(|(_, tool)| !is_restricted || tool.allow_in_restricted_mode())
4118            .filter_map(|(tool_name, tool)| {
4119                let terminal_variant = matches!(
4120                    tool_name.as_ref(),
4121                    TerminalTool::NAME | SandboxedTerminalTool::NAME
4122                );
4123                let profile_tool_name = if terminal_variant {
4124                    TerminalTool::NAME
4125                } else {
4126                    tool_name.as_ref()
4127                };
4128
4129                if tool.supports_provider(&model.provider_id())
4130                    && profile.is_tool_enabled(profile_tool_name)
4131                {
4132                    match (tool_name.as_ref(), use_sandboxed_terminal) {
4133                        (TerminalTool::NAME, false) | (SandboxedTerminalTool::NAME, true) => {
4134                            Some((SharedString::from(TerminalTool::NAME), tool.clone()))
4135                        }
4136                        (TerminalTool::NAME | SandboxedTerminalTool::NAME, _) => None,
4137                        _ => Some((
4138                            provider_compatible_tool_name(tool_name.as_ref()).into(),
4139                            tool.clone(),
4140                        )),
4141                    }
4142                } else {
4143                    None
4144                }
4145            })
4146            .filter(|(tool_name, _)| crate::tools::tool_feature_flag_enabled(tool_name, cx))
4147            .collect::<BTreeMap<_, _>>();
4148
4149        let mut context_server_tools = Vec::new();
4150        let mut seen_tools = tools.keys().cloned().collect::<HashSet<_>>();
4151        let mut duplicate_tool_names = HashSet::default();
4152        for (server_id, server_tools) in self.context_server_registry.read(cx).servers() {
4153            for (tool_name, tool) in server_tools {
4154                if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) {
4155                    let tool_name: SharedString =
4156                        provider_compatible_tool_name(tool_name.as_ref()).into();
4157                    if !seen_tools.insert(tool_name.clone()) {
4158                        duplicate_tool_names.insert(tool_name.clone());
4159                    }
4160                    context_server_tools.push((server_id.clone(), tool_name, tool.clone()));
4161                }
4162            }
4163        }
4164
4165        // When there are duplicate tool names, disambiguate by prefixing them
4166        // with the server ID (converted to snake_case for API compatibility).
4167        // In the rare case there isn't enough space for the disambiguated tool
4168        // name, keep only the last tool with this name.
4169        for (server_id, tool_name, tool) in context_server_tools {
4170            if duplicate_tool_names.contains(&tool_name) {
4171                let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len());
4172                if available >= 2 {
4173                    let mut disambiguated =
4174                        provider_compatible_tool_name(&server_id.0.to_snake_case()).to_string();
4175                    disambiguated.truncate(available - 1);
4176                    disambiguated.push('_');
4177                    disambiguated.push_str(&tool_name);
4178                    tools.insert(disambiguated.into(), tool.clone());
4179                } else {
4180                    tools.insert(tool_name, tool.clone());
4181                }
4182            } else {
4183                tools.insert(tool_name, tool.clone());
4184            }
4185        }
4186
4187        tools
4188    }
4189
4190    fn refresh_turn_tools(&mut self, cx: &App) {
4191        let tools = self.enabled_tools(cx);
4192        if let Some(turn) = self.running_turn.as_mut() {
4193            turn.tools = tools;
4194        }
4195    }
4196
4197    fn tool(&self, name: &str) -> Option<Arc<dyn AnyAgentTool>> {
4198        self.running_turn.as_ref()?.tools.get(name).cloned()
4199    }
4200
4201    pub fn has_tool(&self, name: &str) -> bool {
4202        self.running_turn
4203            .as_ref()
4204            .is_some_and(|turn| turn.tools.contains_key(name))
4205    }
4206
4207    #[cfg(any(test, feature = "test-support"))]
4208    pub fn has_registered_tool(&self, name: &str) -> bool {
4209        self.tools.contains_key(name)
4210    }
4211
4212    pub(crate) fn register_running_subagent(&mut self, subagent: WeakEntity<Thread>) {
4213        self.running_subagents.push(subagent);
4214    }
4215
4216    pub(crate) fn unregister_running_subagent(
4217        &mut self,
4218        subagent_session_id: &acp::SessionId,
4219        cx: &App,
4220    ) {
4221        self.running_subagents.retain(|s| {
4222            s.upgrade()
4223                .map_or(false, |s| s.read(cx).id() != subagent_session_id)
4224        });
4225    }
4226
4227    #[cfg(any(test, feature = "test-support"))]
4228    pub fn running_subagent_ids(&self, cx: &App) -> Vec<acp::SessionId> {
4229        self.running_subagents
4230            .iter()
4231            .filter_map(|s| s.upgrade().map(|s| s.read(cx).id().clone()))
4232            .collect()
4233    }
4234
4235    pub fn is_subagent(&self) -> bool {
4236        self.subagent_context.is_some()
4237    }
4238
4239    pub fn parent_thread_id(&self) -> Option<acp::SessionId> {
4240        self.subagent_context
4241            .as_ref()
4242            .map(|c| c.parent_thread_id.clone())
4243    }
4244
4245    pub fn depth(&self) -> u8 {
4246        self.subagent_context.as_ref().map(|c| c.depth).unwrap_or(0)
4247    }
4248
4249    #[cfg(any(test, feature = "test-support"))]
4250    pub fn set_subagent_context(&mut self, context: SubagentContext) {
4251        self.subagent_context = Some(context);
4252    }
4253
4254    pub fn is_turn_complete(&self) -> bool {
4255        self.running_turn.is_none()
4256    }
4257
4258    fn build_request_messages(
4259        &self,
4260        available_tools: Vec<SharedString>,
4261        cx: &App,
4262    ) -> Vec<LanguageModelRequestMessage> {
4263        let mut messages =
4264            self.build_request_messages_until(available_tools, self.messages.len(), cx);
4265
4266        if let Some(message) = self.pending_message.as_ref() {
4267            messages.extend(message.to_request());
4268        }
4269
4270        messages
4271    }
4272
4273    fn build_request_messages_until(
4274        &self,
4275        available_tools: Vec<SharedString>,
4276        end_ix: usize,
4277        cx: &App,
4278    ) -> Vec<LanguageModelRequestMessage> {
4279        let end_ix = end_ix.min(self.messages.len());
4280        log::trace!("Building request messages from {} thread messages", end_ix);
4281
4282        let user_agents_md = UserAgentsMd::global(cx).and_then(|s| s.content().cloned());
4283        let system_prompt = SystemPromptTemplate {
4284            project: self.project_context.read(cx),
4285            available_tools,
4286            model_name: self.model().map(|m| m.name().0.to_string()),
4287            date: Local::now().format("%Y-%m-%d").to_string(),
4288            user_agents_md,
4289            sandboxing: crate::sandboxing::sandboxing_enabled_for_project(
4290                self.project.read(cx),
4291                cx,
4292            ),
4293            is_linux: cfg!(target_os = "linux"),
4294            is_windows: cfg!(target_os = "windows"),
4295        }
4296        .render(&self.templates)
4297        .context("failed to build system prompt")
4298        .expect("Invalid template");
4299        let mut messages = vec![LanguageModelRequestMessage {
4300            role: Role::System,
4301            content: vec![system_prompt.into()],
4302            cache: false,
4303            reasoning_details: None,
4304        }];
4305        self.extend_request_history_until(&mut messages, end_ix);
4306
4307        if let Some(last_message) = messages.last_mut() {
4308            last_message.cache = true;
4309        }
4310
4311        messages
4312    }
4313
4314    fn extend_request_history_until(
4315        &self,
4316        request_messages: &mut Vec<LanguageModelRequestMessage>,
4317        end_ix: usize,
4318    ) {
4319        extend_request_history_until(&self.messages, request_messages, end_ix);
4320    }
4321
4322    fn build_compaction_telemetry(
4323        &self,
4324        trigger: &'static str,
4325        compaction_model: &Arc<dyn LanguageModel>,
4326        cx: &App,
4327    ) -> Option<CompactionTelemetry> {
4328        let model = self.model()?;
4329        let auto_compact = AgentSettings::get_global(cx).auto_compact;
4330        let max_tokens = model.max_token_count();
4331        let max_input_tokens = max_tokens.saturating_sub(model.max_output_tokens().unwrap_or(0));
4332        let tokens_before = self
4333            .latest_request_token_usage()
4334            .map(|usage| total_input_tokens(usage).saturating_add(usage.output_tokens));
4335        Some(CompactionTelemetry {
4336            trigger,
4337            thread_id: self.id.to_string(),
4338            parent_thread_id: self.parent_thread_id().map(|id| id.to_string()),
4339            prompt_id: self.prompt_id.to_string(),
4340            model: model.telemetry_id(),
4341            compaction_model: compaction_model.telemetry_id(),
4342            thinking_effort: self.thinking_effort.clone(),
4343            max_tokens,
4344            tokens_before,
4345            auto_compact_enabled: auto_compact.enabled,
4346            auto_compact_threshold: auto_compact.threshold.to_string(),
4347            auto_compact_threshold_tokens: auto_compact_threshold_token_count(
4348                auto_compact.threshold,
4349                max_input_tokens,
4350            ),
4351            retries: 0,
4352        })
4353    }
4354
4355    /// Emits a pending compaction telemetry event for a non-success outcome
4356    /// (`"failed"` or `"canceled"`), with no post-compaction token count. A
4357    /// no-op if no compaction telemetry is pending.
4358    fn emit_compaction_telemetry_outcome(&mut self, status: &'static str, error: Option<String>) {
4359        if let Some(telemetry) = self.pending_compaction_telemetry.take() {
4360            telemetry.emit(status, error, None);
4361        }
4362    }
4363
4364    fn compaction_message_target_ix(&self, cx: &App) -> Option<usize> {
4365        let auto_compact = AgentSettings::get_global(cx).auto_compact;
4366        if !auto_compact.enabled {
4367            return None;
4368        }
4369
4370        let model = self.model()?;
4371        let max_token_count = model.max_token_count();
4372        let max_input_tokens =
4373            max_token_count.saturating_sub(model.max_output_tokens().unwrap_or(0));
4374        // Models with a small context window don't leave enough headroom for a
4375        // compaction pass; the UI warns the user about the token limit instead.
4376        if max_input_tokens < MIN_COMPACTION_CONTEXT_WINDOW {
4377            return None;
4378        }
4379        let (usage_ix, usage) = {
4380            let this = &self;
4381            this.messages
4382                .iter()
4383                .enumerate()
4384                .rev()
4385                .find_map(|(ix, message)| {
4386                    let Message::User(user_message) = &**message else {
4387                        return None;
4388                    };
4389                    this.request_token_usage
4390                        .get(&user_message.id)
4391                        .copied()
4392                        .map(|usage| (ix, usage))
4393                })
4394        }?;
4395        if latest_compaction_message_ix_before(&self.messages, self.messages.len())
4396            .is_some_and(|compaction_ix| compaction_ix > usage_ix)
4397        {
4398            return None;
4399        }
4400
4401        let active_tokens = total_input_tokens(usage).saturating_add(usage.output_tokens);
4402        let compaction_threshold =
4403            auto_compact_threshold_token_count(auto_compact.threshold, max_input_tokens);
4404        if active_tokens < compaction_threshold {
4405            return None;
4406        }
4407
4408        let insertion_ix = match self.messages.last() {
4409            Some(message)
4410                if matches!(
4411                    &**message,
4412                    Message::User(UserMessage { id, .. }) if !self.request_token_usage.contains_key(id)
4413                ) =>
4414            {
4415                self.messages.len().saturating_sub(1)
4416            }
4417            _ => self.messages.len(),
4418        };
4419        Some(insertion_ix)
4420    }
4421
4422    /// Insertion point for a manually-triggered compaction.
4423    /// Returns `None` only when there is nothing to summarize (no messages, or the thread already ends in a compaction).
4424    fn forced_compaction_target_ix(&self) -> Option<usize> {
4425        if matches!(
4426            self.messages.last().map(|message| &**message),
4427            None | Some(Message::Compaction(_))
4428        ) {
4429            return None;
4430        }
4431        Some(self.messages.len())
4432    }
4433
4434    fn compaction_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
4435        LanguageModelRegistry::read_global(cx)
4436            .compaction_model()
4437            .map(|m| m.model)
4438            .or_else(|| self.model().cloned())
4439    }
4440
4441    fn build_compaction_request(
4442        &self,
4443        insertion_ix: usize,
4444        model: &Arc<dyn LanguageModel>,
4445        cx: &App,
4446    ) -> LanguageModelRequest {
4447        let mut request = LanguageModelRequest {
4448            thread_id: Some(self.id.to_string()),
4449            prompt_id: Some(self.prompt_id.to_string()),
4450            intent: Some(CompletionIntent::ThreadContextSummarization),
4451            temperature: AgentSettings::temperature_for_model(model, cx),
4452            messages: self.build_request_messages_until(Vec::new(), insertion_ix, cx),
4453            ..Default::default()
4454        };
4455
4456        request.messages.push(LanguageModelRequestMessage {
4457            role: Role::User,
4458            content: vec![COMPACTION_PROMPT.into()],
4459            cache: false,
4460            reasoning_details: None,
4461        });
4462
4463        request
4464    }
4465
4466    pub fn to_markdown(&self) -> String {
4467        let mut markdown = messages_to_markdown(&self.messages);
4468
4469        if let Some(message) = self.pending_message.as_ref() {
4470            markdown.push_str("\n## Assistant\n\n");
4471            markdown.push_str(&message.to_markdown());
4472        }
4473
4474        markdown
4475    }
4476
4477    fn advance_prompt_id(&mut self) {
4478        self.prompt_id = PromptId::new();
4479    }
4480
4481    fn retry_strategy_for(error: &LanguageModelCompletionError) -> Option<RetryStrategy> {
4482        use LanguageModelCompletionError::*;
4483        use http_client::StatusCode;
4484
4485        // General strategy here:
4486        // - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all.
4487        // - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff.
4488        // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times.
4489        match error {
4490            HttpResponseError {
4491                status_code: StatusCode::TOO_MANY_REQUESTS,
4492                ..
4493            } => Some(RetryStrategy::ExponentialBackoff {
4494                initial_delay: BASE_RETRY_DELAY,
4495                max_attempts: MAX_RETRY_ATTEMPTS,
4496            }),
4497            ServerOverloaded { retry_after, .. } | RateLimitExceeded { retry_after, .. } => {
4498                Some(RetryStrategy::Fixed {
4499                    delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
4500                    max_attempts: MAX_RETRY_ATTEMPTS,
4501                })
4502            }
4503            UpstreamProviderError {
4504                status,
4505                retry_after,
4506                ..
4507            } => match *status {
4508                StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => {
4509                    Some(RetryStrategy::Fixed {
4510                        delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
4511                        max_attempts: MAX_RETRY_ATTEMPTS,
4512                    })
4513                }
4514                StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed {
4515                    delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
4516                    // Internal Server Error could be anything, retry up to 3 times.
4517                    max_attempts: 3,
4518                }),
4519                status => {
4520                    // There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"),
4521                    // but we frequently get them in practice. See https://http.dev/529
4522                    if status.as_u16() == 529 {
4523                        Some(RetryStrategy::Fixed {
4524                            delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
4525                            max_attempts: MAX_RETRY_ATTEMPTS,
4526                        })
4527                    } else {
4528                        Some(RetryStrategy::Fixed {
4529                            delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
4530                            max_attempts: 2,
4531                        })
4532                    }
4533                }
4534            },
4535            ApiInternalServerError { .. } => Some(RetryStrategy::Fixed {
4536                delay: BASE_RETRY_DELAY,
4537                max_attempts: 3,
4538            }),
4539            ApiReadResponseError { .. }
4540            | HttpSend { .. }
4541            | DeserializeResponse { .. }
4542            | BadRequestFormat { .. } => Some(RetryStrategy::Fixed {
4543                delay: BASE_RETRY_DELAY,
4544                max_attempts: 3,
4545            }),
4546            // Retrying these errors definitely shouldn't help.
4547            HttpResponseError {
4548                status_code:
4549                    StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED,
4550                ..
4551            }
4552            | AuthenticationError { .. }
4553            | PermissionError { .. }
4554            | NoApiKey { .. }
4555            | ApiEndpointNotFound { .. }
4556            | PromptTooLarge { .. } => None,
4557            // These errors might be transient, so retry them
4558            SerializeRequest { .. } | BuildRequestBody { .. } | StreamEndedUnexpectedly { .. } => {
4559                Some(RetryStrategy::Fixed {
4560                    delay: BASE_RETRY_DELAY,
4561                    max_attempts: 1,
4562                })
4563            }
4564            // Retry all other 4xx and 5xx errors once.
4565            HttpResponseError { status_code, .. }
4566                if status_code.is_client_error() || status_code.is_server_error() =>
4567            {
4568                Some(RetryStrategy::Fixed {
4569                    delay: BASE_RETRY_DELAY,
4570                    max_attempts: 3,
4571                })
4572            }
4573            // Retrying won't help for Payment Required errors.
4574            PaymentRequired => None,
4575            // Retrying won't help until the user consents to data retention
4576            // or switches models.
4577            DataRetentionConsentRequired { .. } => None,
4578            // Conservatively assume that any other errors are non-retryable
4579            HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed {
4580                delay: BASE_RETRY_DELAY,
4581                max_attempts: 2,
4582            }),
4583        }
4584    }
4585}
4586
4587fn total_input_tokens(usage: language_model::TokenUsage) -> u64 {
4588    usage
4589        .input_tokens
4590        .saturating_add(usage.cache_creation_input_tokens)
4591        .saturating_add(usage.cache_read_input_tokens)
4592}
4593
4594fn auto_compact_threshold_token_count(
4595    threshold: AutoCompactThreshold,
4596    max_token_count: u64,
4597) -> u64 {
4598    match threshold {
4599        AutoCompactThreshold::Percentage(percent) => {
4600            ((max_token_count as f64) * percent).ceil() as u64
4601        }
4602        AutoCompactThreshold::TokensUsed(tokens) => tokens,
4603        AutoCompactThreshold::TokensRemaining(tokens) => {
4604            max_token_count.saturating_sub(tokens).saturating_add(1)
4605        }
4606    }
4607}
4608
4609/// Snapshot of the data needed to report an `"Agent Compaction Completed"`
4610/// telemetry event, captured when a compaction starts.
4611struct CompactionTelemetry {
4612    /// `"auto"` for threshold-triggered compaction, `"manual"` for `/compact`.
4613    trigger: &'static str,
4614    thread_id: String,
4615    parent_thread_id: Option<String>,
4616    prompt_id: String,
4617    model: String,
4618    compaction_model: String,
4619    thinking_effort: Option<String>,
4620    max_tokens: u64,
4621    /// Tokens in the context window immediately before compaction.
4622    tokens_before: Option<u64>,
4623    auto_compact_enabled: bool,
4624    auto_compact_threshold: String,
4625    auto_compact_threshold_tokens: u64,
4626    /// Number of times the compaction request was retried before the final
4627    /// outcome.
4628    retries: u32,
4629}
4630
4631impl CompactionTelemetry {
4632    fn emit(self, status: &'static str, error: Option<String>, tokens_after: Option<u64>) {
4633        telemetry::event!(
4634            "Agent Compaction Completed",
4635            trigger = self.trigger,
4636            status = status,
4637            error = error,
4638            thread_id = self.thread_id,
4639            parent_thread_id = self.parent_thread_id,
4640            prompt_id = self.prompt_id,
4641            model = self.model,
4642            compaction_model = self.compaction_model,
4643            thinking_effort = self.thinking_effort,
4644            max_tokens = self.max_tokens,
4645            tokens_before = self.tokens_before,
4646            tokens_after = tokens_after,
4647            auto_compact_enabled = self.auto_compact_enabled,
4648            auto_compact_threshold = self.auto_compact_threshold,
4649            auto_compact_threshold_tokens = self.auto_compact_threshold_tokens,
4650            retries = self.retries,
4651        );
4652    }
4653}
4654
4655fn user_message_byte_len(message: &LanguageModelRequestMessage) -> usize {
4656    message
4657        .content
4658        .iter()
4659        .map(|content| match content {
4660            MessageContent::Text(text) => text.len(),
4661            MessageContent::Image(image) => image.len(),
4662            // These can never occur in a user message
4663            MessageContent::Thinking { .. }
4664            | MessageContent::RedactedThinking(_)
4665            | MessageContent::ToolResult(_)
4666            | MessageContent::ToolUse(_)
4667            | MessageContent::Compaction(_) => 0,
4668        })
4669        .sum()
4670}
4671
4672fn truncate_user_message_to_byte_budget(
4673    mut message: LanguageModelRequestMessage,
4674    byte_budget: usize,
4675) -> Option<LanguageModelRequestMessage> {
4676    let mut remaining_bytes = byte_budget;
4677    let mut content = Vec::with_capacity(message.content.len());
4678
4679    for item in message.content {
4680        match item {
4681            MessageContent::Text(text) => {
4682                let fits = text.len() <= remaining_bytes;
4683                if let Some(text) = take_text_within_byte_budget(text, &mut remaining_bytes) {
4684                    content.push(MessageContent::Text(text));
4685                }
4686                if !fits {
4687                    break;
4688                }
4689            }
4690            MessageContent::Image(image) => {
4691                let byte_len = image.len();
4692                if let Some(bytes) = remaining_bytes.checked_sub(byte_len) {
4693                    remaining_bytes = bytes;
4694                    content.push(MessageContent::Image(image));
4695                } else {
4696                    break;
4697                }
4698            }
4699            // These can never occur in a user message
4700            MessageContent::Thinking { .. }
4701            | MessageContent::RedactedThinking(_)
4702            | MessageContent::ToolResult(_)
4703            | MessageContent::ToolUse(_)
4704            | MessageContent::Compaction(_) => {}
4705        }
4706    }
4707
4708    if content.is_empty() {
4709        None
4710    } else {
4711        message.content = content;
4712        Some(message)
4713    }
4714}
4715
4716fn take_text_within_byte_budget(text: String, remaining_bytes: &mut usize) -> Option<String> {
4717    if text.is_empty() || *remaining_bytes == 0 {
4718        return None;
4719    }
4720
4721    if let Some(bytes) = remaining_bytes.checked_sub(text.len()) {
4722        *remaining_bytes = bytes;
4723        return Some(text);
4724    }
4725
4726    let end = text.floor_char_boundary((*remaining_bytes).min(text.len()));
4727    *remaining_bytes = 0;
4728
4729    let text = text[..end].to_string();
4730
4731    if text.is_empty() { None } else { Some(text) }
4732}
4733
4734/// Describes where a streamed compaction summary should land in the thread
4735/// once it completes successfully.
4736enum CompactionInsertion {
4737    /// Automatic compaction inserts the summary at an index computed up front
4738    /// (which may be before a trailing not-yet-answered user message).
4739    Auto { insertion_ix: usize },
4740    /// Manual `/compact` appends a zero-content user message followed by the summary.
4741    Manual { marker_id: ClientUserMessageId },
4742}
4743
4744struct RunningTurn {
4745    /// Holds the task that handles agent interaction until the end of the turn.
4746    /// Survives across multiple requests as the model performs tool calls and
4747    /// we run tools, report their results.
4748    _task: Task<()>,
4749    /// The current event stream for the running turn. Used to report a final
4750    /// cancellation event if we cancel the turn.
4751    event_stream: ThreadEventStream,
4752    /// The tools that are enabled for the current iteration of the turn.
4753    /// Refreshed at the start of each iteration via `refresh_turn_tools`.
4754    tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
4755    /// Sender to signal tool cancellation. When cancel is called, this is
4756    /// set to true so all tools can detect user-initiated cancellation.
4757    cancellation_tx: watch::Sender<bool>,
4758    /// Senders for tools that support input streaming and have already been
4759    /// started but are still receiving input from the LLM.
4760    streaming_tool_inputs: HashMap<LanguageModelToolUseId, ToolInputSender>,
4761}
4762
4763impl RunningTurn {
4764    fn new(
4765        event_stream: ThreadEventStream,
4766        tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
4767        cancellation_tx: watch::Sender<bool>,
4768        task: Task<()>,
4769    ) -> Self {
4770        Self {
4771            _task: task,
4772            event_stream,
4773            tools,
4774            cancellation_tx,
4775            streaming_tool_inputs: HashMap::default(),
4776        }
4777    }
4778
4779    fn cancel(mut self) -> Task<()> {
4780        log::debug!("Cancelling in progress turn");
4781        self.cancellation_tx.send(true).ok();
4782        self.event_stream.send_canceled();
4783        self._task
4784    }
4785}
4786
4787pub(crate) fn messages_to_markdown(messages: &[Arc<Message>]) -> String {
4788    let mut markdown = String::new();
4789    for (ix, message) in messages.iter().enumerate() {
4790        if ix > 0 {
4791            markdown.push('\n');
4792        }
4793        match &**message {
4794            Message::User(_) => markdown.push_str("## User\n\n"),
4795            Message::Agent(_) => markdown.push_str("## Assistant\n\n"),
4796            Message::Resume | Message::Compaction(_) => {}
4797        }
4798        markdown.push_str(&message.to_markdown());
4799    }
4800    markdown
4801}
4802
4803fn extend_request_history_until(
4804    messages: &[Arc<Message>],
4805    request_messages: &mut Vec<LanguageModelRequestMessage>,
4806    end_ix: usize,
4807) {
4808    let end_ix = end_ix.min(messages.len());
4809    let Some(compaction_ix) = latest_compaction_message_ix_before(messages, end_ix) else {
4810        for message in &messages[..end_ix] {
4811            request_messages.extend(message.to_request());
4812        }
4813        return;
4814    };
4815
4816    if matches!(
4817        &*messages[compaction_ix],
4818        Message::Compaction(CompactionInfo::Summary(_))
4819    ) {
4820        request_messages.extend(retained_user_request_messages_before(
4821            messages,
4822            compaction_ix,
4823        ));
4824    }
4825
4826    for message in &messages[compaction_ix..end_ix] {
4827        request_messages.extend(message.to_request());
4828    }
4829}
4830
4831fn latest_compaction_message_ix_before(messages: &[Arc<Message>], end_ix: usize) -> Option<usize> {
4832    messages[..end_ix]
4833        .iter()
4834        .rposition(|message| matches!(&**message, Message::Compaction(_)))
4835}
4836
4837fn retained_user_request_messages_before(
4838    messages: &[Arc<Message>],
4839    compaction_ix: usize,
4840) -> Vec<LanguageModelRequestMessage> {
4841    let mut remaining_bytes = COMPACTION_RETAINED_USER_MESSAGES_BYTE_BUDGET;
4842    let mut retained_messages = Vec::new();
4843
4844    for message in messages[..compaction_ix].iter().rev() {
4845        let Message::User(user_message) = &**message else {
4846            continue;
4847        };
4848        if user_message.content.is_empty() {
4849            continue;
4850        }
4851
4852        let request_message = user_message.to_request();
4853        let byte_count = user_message_byte_len(&request_message);
4854        if let Some(bytes) = remaining_bytes.checked_sub(byte_count) {
4855            remaining_bytes = bytes;
4856            retained_messages.push(request_message);
4857        } else {
4858            if remaining_bytes > 0
4859                && let Some(request_message) =
4860                    truncate_user_message_to_byte_budget(request_message, remaining_bytes)
4861            {
4862                retained_messages.push(request_message);
4863            }
4864            break;
4865        }
4866    }
4867
4868    retained_messages.reverse();
4869    retained_messages
4870}
4871
4872pub fn build_thread_title_request(
4873    messages: &[Arc<Message>],
4874    temperature: Option<f32>,
4875) -> LanguageModelRequest {
4876    let mut request = LanguageModelRequest {
4877        intent: Some(CompletionIntent::ThreadSummarization),
4878        temperature,
4879        ..Default::default()
4880    };
4881    extend_request_history_until(messages, &mut request.messages, messages.len());
4882    request.messages.push(LanguageModelRequestMessage {
4883        role: Role::User,
4884        content: vec![SUMMARIZE_THREAD_PROMPT.into()],
4885        cache: false,
4886        reasoning_details: None,
4887    });
4888    request
4889}
4890
4891pub async fn stream_thread_title(
4892    model: Arc<dyn LanguageModel>,
4893    request: LanguageModelRequest,
4894    cx: &AsyncApp,
4895) -> Result<String> {
4896    let mut title = String::new();
4897    let mut events = model.stream_completion(request, cx).await?;
4898    while let Some(event) = events.next().await {
4899        let LanguageModelCompletionEvent::Text(text) = event? else {
4900            continue;
4901        };
4902        if let Some(newline_ix) = text.find(|ch| ch == '\n' || ch == '\r') {
4903            title.push_str(&text[..newline_ix]);
4904            break;
4905        }
4906        title.push_str(&text);
4907    }
4908    Ok(title)
4909}
4910
4911pub struct TokenUsageUpdated(pub Option<acp_thread::TokenUsage>);
4912
4913impl EventEmitter<TokenUsageUpdated> for Thread {}
4914
4915pub struct TitleUpdated;
4916
4917impl EventEmitter<TitleUpdated> for Thread {}
4918
4919pub struct ModelChanged;
4920
4921impl EventEmitter<ModelChanged> for Thread {}
4922
4923/// A channel-based wrapper that delivers tool input to a running tool.
4924///
4925/// For non-streaming tools, created via `ToolInput::ready()` so `.recv()` resolves immediately.
4926/// For streaming tools, partial JSON snapshots arrive via `.recv_partial()` as the LLM streams
4927/// them, followed by the final complete input available through `.recv()`.
4928pub struct ToolInput<T> {
4929    rx: mpsc::UnboundedReceiver<ToolInputPayload<serde_json::Value>>,
4930    _phantom: PhantomData<T>,
4931}
4932
4933impl<T: DeserializeOwned> ToolInput<T> {
4934    #[cfg(any(test, feature = "test-support"))]
4935    pub fn resolved(input: impl Serialize) -> Self {
4936        let value = serde_json::to_value(input).expect("failed to serialize tool input");
4937        Self::ready(value)
4938    }
4939
4940    pub fn ready(value: serde_json::Value) -> Self {
4941        let (tx, rx) = mpsc::unbounded();
4942        tx.unbounded_send(ToolInputPayload::Full(value)).ok();
4943        Self {
4944            rx,
4945            _phantom: PhantomData,
4946        }
4947    }
4948
4949    pub fn invalid_json(error_message: String) -> Self {
4950        let (tx, rx) = mpsc::unbounded();
4951        tx.unbounded_send(ToolInputPayload::InvalidJson { error_message })
4952            .ok();
4953        Self {
4954            rx,
4955            _phantom: PhantomData,
4956        }
4957    }
4958
4959    #[cfg(any(test, feature = "test-support"))]
4960    pub fn test() -> (ToolInputSender, Self) {
4961        let (sender, input) = ToolInputSender::channel();
4962        (sender, input.cast())
4963    }
4964
4965    /// Wait for the final deserialized input, ignoring all partial updates.
4966    /// Non-streaming tools can use this to wait until the whole input is available.
4967    pub async fn recv(mut self) -> Result<T> {
4968        while let Ok(value) = self.next().await {
4969            match value {
4970                ToolInputPayload::Full(value) => return Ok(value),
4971                ToolInputPayload::Partial(_) => {}
4972                ToolInputPayload::InvalidJson { error_message } => {
4973                    return Err(anyhow!(error_message));
4974                }
4975            }
4976        }
4977        Err(anyhow!("tool input was not fully received"))
4978    }
4979
4980    pub async fn next(&mut self) -> Result<ToolInputPayload<T>> {
4981        let value = self
4982            .rx
4983            .next()
4984            .await
4985            .ok_or_else(|| anyhow!("tool input was not fully received"))?;
4986
4987        Ok(match value {
4988            ToolInputPayload::Partial(payload) => ToolInputPayload::Partial(payload),
4989            ToolInputPayload::Full(payload) => {
4990                ToolInputPayload::Full(serde_json::from_value(payload)?)
4991            }
4992            ToolInputPayload::InvalidJson { error_message } => {
4993                ToolInputPayload::InvalidJson { error_message }
4994            }
4995        })
4996    }
4997
4998    fn cast<U: DeserializeOwned>(self) -> ToolInput<U> {
4999        ToolInput {
5000            rx: self.rx,
5001            _phantom: PhantomData,
5002        }
5003    }
5004}
5005
5006pub enum ToolInputPayload<T> {
5007    Partial(serde_json::Value),
5008    Full(T),
5009    InvalidJson { error_message: String },
5010}
5011
5012pub struct ToolInputSender {
5013    has_received_final: bool,
5014    tx: mpsc::UnboundedSender<ToolInputPayload<serde_json::Value>>,
5015}
5016
5017impl ToolInputSender {
5018    pub(crate) fn channel() -> (Self, ToolInput<serde_json::Value>) {
5019        let (tx, rx) = mpsc::unbounded();
5020        let sender = Self {
5021            tx,
5022            has_received_final: false,
5023        };
5024        let input = ToolInput {
5025            rx,
5026            _phantom: PhantomData,
5027        };
5028        (sender, input)
5029    }
5030
5031    pub(crate) fn has_received_final(&self) -> bool {
5032        self.has_received_final
5033    }
5034
5035    pub fn send_partial(&mut self, payload: serde_json::Value) {
5036        self.tx
5037            .unbounded_send(ToolInputPayload::Partial(payload))
5038            .ok();
5039    }
5040
5041    pub fn send_full(&mut self, payload: serde_json::Value) {
5042        self.has_received_final = true;
5043        self.tx.unbounded_send(ToolInputPayload::Full(payload)).ok();
5044    }
5045
5046    pub fn send_invalid_json(&mut self, error_message: String) {
5047        self.has_received_final = true;
5048        self.tx
5049            .unbounded_send(ToolInputPayload::InvalidJson { error_message })
5050            .ok();
5051    }
5052}
5053
5054pub trait AgentTool
5055where
5056    Self: 'static + Sized,
5057{
5058    type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema;
5059    type Output: for<'de> Deserialize<'de> + Serialize + Into<LanguageModelToolResultContent>;
5060
5061    const NAME: &'static str;
5062
5063    fn description() -> SharedString {
5064        let schema = schemars::schema_for!(Self::Input);
5065        SharedString::new(
5066            schema
5067                .get("description")
5068                .and_then(|description| description.as_str())
5069                .unwrap_or_default(),
5070        )
5071    }
5072
5073    fn kind() -> acp::ToolKind;
5074
5075    /// The initial tool title to display. Can be updated during the tool run.
5076    fn initial_title(
5077        &self,
5078        input: Result<Self::Input, serde_json::Value>,
5079        cx: &mut App,
5080    ) -> SharedString;
5081
5082    /// Returns the JSON schema that describes the tool's input.
5083    fn input_schema(format: LanguageModelToolSchemaFormat) -> Schema {
5084        language_model::tool_schema::root_schema_for::<Self::Input>(format)
5085    }
5086
5087    /// Returns whether the tool supports streaming of tool use parameters.
5088    fn supports_input_streaming() -> bool {
5089        false
5090    }
5091
5092    /// Some tools rely on a provider for the underlying billing or other reasons.
5093    /// Allow the tool to check if they are compatible, or should be filtered out.
5094    fn supports_provider(_provider: &LanguageModelProviderId) -> bool {
5095        true
5096    }
5097
5098    /// Whether this tool may be provided to an agent in a restricted workspace.
5099    ///
5100    /// Tools that return `false` are never exposed to the model while the
5101    /// workspace is restricted, and will fail if invoked in that state.
5102    fn allow_in_restricted_mode() -> bool {
5103        true
5104    }
5105
5106    /// Runs the tool with the provided input.
5107    ///
5108    /// Returns `Result<Self::Output, Self::Output>` rather than `Result<Self::Output, anyhow::Error>`
5109    /// because tool errors are sent back to the model as tool results. This means error output must
5110    /// be structured and readable by the agent — not an arbitrary `anyhow::Error`. Returning the
5111    /// same `Output` type for both success and failure lets tools provide structured data while
5112    /// still signaling whether the invocation succeeded or failed.
5113    fn run(
5114        self: Arc<Self>,
5115        input: ToolInput<Self::Input>,
5116        event_stream: ToolCallEventStream,
5117        cx: &mut App,
5118    ) -> Task<Result<Self::Output, Self::Output>>;
5119
5120    /// Emits events for a previous execution of the tool.
5121    fn replay(
5122        &self,
5123        _input: Self::Input,
5124        _output: Self::Output,
5125        _event_stream: ToolCallEventStream,
5126        _cx: &mut App,
5127    ) -> Result<()> {
5128        Ok(())
5129    }
5130
5131    fn erase(self) -> Arc<dyn AnyAgentTool> {
5132        Arc::new(Erased(Arc::new(self)))
5133    }
5134}
5135
5136pub struct Erased<T>(T);
5137
5138pub struct AgentToolOutput {
5139    pub llm_output: Vec<LanguageModelToolResultContent>,
5140    pub raw_output: serde_json::Value,
5141}
5142
5143impl From<anyhow::Error> for AgentToolOutput {
5144    fn from(error: anyhow::Error) -> Self {
5145        let llm_output = vec![error.into()];
5146        let raw_output = serde_json::to_value(&llm_output).unwrap_or_else(|e| {
5147            log::error!("Failed to serialize tool output: {e}");
5148            serde_json::Value::Null
5149        });
5150        Self {
5151            raw_output,
5152            llm_output,
5153        }
5154    }
5155}
5156
5157pub trait AnyAgentTool {
5158    fn name(&self) -> SharedString;
5159    fn description(&self) -> SharedString;
5160    fn kind(&self) -> acp::ToolKind;
5161    fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString;
5162    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
5163    fn supports_input_streaming(&self) -> bool {
5164        false
5165    }
5166    fn supports_provider(&self, _provider: &LanguageModelProviderId) -> bool {
5167        true
5168    }
5169    fn allow_in_restricted_mode(&self) -> bool {
5170        true
5171    }
5172    /// See [`AgentTool::run`] for why this returns `Result<AgentToolOutput, AgentToolOutput>`.
5173    fn run(
5174        self: Arc<Self>,
5175        input: ToolInput<serde_json::Value>,
5176        event_stream: ToolCallEventStream,
5177        cx: &mut App,
5178    ) -> Task<Result<AgentToolOutput, AgentToolOutput>>;
5179    fn replay(
5180        &self,
5181        input: serde_json::Value,
5182        output: serde_json::Value,
5183        event_stream: ToolCallEventStream,
5184        cx: &mut App,
5185    ) -> Result<()>;
5186}
5187
5188impl<T> AnyAgentTool for Erased<Arc<T>>
5189where
5190    T: AgentTool,
5191{
5192    fn name(&self) -> SharedString {
5193        T::NAME.into()
5194    }
5195
5196    fn description(&self) -> SharedString {
5197        T::description()
5198    }
5199
5200    fn kind(&self) -> acp::ToolKind {
5201        T::kind()
5202    }
5203
5204    fn supports_input_streaming(&self) -> bool {
5205        T::supports_input_streaming()
5206    }
5207
5208    fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString {
5209        let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input);
5210        self.0.initial_title(parsed_input, _cx)
5211    }
5212
5213    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
5214        let mut json = serde_json::to_value(T::input_schema(format))?;
5215        language_model::tool_schema::adapt_schema_to_format(&mut json, format)?;
5216        Ok(json)
5217    }
5218
5219    fn supports_provider(&self, provider: &LanguageModelProviderId) -> bool {
5220        T::supports_provider(provider)
5221    }
5222
5223    fn allow_in_restricted_mode(&self) -> bool {
5224        T::allow_in_restricted_mode()
5225    }
5226
5227    fn run(
5228        self: Arc<Self>,
5229        input: ToolInput<serde_json::Value>,
5230        event_stream: ToolCallEventStream,
5231        cx: &mut App,
5232    ) -> Task<Result<AgentToolOutput, AgentToolOutput>> {
5233        let tool_input: ToolInput<T::Input> = input.cast();
5234        let task = self.0.clone().run(tool_input, event_stream, cx);
5235        cx.spawn(async move |_cx| match task.await {
5236            Ok(output) => {
5237                let raw_output = serde_json::to_value(&output).unwrap_or_else(|e| {
5238                    log::error!("Failed to serialize tool output: {e}");
5239                    serde_json::Value::Null
5240                });
5241                Ok(AgentToolOutput {
5242                    raw_output,
5243                    llm_output: vec![output.into()],
5244                })
5245            }
5246            Err(error_output) => {
5247                let raw_output = serde_json::to_value(&error_output).unwrap_or_else(|e| {
5248                    log::error!("Failed to serialize tool error output: {e}");
5249                    serde_json::Value::Null
5250                });
5251                Err(AgentToolOutput {
5252                    llm_output: vec![error_output.into()],
5253                    raw_output,
5254                })
5255            }
5256        })
5257    }
5258
5259    fn replay(
5260        &self,
5261        input: serde_json::Value,
5262        output: serde_json::Value,
5263        event_stream: ToolCallEventStream,
5264        cx: &mut App,
5265    ) -> Result<()> {
5266        let input = serde_json::from_value(input)?;
5267        let output = serde_json::from_value(output)?;
5268        self.0.replay(input, output, event_stream, cx)
5269    }
5270}
5271
5272/// Builds the ACP-facing tool call id for a tool use in the message at
5273/// `message_ix`.
5274///
5275/// Provider-issued `tool_use` ids aren't guaranteed unique across separate
5276/// request/response cycles in the same turn -- some providers reset a
5277/// per-request counter (e.g. `call_1`, `call_2`, ...), so the same raw id
5278/// can recur. Scoping by message index keeps the id stable for one tool
5279/// call's lifetime while preventing it from colliding with an unrelated one.
5280pub(crate) fn scoped_tool_call_id(
5281    message_ix: usize,
5282    tool_use_id: &LanguageModelToolUseId,
5283) -> acp::ToolCallId {
5284    // `message_ix` is non-zero-padded decimal, so the `:` delimiter is always
5285    // unambiguous -- this would break if the index were zero-padded.
5286    acp::ToolCallId::new(format!("{message_ix}:{tool_use_id}"))
5287}
5288
5289#[derive(Clone)]
5290struct ThreadEventStream(mpsc::UnboundedSender<Result<ThreadEvent>>);
5291
5292impl ThreadEventStream {
5293    fn send_user_message(&self, message: &UserMessage) {
5294        self.0
5295            .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone())))
5296            .ok();
5297    }
5298
5299    fn send_text(&self, text: &str) {
5300        self.0
5301            .unbounded_send(Ok(ThreadEvent::AgentText(text.to_string())))
5302            .ok();
5303    }
5304
5305    fn send_thinking(&self, text: &str) {
5306        self.0
5307            .unbounded_send(Ok(ThreadEvent::AgentThinking(text.to_string())))
5308            .ok();
5309    }
5310
5311    fn send_tool_call(
5312        &self,
5313        id: &acp::ToolCallId,
5314        tool_name: &str,
5315        title: SharedString,
5316        kind: acp::ToolKind,
5317        input: serde_json::Value,
5318    ) {
5319        self.0
5320            .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call(
5321                id,
5322                tool_name,
5323                title.to_string(),
5324                kind,
5325                input,
5326            ))))
5327            .ok();
5328    }
5329
5330    fn initial_tool_call(
5331        id: &acp::ToolCallId,
5332        tool_name: &str,
5333        title: String,
5334        kind: acp::ToolKind,
5335        input: serde_json::Value,
5336    ) -> acp::ToolCall {
5337        acp::ToolCall::new(id.clone(), title)
5338            .kind(kind)
5339            .raw_input(input)
5340            .meta(acp_thread::meta_with_tool_name(tool_name))
5341    }
5342
5343    fn update_tool_call_fields(
5344        &self,
5345        tool_call_id: &acp::ToolCallId,
5346        fields: acp::ToolCallUpdateFields,
5347        meta: Option<acp::Meta>,
5348    ) {
5349        self.0
5350            .unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
5351                acp::ToolCallUpdate::new(tool_call_id.clone(), fields)
5352                    .meta(meta)
5353                    .into(),
5354            )))
5355            .ok();
5356    }
5357
5358    fn resolve_tool_call_authorization(
5359        &self,
5360        tool_call_id: &acp::ToolCallId,
5361        outcome: acp_thread::SelectedPermissionOutcome,
5362    ) {
5363        self.0
5364            .unbounded_send(Ok(ThreadEvent::ToolCallAuthorizationResolved {
5365                tool_call_id: tool_call_id.clone(),
5366                outcome,
5367            }))
5368            .ok();
5369    }
5370
5371    fn send_retry(&self, status: acp_thread::RetryStatus) {
5372        self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok();
5373    }
5374
5375    fn send_context_compaction(
5376        &self,
5377        id: acp_thread::ContextCompactionId,
5378        status: acp_thread::ContextCompactionStatus,
5379    ) {
5380        self.0
5381            .unbounded_send(Ok(ThreadEvent::ContextCompaction(
5382                acp_thread::ContextCompaction {
5383                    id,
5384                    status,
5385                    summary: None,
5386                },
5387            )))
5388            .ok();
5389    }
5390
5391    fn send_context_compaction_update(
5392        &self,
5393        id: acp_thread::ContextCompactionId,
5394        summary_delta: &str,
5395    ) {
5396        self.0
5397            .unbounded_send(Ok(ThreadEvent::ContextCompactionUpdate(
5398                acp_thread::ContextCompactionUpdate {
5399                    id,
5400                    summary_delta: summary_delta.to_string(),
5401                    status: None,
5402                },
5403            )))
5404            .ok();
5405    }
5406
5407    fn update_context_compaction_status(
5408        &self,
5409        id: acp_thread::ContextCompactionId,
5410        status: acp_thread::ContextCompactionStatus,
5411    ) {
5412        self.0
5413            .unbounded_send(Ok(ThreadEvent::ContextCompactionUpdate(
5414                acp_thread::ContextCompactionUpdate {
5415                    id,
5416                    summary_delta: String::new(),
5417                    status: Some(status),
5418                },
5419            )))
5420            .ok();
5421    }
5422
5423    fn send_stop(&self, reason: acp::StopReason) {
5424        self.0.unbounded_send(Ok(ThreadEvent::Stop(reason))).ok();
5425    }
5426
5427    fn send_canceled(&self) {
5428        self.0
5429            .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled)))
5430            .ok();
5431    }
5432
5433    fn send_error(&self, error: impl Into<anyhow::Error>) {
5434        self.0.unbounded_send(Err(error.into())).ok();
5435    }
5436}
5437
5438/// The user's choice when the OS sandbox could not be created for a command
5439/// (see [`ToolCallEventStream::authorize_sandbox_fallback`]). Only the
5440/// Bubblewrap sandboxes (Linux directly, Windows via WSL) can fail to create a
5441/// sandbox, so this is gated to those platforms.
5442#[cfg(any(target_os = "linux", target_os = "windows"))]
5443#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5444pub(crate) enum SandboxFallbackDecision {
5445    /// Try creating the sandbox again (e.g. after the user installed `bwrap`).
5446    Retry,
5447    /// Run the command without a sandbox.
5448    RunUnsandboxed,
5449    /// Don't run the command at all.
5450    Deny,
5451}
5452
5453#[derive(Clone)]
5454pub struct ToolCallEventStream {
5455    tool_use_id: LanguageModelToolUseId,
5456    /// The ACP-facing id for this tool call (see [`scoped_tool_call_id`]).
5457    /// Distinct from `tool_use_id`, which is the raw, provider-issued id.
5458    tool_call_id: acp::ToolCallId,
5459    stream: ThreadEventStream,
5460    fs: Option<Arc<dyn Fs>>,
5461    cancellation_rx: watch::Receiver<bool>,
5462    /// Shared, thread-scoped sandbox grants (see [`Thread::sandbox_grants`]).
5463    sandbox_grants: Rc<RefCell<ThreadSandboxGrants>>,
5464    /// The owning thread, used to trigger a save when a "for this thread"
5465    /// sandbox grant is recorded so it survives reopening. `None` in tests and
5466    /// for streams not tied to a live thread.
5467    thread: Option<WeakEntity<Thread>>,
5468}
5469
5470impl ToolCallEventStream {
5471    #[cfg(any(test, feature = "test-support"))]
5472    pub fn test() -> (Self, ToolCallEventStreamReceiver) {
5473        let (stream, receiver, _cancellation_tx) = Self::test_with_cancellation();
5474        (stream, receiver)
5475    }
5476
5477    /// Like [`Self::test`], but the returned stream shares the provided
5478    /// thread-scoped sandbox grants. This mirrors how a real [`Thread`] builds a
5479    /// distinct event stream per tool call while sharing one set of grants, so
5480    /// tests can exercise sequences of tool calls within the same conversation.
5481    #[cfg(test)]
5482    pub(crate) fn test_with_grants(
5483        sandbox_grants: Rc<RefCell<ThreadSandboxGrants>>,
5484    ) -> (Self, ToolCallEventStreamReceiver) {
5485        let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>();
5486        let (_cancellation_tx, cancellation_rx) = watch::channel(false);
5487
5488        let stream = ToolCallEventStream::new(
5489            "test_id".into(),
5490            acp::ToolCallId::new("test_id"),
5491            ThreadEventStream(events_tx),
5492            None,
5493            cancellation_rx,
5494            sandbox_grants,
5495            None,
5496        );
5497
5498        (stream, ToolCallEventStreamReceiver(events_rx))
5499    }
5500
5501    #[cfg(any(test, feature = "test-support"))]
5502    pub fn test_with_cancellation() -> (Self, ToolCallEventStreamReceiver, watch::Sender<bool>) {
5503        let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>();
5504        let (cancellation_tx, cancellation_rx) = watch::channel(false);
5505
5506        let stream = ToolCallEventStream::new(
5507            "test_id".into(),
5508            acp::ToolCallId::new("test_id"),
5509            ThreadEventStream(events_tx),
5510            None,
5511            cancellation_rx,
5512            Rc::new(RefCell::new(ThreadSandboxGrants::default())),
5513            None,
5514        );
5515
5516        (
5517            stream,
5518            ToolCallEventStreamReceiver(events_rx),
5519            cancellation_tx,
5520        )
5521    }
5522
5523    /// Signal cancellation for this event stream. Only available in tests.
5524    #[cfg(any(test, feature = "test-support"))]
5525    pub fn signal_cancellation_with_sender(cancellation_tx: &mut watch::Sender<bool>) {
5526        cancellation_tx.send(true).ok();
5527    }
5528
5529    fn new(
5530        tool_use_id: LanguageModelToolUseId,
5531        tool_call_id: acp::ToolCallId,
5532        stream: ThreadEventStream,
5533        fs: Option<Arc<dyn Fs>>,
5534        cancellation_rx: watch::Receiver<bool>,
5535        sandbox_grants: Rc<RefCell<ThreadSandboxGrants>>,
5536        thread: Option<WeakEntity<Thread>>,
5537    ) -> Self {
5538        Self {
5539            tool_use_id,
5540            tool_call_id,
5541            stream,
5542            fs,
5543            cancellation_rx,
5544            sandbox_grants,
5545            thread,
5546        }
5547    }
5548
5549    /// Whether the owning thread is a subagent, so prompts can say "for this
5550    /// subagent" instead of "for this thread".
5551    fn is_subagent(&self, cx: &App) -> bool {
5552        self.thread
5553            .as_ref()
5554            .and_then(|thread| thread.upgrade())
5555            .is_some_and(|thread| thread.read(cx).is_subagent())
5556    }
5557
5558    /// Persist the thread so a freshly recorded "for this thread" sandbox grant
5559    /// survives a reopen. Saving is driven by the agent's `observe` on the
5560    /// thread entity, so a no-op `notify` is enough to schedule it.
5561    fn persist_thread_grants(thread: &Option<WeakEntity<Thread>>, cx: &AsyncApp) {
5562        let Some(thread) = thread else { return };
5563        cx.update(|cx| {
5564            thread.update(cx, |_thread, cx| cx.notify()).ok();
5565        });
5566    }
5567
5568    /// Returns a future that resolves when the user cancels the tool call.
5569    /// Tools should select on this alongside their main work to detect user cancellation.
5570    pub fn cancelled_by_user(&self) -> impl std::future::Future<Output = ()> + '_ {
5571        let mut rx = self.cancellation_rx.clone();
5572        async move {
5573            loop {
5574                if *rx.borrow() {
5575                    return;
5576                }
5577                if rx.changed().await.is_err() {
5578                    // Sender dropped, will never be cancelled
5579                    std::future::pending::<()>().await;
5580                }
5581            }
5582        }
5583    }
5584
5585    /// Returns true if the user has cancelled this tool call.
5586    /// This is useful for checking cancellation state after an operation completes,
5587    /// to determine if the completion was due to user cancellation.
5588    pub fn was_cancelled_by_user(&self) -> bool {
5589        *self.cancellation_rx.clone().borrow()
5590    }
5591
5592    pub fn tool_use_id(&self) -> &LanguageModelToolUseId {
5593        &self.tool_use_id
5594    }
5595
5596    pub fn update_fields(&self, fields: acp::ToolCallUpdateFields) {
5597        self.stream
5598            .update_tool_call_fields(&self.tool_call_id, fields, None);
5599    }
5600
5601    pub fn update_fields_with_meta(
5602        &self,
5603        fields: acp::ToolCallUpdateFields,
5604        meta: Option<acp::Meta>,
5605    ) {
5606        self.stream
5607            .update_tool_call_fields(&self.tool_call_id, fields, meta);
5608    }
5609
5610    pub fn resolve_authorization(&self, outcome: acp_thread::SelectedPermissionOutcome) {
5611        self.stream
5612            .resolve_tool_call_authorization(&self.tool_call_id, outcome);
5613    }
5614
5615    pub fn update_diff(&self, diff: Entity<acp_thread::Diff>) {
5616        self.stream
5617            .0
5618            .unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
5619                acp_thread::ToolCallUpdateDiff {
5620                    id: self.tool_call_id.clone(),
5621                    diff,
5622                }
5623                .into(),
5624            )))
5625            .ok();
5626    }
5627
5628    pub fn subagent_spawned(&self, id: acp::SessionId) {
5629        self.stream
5630            .0
5631            .unbounded_send(Ok(ThreadEvent::SubagentSpawned(id)))
5632            .ok();
5633    }
5634
5635    /// Authorize a third-party tool (e.g., MCP tool from a context server).
5636    ///
5637    /// Unlike built-in tools, third-party tools don't support pattern-based permissions.
5638    /// They only support `default` (allow/deny/confirm) per tool.
5639    ///
5640    /// Uses the dropdown authorization flow with two granularities:
5641    /// - "Always for <display_name> MCP tool" → sets `tools.<tool_id>.default = "allow"` or "deny"
5642    /// - "Only this time" → allow/deny once
5643    pub fn authorize_third_party_tool(
5644        &self,
5645        title: impl Into<String>,
5646        tool_id: String,
5647        display_name: String,
5648        cx: &mut App,
5649    ) -> Task<Result<()>> {
5650        let title = title.into();
5651        let options = acp_thread::PermissionOptions::Dropdown(vec![
5652            acp_thread::PermissionOptionChoice {
5653                allow: acp::PermissionOption::new(
5654                    acp::PermissionOptionId::new(format!("always_allow_mcp:{tool_id}")),
5655                    format!("Always for {display_name} MCP tool"),
5656                    acp::PermissionOptionKind::AllowAlways,
5657                ),
5658                deny: acp::PermissionOption::new(
5659                    acp::PermissionOptionId::new(format!("always_deny_mcp:{tool_id}")),
5660                    format!("Always for {display_name} MCP tool"),
5661                    acp::PermissionOptionKind::RejectAlways,
5662                ),
5663                sub_patterns: vec![],
5664            },
5665            acp_thread::PermissionOptionChoice {
5666                allow: acp::PermissionOption::new(
5667                    acp::PermissionOptionId::new("allow"),
5668                    "Only this time",
5669                    acp::PermissionOptionKind::AllowOnce,
5670                ),
5671                deny: acp::PermissionOption::new(
5672                    acp::PermissionOptionId::new("deny"),
5673                    "Only this time",
5674                    acp::PermissionOptionKind::RejectOnce,
5675                ),
5676                sub_patterns: vec![],
5677            },
5678        ]);
5679
5680        // MCP tools are gated only by tool id (no per-input pattern
5681        // matching), so we pass a single empty input value just to satisfy
5682        // `decide_permission_from_settings`' signature.
5683        let check_settings: Box<dyn Fn(&App) -> ToolPermissionDecision> =
5684            Box::new(move |cx: &App| {
5685                let settings = agent_settings::AgentSettings::get_global(cx);
5686                decide_permission_from_settings(&tool_id, &[String::new()], settings)
5687            });
5688
5689        self.run_authorization_loop(title, options, None, Some(check_settings), cx)
5690    }
5691
5692    /// Gate a tool call on user permission, driven by the agent's
5693    /// tool-permission settings.
5694    ///
5695    /// Evaluates the current settings up-front: returns `Ok(())` immediately
5696    /// if the tool is already allowed, an error if it is denied, and
5697    /// otherwise prompts the user for a decision. While a prompt is pending,
5698    /// a subscription to `SettingsStore` watches for changes (for example,
5699    /// when the user clicks "Always for …" on a sibling tool call and the
5700    /// new rule becomes globally visible). When settings change, the current
5701    /// prompt is dismissed and the decision is re-evaluated. This closes the
5702    /// gap where an "Always for …" decision on one pending tool call would
5703    /// not propagate to other pending tool calls in the same turn or in
5704    /// subagent turns.
5705    ///
5706    /// For authorizations that must always prompt regardless of settings
5707    /// (e.g. symlink-escape confirmations, sensitive settings-file edits),
5708    /// use [`Self::prompt`] instead.
5709    pub fn authorize(
5710        &self,
5711        title: impl Into<String>,
5712        context: ToolPermissionContext,
5713        cx: &mut App,
5714    ) -> Task<Result<()>> {
5715        let title = title.into();
5716        let options = context.build_permission_options();
5717
5718        let tool_name = context.tool_name.clone();
5719        let input_values = context.input_values.clone();
5720        let check_settings: Box<dyn Fn(&App) -> ToolPermissionDecision> =
5721            Box::new(move |cx: &App| {
5722                decide_permission_from_settings(
5723                    &tool_name,
5724                    &input_values,
5725                    agent_settings::AgentSettings::get_global(cx),
5726                )
5727            });
5728
5729        self.run_authorization_loop(title, options, Some(context), Some(check_settings), cx)
5730    }
5731
5732    /// Like [`Self::authorize`], but always prompts the user without
5733    /// consulting settings. Use this for authorizations that must be
5734    /// confirmed even when the user has configured `always_allow` rules —
5735    /// for example, symlink-escape confirmations or edits that target
5736    /// sensitive settings files.
5737    pub fn authorize_always_prompt(
5738        &self,
5739        title: impl Into<String>,
5740        context: ToolPermissionContext,
5741        cx: &mut App,
5742    ) -> Task<Result<()>> {
5743        let title = title.into();
5744        let options = context.build_permission_options();
5745        self.run_authorization_loop(title, options, Some(context), None, cx)
5746    }
5747
5748    /// Gate a sandbox *escalation* (network access, per-path writes, or full
5749    /// filesystem write access) on user approval.
5750    ///
5751    /// Offers the user three grant lifetimes — "once", "for the rest of this
5752    /// thread", and "always". Thread grants live in the shared, in-memory
5753    /// [`ThreadSandboxGrants`]. Always grants are persisted in agent settings
5754    /// and are also observed while a prompt is pending, matching the
5755    /// settings-driven authorization flow for regular tools.
5756    pub(crate) fn authorize_sandbox(
5757        &self,
5758        request: SandboxRequest,
5759        reason: String,
5760        cx: &mut App,
5761    ) -> Task<Result<()>> {
5762        if Self::sandbox_request_covered_by_grants(&request, &self.sandbox_grants, cx) {
5763            return Task::ready(Ok(()));
5764        }
5765
5766        let (network_hosts, network_all_hosts) = match &request.network {
5767            crate::sandboxing::NetworkRequest::None => (Vec::new(), false),
5768            crate::sandboxing::NetworkRequest::AnyHost => (Vec::new(), true),
5769            crate::sandboxing::NetworkRequest::Hosts(hosts) => {
5770                (hosts.iter().map(|host| host.to_string()).collect(), false)
5771            }
5772        };
5773        let sandbox_authorization_details = acp_thread::SandboxAuthorizationDetails {
5774            // The command stays in the tool-call title (set by the terminal
5775            // tool), so the approval card keeps showing it; the details only
5776            // describe the requested access and the agent's reason.
5777            command: None,
5778            network_hosts,
5779            network_all_hosts,
5780            allow_fs_write_all: request.allow_fs_write_all,
5781            unsandboxed: request.unsandboxed,
5782            write_paths: request.write_paths.clone(),
5783            reason,
5784        };
5785        let allow_thread_label = if self.is_subagent(cx) {
5786            "Allow for this subagent"
5787        } else {
5788            "Allow for this thread"
5789        };
5790        let options = acp_thread::PermissionOptions::Flat(vec![
5791            acp::PermissionOption::new(
5792                acp::PermissionOptionId::new(acp_thread::SandboxPermission::AllowOnce.as_id()),
5793                "Allow once",
5794                acp::PermissionOptionKind::AllowOnce,
5795            ),
5796            acp::PermissionOption::new(
5797                acp::PermissionOptionId::new(acp_thread::SandboxPermission::AllowThread.as_id()),
5798                allow_thread_label,
5799                acp::PermissionOptionKind::AllowAlways,
5800            ),
5801            acp::PermissionOption::new(
5802                acp::PermissionOptionId::new(acp_thread::SandboxPermission::AllowAlways.as_id()),
5803                "Allow always",
5804                acp::PermissionOptionKind::AllowAlways,
5805            ),
5806            acp::PermissionOption::new(
5807                acp::PermissionOptionId::new(acp_thread::SandboxPermission::Deny.as_id()),
5808                "Deny",
5809                acp::PermissionOptionKind::RejectOnce,
5810            ),
5811        ]);
5812
5813        let fs = self.fs.clone();
5814        let stream = self.stream.clone();
5815        let tool_call_id = self.tool_call_id.clone();
5816        let sandbox_grants = self.sandbox_grants.clone();
5817        let thread = self.thread.clone();
5818        let auto_allow_outcome = match auto_resolve_permission_outcome(&options, true) {
5819            Ok(outcome) => outcome,
5820            Err(error) => return Task::ready(Err(error)),
5821        };
5822        cx.spawn(async move |cx| {
5823            let (response_tx, mut response_rx) = oneshot::channel();
5824            if let Err(error) = stream
5825                .0
5826                .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
5827                    ToolCallAuthorization {
5828                        tool_call: acp::ToolCallUpdate::new(
5829                            tool_call_id.clone(),
5830                            // Leave the title untouched so the card keeps
5831                            // showing the command (matching the fallback flow).
5832                            acp::ToolCallUpdateFields::new(),
5833                        )
5834                        .meta(acp_thread::meta_with_sandbox_authorization(
5835                            sandbox_authorization_details,
5836                        )),
5837                        options,
5838                        response: response_tx,
5839                        context: None,
5840                        kind: acp_thread::AuthorizationKind::PermissionGrant,
5841                    },
5842                )))
5843            {
5844                log::error!("Failed to send sandbox authorization: {error}");
5845                return Err(anyhow!("Failed to send sandbox authorization: {error}"));
5846            }
5847
5848            let (mut settings_tx, mut settings_rx) = watch::channel(());
5849            let _settings_subscription = cx.update(|cx| {
5850                cx.observe_global::<SettingsStore>(move |_cx| {
5851                    settings_tx.send(()).ok();
5852                })
5853            });
5854
5855            loop {
5856                let settings_changed = async {
5857                    if settings_rx.changed().await.is_err() {
5858                        std::future::pending::<()>().await;
5859                    }
5860                };
5861                futures::select_biased! {
5862                    outcome = (&mut response_rx).fuse() => {
5863                        let outcome = outcome
5864                            .map_err(|_| anyhow!("authorization channel closed"))?;
5865                        return Self::handle_sandbox_permission_outcome(
5866                            &outcome,
5867                            &request,
5868                            sandbox_grants.clone(),
5869                            thread.clone(),
5870                            fs.clone(),
5871                            cx,
5872                        );
5873                    }
5874                    _ = settings_changed.fuse() => {
5875                        if cx.update(|cx| Self::sandbox_request_covered_by_grants(
5876                            &request,
5877                            &sandbox_grants,
5878                            cx,
5879                        )) {
5880                            drop(response_rx);
5881                            stream.resolve_tool_call_authorization(
5882                                &tool_call_id,
5883                                auto_allow_outcome.clone(),
5884                            );
5885                            return Ok(());
5886                        }
5887                    }
5888                }
5889            }
5890        })
5891    }
5892
5893    fn sandbox_request_covered_by_grants(
5894        request: &SandboxRequest,
5895        sandbox_grants: &Rc<RefCell<ThreadSandboxGrants>>,
5896        cx: &App,
5897    ) -> bool {
5898        let settings = AgentSettings::get_global(cx);
5899        sandbox_grants
5900            .borrow()
5901            .covers_with_persistent(request, &settings.sandbox_permissions)
5902    }
5903
5904    fn handle_sandbox_permission_outcome(
5905        outcome: &acp_thread::SelectedPermissionOutcome,
5906        request: &SandboxRequest,
5907        sandbox_grants: Rc<RefCell<ThreadSandboxGrants>>,
5908        thread: Option<WeakEntity<Thread>>,
5909        fs: Option<Arc<dyn Fs>>,
5910        cx: &AsyncApp,
5911    ) -> Result<()> {
5912        debug_assert!(
5913            outcome.params.is_none(),
5914            "unexpected params for sandbox permission"
5915        );
5916
5917        match acp_thread::SandboxPermission::from_id(outcome.option_id.0.as_ref()) {
5918            Some(acp_thread::SandboxPermission::AllowOnce) => Ok(()),
5919            Some(acp_thread::SandboxPermission::AllowThread) => {
5920                sandbox_grants.borrow_mut().record(request);
5921                Self::persist_thread_grants(&thread, cx);
5922                Ok(())
5923            }
5924            Some(acp_thread::SandboxPermission::AllowAlways) => {
5925                Self::persist_sandbox_always_permission(request, fs, cx);
5926                Ok(())
5927            }
5928            Some(acp_thread::SandboxPermission::Deny) => {
5929                Err(anyhow!("Permission to run tool denied by user"))
5930            }
5931            None => {
5932                let other = outcome.option_id.0.as_ref();
5933                debug_assert!(false, "unexpected sandbox permission option_id: {other}");
5934                Err(anyhow!("Permission to run tool denied by user"))
5935            }
5936        }
5937    }
5938
5939    fn persist_sandbox_always_permission(
5940        request: &SandboxRequest,
5941        fs: Option<Arc<dyn Fs>>,
5942        cx: &AsyncApp,
5943    ) {
5944        let Some(fs) = fs else {
5945            log::error!(
5946                "Cannot persist \"allow always\" sandbox permission: no filesystem available"
5947            );
5948            return;
5949        };
5950
5951        let request = request.clone();
5952        cx.update(|cx| {
5953            update_settings_file(fs, cx, move |settings, _| {
5954                let agent = settings.agent.get_or_insert_default();
5955                match &request.network {
5956                    crate::sandboxing::NetworkRequest::None => {}
5957                    crate::sandboxing::NetworkRequest::AnyHost => {
5958                        agent.allow_sandbox_all_hosts();
5959                    }
5960                    crate::sandboxing::NetworkRequest::Hosts(hosts) => {
5961                        // Rebuild the persisted list with subsumption pruning
5962                        // so granting `*.github.com` retires a previously
5963                        // persisted `api.github.com` instead of accumulating
5964                        // redundant entries. Unparsable hand-edited entries
5965                        // are preserved untouched.
5966                        let mut patterns = Vec::new();
5967                        let mut unparsable = Vec::new();
5968                        for raw in agent.sandbox_network_hosts() {
5969                            match http_proxy::HostPattern::parse(raw) {
5970                                Ok(pattern) => {
5971                                    crate::sandboxing::insert_host_pattern(&mut patterns, pattern)
5972                                }
5973                                Err(_) => unparsable.push(raw.clone()),
5974                            }
5975                        }
5976                        for host in hosts {
5977                            crate::sandboxing::insert_host_pattern(&mut patterns, host.clone());
5978                        }
5979                        let mut host_strings = unparsable;
5980                        host_strings.extend(patterns.iter().map(|pattern| pattern.to_string()));
5981                        agent.set_sandbox_network_hosts(host_strings);
5982                    }
5983                }
5984
5985                if request.allow_fs_write_all {
5986                    agent.allow_sandbox_fs_write_all();
5987                }
5988                if request.unsandboxed {
5989                    agent.allow_sandbox_unsandboxed();
5990                }
5991                for path in request.write_paths {
5992                    agent.add_sandbox_write_path(path);
5993                }
5994            });
5995        });
5996    }
5997
5998    /// The sandbox permissions to actually enforce for a command: the union
5999    /// of this command's `request`, everything granted "for the rest of the
6000    /// conversation", and persistent "allow always" sandbox grants.
6001    ///
6002    /// Callers must apply this to the enforced sandbox policy (rather than
6003    /// the raw `request`) so standing grants keep working for later commands
6004    /// that write to a previously approved path without re-requesting it.
6005    pub(crate) fn effective_sandbox_request(
6006        &self,
6007        request: &SandboxRequest,
6008        persistent: &agent_settings::SandboxPermissions,
6009    ) -> SandboxRequest {
6010        self.sandbox_grants
6011            .borrow()
6012            .effective_with_persistent(request, persistent)
6013    }
6014
6015    /// Whether the user allowed running commands unsandboxed for the rest of
6016    /// the thread (distinct from the persistent `allow_unsandboxed` setting).
6017    pub(crate) fn sandbox_fallback_granted_for_thread(&self) -> bool {
6018        self.sandbox_grants.borrow().fallback_granted_for_thread()
6019    }
6020
6021    /// Whether the user approved a model-requested `unsandboxed: true` escape
6022    /// for the rest of this thread. Like the fallback grant, this makes every
6023    /// command in the thread run without a sandbox.
6024    pub(crate) fn unsandboxed_granted_for_thread(&self) -> bool {
6025        self.sandbox_grants.borrow().unsandboxed_granted()
6026    }
6027
6028    /// Whether unsandboxed access is currently in effect: granted for this
6029    /// thread (a model-requested `unsandboxed` escape or a sandbox-creation
6030    /// fallback) or configured persistently via `allow_unsandboxed`. When true,
6031    /// commands already run without any OS sandbox, so per-host network grants
6032    /// no longer provide isolation and callers may skip host authorization
6033    /// entirely.
6034    pub(crate) fn unsandboxed_access_granted(&self, cx: &App) -> bool {
6035        self.unsandboxed_granted_for_thread()
6036            || self.sandbox_fallback_granted_for_thread()
6037            || AgentSettings::get_global(cx)
6038                .sandbox_permissions
6039                .allow_unsandboxed
6040    }
6041
6042    /// Ask the user how to proceed when the OS sandbox could not be created
6043    /// for a command (for example, `bwrap` is missing or user namespaces are
6044    /// disabled).
6045    ///
6046    /// Unlike [`Self::authorize_sandbox`] — which gates a model-requested
6047    /// *escalation* — this surfaces a *system limitation*: the sandbox failed,
6048    /// so the prompt explains why (`reason`) and lets the user retry, run the
6049    /// command unsandboxed (once / for this thread / always), or deny it. The
6050    /// "for this thread" choice is recorded in the in-memory thread grants and
6051    /// "always" is persisted as the `allow_unsandboxed` setting. Only the
6052    /// Bubblewrap sandboxes (Linux directly, Windows via WSL) can fail to
6053    /// create a sandbox, so this is gated to those platforms.
6054    ///
6055    /// `retries` is how many times the user has already pressed Retry for this
6056    /// command; it's shown on the button so repeated presses visibly advance
6057    /// ("Retry", then "Retry (attempt 1)", "Retry (attempt 2)", …).
6058    #[cfg(any(target_os = "linux", target_os = "windows"))]
6059    pub(crate) fn authorize_sandbox_fallback(
6060        &self,
6061        command: Option<String>,
6062        reason: String,
6063        docs_section: Option<String>,
6064        retries: usize,
6065        cx: &mut App,
6066    ) -> Task<Result<SandboxFallbackDecision>> {
6067        let details = acp_thread::SandboxFallbackAuthorizationDetails {
6068            command,
6069            reason,
6070            docs_section,
6071        };
6072        let retry_label = if retries == 0 {
6073            "Retry".to_string()
6074        } else {
6075            format!("Retry (attempt {retries})")
6076        };
6077        let allow_thread_label = if self.is_subagent(cx) {
6078            "Run without sandbox for this subagent"
6079        } else {
6080            "Run without sandbox for this thread"
6081        };
6082        let options = acp_thread::PermissionOptions::Flat(vec![
6083            // Retry isn't an allow/deny choice; the UI renders it with its own
6084            // icon and we dispatch on the option id, so the kind here only
6085            // governs keybindings. Use `RejectAlways` (which has none) so the
6086            // "allow once" shortcut maps to "Run without sandbox once" rather
6087            // than to Retry.
6088            acp::PermissionOption::new(
6089                acp::PermissionOptionId::new(acp_thread::SANDBOX_FALLBACK_RETRY_OPTION_ID),
6090                retry_label,
6091                acp::PermissionOptionKind::RejectAlways,
6092            ),
6093            acp::PermissionOption::new(
6094                acp::PermissionOptionId::new(acp_thread::SandboxPermission::AllowOnce.as_id()),
6095                "Run without sandbox once",
6096                acp::PermissionOptionKind::AllowOnce,
6097            ),
6098            acp::PermissionOption::new(
6099                acp::PermissionOptionId::new(acp_thread::SandboxPermission::AllowThread.as_id()),
6100                allow_thread_label,
6101                acp::PermissionOptionKind::AllowAlways,
6102            ),
6103            acp::PermissionOption::new(
6104                acp::PermissionOptionId::new(acp_thread::SandboxPermission::AllowAlways.as_id()),
6105                "Always run without sandbox",
6106                acp::PermissionOptionKind::AllowAlways,
6107            ),
6108            acp::PermissionOption::new(
6109                acp::PermissionOptionId::new(acp_thread::SandboxPermission::Deny.as_id()),
6110                "Deny",
6111                acp::PermissionOptionKind::RejectOnce,
6112            ),
6113        ]);
6114
6115        let fs = self.fs.clone();
6116        let stream = self.stream.clone();
6117        let tool_call_id = self.tool_call_id.clone();
6118        let sandbox_grants = self.sandbox_grants.clone();
6119        let thread = self.thread.clone();
6120        cx.spawn(async move |cx| {
6121            let (response_tx, response_rx) = oneshot::channel();
6122            if let Err(error) = stream
6123                .0
6124                .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
6125                    ToolCallAuthorization {
6126                        // Deliberately leave the tool-call title untouched so
6127                        // the card keeps showing the *command* (not the
6128                        // failure reason): it's critical the user can see what
6129                        // they're approving to run unsandboxed. The reason is
6130                        // surfaced separately by the fallback details / warning.
6131                        tool_call: acp::ToolCallUpdate::new(
6132                            tool_call_id.clone(),
6133                            acp::ToolCallUpdateFields::new(),
6134                        )
6135                        .meta(
6136                            acp_thread::meta_with_sandbox_fallback_authorization(details),
6137                        ),
6138                        options,
6139                        response: response_tx,
6140                        context: None,
6141                        kind: acp_thread::AuthorizationKind::ActionChoice,
6142                    },
6143                )))
6144            {
6145                log::error!("Failed to send sandbox fallback authorization: {error}");
6146                return Err(anyhow!(
6147                    "Failed to send sandbox fallback authorization: {error}"
6148                ));
6149            }
6150
6151            let outcome = response_rx
6152                .await
6153                .map_err(|_| anyhow!("authorization channel closed"))?;
6154
6155            let option_id = outcome.option_id.0.as_ref();
6156            if option_id == acp_thread::SANDBOX_FALLBACK_RETRY_OPTION_ID {
6157                return Ok(SandboxFallbackDecision::Retry);
6158            }
6159            match acp_thread::SandboxPermission::from_id(option_id) {
6160                Some(acp_thread::SandboxPermission::AllowOnce) => {
6161                    Ok(SandboxFallbackDecision::RunUnsandboxed)
6162                }
6163                Some(acp_thread::SandboxPermission::AllowThread) => {
6164                    sandbox_grants.borrow_mut().record_fallback();
6165                    Self::persist_thread_grants(&thread, cx);
6166                    Ok(SandboxFallbackDecision::RunUnsandboxed)
6167                }
6168                Some(acp_thread::SandboxPermission::AllowAlways) => {
6169                    sandbox_grants.borrow_mut().record_fallback();
6170                    Self::persist_thread_grants(&thread, cx);
6171                    Self::persist_sandbox_unsandboxed_permission(fs, cx);
6172                    Ok(SandboxFallbackDecision::RunUnsandboxed)
6173                }
6174                Some(acp_thread::SandboxPermission::Deny) => Ok(SandboxFallbackDecision::Deny),
6175                None => {
6176                    let other = option_id;
6177                    debug_assert!(false, "unexpected sandbox fallback option_id: {other}");
6178                    Ok(SandboxFallbackDecision::Deny)
6179                }
6180            }
6181        })
6182    }
6183
6184    /// Persist the `allow_unsandboxed` setting. Going forward this turns
6185    /// sandboxing off for the model-facing surface: later turns expose the
6186    /// plain `terminal` tool (with no sandbox prompt section) and commands run
6187    /// without an OS sandbox. On Windows, WSL sandbox setup is skipped.
6188    #[cfg(any(target_os = "linux", target_os = "windows"))]
6189    fn persist_sandbox_unsandboxed_permission(fs: Option<Arc<dyn Fs>>, cx: &AsyncApp) {
6190        let Some(fs) = fs else {
6191            log::error!(
6192                "Cannot persist \"allow always\" unsandboxed permission: no filesystem available"
6193            );
6194            return;
6195        };
6196        cx.update(|cx| {
6197            update_settings_file(fs, cx, move |settings, _| {
6198                settings
6199                    .agent
6200                    .get_or_insert_default()
6201                    .allow_sandbox_unsandboxed();
6202            });
6203        });
6204    }
6205
6206    /// Prompts the user to choose between an explicit set of actions and
6207    /// returns the chosen `option_id`.
6208    ///
6209    /// Unlike [`Self::authorize`] / [`Self::authorize_always_prompt`], this
6210    /// does not interpret the user's choice as a permission grant — callers
6211    /// are responsible for handling each `option_id` explicitly. Use this
6212    /// when a tool needs the user to pick between several side-effecting
6213    /// actions (for example, "Save" vs "Discard" for a dirty buffer).
6214    pub fn prompt_for_decision(
6215        &self,
6216        title: Option<String>,
6217        message: Option<String>,
6218        options: Vec<acp::PermissionOption>,
6219        cx: &mut App,
6220    ) -> Task<Result<acp::PermissionOptionId>> {
6221        let options = acp_thread::PermissionOptions::Flat(options);
6222        let stream = self.stream.clone();
6223        let tool_call_id = self.tool_call_id.clone();
6224        cx.spawn(async move |_cx| {
6225            let mut fields = acp::ToolCallUpdateFields::new();
6226            if let Some(title) = title {
6227                fields = fields.title(title);
6228            }
6229            if let Some(message) = message {
6230                fields = fields.content(vec![acp::ToolCallContent::from(message)]);
6231            }
6232
6233            let (response_tx, response_rx) = oneshot::channel();
6234            if let Err(error) = stream
6235                .0
6236                .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
6237                    ToolCallAuthorization {
6238                        tool_call: acp::ToolCallUpdate::new(tool_call_id.clone(), fields),
6239                        options,
6240                        response: response_tx,
6241                        context: None,
6242                        kind: acp_thread::AuthorizationKind::ActionChoice,
6243                    },
6244                )))
6245            {
6246                log::error!("Failed to send tool call decision prompt: {error}");
6247                return Err(anyhow!("Failed to send tool call decision prompt: {error}"));
6248            }
6249
6250            let outcome = response_rx
6251                .await
6252                .map_err(|_| anyhow!("authorization channel closed"))?;
6253            Ok(outcome.option_id)
6254        })
6255    }
6256
6257    /// Prompts the user for authorization.
6258    ///
6259    /// When `check_settings` is `Some`, this gate is settings-driven: the
6260    /// settings are evaluated up-front (an Allow or Deny result resolves the
6261    /// task immediately without prompting), and while a prompt is pending a
6262    /// `SettingsStore` subscription watches for changes. A subsequent Allow
6263    /// or Deny dismisses the prompt UI and resolves the task without user
6264    /// interaction.
6265    ///
6266    /// When `check_settings` is `None`, the user is always prompted and
6267    /// settings changes are ignored. This suits prompts that aren't
6268    /// settings-driven (e.g. symlink-escape confirmations).
6269    fn run_authorization_loop(
6270        &self,
6271        title: String,
6272        options: acp_thread::PermissionOptions,
6273        context: Option<ToolPermissionContext>,
6274        check_settings: Option<Box<dyn Fn(&App) -> ToolPermissionDecision>>,
6275        cx: &mut App,
6276    ) -> Task<Result<()>> {
6277        // Short-circuit when current settings yield a definitive answer.
6278        if let Some(check) = check_settings.as_ref() {
6279            match check(cx) {
6280                ToolPermissionDecision::Allow => return Task::ready(Ok(())),
6281                ToolPermissionDecision::Deny(reason) => {
6282                    return Task::ready(Err(anyhow!(reason)));
6283                }
6284                ToolPermissionDecision::Confirm => {}
6285            }
6286        }
6287
6288        let fs = self.fs.clone();
6289        let stream = self.stream.clone();
6290        let tool_call_id = self.tool_call_id.clone();
6291        let auto_resolution_outcomes = if check_settings.is_some() {
6292            match (
6293                auto_resolve_permission_outcome(&options, true),
6294                auto_resolve_permission_outcome(&options, false),
6295            ) {
6296                (Ok(allow), Ok(deny)) => Some((allow, deny)),
6297                (Err(error), _) | (_, Err(error)) => return Task::ready(Err(error)),
6298            }
6299        } else {
6300            None
6301        };
6302        cx.spawn(async move |cx| {
6303            let (response_tx, mut response_rx) = oneshot::channel();
6304            if let Err(error) = stream
6305                .0
6306                .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
6307                    ToolCallAuthorization {
6308                        tool_call: acp::ToolCallUpdate::new(
6309                            tool_call_id.clone(),
6310                            acp::ToolCallUpdateFields::new().title(title),
6311                        ),
6312                        options,
6313                        response: response_tx,
6314                        context,
6315                        kind: acp_thread::AuthorizationKind::PermissionGrant,
6316                    },
6317                )))
6318            {
6319                log::error!("Failed to send tool call authorization: {error}");
6320                return Err(anyhow!("Failed to send tool call authorization: {error}"));
6321            }
6322
6323            let Some(check_settings) = check_settings else {
6324                let outcome = response_rx
6325                    .await
6326                    .map_err(|_| anyhow!("authorization channel closed"))?;
6327
6328                return Self::persist_permission_outcome(&outcome, fs, cx);
6329            };
6330            let Some((auto_allow_outcome, auto_deny_outcome)) = auto_resolution_outcomes else {
6331                return Err(anyhow!("missing auto-resolution outcomes"));
6332            };
6333
6334            let (mut settings_tx, mut settings_rx) = watch::channel(());
6335            let _settings_subscription = cx.update(|cx| {
6336                cx.observe_global::<SettingsStore>(move |_cx| {
6337                    settings_tx.send(()).ok();
6338                })
6339            });
6340
6341            // Race the user's response against settings changes. On each
6342            // settings change, re-evaluate `check_settings`: if it now
6343            // yields a definitive Allow or Deny, resolve the prompt
6344            // without user interaction. Otherwise keep waiting on the
6345            // same prompt.
6346            loop {
6347                let settings_changed = async {
6348                    if settings_rx.changed().await.is_err() {
6349                        std::future::pending::<()>().await;
6350                    }
6351                };
6352                futures::select_biased! {
6353                    outcome = (&mut response_rx).fuse() => {
6354                        let outcome = outcome
6355                            .map_err(|_| anyhow!("authorization channel closed"))?;
6356                        return Self::persist_permission_outcome(&outcome, fs.clone(), cx);
6357                    }
6358                    _ = settings_changed.fuse() => {
6359                        // On auto-resolve, we dismiss the prompt UI by
6360                        // resolving the tool call's `WaitingForConfirmation`
6361                        // status with an internal selected outcome. Dropping
6362                        // `response_rx` prevents the synthetic response from
6363                        // being delivered back into this loop.
6364                        match cx.update(|cx| check_settings(cx)) {
6365                            ToolPermissionDecision::Allow => {
6366                                drop(response_rx);
6367                                stream.resolve_tool_call_authorization(
6368                                    &tool_call_id,
6369                                    auto_allow_outcome.clone(),
6370                                );
6371                                return Ok(());
6372                            }
6373                            ToolPermissionDecision::Deny(reason) => {
6374                                drop(response_rx);
6375                                stream.resolve_tool_call_authorization(
6376                                    &tool_call_id,
6377                                    auto_deny_outcome.clone(),
6378                                );
6379                                return Err(anyhow!(reason));
6380                            }
6381                            ToolPermissionDecision::Confirm => continue,
6382                        }
6383                    }
6384                }
6385            }
6386        })
6387    }
6388
6389    /// Interprets a `SelectedPermissionOutcome` and persists any settings changes.
6390    /// Returns `true` if the tool call should be allowed, `false` if denied.
6391    fn persist_permission_outcome(
6392        outcome: &acp_thread::SelectedPermissionOutcome,
6393        fs: Option<Arc<dyn Fs>>,
6394        cx: &AsyncApp,
6395    ) -> Result<()> {
6396        let option_id = outcome.option_id.0.as_ref();
6397        let err = || Err(anyhow!("Permission to run tool denied by user"));
6398
6399        let always_permission = option_id
6400            .strip_prefix("always_allow:")
6401            .map(|tool| (tool, ToolPermissionMode::Allow))
6402            .or_else(|| {
6403                option_id
6404                    .strip_prefix("always_deny:")
6405                    .map(|tool| (tool, ToolPermissionMode::Deny))
6406            })
6407            .or_else(|| {
6408                option_id
6409                    .strip_prefix("always_allow_mcp:")
6410                    .map(|tool| (tool, ToolPermissionMode::Allow))
6411            })
6412            .or_else(|| {
6413                option_id
6414                    .strip_prefix("always_deny_mcp:")
6415                    .map(|tool| (tool, ToolPermissionMode::Deny))
6416            });
6417
6418        if let Some((tool, mode)) = always_permission {
6419            let params = outcome.params.as_ref();
6420            Self::persist_always_permission(tool, mode, params, fs, cx);
6421            return if mode == ToolPermissionMode::Allow {
6422                Ok(())
6423            } else {
6424                err()
6425            };
6426        }
6427
6428        // Handle simple "allow" / "deny" (once, no persistence)
6429        if option_id == "allow" || option_id == "deny" {
6430            debug_assert!(
6431                outcome.params.is_none(),
6432                "unexpected params for once-only permission"
6433            );
6434            return if option_id == "allow" { Ok(()) } else { err() };
6435        }
6436
6437        debug_assert!(false, "unexpected permission option_id: {option_id}");
6438
6439        err()
6440    }
6441
6442    /// Persists an "always allow" or "always deny" permission, using sub_patterns
6443    /// from params when present.
6444    fn persist_always_permission(
6445        tool: &str,
6446        mode: ToolPermissionMode,
6447        params: Option<&acp_thread::SelectedPermissionParams>,
6448        fs: Option<Arc<dyn Fs>>,
6449        cx: &AsyncApp,
6450    ) {
6451        let Some(fs) = fs else {
6452            return;
6453        };
6454
6455        match params {
6456            Some(acp_thread::SelectedPermissionParams::Terminal {
6457                patterns: sub_patterns,
6458            }) => {
6459                debug_assert!(
6460                    !sub_patterns.is_empty(),
6461                    "empty sub_patterns for tool {tool} — callers should pass None instead"
6462                );
6463                let tool = tool.to_string();
6464                let sub_patterns = sub_patterns.clone();
6465                cx.update(|cx| {
6466                    update_settings_file(fs, cx, move |settings, _| {
6467                        let agent = settings.agent.get_or_insert_default();
6468                        for pattern in sub_patterns {
6469                            match mode {
6470                                ToolPermissionMode::Allow => {
6471                                    agent.add_tool_allow_pattern(&tool, pattern);
6472                                }
6473                                ToolPermissionMode::Deny => {
6474                                    agent.add_tool_deny_pattern(&tool, pattern);
6475                                }
6476                                // If there's no matching pattern this will
6477                                // default to confirm, so falling through is
6478                                // fine here.
6479                                ToolPermissionMode::Confirm => (),
6480                            }
6481                        }
6482                    });
6483                });
6484            }
6485            None => {
6486                let tool = tool.to_string();
6487                cx.update(|cx| {
6488                    update_settings_file(fs, cx, move |settings, _| {
6489                        settings
6490                            .agent
6491                            .get_or_insert_default()
6492                            .set_tool_default_permission(&tool, mode);
6493                    });
6494                });
6495            }
6496        }
6497    }
6498}
6499
6500#[cfg(any(test, feature = "test-support"))]
6501pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver<Result<ThreadEvent>>);
6502
6503#[cfg(any(test, feature = "test-support"))]
6504impl ToolCallEventStreamReceiver {
6505    pub async fn expect_authorization(&mut self) -> ToolCallAuthorization {
6506        let event = self.0.next().await;
6507        if let Some(Ok(ThreadEvent::ToolCallAuthorization(auth))) = event {
6508            auth
6509        } else {
6510            panic!("Expected ToolCallAuthorization but got: {:?}", event);
6511        }
6512    }
6513
6514    pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields {
6515        let event = self.0.next().await;
6516        if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
6517            update,
6518        )))) = event
6519        {
6520            update.fields
6521        } else {
6522            panic!("Expected update fields but got: {:?}", event);
6523        }
6524    }
6525
6526    pub async fn expect_authorization_resolved(
6527        &mut self,
6528    ) -> (acp::ToolCallId, acp_thread::SelectedPermissionOutcome) {
6529        let event = self.0.next().await;
6530        if let Some(Ok(ThreadEvent::ToolCallAuthorizationResolved {
6531            tool_call_id,
6532            outcome,
6533        })) = event
6534        {
6535            (tool_call_id, outcome)
6536        } else {
6537            panic!("Expected authorization resolved but got: {:?}", event);
6538        }
6539    }
6540
6541    pub async fn expect_diff(&mut self) -> Entity<acp_thread::Diff> {
6542        let event = self.0.next().await;
6543        if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff(
6544            update,
6545        )))) = event
6546        {
6547            update.diff
6548        } else {
6549            panic!("Expected diff but got: {:?}", event);
6550        }
6551    }
6552
6553    pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
6554        let event = self.0.next().await;
6555        if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
6556            update,
6557        )))) = event
6558        {
6559            update.terminal
6560        } else {
6561            panic!("Expected terminal but got: {:?}", event);
6562        }
6563    }
6564}
6565
6566#[cfg(any(test, feature = "test-support"))]
6567impl std::ops::Deref for ToolCallEventStreamReceiver {
6568    type Target = mpsc::UnboundedReceiver<Result<ThreadEvent>>;
6569
6570    fn deref(&self) -> &Self::Target {
6571        &self.0
6572    }
6573}
6574
6575#[cfg(any(test, feature = "test-support"))]
6576impl std::ops::DerefMut for ToolCallEventStreamReceiver {
6577    fn deref_mut(&mut self) -> &mut Self::Target {
6578        &mut self.0
6579    }
6580}
6581
6582impl From<&str> for UserMessageContent {
6583    fn from(text: &str) -> Self {
6584        Self::Text(text.into())
6585    }
6586}
6587
6588impl From<String> for UserMessageContent {
6589    fn from(text: String) -> Self {
6590        Self::Text(text)
6591    }
6592}
6593
6594impl UserMessageContent {
6595    pub fn from_content_block(value: acp::ContentBlock, path_style: PathStyle) -> Self {
6596        match value {
6597            acp::ContentBlock::Text(text_content) => Self::Text(text_content.text),
6598            acp::ContentBlock::Image(image_content) => Self::Image(convert_image(image_content)),
6599            acp::ContentBlock::Audio(_) => {
6600                // TODO
6601                Self::Text("[audio]".to_string())
6602            }
6603            acp::ContentBlock::ResourceLink(resource_link) => {
6604                match MentionUri::parse(&resource_link.uri, path_style) {
6605                    Ok(uri) => Self::Mention {
6606                        uri,
6607                        content: SharedString::default(),
6608                    },
6609                    Err(err) => {
6610                        log::error!("Failed to parse mention link: {}", err);
6611                        Self::Text(format!("[{}]({})", resource_link.name, resource_link.uri))
6612                    }
6613                }
6614            }
6615            acp::ContentBlock::Resource(resource) => match resource.resource {
6616                acp::EmbeddedResourceResource::TextResourceContents(resource) => {
6617                    match MentionUri::parse(&resource.uri, path_style) {
6618                        Ok(uri) => Self::Mention {
6619                            uri,
6620                            content: resource.text.into(),
6621                        },
6622                        Err(err) => {
6623                            log::error!("Failed to parse mention link: {}", err);
6624                            Self::Text(
6625                                MarkdownCodeBlock {
6626                                    tag: &resource.uri,
6627                                    text: &resource.text,
6628                                }
6629                                .to_string(),
6630                            )
6631                        }
6632                    }
6633                }
6634                acp::EmbeddedResourceResource::BlobResourceContents(_) => {
6635                    // TODO
6636                    Self::Text("[blob]".to_string())
6637                }
6638                other => {
6639                    log::warn!("Unexpected content type: {:?}", other);
6640                    Self::Text("[unknown]".to_string())
6641                }
6642            },
6643            other => {
6644                log::warn!("Unexpected content type: {:?}", other);
6645                Self::Text("[unknown]".to_string())
6646            }
6647        }
6648    }
6649}
6650
6651impl From<UserMessageContent> for acp::ContentBlock {
6652    fn from(content: UserMessageContent) -> Self {
6653        match content {
6654            UserMessageContent::Text(text) => text.into(),
6655            UserMessageContent::Image(image) => {
6656                acp::ContentBlock::Image(acp::ImageContent::new(image.source, "image/png"))
6657            }
6658            UserMessageContent::Mention { uri, content } => acp::ContentBlock::Resource(
6659                acp::EmbeddedResource::new(acp::EmbeddedResourceResource::TextResourceContents(
6660                    acp::TextResourceContents::new(content, uri.to_uri().to_string()),
6661                )),
6662            ),
6663        }
6664    }
6665}
6666
6667fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage {
6668    LanguageModelImage {
6669        source: image_content.data.into(),
6670    }
6671}
6672
6673#[cfg(test)]
6674mod tests {
6675    use super::*;
6676    use gpui::TestAppContext;
6677    use language_model::LanguageModelToolUseId;
6678    use language_model::fake_provider::FakeLanguageModel;
6679    use serde_json::json;
6680    use settings::LanguageModelProviderSetting;
6681    use std::sync::Arc;
6682
6683    async fn setup_thread_for_test(cx: &mut TestAppContext) -> (Entity<Thread>, ThreadEventStream) {
6684        cx.update(|cx| {
6685            let settings_store = settings::SettingsStore::test(cx);
6686            cx.set_global(settings_store);
6687
6688            LanguageModelRegistry::test(cx);
6689        });
6690
6691        let fs = fs::FakeFs::new(cx.background_executor.clone());
6692        let templates = Templates::new();
6693        let project = Project::test(fs.clone(), [], cx).await;
6694
6695        cx.update(|cx| {
6696            let project_context = cx.new(|_cx| prompt_store::ProjectContext::default());
6697            let context_server_store = project.read(cx).context_server_store();
6698            let context_server_registry =
6699                cx.new(|cx| ContextServerRegistry::new(context_server_store, cx));
6700
6701            let thread = cx.new(|cx| {
6702                Thread::new(
6703                    project,
6704                    project_context,
6705                    context_server_registry,
6706                    templates,
6707                    None,
6708                    cx,
6709                )
6710            });
6711
6712            let (event_tx, _event_rx) = mpsc::unbounded();
6713            let event_stream = ThreadEventStream(event_tx);
6714
6715            (thread, event_stream)
6716        })
6717    }
6718
6719    fn set_auto_compact_settings(cx: &mut App, auto_compact: agent_settings::AutoCompactSettings) {
6720        let mut settings = AgentSettings::get_global(cx).clone();
6721        settings.auto_compact = auto_compact;
6722        AgentSettings::override_global(settings, cx);
6723    }
6724
6725    fn set_registry_compaction_model(cx: &mut App, model: Option<Arc<dyn LanguageModel>>) {
6726        use language_model::fake_provider::FakeLanguageModelProvider;
6727        use language_model::{ConfiguredModel, LanguageModelProvider};
6728        LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
6729            let configured = model.map(|m| ConfiguredModel {
6730                provider: Arc::new(FakeLanguageModelProvider::default())
6731                    as Arc<dyn LanguageModelProvider>,
6732                model: m,
6733            });
6734            registry.set_compaction_model(configured, cx);
6735        });
6736    }
6737
6738    #[test]
6739    fn test_summary_compaction_renders_for_request_and_markdown() {
6740        let message = Message::Compaction(CompactionInfo::Summary("Older context".into()));
6741
6742        assert_eq!(message.role(), Role::User);
6743        assert_eq!(message.to_markdown(), "--- Context Compacted ---\n");
6744
6745        let request_messages = message.to_request();
6746        assert_eq!(request_messages.len(), 1);
6747        assert_eq!(request_messages[0].role, Role::User);
6748        assert!(!request_messages[0].cache);
6749        assert_eq!(request_messages[0].reasoning_details, None);
6750        assert_eq!(request_messages[0].content.len(), 1);
6751        let language_model::MessageContent::Text(text) = &request_messages[0].content[0] else {
6752            panic!("expected text summary context");
6753        };
6754        assert_eq!(
6755            text.as_str(),
6756            "The previous conversation was compacted. Use this summary as context:\n\nOlder context"
6757        );
6758    }
6759
6760    fn user_text_message(id: ClientUserMessageId, text: &str) -> Arc<Message> {
6761        Arc::new(Message::User(UserMessage {
6762            id,
6763            content: vec![UserMessageContent::Text(text.to_string())].into(),
6764        }))
6765    }
6766
6767    fn agent_text_message(text: &str) -> Arc<Message> {
6768        Arc::new(Message::Agent(AgentMessage {
6769            content: vec![AgentMessageContent::Text(text.to_string())],
6770            ..Default::default()
6771        }))
6772    }
6773
6774    fn summary_compaction(summary: &str) -> Arc<Message> {
6775        Arc::new(Message::Compaction(CompactionInfo::Summary(summary.into())))
6776    }
6777
6778    fn summary_request_text(summary: &str) -> String {
6779        format!(
6780            "The previous conversation was compacted. Use this summary as context:\n\n{summary}"
6781        )
6782    }
6783
6784    fn request_texts_after_system(messages: &[LanguageModelRequestMessage]) -> Vec<String> {
6785        messages
6786            .iter()
6787            .skip(1)
6788            .map(LanguageModelRequestMessage::string_contents)
6789            .collect()
6790    }
6791
6792    fn request_texts(messages: &[LanguageModelRequestMessage]) -> Vec<String> {
6793        messages
6794            .iter()
6795            .map(LanguageModelRequestMessage::string_contents)
6796            .collect()
6797    }
6798
6799    #[gpui::test]
6800    async fn test_thread_summary_request_uses_compacted_history(cx: &mut TestAppContext) {
6801        let (thread, _event_stream) = setup_thread_for_test(cx).await;
6802        let summary_model = Arc::new(FakeLanguageModel::default());
6803
6804        let summary_task = cx.update(|cx| {
6805            thread.update(cx, |thread, cx| {
6806                thread.set_summarization_model(Some(summary_model.clone()), cx);
6807                thread
6808                    .messages
6809                    .push(user_text_message(ClientUserMessageId::new(), "old user"));
6810                thread.messages.push(agent_text_message("old assistant"));
6811                thread.messages.push(summary_compaction("first summary"));
6812                thread.messages.push(user_text_message(
6813                    ClientUserMessageId::new(),
6814                    "between user",
6815                ));
6816                thread
6817                    .messages
6818                    .push(agent_text_message("between assistant"));
6819                thread.messages.push(summary_compaction("latest summary"));
6820                thread
6821                    .messages
6822                    .push(user_text_message(ClientUserMessageId::new(), "after user"));
6823                thread.messages.push(agent_text_message("after assistant"));
6824
6825                thread.summary(cx)
6826            })
6827        });
6828        cx.run_until_parked();
6829
6830        let summary_request = summary_model.pending_completions().pop().unwrap();
6831        assert_eq!(
6832            summary_request.intent,
6833            Some(CompletionIntent::ThreadContextSummarization)
6834        );
6835        assert_eq!(
6836            request_texts(&summary_request.messages),
6837            vec![
6838                "old user".to_string(),
6839                "between user".to_string(),
6840                summary_request_text("latest summary"),
6841                "after user".to_string(),
6842                "after assistant".to_string(),
6843                SUMMARIZE_THREAD_DETAILED_PROMPT.to_string(),
6844            ]
6845        );
6846
6847        summary_model.send_completion_stream_text_chunk(&summary_request, "thread summary");
6848        summary_model.end_completion_stream(&summary_request);
6849        assert_eq!(summary_task.await.as_deref(), Some("thread summary"));
6850    }
6851
6852    #[test]
6853    fn test_thread_title_request_uses_compacted_history() {
6854        let messages = vec![
6855            user_text_message(ClientUserMessageId::new(), "old user"),
6856            agent_text_message("old assistant"),
6857            summary_compaction("first summary"),
6858            user_text_message(ClientUserMessageId::new(), "between user"),
6859            agent_text_message("between assistant"),
6860            summary_compaction("latest summary"),
6861            user_text_message(ClientUserMessageId::new(), "after user"),
6862            agent_text_message("after assistant"),
6863        ];
6864
6865        let request = build_thread_title_request(&messages, Some(0.2));
6866
6867        assert_eq!(request.intent, Some(CompletionIntent::ThreadSummarization));
6868        assert_eq!(request.temperature, Some(0.2));
6869        assert_eq!(
6870            request_texts(&request.messages),
6871            vec![
6872                "old user".to_string(),
6873                "between user".to_string(),
6874                summary_request_text("latest summary"),
6875                "after user".to_string(),
6876                "after assistant".to_string(),
6877                SUMMARIZE_THREAD_PROMPT.to_string(),
6878            ]
6879        );
6880    }
6881
6882    #[gpui::test]
6883    async fn test_compaction_threshold_uses_percentage_setting(cx: &mut TestAppContext) {
6884        let (thread, _event_stream) = setup_thread_for_test(cx).await;
6885        let model = Arc::new(FakeLanguageModel::default());
6886        let user_message_id = ClientUserMessageId::new();
6887
6888        cx.update(|cx| {
6889            thread.update(cx, |thread, cx| {
6890                thread.set_model(model, cx);
6891                thread
6892                    .messages
6893                    .push(user_text_message(user_message_id.clone(), "below limit"));
6894                thread.request_token_usage.insert(
6895                    user_message_id.clone(),
6896                    language_model::TokenUsage {
6897                        input_tokens: 899_999,
6898                        ..Default::default()
6899                    },
6900                );
6901
6902                assert_eq!(thread.compaction_message_target_ix(cx), None);
6903
6904                thread.request_token_usage.insert(
6905                    user_message_id.clone(),
6906                    language_model::TokenUsage {
6907                        input_tokens: 900_000,
6908                        ..Default::default()
6909                    },
6910                );
6911
6912                assert_eq!(thread.compaction_message_target_ix(cx), Some(1));
6913            });
6914        });
6915    }
6916
6917    #[gpui::test]
6918    async fn test_compaction_threshold_accounts_for_max_output_tokens(cx: &mut TestAppContext) {
6919        let (thread, _event_stream) = setup_thread_for_test(cx).await;
6920        let model = Arc::new(FakeLanguageModel::default());
6921        model.set_max_output_tokens(Some(32_000));
6922        let user_message_id = ClientUserMessageId::new();
6923
6924        cx.update(|cx| {
6925            thread.update(cx, |thread, cx| {
6926                thread.set_model(model, cx);
6927                thread.messages.push(user_text_message(
6928                    user_message_id.clone(),
6929                    "near input limit",
6930                ));
6931                thread.request_token_usage.insert(
6932                    user_message_id.clone(),
6933                    language_model::TokenUsage {
6934                        input_tokens: 871_199,
6935                        ..Default::default()
6936                    },
6937                );
6938
6939                assert_eq!(thread.compaction_message_target_ix(cx), None);
6940
6941                thread.request_token_usage.insert(
6942                    user_message_id.clone(),
6943                    language_model::TokenUsage {
6944                        input_tokens: 871_200,
6945                        ..Default::default()
6946                    },
6947                );
6948
6949                assert_eq!(thread.compaction_message_target_ix(cx), Some(1));
6950
6951                set_auto_compact_settings(
6952                    cx,
6953                    agent_settings::AutoCompactSettings {
6954                        enabled: true,
6955                        threshold: AutoCompactThreshold::TokensRemaining(20_000),
6956                    },
6957                );
6958                thread.request_token_usage.insert(
6959                    user_message_id.clone(),
6960                    language_model::TokenUsage {
6961                        input_tokens: 948_000,
6962                        ..Default::default()
6963                    },
6964                );
6965
6966                assert_eq!(thread.compaction_message_target_ix(cx), None);
6967
6968                thread.request_token_usage.insert(
6969                    user_message_id.clone(),
6970                    language_model::TokenUsage {
6971                        input_tokens: 948_001,
6972                        ..Default::default()
6973                    },
6974                );
6975
6976                assert_eq!(thread.compaction_message_target_ix(cx), Some(1));
6977            });
6978        });
6979    }
6980
6981    #[gpui::test]
6982    async fn test_compaction_threshold_respects_enabled_setting(cx: &mut TestAppContext) {
6983        let (thread, _event_stream) = setup_thread_for_test(cx).await;
6984        let model = Arc::new(FakeLanguageModel::default());
6985        let user_message_id = ClientUserMessageId::new();
6986
6987        cx.update(|cx| {
6988            set_auto_compact_settings(
6989                cx,
6990                agent_settings::AutoCompactSettings {
6991                    enabled: false,
6992                    threshold: AutoCompactThreshold::Percentage(0.9),
6993                },
6994            );
6995            thread.update(cx, |thread, cx| {
6996                thread.set_model(model, cx);
6997                thread
6998                    .messages
6999                    .push(user_text_message(user_message_id.clone(), "near limit"));
7000                thread.request_token_usage.insert(
7001                    user_message_id.clone(),
7002                    language_model::TokenUsage {
7003                        input_tokens: 960_000,
7004                        ..Default::default()
7005                    },
7006                );
7007
7008                assert_eq!(thread.compaction_message_target_ix(cx), None);
7009            });
7010        });
7011    }
7012
7013    #[gpui::test]
7014    async fn test_compaction_threshold_respects_token_settings(cx: &mut TestAppContext) {
7015        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7016        let model = Arc::new(FakeLanguageModel::default());
7017        let user_message_id = ClientUserMessageId::new();
7018
7019        cx.update(|cx| {
7020            set_auto_compact_settings(
7021                cx,
7022                agent_settings::AutoCompactSettings {
7023                    enabled: true,
7024                    threshold: AutoCompactThreshold::TokensUsed(100_000),
7025                },
7026            );
7027            thread.update(cx, |thread, cx| {
7028                thread.set_model(model, cx);
7029                thread.messages.push(user_text_message(
7030                    user_message_id.clone(),
7031                    "fixed token limit",
7032                ));
7033                thread.request_token_usage.insert(
7034                    user_message_id.clone(),
7035                    language_model::TokenUsage {
7036                        input_tokens: 99_999,
7037                        ..Default::default()
7038                    },
7039                );
7040
7041                assert_eq!(thread.compaction_message_target_ix(cx), None);
7042
7043                thread.request_token_usage.insert(
7044                    user_message_id.clone(),
7045                    language_model::TokenUsage {
7046                        input_tokens: 100_000,
7047                        ..Default::default()
7048                    },
7049                );
7050
7051                assert_eq!(thread.compaction_message_target_ix(cx), Some(1));
7052
7053                set_auto_compact_settings(
7054                    cx,
7055                    agent_settings::AutoCompactSettings {
7056                        enabled: true,
7057                        threshold: AutoCompactThreshold::TokensRemaining(20_000),
7058                    },
7059                );
7060                thread.request_token_usage.insert(
7061                    user_message_id.clone(),
7062                    language_model::TokenUsage {
7063                        input_tokens: 980_000,
7064                        ..Default::default()
7065                    },
7066                );
7067
7068                assert_eq!(thread.compaction_message_target_ix(cx), None);
7069
7070                thread.request_token_usage.insert(
7071                    user_message_id.clone(),
7072                    language_model::TokenUsage {
7073                        input_tokens: 980_001,
7074                        ..Default::default()
7075                    },
7076                );
7077
7078                assert_eq!(thread.compaction_message_target_ix(cx), Some(1));
7079            });
7080        });
7081    }
7082
7083    #[gpui::test]
7084    async fn test_compaction_unavailable_for_small_context_window(cx: &mut TestAppContext) {
7085        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7086        let model = Arc::new(FakeLanguageModel::default());
7087        // A context window below the minimum disables auto-compaction.
7088        model.set_max_token_count(MIN_COMPACTION_CONTEXT_WINDOW - 1);
7089        let user_message_id = ClientUserMessageId::new();
7090
7091        cx.update(|cx| {
7092            thread.update(cx, |thread, cx| {
7093                thread.set_model(model, cx);
7094                thread
7095                    .messages
7096                    .push(user_text_message(user_message_id.clone(), "near limit"));
7097                thread.request_token_usage.insert(
7098                    user_message_id.clone(),
7099                    language_model::TokenUsage {
7100                        input_tokens: u64::MAX,
7101                        ..Default::default()
7102                    },
7103                );
7104
7105                assert_eq!(thread.compaction_message_target_ix(cx), None);
7106            });
7107        });
7108    }
7109
7110    #[gpui::test]
7111    async fn test_compaction_inserts_before_new_user_and_requests_compacted_window(
7112        cx: &mut TestAppContext,
7113    ) {
7114        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7115        let model = Arc::new(FakeLanguageModel::default());
7116        let old_user_message_id = ClientUserMessageId::new();
7117        let new_user_message_id = ClientUserMessageId::new();
7118
7119        cx.update(|cx| {
7120            thread.update(cx, |thread, cx| {
7121                thread.set_model(model.clone(), cx);
7122                thread
7123                    .messages
7124                    .push(user_text_message(old_user_message_id.clone(), "old user"));
7125                thread.messages.push(agent_text_message("old assistant"));
7126                thread.request_token_usage.insert(
7127                    old_user_message_id.clone(),
7128                    language_model::TokenUsage {
7129                        input_tokens: 960_000,
7130                        ..Default::default()
7131                    },
7132                );
7133            });
7134        });
7135
7136        let _events = cx
7137            .update(|cx| {
7138                thread.update(cx, |thread, cx| {
7139                    thread.send(new_user_message_id, vec!["new prompt"], cx)
7140                })
7141            })
7142            .unwrap();
7143        cx.run_until_parked();
7144
7145        let compaction_request = model.pending_completions().pop().unwrap();
7146        assert_eq!(
7147            compaction_request.intent,
7148            Some(CompletionIntent::ThreadContextSummarization)
7149        );
7150        let compaction_texts = request_texts_after_system(&compaction_request.messages);
7151        assert_eq!(compaction_texts.len(), 3);
7152        assert_eq!(compaction_texts[0], "old user");
7153        assert_eq!(compaction_texts[1], "old assistant");
7154        assert_eq!(compaction_texts[2], COMPACTION_PROMPT);
7155
7156        model.send_completion_stream_text_chunk(&compaction_request, "compacted old context");
7157        model.end_completion_stream(&compaction_request);
7158        cx.run_until_parked();
7159
7160        let final_request = model.pending_completions().pop().unwrap();
7161        assert_eq!(final_request.intent, Some(CompletionIntent::UserPrompt));
7162        assert_eq!(
7163            request_texts_after_system(&final_request.messages),
7164            vec![
7165                "old user".to_string(),
7166                summary_request_text("compacted old context"),
7167                "new prompt".to_string(),
7168            ]
7169        );
7170
7171        model.send_completion_stream_text_chunk(&final_request, "answer");
7172        model.end_completion_stream(&final_request);
7173        cx.run_until_parked();
7174
7175        cx.update(|cx| {
7176            thread.read_with(cx, |thread, _cx| {
7177                assert!(matches!(&*thread.messages[0], Message::User(_)));
7178                assert!(matches!(&*thread.messages[1], Message::Agent(_)));
7179                assert!(matches!(
7180                    &*thread.messages[2],
7181                    Message::Compaction(CompactionInfo::Summary(summary)) if summary.as_ref() == "compacted old context"
7182                ));
7183                assert!(matches!(&*thread.messages[3], Message::User(_)));
7184            });
7185        });
7186    }
7187
7188    #[gpui::test]
7189    async fn test_manual_compact_forces_summary(cx: &mut TestAppContext) {
7190        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7191        let model = Arc::new(FakeLanguageModel::default());
7192        // A context window below the minimum and no recorded token usage would
7193        // both disable *automatic* compaction. Manual compaction forces it anyway.
7194        model.set_max_token_count(MIN_COMPACTION_CONTEXT_WINDOW - 1);
7195        let user_message_id = ClientUserMessageId::new();
7196        let compact_message_id = ClientUserMessageId::new();
7197
7198        cx.update(|cx| {
7199            thread.update(cx, |thread, cx| {
7200                thread.set_model(model.clone(), cx);
7201                thread
7202                    .messages
7203                    .push(user_text_message(user_message_id.clone(), "old user"));
7204                thread.messages.push(agent_text_message("old assistant"));
7205                // Auto-compaction would be a no-op here.
7206                assert_eq!(thread.compaction_message_target_ix(cx), None);
7207            });
7208        });
7209
7210        let _events = cx
7211            .update(|cx| {
7212                thread.update(cx, |thread, cx| {
7213                    thread.compact(compact_message_id.clone(), cx)
7214                })
7215            })
7216            .unwrap();
7217        cx.run_until_parked();
7218
7219        let compaction_request = model.pending_completions().pop().unwrap();
7220        assert_eq!(
7221            compaction_request.intent,
7222            Some(CompletionIntent::ThreadContextSummarization)
7223        );
7224        let compaction_texts = request_texts_after_system(&compaction_request.messages);
7225        assert_eq!(compaction_texts.len(), 3);
7226        assert_eq!(compaction_texts[0], "old user");
7227        assert_eq!(compaction_texts[1], "old assistant");
7228        assert_eq!(compaction_texts[2], COMPACTION_PROMPT);
7229
7230        model.send_completion_stream_text_chunk(&compaction_request, "summary of old context");
7231        model.end_completion_stream(&compaction_request);
7232        cx.run_until_parked();
7233
7234        // The compaction summary is appended after a zero-content user message
7235        // marker, and no follow-up model turn is requested — `/compact` only
7236        // compacts.
7237        assert!(model.pending_completions().is_empty());
7238        cx.update(|cx| {
7239            thread.read_with(cx, |thread, _cx| {
7240                assert!(matches!(&*thread.messages[0], Message::User(_)));
7241                assert!(matches!(&*thread.messages[1], Message::Agent(_)));
7242                assert!(matches!(
7243                    &*thread.messages[2],
7244                    Message::User(UserMessage { id, content }) if id == &compact_message_id && content.is_empty()
7245                ));
7246                assert!(matches!(
7247                    &*thread.messages[3],
7248                    Message::Compaction(CompactionInfo::Summary(summary)) if summary.as_ref() == "summary of old context"
7249                ));
7250                // Re-running `/compact` with nothing new to summarize is a
7251                // no-op: the thread already ends in a compaction.
7252                assert_eq!(thread.forced_compaction_target_ix(), None);
7253            });
7254
7255            thread
7256                .update(cx, |thread, cx| thread.truncate(compact_message_id.clone(), cx))
7257                .unwrap();
7258
7259            thread.read_with(cx, |thread, _cx| {
7260                assert_eq!(thread.messages.len(), 2);
7261                assert!(matches!(&*thread.messages[0], Message::User(_)));
7262                assert!(matches!(&*thread.messages[1], Message::Agent(_)));
7263            });
7264        });
7265    }
7266
7267    /// Cancelling an in-flight manual compaction must not leave the zero-content
7268    /// rewind marker (or a partial summary) dangling at the end of the thread.
7269    #[gpui::test]
7270    async fn test_manual_compact_cancelled_leaves_no_marker(cx: &mut TestAppContext) {
7271        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7272        let model = Arc::new(FakeLanguageModel::default());
7273
7274        cx.update(|cx| {
7275            thread.update(cx, |thread, cx| {
7276                thread.set_model(model.clone(), cx);
7277                thread
7278                    .messages
7279                    .push(user_text_message(ClientUserMessageId::new(), "old user"));
7280                thread.messages.push(agent_text_message("old assistant"));
7281            });
7282        });
7283
7284        let _events = cx
7285            .update(|cx| {
7286                thread.update(cx, |thread, cx| {
7287                    thread.compact(ClientUserMessageId::new(), cx)
7288                })
7289            })
7290            .unwrap();
7291        cx.run_until_parked();
7292        // The compaction request is in flight but hasn't streamed a summary.
7293        assert_eq!(model.pending_completions().len(), 1);
7294
7295        cx.update(|cx| thread.update(cx, |thread, cx| thread.cancel(cx)))
7296            .await;
7297        cx.run_until_parked();
7298
7299        thread.read_with(cx, |thread, _cx| {
7300            assert_eq!(thread.messages.len(), 2);
7301            assert!(matches!(&*thread.messages[0], Message::User(_)));
7302            assert!(matches!(&*thread.messages[1], Message::Agent(_)));
7303        });
7304    }
7305
7306    /// A failed compaction (here, an empty summary) reports an error and leaves
7307    /// the thread untouched — no marker, no compaction.
7308    #[gpui::test]
7309    async fn test_manual_compact_empty_summary_leaves_no_marker(cx: &mut TestAppContext) {
7310        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7311        let model = Arc::new(FakeLanguageModel::default());
7312
7313        cx.update(|cx| {
7314            thread.update(cx, |thread, cx| {
7315                thread.set_model(model.clone(), cx);
7316                thread
7317                    .messages
7318                    .push(user_text_message(ClientUserMessageId::new(), "old user"));
7319                thread.messages.push(agent_text_message("old assistant"));
7320            });
7321        });
7322
7323        let mut events = cx
7324            .update(|cx| {
7325                thread.update(cx, |thread, cx| {
7326                    thread.compact(ClientUserMessageId::new(), cx)
7327                })
7328            })
7329            .unwrap();
7330        cx.run_until_parked();
7331
7332        let request = model.pending_completions().pop().unwrap();
7333        // End the stream without emitting any summary text.
7334        model.end_completion_stream(&request);
7335        cx.run_until_parked();
7336
7337        // An error is surfaced, and the thread is left exactly as it was. The
7338        // compaction task drops the event stream after failing, so the channel
7339        // closes and this drain terminates.
7340        let mut saw_error = false;
7341        while let Some(event) = events.next().await {
7342            if event.is_err() {
7343                saw_error = true;
7344            }
7345        }
7346        assert!(saw_error, "expected an error event for the empty summary");
7347        thread.read_with(cx, |thread, _cx| {
7348            assert_eq!(thread.messages.len(), 2);
7349            assert!(matches!(&*thread.messages[0], Message::User(_)));
7350            assert!(matches!(&*thread.messages[1], Message::Agent(_)));
7351        });
7352    }
7353
7354    /// `/compact` on an empty thread (nothing to summarize) is a no-op: it
7355    /// issues no model request and adds no marker.
7356    #[gpui::test]
7357    async fn test_manual_compact_noop_on_empty_thread(cx: &mut TestAppContext) {
7358        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7359        let model = Arc::new(FakeLanguageModel::default());
7360        cx.update(|cx| thread.update(cx, |thread, cx| thread.set_model(model.clone(), cx)));
7361
7362        let _events = cx
7363            .update(|cx| {
7364                thread.update(cx, |thread, cx| {
7365                    thread.compact(ClientUserMessageId::new(), cx)
7366                })
7367            })
7368            .unwrap();
7369        cx.run_until_parked();
7370
7371        assert!(model.pending_completions().is_empty());
7372        thread.read_with(cx, |thread, _cx| {
7373            assert!(thread.messages.is_empty());
7374        });
7375    }
7376
7377    /// The zero-content marker replays as an empty user message, which the UI
7378    /// drops (it renders content blocks, of which there are none), so reloading
7379    /// a compacted thread doesn't surface an empty `/compact` bubble.
7380    #[gpui::test]
7381    async fn test_manual_compact_marker_replays_as_empty_user_message(cx: &mut TestAppContext) {
7382        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7383        let marker_id = ClientUserMessageId::new();
7384
7385        let mut replay_events = cx.update(|cx| {
7386            thread.update(cx, |thread, cx| {
7387                thread
7388                    .messages
7389                    .push(user_text_message(ClientUserMessageId::new(), "before"));
7390                thread.messages.push(agent_text_message("answer"));
7391                thread.messages.push(Arc::new(Message::User(UserMessage {
7392                    id: marker_id.clone(),
7393                    content: Arc::from([]),
7394                })));
7395                thread.messages.push(summary_compaction("summary"));
7396                thread.replay(cx)
7397            })
7398        });
7399
7400        // Skip the leading "before"/"answer" replay events.
7401        let _ = replay_events.next().await;
7402        let _ = replay_events.next().await;
7403
7404        let event = replay_events.next().await;
7405        match event {
7406            Some(Ok(ThreadEvent::UserMessage(message))) => {
7407                assert_eq!(message.id, marker_id);
7408                assert!(
7409                    message.content.is_empty(),
7410                    "marker should replay with no content so the UI renders nothing"
7411                );
7412            }
7413            _ => panic!("expected the marker to replay as a user message, got {event:?}"),
7414        }
7415
7416        let event = replay_events.next().await;
7417        assert!(
7418            matches!(&event, Some(Ok(ThreadEvent::ContextCompaction(_)))),
7419            "expected the compaction to replay after the marker, got {event:?}"
7420        );
7421    }
7422
7423    /// When `agent.compaction_model` is configured, manual `/compact` streams
7424    /// to the configured model rather than the thread's primary model.
7425    #[gpui::test]
7426    async fn test_compaction_uses_configured_compaction_model(cx: &mut TestAppContext) {
7427        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7428        let thread_model = Arc::new(FakeLanguageModel::default());
7429        let compaction_model = Arc::new(FakeLanguageModel::default());
7430
7431        cx.update(|cx| {
7432            thread.update(cx, |thread, cx| {
7433                thread.set_model(thread_model.clone(), cx);
7434                thread
7435                    .messages
7436                    .push(user_text_message(ClientUserMessageId::new(), "old user"));
7437                thread.messages.push(agent_text_message("old assistant"));
7438            });
7439            set_registry_compaction_model(cx, Some(compaction_model.clone()));
7440        });
7441
7442        let _events = cx
7443            .update(|cx| {
7444                thread.update(cx, |thread, cx| {
7445                    thread.compact(ClientUserMessageId::new(), cx)
7446                })
7447            })
7448            .unwrap();
7449        cx.run_until_parked();
7450
7451        assert_eq!(
7452            thread_model.pending_completions().len(),
7453            0,
7454            "thread's primary model should not have been used for compaction"
7455        );
7456        let request = compaction_model
7457            .pending_completions()
7458            .pop()
7459            .expect("compaction model should have received the request");
7460        assert_eq!(
7461            request.intent,
7462            Some(CompletionIntent::ThreadContextSummarization)
7463        );
7464
7465        thread.read_with(cx, |thread, _cx| {
7466            let telemetry = thread
7467                .pending_compaction_telemetry
7468                .as_ref()
7469                .expect("pending telemetry");
7470            assert_eq!(telemetry.model, compaction_model.telemetry_id());
7471        });
7472
7473        compaction_model.send_completion_stream_text_chunk(&request, "summary");
7474        compaction_model.end_completion_stream(&request);
7475        cx.run_until_parked();
7476    }
7477
7478    /// When `agent.compaction_model` is configured but doesn't resolve (e.g.
7479    /// the provider isn't registered), manual `/compact` falls back to the
7480    /// thread's primary model and the telemetry reflects the actual stream
7481    /// model — not the one the user tried to configure.
7482    #[gpui::test]
7483    async fn test_compaction_falls_back_when_compaction_model_unavailable(cx: &mut TestAppContext) {
7484        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7485        let thread_model = Arc::new(FakeLanguageModel::default());
7486
7487        cx.update(|cx| {
7488            thread.update(cx, |thread, cx| {
7489                thread.set_model(thread_model.clone(), cx);
7490                thread
7491                    .messages
7492                    .push(user_text_message(ClientUserMessageId::new(), "old user"));
7493                thread.messages.push(agent_text_message("old assistant"));
7494            });
7495            // Settings say "configured"; registry says "couldn't resolve".
7496            let mut settings = AgentSettings::get_global(cx).clone();
7497            settings.compaction_model = Some(LanguageModelSelection {
7498                provider: LanguageModelProviderSetting("missing".into()),
7499                model: "missing-model".into(),
7500                enable_thinking: false,
7501                effort: None,
7502                speed: None,
7503            });
7504            AgentSettings::override_global(settings, cx);
7505            set_registry_compaction_model(cx, None);
7506        });
7507
7508        let _events = cx
7509            .update(|cx| {
7510                thread.update(cx, |thread, cx| {
7511                    thread.compact(ClientUserMessageId::new(), cx)
7512                })
7513            })
7514            .unwrap();
7515        cx.run_until_parked();
7516
7517        let request = thread_model
7518            .pending_completions()
7519            .pop()
7520            .expect("thread model should have received the fallback request");
7521        assert_eq!(
7522            request.intent,
7523            Some(CompletionIntent::ThreadContextSummarization)
7524        );
7525
7526        thread.read_with(cx, |thread, _cx| {
7527            let telemetry = thread
7528                .pending_compaction_telemetry
7529                .as_ref()
7530                .expect("pending telemetry");
7531            assert_eq!(telemetry.model, thread_model.telemetry_id());
7532        });
7533
7534        thread_model.send_completion_stream_text_chunk(&request, "summary");
7535        thread_model.end_completion_stream(&request);
7536        cx.run_until_parked();
7537    }
7538
7539    /// Auto-compaction triggered by the threshold also honors
7540    /// `agent.compaction_model`.
7541    #[gpui::test]
7542    async fn test_auto_compaction_uses_compaction_model(cx: &mut TestAppContext) {
7543        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7544        let thread_model = Arc::new(FakeLanguageModel::default());
7545        let compaction_model = Arc::new(FakeLanguageModel::default());
7546        let old_user_message_id = ClientUserMessageId::new();
7547
7548        cx.update(|cx| {
7549            thread.update(cx, |thread, cx| {
7550                thread.set_model(thread_model.clone(), cx);
7551                thread
7552                    .messages
7553                    .push(user_text_message(old_user_message_id.clone(), "old user"));
7554                thread.messages.push(agent_text_message("old assistant"));
7555                thread.request_token_usage.insert(
7556                    old_user_message_id.clone(),
7557                    TokenUsage {
7558                        input_tokens: u64::MAX,
7559                        ..Default::default()
7560                    },
7561                );
7562            });
7563            set_auto_compact_settings(
7564                cx,
7565                agent_settings::AutoCompactSettings {
7566                    enabled: true,
7567                    threshold: agent_settings::AutoCompactThreshold::Percentage(0.5),
7568                },
7569            );
7570            set_registry_compaction_model(cx, Some(compaction_model.clone()));
7571        });
7572
7573        // The auto-compact gate fires inside `run_turn` when we kick off a
7574        // new user message. Drive a `send` so `perform_compaction_if_needed`
7575        // runs.
7576        let _events = cx
7577            .update(|cx| {
7578                thread.update(cx, |thread, cx| {
7579                    thread.send(ClientUserMessageId::new(), vec!["new prompt"], cx)
7580                })
7581            })
7582            .unwrap();
7583        cx.run_until_parked();
7584
7585        assert_eq!(thread_model.pending_completions().len(), 0);
7586        let request = compaction_model
7587            .pending_completions()
7588            .pop()
7589            .expect("compaction model should have received the auto-compaction request");
7590        assert_eq!(
7591            request.intent,
7592            Some(CompletionIntent::ThreadContextSummarization)
7593        );
7594
7595        compaction_model.send_completion_stream_text_chunk(&request, "summary");
7596        compaction_model.end_completion_stream(&request);
7597        cx.run_until_parked();
7598    }
7599
7600    #[gpui::test]
7601    async fn test_compaction_usage_counts_toward_cumulative_usage(cx: &mut TestAppContext) {
7602        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7603        let model = Arc::new(FakeLanguageModel::default());
7604        let old_user_message_id = ClientUserMessageId::new();
7605        let new_user_message_id = ClientUserMessageId::new();
7606        let prior_usage = TokenUsage {
7607            input_tokens: 960_000,
7608            output_tokens: 25,
7609            ..Default::default()
7610        };
7611        let compaction_usage = TokenUsage {
7612            input_tokens: 40,
7613            output_tokens: 9,
7614            cache_creation_input_tokens: 2,
7615            cache_read_input_tokens: 3,
7616        };
7617        let final_usage = TokenUsage {
7618            input_tokens: 500,
7619            output_tokens: 50,
7620            ..Default::default()
7621        };
7622
7623        cx.update(|cx| {
7624            thread.update(cx, |thread, cx| {
7625                thread.set_model(model.clone(), cx);
7626                thread
7627                    .messages
7628                    .push(user_text_message(old_user_message_id.clone(), "old user"));
7629                thread.messages.push(agent_text_message("old assistant"));
7630                thread
7631                    .request_token_usage
7632                    .insert(old_user_message_id.clone(), prior_usage);
7633                thread.cumulative_token_usage = prior_usage;
7634                thread.current_request_token_usage = prior_usage;
7635            });
7636        });
7637
7638        let _events = cx
7639            .update(|cx| {
7640                thread.update(cx, |thread, cx| {
7641                    thread.send(new_user_message_id.clone(), vec!["new prompt"], cx)
7642                })
7643            })
7644            .unwrap();
7645        cx.run_until_parked();
7646
7647        let compaction_request = model.pending_completions().pop().unwrap();
7648        assert_eq!(
7649            compaction_request.intent,
7650            Some(CompletionIntent::ThreadContextSummarization)
7651        );
7652
7653        model.send_completion_stream_event(
7654            &compaction_request,
7655            LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
7656                input_tokens: 40,
7657                output_tokens: 4,
7658                ..Default::default()
7659            }),
7660        );
7661        model.send_completion_stream_event(
7662            &compaction_request,
7663            LanguageModelCompletionEvent::UsageUpdate(compaction_usage),
7664        );
7665        model.send_completion_stream_text_chunk(&compaction_request, "compacted old context");
7666        model.end_completion_stream(&compaction_request);
7667        cx.run_until_parked();
7668
7669        let expected_after_compaction = prior_usage + compaction_usage;
7670        thread.read_with(cx, |thread, _cx| {
7671            assert_eq!(thread.cumulative_token_usage(), expected_after_compaction);
7672            assert!(
7673                !thread
7674                    .request_token_usage
7675                    .contains_key(&new_user_message_id)
7676            );
7677        });
7678
7679        let final_request = model.pending_completions().pop().unwrap();
7680        assert_eq!(final_request.intent, Some(CompletionIntent::UserPrompt));
7681
7682        model.send_completion_stream_event(
7683            &final_request,
7684            LanguageModelCompletionEvent::UsageUpdate(final_usage),
7685        );
7686        model.end_completion_stream(&final_request);
7687        cx.run_until_parked();
7688
7689        thread.read_with(cx, |thread, _cx| {
7690            assert_eq!(
7691                thread.cumulative_token_usage(),
7692                expected_after_compaction + final_usage
7693            );
7694            assert_eq!(
7695                thread.request_token_usage.get(&new_user_message_id),
7696                Some(&final_usage)
7697            );
7698        });
7699    }
7700
7701    #[gpui::test]
7702    async fn test_replay_emits_context_compaction(cx: &mut TestAppContext) {
7703        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7704        let user_message_id = ClientUserMessageId::new();
7705
7706        let mut replay_events = cx.update(|cx| {
7707            thread.update(cx, |thread, cx| {
7708                thread
7709                    .messages
7710                    .push(user_text_message(user_message_id.clone(), "before"));
7711                thread.messages.push(summary_compaction("summary"));
7712                thread.messages.push(agent_text_message("after"));
7713
7714                thread.replay(cx)
7715            })
7716        });
7717
7718        let event = replay_events.next().await;
7719        assert!(
7720            matches!(
7721                &event,
7722                Some(Ok(ThreadEvent::UserMessage(UserMessage { id, .. }))) if id == &user_message_id
7723            ),
7724            "expected replayed user message, got {event:?}"
7725        );
7726
7727        let event = replay_events.next().await;
7728        let compaction_id = match &event {
7729            Some(Ok(ThreadEvent::ContextCompaction(compaction))) => compaction.id.clone(),
7730            _ => panic!("expected context compaction event, got {event:?}"),
7731        };
7732
7733        let event = replay_events.next().await;
7734        assert!(
7735            matches!(
7736                &event,
7737                Some(Ok(ThreadEvent::ContextCompactionUpdate(update)))
7738                    if update.id == compaction_id && update.summary_delta == "summary"
7739            ),
7740            "expected context compaction summary event, got {event:?}"
7741        );
7742
7743        let event = replay_events.next().await;
7744        assert!(
7745            matches!(&event, Some(Ok(ThreadEvent::AgentText(text))) if text == "after"),
7746            "expected replayed agent text, got {event:?}"
7747        );
7748    }
7749
7750    #[gpui::test]
7751    async fn test_native_compaction_boundary(cx: &mut TestAppContext) {
7752        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7753
7754        let request_messages = cx.update(|cx| {
7755            thread.update(cx, |thread, cx| {
7756                thread.messages.push(user_text_message(
7757                    ClientUserMessageId::new(),
7758                    "before native",
7759                ));
7760                thread.messages.push(Arc::new(Message::Compaction(
7761                    CompactionInfo::ProviderNative {
7762                        provider: LanguageModelProviderId::from("openai".to_string()),
7763                        items: vec![json!({"type": "compaction"})],
7764                    },
7765                )));
7766                thread.messages.push(user_text_message(
7767                    ClientUserMessageId::new(),
7768                    "after native",
7769                ));
7770
7771                thread.build_request_messages(Vec::new(), cx)
7772            })
7773        });
7774
7775        assert_eq!(
7776            request_texts_after_system(&request_messages),
7777            vec!["after native".to_string()]
7778        );
7779    }
7780
7781    #[gpui::test]
7782    async fn test_retained_users_truncate_oldest(cx: &mut TestAppContext) {
7783        let (thread, _event_stream) = setup_thread_for_test(cx).await;
7784        let mut long_text = "START".to_string();
7785        long_text.push_str(&"x".repeat(COMPACTION_RETAINED_USER_MESSAGES_BYTE_BUDGET));
7786        long_text.push_str("END");
7787
7788        let request_messages = cx.update(|cx| {
7789            thread.update(cx, |thread, cx| {
7790                thread.messages.push(user_text_message(
7791                    ClientUserMessageId::new(),
7792                    "dropped older user",
7793                ));
7794                thread
7795                    .messages
7796                    .push(agent_text_message("dropped assistant"));
7797                thread
7798                    .messages
7799                    .push(user_text_message(ClientUserMessageId::new(), &long_text));
7800                thread
7801                    .messages
7802                    .push(user_text_message(ClientUserMessageId::new(), "new"));
7803                thread.messages.push(summary_compaction("summary context"));
7804                thread.messages.push(agent_text_message("after assistant"));
7805                thread
7806                    .messages
7807                    .push(user_text_message(ClientUserMessageId::new(), "after user"));
7808
7809                thread.build_request_messages(Vec::new(), cx)
7810            })
7811        });
7812
7813        let request_texts = request_texts_after_system(&request_messages);
7814        assert_eq!(request_texts.len(), 5);
7815        assert_eq!(
7816            request_texts[0],
7817            format!(
7818                "START{}",
7819                "x".repeat(
7820                    COMPACTION_RETAINED_USER_MESSAGES_BYTE_BUDGET - "START".len() - "new".len()
7821                )
7822            )
7823        );
7824        assert_eq!(request_texts[1], "new");
7825        assert_eq!(request_texts[2], summary_request_text("summary context"));
7826        assert_eq!(request_texts[3], "after assistant");
7827        assert_eq!(request_texts[4], "after user");
7828        assert!(request_texts.iter().all(
7829            |text| !text.contains("dropped older user") && !text.contains("dropped assistant")
7830        ));
7831    }
7832
7833    #[test]
7834    fn test_truncate_text_utf8_boundary() {
7835        let message = LanguageModelRequestMessage {
7836            role: Role::User,
7837            content: vec![MessageContent::Text("hello 👋 world".to_string())],
7838            cache: false,
7839            reasoning_details: None,
7840        };
7841
7842        let truncated = truncate_user_message_to_byte_budget(message, 8).unwrap();
7843        assert_eq!(
7844            truncated.content,
7845            vec![MessageContent::Text("hello ".to_string())]
7846        );
7847    }
7848
7849    #[test]
7850    fn test_truncate_keeps_fitting_images() {
7851        let image = LanguageModelImage {
7852            source: "image".into(),
7853        };
7854        let message = LanguageModelRequestMessage {
7855            role: Role::User,
7856            content: vec![
7857                MessageContent::Text("abc".to_string()),
7858                MessageContent::Image(image.clone()),
7859            ],
7860            cache: false,
7861            reasoning_details: None,
7862        };
7863
7864        let truncated = truncate_user_message_to_byte_budget(message, 8).unwrap();
7865        assert_eq!(
7866            truncated.content,
7867            vec![
7868                MessageContent::Text("abc".to_string()),
7869                MessageContent::Image(image),
7870            ]
7871        );
7872    }
7873
7874    fn setup_parent_with_subagents(
7875        cx: &mut TestAppContext,
7876        parent: &Entity<Thread>,
7877        count: usize,
7878    ) -> Vec<Entity<Thread>> {
7879        cx.update(|cx| {
7880            let mut subagents = Vec::new();
7881            for _ in 0..count {
7882                let subagent = cx.new(|cx| Thread::new_subagent(parent, cx));
7883                parent.update(cx, |thread, _cx| {
7884                    thread.register_running_subagent(subagent.downgrade());
7885                });
7886                subagents.push(subagent);
7887            }
7888            subagents
7889        })
7890    }
7891
7892    struct ReplayImageTool;
7893
7894    impl AgentTool for ReplayImageTool {
7895        type Input = ();
7896        type Output = String;
7897
7898        const NAME: &'static str = "registered_image_tool";
7899
7900        fn kind() -> acp::ToolKind {
7901            acp::ToolKind::Other
7902        }
7903
7904        fn initial_title(
7905            &self,
7906            _input: Result<Self::Input, serde_json::Value>,
7907            _cx: &mut App,
7908        ) -> SharedString {
7909            "Registered Image Tool".into()
7910        }
7911
7912        fn run(
7913            self: Arc<Self>,
7914            _input: ToolInput<Self::Input>,
7915            _event_stream: ToolCallEventStream,
7916            _cx: &mut App,
7917        ) -> Task<Result<Self::Output, Self::Output>> {
7918            Task::ready(Ok(String::new()))
7919        }
7920    }
7921
7922    #[gpui::test]
7923    async fn test_authorize_sandbox_allow_always_does_not_cache_thread_grant(
7924        cx: &mut TestAppContext,
7925    ) {
7926        crate::tests::init_test(cx);
7927
7928        let (event_stream, mut receiver) = ToolCallEventStream::test();
7929        let request = SandboxRequest {
7930            network: crate::sandboxing::NetworkRequest::None,
7931            allow_fs_write_all: false,
7932            unsandboxed: false,
7933            write_paths: vec![
7934                PathBuf::from("/tmp/build"),
7935                PathBuf::from("/tmp/cache"),
7936                PathBuf::from("/tmp/logs"),
7937                PathBuf::from("/tmp/secret"),
7938            ],
7939        };
7940
7941        let authorize = cx.update(|cx| {
7942            event_stream.authorize_sandbox(
7943                request.clone(),
7944                "needs to write build artifacts".to_string(),
7945                cx,
7946            )
7947        });
7948        let authorization = receiver.expect_authorization().await;
7949        let details =
7950            acp_thread::sandbox_authorization_details_from_meta(&authorization.tool_call.meta)
7951                .expect("sandbox authorization should include request details");
7952        assert!(details.network_hosts.is_empty());
7953        assert!(!details.network_all_hosts);
7954        assert_eq!(details.allow_fs_write_all, request.allow_fs_write_all);
7955        assert_eq!(details.unsandboxed, request.unsandboxed);
7956        assert_eq!(details.write_paths, request.write_paths);
7957        assert!(authorization.tool_call.fields.content.is_none());
7958
7959        let acp_thread::PermissionOptions::Flat(options) = &authorization.options else {
7960            panic!("expected flat sandbox permission options");
7961        };
7962        let options = options
7963            .iter()
7964            .map(|option| {
7965                (
7966                    option.option_id.0.as_ref(),
7967                    option.name.as_ref(),
7968                    option.kind,
7969                )
7970            })
7971            .collect::<Vec<_>>();
7972        assert_eq!(
7973            options,
7974            vec![
7975                ("allow", "Allow once", acp::PermissionOptionKind::AllowOnce),
7976                (
7977                    "allow_thread",
7978                    "Allow for this thread",
7979                    acp::PermissionOptionKind::AllowAlways,
7980                ),
7981                (
7982                    "allow_always",
7983                    "Allow always",
7984                    acp::PermissionOptionKind::AllowAlways,
7985                ),
7986                ("deny", "Deny", acp::PermissionOptionKind::RejectOnce),
7987            ]
7988        );
7989
7990        let send_result = authorization
7991            .response
7992            .send(acp_thread::SelectedPermissionOutcome::new(
7993                acp::PermissionOptionId::new("allow_always"),
7994                acp::PermissionOptionKind::AllowAlways,
7995            ));
7996        assert!(send_result.is_ok());
7997        authorize.await.unwrap();
7998
7999        // "Allow always" persists to settings only
8000        let effective = event_stream.effective_sandbox_request(
8001            &SandboxRequest::default(),
8002            &agent_settings::SandboxPermissions::default(),
8003        );
8004        assert!(
8005            effective.write_paths.is_empty(),
8006            "allow always should not record an in-memory thread grant: {:?}",
8007            effective.write_paths
8008        );
8009    }
8010
8011    #[cfg(target_os = "linux")]
8012    #[gpui::test]
8013    async fn test_authorize_sandbox_fallback_options_and_details(cx: &mut TestAppContext) {
8014        crate::tests::init_test(cx);
8015
8016        let (event_stream, mut receiver) = ToolCallEventStream::test();
8017        let authorize = cx.update(|cx| {
8018            event_stream.authorize_sandbox_fallback(
8019                Some("cargo build".to_string()),
8020                "bwrap not found on PATH".to_string(),
8021                Some("installing-bubblewrap".to_string()),
8022                0,
8023                cx,
8024            )
8025        });
8026        let authorization = receiver.expect_authorization().await;
8027        let details = acp_thread::sandbox_fallback_authorization_details_from_meta(
8028            &authorization.tool_call.meta,
8029        )
8030        .expect("fallback authorization should include details");
8031        assert_eq!(details.command.as_deref(), Some("cargo build"));
8032        assert_eq!(details.reason, "bwrap not found on PATH");
8033        assert_eq!(
8034            details.docs_section.as_deref(),
8035            Some("installing-bubblewrap")
8036        );
8037
8038        let acp_thread::PermissionOptions::Flat(options) = &authorization.options else {
8039            panic!("expected flat fallback permission options");
8040        };
8041        let options = options
8042            .iter()
8043            .map(|option| (option.option_id.0.as_ref(), option.name.as_ref()))
8044            .collect::<Vec<_>>();
8045        assert_eq!(
8046            options,
8047            vec![
8048                ("retry", "Retry"),
8049                ("allow", "Run without sandbox once"),
8050                ("allow_thread", "Run without sandbox for this thread"),
8051                ("allow_always", "Always run without sandbox"),
8052                ("deny", "Deny"),
8053            ]
8054        );
8055
8056        authorization
8057            .response
8058            .send(acp_thread::SelectedPermissionOutcome::new(
8059                acp::PermissionOptionId::new(acp_thread::SANDBOX_FALLBACK_RETRY_OPTION_ID),
8060                acp::PermissionOptionKind::RejectAlways,
8061            ))
8062            .unwrap();
8063        assert_eq!(authorize.await.unwrap(), SandboxFallbackDecision::Retry);
8064    }
8065
8066    #[cfg(target_os = "linux")]
8067    #[gpui::test]
8068    async fn test_authorize_sandbox_fallback_retry_label_counts_attempts(cx: &mut TestAppContext) {
8069        crate::tests::init_test(cx);
8070
8071        async fn retry_label(cx: &mut TestAppContext, retries: usize) -> String {
8072            let (event_stream, mut receiver) = ToolCallEventStream::test();
8073            let authorize = cx.update(|cx| {
8074                event_stream.authorize_sandbox_fallback(
8075                    None,
8076                    "probe failed".to_string(),
8077                    None,
8078                    retries,
8079                    cx,
8080                )
8081            });
8082            let authorization = receiver.expect_authorization().await;
8083            let acp_thread::PermissionOptions::Flat(options) = &authorization.options else {
8084                panic!("expected flat fallback permission options");
8085            };
8086            let label = options
8087                .iter()
8088                .find(|option| {
8089                    option.option_id.0.as_ref() == acp_thread::SANDBOX_FALLBACK_RETRY_OPTION_ID
8090                })
8091                .expect("retry option present")
8092                .name
8093                .to_string();
8094            authorization
8095                .response
8096                .send(acp_thread::SelectedPermissionOutcome::new(
8097                    acp::PermissionOptionId::new(acp_thread::SANDBOX_FALLBACK_RETRY_OPTION_ID),
8098                    acp::PermissionOptionKind::RejectAlways,
8099                ))
8100                .unwrap();
8101            authorize.await.unwrap();
8102            label
8103        }
8104
8105        assert_eq!(retry_label(cx, 0).await, "Retry");
8106        assert_eq!(retry_label(cx, 1).await, "Retry (attempt 1)");
8107        assert_eq!(retry_label(cx, 2).await, "Retry (attempt 2)");
8108    }
8109
8110    #[cfg(target_os = "linux")]
8111    #[gpui::test]
8112    async fn test_authorize_sandbox_fallback_allow_thread_records_grant(cx: &mut TestAppContext) {
8113        crate::tests::init_test(cx);
8114
8115        let (event_stream, mut receiver) = ToolCallEventStream::test();
8116        assert!(!event_stream.sandbox_fallback_granted_for_thread());
8117
8118        let authorize = cx.update(|cx| {
8119            event_stream.authorize_sandbox_fallback(
8120                Some("cargo build".to_string()),
8121                "user namespaces are disabled".to_string(),
8122                None,
8123                0,
8124                cx,
8125            )
8126        });
8127        let authorization = receiver.expect_authorization().await;
8128        authorization
8129            .response
8130            .send(acp_thread::SelectedPermissionOutcome::new(
8131                acp::PermissionOptionId::new(acp_thread::SandboxPermission::AllowThread.as_id()),
8132                acp::PermissionOptionKind::AllowAlways,
8133            ))
8134            .unwrap();
8135        assert_eq!(
8136            authorize.await.unwrap(),
8137            SandboxFallbackDecision::RunUnsandboxed
8138        );
8139
8140        // The thread-scoped grant now lets later commands skip the sandbox
8141        // without prompting again.
8142        assert!(event_stream.sandbox_fallback_granted_for_thread());
8143    }
8144
8145    #[cfg(target_os = "linux")]
8146    #[gpui::test]
8147    async fn test_authorize_sandbox_fallback_deny(cx: &mut TestAppContext) {
8148        crate::tests::init_test(cx);
8149
8150        let (event_stream, mut receiver) = ToolCallEventStream::test();
8151        let authorize = cx.update(|cx| {
8152            event_stream.authorize_sandbox_fallback(
8153                None,
8154                "bwrap probe failed".to_string(),
8155                None,
8156                0,
8157                cx,
8158            )
8159        });
8160        let authorization = receiver.expect_authorization().await;
8161        authorization
8162            .response
8163            .send(acp_thread::SelectedPermissionOutcome::new(
8164                acp::PermissionOptionId::new(acp_thread::SandboxPermission::Deny.as_id()),
8165                acp::PermissionOptionKind::RejectOnce,
8166            ))
8167            .unwrap();
8168        assert_eq!(authorize.await.unwrap(), SandboxFallbackDecision::Deny);
8169        assert!(!event_stream.sandbox_fallback_granted_for_thread());
8170    }
8171
8172    #[test]
8173    fn test_auto_resolve_permission_outcome_uses_once_only_options() {
8174        let options = acp_thread::PermissionOptions::Dropdown(vec![
8175            acp_thread::PermissionOptionChoice {
8176                allow: acp::PermissionOption::new(
8177                    acp::PermissionOptionId::new("always_allow:test_tool"),
8178                    "Always allow",
8179                    acp::PermissionOptionKind::AllowAlways,
8180                ),
8181                deny: acp::PermissionOption::new(
8182                    acp::PermissionOptionId::new("always_deny:test_tool"),
8183                    "Always deny",
8184                    acp::PermissionOptionKind::RejectAlways,
8185                ),
8186                sub_patterns: vec![],
8187            },
8188            acp_thread::PermissionOptionChoice {
8189                allow: acp::PermissionOption::new(
8190                    acp::PermissionOptionId::new("allow"),
8191                    "Allow once",
8192                    acp::PermissionOptionKind::AllowOnce,
8193                ),
8194                deny: acp::PermissionOption::new(
8195                    acp::PermissionOptionId::new("deny"),
8196                    "Deny once",
8197                    acp::PermissionOptionKind::RejectOnce,
8198                ),
8199                sub_patterns: vec![],
8200            },
8201        ]);
8202
8203        let allow = auto_resolve_permission_outcome(&options, true)
8204            .expect("allow auto-resolve should use once-only option");
8205        assert_eq!(allow.option_id, acp::PermissionOptionId::new("allow"));
8206        assert_eq!(allow.option_kind, acp::PermissionOptionKind::AllowOnce);
8207
8208        let deny = auto_resolve_permission_outcome(&options, false)
8209            .expect("deny auto-resolve should use once-only option");
8210        assert_eq!(deny.option_id, acp::PermissionOptionId::new("deny"));
8211        assert_eq!(deny.option_kind, acp::PermissionOptionKind::RejectOnce);
8212    }
8213
8214    #[gpui::test]
8215    async fn test_replay_tool_call_replays_image_content(cx: &mut TestAppContext) {
8216        let (thread, _event_stream) = setup_thread_for_test(cx).await;
8217
8218        let registered_tool_use_id = LanguageModelToolUseId::from("registered_tool_id");
8219        let missing_tool_use_id = LanguageModelToolUseId::from("missing_tool_id");
8220        let image_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
8221        let image = LanguageModelImage {
8222            source: image_data.into(),
8223        };
8224
8225        let mut replay_events = cx.update(|cx| {
8226            thread.update(cx, |thread, cx| {
8227                thread.add_tool(ReplayImageTool);
8228
8229                let registered_tool_use = LanguageModelToolUse {
8230                    id: registered_tool_use_id.clone(),
8231                    name: ReplayImageTool::NAME.into(),
8232                    raw_input: "null".to_string(),
8233                    input: language_model::LanguageModelToolUseInput::Json(json!(null)),
8234                    is_input_complete: true,
8235                    thought_signature: None,
8236                };
8237                let missing_tool_use = LanguageModelToolUse {
8238                    id: missing_tool_use_id.clone(),
8239                    name: "missing_image_tool".into(),
8240                    raw_input: "{}".to_string(),
8241                    input: language_model::LanguageModelToolUseInput::Json(json!({})),
8242                    is_input_complete: true,
8243                    thought_signature: None,
8244                };
8245
8246                let mut tool_results = IndexMap::default();
8247                tool_results.insert(
8248                    registered_tool_use_id.clone(),
8249                    LanguageModelToolResult {
8250                        tool_use_id: registered_tool_use_id.clone(),
8251                        tool_name: ReplayImageTool::NAME.into(),
8252                        is_error: false,
8253                        content: vec![
8254                            LanguageModelToolResultContent::Text("before".into()),
8255                            LanguageModelToolResultContent::Image(image.clone()),
8256                            LanguageModelToolResultContent::Text("after".into()),
8257                        ],
8258                        output: Some(json!("raw output")),
8259                    },
8260                );
8261                tool_results.insert(
8262                    missing_tool_use_id.clone(),
8263                    LanguageModelToolResult {
8264                        tool_use_id: missing_tool_use_id.clone(),
8265                        tool_name: "missing_image_tool".into(),
8266                        is_error: false,
8267                        content: vec![LanguageModelToolResultContent::Image(image.clone())],
8268                        output: Some(json!("raw output")),
8269                    },
8270                );
8271
8272                thread.messages.push(Arc::new(Message::Agent(AgentMessage {
8273                    content: vec![
8274                        AgentMessageContent::ToolUse(registered_tool_use),
8275                        AgentMessageContent::ToolUse(missing_tool_use),
8276                    ],
8277                    tool_results,
8278                    reasoning_details: None,
8279                })));
8280
8281                thread.replay(cx)
8282            })
8283        });
8284
8285        let mut tool_use_ids_with_image_content = HashSet::default();
8286        while let Some(event) = replay_events.next().await {
8287            let event = event.unwrap();
8288            if let ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) =
8289                event
8290                && let Some(content) = &update.fields.content
8291                && content.iter().any(|content| {
8292                    matches!(
8293                        content,
8294                        acp::ToolCallContent::Content(acp::Content {
8295                            content: acp::ContentBlock::Image(_),
8296                            ..
8297                        })
8298                    )
8299                })
8300            {
8301                tool_use_ids_with_image_content.insert(update.tool_call_id.to_string());
8302            }
8303        }
8304
8305        // Both tool uses live in the message pushed above, at index 0 (see
8306        // `scoped_tool_call_id`).
8307        assert!(
8308            tool_use_ids_with_image_content
8309                .contains(&scoped_tool_call_id(0, &registered_tool_use_id).to_string())
8310        );
8311        assert!(
8312            tool_use_ids_with_image_content
8313                .contains(&scoped_tool_call_id(0, &missing_tool_use_id).to_string())
8314        );
8315    }
8316
8317    #[gpui::test]
8318    async fn test_set_model_propagates_to_subagents(cx: &mut TestAppContext) {
8319        let (parent, _event_stream) = setup_thread_for_test(cx).await;
8320        let subagents = setup_parent_with_subagents(cx, &parent, 2);
8321
8322        let new_model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel::with_id_and_thinking(
8323            "test-provider",
8324            "new-model",
8325            "New Model",
8326            false,
8327        ));
8328
8329        cx.update(|cx| {
8330            parent.update(cx, |thread, cx| {
8331                thread.set_model(new_model, cx);
8332            });
8333
8334            for subagent in &subagents {
8335                let subagent_model_id = subagent.read(cx).model().unwrap().id();
8336                assert_eq!(
8337                    subagent_model_id.0.as_ref(),
8338                    "new-model",
8339                    "Subagent model should match parent model after set_model"
8340                );
8341            }
8342        });
8343    }
8344
8345    #[gpui::test]
8346    async fn test_set_summarization_model_propagates_to_subagents(cx: &mut TestAppContext) {
8347        let (parent, _event_stream) = setup_thread_for_test(cx).await;
8348        let subagents = setup_parent_with_subagents(cx, &parent, 2);
8349
8350        let summary_model: Arc<dyn LanguageModel> =
8351            Arc::new(FakeLanguageModel::with_id_and_thinking(
8352                "test-provider",
8353                "summary-model",
8354                "Summary Model",
8355                false,
8356            ));
8357
8358        cx.update(|cx| {
8359            parent.update(cx, |thread, cx| {
8360                thread.set_summarization_model(Some(summary_model), cx);
8361            });
8362
8363            for subagent in &subagents {
8364                let subagent_summary_id = subagent.read(cx).summarization_model().unwrap().id();
8365                assert_eq!(
8366                    subagent_summary_id.0.as_ref(),
8367                    "summary-model",
8368                    "Subagent summarization model should match parent after set_summarization_model"
8369                );
8370            }
8371        });
8372    }
8373
8374    #[gpui::test]
8375    async fn test_set_thinking_enabled_propagates_to_subagents(cx: &mut TestAppContext) {
8376        let (parent, _event_stream) = setup_thread_for_test(cx).await;
8377        let subagents = setup_parent_with_subagents(cx, &parent, 2);
8378
8379        cx.update(|cx| {
8380            parent.update(cx, |thread, cx| {
8381                thread.set_thinking_enabled(true, cx);
8382            });
8383
8384            for subagent in &subagents {
8385                assert!(
8386                    subagent.read(cx).thinking_enabled(),
8387                    "Subagent thinking should be enabled after parent enables it"
8388                );
8389            }
8390
8391            parent.update(cx, |thread, cx| {
8392                thread.set_thinking_enabled(false, cx);
8393            });
8394
8395            for subagent in &subagents {
8396                assert!(
8397                    !subagent.read(cx).thinking_enabled(),
8398                    "Subagent thinking should be disabled after parent disables it"
8399                );
8400            }
8401        });
8402    }
8403
8404    #[gpui::test]
8405    async fn test_set_thinking_effort_propagates_to_subagents(cx: &mut TestAppContext) {
8406        let (parent, _event_stream) = setup_thread_for_test(cx).await;
8407        let subagents = setup_parent_with_subagents(cx, &parent, 2);
8408
8409        cx.update(|cx| {
8410            parent.update(cx, |thread, cx| {
8411                thread.set_thinking_effort(Some("high".to_string()), cx);
8412            });
8413
8414            for subagent in &subagents {
8415                assert_eq!(
8416                    subagent.read(cx).thinking_effort().map(|s| s.as_str()),
8417                    Some("high"),
8418                    "Subagent thinking effort should match parent"
8419                );
8420            }
8421
8422            parent.update(cx, |thread, cx| {
8423                thread.set_thinking_effort(None, cx);
8424            });
8425
8426            for subagent in &subagents {
8427                assert_eq!(
8428                    subagent.read(cx).thinking_effort(),
8429                    None,
8430                    "Subagent thinking effort should be None after parent clears it"
8431                );
8432            }
8433        });
8434    }
8435
8436    #[gpui::test]
8437    async fn test_subagent_inherits_settings_at_creation(cx: &mut TestAppContext) {
8438        let (parent, _event_stream) = setup_thread_for_test(cx).await;
8439
8440        cx.update(|cx| {
8441            parent.update(cx, |thread, cx| {
8442                thread.set_speed(Speed::Fast, cx);
8443                thread.set_thinking_enabled(true, cx);
8444                thread.set_thinking_effort(Some("high".to_string()), cx);
8445                thread.set_profile(AgentProfileId("custom-profile".into()), cx);
8446            });
8447        });
8448
8449        let subagents = setup_parent_with_subagents(cx, &parent, 1);
8450
8451        cx.update(|cx| {
8452            let sub = subagents[0].read(cx);
8453            assert_eq!(sub.speed(), Some(Speed::Fast));
8454            assert!(sub.thinking_enabled());
8455            assert_eq!(sub.thinking_effort().map(|s| s.as_str()), Some("high"));
8456            assert_eq!(sub.profile(), &AgentProfileId("custom-profile".into()));
8457        });
8458    }
8459
8460    #[gpui::test]
8461    async fn test_set_speed_propagates_to_subagents(cx: &mut TestAppContext) {
8462        let (parent, _event_stream) = setup_thread_for_test(cx).await;
8463        let subagents = setup_parent_with_subagents(cx, &parent, 2);
8464
8465        cx.update(|cx| {
8466            parent.update(cx, |thread, cx| {
8467                thread.set_speed(Speed::Fast, cx);
8468            });
8469
8470            for subagent in &subagents {
8471                assert_eq!(
8472                    subagent.read(cx).speed(),
8473                    Some(Speed::Fast),
8474                    "Subagent speed should match parent after set_speed"
8475                );
8476            }
8477        });
8478    }
8479
8480    #[gpui::test]
8481    async fn test_dropped_subagent_does_not_panic(cx: &mut TestAppContext) {
8482        let (parent, _event_stream) = setup_thread_for_test(cx).await;
8483        let subagents = setup_parent_with_subagents(cx, &parent, 1);
8484
8485        // Drop the subagent so the WeakEntity can no longer be upgraded
8486        drop(subagents);
8487
8488        // Should not panic even though the subagent was dropped
8489        cx.update(|cx| {
8490            parent.update(cx, |thread, cx| {
8491                thread.set_thinking_enabled(true, cx);
8492                thread.set_speed(Speed::Fast, cx);
8493                thread.set_thinking_effort(Some("high".to_string()), cx);
8494            });
8495        });
8496    }
8497
8498    #[gpui::test]
8499    async fn test_handle_tool_use_json_parse_error_adds_tool_use_to_content(
8500        cx: &mut TestAppContext,
8501    ) {
8502        let (thread, event_stream) = setup_thread_for_test(cx).await;
8503
8504        let tool_use_id = LanguageModelToolUseId::from("test_tool_id");
8505        let tool_name: Arc<str> = Arc::from("test_tool");
8506        let raw_input: Arc<str> = Arc::from("{invalid json");
8507        let json_parse_error = "expected value at line 1 column 1".to_string();
8508
8509        let (_cancellation_tx, cancellation_rx) = watch::channel(false);
8510
8511        let (_owning_message_ix, result) = cx
8512            .update(|cx| {
8513                thread.update(cx, |thread, cx| {
8514                    // Call the function under test
8515                    thread
8516                        .handle_tool_use_json_parse_error_event(
8517                            tool_use_id.clone(),
8518                            tool_name.clone(),
8519                            raw_input.clone(),
8520                            json_parse_error,
8521                            &event_stream,
8522                            cancellation_rx,
8523                            cx,
8524                        )
8525                        .unwrap()
8526                })
8527            })
8528            .await;
8529
8530        // Verify the result is an error
8531        assert!(result.is_error);
8532        assert_eq!(result.tool_use_id, tool_use_id);
8533        assert_eq!(result.tool_name, tool_name);
8534        assert!(matches!(
8535            result.content.as_slice(),
8536            [LanguageModelToolResultContent::Text(_)]
8537        ));
8538
8539        thread.update(cx, |thread, _cx| {
8540            // Verify the tool use was added to the message content
8541            {
8542                let last_message = thread.pending_message();
8543                assert_eq!(
8544                    last_message.content.len(),
8545                    1,
8546                    "Should have one tool_use in content"
8547                );
8548
8549                match &last_message.content[0] {
8550                    AgentMessageContent::ToolUse(tool_use) => {
8551                        assert_eq!(tool_use.id, tool_use_id);
8552                        assert_eq!(tool_use.name, tool_name);
8553                        assert_eq!(tool_use.raw_input, raw_input.to_string());
8554                        assert!(tool_use.is_input_complete);
8555                        // Should fall back to empty object for invalid JSON
8556                        assert_eq!(
8557                            tool_use.input,
8558                            language_model::LanguageModelToolUseInput::Json(json!({}))
8559                        );
8560                    }
8561                    _ => panic!("Expected ToolUse content"),
8562                }
8563            }
8564
8565            // Insert the tool result (simulating what the caller does)
8566            thread
8567                .pending_message()
8568                .tool_results
8569                .insert(result.tool_use_id.clone(), result);
8570
8571            // Verify the tool result was added
8572            let last_message = thread.pending_message();
8573            assert_eq!(
8574                last_message.tool_results.len(),
8575                1,
8576                "Should have one tool_result"
8577            );
8578            assert!(last_message.tool_results.contains_key(&tool_use_id));
8579        })
8580    }
8581}
8582
Served at tenant.openagents/omega Member data and write actions are omitted.