Prompt before running some tools (#27284)

Also includes some fixes for how the Lua tool was being generated.

<img width="644" alt="Screenshot 2025-03-21 at 6 26 18 PM"
src="https://github.com/user-attachments/assets/51bd1685-5b3f-4ed3-b11e-6fa8017847d4"
/>


Release Notes:

- N/A

---------

Co-authored-by: Ben <ben@zed.dev>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
This commit is contained in:
Richard Feldman 2025-03-22 00:05:34 -04:00 committed by GitHub
parent 90649fbc89
commit 4c86cda909
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 666 additions and 329 deletions

View file

@ -3,7 +3,7 @@ use crate::thread::{
ThreadEvent, ThreadFeedback, ThreadEvent, ThreadFeedback,
}; };
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::tool_use::{ToolUse, ToolUseStatus}; use crate::tool_use::{PendingToolUseStatus, ToolType, ToolUse, ToolUseStatus};
use crate::ui::ContextPill; use crate::ui::ContextPill;
use collections::HashMap; use collections::HashMap;
use editor::{Editor, MultiBuffer}; use editor::{Editor, MultiBuffer};
@ -471,11 +471,18 @@ impl ActiveThread {
for tool_use in tool_uses { for tool_use in tool_uses {
self.render_tool_use_label_markdown( self.render_tool_use_label_markdown(
tool_use.id, tool_use.id.clone(),
tool_use.ui_text.clone(), tool_use.ui_text.clone(),
window, window,
cx, cx,
); );
self.render_scripting_tool_use_markdown(
tool_use.id,
tool_use.name.as_ref(),
tool_use.input.clone(),
window,
cx,
);
} }
} }
ThreadEvent::ToolFinished { ThreadEvent::ToolFinished {
@ -491,13 +498,6 @@ impl ActiveThread {
window, window,
cx, cx,
); );
self.render_scripting_tool_use_markdown(
tool_use.id.clone(),
tool_use.name.as_ref(),
tool_use.input.clone(),
window,
cx,
);
} }
if self.thread.read(cx).all_tools_finished() { if self.thread.read(cx).all_tools_finished() {
@ -996,7 +996,8 @@ impl ActiveThread {
) )
.child(div().p_2().child(message_content)), .child(div().p_2().child(message_content)),
), ),
Role::Assistant => v_flex() Role::Assistant => {
v_flex()
.id(("message-container", ix)) .id(("message-container", ix))
.ml_2() .ml_2()
.pl_2() .pl_2()
@ -1014,11 +1015,12 @@ impl ActiveThread {
.map(|tool_use| self.render_tool_use(tool_use, cx)), .map(|tool_use| self.render_tool_use(tool_use, cx)),
) )
.children(scripting_tool_uses.into_iter().map(|tool_use| { .children(scripting_tool_uses.into_iter().map(|tool_use| {
self.render_scripting_tool_use(tool_use, window, cx) self.render_scripting_tool_use(tool_use, cx)
})), })),
) )
}, },
), )
}
Role::System => div().id(("message-container", ix)).py_1().px_2().child( Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex() v_flex()
.bg(colors.editor_background) .bg(colors.editor_background)
@ -1379,7 +1381,8 @@ impl ActiveThread {
) )
.child({ .child({
let (icon_name, color, animated) = match &tool_use.status { let (icon_name, color, animated) = match &tool_use.status {
ToolUseStatus::Pending => { ToolUseStatus::Pending
| ToolUseStatus::NeedsConfirmation => {
(IconName::Warning, Color::Warning, false) (IconName::Warning, Color::Warning, false)
} }
ToolUseStatus::Running => { ToolUseStatus::Running => {
@ -1500,6 +1503,14 @@ impl ActiveThread {
), ),
), ),
ToolUseStatus::Pending => container, ToolUseStatus::Pending => container,
ToolUseStatus::NeedsConfirmation => container.child(
content_container().child(
Label::new("Asking Permission")
.size(LabelSize::Small)
.color(Color::Muted)
.buffer_font(cx),
),
),
}), }),
) )
}), }),
@ -1509,7 +1520,6 @@ impl ActiveThread {
fn render_scripting_tool_use( fn render_scripting_tool_use(
&self, &self,
tool_use: ToolUse, tool_use: ToolUse,
window: &Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let is_open = self let is_open = self
@ -1555,13 +1565,25 @@ impl ActiveThread {
} }
}), }),
)) ))
.child(div().text_ui_sm(cx).child(render_markdown( .child(
tool_use.ui_text.clone(), h_flex()
self.language_registry.clone(), .gap_1p5()
window, .child(
cx, Icon::new(IconName::Terminal)
))) .size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
div()
.text_ui_sm(cx)
.children(
self.rendered_tool_use_labels
.get(&tool_use.id)
.cloned(),
)
.truncate(), .truncate(),
),
),
) )
.child( .child(
Label::new(match tool_use.status { Label::new(match tool_use.status {
@ -1569,6 +1591,7 @@ impl ActiveThread {
ToolUseStatus::Running => "Running", ToolUseStatus::Running => "Running",
ToolUseStatus::Finished(_) => "Finished", ToolUseStatus::Finished(_) => "Finished",
ToolUseStatus::Error(_) => "Error", ToolUseStatus::Error(_) => "Error",
ToolUseStatus::NeedsConfirmation => "Asking Permission",
}) })
.size(LabelSize::XSmall) .size(LabelSize::XSmall)
.buffer_font(cx), .buffer_font(cx),
@ -1620,6 +1643,13 @@ impl ActiveThread {
.child(Label::new(err)), .child(Label::new(err)),
), ),
ToolUseStatus::Pending | ToolUseStatus::Running => parent, ToolUseStatus::Pending | ToolUseStatus::Running => parent,
ToolUseStatus::NeedsConfirmation => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Asking Permission")),
),
}), }),
) )
}), }),
@ -1682,6 +1712,45 @@ impl ActiveThread {
.into_any() .into_any()
} }
fn handle_allow_tool(
&mut self,
tool_use_id: LanguageModelToolUseId,
_: &ClickEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
.thread
.read(cx)
.pending_tool(&tool_use_id)
.map(|tool_use| tool_use.status.clone())
{
self.thread.update(cx, |thread, cx| {
thread.run_tool(
c.tool_use_id.clone(),
c.ui_text.clone(),
c.input.clone(),
&c.messages,
c.tool_type.clone(),
cx,
);
});
}
}
fn handle_deny_tool(
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_type: ToolType,
_: &ClickEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.thread.update(cx, |thread, cx| {
thread.deny_tool_use(tool_use_id, tool_type, cx);
});
}
fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) { fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref() let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
else { else {
@ -1704,12 +1773,82 @@ impl ActiveThread {
task.detach(); task.detach();
} }
} }
fn render_confirmations<'a>(
&'a mut self,
cx: &'a mut Context<Self>,
) -> impl Iterator<Item = AnyElement> + 'a {
let thread = self.thread.read(cx);
thread
.tools_needing_confirmation()
.map(|(tool_type, tool)| {
div()
.m_3()
.p_2()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_lg()
.child(
v_flex()
.gap_1()
.child(
v_flex()
.gap_0p5()
.child(
Label::new("The agent wants to run this action:")
.color(Color::Muted),
)
.child(div().p_3().child(Label::new(&tool.ui_text))),
)
.child(
h_flex()
.gap_1()
.child({
let tool_id = tool.id.clone();
Button::new("allow-tool-action", "Allow").on_click(
cx.listener(move |this, event, window, cx| {
this.handle_allow_tool(
tool_id.clone(),
event,
window,
cx,
)
}),
)
})
.child({
let tool_id = tool.id.clone();
Button::new("deny-tool", "Deny").on_click(cx.listener(
move |this, event, window, cx| {
this.handle_deny_tool(
tool_id.clone(),
tool_type.clone(),
event,
window,
cx,
)
},
))
}),
)
.child(
Label::new("Note: A future release will introduce a way to remember your answers to these. In the meantime, you can avoid these prompts by adding \"assistant\": { \"always_allow_tool_actions\": true } to your settings.json.")
.color(Color::Muted)
.size(LabelSize::Small),
),
)
.into_any()
})
}
} }
impl Render for ActiveThread { impl Render for ActiveThread {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex() v_flex()
.size_full() .size_full()
.child(list(self.list_state.clone()).flex_grow()) .child(list(self.list_state.clone()).flex_grow())
.children(self.render_confirmations(cx))
} }
} }

