Compare commits

...

14 Commits

Author SHA1 Message Date
Richard Feldman
ac63ea504a wip 2025-04-16 11:10:25 -04:00
Richard Feldman
11aaea964f Use ToolOutput in rendering 2025-04-09 11:05:37 -04:00
Richard Feldman
cd9a45a262 Merge remote-tracking branch 'origin/main' into tool-rendering 2025-04-09 10:23:27 -04:00
Richard Feldman
1965029528 Refactor tool rendering API 2025-04-09 10:14:27 -04:00
Richard Feldman
46c738d724 Delete unused impl 2025-04-08 16:49:52 -04:00
Richard Feldman
b18bd2d67b Revert "Use erased_serde for ToolOutput serialization"
This reverts commit cad3ee8def.
2025-04-08 16:33:55 -04:00
Richard Feldman
7e0a97fabe Revert "wip"
This reverts commit 8ba9429e60.
2025-04-08 16:33:50 -04:00
Richard Feldman
8ba9429e60 wip 2025-04-08 16:33:49 -04:00
Richard Feldman
cad3ee8def Use erased_serde for ToolOutput serialization 2025-04-08 14:19:54 -04:00
Richard Feldman
42cb40170e Implement missing traits 2025-04-08 14:07:50 -04:00
Richard Feldman
7dc3a39edf Break cyclic dep 2025-04-08 14:07:40 -04:00
Richard Feldman
fc1b3889c2 wip 2025-04-08 13:37:59 -04:00
Richard Feldman
34c6fee959 Add ToolOutput 2025-04-08 12:52:18 -04:00
Richard Feldman
b25ec65b48 Add custom render method to Tool trait 2025-04-08 11:26:08 -04:00
26 changed files with 229 additions and 114 deletions

View File

@@ -21,7 +21,10 @@ use gpui::{
linear_color_stop, linear_gradient, list, percentage, pulsating_between, linear_color_stop, linear_gradient, list, percentage, pulsating_between,
}; };
use language::{Buffer, LanguageRegistry}; use language::{Buffer, LanguageRegistry};
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelToolUseId, Role}; use language_model::{
ConfiguredModel, LanguageModelRegistry, LanguageModelToolUseId, Role, StringToolOutput,
ToolOutput,
};
use markdown::parser::CodeBlockKind; use markdown::parser::CodeBlockKind;
use markdown::{Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, without_fences}; use markdown::{Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, without_fences};
use project::ProjectItem as _; use project::ProjectItem as _;
@@ -36,6 +39,7 @@ use text::ToPoint;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*}; use ui::{Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::ResultExt as _; use util::ResultExt as _;
use util::markdown::MarkdownString;
use workspace::{OpenOptions, Workspace}; use workspace::{OpenOptions, Workspace};
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
@@ -74,7 +78,7 @@ struct RenderedMessage {
struct RenderedToolUse { struct RenderedToolUse {
label: Entity<Markdown>, label: Entity<Markdown>,
input: Entity<Markdown>, input: Entity<Markdown>,
output: Entity<Markdown>, output: ToolOutput,
} }
impl RenderedMessage { impl RenderedMessage {
@@ -729,21 +733,28 @@ impl ActiveThread {
tool_use_id: LanguageModelToolUseId, tool_use_id: LanguageModelToolUseId,
tool_label: impl Into<SharedString>, tool_label: impl Into<SharedString>,
tool_input: &serde_json::Value, tool_input: &serde_json::Value,
tool_output: SharedString, tool_output: ToolOutput,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let rendered = RenderedToolUse { let rendered = RenderedToolUse {
label: render_tool_use_markdown(tool_label.into(), self.language_registry.clone(), cx), label: render_tool_use_markdown(tool_label.into(), self.language_registry.clone(), cx),
input: render_tool_use_markdown( input: render_tool_use_markdown(
format!( MarkdownString::code_block(
"```json\n{}\n```", "json",
serde_json::to_string_pretty(tool_input).unwrap_or_default() &serde_json::to_string_pretty(tool_input).unwrap_or_default(),
) )
.to_string()
.into(), .into(),
self.language_registry.clone(), self.language_registry.clone(),
cx, cx,
), ),
output: render_tool_use_markdown(tool_output, self.language_registry.clone(), cx), output: render_tool_use_markdown(
tool_output.clone(),
self.language_registry.clone(),
cx,
),
output: StringToolOutput::new(tool_output, language_registry: Arc<LanguageRegistry>),
}; };
self.rendered_tool_uses self.rendered_tool_uses
.insert(tool_use_id.clone(), rendered); .insert(tool_use_id.clone(), rendered);
@@ -2094,24 +2105,50 @@ impl ActiveThread {
results_content_container() results_content_container()
.border_t_1() .border_t_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
.child(
Label::new("Result")
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
)
.child(div().w_full().text_ui_sm(cx).children( .child(div().w_full().text_ui_sm(cx).children(
rendered_tool_use.as_ref().map(|rendered| { rendered_tool_use.as_ref().map(|rendered| {
MarkdownElement::new( let tool_name = tool_use.name.to_string();
rendered.output.clone(), let tool_registry = assistant_tool::ToolRegistry::global(cx);
tool_use_markdown_style(window, cx),
) if let Some(_tool) = tool_registry.tool(&tool_name) {
.on_url_click({ // Tool doesn't have a render method, but ToolOutput does
let workspace = self.workspace.clone(); match rendered.output.render(window, cx) {
move |text, window, cx| { Some(rendered) => rendered,
open_markdown_link(text, workspace.clone(), window, cx); None => {
// Default to rendering the output as markdown
div()
.child(
Label::new("Result")
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
)
.child(
div().w_full().text_ui_sm(cx).child(
MarkdownElement::new(
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(
text,
workspace.clone(),
window,
cx,
);
}
}),
),
)
.into_any_element()
}
} }
}) } else {
log::error!("Tool not found: {tool_name}");
gpui::Empty.into_any_element()
}
}), }),
)), )),
), ),
@@ -2158,16 +2195,30 @@ impl ActiveThread {
div() div()
.text_ui_sm(cx) .text_ui_sm(cx)
.children(rendered_tool_use.as_ref().map(|rendered| { .children(rendered_tool_use.as_ref().map(|rendered| {
MarkdownElement::new( let tool_name = tool_use.name.to_string();
rendered.output.clone(), let tool_registry = assistant_tool::ToolRegistry::global(cx);
tool_use_markdown_style(window, cx),
) tool_registry
.on_url_click({ .tool(&tool_name)
let workspace = self.workspace.clone(); .and_then(|_tool| None) // Tool doesn't have a render method, but ToolOutput does
move |text, window, cx| { .unwrap_or_else(|| {
open_markdown_link(text, workspace.clone(), window, cx); MarkdownElement::new(
} rendered.output.clone(),
}) tool_use_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(
text,
workspace.clone(),
window,
cx,
);
}
})
.into_any_element()
})
})), })),
), ),
), ),

