agent: Return `ToolResult` from `run` inside `Tool` (#28763)

Bennet Bo Fenner created

This is just a refactor which adds no functionality.
We now return a `ToolResult` from `Tool > run(...)`. For now this just
wraps the output task in a struct. We'll use this to implement custom
rendering of tools, see #28621.

Release Notes:

- N/A

Change summary

crates/agent/src/thread.rs                           |  6 +-
crates/assistant_tool/src/assistant_tool.rs          | 15 ++++++++
crates/assistant_tools/src/batch_tool.rs             | 13 ++++---
crates/assistant_tools/src/code_action_tool.rs       |  8 ++--
crates/assistant_tools/src/code_symbols_tool.rs      |  9 +++--
crates/assistant_tools/src/contents_tool.rs          | 18 +++++-----
crates/assistant_tools/src/copy_path_tool.rs         |  7 ++-
crates/assistant_tools/src/create_directory_tool.rs  | 11 ++++--
crates/assistant_tools/src/create_file_tool.rs       | 11 ++++--
crates/assistant_tools/src/delete_path_tool.rs       | 13 +++++---
crates/assistant_tools/src/diagnostics_tool.rs       | 11 ++++--
crates/assistant_tools/src/fetch_tool.rs             | 22 +++++++------
crates/assistant_tools/src/find_replace_file_tool.rs |  8 ++--
crates/assistant_tools/src/list_directory_tool.rs    | 20 ++++++------
crates/assistant_tools/src/move_path_tool.rs         |  7 ++-
crates/assistant_tools/src/now_tool.rs               |  8 ++--
crates/assistant_tools/src/open_tool.rs              |  7 ++-
crates/assistant_tools/src/path_search_tool.rs       | 10 +++---
crates/assistant_tools/src/read_file_tool.rs         | 10 +++---
crates/assistant_tools/src/regex_search_tool.rs      | 10 +++---
crates/assistant_tools/src/rename_tool.rs            |  8 ++--
crates/assistant_tools/src/symbol_info_tool.rs       |  8 ++--
crates/assistant_tools/src/terminal_tool.rs          | 19 +++++++----
crates/assistant_tools/src/thinking_tool.rs          |  5 +-
crates/context_server/src/context_server_tool.rs     |  7 ++-
25 files changed, 155 insertions(+), 116 deletions(-)

Detailed changes

crates/agent/src/thread.rs 🔗

@@ -1407,8 +1407,8 @@ impl Thread {
     ) -> Task<()> {
         let tool_name: Arc<str> = tool.name().into();
 
-        let run_tool = if self.tools.read(cx).is_disabled(&tool.source(), &tool_name) {
-            Task::ready(Err(anyhow!("tool is disabled: {tool_name}")))
+        let tool_result = if self.tools.read(cx).is_disabled(&tool.source(), &tool_name) {
+            Task::ready(Err(anyhow!("tool is disabled: {tool_name}"))).into()
         } else {
             tool.run(
                 input,
@@ -1421,7 +1421,7 @@ impl Thread {
 
         cx.spawn({
             async move |thread: WeakEntity<Thread>, cx| {
-                let output = run_tool.await;
+                let output = tool_result.output.await;
 
                 thread
                     .update(cx, |thread, cx| {

crates/assistant_tool/src/assistant_tool.rs 🔗

@@ -24,6 +24,19 @@ pub fn init(cx: &mut App) {
     ToolRegistry::default_global(cx);
 }
 
+/// The result of running a tool
+pub struct ToolResult {
+    /// The asynchronous task that will eventually resolve to the tool's output
+    pub output: Task<Result<String>>,
+}
+
+impl From<Task<Result<String>>> for ToolResult {
+    /// Convert from a task to a ToolResult
+    fn from(output: Task<Result<String>>) -> Self {
+        Self { output }
+    }
+}
+
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
 pub enum ToolSource {
     /// A native tool built-in to Zed.
@@ -68,7 +81,7 @@ pub trait Tool: 'static + Send + Sync {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>>;
+    ) -> ToolResult;
 }
 
 impl Debug for dyn Tool {

crates/assistant_tools/src/batch_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
+use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
 use futures::future::join_all;
 use gpui::{App, AppContext, Entity, Task};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -219,14 +219,14 @@ impl Tool for BatchTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<BatchToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         if input.invocations.is_empty() {
-            return Task::ready(Err(anyhow!("No tool invocations provided")));
+            return Task::ready(Err(anyhow!("No tool invocations provided"))).into();
         }
 
         let run_tools_concurrently = input.run_tools_concurrently;
@@ -257,11 +257,11 @@ impl Tool for BatchTool {
                     let project = project.clone();
                     let action_log = action_log.clone();
                     let messages = messages.clone();
-                    let task = cx
+                    let tool_result = cx
                         .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
                         .map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
 
-                    tasks.push(task);
+                    tasks.push(tool_result.output);
                 }
 
                 Ok((tasks, tool_names))
@@ -306,5 +306,6 @@ impl Tool for BatchTool {
 
             Ok(formatted_results.trim().to_string())
         })
+        .into()
     }
 }

crates/assistant_tools/src/code_action_tool.rs 🔗

@@ -1,5 +1,5 @@
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, Entity, Task};
 use language::{self, Anchor, Buffer, ToPointUtf16};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -141,10 +141,10 @@ impl Tool for CodeActionTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<CodeActionToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         cx.spawn(async move |cx| {
@@ -319,7 +319,7 @@ impl Tool for CodeActionTool {
 
                 Ok(response)
             }
-        })
+        }).into()
     }
 }
 

crates/assistant_tools/src/code_symbols_tool.rs 🔗

@@ -4,7 +4,7 @@ use std::sync::Arc;
 
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use collections::IndexMap;
 use gpui::{App, AsyncApp, Entity, Task};
 use language::{OutlineItem, ParseStatus, Point};
@@ -129,10 +129,10 @@ impl Tool for CodeSymbolsTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         let regex = match input.regex {
@@ -141,7 +141,7 @@ impl Tool for CodeSymbolsTool {
                 .build()
             {
                 Ok(regex) => Some(regex),
-                Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))),
+                Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))).into(),
             },
             None => None,
         };
