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,
};
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::{Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, without_fences};
use project::ProjectItem as _;
@@ -36,6 +39,7 @@ use text::ToPoint;
use theme::ThemeSettings;
use ui::{Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::ResultExt as _;
use util::markdown::MarkdownString;
use workspace::{OpenOptions, Workspace};
use crate::context_store::ContextStore;
@@ -74,7 +78,7 @@ struct RenderedMessage {
struct RenderedToolUse {
label: Entity<Markdown>,
input: Entity<Markdown>,
output: Entity<Markdown>,
output: ToolOutput,
}
impl RenderedMessage {
@@ -729,21 +733,28 @@ impl ActiveThread {
tool_use_id: LanguageModelToolUseId,
tool_label: impl Into<SharedString>,
tool_input: &serde_json::Value,
tool_output: SharedString,
tool_output: ToolOutput,
cx: &mut Context<Self>,
) {
let rendered = RenderedToolUse {
label: render_tool_use_markdown(tool_label.into(), self.language_registry.clone(), cx),
input: render_tool_use_markdown(
format!(
"```json\n{}\n```",
serde_json::to_string_pretty(tool_input).unwrap_or_default()
MarkdownString::code_block(
"json",
&serde_json::to_string_pretty(tool_input).unwrap_or_default(),
)
.to_string()
.into(),
self.language_registry.clone(),
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
.insert(tool_use_id.clone(), rendered);
@@ -2094,24 +2105,50 @@ impl ActiveThread {
results_content_container()
.border_t_1()
.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(
rendered_tool_use.as_ref().map(|rendered| {
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);
let tool_name = tool_use.name.to_string();
let tool_registry = assistant_tool::ToolRegistry::global(cx);
if let Some(_tool) = tool_registry.tool(&tool_name) {
// Tool doesn't have a render method, but ToolOutput does
match rendered.output.render(window, cx) {
Some(rendered) => rendered,
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()
.text_ui_sm(cx)
.children(rendered_tool_use.as_ref().map(|rendered| {
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);
}
})
let tool_name = tool_use.name.to_string();
let tool_registry = assistant_tool::ToolRegistry::global(cx);
tool_registry
.tool(&tool_name)
.and_then(|_tool| None) // Tool doesn't have a render method, but ToolOutput does
.unwrap_or_else(|| {
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::future::Shared;
use gpui::{App, SharedString, Task};
use language_model::ToolOutput;
use language_model::{
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
@@ -32,7 +33,7 @@ pub enum ToolUseStatus {
NeedsConfirmation,
Pending,
Running,
Finished(SharedString),
Finished(ToolOutput),
Error(SharedString),
}
@@ -131,6 +132,7 @@ impl ToolUseState {
tool_name: tool_use.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
tool_output: None,
},
);
}
@@ -153,6 +155,7 @@ impl ToolUseState {
tool_name: tool_use.name.clone(),
content: "Tool canceled by user".into(),
is_error: true,
tool_output: None,
},
);
pending_tools.push(tool_use.clone());
@@ -331,7 +334,7 @@ impl ToolUseState {
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
output: Result<String>,
output: Result<ToolOutput>,
cx: &App,
) -> Option<PendingToolUse> {
match output {
@@ -346,16 +349,19 @@ impl ToolUseState {
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
let tool_result = if tool_result.len() <= tool_output_limit {
tool_result
} else {
let truncated = truncate_lines_to_byte_limit(&tool_result, tool_output_limit);
// Get string representation of the tool result
let response_text = tool_result.response_for_model();
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
// Check length and truncate if needed
let final_tool_result = if response_text.len() <= tool_output_limit {
response_text.to_string()
} 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(
@@ -363,8 +369,9 @@ impl ToolUseState {
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: tool_result.into(),
content: Arc::from(final_tool_result),
is_error: false,
tool_output: Some(Arc::new(tool_result)),
},
);
self.pending_tool_uses_by_id.remove(&tool_use_id)
@@ -377,6 +384,7 @@ impl ToolUseState {
tool_name,
content: err.to_string().into(),
is_error: true,
tool_output: None,
},
);
@@ -451,6 +459,7 @@ impl ToolUseState {
} else {
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 anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use gpui::{self, App, Entity, SharedString, Task};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use language_model::ToolOutput;
use project::Project;
pub use crate::action_log::*;
// StringToolOutput is now directly imported from language_model
pub use crate::tool_registry::*;
pub use crate::tool_working_set::*;
@@ -66,7 +68,7 @@ pub trait Tool: 'static + Send + Sync {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>>;
) -> Task<Result<ToolOutput>>;
}
impl Debug for dyn Tool {

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use futures::io::BufReader;
use futures::{AsyncBufReadExt, AsyncReadExt};
use gpui::{App, Entity, Task};
@@ -76,7 +77,7 @@ impl Tool for BashTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input: BashToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -123,7 +124,7 @@ impl Tool for BashTool {
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.
let command = format!("({}) 2>&1", input.command);
@@ -192,25 +193,21 @@ impl Tool for BashTool {
String::from_utf8_lossy(&output_bytes).into()
};
let output_with_status = if status.success() {
if status.success() {
if output_string.is_empty() {
"Command executed successfully.".to_string()
Ok(StringToolOutput::new("Command executed successfully."))
} else {
output_string.to_string()
Ok(StringToolOutput::new(output_string))
}
} else {
format!(
Ok(StringToolOutput::new(format!(
"{}{}{}{}",
ERR_MESSAGE_1,
status.code().unwrap_or(-1),
ERR_MESSAGE_2,
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 anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use language_model::{ToolOutput, StringToolOutput};
use futures::future::join_all;
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -210,7 +211,7 @@ impl Tool for BatchTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<BatchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -280,7 +281,7 @@ impl Tool for BatchTool {
match result {
Ok(output) => {
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) => {
error_occurred = true;
@@ -295,7 +296,7 @@ impl Tool for BatchTool {
.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 anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use collections::IndexMap;
use gpui::{App, AsyncApp, Entity, Task};
use language::{OutlineItem, ParseStatus, Point};
@@ -129,7 +130,7 @@ impl Tool for CodeSymbolsTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -147,8 +148,8 @@ impl Tool for CodeSymbolsTool {
};
cx.spawn(async move |cx| match input.path {
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
None => project_symbols(project, 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.map(StringToolOutput::new),
})
}
}

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -77,7 +78,7 @@ impl Tool for CopyPathTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -105,10 +106,10 @@ impl Tool for CopyPathTool {
cx.background_spawn(async move {
match copy_task.await {
Ok(_) => Ok(format!(
Ok(_) => Ok(StringToolOutput::new(format!(
"Copied {} to {}",
input.source_path, input.destination_path
)),
))),
Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}",
input.source_path,

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -68,7 +69,7 @@ impl Tool for CreateDirectoryTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -87,7 +88,7 @@ impl Tool for CreateDirectoryTool {
.await
.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 anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -73,7 +74,7 @@ impl Tool for CreateFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -104,7 +105,7 @@ impl Tool for CreateFileTool {
.await
.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 anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -63,7 +64,7 @@ impl Tool for DeletePathTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
Ok(input) => input.path,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -124,7 +125,7 @@ impl Tool for DeletePathTool {
match delete {
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}")),
},
None => Err(anyhow!(

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -83,7 +84,7 @@ impl Tool for DiagnosticsTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
match serde_json::from_value::<DiagnosticsToolInput>(input)
.ok()
.and_then(|input| input.path)
@@ -120,9 +121,9 @@ impl Tool for DiagnosticsTool {
}
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 {
Ok(output)
Ok(StringToolOutput::new(output))
}
})
}
@@ -155,9 +156,9 @@ impl Tool for DiagnosticsTool {
});
if has_diagnostics {
Task::ready(Ok(output))
Task::ready(Ok(StringToolOutput::new(output)))
} 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 anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use futures::AsyncReadExt as _;
use gpui::{App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
@@ -146,7 +147,7 @@ impl Tool for FetchTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<FetchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -164,7 +165,7 @@ impl Tool for FetchTool {
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 anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -159,7 +160,7 @@ impl Tool for FindReplaceFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -251,7 +252,7 @@ impl Tool for FindReplaceFileTool {
}).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 anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -77,7 +78,7 @@ impl Tool for ListDirectoryTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -101,7 +102,7 @@ impl Tool for ListDirectoryTool {
.collect::<Vec<_>>()
.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 {
@@ -133,8 +134,8 @@ impl Tool for ListDirectoryTool {
.unwrap();
}
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 anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -90,7 +91,7 @@ impl Tool for MovePathTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<MovePathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -116,10 +117,10 @@ impl Tool for MovePathTool {
cx.background_spawn(async move {
match rename_task.await {
Ok(_) => Ok(format!(
Ok(_) => Ok(StringToolOutput::new(format!(
"Moved {} to {}",
input.source_path, input.destination_path
)),
))),
Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}",
input.source_path,

View File

@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use chrono::{Local, Utc};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -60,7 +61,7 @@ impl Tool for NowTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input: NowToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -72,6 +73,6 @@ impl Tool for NowTool {
};
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 anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -53,7 +54,7 @@ impl Tool for OpenTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input: OpenToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -62,7 +63,7 @@ impl Tool for OpenTool {
cx.background_spawn(async move {
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 anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -71,7 +72,7 @@ impl Tool for PathSearchTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
Ok(input) => (input.offset, input.glob),
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -110,7 +111,7 @@ impl Tool for PathSearchTool {
}
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 {
// Sort to group entries in the same directory together.
matches.sort();
@@ -134,7 +135,7 @@ impl Tool for PathSearchTool {
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 anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, Entity, Task};
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -88,7 +89,7 @@ impl Tool for ReadFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -114,9 +115,9 @@ impl Tool for ReadFileTool {
let lines = text.split('\n').skip(start - 1);
if let Some(end) = input.end_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 {
Itertools::intersperse(lines, "\n").collect()
Itertools::intersperse(lines, "\n").collect::<String>()
}
})?;
@@ -124,7 +125,7 @@ impl Tool for ReadFileTool {
log.buffer_read(buffer, cx);
})?;
Ok(result)
Ok(StringToolOutput::new(result))
} else {
// 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())?;
@@ -137,13 +138,13 @@ impl Tool for ReadFileTool {
log.buffer_read(buffer, cx);
})?;
Ok(result)
Ok(StringToolOutput::new(result))
} else {
// File is too big, so return an error with the outline
// and a suggestion to read again with line numbers.
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 anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use futures::StreamExt;
use gpui::{App, Entity, Task};
use language::OffsetRangeExt;
@@ -83,7 +84,7 @@ impl Tool for RegexSearchTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
const CONTEXT_LINES: u32 = 2;
let (offset, regex) = match serde_json::from_value::<RegexSearchToolInput>(input) {
@@ -179,16 +180,16 @@ impl Tool for RegexSearchTool {
}
if matches_found == 0 {
Ok("No matches found".to_string())
Ok(StringToolOutput::new("No matches found".to_string()))
} 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}",
offset + 1,
offset + matches_found,
offset + RESULTS_PER_PAGE,
))
)))
} 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 assistant_tool::{ActionLog, Tool};
use language_model::{ToolOutput, StringToolOutput};
use gpui::{App, AsyncApp, Entity, Task};
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -122,7 +123,7 @@ impl Tool for SymbolInfoTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> Task<Result<ToolOutput>> {
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -203,7 +204,7 @@ impl Tool for SymbolInfoTool {
if output.is_empty() {
Err(anyhow!("None found."))
} else {
Ok(output)
Ok(StringToolOutput::new(output))
}
})
}

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ use std::io::{Cursor, Write};
use std::sync::Arc;
use crate::role::Role;
use crate::tool_output::ToolOutput;
use crate::{LanguageModelToolUse, LanguageModelToolUseId};
use base64::write::EncoderWriter;
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 tool_use_id: LanguageModelToolUseId,
pub tool_name: Arc<str>,
pub is_error: bool,
pub content: Arc<str>,
pub tool_output: Option<Arc<ToolOutput>>,
}
#[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!(),
}
}
}