View File

@@ -6,6 +6,7 @@ use collections::HashMap;
use futures::FutureExt as _; use futures::FutureExt as _;
use futures::future::Shared; use futures::future::Shared;
use gpui::{App, SharedString, Task}; use gpui::{App, SharedString, Task};
use language_model::ToolOutput;
use language_model::{ use language_model::{
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
@@ -32,7 +33,7 @@ pub enum ToolUseStatus {
NeedsConfirmation, NeedsConfirmation,
Pending, Pending,
Running, Running,
Finished(SharedString), Finished(ToolOutput),
Error(SharedString), Error(SharedString),
} }
@@ -131,6 +132,7 @@ impl ToolUseState {
tool_name: tool_use.clone(), tool_name: tool_use.clone(),
is_error: tool_result.is_error, is_error: tool_result.is_error,
content: tool_result.content.clone(), content: tool_result.content.clone(),
tool_output: None,
}, },
); );
} }
@@ -153,6 +155,7 @@ impl ToolUseState {
tool_name: tool_use.name.clone(), tool_name: tool_use.name.clone(),
content: "Tool canceled by user".into(), content: "Tool canceled by user".into(),
is_error: true, is_error: true,
tool_output: None,
}, },
); );
pending_tools.push(tool_use.clone()); pending_tools.push(tool_use.clone());
@@ -331,7 +334,7 @@ impl ToolUseState {
&mut self, &mut self,
tool_use_id: LanguageModelToolUseId, tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>, tool_name: Arc<str>,
output: Result<String>, output: Result<ToolOutput>,
cx: &App, cx: &App,
) -> Option<PendingToolUse> { ) -> Option<PendingToolUse> {
match output { match output {
@@ -346,16 +349,19 @@ impl ToolUseState {
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE) .map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX); .unwrap_or(usize::MAX);
let tool_result = if tool_result.len() <= tool_output_limit { // Get string representation of the tool result
tool_result let response_text = tool_result.response_for_model();
} else {
let truncated = truncate_lines_to_byte_limit(&tool_result, tool_output_limit);
format!( // Check length and truncate if needed
"Tool result too long. The first {} bytes:\n\n{}", let final_tool_result = if response_text.len() <= tool_output_limit {
truncated.len(), response_text.to_string()
truncated } else {
) let response_string = response_text.to_string();
let truncated =
truncate_lines_to_byte_limit(&response_string, tool_output_limit);
let truncated_len = truncated.len();
format!("Tool result too long. The first {truncated_len} bytes:\n\n{truncated}")
}; };
self.tool_results.insert( self.tool_results.insert(
@@ -363,8 +369,9 @@ impl ToolUseState {
LanguageModelToolResult { LanguageModelToolResult {
tool_use_id: tool_use_id.clone(), tool_use_id: tool_use_id.clone(),
tool_name, tool_name,
content: tool_result.into(), content: Arc::from(final_tool_result),
is_error: false, is_error: false,
tool_output: Some(Arc::new(tool_result)),
}, },
); );
self.pending_tool_uses_by_id.remove(&tool_use_id) self.pending_tool_uses_by_id.remove(&tool_use_id)
@@ -377,6 +384,7 @@ impl ToolUseState {
tool_name, tool_name,
content: err.to_string().into(), content: err.to_string().into(),
is_error: true, is_error: true,
tool_output: None,
}, },
); );
@@ -451,6 +459,7 @@ impl ToolUseState {
} else { } else {
tool_result.content.clone() tool_result.content.clone()
}, },
tool_output: tool_result.tool_output.clone(),
}, },
)); ));
} }