@@ -150,6 +150,7 @@ impl Tool for CodeSymbolsTool {
             Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
             None => project_symbols(project, regex, input.offset, cx).await,
         })
+        .into()
     }
 }
 

crates/assistant_tools/src/contents_tool.rs 🔗

@@ -2,7 +2,7 @@ use std::sync::Arc;
 
 use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, Entity, Task};
 use itertools::Itertools;
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -103,10 +103,10 @@ impl Tool for ContentsTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<ContentsToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         // Sometimes models will return these even though we tell it to give a path and not a glob.
@@ -127,23 +127,23 @@ impl Tool for ContentsTool {
                 .collect::<Vec<_>>()
                 .join("\n");
 
-            return Task::ready(Ok(output));
+            return Task::ready(Ok(output)).into();
         }
 
         let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
-            return Task::ready(Err(anyhow!("Path {} not found in project", &input.path)));
+            return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
         };
 
         let Some(worktree) = project
             .read(cx)
             .worktree_for_id(project_path.worktree_id, cx)
         else {
-            return Task::ready(Err(anyhow!("Worktree not found")));
+            return Task::ready(Err(anyhow!("Worktree not found"))).into();
         };
         let worktree = worktree.read(cx);
 
         let Some(entry) = worktree.entry_for_path(&project_path.path) else {
-            return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
+            return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
         };
 
         // If it's a directory, list its contents
@@ -184,7 +184,7 @@ impl Tool for ContentsTool {
                 ).ok();
             }
 
-            Task::ready(Ok(output))
+            Task::ready(Ok(output)).into()
         } else {
             // It's a file, so read its contents
             let file_path = input.path.clone();
@@ -233,7 +233,7 @@ impl Tool for ContentsTool {
                         Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start and end fields to see the implementations of symbols in the outline."))
                     }
                 }
