diff --git a/prompts/en/tools/react_remove_description.md.j2 b/prompts/en/tools/react_remove_description.md.j2 new file mode 100644 index 000000000..bae0b1569 --- /dev/null +++ b/prompts/en/tools/react_remove_description.md.j2 @@ -0,0 +1 @@ +Remove an emoji reaction you previously added to the user's message. Use this to clean up acknowledgment reactions (like 👀) after you've posted your response. \ No newline at end of file diff --git a/src/prompts/text.rs b/src/prompts/text.rs index 53122f42d..651c1934a 100644 --- a/src/prompts/text.rs +++ b/src/prompts/text.rs @@ -151,6 +151,9 @@ fn lookup(lang: &str, key: &str) -> &'static str { ("en", "tools/cancel") => include_str!("../../prompts/en/tools/cancel_description.md.j2"), ("en", "tools/skip") => include_str!("../../prompts/en/tools/skip_description.md.j2"), ("en", "tools/react") => include_str!("../../prompts/en/tools/react_description.md.j2"), + ("en", "tools/react_remove") => { + include_str!("../../prompts/en/tools/react_remove_description.md.j2") + } ("en", "tools/set_status") => { include_str!("../../prompts/en/tools/set_status_description.md.j2") } diff --git a/src/tools.rs b/src/tools.rs index 9e19c4538..b744819ad 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -6,7 +6,7 @@ //! ## ToolServer Topology //! //! **Channel ToolServer** (one per channel): -//! - `reply`, `branch`, `spawn_worker`, `route`, `cancel`, `skip`, `react` — added +//! - `reply`, `branch`, `spawn_worker`, `route`, `cancel`, `skip`, `react`, `react_remove` — added //! dynamically per conversation turn via `add_channel_tools()` / //! `remove_channel_tools()` because they hold per-channel state. //! - No memory tools — the channel delegates memory work to branches. @@ -45,6 +45,7 @@ pub mod memory_recall; pub mod memory_save; pub mod project_manage; pub mod react; +pub mod react_remove; pub mod read_skill; pub mod reply; pub mod route; @@ -115,6 +116,7 @@ pub use project_manage::{ ProjectManageArgs, ProjectManageError, ProjectManageOutput, ProjectManageTool, }; pub use react::{ReactArgs, ReactError, ReactOutput, ReactTool}; +pub use react_remove::{ReactRemoveArgs, ReactRemoveError, ReactRemoveOutput, ReactRemoveTool}; pub use read_skill::{ReadSkillArgs, ReadSkillError, ReadSkillOutput, ReadSkillTool}; pub use reply::{RepliedFlag, ReplyArgs, ReplyError, ReplyOutput, ReplyTool, new_replied_flag}; pub use route::{RouteArgs, RouteError, RouteOutput, RouteTool}; @@ -433,6 +435,9 @@ pub async fn add_channel_tools( .add_tool(SkipTool::new(skip_flag.clone(), response_tx.clone())) .await?; handle.add_tool(ReactTool::new(response_tx.clone())).await?; + handle + .add_tool(ReactRemoveTool::new(response_tx.clone())) + .await?; if let Some(cron_tool) = cron_tool { let cron_tool = cron_tool .with_default_delivery_target(default_delivery_target_for_conversation( @@ -492,6 +497,7 @@ pub async fn remove_channel_tools( handle.remove_tool(SkipTool::NAME).await?; handle.remove_tool(SendFileTool::NAME).await?; handle.remove_tool(ReactTool::NAME).await?; + handle.remove_tool(ReactRemoveTool::NAME).await?; handle.remove_tool(ProjectManageTool::NAME).await?; // Cron, send_message, send_agent_message, and attachment_recall removal is // best-effort since not all channels have them diff --git a/src/tools/react_remove.rs b/src/tools/react_remove.rs new file mode 100644 index 000000000..7c910c4d3 --- /dev/null +++ b/src/tools/react_remove.rs @@ -0,0 +1,79 @@ +//! React-remove tool for removing emoji reactions from messages (channel only). + +use crate::{OutboundResponse, RoutedSender}; +use rig::completion::ToolDefinition; +use rig::tool::Tool; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Tool for removing a reaction from the triggering message. +#[derive(Debug, Clone)] +pub struct ReactRemoveTool { + response_tx: RoutedSender, +} + +impl ReactRemoveTool { + pub fn new(response_tx: RoutedSender) -> Self { + Self { response_tx } + } +} + +/// Error type for react_remove tool. +#[derive(Debug, thiserror::Error)] +#[error("React remove failed: {0}")] +pub struct ReactRemoveError(String); + +/// Arguments for react_remove tool. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ReactRemoveArgs { + /// The emoji to remove. Use the same unicode emoji character that was originally reacted (e.g. "👍", "👀"). + pub emoji: String, +} + +/// Output from react_remove tool. +#[derive(Debug, Serialize)] +pub struct ReactRemoveOutput { + pub success: bool, + pub emoji: String, +} + +impl Tool for ReactRemoveTool { + const NAME: &'static str = "react_remove"; + + type Error = ReactRemoveError; + type Args = ReactRemoveArgs; + type Output = ReactRemoveOutput; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: crate::prompts::text::get("tools/react_remove").to_string(), + parameters: serde_json::json!({ + "type": "object", + "properties": { + "emoji": { + "type": "string", + "description": "The unicode emoji character to remove (e.g. \"👍\", \"👀\")." + } + }, + "required": ["emoji"] + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + tracing::info!(emoji = %args.emoji, "react_remove tool called"); + + self.response_tx + .send(OutboundResponse::RemoveReaction(args.emoji.clone())) + .await + .map_err(|error| { + ReactRemoveError(format!("failed to send remove reaction: {error}")) + })?; + + Ok(ReactRemoveOutput { + success: true, + emoji: args.emoji, + }) + } +}