View file

@ -3,14 +3,15 @@ use std::io::Write;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use assistant_tool::{ActionLog, ToolWorkingSet}; use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap, HashSet}; use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs; use fs::Fs;
use futures::future::Shared; use futures::future::Shared;
use futures::{FutureExt, StreamExt as _}; use futures::{FutureExt, StreamExt as _};
use git; use git;
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task}; use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
use language_model::{ use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
@ -24,6 +25,7 @@ use prompt_store::{
}; };
use scripting_tool::{ScriptingSession, ScriptingTool}; use scripting_tool::{ScriptingSession, ScriptingTool};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings;
use util::{maybe, post_inc, ResultExt as _, TryFutureExt as _}; use util::{maybe, post_inc, ResultExt as _, TryFutureExt as _};
use uuid::Uuid; use uuid::Uuid;
@ -32,7 +34,7 @@ use crate::thread_store::{
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult, SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
SerializedToolUse, SerializedToolUse,
}; };
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState}; use crate::tool_use::{PendingToolUse, PendingToolUseStatus, ToolType, ToolUse, ToolUseState};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum RequestKind { pub enum RequestKind {
@ -350,6 +352,44 @@ impl Thread {
&self.tools &self.tools
} }
pub fn pending_tool(&self, id: &LanguageModelToolUseId) -> Option<&PendingToolUse> {
self.tool_use
.pending_tool_uses()
.into_iter()
.find(|tool_use| &tool_use.id == id)
.or_else(|| {
self.scripting_tool_use
.pending_tool_uses()
.into_iter()
.find(|tool_use| &tool_use.id == id)
})
}
pub fn tools_needing_confirmation(&self) -> impl Iterator<Item = (ToolType, &PendingToolUse)> {
self.tool_use
.pending_tool_uses()
.into_iter()
.filter_map(|tool_use| {
if let PendingToolUseStatus::NeedsConfirmation(confirmation) = &tool_use.status {
Some((confirmation.tool_type.clone(), tool_use))
} else {
None
}
})
.chain(
self.scripting_tool_use
.pending_tool_uses()
.into_iter()
.filter_map(|tool_use| {
if tool_use.status.needs_confirmation() {
Some((ToolType::ScriptingTool, tool_use))
} else {
None
}
}),
)
}
pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> { pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
self.checkpoints_by_message.get(&id).cloned() self.checkpoints_by_message.get(&id).cloned()
} }
@ -1178,6 +1218,7 @@ impl Thread {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoIterator<Item = PendingToolUse> { ) -> impl IntoIterator<Item = PendingToolUse> {
let request = self.to_completion_request(RequestKind::Chat, cx); let request = self.to_completion_request(RequestKind::Chat, cx);
let messages = Arc::new(request.messages);
let pending_tool_uses = self let pending_tool_uses = self
.tool_use .tool_use
.pending_tool_uses() .pending_tool_uses()
@ -1188,18 +1229,33 @@ impl Thread {
for tool_use in pending_tool_uses.iter() { for tool_use in pending_tool_uses.iter() {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) { if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run( if tool.needs_confirmation()
&& !AssistantSettings::get_global(cx).always_allow_tool_actions
{
self.tool_use.confirm_tool_use(
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(), tool_use.input.clone(),
&request.messages, messages.clone(),
self.project.clone(), ToolType::NonScriptingTool(tool),
self.action_log.clone(), );
} else {
self.run_tool(
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
&messages,
ToolType::NonScriptingTool(tool),
cx, cx,
); );
}
self.insert_tool_output( } else if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
self.run_tool(
tool_use.id.clone(), tool_use.id.clone(),
tool_use.ui_text.clone().into(), tool_use.ui_text.clone(),
task, tool_use.input.clone(),
&messages,
ToolType::NonScriptingTool(tool),
cx, cx,
); );
} }
@ -1214,7 +1270,87 @@ impl Thread {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for scripting_tool_use in pending_scripting_tool_uses.iter() { for scripting_tool_use in pending_scripting_tool_uses.iter() {
let task = match ScriptingTool::deserialize_input(scripting_tool_use.input.clone()) { self.scripting_tool_use.confirm_tool_use(
scripting_tool_use.id.clone(),
scripting_tool_use.ui_text.clone(),
scripting_tool_use.input.clone(),
messages.clone(),
ToolType::ScriptingTool,
);
}
pending_tool_uses
.into_iter()
.chain(pending_scripting_tool_uses)
}
pub fn run_tool(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<SharedString>,
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
tool_type: ToolType,
cx: &mut Context<'_, Thread>,
) {
match tool_type {
ToolType::ScriptingTool => {
let task = self.spawn_scripting_tool_use(tool_use_id.clone(), input, cx);
self.scripting_tool_use
.run_pending_tool(tool_use_id, ui_text.into(), task);
}
ToolType::NonScriptingTool(tool) => {
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
self.tool_use
.run_pending_tool(tool_use_id, ui_text.into(), task);
}
}
}
fn spawn_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
messages: &[LanguageModelRequestMessage],
input: serde_json::Value,
tool: Arc<dyn Tool>,
cx: &mut Context<Thread>,
) -> Task<()> {
let run_tool = tool.run(
input,
messages,
self.project.clone(),
self.action_log.clone(),
cx,
);
cx.spawn({
async move |thread: WeakEntity<Thread>, cx| {
let output = run_tool.await;
thread
.update(cx, |thread, cx| {
let pending_tool_use = thread
.tool_use
.insert_tool_output(tool_use_id.clone(), output);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,
canceled: false,
});
})
.ok();
}
})
}
fn spawn_scripting_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
input: serde_json::Value,
cx: &mut Context<Thread>,
) -> Task<()> {
let task = match ScriptingTool::deserialize_input(input) {
Err(err) => Task::ready(Err(err.into())), Err(err) => Task::ready(Err(err.into())),
Ok(input) => { Ok(input) => {
let (script_id, script_task) = let (script_id, script_task) =
@ -1241,58 +1377,10 @@ impl Thread {
} }
}; };
let ui_text: SharedString = scripting_tool_use.name.clone().into(); cx.spawn({
self.insert_scripting_tool_output(scripting_tool_use.id.clone(), ui_text, task, cx);
}
pending_tool_uses
.into_iter()
.chain(pending_scripting_tool_uses)
}
pub fn insert_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: SharedString,
output: Task<Result<String>>,
cx: &mut Context<Self>,
) {
let insert_output_task = cx.spawn({
let tool_use_id = tool_use_id.clone(); let tool_use_id = tool_use_id.clone();
async move |thread, cx| { async move |thread, cx| {
let output = output.await; let output = task.await;
thread
.update(cx, |thread, cx| {
let pending_tool_use = thread
.tool_use
.insert_tool_output(tool_use_id.clone(), output);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,
canceled: false,
});
})
.ok();
}
});
self.tool_use
.run_pending_tool(tool_use_id, ui_text, insert_output_task);
}
pub fn insert_scripting_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: SharedString,
output: Task<Result<String>>,
cx: &mut Context<Self>,
) {
let insert_output_task = cx.spawn({
let tool_use_id = tool_use_id.clone();
async move |thread, cx| {
let output = output.await;
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
let pending_tool_use = thread let pending_tool_use = thread
@ -1307,10 +1395,7 @@ impl Thread {
}) })
.ok(); .ok();
} }
}); })
self.scripting_tool_use
.run_pending_tool(tool_use_id, ui_text, insert_output_task);
} }
pub fn attach_tool_results( pub fn attach_tool_results(
@ -1568,6 +1653,30 @@ impl Thread {
pub fn cumulative_token_usage(&self) -> TokenUsage { pub fn cumulative_token_usage(&self) -> TokenUsage {
self.cumulative_token_usage.clone() self.cumulative_token_usage.clone()
} }
pub fn deny_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_type: ToolType,
cx: &mut Context<Self>,
) {
let err = Err(anyhow::anyhow!(
"Permission to run tool action denied by user"
));
if let ToolType::ScriptingTool = tool_type {
self.scripting_tool_use
.insert_tool_output(tool_use_id.clone(), err);
} else {
self.tool_use.insert_tool_output(tool_use_id.clone(), err);
}
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use: None,
canceled: true,
});
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use assistant_tool::ToolWorkingSet; use assistant_tool::{Tool, ToolWorkingSet};
use collections::HashMap; use collections::HashMap;
use futures::future::Shared; use futures::future::Shared;
use futures::FutureExt as _; use futures::FutureExt as _;
@ -10,6 +10,7 @@ use language_model::{
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role, LanguageModelToolUseId, MessageContent, Role,
}; };
use scripting_tool::ScriptingTool;
use crate::thread::MessageId; use crate::thread::MessageId;
use crate::thread_store::SerializedMessage; use crate::thread_store::SerializedMessage;
@ -25,6 +26,7 @@ pub struct ToolUse {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ToolUseStatus { pub enum ToolUseStatus {
NeedsConfirmation,
Pending, Pending,
Running, Running,
Finished(SharedString), Finished(SharedString),
@ -163,16 +165,19 @@ impl ToolUseState {
} }
if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) { if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) {
return match pending_tool_use.status { match pending_tool_use.status {
PendingToolUseStatus::Idle => ToolUseStatus::Pending, PendingToolUseStatus::Idle => ToolUseStatus::Pending,
PendingToolUseStatus::NeedsConfirmation { .. } => {
ToolUseStatus::NeedsConfirmation
}
PendingToolUseStatus::Running { .. } => ToolUseStatus::Running, PendingToolUseStatus::Running { .. } => ToolUseStatus::Running,
PendingToolUseStatus::Error(ref err) => { PendingToolUseStatus::Error(ref err) => {
ToolUseStatus::Error(err.clone().into()) ToolUseStatus::Error(err.clone().into())
} }
};
} }
} else {
ToolUseStatus::Pending ToolUseStatus::Pending
}
})(); })();
tool_uses.push(ToolUse { tool_uses.push(ToolUse {
@ -195,6 +200,8 @@ impl ToolUseState {
) -> SharedString { ) -> SharedString {
if let Some(tool) = self.tools.tool(tool_name, cx) { if let Some(tool) = self.tools.tool(tool_name, cx) {
tool.ui_text(input).into() tool.ui_text(input).into()
} else if tool_name == ScriptingTool::NAME {
"Run Lua Script".into()
} else { } else {
"Unknown tool".into() "Unknown tool".into()
} }
@ -272,6 +279,28 @@ impl ToolUseState {
} }
} }
pub fn confirm_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<Arc<str>>,
input: serde_json::Value,
messages: Arc<Vec<LanguageModelRequestMessage>>,
tool_type: ToolType,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
let ui_text = ui_text.into();
tool_use.ui_text = ui_text.clone();
let confirmation = Confirmation {
tool_use_id,
input,
messages,
tool_type,
ui_text,
};
tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
}
}
pub fn insert_tool_output( pub fn insert_tool_output(
&mut self, &mut self,
tool_use_id: LanguageModelToolUseId, tool_use_id: LanguageModelToolUseId,
@ -369,9 +398,25 @@ pub struct PendingToolUse {
pub status: PendingToolUseStatus, pub status: PendingToolUseStatus,
} }
#[derive(Debug, Clone)]
pub enum ToolType {
ScriptingTool,
NonScriptingTool(Arc<dyn Tool>),
}
#[derive(Debug, Clone)]
pub struct Confirmation {
pub tool_use_id: LanguageModelToolUseId,
pub input: serde_json::Value,
pub ui_text: Arc<str>,
pub messages: Arc<Vec<LanguageModelRequestMessage>>,
pub tool_type: ToolType,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum PendingToolUseStatus { pub enum PendingToolUseStatus {
Idle, Idle,
NeedsConfirmation(Arc<Confirmation>),
Running { _task: Shared<Task<()>> }, Running { _task: Shared<Task<()>> },
Error(#[allow(unused)] Arc<str>), Error(#[allow(unused)] Arc<str>),
} }
@ -384,4 +429,8 @@ impl PendingToolUseStatus {
pub fn is_error(&self) -> bool { pub fn is_error(&self) -> bool {
matches!(self, PendingToolUseStatus::Error(_)) matches!(self, PendingToolUseStatus::Error(_))
} }
pub fn needs_confirmation(&self) -> bool {
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
}
} }

View file

@ -72,6 +72,7 @@ pub struct AssistantSettings {
pub using_outdated_settings_version: bool, pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool, pub enable_experimental_live_diffs: bool,
pub profiles: IndexMap<Arc<str>, AgentProfile>, pub profiles: IndexMap<Arc<str>, AgentProfile>,
pub always_allow_tool_actions: bool,
} }
impl AssistantSettings { impl AssistantSettings {
@ -173,6 +174,7 @@ impl AssistantSettingsContent {
inline_alternatives: None, inline_alternatives: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
profiles: None, profiles: None,
always_allow_tool_actions: None,
}, },
VersionedAssistantSettingsContent::V2(settings) => settings.clone(), VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
}, },
@ -195,6 +197,7 @@ impl AssistantSettingsContent {
inline_alternatives: None, inline_alternatives: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
profiles: None, profiles: None,
always_allow_tool_actions: None,
}, },
} }
} }
@ -325,6 +328,7 @@ impl Default for VersionedAssistantSettingsContent {
inline_alternatives: None, inline_alternatives: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
profiles: None, profiles: None,
always_allow_tool_actions: None,
}) })
} }
} }
@ -363,6 +367,11 @@ pub struct AssistantSettingsContentV2 {
enable_experimental_live_diffs: Option<bool>, enable_experimental_live_diffs: Option<bool>,
#[schemars(skip)] #[schemars(skip)]
profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>, profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
/// Whenever a tool action would normally wait for your confirmation
/// that you allow it, always choose to allow it.
///
/// Default: false
always_allow_tool_actions: Option<bool>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
@ -499,6 +508,10 @@ impl Settings for AssistantSettings {
&mut settings.enable_experimental_live_diffs, &mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs, value.enable_experimental_live_diffs,
); );
merge(
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
);
if let Some(profiles) = value.profiles { if let Some(profiles) = value.profiles {
settings settings
@ -579,6 +592,7 @@ mod tests {
default_height: None, default_height: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
profiles: None, profiles: None,
always_allow_tool_actions: None,
}), }),
) )
}, },

