1use std::time::Duration;
2
3use rmcp::{
4 ErrorData, Json, handler::server::wrapper::Parameters, service::RequestContext, tool,
5 tool_router,
6};
7use schemars::JsonSchema;
8use serde::Deserialize;
9
10use super::{Bus, auth_of};
11use crate::{
12 model::{AskResult, ChannelInfo, ChannelList, MessageList, PostMessageResult},
13 store::{agent_id_by_name, messaging},
14};
15
16const ASK_DEFAULT_TIMEOUT_SECS: i64 = 45;
17const ASK_MAX_TIMEOUT_SECS: i64 = 55;
18
19fn default_limit() -> i64 {
20 50
21}
22fn default_true() -> bool {
23 true
24}
25fn default_scope() -> String {
26 "all".into()
27}
28
29#[derive(Debug, Deserialize, JsonSchema)]
30pub struct CreateChannelArgs {
31 pub name: String,
34 #[serde(default)]
36 pub topic: Option<String>,
37}
38
39#[derive(Debug, Deserialize, JsonSchema)]
40pub struct PostMessageArgs {
41 #[serde(default)]
43 pub channel: Option<String>,
44 #[serde(default)]
46 pub to: Option<String>,
47 pub body: String,
49 #[serde(default)]
51 pub reply_to: Option<i64>,
52 #[serde(default)]
54 pub metadata: Option<serde_json::Value>,
55}
56
57#[derive(Debug, Deserialize, JsonSchema)]
58pub struct ReadMessagesArgs {
59 #[serde(default = "default_scope")]
62 pub scope: String,
63 #[serde(default = "default_true")]
66 pub only_new: bool,
67 #[serde(default = "default_limit")]
69 pub limit: i64,
70}
71
72#[derive(Debug, Deserialize, JsonSchema)]
73pub struct AskAgentArgs {
74 pub to: String,
76 #[serde(default)]
79 pub question: Option<String>,
80 #[serde(default)]
83 pub timeout_seconds: Option<i64>,
84 #[serde(default)]
87 pub resume_message_id: Option<i64>,
88}
89
90#[derive(Debug, Deserialize, JsonSchema)]
91pub struct SearchMessagesArgs {
92 pub query: String,
94 #[serde(default = "default_limit")]
96 pub limit: i64,
97}
98
99#[tool_router(router = messaging_router, vis = "pub")]
100impl Bus {
101 #[tool(
102 description = "List the team's channels with their topic and message count. \
103 Use this before posting so you pick an existing channel."
104 )]
105 async fn list_channels(
106 &self,
107 ctx: RequestContext<rmcp::RoleServer>,
108 ) -> Result<Json<ChannelList>, ErrorData> {
109 let auth = auth_of(&ctx)?;
110 Ok(Json(messaging::list_channels(&self.db, &auth).await?))
111 }
112
113 #[tool(
114 description = "Create a channel (or update its topic if it already exists). \
115 Channels are shared by the whole team."
116 )]
117 async fn create_channel(
118 &self,
119 ctx: RequestContext<rmcp::RoleServer>,
120 Parameters(args): Parameters<CreateChannelArgs>,
121 ) -> Result<Json<ChannelInfo>, ErrorData> {
122 let auth = auth_of(&ctx)?;
123 Ok(Json(
124 messaging::create_channel(&self.db, &auth, &args.name, args.topic).await?,
125 ))
126 }
127
128 #[tool(
129 description = "Send a message to the team. Set `channel` to broadcast, or `to` \
130 with an agent handle to send a direct message. The sender is \
131 your own identity and cannot be spoofed."
132 )]
133 async fn post_message(
134 &self,
135 ctx: RequestContext<rmcp::RoleServer>,
136 Parameters(args): Parameters<PostMessageArgs>,
137 ) -> Result<Json<PostMessageResult>, ErrorData> {
138 let auth = auth_of(&ctx)?;
139 let input = messaging::PostInput {
140 channel: args.channel,
141 to: args.to,
142 body: args.body,
143 reply_to: args.reply_to,
144 metadata: args.metadata,
145 };
146 Ok(Json(messaging::post_message(&self.db, &auth, input).await?))
147 }
148
149 #[tool(
150 description = "Read messages from the bus. By default returns only what you \
151 have not seen and marks it as read, so calling it repeatedly \
152 gives you an incremental feed of what teammates are saying."
153 )]
154 async fn read_messages(
155 &self,
156 ctx: RequestContext<rmcp::RoleServer>,
157 Parameters(args): Parameters<ReadMessagesArgs>,
158 ) -> Result<Json<MessageList>, ErrorData> {
159 let auth = auth_of(&ctx)?;
160 let input = messaging::ReadInput {
161 scope: args.scope,
162 only_new: args.only_new,
163 limit: args.limit,
164 };
165 Ok(Json(
166 messaging::read_messages(&self.db, &auth, input).await?,
167 ))
168 }
169
170 #[tool(
171 description = "Ask a teammate's agent a question and block until they answer or the \
172 timeout passes. Sends the question as a direct message and waits for \
173 their reply, so one call replaces post_message + wait_for_updates + \
174 read_messages. On timeout, call it again with `resume_message_id` set \
175 to the returned question_message_id to keep waiting without asking \
176 twice. If you receive a question yourself, answer with post_message \
177 (`to` the asker, `reply_to` the question id)."
178 )]
179 async fn ask_agent(
180 &self,
181 ctx: RequestContext<rmcp::RoleServer>,
182 Parameters(args): Parameters<AskAgentArgs>,
183 ) -> Result<Json<AskResult>, ErrorData> {
184 let auth = auth_of(&ctx)?;
185 let to = args.to.trim().to_owned();
186 let timeout = args
187 .timeout_seconds
188 .unwrap_or(ASK_DEFAULT_TIMEOUT_SECS)
189 .clamp(5, ASK_MAX_TIMEOUT_SECS);
190
191 let target_id = agent_id_by_name(&self.db, auth.team_id, &to).await?;
192 if target_id == auth.agent_id {
193 return Err(crate::error::BusError::invalid(
194 "you cannot ask yourself; call list_agents and pick a teammate",
195 )
196 .into());
197 }
198
199 let mut rx = self.hub.subscribe();
202
203 let question_id = match args.resume_message_id {
204 Some(id) => {
205 messaging::verify_question(&self.db, &auth, target_id, id).await?;
206 id
207 }
208 None => {
209 let question = args
210 .question
211 .as_deref()
212 .map(str::trim)
213 .filter(|q| !q.is_empty())
214 .ok_or_else(|| {
215 crate::error::BusError::invalid(
216 "provide `question`, or `resume_message_id` to keep waiting on \
217 an earlier one",
218 )
219 })?;
220 let posted = messaging::post_message(
221 &self.db,
222 &auth,
223 messaging::PostInput {
224 channel: None,
225 to: Some(to.clone()),
226 body: question.to_owned(),
227 reply_to: None,
228 metadata: Some(serde_json::json!({ "question": true })),
229 },
230 )
231 .await?;
232 posted.message.id
233 }
234 };
235
236 let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout as u64);
237 loop {
238 if let Some(answer) =
241 messaging::find_answer(&self.db, &auth, target_id, question_id).await?
242 {
243 return Ok(Json(AskResult {
244 answered: true,
245 to,
246 question_message_id: question_id,
247 answer: Some(answer),
248 suggestion: "The answer also sits unread in your inbox; read_messages \
249 will mark it read."
250 .into(),
251 }));
252 }
253
254 let woke = loop {
256 tokio::select! {
257 _ = tokio::time::sleep_until(deadline) => break false,
258 recv = rx.recv() => match recv {
259 Ok(ev) => {
260 if ev.kind() == "message"
261 && ev.sender_agent_id() == Some(target_id)
262 && ev.recipient_agent_id() == Some(auth.agent_id)
263 {
264 break true;
265 }
266 }
267 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => break true,
269 Err(tokio::sync::broadcast::error::RecvError::Closed) => break false,
270 }
271 }
272 };
273
274 if !woke {
275 let answer =
277 messaging::find_answer(&self.db, &auth, target_id, question_id).await?;
278 let answered = answer.is_some();
279 return Ok(Json(AskResult {
280 answered,
281 suggestion: if answered {
282 "The answer also sits unread in your inbox; read_messages will \
283 mark it read."
284 .into()
285 } else {
286 format!(
287 "{to} has not answered within {timeout}s. Call ask_agent again \
288 with resume_message_id={question_id} to keep waiting without \
289 re-sending, or do other work and check read_messages later."
290 )
291 },
292 to,
293 question_message_id: question_id,
294 answer,
295 }));
296 }
297 }
298 }
299
300 #[tool(
301 description = "Full-text search across the team's channel history and your own \
302 direct messages. Does not affect your read cursor."
303 )]
304 async fn search_messages(
305 &self,
306 ctx: RequestContext<rmcp::RoleServer>,
307 Parameters(args): Parameters<SearchMessagesArgs>,
308 ) -> Result<Json<MessageList>, ErrorData> {
309 let auth = auth_of(&ctx)?;
310 Ok(Json(
311 messaging::search_messages(&self.db, &auth, &args.query, args.limit).await?,
312 ))
313 }
314}