-            })
+            }).into()
         }
     }
 }

crates/assistant_tools/src/copy_path_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, AppContext, Entity, Task};
 use language_model::LanguageModelRequestMessage;
 use language_model::LanguageModelToolSchemaFormat;
@@ -77,10 +77,10 @@ impl Tool for CopyPathTool {
         project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<CopyPathToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
         let copy_task = project.update(cx, |project, cx| {
             match project
@@ -117,5 +117,6 @@ impl Tool for CopyPathTool {
                 )),
             }
         })
+        .into()
     }
 }

crates/assistant_tools/src/create_directory_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, Entity, Task};
 use language_model::LanguageModelRequestMessage;
 use language_model::LanguageModelToolSchemaFormat;
@@ -68,14 +68,16 @@ impl Tool for CreateDirectoryTool {
         project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
         let project_path = match project.read(cx).find_project_path(&input.path, cx) {
             Some(project_path) => project_path,
-            None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
+            None => {
+                return Task::ready(Err(anyhow!("Path to create was outside the project"))).into();
+            }
         };
         let destination_path: Arc<str> = input.path.as_str().into();
 
@@ -89,5 +91,6 @@ impl Tool for CreateDirectoryTool {
 
             Ok(format!("Created directory {destination_path}"))
         })
+        .into()
     }
 }

crates/assistant_tools/src/create_file_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, Entity, Task};
 use language_model::LanguageModelRequestMessage;
 use language_model::LanguageModelToolSchemaFormat;
@@ -73,14 +73,16 @@ impl Tool for CreateFileTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<CreateFileToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
         let project_path = match project.read(cx).find_project_path(&input.path, cx) {
             Some(project_path) => project_path,
-            None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
+            None => {
+                return Task::ready(Err(anyhow!("Path to create was outside the project"))).into();
+            }
         };
         let contents: Arc<str> = input.contents.as_str().into();
         let destination_path: Arc<str> = input.path.as_str().into();
@@ -106,5 +108,6 @@ impl Tool for CreateFileTool {
 
             Ok(format!("Created file {destination_path}"))
         })
+        .into()
     }
 }

crates/assistant_tools/src/delete_path_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use futures::{SinkExt, StreamExt, channel::mpsc};
 use gpui::{App, AppContext, Entity, Task};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -63,15 +63,16 @@ impl Tool for DeletePathTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
             Ok(input) => input.path,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
         let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else {
             return Task::ready(Err(anyhow!(
                 "Couldn't delete {path_str} because that path isn't in this project."
-            )));
+            )))
+            .into();
         };
 
         let Some(worktree) = project
@@ -80,7 +81,8 @@ impl Tool for DeletePathTool {
         else {
             return Task::ready(Err(anyhow!(
                 "Couldn't delete {path_str} because that path isn't in this project."
-            )));
+            )))
+            .into();
         };
 
         let worktree_snapshot = worktree.read(cx).snapshot();
@@ -132,5 +134,6 @@ impl Tool for DeletePathTool {
                 )),
             }
         })
+        .into()
     }
 }

crates/assistant_tools/src/diagnostics_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, Entity, Task};
 use language::{DiagnosticSeverity, OffsetRangeExt};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -83,14 +83,15 @@ impl Tool for DiagnosticsTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         match serde_json::from_value::<DiagnosticsToolInput>(input)
             .ok()
             .and_then(|input| input.path)
         {
             Some(path) if !path.is_empty() => {
                 let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
-                    return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
+                    return Task::ready(Err(anyhow!("Could not find path {path} in project",)))
+                        .into();
                 };
 
                 let buffer =
@@ -125,6 +126,7 @@ impl Tool for DiagnosticsTool {
                         Ok(output)
                     }
                 })
+                .into()
             }
             _ => {
                 let project = project.read(cx);
@@ -155,9 +157,10 @@ impl Tool for DiagnosticsTool {
                 });
 
                 if has_diagnostics {
-                    Task::ready(Ok(output))
+                    Task::ready(Ok(output)).into()
                 } else {
                     Task::ready(Ok("No errors or warnings found in the project.".to_string()))
+                        .into()
                 }
             }
         }