View File

@@ -8,13 +8,15 @@ use std::fmt::Formatter;
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use gpui::{App, Entity, SharedString, Task}; use gpui::{self, App, Entity, SharedString, Task};
use icons::IconName; use icons::IconName;
use language_model::LanguageModelRequestMessage; use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat; use language_model::LanguageModelToolSchemaFormat;
use language_model::ToolOutput;
use project::Project; use project::Project;
pub use crate::action_log::*; pub use crate::action_log::*;
// StringToolOutput is now directly imported from language_model
pub use crate::tool_registry::*; pub use crate::tool_registry::*;
pub use crate::tool_working_set::*; pub use crate::tool_working_set::*;
@@ -66,7 +68,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>>; ) -> Task<Result<ToolOutput>>;
} }
impl Debug for dyn Tool { impl Debug for dyn Tool {

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
use futures::io::BufReader; use futures::io::BufReader;
use futures::{AsyncBufReadExt, AsyncReadExt}; use futures::{AsyncBufReadExt, AsyncReadExt};
use gpui::{App, Entity, Task}; use gpui::{App, Entity, Task};
@@ -76,7 +77,7 @@ impl Tool for BashTool {
project: Entity<Project>, project: Entity<Project>,
_action_log: Entity<ActionLog>, _action_log: Entity<ActionLog>,
cx: &mut App, cx: &mut App,
) -> Task<Result<String>> { ) -> Task<Result<ToolOutput>> {
let input: BashToolInput = match serde_json::from_value(input) { let input: BashToolInput = 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))),
@@ -123,7 +124,7 @@ impl Tool for BashTool {
worktree.read(cx).abs_path() worktree.read(cx).abs_path()
}; };
cx.spawn(async move |_| { cx.spawn(async move |_| -> Result<ToolOutput> {
// Add 2>&1 to merge stderr into stdout for proper interleaving. // Add 2>&1 to merge stderr into stdout for proper interleaving.
let command = format!("({}) 2>&1", input.command); let command = format!("({}) 2>&1", input.command);
@@ -192,25 +193,21 @@ impl Tool for BashTool {
String::from_utf8_lossy(&output_bytes).into() String::from_utf8_lossy(&output_bytes).into()
}; };
let output_with_status = if status.success() { if status.success() {
if output_string.is_empty() { if output_string.is_empty() {
"Command executed successfully.".to_string() Ok(StringToolOutput::new("Command executed successfully."))
} else { } else {
output_string.to_string() Ok(StringToolOutput::new(output_string))
} }
} else { } else {
format!( Ok(StringToolOutput::new(format!(
"{}{}{}{}", "{}{}{}{}",
ERR_MESSAGE_1, ERR_MESSAGE_1,
status.code().unwrap_or(-1), status.code().unwrap_or(-1),
ERR_MESSAGE_2, ERR_MESSAGE_2,
output_string, output_string,
) )))
}; }
debug_assert!(output_with_status.len() <= STDOUT_LIMIT);
Ok(output_with_status)
}) })
} }
} }

