1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SubTask {
14 pub id: String,
16
17 pub name: String,
19
20 pub instruction: String,
22
23 pub specialty: Option<String>,
25
26 pub dependencies: Vec<String>,
28
29 pub priority: i32,
31
32 pub max_steps: usize,
34
35 pub context: SubTaskContext,
37
38 pub status: SubTaskStatus,
40
41 pub assigned_agent: Option<String>,
43
44 pub created_at: DateTime<Utc>,
46
47 pub completed_at: Option<DateTime<Utc>>,
49
50 pub stage: usize,
52
53 #[serde(default)]
65 pub needs_worktree: Option<bool>,
66}
67
68impl SubTask {
69 pub fn new(name: impl Into<String>, instruction: impl Into<String>) -> Self {
71 Self {
72 id: Uuid::new_v4().to_string(),
73 name: name.into(),
74 instruction: instruction.into(),
75 specialty: None,
76 dependencies: Vec::new(),
77 priority: 0,
78 max_steps: 100,
79 context: SubTaskContext::default(),
80 status: SubTaskStatus::Pending,
81 assigned_agent: None,
82 created_at: Utc::now(),
83 completed_at: None,
84 stage: 0,
85 needs_worktree: None,
86 }
87 }
88
89 pub fn with_specialty(mut self, specialty: impl Into<String>) -> Self {
91 self.specialty = Some(specialty.into());
92 self
93 }
94
95 pub fn with_dependencies(mut self, deps: Vec<String>) -> Self {
97 self.dependencies = deps;
98 self
99 }
100
101 pub fn with_priority(mut self, priority: i32) -> Self {
103 self.priority = priority;
104 self
105 }
106
107 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
109 self.max_steps = max_steps;
110 self
111 }
112
113 pub fn with_context(mut self, context: SubTaskContext) -> Self {
115 self.context = context;
116 self
117 }
118
119 pub fn with_needs_worktree(mut self, needs: bool) -> Self {
126 self.needs_worktree = Some(needs);
127 self
128 }
129
130 pub fn needs_worktree(&self) -> bool {
144 if let Some(explicit) = self.needs_worktree {
145 return explicit;
146 }
147 let haystack_specialty = self.specialty.as_deref().unwrap_or("").to_ascii_lowercase();
148 let haystack_instruction = self.instruction.to_ascii_lowercase();
149
150 const READONLY_HINTS: &[&str] = &[
151 "research",
152 "review",
153 "analy",
154 "audit",
155 "plan",
156 "fact",
157 "summari",
158 "explore",
159 "docs",
160 "read-only",
161 "readonly",
162 ];
163 const MUTATING_HINTS: &[&str] = &[
164 "write",
165 "edit",
166 "create ",
167 "fix",
168 "implement",
169 "refactor",
170 "apply",
171 "commit",
172 "patch",
173 "scaffold",
174 "build",
175 ];
176
177 if READONLY_HINTS
178 .iter()
179 .any(|k| haystack_specialty.contains(k))
180 && !MUTATING_HINTS
181 .iter()
182 .any(|k| haystack_instruction.contains(k))
183 {
184 return false;
185 }
186 true
187 }
188
189 pub fn can_run(&self, completed: &[String]) -> bool {
191 self.dependencies.iter().all(|dep| completed.contains(dep))
192 }
193
194 pub fn start(&mut self, agent_id: &str) {
196 self.status = SubTaskStatus::Running;
197 self.assigned_agent = Some(agent_id.to_string());
198 }
199
200 pub fn complete(&mut self, success: bool) {
202 self.status = if success {
203 SubTaskStatus::Completed
204 } else {
205 SubTaskStatus::Failed
206 };
207 self.completed_at = Some(Utc::now());
208 }
209}
210
211#[derive(Debug, Clone, Default, Serialize, Deserialize)]
213pub struct SubTaskContext {
214 pub parent_task: Option<String>,
216
217 pub dependency_results: HashMap<String, String>,
219
220 pub shared_resources: Vec<String>,
222
223 pub metadata: HashMap<String, serde_json::Value>,
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum SubTaskStatus {
231 Pending,
233
234 Blocked,
236
237 Running,
239
240 Completed,
242
243 Failed,
245
246 Cancelled,
248
249 TimedOut,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct SubAgent {
256 pub id: String,
258
259 pub name: String,
261
262 pub specialty: String,
264
265 pub subtask_id: String,
267
268 pub status: SubAgentStatus,
270
271 pub steps: usize,
273
274 pub tool_calls: Vec<ToolCallRecord>,
276
277 pub output: String,
279
280 pub model: String,
282
283 pub provider: String,
285
286 pub created_at: DateTime<Utc>,
288
289 pub last_active: DateTime<Utc>,
291}
292
293impl SubAgent {
294 pub fn new(
296 name: impl Into<String>,
297 specialty: impl Into<String>,
298 subtask_id: impl Into<String>,
299 model: impl Into<String>,
300 provider: impl Into<String>,
301 ) -> Self {
302 let now = Utc::now();
303 Self {
304 id: Uuid::new_v4().to_string(),
305 name: name.into(),
306 specialty: specialty.into(),
307 subtask_id: subtask_id.into(),
308 status: SubAgentStatus::Initializing,
309 steps: 0,
310 tool_calls: Vec::new(),
311 output: String::new(),
312 model: model.into(),
313 provider: provider.into(),
314 created_at: now,
315 last_active: now,
316 }
317 }
318
319 pub fn record_tool_call(&mut self, name: &str, success: bool) {
321 self.tool_calls.push(ToolCallRecord {
322 name: name.to_string(),
323 timestamp: Utc::now(),
324 success,
325 });
326 self.steps += 1;
327 self.last_active = Utc::now();
328 }
329
330 pub fn append_output(&mut self, text: &str) {
332 self.output.push_str(text);
333 self.last_active = Utc::now();
334 }
335
336 pub fn set_status(&mut self, status: SubAgentStatus) {
338 self.status = status;
339 self.last_active = Utc::now();
340 }
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
345#[serde(rename_all = "snake_case")]
346pub enum SubAgentStatus {
347 Initializing,
349
350 Working,
352
353 Waiting,
355
356 Completed,
358
359 Failed,
361
362 Terminated,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct ToolCallRecord {
369 pub name: String,
370 pub timestamp: DateTime<Utc>,
371 pub success: bool,
372}
373
374pub fn is_transient_error(error: &str) -> bool {
379 let lower = error.to_ascii_lowercase();
380 if lower.contains("rate limit")
382 || lower.contains("ratelimit")
383 || lower.contains("too many requests")
384 || lower.contains("429")
385 {
386 return true;
387 }
388 if lower.contains("timeout")
390 || lower.contains("timed out")
391 || lower.contains("connection reset")
392 || lower.contains("connection refused")
393 || lower.contains("broken pipe")
394 || lower.contains("network")
395 || lower.contains("socket")
396 || lower.contains("io error")
397 {
398 return true;
399 }
400 if lower.contains("503")
402 || lower.contains("502")
403 || lower.contains("504")
404 || lower.contains("service unavailable")
405 || lower.contains("bad gateway")
406 || lower.contains("gateway timeout")
407 || lower.contains("overloaded")
408 || lower.contains("temporarily unavailable")
409 {
410 return true;
411 }
412 false
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct SubTaskResult {
418 pub subtask_id: String,
420
421 pub subagent_id: String,
423
424 pub success: bool,
426
427 pub result: String,
429
430 pub steps: usize,
432
433 pub tool_calls: usize,
435
436 pub execution_time_ms: u64,
438
439 pub error: Option<String>,
441
442 pub artifacts: Vec<String>,
444
445 #[serde(default)]
447 pub retry_count: u32,
448}