Skip to main content

ai_crew_sync/tools/
tasks.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::{ClaimResult, TaskDetail, TaskInfo, TaskList},
11    store::tasks,
12};
13
14fn default_limit() -> i64 {
15    50
16}
17
18#[derive(Debug, Deserialize, JsonSchema)]
19pub struct CreateTaskArgs {
20    /// Stable, human-recognisable identifier, e.g. "refactor-auth" or "api#421".
21    /// Must be unique within the team.
22    pub key: String,
23    /// One-line summary of the work.
24    pub title: String,
25    /// Optional longer description: acceptance criteria, relevant files, context
26    /// a teammate's agent would need to pick this up cold.
27    #[serde(default)]
28    pub description: Option<String>,
29    /// Optional structured payload (any JSON object).
30    #[serde(default)]
31    pub metadata: Option<serde_json::Value>,
32    /// Keys of existing tasks this one depends on. It cannot be claimed until
33    /// every dependency is done or cancelled, and claim_next_task skips it.
34    #[serde(default)]
35    pub depends_on: Vec<String>,
36}
37
38#[derive(Debug, Deserialize, JsonSchema)]
39pub struct ListTasksArgs {
40    /// Filter by status: "open", "claimed", "done", "cancelled", or "any".
41    /// Defaults to all statuses.
42    #[serde(default)]
43    pub status: Option<String>,
44    /// Only return tasks currently claimed by you.
45    #[serde(default)]
46    pub mine_only: bool,
47    /// Maximum tasks to return (1-200).
48    #[serde(default = "default_limit")]
49    pub limit: i64,
50}
51
52#[derive(Debug, Deserialize, JsonSchema)]
53pub struct TaskKeyArgs {
54    /// The task key.
55    pub key: String,
56}
57
58#[derive(Debug, Deserialize, JsonSchema)]
59pub struct ClaimTaskArgs {
60    /// The task key to claim.
61    pub key: String,
62    /// How long your claim should hold before another agent may take over.
63    /// Defaults to 900 (15 minutes). Renew it if the work runs longer.
64    #[serde(default)]
65    pub lease_seconds: Option<i64>,
66}
67
68#[derive(Debug, Deserialize, JsonSchema)]
69pub struct ClaimNextArgs {
70    /// Lease duration in seconds for the claim. Defaults to 900.
71    #[serde(default)]
72    pub lease_seconds: Option<i64>,
73}
74
75#[derive(Debug, Deserialize, JsonSchema)]
76pub struct CompleteTaskArgs {
77    /// The task key.
78    pub key: String,
79    /// What was done, and anything the next person needs to know. This is what
80    /// teammates will read instead of asking you.
81    #[serde(default)]
82    pub result: Option<String>,
83}
84
85#[tool_router(router = tasks_router, vis = "pub")]
86impl Bus {
87    #[tool(
88        description = "Register a unit of shared work so the team can coordinate on it. \
89                       Creating a task does not claim it. Use depends_on to chain work \
90                       into a pipeline."
91    )]
92    async fn create_task(
93        &self,
94        ctx: RequestContext<rmcp::RoleServer>,
95        Parameters(args): Parameters<CreateTaskArgs>,
96    ) -> Result<Json<TaskInfo>, ErrorData> {
97        let auth = auth_of(&ctx)?;
98        let input = tasks::CreateInput {
99            key: args.key,
100            title: args.title,
101            description: args.description,
102            metadata: args.metadata,
103            depends_on: args.depends_on,
104        };
105        Ok(Json(tasks::create_task(&self.db, &auth, input).await?))
106    }
107
108    #[tool(
109        description = "List the team's tasks with who holds each one. Check this before \
110                       starting work so you do not duplicate a teammate's effort."
111    )]
112    async fn list_tasks(
113        &self,
114        ctx: RequestContext<rmcp::RoleServer>,
115        Parameters(args): Parameters<ListTasksArgs>,
116    ) -> Result<Json<TaskList>, ErrorData> {
117        let auth = auth_of(&ctx)?;
118        Ok(Json(
119            tasks::list_tasks(&self.db, &auth, args.status, args.mine_only, args.limit).await?,
120        ))
121    }
122
123    #[tool(description = "Get one task with its full history of claims and completions.")]
124    async fn get_task(
125        &self,
126        ctx: RequestContext<rmcp::RoleServer>,
127        Parameters(args): Parameters<TaskKeyArgs>,
128    ) -> Result<Json<TaskDetail>, ErrorData> {
129        let auth = auth_of(&ctx)?;
130        Ok(Json(tasks::get_task(&self.db, &auth, &args.key).await?))
131    }
132
133    #[tool(
134        description = "Take exclusive ownership of a task before working on it. Fails \
135                       cleanly (claimed=false) if a teammate holds an unexpired claim. \
136                       Re-claiming a task you already hold extends your lease."
137    )]
138    async fn claim_task(
139        &self,
140        ctx: RequestContext<rmcp::RoleServer>,
141        Parameters(args): Parameters<ClaimTaskArgs>,
142    ) -> Result<Json<ClaimResult>, ErrorData> {
143        let auth = auth_of(&ctx)?;
144        Ok(Json(
145            tasks::claim_task(&self.db, &auth, &args.key, args.lease_seconds).await?,
146        ))
147    }
148
149    #[tool(
150        description = "Claim the oldest available task without naming it. Safe to call \
151                       concurrently from several agents: each gets a different task."
152    )]
153    async fn claim_next_task(
154        &self,
155        ctx: RequestContext<rmcp::RoleServer>,
156        Parameters(args): Parameters<ClaimNextArgs>,
157    ) -> Result<Json<ClaimResult>, ErrorData> {
158        let auth = auth_of(&ctx)?;
159        Ok(Json(
160            tasks::claim_next_task(&self.db, &auth, args.lease_seconds).await?,
161        ))
162    }
163
164    #[tool(
165        description = "Extend the lease on a task you hold. Call this periodically \
166                       during long work so the claim does not lapse and get stolen."
167    )]
168    async fn renew_task_lease(
169        &self,
170        ctx: RequestContext<rmcp::RoleServer>,
171        Parameters(args): Parameters<ClaimTaskArgs>,
172    ) -> Result<Json<TaskInfo>, ErrorData> {
173        let auth = auth_of(&ctx)?;
174        Ok(Json(
175            tasks::renew_lease(&self.db, &auth, &args.key, args.lease_seconds).await?,
176        ))
177    }
178
179    #[tool(
180        description = "Give up a task you claimed without finishing it, returning it to \
181                       the open pool for someone else."
182    )]
183    async fn release_task(
184        &self,
185        ctx: RequestContext<rmcp::RoleServer>,
186        Parameters(args): Parameters<TaskKeyArgs>,
187    ) -> Result<Json<TaskInfo>, ErrorData> {
188        let auth = auth_of(&ctx)?;
189        Ok(Json(tasks::release_task(&self.db, &auth, &args.key).await?))
190    }
191
192    #[tool(
193        description = "Mark a task as done and record what was done. Write the result as \
194                       if a teammate's agent will read it with no other context."
195    )]
196    async fn complete_task(
197        &self,
198        ctx: RequestContext<rmcp::RoleServer>,
199        Parameters(args): Parameters<CompleteTaskArgs>,
200    ) -> Result<Json<TaskInfo>, ErrorData> {
201        let auth = auth_of(&ctx)?;
202        Ok(Json(
203            tasks::complete_task(&self.db, &auth, &args.key, args.result).await?,
204        ))
205    }
206}