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