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    /// Model-suggested worktree preference. This is advisory, not
54    /// authoritative: mutating instructions still require isolation even
55    /// when model metadata claims the task is read-only.
56    #[serde(default)]
57    pub needs_worktree: Option<bool>,
58}
59
60impl SubTask {
61    /// Create a new subtask
62    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    /// Add a specialty
82    pub fn with_specialty(mut self, specialty: impl Into<String>) -> Self {
83        self.specialty = Some(specialty.into());
84        self
85    }
86
87    /// Add dependencies
88    pub fn with_dependencies(mut self, deps: Vec<String>) -> Self {
89        self.dependencies = deps;
90        self
91    }
92
93    /// Set priority
94    pub fn with_priority(mut self, priority: i32) -> Self {
95        self.priority = priority;
96        self
97    }
98
99    /// Set max steps
100    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
101        self.max_steps = max_steps;
102        self
103    }
104
105    /// Add context
106    pub fn with_context(mut self, context: SubTaskContext) -> Self {
107        self.context = context;
108        self
109    }
110
111    /// Record the model-suggested worktree preference. `false` is only
112    /// advisory; mutating instructions still require isolation.
113    pub fn with_needs_worktree(mut self, needs: bool) -> Self {
114        self.needs_worktree = Some(needs);
115        self
116    }
117
118    /// Decide whether this subtask should run in an isolated worktree.
119    /// Mutating intent always wins over model-provided read-only metadata.
120    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    /// Whether this task should receive only read-only tools.
128    pub fn is_read_only(&self) -> bool {
129        !self.needs_worktree()
130    }
131
132    /// Check if this subtask can run (all dependencies complete)
133    pub fn can_run(&self, completed: &[String]) -> bool {
134        self.dependencies.iter().all(|dep| completed.contains(dep))
135    }
136
137    /// Mark as running
138    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    /// Mark as completed
144    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/// Context passed to a subtask
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183pub struct SubTaskContext {
184    /// Parent task information
185    pub parent_task: Option<String>,
186
187    /// Results from dependency subtasks
188    pub dependency_results: HashMap<String, String>,
189
190    /// Shared files or resources
191    pub shared_resources: Vec<String>,
192
193    /// Additional metadata
194    pub metadata: HashMap<String, serde_json::Value>,
195}
196
197/// Status of a subtask
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
199#[serde(rename_all = "snake_case")]
200pub enum SubTaskStatus {
201    /// Waiting to be scheduled
202    Pending,
203
204    /// Waiting for dependencies
205    Blocked,
206
207    /// Currently executing
208    Running,
209
210    /// Successfully completed
211    Completed,
212
213    /// Failed with error
214    Failed,
215
216    /// Cancelled by orchestrator
217    Cancelled,
218
219    /// Timed out
220    TimedOut,
221}
222
223/// A sub-agent executing a subtask
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct SubAgent {
226    /// Unique identifier
227    pub id: String,
228
229    /// Display name
230    pub name: String,
231
232    /// Specialty/role (e.g., "AI Researcher", "Code Writer", "Fact Checker")
233    pub specialty: String,
234
235    /// The subtask this agent is working on
236    pub subtask_id: String,
237
238    /// Current status
239    pub status: SubAgentStatus,
240
241    /// Number of steps taken
242    pub steps: usize,
243
244    /// Tool calls made
245    pub tool_calls: Vec<ToolCallRecord>,
246
247    /// Accumulated output
248    pub output: String,
249
250    /// Model being used
251    pub model: String,
252
253    /// Provider
254    pub provider: String,
255
256    /// Creation timestamp
257    pub created_at: DateTime<Utc>,
258
259    /// Last activity timestamp
260    pub last_active: DateTime<Utc>,
261}
262
263impl SubAgent {
264    /// Create a new sub-agent for a subtask
265    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    /// Record a tool call
290    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    /// Append to output
301    pub fn append_output(&mut self, text: &str) {
302        self.output.push_str(text);
303        self.last_active = Utc::now();
304    }
305
306    /// Set status
307    pub fn set_status(&mut self, status: SubAgentStatus) {
308        self.status = status;
309        self.last_active = Utc::now();
310    }
311}
312
313/// Status of a sub-agent
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "snake_case")]
316pub enum SubAgentStatus {
317    /// Being initialized
318    Initializing,
319
320    /// Actively working
321    Working,
322
323    /// Waiting for resource/dependency
324    Waiting,
325
326    /// Successfully completed
327    Completed,
328
329    /// Failed
330    Failed,
331
332    /// Terminated by orchestrator
333    Terminated,
334}
335
336/// Record of a tool call
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct ToolCallRecord {
339    pub name: String,
340    pub timestamp: DateTime<Utc>,
341    pub success: bool,
342}
343
344/// Determine whether an error message represents a transient failure that
345/// is safe to retry (network issues, rate limiting, timeouts).
346///
347/// Returns `true` if the error is likely transient and a retry may succeed.
348pub fn is_transient_error(error: &str) -> bool {
349    let lower = error.to_ascii_lowercase();
350    // Rate limiting
351    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    // Timeout / connection issues
359    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    // Transient server errors
371    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/// Result of a subtask execution
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct SubTaskResult {
388    /// Subtask ID
389    pub subtask_id: String,
390
391    /// Sub-agent ID that executed it
392    pub subagent_id: String,
393
394    /// Whether it succeeded
395    pub success: bool,
396
397    /// Result text
398    pub result: String,
399
400    /// Number of steps taken
401    pub steps: usize,
402
403    /// Tool calls made
404    pub tool_calls: usize,
405
406    /// Execution time (milliseconds)
407    pub execution_time_ms: u64,
408
409    /// Any error message
410    pub error: Option<String>,
411
412    /// Artifacts produced
413    pub artifacts: Vec<String>,
414
415    /// Number of retry attempts before this result was produced
416    #[serde(default)]
417    pub retry_count: u32,
418}