crates/assistant_tools/src/fetch_tool.rs 🔗

@@ -4,7 +4,7 @@ use std::sync::Arc;
 
 use crate::schema::json_schema_for;
 use anyhow::{Context as _, Result, anyhow, bail};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use futures::AsyncReadExt as _;
 use gpui::{App, AppContext as _, Entity, Task};
 use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
@@ -146,10 +146,10 @@ impl Tool for FetchTool {
         _project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<FetchToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         let text = cx.background_spawn({
@@ -158,13 +158,15 @@ impl Tool for FetchTool {
             async move { Self::build_message(http_client, &url).await }
         });
 
-        cx.foreground_executor().spawn(async move {
-            let text = text.await?;
-            if text.trim().is_empty() {
-                bail!("no textual content found");
-            }
+        cx.foreground_executor()
+            .spawn(async move {
+                let text = text.await?;
+                if text.trim().is_empty() {
+                    bail!("no textual content found");
+                }
 
-            Ok(text)
-        })
+                Ok(text)
+            })
+            .into()
     }
 }

crates/assistant_tools/src/find_replace_file_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, AppContext, AsyncApp, Entity, Task};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
 use project::Project;
@@ -169,10 +169,10 @@ impl Tool for FindReplaceFileTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         cx.spawn(async move |cx: &mut AsyncApp| {
@@ -263,6 +263,6 @@ impl Tool for FindReplaceFileTool {
 
             Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
 
-        })
+        }).into()
     }
 }

crates/assistant_tools/src/list_directory_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, Entity, Task};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
 use project::Project;
@@ -77,10 +77,10 @@ impl Tool for ListDirectoryTool {
         project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         // Sometimes models will return these even though we tell it to give a path and not a glob.
@@ -101,26 +101,26 @@ impl Tool for ListDirectoryTool {
                 .collect::<Vec<_>>()
                 .join("\n");
 
-            return Task::ready(Ok(output));
+            return Task::ready(Ok(output)).into();
         }
 
         let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
-            return Task::ready(Err(anyhow!("Path {} not found in project", input.path)));
+            return Task::ready(Err(anyhow!("Path {} not found in project", input.path))).into();
         };
         let Some(worktree) = project
             .read(cx)
             .worktree_for_id(project_path.worktree_id, cx)
         else {
-            return Task::ready(Err(anyhow!("Worktree not found")));
+            return Task::ready(Err(anyhow!("Worktree not found"))).into();
         };
         let worktree = worktree.read(cx);
 
         let Some(entry) = worktree.entry_for_path(&project_path.path) else {
-            return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
+            return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
         };
 
         if !entry.is_dir() {
-            return Task::ready(Err(anyhow!("{} is not a directory.", input.path)));
+            return Task::ready(Err(anyhow!("{} is not a directory.", input.path))).into();
         }
 
         let mut output = String::new();
@@ -133,8 +133,8 @@ impl Tool for ListDirectoryTool {
             .unwrap();
         }
         if output.is_empty() {
-            return Task::ready(Ok(format!("{} is empty.", input.path)));
+            return Task::ready(Ok(format!("{} is empty.", input.path))).into();
         }
-        Task::ready(Ok(output))
+        Task::ready(Ok(output)).into()
     }
 }

crates/assistant_tools/src/move_path_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, AppContext, Entity, Task};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
 use project::Project;
@@ -90,10 +90,10 @@ impl Tool for MovePathTool {
         project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<MovePathToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
         let rename_task = project.update(cx, |project, cx| {
             match project
@@ -128,5 +128,6 @@ impl Tool for MovePathTool {
                 )),
             }
         })
+        .into()
     }
 }

crates/assistant_tools/src/now_tool.rs 🔗

