Skip to repository content8582 lines · 331.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:30:30.411Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
thread.rs
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, ®istered_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