View File

@@ -1,6 +1,7 @@
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, ToolWorkingSet};
use language_model::{ToolOutput, StringToolOutput};
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};
@@ -210,7 +211,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -280,7 +281,7 @@ impl Tool for BatchTool {
match result { match result {
Ok(output) => { Ok(output) => {
formatted_results formatted_results
.push_str(&format!("Tool '{}' result:\n{}\n\n", tool_name, output)); .push_str(&format!("Tool '{}' result:\n{}\n\n", tool_name, output.response_for_model()));
} }
Err(err) => { Err(err) => {
error_occurred = true; error_occurred = true;
@@ -295,7 +296,7 @@ impl Tool for BatchTool {
.push_str("Note: Some tool invocations failed. See individual results above."); .push_str("Note: Some tool invocations failed. See individual results above.");
} }
Ok(formatted_results.trim().to_string()) Ok(StringToolOutput::new(formatted_results.trim().to_string()))
}) })
} }
} }

View File

@@ -5,6 +5,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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +130,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -147,8 +148,8 @@ impl Tool for CodeSymbolsTool {
}; };
cx.spawn(async move |cx| match input.path { cx.spawn(async move |cx| match input.path {
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.map(StringToolOutput::new),
None => project_symbols(project, regex, input.offset, cx).await, None => project_symbols(project, regex, input.offset, cx).await.map(StringToolOutput::new),
}) })
} }
} }

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +78,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -105,10 +106,10 @@ impl Tool for CopyPathTool {
cx.background_spawn(async move { cx.background_spawn(async move {
match copy_task.await { match copy_task.await {
Ok(_) => Ok(format!( Ok(_) => Ok(StringToolOutput::new(format!(
"Copied {} to {}", "Copied {} to {}",
input.source_path, input.destination_path input.source_path, input.destination_path
)), ))),
Err(err) => Err(anyhow!( Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}", "Failed to copy {} to {}: {}",
input.source_path, input.source_path,

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +69,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -87,7 +88,7 @@ impl Tool for CreateDirectoryTool {
.await .await
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?; .map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
Ok(format!("Created directory {destination_path}")) Ok(StringToolOutput::new(format!("Created directory {destination_path}")))
}) })
} }
} }

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +74,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -104,7 +105,7 @@ impl Tool for CreateFileTool {
.await .await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?; .map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
Ok(format!("Created file {destination_path}")) Ok(StringToolOutput::new(format!("Created file {destination_path}")))
}) })
} }
} }

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +64,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -124,7 +125,7 @@ impl Tool for DeletePathTool {
match delete { match delete {
Some(deletion_task) => match deletion_task.await { Some(deletion_task) => match deletion_task.await {
Ok(()) => Ok(format!("Deleted {path_str}")), Ok(()) => Ok(StringToolOutput::new(format!("Deleted {path_str}"))),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")), Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
}, },
None => Err(anyhow!( None => Err(anyhow!(

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +84,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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)
@@ -120,9 +121,9 @@ impl Tool for DiagnosticsTool {
} }
if output.is_empty() { if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string()) Ok(StringToolOutput::new("File doesn't have errors or warnings!"))
} else { } else {
Ok(output) Ok(StringToolOutput::new(output))
} }
}) })
} }
@@ -155,9 +156,9 @@ impl Tool for DiagnosticsTool {
}); });
if has_diagnostics { if has_diagnostics {
Task::ready(Ok(output)) Task::ready(Ok(StringToolOutput::new(output)))
} else { } else {
Task::ready(Ok("No errors or warnings found in the project.".to_string())) Task::ready(Ok(StringToolOutput::new("No errors or warnings found in the project.")))
} }
} }
} }

View File

