Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions prompts/en/tools/react_remove_description.md.j2
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions src/prompts/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
8 changes: 7 additions & 1 deletion src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions src/tools/react_remove.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Output, Self::Error> {
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,
})
}
}