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

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
This commit is contained in:
Bennet Bo Fenner 2025-04-15 08:28:09 -06:00 committed by GitHub
parent 32829d9f12
commit 5e094553fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 155 additions and 116 deletions

View file

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

View file

@ -24,6 +24,19 @@ pub fn init(cx: &mut App) {
ToolRegistry::default_global(cx); 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)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum ToolSource { pub enum ToolSource {
/// A native tool built-in to Zed. /// A native tool built-in to Zed.
@ -68,7 +81,7 @@ pub trait Tool: 'static + Send + Sync {
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
cx: &mut App, cx: &mut App,
) -> Task<Result<String>>; ) -> ToolResult;
} }
impl Debug for dyn Tool { impl Debug for dyn Tool {

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use crate::{code_symbols_tool::file_outline, schema::json_schema_for}; use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool}; use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task}; use gpui::{App, Entity, Task};
use itertools::Itertools; use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@ -103,10 +103,10 @@ impl Tool for ContentsTool {
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
cx: &mut App, cx: &mut App,
) -> Task<Result<String>> { ) -> ToolResult {
let input = match serde_json::from_value::<ContentsToolInput>(input) { let input = match serde_json::from_value::<ContentsToolInput>(input) {
Ok(input) => 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. // 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<_>>() .collect::<Vec<_>>()
.join("\n"); .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 { 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 let Some(worktree) = project
.read(cx) .read(cx)
.worktree_for_id(project_path.worktree_id, cx) .worktree_for_id(project_path.worktree_id, cx)
else { else {
return Task::ready(Err(anyhow!("Worktree not found"))); return Task::ready(Err(anyhow!("Worktree not found"))).into();
}; };
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
let Some(entry) = worktree.entry_for_path(&project_path.path) else { 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 // If it's a directory, list its contents
@ -184,7 +184,7 @@ impl Tool for ContentsTool {
).ok(); ).ok();
} }
Task::ready(Ok(output)) Task::ready(Ok(output)).into()
} else { } else {
// It's a file, so read its contents // It's a file, so read its contents
let file_path = input.path.clone(); 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.")) 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()
} }
} }
} }

View file

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

View file

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

View file

@ -1,6 +1,6 @@
use crate::schema::json_schema_for; use crate::schema::json_schema_for;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool}; use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task}; use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage; use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat; use language_model::LanguageModelToolSchemaFormat;
@ -73,14 +73,16 @@ impl Tool for CreateFileTool {
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
cx: &mut App, cx: &mut App,
) -> Task<Result<String>> { ) -> ToolResult {
let input = match serde_json::from_value::<CreateFileToolInput>(input) { let input = match serde_json::from_value::<CreateFileToolInput>(input) {
Ok(input) => 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) { let project_path = match project.read(cx).find_project_path(&input.path, cx) {
Some(project_path) => project_path, 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 contents: Arc<str> = input.contents.as_str().into();
let destination_path: Arc<str> = input.path.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}")) Ok(format!("Created file {destination_path}"))
}) })
.into()
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
use crate::schema::json_schema_for; use crate::schema::json_schema_for;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool}; use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task}; use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project; use project::Project;
@ -77,10 +77,10 @@ impl Tool for ListDirectoryTool {
project: Entity<Project>, project: Entity<Project>,
_action_log: Entity<ActionLog>, _action_log: Entity<ActionLog>,
cx: &mut App, cx: &mut App,
) -> Task<Result<String>> { ) -> ToolResult {
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) { let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
Ok(input) => 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. // 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<_>>() .collect::<Vec<_>>()
.join("\n"); .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 { 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 let Some(worktree) = project
.read(cx) .read(cx)
.worktree_for_id(project_path.worktree_id, cx) .worktree_for_id(project_path.worktree_id, cx)
else { else {
return Task::ready(Err(anyhow!("Worktree not found"))); return Task::ready(Err(anyhow!("Worktree not found"))).into();
}; };
let worktree = worktree.read(cx); let worktree = worktree.read(cx);
let Some(entry) = worktree.entry_for_path(&project_path.path) else { 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() { 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(); let mut output = String::new();
@ -133,8 +133,8 @@ impl Tool for ListDirectoryTool {
.unwrap(); .unwrap();
} }
if output.is_empty() { 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()
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use crate::{code_symbols_tool::file_outline, schema::json_schema_for}; use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool}; use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task}; use gpui::{App, Entity, Task};
use itertools::Itertools; use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}; use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@ -88,14 +88,14 @@ impl Tool for ReadFileTool {
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
cx: &mut App, cx: &mut App,
) -> Task<Result<String>> { ) -> ToolResult {
let input = match serde_json::from_value::<ReadFileToolInput>(input) { let input = match serde_json::from_value::<ReadFileToolInput>(input) {
Ok(input) => 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 { 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(); 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.")) 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()
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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