ai_crew_sync/tools/
presence.rs1use rmcp::{
2 ErrorData, Json, handler::server::wrapper::Parameters, service::RequestContext, tool,
3 tool_router,
4};
5use schemars::JsonSchema;
6use serde::Deserialize;
7
8use super::{Bus, auth_of};
9use crate::{
10 model::{AgentInfo, AgentList, SessionList},
11 store::presence,
12};
13
14#[derive(Debug, Deserialize, JsonSchema)]
15pub struct HeartbeatArgs {
16 #[serde(default)]
18 pub status: Option<String>,
19 #[serde(default)]
21 pub repo: Option<String>,
22 #[serde(default)]
24 pub branch: Option<String>,
25 #[serde(default)]
28 pub activity: Option<String>,
29 #[serde(default)]
34 pub project: Option<String>,
35 #[serde(default)]
40 pub role: Option<String>,
41 #[serde(default)]
44 pub ttl_seconds: Option<i64>,
45}
46
47#[derive(Debug, Deserialize, JsonSchema)]
48pub struct ListSessionsArgs {
49 #[serde(default)]
51 pub project: Option<String>,
52 #[serde(default)]
54 pub role: Option<String>,
55 #[serde(default)]
57 pub online_only: bool,
58 #[serde(default)]
61 pub limit: Option<i64>,
62}
63
64#[derive(Debug, Deserialize, JsonSchema)]
65pub struct ListAgentsArgs {
66 #[serde(default)]
68 pub online_only: bool,
69}
70
71#[tool_router(router = presence_router, vis = "pub")]
72impl Bus {
73 #[tool(
74 description = "Publish what you are currently working on so teammates' agents can \
75 see it. Call this when you start a piece of work and whenever the \
76 focus changes. Omitted fields keep their previous value."
77 )]
78 async fn heartbeat(
79 &self,
80 ctx: RequestContext<rmcp::RoleServer>,
81 Parameters(args): Parameters<HeartbeatArgs>,
82 ) -> Result<Json<AgentInfo>, ErrorData> {
83 let auth = auth_of(&ctx)?;
84 let input = presence::HeartbeatInput {
85 status: args.status,
86 repo: args.repo,
87 branch: args.branch,
88 activity: args.activity,
89 project: args.project,
90 role: args.role,
91 ttl_seconds: args.ttl_seconds,
92 };
93 Ok(Json(presence::heartbeat(&self.db, &auth, input).await?))
94 }
95
96 #[tool(
97 description = "See who else is on the bus, whether they are online, and what each \
98 one is working on. Useful before claiming work or asking a question."
99 )]
100 async fn list_agents(
101 &self,
102 ctx: RequestContext<rmcp::RoleServer>,
103 Parameters(args): Parameters<ListAgentsArgs>,
104 ) -> Result<Json<AgentList>, ErrorData> {
105 let auth = auth_of(&ctx)?;
106 Ok(Json(
107 presence::list_agents(&self.db, &auth, args.online_only).await?,
108 ))
109 }
110
111 #[tool(
112 description = "Find the window to talk to. Lists every session in your team \
113 with its address (`agent/session`), project and role, so you can \
114 reach 'the design window of market-data' rather than guessing. \
115 Filter by project and/or role; several sessions may share both \
116 (two reviewers), and each keeps its own address — pick one, never \
117 broadcast a private instruction to all of them. Use the address in \
118 post_message `to` or ask_agent `to`. CHECK `exact` FIRST: it is \
119 false for the shared session, whose address is the bare agent name \
120 and reaches EVERY window that agent has, so it is never a private \
121 target. Labels are what a session said about itself, not proof of \
122 anything."
123 )]
124 async fn list_sessions(
125 &self,
126 ctx: RequestContext<rmcp::RoleServer>,
127 Parameters(args): Parameters<ListSessionsArgs>,
128 ) -> Result<Json<SessionList>, ErrorData> {
129 let auth = auth_of(&ctx)?;
130 Ok(Json(
131 presence::list_sessions(
132 &self.db,
133 &auth,
134 presence::SessionFilter {
135 project: args.project,
136 role: args.role,
137 online_only: args.online_only,
138 limit: args.limit,
139 },
140 )
141 .await?,
142 ))
143 }
144}