Skip to main content

ai_crew_sync/tools/
presence.rs

1use 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    /// One of "active", "idle", "busy", "blocked". Defaults to "active".
17    #[serde(default)]
18    pub status: Option<String>,
19    /// Repository you are working in, e.g. "acme/api".
20    #[serde(default)]
21    pub repo: Option<String>,
22    /// Branch you are on.
23    #[serde(default)]
24    pub branch: Option<String>,
25    /// Short description of what you are doing right now, e.g. "rewriting the
26    /// token refresh flow". This is what teammates see in list_agents.
27    #[serde(default)]
28    pub activity: Option<String>,
29    /// Discovery label: the logical project this session works on, usually
30    /// the repository name (e.g. "market-data"). One lower-case word. Lets
31    /// teammates find this window with list_sessions, and names the channel
32    /// this session posts to by default. Omit to keep, "" to clear.
33    #[serde(default)]
34    pub project: Option<String>,
35    /// Discovery label: what this session does on that project —
36    /// "implementation", "design", "review", … One lower-case word. Several
37    /// sessions may share a role; each keeps its own address. Omit to keep,
38    /// "" to clear.
39    #[serde(default)]
40    pub role: Option<String>,
41    /// How long this presence stays valid before you are shown as offline.
42    /// Defaults to 600 (10 minutes).
43    #[serde(default)]
44    pub ttl_seconds: Option<i64>,
45}
46
47#[derive(Debug, Deserialize, JsonSchema)]
48pub struct ListSessionsArgs {
49    /// Only sessions that published this project label.
50    #[serde(default)]
51    pub project: Option<String>,
52    /// Only sessions that published this role label.
53    #[serde(default)]
54    pub role: Option<String>,
55    /// Only sessions whose presence has not expired.
56    #[serde(default)]
57    pub online_only: bool,
58    /// Most sessions to return (1-1000, default 200). Narrow with `project`
59    /// or `role` rather than raising it.
60    #[serde(default)]
61    pub limit: Option<i64>,
62}
63
64#[derive(Debug, Deserialize, JsonSchema)]
65pub struct ListAgentsArgs {
66    /// Only return agents whose presence has not expired.
67    #[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}