Add persistent local agent tools
This commit is contained in:
@@ -22,6 +22,7 @@ pub(super) struct TitleRequest {
|
||||
pub(crate) struct ChatMessage {
|
||||
pub(super) id: i32,
|
||||
pub(super) user: bool,
|
||||
pub(super) tool: bool,
|
||||
pub(super) reasoning: Option<String>,
|
||||
pub(super) reasoning_complete: bool,
|
||||
pub(super) reasoning_open: bool,
|
||||
@@ -40,11 +41,12 @@ impl ChatMessage {
|
||||
}
|
||||
|
||||
pub(super) fn refresh_markdown(&mut self) {
|
||||
if !self.user {
|
||||
if !self.user && !self.tool {
|
||||
let visible = crate::agent::visible_content(&self.content);
|
||||
let content = if self.reasoning.is_some() {
|
||||
self.content.trim_start()
|
||||
visible.trim_start()
|
||||
} else {
|
||||
&self.content
|
||||
visible
|
||||
};
|
||||
self.markdown = markdown::parse(content).collect();
|
||||
}
|
||||
@@ -56,6 +58,7 @@ impl From<StoredMessage> for ChatMessage {
|
||||
let mut message = Self {
|
||||
id: message.id,
|
||||
user: message.user,
|
||||
tool: message.tool,
|
||||
reasoning: message.reasoning,
|
||||
reasoning_complete: message.reasoning_complete,
|
||||
reasoning_open: false,
|
||||
@@ -88,13 +91,15 @@ impl App {
|
||||
crate::settings::effective_settings(model, &generation, &runtime, &models_path())
|
||||
})
|
||||
});
|
||||
let effective = match effective {
|
||||
let mut effective = match effective {
|
||||
Ok(settings) => settings,
|
||||
Err(error) => {
|
||||
self.error = Some(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
effective.turn.system_prompt =
|
||||
crate::agent::system_prompt(model, &effective.turn.system_prompt);
|
||||
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut messages = self
|
||||
@@ -102,6 +107,7 @@ impl App {
|
||||
.iter()
|
||||
.map(|message| ChatTurn {
|
||||
user: message.user,
|
||||
tool: message.tool,
|
||||
skip_previous_eos: false,
|
||||
reasoning: message.reasoning.clone(),
|
||||
reasoning_complete: message.reasoning_complete,
|
||||
@@ -111,6 +117,7 @@ impl App {
|
||||
#[cfg(target_os = "macos")]
|
||||
messages.push(ChatTurn {
|
||||
user: true,
|
||||
tool: false,
|
||||
skip_previous_eos: false,
|
||||
reasoning: None,
|
||||
reasoning_complete: true,
|
||||
@@ -189,6 +196,31 @@ impl App {
|
||||
}
|
||||
|
||||
pub(super) fn poll_generation(&mut self) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(active) = &self.active_tools {
|
||||
match crate::agent::try_tool_result(active) {
|
||||
Ok(Some(result)) => {
|
||||
let cancelled = active.cancel.load(Ordering::Relaxed);
|
||||
self.active_tools = None;
|
||||
if cancelled {
|
||||
self.generating = false;
|
||||
return false;
|
||||
}
|
||||
if let Err(error) = self.continue_after_tool_result(&result) {
|
||||
self.generating = false;
|
||||
self.error = Some(error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Ok(None) => return false,
|
||||
Err(error) => {
|
||||
self.active_tools = None;
|
||||
self.generating = false;
|
||||
self.error = Some(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
let Some(active) = &mut self.active_generation else {
|
||||
self.generating = false;
|
||||
@@ -221,9 +253,32 @@ impl App {
|
||||
context_changed = true;
|
||||
}
|
||||
Ok(GenerationEvent::Finished(result)) => {
|
||||
self.generating = false;
|
||||
if let Err(error) = result {
|
||||
self.error = Some(error);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let model = ModelChoice::from_id(&self.preferences.selected_model)
|
||||
.unwrap_or_default();
|
||||
let content = self
|
||||
.conversation
|
||||
.last()
|
||||
.map(|message| message.content.clone())
|
||||
.unwrap_or_default();
|
||||
match crate::agent::parse_tool_calls(model, &content) {
|
||||
Ok((_, calls)) if !calls.is_empty() => {
|
||||
if let Err(error) = self.start_agent_tools(calls) {
|
||||
self.generating = false;
|
||||
self.error = Some(error);
|
||||
}
|
||||
}
|
||||
Ok(_) => self.generating = false,
|
||||
Err(error) => {
|
||||
self.active_tools = Some(crate::agent::error_async(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
self.generating = false;
|
||||
self.error = Some(error);
|
||||
}
|
||||
}
|
||||
self.active_generation = None;
|
||||
break;
|
||||
@@ -286,6 +341,83 @@ impl App {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn start_agent_tools(&mut self, calls: Vec<crate::agent::ToolCall>) -> Result<(), String> {
|
||||
let session_id = self
|
||||
.selected_session
|
||||
.ok_or_else(|| "The active session is unavailable.".to_owned())?;
|
||||
if self.agent_tools.as_ref().map(|(id, _)| *id) != Some(session_id) {
|
||||
let project_id = self
|
||||
.selected_project
|
||||
.ok_or_else(|| "The active project is unavailable.".to_owned())?;
|
||||
let root = self
|
||||
.projects
|
||||
.iter()
|
||||
.find(|project| project.project.id == project_id)
|
||||
.map(|project| PathBuf::from(&project.project.path))
|
||||
.ok_or_else(|| "The active project is unavailable.".to_owned())?;
|
||||
let tools = crate::agent::Tools::new(&root, self.preferences.context_tokens)?;
|
||||
self.agent_tools = Some((session_id, Arc::new(Mutex::new(tools))));
|
||||
}
|
||||
let tools = Arc::clone(&self.agent_tools.as_ref().unwrap().1);
|
||||
self.active_tools = Some(crate::agent::execute_async(tools, calls));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn continue_after_tool_result(&mut self, result: &str) -> Result<(), String> {
|
||||
let session_id = self
|
||||
.selected_session
|
||||
.ok_or_else(|| "The active session is unavailable.".to_owned())?;
|
||||
let model = ModelChoice::from_id(&self.preferences.selected_model)
|
||||
.ok_or_else(|| "The selected model is not supported.".to_owned())?;
|
||||
let generation = self.preferences.generation()?;
|
||||
let runtime = self.preferences.runtime()?;
|
||||
let mut effective =
|
||||
crate::settings::effective_settings(model, &generation, &runtime, &models_path())?;
|
||||
effective.turn.system_prompt =
|
||||
crate::agent::system_prompt(model, &effective.turn.system_prompt);
|
||||
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
|
||||
let saved = self
|
||||
.database
|
||||
.as_mut()
|
||||
.ok_or_else(|| "The project database is unavailable.".to_owned())?
|
||||
.continue_tool_turn(session_id, result, assistant_reasoning)
|
||||
.map_err(|error| format!("Could not save the tool turn: {error}"))?;
|
||||
self.conversation.push(ChatMessage::from(saved.0));
|
||||
let messages = self
|
||||
.conversation
|
||||
.iter()
|
||||
.map(|message| ChatTurn {
|
||||
user: message.user,
|
||||
tool: message.tool,
|
||||
skip_previous_eos: false,
|
||||
reasoning: message.reasoning.clone(),
|
||||
reasoning_complete: message.reasoning_complete,
|
||||
content: message.content.clone(),
|
||||
})
|
||||
.collect();
|
||||
let mut assistant = ChatMessage::from(saved.1);
|
||||
assistant.reasoning_open = assistant_reasoning;
|
||||
self.conversation.push(assistant);
|
||||
let idle_timeout =
|
||||
Duration::from_secs(self.preferences.idle_timeout_minutes.max(1) as u64 * 60);
|
||||
self.active_generation = Some(
|
||||
self.generation_service
|
||||
.as_ref()
|
||||
.ok_or_else(|| "The model runtime is unavailable.".to_owned())?
|
||||
.generate(
|
||||
effective.engine,
|
||||
effective.turn,
|
||||
messages,
|
||||
CheckpointTarget::Local(session_checkpoint_path(session_id)),
|
||||
idle_timeout,
|
||||
)?,
|
||||
);
|
||||
self.tokens_per_second = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Asks the model for a session title without opening a session for it: the
|
||||
/// turn runs against the shared transient KV cache and only its text is kept.
|
||||
///
|
||||
@@ -318,6 +450,7 @@ impl App {
|
||||
.filter(|message| !message.content.trim().is_empty())
|
||||
.map(|message| ChatTurn {
|
||||
user: message.user,
|
||||
tool: message.tool,
|
||||
skip_previous_eos: false,
|
||||
reasoning: None,
|
||||
reasoning_complete: true,
|
||||
@@ -329,6 +462,7 @@ impl App {
|
||||
}
|
||||
messages.push(ChatTurn {
|
||||
user: true,
|
||||
tool: false,
|
||||
skip_previous_eos: false,
|
||||
reasoning: None,
|
||||
reasoning_complete: true,
|
||||
|
||||
@@ -57,7 +57,13 @@ impl App {
|
||||
} else {
|
||||
let markdown_style = markdown::Style::from_palette(app_theme().palette());
|
||||
for (index, message) in self.conversation.iter().enumerate() {
|
||||
let label = if message.user { "You" } else { "DS4" };
|
||||
let label = if message.user {
|
||||
"You"
|
||||
} else if message.tool {
|
||||
"Tool"
|
||||
} else {
|
||||
"DS4"
|
||||
};
|
||||
let active = self.generating && index + 1 == self.conversation.len();
|
||||
let mut body = column![text(label).size(11)].spacing(5);
|
||||
if let Some(reasoning) = &message.reasoning {
|
||||
@@ -89,11 +95,11 @@ impl App {
|
||||
}
|
||||
}
|
||||
if !message.content.is_empty() {
|
||||
if message.user || message.markdown.is_empty() {
|
||||
if message.user || message.tool || message.markdown.is_empty() {
|
||||
let content = if message.reasoning.is_some() {
|
||||
message.content.trim_start()
|
||||
crate::agent::visible_content(&message.content).trim_start()
|
||||
} else {
|
||||
&message.content
|
||||
crate::agent::visible_content(&message.content)
|
||||
};
|
||||
body = body.push(text(content).size(14));
|
||||
} else {
|
||||
@@ -109,6 +115,14 @@ impl App {
|
||||
} else if active && message.reasoning.is_none() {
|
||||
body = body.push(text("Loading model…").size(14));
|
||||
}
|
||||
if !message.user
|
||||
&& !message.tool
|
||||
&& let Some(model) = ModelChoice::from_id(&self.preferences.selected_model)
|
||||
{
|
||||
for summary in crate::agent::tool_summaries(model, &message.content) {
|
||||
body = body.push(text(summary).size(13).color(muted_text()));
|
||||
}
|
||||
}
|
||||
let user = message.user;
|
||||
messages = messages.push(
|
||||
container(body)
|
||||
|
||||
Reference in New Issue
Block a user