Skip to main content

codetether_agent/swarm/
subtask.rs

1//! SubTask and SubAgent definitions
2//!
3//! A SubTask is a unit of work that can be executed in parallel.
4//! A SubAgent is a dynamically instantiated agent for executing a subtask.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11/// A sub-task that can be executed by a sub-agent
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SubTask {
14    /// Unique identifier
15    pub id: String,
16
17    /// Human-readable name/description
18    pub name: String,
19
20    /// The task instruction for the sub-agent
21    pub instruction: String,
22
23    /// Specialty/domain for this subtask
24    pub specialty: Option<String>,
25
26    /// Dependencies on other subtasks (by ID)
27    pub dependencies: Vec<String>,
28
29    /// Priority (higher = more important)
30    pub priority: i32,
31
32    /// Maximum steps allowed for this subtask
33    pub max_steps: usize,
34
35    /// Input context from parent or dependencies
36    pub context: SubTaskContext,
37
38    /// Current status
39    pub status: SubTaskStatus,
40
41    /// Assigned sub-agent ID
42    pub assigned_agent: Option<String>,
43
44    /// Creation timestamp
45    pub created_at: DateTime<Utc>,
46
47    /// Completion timestamp
48    pub completed_at: Option<DateTime<Utc>>,
49
50    /// Stage in the execution plan (0 = can run immediately)
51    pub stage: usize,
52
53    /// Explicit override for whether this subtask needs an isolated
54    /// git worktree. `None` (the default) runs [`SubTask::needs_worktree`]
55    /// heuristics on specialty + instruction; `Some(true)` forces a
56    /// worktree; `Some(false)` forces the shared working directory.
57    ///
58    /// Skipping the worktree for read-only tasks (research, review,
59    /// planning, fact-check) saves ~1s of setup, an inode, and
60    /// `.git/worktrees` lock contention when running many agents
61    /// in parallel. Tasks that edit files should keep the default or
62    /// explicitly request a worktree so their edits don't collide with
63    /// sibling agents in the same swarm.
64    #[serde(default)]
65    pub needs_worktree: Option<bool>,
66}
67
68impl SubTask {
69    /// Create a new subtask
70    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    /// Add a specialty
90    pub fn with_specialty(mut self, specialty: impl Into<String>) -> Self {
91        self.specialty = Some(specialty.into());
92        self
93    }
94
95    /// Add dependencies
96    pub fn with_dependencies(mut self, deps: Vec<String>) -> Self {
97        self.dependencies = deps;
98        self
99    }
100
101    /// Set priority
102    pub fn with_priority(mut self, priority: i32) -> Self {
103        self.priority = priority;
104        self
105    }
106
107    /// Set max steps
108    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
109        self.max_steps = max_steps;
110        self
111    }
112
113    /// Add context
114    pub fn with_context(mut self, context: SubTaskContext) -> Self {
115        self.context = context;
116        self
117    }
118
119    /// Explicitly set whether this subtask needs a worktree.
120    ///
121    /// `true` forces isolation; `false` forces the shared directory;
122    /// omit this call to fall back to [`SubTask::needs_worktree`]
123    /// heuristics. See the field docs on [`SubTask::needs_worktree`]
124    /// for why this matters in large swarms.
125    pub fn with_needs_worktree(mut self, needs: bool) -> Self {
126        self.needs_worktree = Some(needs);
127        self
128    }
129
130    /// Decide whether this subtask should run in an isolated worktree.
131    ///
132    /// Honours the explicit override when set; otherwise applies a
133    /// conservative heuristic:
134    ///
135    /// - Specialty keywords like `research`, `review`, `analy`,
136    ///   `audit`, `plan`, `fact`, `summari`, `search`, `explore`,
137    ///   `docs` imply a read-only task → no worktree.
138    /// - Instruction keywords like `write`, `edit`, `create`, `fix`,
139    ///   `implement`, `refactor`, `apply`, `commit` imply file
140    ///   mutation → worktree.
141    /// - Unknown tasks default to `true` (worktree) to stay safe
142    ///   against accidental cross-agent edit collisions.
143    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    /// Check if this subtask can run (all dependencies complete)
190    pub fn can_run(&self, completed: &[String]) -> bool {
191        self.dependencies.iter().all(|dep| completed.contains(dep))
192    }
193
194    /// Mark as running
195    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    /// Mark as completed
201    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/// Context passed to a subtask
212#[derive(Debug, Clone, Default, Serialize, Deserialize)]
213pub struct SubTaskContext {
214    /// Parent task information
215    pub parent_task: Option<String>,
216
217    /// Results from dependency subtasks
218    pub dependency_results: HashMap<String, String>,
219
220    /// Shared files or resources
221    pub shared_resources: Vec<String>,
222
223    /// Additional metadata
224    pub metadata: HashMap<String, serde_json::Value>,
225}
226
227/// Status of a subtask
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum SubTaskStatus {
231    /// Waiting to be scheduled
232    Pending,
233
234    /// Waiting for dependencies
235    Blocked,
236
237    /// Currently executing
238    Running,
239
240    /// Successfully completed
241    Completed,
242
243    /// Failed with error
244    Failed,
245
246    /// Cancelled by orchestrator
247    Cancelled,
248
249    /// Timed out
250    TimedOut,
251}
252
253/// A sub-agent executing a subtask
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct SubAgent {
256    /// Unique identifier
257    pub id: String,
258
259    /// Display name
260    pub name: String,
261
262    /// Specialty/role (e.g., "AI Researcher", "Code Writer", "Fact Checker")
263    pub specialty: String,
264
265    /// The subtask this agent is working on
266    pub subtask_id: String,
267
268    /// Current status
269    pub status: SubAgentStatus,
270
271    /// Number of steps taken
272    pub steps: usize,
273
274    /// Tool calls made
275    pub tool_calls: Vec<ToolCallRecord>,
276
277    /// Accumulated output
278    pub output: String,
279
280    /// Model being used
281    pub model: String,
282
283    /// Provider
284    pub provider: String,
285
286    /// Creation timestamp
287    pub created_at: DateTime<Utc>,
288
289    /// Last activity timestamp
290    pub last_active: DateTime<Utc>,
291}
292
293impl SubAgent {
294    /// Create a new sub-agent for a subtask
295    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    /// Record a tool call
320    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    /// Append to output
331    pub fn append_output(&mut self, text: &str) {
332        self.output.push_str(text);
333        self.last_active = Utc::now();
334    }
335
336    /// Set status
337    pub fn set_status(&mut self, status: SubAgentStatus) {
338        self.status = status;
339        self.last_active = Utc::now();
340    }
341}
342
343/// Status of a sub-agent
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
345#[serde(rename_all = "snake_case")]
346pub enum SubAgentStatus {
347    /// Being initialized
348    Initializing,
349
350    /// Actively working
351    Working,
352
353    /// Waiting for resource/dependency
354    Waiting,
355
356    /// Successfully completed
357    Completed,
358
359    /// Failed
360    Failed,
361
362    /// Terminated by orchestrator
363    Terminated,
364}
365
366/// Record of a tool call
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct ToolCallRecord {
369    pub name: String,
370    pub timestamp: DateTime<Utc>,
371    pub success: bool,
372}
373
374/// Determine whether an error message represents a transient failure that
375/// is safe to retry (network issues, rate limiting, timeouts).
376///
377/// Returns `true` if the error is likely transient and a retry may succeed.
378pub fn is_transient_error(error: &str) -> bool {
379    let lower = error.to_ascii_lowercase();
380    // Rate limiting
381    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    // Timeout / connection issues
389    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    // Transient server errors
401    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/// Result of a subtask execution
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct SubTaskResult {
418    /// Subtask ID
419    pub subtask_id: String,
420
421    /// Sub-agent ID that executed it
422    pub subagent_id: String,
423
424    /// Whether it succeeded
425    pub success: bool,
426
427    /// Result text
428    pub result: String,
429
430    /// Number of steps taken
431    pub steps: usize,
432
433    /// Tool calls made
434    pub tool_calls: usize,
435
436    /// Execution time (milliseconds)
437    pub execution_time_ms: u64,
438
439    /// Any error message
440    pub error: Option<String>,
441
442    /// Artifacts produced
443    pub artifacts: Vec<String>,
444
445    /// Number of retry attempts before this result was produced
446    #[serde(default)]
447    pub retry_count: u32,
448}