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