@@ -5,6 +5,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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +147,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -164,7 +165,7 @@ impl Tool for FetchTool {
bail!("no textual content found"); bail!("no textual content found");
} }
Ok(text) Ok(StringToolOutput::new(text))
}) })
} }
} }

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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;
@@ -159,7 +160,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -251,7 +252,7 @@ impl Tool for FindReplaceFileTool {
}).await; }).await;
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str)) Ok(StringToolOutput::new(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str)))
}) })
} }

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +78,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -101,7 +102,7 @@ impl Tool for ListDirectoryTool {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
return Task::ready(Ok(output)); return Task::ready(Ok(StringToolOutput::new(output)));
} }
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 {
@@ -133,8 +134,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(StringToolOutput::new(format!("{} is empty.", input.path))));
} }
Task::ready(Ok(output)) Task::ready(Ok(StringToolOutput::new(output)))
} }
} }

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +91,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -116,10 +117,10 @@ impl Tool for MovePathTool {
cx.background_spawn(async move { cx.background_spawn(async move {
match rename_task.await { match rename_task.await {
Ok(_) => Ok(format!( Ok(_) => Ok(StringToolOutput::new(format!(
"Moved {} to {}", "Moved {} to {}",
input.source_path, input.destination_path input.source_path, input.destination_path
)), ))),
Err(err) => Err(anyhow!( Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}", "Failed to move {} to {}: {}",
input.source_path, input.source_path,

View File

@@ -3,6 +3,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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +61,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -72,6 +73,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(StringToolOutput::new(text)))
} }
} }

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +54,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -62,7 +63,7 @@ impl Tool for OpenTool {
cx.background_spawn(async move { cx.background_spawn(async move {
open::that(&input.path_or_url).context("Failed to open URL or file path")?; open::that(&input.path_or_url).context("Failed to open URL or file path")?;
Ok(format!("Successfully opened {}", input.path_or_url)) Ok(StringToolOutput::new(format!("Successfully opened {}", input.path_or_url)))
}) })
} }
} }

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +72,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -110,7 +111,7 @@ impl Tool for PathSearchTool {
} }
if matches.is_empty() { if matches.is_empty() {
Ok(format!("No paths in the project matched the glob {glob:?}")) Ok(StringToolOutput::new(format!("No paths in the project matched the glob {glob:?}")))
} else { } else {
// Sort to group entries in the same directory together. // Sort to group entries in the same directory together.
matches.sort(); matches.sort();
@@ -134,7 +135,7 @@ impl Tool for PathSearchTool {
matches.join("\n") matches.join("\n")
}; };
Ok(response) Ok(StringToolOutput::new(response))
} }
}) })
} }

View File

@@ -3,6 +3,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};
use language_model::{ToolOutput, StringToolOutput};
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,7 +89,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -114,9 +115,9 @@ impl Tool for ReadFileTool {
let lines = text.split('\n').skip(start - 1); let lines = text.split('\n').skip(start - 1);
if let Some(end) = input.end_line { if let Some(end) = input.end_line {
let count = end.saturating_sub(start).max(1); // Ensure at least 1 line let count = end.saturating_sub(start).max(1); // Ensure at least 1 line
Itertools::intersperse(lines.take(count), "\n").collect() Itertools::intersperse(lines.take(count), "\n").collect::<String>()
} else { } else {
Itertools::intersperse(lines, "\n").collect() Itertools::intersperse(lines, "\n").collect::<String>()
} }
})?; })?;
@@ -124,7 +125,7 @@ impl Tool for ReadFileTool {
log.buffer_read(buffer, cx); log.buffer_read(buffer, cx);
})?; })?;
Ok(result) Ok(StringToolOutput::new(result))
} else { } else {
// No line ranges specified, so check file size to see if it's too big. // No line ranges specified, so check file size to see if it's too big.
let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?; let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
@@ -137,13 +138,13 @@ impl Tool for ReadFileTool {
log.buffer_read(buffer, cx); log.buffer_read(buffer, cx);
})?; })?;
Ok(result) Ok(StringToolOutput::new(result))
} else { } else {
// File is too big, so return an error with the outline // File is too big, so return an error with the outline
// and a suggestion to read again with line numbers. // and a suggestion to read again with line numbers.
let outline = file_outline(project, file_path, action_log, None, 0, cx).await?; let outline = file_outline(project, file_path, action_log, None, 0, cx).await?;
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(StringToolOutput::new(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.")))
} }
} }
}) })

View File

@@ -1,6 +1,7 @@
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};
use language_model::{ToolOutput, StringToolOutput};
use futures::StreamExt; use futures::StreamExt;
use gpui::{App, Entity, Task}; use gpui::{App, Entity, Task};
use language::OffsetRangeExt; use language::OffsetRangeExt;
@@ -83,7 +84,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
const CONTEXT_LINES: u32 = 2; const CONTEXT_LINES: u32 = 2;
let (offset, regex) = match serde_json::from_value::<RegexSearchToolInput>(input) { let (offset, regex) = match serde_json::from_value::<RegexSearchToolInput>(input) {
@@ -179,16 +180,16 @@ impl Tool for RegexSearchTool {
} }
if matches_found == 0 { if matches_found == 0 {
Ok("No matches found".to_string()) Ok(StringToolOutput::new("No matches found".to_string()))
} else if has_more_matches { } else if has_more_matches {
Ok(format!( Ok(StringToolOutput::new(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}", "Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
offset + 1, offset + 1,
offset + matches_found, offset + matches_found,
offset + RESULTS_PER_PAGE, offset + RESULTS_PER_PAGE,
)) )))
} else { } else {
Ok(format!("Found {matches_found} matches:\n{output}")) Ok(StringToolOutput::new(format!("Found {matches_found} matches:\n{output}")))
} }
}) })
} }