View file

@ -1,14 +1,14 @@
mod tool_registry; mod tool_registry;
mod tool_working_set; mod tool_working_set;
use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use gpui::{App, Context, Entity, SharedString, Task}; use gpui::{App, Context, Entity, SharedString, Task};
use language::Buffer; use language::Buffer;
use language_model::LanguageModelRequestMessage; use language_model::LanguageModelRequestMessage;
use project::Project; use project::Project;
use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
pub use crate::tool_registry::*; pub use crate::tool_registry::*;
pub use crate::tool_working_set::*; pub use crate::tool_working_set::*;
@ -38,6 +38,10 @@ pub trait Tool: 'static + Send + Sync {
ToolSource::Native ToolSource::Native
} }
/// Returns true iff the tool needs the users's confirmation
/// before having permission to run.
fn needs_confirmation(&self) -> bool;
/// Returns the JSON schema that describes the tool's input. /// Returns the JSON schema that describes the tool's input.
fn input_schema(&self) -> serde_json::Value { fn input_schema(&self) -> serde_json::Value {
serde_json::Value::Object(serde_json::Map::default()) serde_json::Value::Object(serde_json::Map::default())
@ -57,6 +61,12 @@ pub trait Tool: 'static + Send + Sync {
) -> Task<Result<String>>; ) -> Task<Result<String>>;
} }
impl Debug for dyn Tool {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Tool").field("name", &self.name()).finish()
}
}
/// Tracks actions performed by tools in a thread /// Tracks actions performed by tools in a thread
#[derive(Debug)] #[derive(Debug)]
pub struct ActionLog { pub struct ActionLog {

View file

@ -23,6 +23,10 @@ impl Tool for BashTool {
"bash".to_string() "bash".to_string()
} }
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./bash_tool/description.md").to_string() include_str!("./bash_tool/description.md").to_string()
} }