@@ -2,7 +2,7 @@ use std::sync::Arc;
 
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use chrono::{Local, Utc};
 use gpui::{App, Entity, Task};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -60,10 +60,10 @@ impl Tool for NowTool {
         _project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         _cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input: NowToolInput = match serde_json::from_value(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         let now = match input.timezone {
@@ -72,6 +72,6 @@ impl Tool for NowTool {
         };
         let text = format!("The current datetime is {now}.");
 
-        Task::ready(Ok(text))
+        Task::ready(Ok(text)).into()
     }
 }

crates/assistant_tools/src/open_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, AppContext, Entity, Task};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
 use project::Project;
@@ -53,10 +53,10 @@ impl Tool for OpenTool {
         _project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input: OpenToolInput = match serde_json::from_value(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         cx.background_spawn(async move {
@@ -64,5 +64,6 @@ impl Tool for OpenTool {
 
             Ok(format!("Successfully opened {}", input.path_or_url))
         })
+        .into()
     }
 }

crates/assistant_tools/src/path_search_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, AppContext, Entity, Task};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
 use project::Project;
@@ -71,10 +71,10 @@ impl Tool for PathSearchTool {
         project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
             Ok(input) => (input.offset, input.glob),
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         let path_matcher = match PathMatcher::new([
@@ -82,7 +82,7 @@ impl Tool for PathSearchTool {
             if glob.is_empty() { "*" } else { &glob },
         ]) {
             Ok(matcher) => matcher,
-            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
+            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))).into(),
         };
         let snapshots: Vec<Snapshot> = project
             .read(cx)
@@ -136,6 +136,6 @@ impl Tool for PathSearchTool {
 
                 Ok(response)
             }
-        })
+        }).into()
     }
 }

crates/assistant_tools/src/read_file_tool.rs 🔗

@@ -2,7 +2,7 @@ use std::sync::Arc;
 
 use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, Entity, Task};
 use itertools::Itertools;
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -88,14 +88,14 @@ impl Tool for ReadFileTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<ReadFileToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
-            return Task::ready(Err(anyhow!("Path {} not found in project", &input.path,)));
+            return Task::ready(Err(anyhow!("Path {} not found in project", &input.path,))).into();
         };
 
         let file_path = input.path.clone();
@@ -146,6 +146,6 @@ impl Tool for ReadFileTool {
                     Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start_line and end_line fields to see the implementations of symbols in the outline."))
                 }
             }
-        })
+        }).into()
     }
 }

crates/assistant_tools/src/regex_search_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use futures::StreamExt;
 use gpui::{App, Entity, Task};
 use language::OffsetRangeExt;
@@ -92,13 +92,13 @@ impl Tool for RegexSearchTool {
         project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         const CONTEXT_LINES: u32 = 2;
 
         let (offset, regex, case_sensitive) =
             match serde_json::from_value::<RegexSearchToolInput>(input) {
                 Ok(input) => (input.offset, input.regex, input.case_sensitive),
-                Err(err) => return Task::ready(Err(anyhow!(err))),
+                Err(err) => return Task::ready(Err(anyhow!(err))).into(),
             };
 
         let query = match SearchQuery::regex(
@@ -112,7 +112,7 @@ impl Tool for RegexSearchTool {
             None,
         ) {
             Ok(query) => query,
-            Err(error) => return Task::ready(Err(error)),
+            Err(error) => return Task::ready(Err(error)).into(),
         };
 
         let results = project.update(cx, |project, cx| project.search(query, cx));
@@ -201,6 +201,6 @@ impl Tool for RegexSearchTool {
             } else {
                 Ok(format!("Found {matches_found} matches:\n{output}"))
             }
-        })
+        }).into()
     }
 }

crates/assistant_tools/src/rename_tool.rs 🔗

@@ -1,5 +1,5 @@
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, Entity, Task};
 use language::{self, Buffer, ToPointUtf16};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -88,10 +88,10 @@ impl Tool for RenameTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<RenameToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         cx.spawn(async move |cx| {
@@ -138,7 +138,7 @@ impl Tool for RenameTool {
             })?;
 
             Ok(format!("Renamed '{}' to '{}'", input.symbol, input.new_name))
-        })
+        }).into()
     }
 }
 

