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)]
57 pub needs_worktree: Option<bool>,
58}
59
60impl SubTask {
61 pub fn new(name: impl Into<String>, instruction: impl Into<String>) -> Self {
63 Self {
64 id: Uuid::new_v4().to_string(),
65 name: name.into(),
66 instruction: instruction.into(),
67 specialty: None,
68 dependencies: Vec::new(),
69 priority: 0,
70 max_steps: 100,
71 context: SubTaskContext::default(),
72 status: SubTaskStatus::Pending,
73 assigned_agent: None,
74 created_at: Utc::now(),
75 completed_at: None,
76 stage: 0,
77 needs_worktree: None,
78 }
79 }
80
81 pub fn with_specialty(mut self, specialty: impl Into<String>) -> Self {
83 self.specialty = Some(specialty.into());
84 self
85 }
86
87 pub fn with_dependencies(mut self, deps: Vec<String>) -> Self {
89 self.dependencies = deps;
90 self
91 }
92
93 pub fn with_priority(mut self, priority: i32) -> Self {
95 self.priority = priority;
96 self
97 }
98
99 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
101 self.max_steps = max_steps;
102 self
103 }
104
105 pub fn with_context(mut self, context: SubTaskContext) -> Self {
107 self.context = context;
108 self
109 }
110
111 pub fn with_needs_worktree(mut self, needs: bool) -> Self {
114 self.needs_worktree = Some(needs);
115 self
116 }
117
118 pub fn needs_worktree(&self) -> bool {
121 match classify_task(&self.instruction, self.specialty.as_deref()) {
122 TaskKind::Mutating => true,
123 TaskKind::ReadOnly => self.needs_worktree.unwrap_or(false),
124 }
125 }
126
127 pub fn is_read_only(&self) -> bool {
129 !self.needs_worktree()
130 }
131
132 pub fn can_run(&self, completed: &[String]) -> bool {
134 self.dependencies.iter().all(|dep| completed.contains(dep))
135 }
136
137 pub fn start(&mut self, agent_id: &str) {
139 self.status = SubTaskStatus::Running;
140 self.assigned_agent = Some(agent_id.to_string());
141 }
142
143 pub fn complete(&mut self, success: bool) {
145 self.status = if success {
146 SubTaskStatus::Completed
147 } else {
148 SubTaskStatus::Failed
149 };
150 self.completed_at = Some(Utc::now());
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155enum TaskKind {
156 ReadOnly,
157 Mutating,
158}
159
160fn classify_task(instruction: &str, specialty: Option<&str>) -> TaskKind {
161 let instruction = instruction.to_ascii_lowercase();
162 let mutating = "write edit create fix implement refactor apply commit patch scaffold build delete remove rename move mkdir install deploy";
163 if mutating
164 .split_whitespace()
165 .any(|needle| instruction.contains(needle))
166 {
167 return TaskKind::Mutating;
168 }
169 let specialty = specialty.unwrap_or_default().to_ascii_lowercase();
170 let readonly = "research review analy audit plan fact summari explore docs read-only readonly search investigate";
171 if readonly
172 .split_whitespace()
173 .any(|needle| specialty.contains(needle))
174 {
175 TaskKind::ReadOnly
176 } else {
177 TaskKind::Mutating
178 }
179}
180
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183pub struct SubTaskContext {
184 pub parent_task: Option<String>,
186
187 pub dependency_results: HashMap<String, String>,
189
190 pub shared_resources: Vec<String>,
192
193 pub metadata: HashMap<String, serde_json::Value>,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
199#[serde(rename_all = "snake_case")]
200pub enum SubTaskStatus {
201 Pending,
203
204 Blocked,
206
207 Running,
209
210 Completed,
212
213 Failed,
215
216 Cancelled,
218
219 TimedOut,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct SubAgent {
226 pub id: String,
228
229 pub name: String,
231
232 pub specialty: String,
234
235 pub subtask_id: String,
237
238 pub status: SubAgentStatus,
240
241 pub steps: usize,
243
244 pub tool_calls: Vec<ToolCallRecord>,
246
247 pub output: String,
249
250 pub model: String,
252
253 pub provider: String,
255
256 pub created_at: DateTime<Utc>,
258
259 pub last_active: DateTime<Utc>,
261}
262
263impl SubAgent {
264 pub fn new(
266 name: impl Into<String>,
267 specialty: impl Into<String>,
268 subtask_id: impl Into<String>,
269 model: impl Into<String>,
270 provider: impl Into<String>,
271 ) -> Self {
272 let now = Utc::now();
273 Self {
274 id: Uuid::new_v4().to_string(),
275 name: name.into(),
276 specialty: specialty.into(),
277 subtask_id: subtask_id.into(),
278 status: SubAgentStatus::Initializing,
279 steps: 0,
280 tool_calls: Vec::new(),
281 output: String::new(),
282 model: model.into(),
283 provider: provider.into(),
284 created_at: now,
285 last_active: now,
286 }
287 }
288
289 pub fn record_tool_call(&mut self, name: &str, success: bool) {
291 self.tool_calls.push(ToolCallRecord {
292 name: name.to_string(),
293 timestamp: Utc::now(),
294 success,
295 });
296 self.steps += 1;
297 self.last_active = Utc::now();
298 }
299
300 pub fn append_output(&mut self, text: &str) {
302 self.output.push_str(text);
303 self.last_active = Utc::now();
304 }
305
306 pub fn set_status(&mut self, status: SubAgentStatus) {
308 self.status = status;
309 self.last_active = Utc::now();
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "snake_case")]
316pub enum SubAgentStatus {
317 Initializing,
319
320 Working,
322
323 Waiting,
325
326 Completed,
328
329 Failed,
331
332 Terminated,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct ToolCallRecord {
339 pub name: String,
340 pub timestamp: DateTime<Utc>,
341 pub success: bool,
342}
343
344pub fn is_transient_error(error: &str) -> bool {
349 let lower = error.to_ascii_lowercase();
350 if lower.contains("rate limit")
352 || lower.contains("ratelimit")
353 || lower.contains("too many requests")
354 || lower.contains("429")
355 {
356 return true;
357 }
358 if lower.contains("timeout")
360 || lower.contains("timed out")
361 || lower.contains("connection reset")
362 || lower.contains("connection refused")
363 || lower.contains("broken pipe")
364 || lower.contains("network")
365 || lower.contains("socket")
366 || lower.contains("io error")
367 {
368 return true;
369 }
370 if lower.contains("503")
372 || lower.contains("502")
373 || lower.contains("504")
374 || lower.contains("service unavailable")
375 || lower.contains("bad gateway")
376 || lower.contains("gateway timeout")
377 || lower.contains("overloaded")
378 || lower.contains("temporarily unavailable")
379 {
380 return true;
381 }
382 false
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct SubTaskResult {
388 pub subtask_id: String,
390
391 pub subagent_id: String,
393
394 pub success: bool,
396
397 pub result: String,
399
400 pub steps: usize,
402
403 pub tool_calls: usize,
405
406 pub execution_time_ms: u64,
408
409 pub error: Option<String>,
411
412 pub artifacts: Vec<String>,
414
415 #[serde(default)]
417 pub retry_count: u32,
418}