View file

@ -30,6 +30,10 @@ impl Tool for DeletePathTool {
"delete-path".into() "delete-path".into()
} }
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./delete_path_tool/description.md").into() include_str!("./delete_path_tool/description.md").into()
} }

View file

@ -37,6 +37,10 @@ impl Tool for DiagnosticsTool {
"diagnostics".into() "diagnostics".into()
} }
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./diagnostics_tool/description.md").into() include_str!("./diagnostics_tool/description.md").into()
} }

View file

@ -79,6 +79,10 @@ impl Tool for EditFilesTool {
"edit-files".into() "edit-files".into()
} }
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./edit_files_tool/description.md").into() include_str!("./edit_files_tool/description.md").into()
} }
@ -145,30 +149,22 @@ impl Tool for EditFilesTool {
struct EditToolRequest { struct EditToolRequest {
parser: EditActionParser, parser: EditActionParser,
editor_response: EditorResponse, output: String,
changed_buffers: HashSet<Entity<language::Buffer>>,
bad_searches: Vec<BadSearch>,
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>, tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
} }
enum EditorResponse { #[derive(Debug)]
/// The editor model hasn't produced any actions yet. enum DiffResult {
/// If we don't have any by the end, we'll return its message to the architect model. BadSearch(BadSearch),
Message(String), Diff(language::Diff),
/// The editor model produced at least one action.
Actions {
applied: Vec<AppliedAction>,
search_errors: Vec<SearchError>,
},
}
struct AppliedAction {
source: String,
buffer: Entity<language::Buffer>,
} }
#[derive(Debug)] #[derive(Debug)]
enum SearchError { enum BadSearch {
NoMatch { NoMatch {
file_path: String, file_path: String,
search: String, search: String,
@ -234,7 +230,10 @@ impl EditToolRequest {
let mut request = Self { let mut request = Self {
parser: EditActionParser::new(), parser: EditActionParser::new(),
editor_response: EditorResponse::Message(String::with_capacity(256)), // we start with the success header so we don't need to shift the output in the common case
output: Self::SUCCESS_OUTPUT_HEADER.to_string(),
changed_buffers: HashSet::default(),
bad_searches: Vec::new(),
action_log, action_log,
project, project,
tool_log, tool_log,
@ -251,12 +250,6 @@ impl EditToolRequest {
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> { async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
let new_actions = self.parser.parse_chunk(chunk); let new_actions = self.parser.parse_chunk(chunk);
if let EditorResponse::Message(ref mut message) = self.editor_response {
if new_actions.is_empty() {
message.push_str(chunk);
}
}
if let Some((ref log, req_id)) = self.tool_log { if let Some((ref log, req_id)) = self.tool_log {
log.update(cx, |log, cx| { log.update(cx, |log, cx| {
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx) log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
@ -287,11 +280,6 @@ impl EditToolRequest {
.update(cx, |project, cx| project.open_buffer(project_path, cx))? .update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?; .await?;
enum DiffResult {
Diff(language::Diff),
SearchError(SearchError),
}
let result = match action { let result = match action {
EditAction::Replace { EditAction::Replace {
old, old,
@ -301,39 +289,7 @@ impl EditToolRequest {
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
cx.background_executor() cx.background_executor()
.spawn(async move { .spawn(Self::replace_diff(old, new, file_path, snapshot))
if snapshot.is_empty() {
let exists = snapshot
.file()
.map_or(false, |file| file.disk_state().exists());
let error = SearchError::EmptyBuffer {
file_path: file_path.display().to_string(),
exists,
search: old,
};
return anyhow::Ok(DiffResult::SearchError(error));
}
let replace_result =
// Try to match exactly
replace_exact(&old, &new, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
let Some(diff) = replace_result else {
let error = SearchError::NoMatch {
search: old,
file_path: file_path.display().to_string(),
};
return Ok(DiffResult::SearchError(error));
};
Ok(DiffResult::Diff(diff))
})
.await .await
} }
EditAction::Write { content, .. } => Ok(DiffResult::Diff( EditAction::Write { content, .. } => Ok(DiffResult::Diff(
@ -344,144 +300,134 @@ impl EditToolRequest {
}?; }?;
match result { match result {
DiffResult::SearchError(error) => { DiffResult::BadSearch(invalid_replace) => {
self.push_search_error(error); self.bad_searches.push(invalid_replace);
} }
DiffResult::Diff(diff) => { DiffResult::Diff(diff) => {
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?; let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
self.push_applied_action(AppliedAction { source, buffer }); write!(&mut self.output, "\n\n{}", source)?;
self.changed_buffers.insert(buffer);
} }
} }
anyhow::Ok(()) Ok(())
} }
fn push_search_error(&mut self, error: SearchError) { async fn replace_diff(
match &mut self.editor_response { old: String,
EditorResponse::Message(_) => { new: String,
self.editor_response = EditorResponse::Actions { file_path: std::path::PathBuf,
applied: Vec::new(), snapshot: language::BufferSnapshot,
search_errors: vec![error], ) -> Result<DiffResult> {
if snapshot.is_empty() {
let exists = snapshot
.file()
.map_or(false, |file| file.disk_state().exists());
return Ok(DiffResult::BadSearch(BadSearch::EmptyBuffer {
file_path: file_path.display().to_string(),
exists,
search: old,
}));
}
let result =
// Try to match exactly
replace_exact(&old, &new, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
let Some(diff) = result else {
return anyhow::Ok(DiffResult::BadSearch(BadSearch::NoMatch {
search: old,
file_path: file_path.display().to_string(),
}));
}; };
}
EditorResponse::Actions { search_errors, .. } => { anyhow::Ok(DiffResult::Diff(diff))
search_errors.push(error);
}
}
} }
fn push_applied_action(&mut self, action: AppliedAction) { const SUCCESS_OUTPUT_HEADER: &str = "Successfully applied. Here's a list of changes:";
match &mut self.editor_response { const ERROR_OUTPUT_HEADER_NO_EDITS: &str = "I couldn't apply any edits!";
EditorResponse::Message(_) => { const ERROR_OUTPUT_HEADER_WITH_EDITS: &str =
self.editor_response = EditorResponse::Actions { "Errors occurred. First, here's a list of the edits we managed to apply:";
applied: vec![action],
search_errors: Vec::new(),
};
}
EditorResponse::Actions { applied, .. } => {
applied.push(action);
}
}
}
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> { async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
match self.editor_response { let changed_buffer_count = self.changed_buffers.len();
EditorResponse::Message(message) => Err(anyhow!(
"No edits were applied! You might need to provide more context.\n\n{}",
message
)),
EditorResponse::Actions {
applied,
search_errors,
} => {
let mut output = String::with_capacity(1024);
let parse_errors = self.parser.errors(); // Save each buffer once at the end
let has_errors = !search_errors.is_empty() || !parse_errors.is_empty(); for buffer in &self.changed_buffers {
if has_errors {
let error_count = search_errors.len() + parse_errors.len();
if applied.is_empty() {
writeln!(
&mut output,
"{} errors occurred! No edits were applied.",
error_count,
)?;
} else {
writeln!(
&mut output,
"{} errors occurred, but {} edits were correctly applied.",
error_count,
applied.len(),
)?;
writeln!(
&mut output,
"# {} SEARCH/REPLACE block(s) applied:\n\nDo not re-send these since they are already applied!\n",
applied.len()
)?;
}
} else {
write!(
&mut output,
"Successfully applied! Here's a list of applied edits:"
)?;
}
let mut changed_buffers = HashSet::default();
for action in applied {
changed_buffers.insert(action.buffer);
write!(&mut output, "\n\n{}", action.source)?;
}
for buffer in &changed_buffers {
self.project self.project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?; .await?;
} }
self.action_log self.action_log
.update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx)) .update(cx, |log, cx| log.buffer_edited(self.changed_buffers, cx))
.log_err(); .log_err();
if !search_errors.is_empty() { let errors = self.parser.errors();
if errors.is_empty() && self.bad_searches.is_empty() {
if changed_buffer_count == 0 {
return Err(anyhow!(
"The instructions didn't lead to any changes. You might need to consult the file contents first."
));
}
Ok(self.output)
} else {
let mut output = self.output;
if output.is_empty() {
output.replace_range(
0..Self::SUCCESS_OUTPUT_HEADER.len(),
Self::ERROR_OUTPUT_HEADER_NO_EDITS,
);
} else {
output.replace_range(
0..Self::SUCCESS_OUTPUT_HEADER.len(),
Self::ERROR_OUTPUT_HEADER_WITH_EDITS,
);
}
if !self.bad_searches.is_empty() {
writeln!( writeln!(
&mut output, &mut output,
"\n\n## {} SEARCH/REPLACE block(s) failed to match:\n", "\n\n# {} SEARCH/REPLACE block(s) failed to match:\n",
search_errors.len() self.bad_searches.len()
)?; )?;
for error in search_errors { for bad_search in self.bad_searches {
match error { match bad_search {
SearchError::NoMatch { file_path, search } => { BadSearch::NoMatch { file_path, search } => {
writeln!( writeln!(
&mut output, &mut output,
"### No exact match in: `{}`\n```\n{}\n```\n", "## No exact match in: `{}`\n```\n{}\n```\n",
file_path, search, file_path, search,
)?; )?;
} }
SearchError::EmptyBuffer { BadSearch::EmptyBuffer {
file_path, file_path,
exists: true, exists: true,
search, search,
} => { } => {
writeln!( writeln!(
&mut output, &mut output,
"### No match because `{}` is empty:\n```\n{}\n```\n", "## No match because `{}` is empty:\n```\n{}\n```\n",
file_path, search, file_path, search,
)?; )?;
} }
SearchError::EmptyBuffer { BadSearch::EmptyBuffer {
file_path, file_path,
exists: false, exists: false,
search, search,
} => { } => {
writeln!( writeln!(
&mut output, &mut output,
"### No match because `{}` does not exist:\n```\n{}\n```\n", "## No match because `{}` does not exist:\n```\n{}\n```\n",
file_path, search, file_path, search,
)?; )?;
} }
@ -494,29 +440,37 @@ impl EditToolRequest {
)?; )?;
} }
if !parse_errors.is_empty() { if !errors.is_empty() {
writeln!( writeln!(
&mut output, &mut output,
"\n\n## {} SEARCH/REPLACE blocks failed to parse:", "\n\n# {} SEARCH/REPLACE blocks failed to parse:",
parse_errors.len() errors.len()
)?; )?;
for error in parse_errors { for error in errors {
writeln!(&mut output, "- {}", error)?; writeln!(&mut output, "- {}", error)?;
} }
} }
if has_errors { if changed_buffer_count > 0 {
writeln!(&mut output, writeln!(
"\n\nYou can fix errors by running the tool again. You can include instructions, \ &mut output,
"\n\nThe other SEARCH/REPLACE blocks were applied successfully. Do not re-send them!",
)?;
}
writeln!(
&mut output,
"{}You can fix errors by running the tool again. You can include instructions, \
but errors are part of the conversation so you don't need to repeat them.", but errors are part of the conversation so you don't need to repeat them.",
if changed_buffer_count == 0 {
"\n\n"
} else {
""
}
)?; )?;
Err(anyhow!(output)) Err(anyhow!(output))
} else {
Ok(output)
}
}
} }
} }
} }

View file

@ -113,6 +113,10 @@ impl Tool for FetchTool {
"fetch".to_string() "fetch".to_string()
} }
fn needs_confirmation(&self) -> bool {
true
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./fetch_tool/description.md").to_string() include_str!("./fetch_tool/description.md").to_string()
} }

View file

@ -31,7 +31,7 @@ pub struct ListDirectoryToolInput {
/// ///
/// If you wanna list contents in the directory `foo/baz`, you should use the path `foo/baz`. /// If you wanna list contents in the directory `foo/baz`, you should use the path `foo/baz`.
/// </example> /// </example>
pub path: Arc<Path>, pub path: String,
} }
pub struct ListDirectoryTool; pub struct ListDirectoryTool;
@ -41,6 +41,10 @@ impl Tool for ListDirectoryTool {
"list-directory".into() "list-directory".into()
} }
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./list_directory_tool/description.md").into() include_str!("./list_directory_tool/description.md").into()
} }
@ -52,7 +56,7 @@ impl Tool for ListDirectoryTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) { match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
Ok(input) => format!("List the `{}` directory's contents", input.path.display()), Ok(input) => format!("List the `{}` directory's contents", input.path),
Err(_) => "List directory".to_string(), Err(_) => "List directory".to_string(),
} }
} }
@ -70,11 +74,29 @@ impl Tool for ListDirectoryTool {
Err(err) => return Task::ready(Err(anyhow!(err))), Err(err) => return Task::ready(Err(anyhow!(err))),
}; };
// Sometimes models will return these even though we tell it to give a path and not a glob.
// When this happens, just list the root worktree directories.
if matches!(input.path.as_str(), "." | "" | "./" | "*") {
let output = project
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
worktree.read(cx).root_entry().and_then(|entry| {
if entry.is_dir() {
entry.path.to_str()
} else {
None
}
})
})
.collect::<Vec<_>>()
.join("\n");
return Task::ready(Ok(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 {
return Task::ready(Err(anyhow!( return Task::ready(Err(anyhow!("Path {} not found in project", input.path)));
"Path {} not found in project",
input.path.display()
)));
}; };
let Some(worktree) = project let Some(worktree) = project
.read(cx) .read(cx)
@ -85,11 +107,11 @@ impl Tool for ListDirectoryTool {
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.display()))); return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
}; };
if !entry.is_dir() { if !entry.is_dir() {
return Task::ready(Err(anyhow!("{} is not a directory.", input.path.display()))); return Task::ready(Err(anyhow!("{} is not a directory.", input.path)));
} }
let mut output = String::new(); let mut output = String::new();
@ -102,7 +124,7 @@ impl Tool for ListDirectoryTool {
.unwrap(); .unwrap();
} }
if output.is_empty() { if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path.display()))); return Task::ready(Ok(format!("{} is empty.", input.path)));
} }
Task::ready(Ok(output)) Task::ready(Ok(output))
} }