crates/assistant_tools/src/symbol_info_tool.rs 🔗

@@ -1,5 +1,5 @@
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, AsyncApp, Entity, Task};
 use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -122,10 +122,10 @@ impl Tool for SymbolInfoTool {
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         cx.spawn(async move |cx| {
@@ -205,7 +205,7 @@ impl Tool for SymbolInfoTool {
             } else {
                 Ok(output)
             }
-        })
+        }).into()
     }
 }
 

crates/assistant_tools/src/terminal_tool.rs 🔗

@@ -1,6 +1,6 @@
 use crate::schema::json_schema_for;
 use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use futures::io::BufReader;
 use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};
 use gpui::{App, AppContext, Entity, Task};
@@ -79,10 +79,10 @@ impl Tool for TerminalTool {
         project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         let input: TerminalToolInput = match serde_json::from_value(input) {
             Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))),
+            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
         };
 
         let project = project.read(cx);
@@ -93,13 +93,15 @@ impl Tool for TerminalTool {
 
             let only_worktree = match worktrees.next() {
                 Some(worktree) => worktree,
-                None => return Task::ready(Err(anyhow!("No worktrees found in the project"))),
+                None => {
+                    return Task::ready(Err(anyhow!("No worktrees found in the project"))).into();
+                }
             };
 
             if worktrees.next().is_some() {
                 return Task::ready(Err(anyhow!(
                     "'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly."
-                )));
+                ))).into();
             }
 
             only_worktree.read(cx).abs_path()
@@ -111,7 +113,8 @@ impl Tool for TerminalTool {
             {
                 return Task::ready(Err(anyhow!(
                     "The absolute path must be within one of the project's worktrees"
-                )));
+                )))
+                .into();
             }
 
             input_path.into()
@@ -120,13 +123,15 @@ impl Tool for TerminalTool {
                 return Task::ready(Err(anyhow!(
                     "`cd` directory {} not found in the project",
                     &input.cd
-                )));
+                )))
+                .into();
             };
 
             worktree.read(cx).abs_path()
         };
 
         cx.background_spawn(run_command_limited(working_dir, input.command))
+            .into()
     }
 }
 

crates/assistant_tools/src/thinking_tool.rs 🔗

@@ -2,7 +2,7 @@ use std::sync::Arc;
 
 use crate::schema::json_schema_for;
 use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
+use assistant_tool::{ActionLog, Tool, ToolResult};
 use gpui::{App, Entity, Task};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
 use project::Project;
@@ -51,11 +51,12 @@ impl Tool for ThinkingTool {
         _project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         _cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         // This tool just "thinks out loud" and doesn't perform any actions.
         Task::ready(match serde_json::from_value::<ThinkingToolInput>(input) {
             Ok(_input) => Ok("Finished thinking.".to_string()),
             Err(err) => Err(anyhow!(err)),
         })
+        .into()
     }
 }

crates/context_server/src/context_server_tool.rs 🔗

@@ -1,7 +1,7 @@
 use std::sync::Arc;
 
 use anyhow::{Result, anyhow, bail};
-use assistant_tool::{ActionLog, Tool, ToolSource};
+use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
 use gpui::{App, Entity, Task};
 use icons::IconName;
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -78,7 +78,7 @@ impl Tool for ContextServerTool {
         _project: Entity<Project>,
         _action_log: Entity<ActionLog>,
         cx: &mut App,
-    ) -> Task<Result<String>> {
+    ) -> ToolResult {
         if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
             let tool_name = self.tool.name.clone();
             let server_clone = server.clone();
@@ -118,8 +118,9 @@ impl Tool for ContextServerTool {
                 }
                 Ok(result)
             })
+            .into()
         } else {
-            Task::ready(Err(anyhow!("Context server not found")))
+            Task::ready(Err(anyhow!("Context server not found"))).into()
         }
     }
 }