View File

@@ -1,5 +1,6 @@
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool}; use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
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,7 +123,7 @@ 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>> { ) -> Task<Result<ToolOutput>> {
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))),
@@ -203,7 +204,7 @@ impl Tool for SymbolInfoTool {
if output.is_empty() { if output.is_empty() {
Err(anyhow!("None found.")) Err(anyhow!("None found."))
} else { } else {
Ok(output) Ok(StringToolOutput::new(output))
} }
}) })
} }

View File

@@ -3,6 +3,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};
use language_model::{ToolOutput, StringToolOutput};
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,10 +52,10 @@ 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>> { ) -> Task<Result<ToolOutput>> {
// 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(StringToolOutput::new("Finished thinking.")),
Err(err) => Err(anyhow!(err)), Err(err) => Err(anyhow!(err)),
}) })
} }

View File

@@ -2,6 +2,7 @@ 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, ToolSource};
use language_model::{ToolOutput, StringToolOutput};
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};
@@ -76,7 +77,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>> { ) -> Task<Result<ToolOutput>> {
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();
@@ -114,7 +115,7 @@ impl Tool for ContextServerTool {
} }
} }
} }
Ok(result) Ok(StringToolOutput::new(result))
}) })
} else { } else {
Task::ready(Err(anyhow!("Context server not found"))) Task::ready(Err(anyhow!("Context server not found")))

View File

@@ -4,6 +4,7 @@ mod registry;
mod request; mod request;
mod role; mod role;
mod telemetry; mod telemetry;
mod tool_output;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod fake_provider; pub mod fake_provider;
@@ -26,6 +27,7 @@ use util::serde::is_default;
pub use crate::model::*; pub use crate::model::*;
pub use crate::rate_limiter::*; pub use crate::rate_limiter::*;
pub use crate::tool_output::{ToolOutput, StringToolOutput};
pub use crate::registry::*; pub use crate::registry::*;
pub use crate::request::*; pub use crate::request::*;
pub use crate::role::*; pub use crate::role::*;

View File

@@ -2,6 +2,7 @@ use std::io::{Cursor, Write};
use std::sync::Arc; use std::sync::Arc;
use crate::role::Role; use crate::role::Role;
use crate::tool_output::ToolOutput;
use crate::{LanguageModelToolUse, LanguageModelToolUseId}; use crate::{LanguageModelToolUse, LanguageModelToolUseId};
use base64::write::EncoderWriter; use base64::write::EncoderWriter;
use gpui::{ use gpui::{
@@ -164,12 +165,13 @@ impl LanguageModelImage {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct LanguageModelToolResult { pub struct LanguageModelToolResult {
pub tool_use_id: LanguageModelToolUseId, pub tool_use_id: LanguageModelToolUseId,
pub tool_name: Arc<str>, pub tool_name: Arc<str>,
pub is_error: bool, pub is_error: bool,
pub content: Arc<str>, pub content: Arc<str>,
pub tool_output: Option<Arc<ToolOutput>>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]

View File

@@ -0,0 +1,33 @@
use gpui::{AnyElement, App, SharedString, Window};
use serde::{Deserialize, Serialize};
/// An enum that represents different types of tool outputs that can be provided to the language model
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ToolOutput {
/// A simple string output
String {
string: SharedString,
rendered: Entity<Markdown>,
},
// Add other tool output types here as variants
}
impl ToolOutput {
/// Returns a string that will be given to the model
/// as the tool output.
pub fn response_for_model(&self) -> SharedString {
match self {
ToolOutput::String(output) => output.0.clone(),
// Handle other variants here
}
}
/// Returns a custom UI element to render the tool's output.
/// Returns None by default to indicate that rendering has not yet been
/// implemented for this tool, and the caller should do some default rendering.
pub fn render(&self, _window: &mut Window, _cx: &App) -> Option<AnyElement> {
match self {
ToolOutput::String { string, rendered } => todo!(),
}
}
}