1use argentor_core::{ArgentorResult, ToolCall, ToolResult};
2use argentor_skills::skill::{Skill, SkillDescriptor};
3use async_trait::async_trait;
4use std::sync::Arc;
5
6#[async_trait]
9pub trait TaskQueueHandle: Send + Sync {
10 async fn add_task(
12 &self,
13 description: String,
14 role: String,
15 dependencies: Vec<String>,
16 ) -> ArgentorResult<String>;
17
18 async fn get_task_info(&self, task_id: &str) -> ArgentorResult<Option<TaskInfo>>;
20
21 async fn list_tasks(&self) -> ArgentorResult<Vec<TaskInfo>>;
23
24 async fn task_summary(&self) -> ArgentorResult<TaskSummary>;
26}
27
28#[derive(Debug, Clone, serde::Serialize)]
30pub struct TaskInfo {
31 pub id: String,
33 pub description: String,
35 pub role: String,
37 pub status: String,
39}
40
41#[derive(Debug, Clone, serde::Serialize)]
43pub struct TaskSummary {
44 pub total: usize,
46 pub pending: usize,
48 pub running: usize,
50 pub completed: usize,
52 pub failed: usize,
54 pub needs_review: usize,
56}
57
58pub struct AgentDelegateSkill {
60 descriptor: SkillDescriptor,
61 queue: Arc<dyn TaskQueueHandle>,
62}
63
64impl AgentDelegateSkill {
65 pub fn new(queue: Arc<dyn TaskQueueHandle>) -> Self {
67 Self {
68 descriptor: SkillDescriptor {
69 name: "agent_delegate".to_string(),
70 description: "Delegate a subtask to a worker agent. Specify the task description, \
71 target role (spec/coder/tester/reviewer), and optional dependency task IDs."
72 .to_string(),
73 parameters_schema: serde_json::json!({
74 "type": "object",
75 "properties": {
76 "description": {
77 "type": "string",
78 "description": "Description of the subtask to delegate"
79 },
80 "role": {
81 "type": "string",
82 "enum": ["spec", "coder", "tester", "reviewer"],
83 "description": "Worker role to assign the task to"
84 },
85 "dependencies": {
86 "type": "array",
87 "items": { "type": "string" },
88 "description": "Task IDs that must complete before this task starts"
89 }
90 },
91 "required": ["description", "role"]
92 }),
93 required_capabilities: vec![],
94 requires_approval: false,
95 },
96 queue,
97 }
98 }
99}
100
101#[async_trait]
102impl Skill for AgentDelegateSkill {
103 fn descriptor(&self) -> &SkillDescriptor {
104 &self.descriptor
105 }
106
107 async fn execute(&self, call: ToolCall) -> ArgentorResult<ToolResult> {
108 let description = call.arguments["description"]
109 .as_str()
110 .unwrap_or("")
111 .to_string();
112 let role = call.arguments["role"].as_str().unwrap_or("").to_string();
113
114 if description.is_empty() {
115 return Ok(ToolResult::error(&call.id, "Task description is required"));
116 }
117 if role.is_empty() {
118 return Ok(ToolResult::error(&call.id, "Role is required"));
119 }
120
121 let valid_roles = ["spec", "coder", "tester", "reviewer"];
122 if !valid_roles.contains(&role.as_str()) {
123 return Ok(ToolResult::error(
124 &call.id,
125 format!("Invalid role '{role}'. Must be one of: {valid_roles:?}"),
126 ));
127 }
128
129 let dependencies: Vec<String> = call.arguments["dependencies"]
130 .as_array()
131 .map(|arr| {
132 arr.iter()
133 .filter_map(|v| v.as_str().map(String::from))
134 .collect()
135 })
136 .unwrap_or_default();
137
138 let task_id = self
139 .queue
140 .add_task(description.clone(), role.clone(), dependencies)
141 .await?;
142
143 Ok(ToolResult::success(
144 &call.id,
145 serde_json::json!({
146 "delegated": true,
147 "task_id": task_id,
148 "role": role,
149 "description": description
150 })
151 .to_string(),
152 ))
153 }
154}
155
156#[cfg(test)]
157#[allow(clippy::unwrap_used, clippy::expect_used)]
158mod tests {
159 use super::*;
160 use std::sync::atomic::{AtomicUsize, Ordering};
161 use tokio::sync::RwLock;
162
163 struct MockQueue {
165 tasks: RwLock<Vec<TaskInfo>>,
166 counter: AtomicUsize,
167 }
168
169 impl MockQueue {
170 fn new() -> Self {
171 Self {
172 tasks: RwLock::new(Vec::new()),
173 counter: AtomicUsize::new(1),
174 }
175 }
176 }
177
178 #[async_trait]
179 impl TaskQueueHandle for MockQueue {
180 async fn add_task(
181 &self,
182 description: String,
183 role: String,
184 _dependencies: Vec<String>,
185 ) -> ArgentorResult<String> {
186 let id = format!("task-{}", self.counter.fetch_add(1, Ordering::SeqCst));
187 let mut tasks = self.tasks.write().await;
188 tasks.push(TaskInfo {
189 id: id.clone(),
190 description,
191 role,
192 status: "pending".to_string(),
193 });
194 Ok(id)
195 }
196
197 async fn get_task_info(&self, task_id: &str) -> ArgentorResult<Option<TaskInfo>> {
198 let tasks = self.tasks.read().await;
199 Ok(tasks.iter().find(|t| t.id == task_id).cloned())
200 }
201
202 async fn list_tasks(&self) -> ArgentorResult<Vec<TaskInfo>> {
203 Ok(self.tasks.read().await.clone())
204 }
205
206 async fn task_summary(&self) -> ArgentorResult<TaskSummary> {
207 let tasks = self.tasks.read().await;
208 Ok(TaskSummary {
209 total: tasks.len(),
210 pending: tasks.iter().filter(|t| t.status == "pending").count(),
211 running: 0,
212 completed: 0,
213 failed: 0,
214 needs_review: 0,
215 })
216 }
217 }
218
219 #[tokio::test]
220 async fn test_delegate_task() {
221 let queue = Arc::new(MockQueue::new());
222 let skill = AgentDelegateSkill::new(queue.clone());
223 let call = ToolCall {
224 id: "t1".to_string(),
225 name: "agent_delegate".to_string(),
226 arguments: serde_json::json!({
227 "description": "Write unit tests",
228 "role": "tester"
229 }),
230 };
231 let result = skill.execute(call).await.unwrap();
232 assert!(!result.is_error);
233 let parsed: serde_json::Value = serde_json::from_str(&result.content).unwrap();
234 assert_eq!(parsed["delegated"], true);
235 assert_eq!(parsed["role"], "tester");
236
237 let tasks = queue.list_tasks().await.unwrap();
238 assert_eq!(tasks.len(), 1);
239 assert_eq!(tasks[0].description, "Write unit tests");
240 }
241
242 #[tokio::test]
243 async fn test_delegate_with_dependencies() {
244 let queue = Arc::new(MockQueue::new());
245 let skill = AgentDelegateSkill::new(queue.clone());
246 let call = ToolCall {
247 id: "t2".to_string(),
248 name: "agent_delegate".to_string(),
249 arguments: serde_json::json!({
250 "description": "Implement feature",
251 "role": "coder",
252 "dependencies": ["task-1", "task-2"]
253 }),
254 };
255 let result = skill.execute(call).await.unwrap();
256 assert!(!result.is_error);
257 }
258
259 #[tokio::test]
260 async fn test_delegate_empty_description_error() {
261 let queue = Arc::new(MockQueue::new());
262 let skill = AgentDelegateSkill::new(queue);
263 let call = ToolCall {
264 id: "t3".to_string(),
265 name: "agent_delegate".to_string(),
266 arguments: serde_json::json!({
267 "description": "",
268 "role": "coder"
269 }),
270 };
271 let result = skill.execute(call).await.unwrap();
272 assert!(result.is_error);
273 }
274
275 #[tokio::test]
276 async fn test_delegate_invalid_role_error() {
277 let queue = Arc::new(MockQueue::new());
278 let skill = AgentDelegateSkill::new(queue);
279 let call = ToolCall {
280 id: "t4".to_string(),
281 name: "agent_delegate".to_string(),
282 arguments: serde_json::json!({
283 "description": "Do something",
284 "role": "manager"
285 }),
286 };
287 let result = skill.execute(call).await.unwrap();
288 assert!(result.is_error);
289 }
290}