View file

@ -31,6 +31,10 @@ impl Tool for NowTool {
"now".into() "now".into()
} }
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String { fn description(&self) -> String {
"Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into() "Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into()
} }

View file

@ -39,6 +39,10 @@ impl Tool for PathSearchTool {
"path-search".into() "path-search".into()
} }
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./path_search_tool/description.md").into() include_str!("./path_search_tool/description.md").into()
} }

View file

@ -44,6 +44,10 @@ impl Tool for ReadFileTool {
"read-file".into() "read-file".into()
} }
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./read_file_tool/description.md").into() include_str!("./read_file_tool/description.md").into()
} }

View file

@ -41,6 +41,10 @@ impl Tool for RegexSearchTool {
"regex-search".into() "regex-search".into()
} }
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./regex_search_tool/description.md").into() include_str!("./regex_search_tool/description.md").into()
} }

View file

@ -22,6 +22,10 @@ impl Tool for ThinkingTool {
"thinking".to_string() "thinking".to_string()
} }
fn needs_confirmation(&self) -> bool {
false
}
fn description(&self) -> String { fn description(&self) -> String {
include_str!("./thinking_tool/description.md").to_string() include_str!("./thinking_tool/description.md").to_string()
} }

View file

@ -44,6 +44,10 @@ impl Tool for ContextServerTool {
} }
} }
fn needs_confirmation(&self) -> bool {
true
}
fn input_schema(&self) -> serde_json::Value { fn input_schema(&self) -> serde_json::Value {
match &self.tool.input_schema { match &self.tool.input_schema {
serde_json::Value::Null => { serde_json::Value::Null => {