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
39async fn has_another_live_session(
45 pool: &sqlx::PgPool,
46 auth: &crate::auth::AuthCtx,
47) -> Result<bool, crate::error::BusError> {
48 let (other,): (bool,) = sqlx::query_as(
49 "SELECT EXISTS (
50 SELECT 1 FROM agent_presence
51 WHERE agent_id = $1 AND session <> $2 AND expires_at > now()
52 )",
53 )
54 .bind(auth.agent_id)
55 .bind(&auth.session)
56 .fetch_one(pool)
57 .await?;
58 Ok(other)
59}
60
61#[derive(Debug, Deserialize, JsonSchema)]
62pub struct PostMessageArgs {
63 #[serde(default)]
65 pub channel: Option<String>,
66 #[serde(default)]
74 pub announce: bool,
75 #[serde(default)]
81 pub to: Option<String>,
82 pub body: String,
85 #[serde(default)]
87 pub reply_to: Option<i64>,
88 #[serde(default)]
90 #[schemars(schema_with = "crate::model::any_json_schema")]
91 pub metadata: Option<serde_json::Value>,
92 #[serde(default)]
95 pub attachments: Option<Vec<AttachmentInput>>,
96}
97
98#[derive(Debug, Deserialize, JsonSchema)]
99pub struct AttachmentInput {
100 pub filename: String,
101 #[serde(default)]
103 pub content_type: Option<String>,
104 pub data_base64: String,
106}
107
108#[derive(Debug, Deserialize, JsonSchema)]
109pub struct ReadMessagesArgs {
110 #[serde(default = "default_scope")]
113 pub scope: String,
114 #[serde(default = "default_true")]
117 pub only_new: bool,
118 #[serde(default = "default_limit")]
120 pub limit: i64,
121 #[serde(default)]
125 pub all_sessions: bool,
126}
127
128#[derive(Debug, Deserialize, JsonSchema)]
129pub struct AskAgentArgs {
130 pub to: String,
134 #[serde(default)]
137 pub question: Option<String>,
138 #[serde(default)]
141 pub timeout_seconds: Option<i64>,
142 #[serde(default)]
145 pub resume_message_id: Option<i64>,
146}
147
148#[derive(Debug, Deserialize, JsonSchema)]
149pub struct SearchMessagesArgs {
150 pub query: String,
152 #[serde(default = "default_limit")]
154 pub limit: i64,
155}
156
157#[tool_router(router = messaging_router, vis = "pub")]
158impl Bus {
159 #[tool(
160 description = "List the team's channels with their topic and message count. \
161 Use this before posting so you pick an existing channel."
162 )]
163 async fn list_channels(
164 &self,
165 ctx: RequestContext<rmcp::RoleServer>,
166 ) -> Result<Json<ChannelList>, ErrorData> {
167 let auth = auth_of(&ctx)?;
168 Ok(Json(messaging::list_channels(&self.db, &auth).await?))
169 }
170
171 #[tool(
172 description = "Create a channel (or update its topic if it already exists). \
173 Channels are shared by the whole team."
174 )]
175 async fn create_channel(
176 &self,
177 ctx: RequestContext<rmcp::RoleServer>,
178 Parameters(args): Parameters<CreateChannelArgs>,
179 ) -> Result<Json<ChannelInfo>, ErrorData> {
180 let auth = auth_of(&ctx)?;
181 Ok(Json(
182 messaging::create_channel(&self.db, &auth, &args.name, args.topic).await?,
183 ))
184 }
185
186 #[tool(
187 description = "Send a message to the team. Set `channel` to broadcast, or `to` \
188 with an agent handle to send a direct message. The sender is \
189 your own identity and cannot be spoofed."
190 )]
191 async fn post_message(
192 &self,
193 ctx: RequestContext<rmcp::RoleServer>,
194 Parameters(args): Parameters<PostMessageArgs>,
195 ) -> Result<Json<PostMessageResult>, ErrorData> {
196 let auth = auth_of(&ctx)?;
197 let raw = args.attachments.unwrap_or_default();
198 if raw.len() > 8 {
199 return Err(
200 crate::error::BusError::invalid("a message carries at most 8 attachments").into(),
201 );
202 }
203 let attachments = raw
204 .into_iter()
205 .map(|a| {
206 crate::store::attachments::decode_input(&a.filename, a.content_type, &a.data_base64)
207 })
208 .collect::<Result<Vec<_>, _>>()?;
209 let input = messaging::PostInput {
210 channel: args.channel,
211 to: args.to,
212 announce: args.announce,
213 body: args.body,
214 reply_to: args.reply_to,
215 metadata: args.metadata,
216 attachments,
217 };
218 Ok(Json(messaging::post_message(&self.db, &auth, input).await?))
219 }
220
221 #[tool(
222 description = "Read messages from the bus. By default returns only what you \
223 have not seen and marks it as read, so calling it repeatedly \
224 gives you an incremental feed of what teammates are saying."
225 )]
226 async fn read_messages(
227 &self,
228 ctx: RequestContext<rmcp::RoleServer>,
229 Parameters(args): Parameters<ReadMessagesArgs>,
230 ) -> Result<Json<MessageList>, ErrorData> {
231 let auth = auth_of(&ctx)?;
232 let input = messaging::ReadInput {
233 scope: args.scope,
234 only_new: args.only_new,
235 limit: args.limit,
236 all_sessions: args.all_sessions,
237 };
238 Ok(Json(
239 messaging::read_messages(&self.db, &auth, input).await?,
240 ))
241 }
242
243 #[tool(
244 description = "Ask a teammate's agent a question and block until they answer or the \
245 timeout passes. Sends the question as a direct message and waits for \
246 their reply, so one call replaces post_message + wait_for_updates + \
247 read_messages. On timeout, call it again with `resume_message_id` set \
248 to the returned question_message_id to keep waiting without asking \
249 twice. If you receive a question yourself, answer with post_message \
250 (`to` the asker, `reply_to` the question id)."
251 )]
252 async fn ask_agent(
253 &self,
254 ctx: RequestContext<rmcp::RoleServer>,
255 Parameters(args): Parameters<AskAgentArgs>,
256 ) -> Result<Json<AskResult>, ErrorData> {
257 let auth = auth_of(&ctx)?;
258 let to = args.to.trim().to_owned();
259 let timeout = args
260 .timeout_seconds
261 .unwrap_or(ASK_DEFAULT_TIMEOUT_SECS)
262 .clamp(5, ASK_MAX_TIMEOUT_SECS);
263
264 let (target_name, target_session) = messaging::parse_address(&to)?;
265 let target_id = agent_id_by_name(&self.db, auth.team_id, &target_name).await?;
266 if target_id == auth.agent_id {
274 let only_me = match target_session.as_deref() {
275 Some(s) => s == auth.session,
277 None => !has_another_live_session(&self.db, &auth).await?,
280 };
281 if only_me {
282 return Err(crate::error::BusError::invalid(
283 "nothing would ever read that question: the address resolves to this \
284 session and no other window of yours is live. Ask a teammate, or \
285 another of your own sessions as 'you/<session>' — list_agents shows \
286 which are open.",
287 )
288 .into());
289 }
290 }
291
292 let mut rx = self.hub.subscribe();
295
296 let question_id = match args.resume_message_id {
297 Some(id) => {
298 messaging::verify_question(
299 &self.db,
300 &auth,
301 target_id,
302 target_session.as_deref(),
303 id,
304 )
305 .await?;
306 id
307 }
308 None => {
309 let question = args
310 .question
311 .as_deref()
312 .map(str::trim)
313 .filter(|q| !q.is_empty())
314 .ok_or_else(|| {
315 crate::error::BusError::invalid(
316 "provide `question`, or `resume_message_id` to keep waiting on \
317 an earlier one",
318 )
319 })?;
320 let posted = messaging::post_message(
321 &self.db,
322 &auth,
323 messaging::PostInput {
324 channel: None,
325 to: Some(to.clone()),
326 announce: false,
329 body: question.to_owned(),
330 reply_to: None,
331 metadata: Some(serde_json::json!({ "question": true })),
332 attachments: Vec::new(),
333 },
334 )
335 .await?;
336 posted.message.id
337 }
338 };
339
340 let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout as u64);
341 loop {
342 if let Some(answer) = messaging::find_answer(
345 &self.db,
346 &auth,
347 target_id,
348 target_session.as_deref(),
349 question_id,
350 )
351 .await?
352 {
353 return Ok(Json(AskResult {
354 answered: true,
355 to,
356 question_message_id: question_id,
357 answer: Some(answer),
358 suggestion: "The answer also sits unread in your inbox; read_messages \
359 will mark it read."
360 .into(),
361 }));
362 }
363
364 let woke = loop {
366 tokio::select! {
367 _ = tokio::time::sleep_until(deadline) => break false,
368 recv = rx.recv() => match recv {
369 Ok(ev) => {
370 let for_us = ev.recipient_session().is_none_or(|s| s == auth.session);
375 if ev.kind() == "message"
376 && ev.sender_agent_id() == Some(target_id)
377 && ev.recipient_agent_id() == Some(auth.agent_id)
378 && ev.message_id().is_some_and(|id| id > question_id)
379 && for_us
380 {
381 break true;
382 }
383 }
384 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => break true,
386 Err(tokio::sync::broadcast::error::RecvError::Closed) => break false,
387 }
388 }
389 };
390
391 if !woke {
392 let answer = messaging::find_answer(
394 &self.db,
395 &auth,
396 target_id,
397 target_session.as_deref(),
398 question_id,
399 )
400 .await?;
401 let answered = answer.is_some();
402 return Ok(Json(AskResult {
403 answered,
404 suggestion: if answered {
405 "The answer also sits unread in your inbox; read_messages will \
406 mark it read."
407 .into()
408 } else {
409 format!(
410 "{to} has not answered within {timeout}s. Call ask_agent again \
411 with resume_message_id={question_id} to keep waiting without \
412 re-sending, or do other work and check read_messages later."
413 )
414 },
415 to,
416 question_message_id: question_id,
417 answer,
418 }));
419 }
420 }
421 }
422
423 #[tool(
424 description = "Full-text search across the team's channel history and your own \
425 direct messages. Does not affect your read cursor."
426 )]
427 async fn search_messages(
428 &self,
429 ctx: RequestContext<rmcp::RoleServer>,
430 Parameters(args): Parameters<SearchMessagesArgs>,
431 ) -> Result<Json<MessageList>, ErrorData> {
432 let auth = auth_of(&ctx)?;
433 Ok(Json(
434 messaging::search_messages(&self.db, &auth, &args.query, args.limit).await?,
435 ))
436 }
437}