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
54impl SubTask {
55    /// Create a new subtask
56    pub fn new(name: impl Into<String>, instruction: impl Into<String>) -> Self {
57        Self {
58            id: Uuid::new_v4().to_string(),
59            name: name.into(),
60            instruction: instruction.into(),
61            specialty: None,
62            dependencies: Vec::new(),
63            priority: 0,
64            max_steps: 100,
65            context: SubTaskContext::default(),
66            status: SubTaskStatus::Pending,
67            assigned_agent: None,
68            created_at: Utc::now(),
69            completed_at: None,
70            stage: 0,
71        }
72    }
73
74    /// Add a specialty
75    pub fn with_specialty(mut self, specialty: impl Into<String>) -> Self {
76        self.specialty = Some(specialty.into());
77        self
78    }
79
80    /// Add dependencies
81    pub fn with_dependencies(mut self, deps: Vec<String>) -> Self {
82        self.dependencies = deps;
83        self
84    }
85
86    /// Set priority
87    pub fn with_priority(mut self, priority: i32) -> Self {
88        self.priority = priority;
89        self
90    }
91
92    /// Set max steps
93    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
94        self.max_steps = max_steps;
95        self
96    }
97
98    /// Add context
99    pub fn with_context(mut self, context: SubTaskContext) -> Self {
100        self.context = context;
101        self
102    }
103
104    /// Check if this subtask can run (all dependencies complete)
105    pub fn can_run(&self, completed: &[String]) -> bool {
106        self.dependencies.iter().all(|dep| completed.contains(dep))
107    }
108
109    /// Mark as running
110    pub fn start(&mut self, agent_id: &str) {
111        self.status = SubTaskStatus::Running;
112        self.assigned_agent = Some(agent_id.to_string());
113    }
114
115    /// Mark as completed
116    pub fn complete(&mut self, success: bool) {
117        self.status = if success {
118            SubTaskStatus::Completed
119        } else {
120            SubTaskStatus::Failed
121        };
122        self.completed_at = Some(Utc::now());
123    }
124}
125
126/// Context passed to a subtask
127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128pub struct SubTaskContext {
129    /// Parent task information
130    pub parent_task: Option<String>,
131
132    /// Results from dependency subtasks
133    pub dependency_results: HashMap<String, String>,
134
135    /// Shared files or resources
136    pub shared_resources: Vec<String>,
137
138    /// Additional metadata
139    pub metadata: HashMap<String, serde_json::Value>,
140}
141
142/// Status of a subtask
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "snake_case")]
145pub enum SubTaskStatus {
146    /// Waiting to be scheduled
147    Pending,
148
149    /// Waiting for dependencies
150    Blocked,
151
152    /// Currently executing
153    Running,
154
155    /// Successfully completed
156    Completed,
157
158    /// Failed with error
159    Failed,
160
161    /// Cancelled by orchestrator
162    Cancelled,
163
164    /// Timed out
165    TimedOut,
166}
167
168/// A sub-agent executing a subtask
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct SubAgent {
171    /// Unique identifier
172    pub id: String,
173
174    /// Display name
175    pub name: String,
176
177    /// Specialty/role (e.g., "AI Researcher", "Code Writer", "Fact Checker")
178    pub specialty: String,
179
180    /// The subtask this agent is working on
181    pub subtask_id: String,
182
183    /// Current status
184    pub status: SubAgentStatus,
185
186    /// Number of steps taken
187    pub steps: usize,
188
189    /// Tool calls made
190    pub tool_calls: Vec<ToolCallRecord>,
191
192    /// Accumulated output
193    pub output: String,
194
195    /// Model being used
196    pub model: String,
197
198    /// Provider
199    pub provider: String,
200
201    /// Creation timestamp
202    pub created_at: DateTime<Utc>,
203
204    /// Last activity timestamp
205    pub last_active: DateTime<Utc>,
206}
207
208impl SubAgent {
209    /// Create a new sub-agent for a subtask
210    pub fn new(
211        name: impl Into<String>,
212        specialty: impl Into<String>,
213        subtask_id: impl Into<String>,
214        model: impl Into<String>,
215        provider: impl Into<String>,
216    ) -> Self {
217        let now = Utc::now();
218        Self {
219            id: Uuid::new_v4().to_string(),
220            name: name.into(),
221            specialty: specialty.into(),
222            subtask_id: subtask_id.into(),
223            status: SubAgentStatus::Initializing,
224            steps: 0,
225            tool_calls: Vec::new(),
226            output: String::new(),
227            model: model.into(),
228            provider: provider.into(),
229            created_at: now,
230            last_active: now,
231        }
232    }
233
234    /// Record a tool call
235    pub fn record_tool_call(&mut self, name: &str, success: bool) {
236        self.tool_calls.push(ToolCallRecord {
237            name: name.to_string(),
238            timestamp: Utc::now(),
239            success,
240        });
241        self.steps += 1;
242        self.last_active = Utc::now();
243    }
244
245    /// Append to output
246    pub fn append_output(&mut self, text: &str) {
247        self.output.push_str(text);
248        self.last_active = Utc::now();
249    }
250
251    /// Set status
252    pub fn set_status(&mut self, status: SubAgentStatus) {
253        self.status = status;
254        self.last_active = Utc::now();
255    }
256}
257
258/// Status of a sub-agent
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(rename_all = "snake_case")]
261pub enum SubAgentStatus {
262    /// Being initialized
263    Initializing,
264
265    /// Actively working
266    Working,
267
268    /// Waiting for resource/dependency
269    Waiting,
270
271    /// Successfully completed
272    Completed,
273
274    /// Failed
275    Failed,
276
277    /// Terminated by orchestrator
278    Terminated,
279}
280
281/// Record of a tool call
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct ToolCallRecord {
284    pub name: String,
285    pub timestamp: DateTime<Utc>,
286    pub success: bool,
287}
288
289/// Determine whether an error message represents a transient failure that
290/// is safe to retry (network issues, rate limiting, timeouts).
291///
292/// Returns `true` if the error is likely transient and a retry may succeed.
293pub fn is_transient_error(error: &str) -> bool {
294    let lower = error.to_ascii_lowercase();
295    // Rate limiting
296    if lower.contains("rate limit")
297        || lower.contains("ratelimit")
298        || lower.contains("too many requests")
299        || lower.contains("429")
300    {
301        return true;
302    }
303    // Timeout / connection issues
304    if lower.contains("timeout")
305        || lower.contains("timed out")
306        || lower.contains("connection reset")
307        || lower.contains("connection refused")
308        || lower.contains("broken pipe")
309        || lower.contains("network")
310        || lower.contains("socket")
311        || lower.contains("io error")
312    {
313        return true;
314    }
315    // Transient server errors
316    if lower.contains("503")
317        || lower.contains("502")
318        || lower.contains("504")
319        || lower.contains("service unavailable")
320        || lower.contains("bad gateway")
321        || lower.contains("gateway timeout")
322        || lower.contains("overloaded")
323        || lower.contains("temporarily unavailable")
324    {
325        return true;
326    }
327    false
328}
329
330/// Result of a subtask execution
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct SubTaskResult {
333    /// Subtask ID
334    pub subtask_id: String,
335
336    /// Sub-agent ID that executed it
337    pub subagent_id: String,
338
339    /// Whether it succeeded
340    pub success: bool,
341
342    /// Result text
343    pub result: String,
344
345    /// Number of steps taken
346    pub steps: usize,
347
348    /// Tool calls made
349    pub tool_calls: usize,
350
351    /// Execution time (milliseconds)
352    pub execution_time_ms: u64,
353
354    /// Any error message
355    pub error: Option<String>,
356
357    /// Artifacts produced
358    pub artifacts: Vec<String>,
359
360    /// Number of retry attempts before this result was produced
361    #[serde(default)]
362    pub retry_count: u32,
363}