j_agent/tools/
send_message.rs1use crate::agent::thread_identity::current_agent_name;
2use crate::teammate::TeammateManager;
3use crate::tools::{PlanDecision, Tool, ToolResult, parse_tool_args, schema_to_tool_params};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use serde_json::Value;
7use std::borrow::Cow;
8use std::sync::{Arc, Mutex, atomic::AtomicBool};
9
10#[derive(Deserialize, JsonSchema)]
12struct SendMessageParams {
13 message: String,
15 #[serde(default)]
17 to: Option<String>,
18}
19
20pub struct SendMessageTool {
22 pub teammate_manager: Arc<Mutex<TeammateManager>>,
23}
24
25impl SendMessageTool {
26 pub const NAME: &'static str = "SendMessage";
27}
28
29impl Tool for SendMessageTool {
30 fn name(&self) -> &str {
31 Self::NAME
32 }
33
34 fn description(&self) -> Cow<'_, str> {
35 r#"
36 Send a message to teammates. This is the ONLY way to make your words visible to other agents.
37
38 Your plain text output (prose without tool calls) is private thinking — only you and the human user can see it.
39 To communicate with teammates, you MUST use this tool.
40
41 Usage:
42 - message: The text content to send
43 - to: Optional target agent name. The message is broadcast to everyone,
44 but @mentioning wakes the target agent (interrupts their idle state).
45
46 Messages appear in the chat as: <YourName> @Target message
47 All agents receive the message, but the @mentioned agent knows it's addressed to them.
48
49 Note: If new messages arrive while you're thinking, your SendMessage may be held for
50 re-evaluation. You'll receive a system_reminder and can choose to resend or revise.
51
52 Example:
53 {"message": "API endpoints are done, check routes/api.js", "to": "Frontend"}
54 {"message": "All frontend pages are complete", "to": "Main"}
55 {"message": "Heads up: database schema has been updated"}
56 "#.into()
57 }
58
59 fn parameters_schema(&self) -> Value {
60 schema_to_tool_params::<SendMessageParams>()
61 }
62
63 fn execute(&self, arguments: &str, _cancelled: &Arc<AtomicBool>) -> ToolResult {
64 let params: SendMessageParams = match parse_tool_args(arguments) {
65 Ok(p) => p,
66 Err(e) => return e,
67 };
68
69 if params.message.trim().is_empty() {
70 return ToolResult {
71 output: "Message cannot be empty".to_string(),
72 is_error: true,
73 images: vec![],
74 plan_decision: PlanDecision::None,
75 };
76 }
77
78 let from = current_agent_name();
79 let to = params.to.as_deref().map(|s| {
80 let s = s.trim_start_matches('@');
81 s.strip_prefix("Teammate@")
83 .or_else(|| s.strip_prefix("SubAgent@"))
84 .unwrap_or(s)
85 });
86
87 match self.teammate_manager.lock() {
88 Ok(manager) => {
89 manager.broadcast(&from, ¶ms.message, to);
90 let target_desc = to
91 .map(|t| format!("@{}", t))
92 .unwrap_or_else(|| "all teammates".to_string());
93 ToolResult {
94 output: format!("Message sent to {}", target_desc),
95 is_error: false,
96 images: vec![],
97 plan_decision: PlanDecision::None,
98 }
99 }
100 Err(_) => ToolResult {
101 output: "Failed to acquire teammate manager lock".to_string(),
102 is_error: true,
103 images: vec![],
104 plan_decision: PlanDecision::None,
105 },
106 }
107 }
108
109 fn requires_confirmation(&self) -> bool {
110 false
111 }
112
113 fn is_available(&self) -> bool {
114 self.teammate_manager
115 .lock()
116 .map(|m| m.has_active_teammates())
117 .unwrap_or(false)
118 }
119}