ai_crew_sync/tools/
mod.rs1pub mod attachments;
2pub mod events;
3pub mod locks;
4pub mod messaging;
5pub mod notes;
6pub mod presence;
7pub mod tasks;
8
9use rmcp::{
10 ErrorData, Json, ServerHandler,
11 handler::server::{router::tool::ToolRouter, wrapper::Parameters},
12 model::{ServerCapabilities, ServerInfo},
13 service::RequestContext,
14 tool, tool_handler, tool_router,
15};
16use schemars::JsonSchema;
17use serde::Deserialize;
18use sqlx::PgPool;
19
20use crate::{
21 auth::AuthCtx,
22 events::EventHub,
23 model::{DigestResult, WhoAmI},
24 store,
25};
26
27pub const INSTRUCTIONS: &str = r#"
28Shared coordination bus for a team of AI coding agents. Every agent in the
29team is connected to the same bus, so anything you write here is visible to your
30teammates' agents, and anything they write is visible to you.
31
32Identity is taken from your bearer token; you never pass your own name as an
33argument. Call `whoami` once at the start of a session to learn your handle and
34see whether anything is waiting for you.
35
36Four capabilities:
37
38- Messaging. `post_message` to a `channel` (broadcast to the whole team) or to a
39 single agent via `to` (direct message). `read_messages` returns only what you
40 have not seen yet by default, and advances your read cursor.
41- Task coordination. Before starting shared work, `claim_task` so two agents do
42 not do the same job twice. Claims hold a lease that expires, so call
43 `renew_task_lease` on long jobs and `complete_task` or `release_task` when you
44 stop. An expired lease can be taken over by anyone. Tasks can depend on other
45 tasks (`depends_on` at creation); blocked tasks cannot be claimed until their
46 dependencies are done, and `claim_next_task` skips them.
47- Locks. `acquire_lock` before touching a contended resource ("deploy:staging",
48 "schema:users"); `release_lock` when finished. Lighter than a task, expires on
49 its own.
50- Presence. `heartbeat` publishes what you are working on (repo, branch, a short
51 activity string). `list_agents` shows who else is active right now.
52- Shared notes. `set_note` / `get_note` / `search_notes` are the team's durable
53 memory: decisions, gotchas, deploy state. Prefer a note over repeating the
54 same explanation in chat.
55- Waiting. When you are blocked on teammates, call `wait_for_updates` instead of
56 polling: it blocks until a relevant message/task/lock/note event arrives or
57 the timeout passes. `team_digest` summarises the last hours of team activity —
58 useful at session start to catch up.
59- Attachments. Small files (diffs, logs, configs — max 256 KiB each) travel
60 with messages (`post_message` `attachments`) or tasks (`attach_file`), and
61 are fetched with `get_attachment`. Share the artifact itself instead of
62 describing it.
63- Asking. When you need an answer from a specific teammate to continue,
64 `ask_agent` sends them the question and waits for the reply in one call. If
65 you receive a question (a direct message marked `"question": true`), answer
66 promptly with `post_message` (`to` the asker, `reply_to` the question id) —
67 their agent is blocked waiting on you.
68
69Conventions worth following: keep messages short and factual, scope notes by
70repository name, and use task keys that a human would recognise.
71"#;
72
73#[derive(Clone)]
77pub struct Bus {
78 pub db: PgPool,
79 pub hub: EventHub,
80 pub tool_router: ToolRouter<Self>,
81}
82
83impl Bus {
84 pub fn new(db: PgPool, hub: EventHub) -> Self {
85 let tool_router = Self::core_router()
86 + Self::messaging_router()
87 + Self::tasks_router()
88 + Self::presence_router()
89 + Self::notes_router()
90 + Self::locks_router()
91 + Self::events_router()
92 + Self::attachments_router();
93 Self {
94 db,
95 hub,
96 tool_router,
97 }
98 }
99}
100
101pub fn auth_of(ctx: &RequestContext<rmcp::RoleServer>) -> Result<AuthCtx, ErrorData> {
105 ctx.extensions
106 .get::<http::request::Parts>()
107 .and_then(|parts| parts.extensions.get::<AuthCtx>())
108 .cloned()
109 .ok_or_else(|| {
110 ErrorData::invalid_request(
111 "no authentication context on this request; the server is misconfigured",
112 None,
113 )
114 })
115}
116
117#[tool_router(router = core_router, vis = "pub")]
118impl Bus {
119 #[tool(
120 description = "Identify yourself on the bus: your agent handle, your team, \
121 how many unread direct messages you have and how many tasks \
122 you currently hold. Call this first in a session."
123 )]
124 async fn whoami(
125 &self,
126 ctx: RequestContext<rmcp::RoleServer>,
127 ) -> Result<Json<WhoAmI>, ErrorData> {
128 let auth = auth_of(&ctx)?;
129 Ok(Json(store::whoami(&self.db, &auth).await?))
130 }
131
132 #[tool(
133 description = "Summarise the team's last hours: channel activity, tasks that moved, \
134 notes touched, who was around, active locks. Call it at session start \
135 to catch up, or to prepare a standup. Direct messages are excluded."
136 )]
137 async fn team_digest(
138 &self,
139 ctx: RequestContext<rmcp::RoleServer>,
140 Parameters(args): Parameters<DigestArgs>,
141 ) -> Result<Json<DigestResult>, ErrorData> {
142 let auth = auth_of(&ctx)?;
143 Ok(Json(
144 store::digest::team_digest(&self.db, &auth, args.hours.unwrap_or(24)).await?,
145 ))
146 }
147}
148
149#[derive(Debug, Deserialize, JsonSchema)]
150pub struct DigestArgs {
151 #[serde(default)]
153 pub hours: Option<i64>,
154}
155
156#[tool_handler(router = self.tool_router)]
157impl ServerHandler for Bus {
158 fn get_info(&self) -> ServerInfo {
159 let mut info = ServerInfo::new(ServerCapabilities::builder().enable_tools().build());
160 info.instructions = Some(INSTRUCTIONS.trim().to_string());
161 info
162 }
163}