1use std::cmp::Ordering;
2use std::collections::{BinaryHeap, HashSet};
3
4use crate::scheduler::policy::SchedulerPolicyConfig;
5use crate::types::error::{DeepStrikeError, Result};
6use crate::types::result::LoopResult;
7use crate::types::task::RuntimeTask;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum TaskStatus {
11 Pending,
12 Ready,
13 Running,
14 Completed,
15 CompletedPartial,
16 Failed,
17 SkippedUpstreamFailed,
18}
19
20impl TaskStatus {
21 pub fn is_terminal(self) -> bool {
22 matches!(
23 self,
24 Self::Completed | Self::CompletedPartial | Self::Failed | Self::SkippedUpstreamFailed
25 )
26 }
27}
28
29#[derive(Debug, Clone)]
30pub struct TaskNode {
31 pub id: usize,
32 pub task: RuntimeTask,
33 pub status: TaskStatus,
34 pub result: Option<LoopResult>,
35 pub dependencies: Vec<usize>,
36}
37
38pub struct TaskGraph {
42 nodes: Vec<TaskNode>,
43 in_degree: Vec<usize>,
46 reverse_adjacency: Vec<Vec<usize>>,
48 ready_heap: BinaryHeap<ReadyEntry>,
49 ready_generation: Vec<u64>,
50 enqueued_round: Vec<u64>,
51 enqueue_sequence: u64,
52 ready_round: u64,
53 scheduling: Vec<SchedulingMetadata>,
54 scheduler_policy: SchedulerPolicyConfig,
55}
56
57#[derive(Debug, Clone, Copy, Default)]
58struct SchedulingMetadata {
59 critical_path_remaining: u64,
60 downstream_fanout: u64,
61 token_cost: u64,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65struct ReadyEntry {
66 priority: i128,
67 enqueue_sequence: u64,
68 node_id: usize,
69 generation: u64,
70}
71
72impl Ord for ReadyEntry {
73 fn cmp(&self, other: &Self) -> Ordering {
74 self.priority
75 .cmp(&other.priority)
76 .then_with(|| other.enqueue_sequence.cmp(&self.enqueue_sequence))
77 .then_with(|| other.node_id.cmp(&self.node_id))
78 }
79}
80
81impl PartialOrd for ReadyEntry {
82 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
83 Some(self.cmp(other))
84 }
85}
86
87impl TaskGraph {
88 pub fn new() -> Self {
89 Self {
90 nodes: Vec::new(),
91 in_degree: Vec::new(),
92 reverse_adjacency: Vec::new(),
93 ready_heap: BinaryHeap::new(),
94 ready_generation: Vec::new(),
95 enqueued_round: Vec::new(),
96 enqueue_sequence: 0,
97 ready_round: 0,
98 scheduling: Vec::new(),
99 scheduler_policy: SchedulerPolicyConfig::default(),
100 }
101 }
102
103 pub fn add(&mut self, task: RuntimeTask, mut dependencies: Vec<usize>) -> usize {
107 let mut seen = std::collections::HashSet::new();
108 dependencies.retain(|d| seen.insert(*d));
109 let id = self.nodes.len();
110 let deg = dependencies.len();
111 let max_index = dependencies.iter().copied().max().unwrap_or(id).max(id);
112 self.reverse_adjacency.resize_with(max_index + 1, Vec::new);
113 for &dependency in &dependencies {
114 self.reverse_adjacency[dependency].push(id);
115 }
116 self.nodes.push(TaskNode {
117 id,
118 task,
119 status: if deg == 0 {
120 TaskStatus::Ready
121 } else {
122 TaskStatus::Pending
123 },
124 result: None,
125 dependencies,
126 });
127 self.in_degree.push(deg);
128 self.ready_generation.push(0);
129 self.enqueued_round.push(self.ready_round);
130 self.scheduling.push(SchedulingMetadata::default());
131 if deg == 0 {
132 self.enqueue_ready(id);
133 }
134 id
135 }
136
137 pub fn topological_sort(&self) -> Result<Vec<usize>> {
139 let n = self.nodes.len();
140 let mut in_deg: Vec<usize> = self
144 .nodes
145 .iter()
146 .map(|node| node.dependencies.len())
147 .collect();
148
149 let mut queue: Vec<usize> = (0..n).filter(|&i| in_deg[i] == 0).collect();
150 let mut order = Vec::with_capacity(n);
151
152 while let Some(id) = queue.pop() {
153 order.push(id);
154 for &next in self.reverse_adjacency.get(id).into_iter().flatten() {
155 in_deg[next] -= 1;
156 if in_deg[next] == 0 {
157 queue.push(next);
158 }
159 }
160 }
161
162 if order.len() != n {
163 return Err(DeepStrikeError::OrchestrationCycle);
164 }
165 Ok(order)
166 }
167
168 pub fn ready_tasks(&mut self) -> Vec<usize> {
170 let mut valid_entries = Vec::new();
174 let mut ready = Vec::new();
175 while let Some(entry) = self.ready_heap.pop() {
176 if self.nodes.get(entry.node_id).map(|node| node.status) == Some(TaskStatus::Ready)
177 && self.ready_generation[entry.node_id] == entry.generation
178 {
179 ready.push(entry.node_id);
180 valid_entries.push(entry);
181 }
182 }
183 self.ready_heap.extend(valid_entries);
184 self.ready_round = self.ready_round.saturating_add(1);
185 ready
186 }
187
188 pub fn start(&mut self, task_id: usize) {
190 if let Some(node) = self.nodes.get_mut(task_id) {
191 node.status = TaskStatus::Running;
192 }
193 }
194
195 pub fn set_ready(&mut self, task_id: usize) {
199 if let Some(node) = self.nodes.get_mut(task_id) {
200 if node.status != TaskStatus::Ready {
201 node.status = TaskStatus::Ready;
202 self.enqueue_ready(task_id);
203 }
204 }
205 }
206
207 pub fn complete(&mut self, task_id: usize, result: LoopResult) {
213 {
214 let Some(node) = self.nodes.get_mut(task_id) else {
215 return;
216 };
217 if node.status.is_terminal() {
218 return;
219 }
220 node.status = TaskStatus::Completed;
221 node.result = Some(result);
222 }
223 let dependents = self
224 .reverse_adjacency
225 .get(task_id)
226 .cloned()
227 .unwrap_or_default();
228 for dep_id in dependents {
229 self.in_degree[dep_id] -= 1;
230 if self.in_degree[dep_id] == 0 {
231 let should_enqueue =
232 self.nodes.get(dep_id).map(|n| n.status) == Some(TaskStatus::Pending);
233 if should_enqueue {
234 self.nodes[dep_id].status = TaskStatus::Ready;
235 self.enqueue_ready(dep_id);
236 }
237 }
238 }
239 }
240
241 pub fn complete_partial(&mut self, task_id: usize, result: LoopResult) {
242 if let Some(node) = self.nodes.get_mut(task_id) {
243 if !node.status.is_terminal() {
244 node.status = TaskStatus::CompletedPartial;
245 node.result = Some(result);
246 }
247 }
248 }
249
250 pub fn fail(&mut self, task_id: usize) {
254 if let Some(node) = self.nodes.get_mut(task_id) {
255 if !node.status.is_terminal() {
256 node.status = TaskStatus::Failed;
257 }
258 }
259 }
260
261 pub fn fail_with_result(&mut self, task_id: usize, result: LoopResult) {
262 if let Some(node) = self.nodes.get_mut(task_id) {
263 if !node.status.is_terminal() {
264 node.status = TaskStatus::Failed;
265 node.result = Some(result);
266 }
267 }
268 }
269
270 pub fn skip_upstream_failed(&mut self, task_id: usize) {
271 if let Some(node) = self.nodes.get_mut(task_id) {
272 if !node.status.is_terminal() {
273 node.status = TaskStatus::SkippedUpstreamFailed;
274 }
275 }
276 }
277
278 pub fn get(&self, task_id: usize) -> Option<&TaskNode> {
279 self.nodes.get(task_id)
280 }
281
282 pub fn len(&self) -> usize {
283 self.nodes.len()
284 }
285
286 pub fn is_empty(&self) -> bool {
287 self.nodes.is_empty()
288 }
289
290 pub fn all_done(&self) -> bool {
291 self.nodes.iter().all(|n| n.status.is_terminal())
292 }
293
294 pub fn configure_scheduling(&mut self, policy: SchedulerPolicyConfig, token_costs: &[u64]) {
295 self.scheduler_policy = policy;
296 let order = self
297 .topological_sort()
298 .unwrap_or_else(|_| (0..self.nodes.len()).collect());
299 let mut reachable: Vec<HashSet<usize>> = vec![HashSet::new(); self.nodes.len()];
300 for &node in order.iter().rev() {
301 let mut critical = 1u64;
302 let children = self
303 .reverse_adjacency
304 .get(node)
305 .cloned()
306 .unwrap_or_default();
307 for child in children {
308 critical = critical.max(1 + self.scheduling[child].critical_path_remaining);
309 reachable[node].insert(child);
310 let descendants: Vec<usize> = reachable[child].iter().copied().collect();
311 reachable[node].extend(descendants);
312 }
313 self.scheduling[node] = SchedulingMetadata {
314 critical_path_remaining: critical,
315 downstream_fanout: reachable[node].len() as u64,
316 token_cost: token_costs.get(node).copied().unwrap_or(0),
317 };
318 }
319 self.rebuild_ready_heap();
320 }
321
322 fn rebuild_ready_heap(&mut self) {
323 self.ready_heap.clear();
324 for node_id in 0..self.nodes.len() {
325 if self.nodes[node_id].status == TaskStatus::Ready {
326 self.push_ready_entry(node_id);
327 }
328 }
329 }
330
331 fn enqueue_ready(&mut self, task_id: usize) {
332 self.ready_generation[task_id] = self.ready_generation[task_id].saturating_add(1);
333 self.enqueued_round[task_id] = self.ready_round;
334 self.enqueue_sequence = self.enqueue_sequence.saturating_add(1);
335 self.push_ready_entry(task_id);
336 }
337
338 fn push_ready_entry(&mut self, task_id: usize) {
339 let metadata = self.scheduling[task_id];
340 let policy = self.scheduler_policy;
341 let priority = i128::from(policy.critical_path_weight)
342 * i128::from(metadata.critical_path_remaining)
343 + i128::from(policy.fanout_weight) * i128::from(metadata.downstream_fanout)
344 - i128::from(policy.age_weight) * i128::from(self.enqueued_round[task_id])
345 - i128::from(policy.token_cost_weight) * i128::from(metadata.token_cost);
346 self.ready_heap.push(ReadyEntry {
347 priority,
348 enqueue_sequence: self.enqueue_sequence,
349 node_id: task_id,
350 generation: self.ready_generation[task_id],
351 });
352 }
353}
354
355impl Default for TaskGraph {
356 fn default() -> Self {
357 Self::new()
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn topological_sort_linear() {
367 let mut g = TaskGraph::new();
368 let a = g.add(RuntimeTask::new("A"), vec![]);
369 let b = g.add(RuntimeTask::new("B"), vec![a]);
370 let c = g.add(RuntimeTask::new("C"), vec![b]);
371
372 let order = g.topological_sort().unwrap();
373 assert_eq!(order, vec![0, 1, 2]);
374 let _ = (a, c);
375 }
376
377 #[test]
378 fn detects_cycle() {
379 let mut g = TaskGraph::new();
380 g.nodes.push(TaskNode {
381 id: 0,
382 task: RuntimeTask::new("A"),
383 status: TaskStatus::Pending,
384 result: None,
385 dependencies: vec![1],
386 });
387 g.nodes.push(TaskNode {
388 id: 1,
389 task: RuntimeTask::new("B"),
390 status: TaskStatus::Pending,
391 result: None,
392 dependencies: vec![0],
393 });
394 g.in_degree.push(1);
395 g.in_degree.push(1);
396
397 assert!(g.topological_sort().is_err());
398 }
399
400 #[test]
401 fn ready_tasks_respects_deps() {
402 let mut g = TaskGraph::new();
403 let a = g.add(RuntimeTask::new("A"), vec![]);
404 let _b = g.add(RuntimeTask::new("B"), vec![a]);
405
406 assert_eq!(g.ready_tasks(), vec![0]); }
408
409 #[test]
410 fn set_ready_rearms_without_promoting_dependents() {
411 let mut g = TaskGraph::new();
412 let a = g.add(RuntimeTask::new("A"), vec![]); let b = g.add(RuntimeTask::new("B"), vec![a]); g.start(a);
415 g.set_ready(a);
417 assert_eq!(g.nodes[a].status, TaskStatus::Ready);
418 assert_eq!(g.nodes[b].status, TaskStatus::Pending);
419 assert_eq!(g.ready_tasks(), vec![a]);
420 }
421
422 #[test]
423 fn complete_promotes_dependent() {
424 use crate::types::result::{LoopResult, TerminationReason};
425 let mut g = TaskGraph::new();
426 let a = g.add(RuntimeTask::new("A"), vec![]);
427 let b = g.add(RuntimeTask::new("B"), vec![a]);
428
429 assert_eq!(g.nodes[b].status, TaskStatus::Pending);
430 g.complete(
431 a,
432 LoopResult {
433 termination: TerminationReason::Completed,
434 final_message: None,
435 turns_used: 1,
436 total_tokens_used: 0,
437 loop_continue: None,
438 classify_branch: None,
439 tournament_winner: None,
440 pace_decision: None,
441 },
442 );
443 assert_eq!(g.nodes[b].status, TaskStatus::Ready);
444 }
445
446 #[test]
447 fn duplicate_complete_is_idempotent() {
448 use crate::types::result::{LoopResult, TerminationReason};
449 let result = || LoopResult {
450 termination: TerminationReason::Completed,
451 final_message: None,
452 turns_used: 1,
453 total_tokens_used: 0,
454 loop_continue: None,
455 classify_branch: None,
456 tournament_winner: None,
457 pace_decision: None,
458 };
459 let mut g = TaskGraph::new();
461 let a = g.add(RuntimeTask::new("A"), vec![]);
462 let c = g.add(RuntimeTask::new("C"), vec![]);
463 let b = g.add(RuntimeTask::new("B"), vec![a, c]);
464
465 g.complete(a, result());
466 g.complete(a, result()); assert_eq!(g.nodes[b].status, TaskStatus::Pending);
468 g.complete(c, result());
469 assert_eq!(g.nodes[b].status, TaskStatus::Ready);
470 g.fail(a);
472 assert_eq!(g.nodes[a].status, TaskStatus::Completed);
473 }
474
475 #[test]
476 fn critical_path_priority_beats_lower_node_id() {
477 let mut g = TaskGraph::new();
478 let wide = g.add(RuntimeTask::new("wide"), vec![]);
479 let chain = g.add(RuntimeTask::new("chain"), vec![]);
480 g.add(RuntimeTask::new("wide-child-a"), vec![wide]);
481 g.add(RuntimeTask::new("wide-child-b"), vec![wide]);
482 let chain_2 = g.add(RuntimeTask::new("chain-2"), vec![chain]);
483 let chain_3 = g.add(RuntimeTask::new("chain-3"), vec![chain_2]);
484 g.add(RuntimeTask::new("chain-4"), vec![chain_3]);
485
486 g.configure_scheduling(SchedulerPolicyConfig::default(), &[]);
487
488 assert_eq!(g.ready_tasks(), vec![chain, wide]);
489 }
490
491 #[test]
492 fn zero_weights_use_fifo_and_loop_rearm_yields() {
493 let mut g = TaskGraph::new();
494 let loop_node = g.add(RuntimeTask::new("loop"), vec![]);
495 let peer = g.add(RuntimeTask::new("peer"), vec![]);
496 let policy = SchedulerPolicyConfig {
497 critical_path_weight: 0,
498 fanout_weight: 0,
499 age_weight: 0,
500 token_cost_weight: 0,
501 ..SchedulerPolicyConfig::default()
502 };
503 g.configure_scheduling(policy, &[]);
504 assert_eq!(g.ready_tasks(), vec![loop_node, peer]);
505
506 g.start(loop_node);
507 g.set_ready(loop_node);
508 assert_eq!(g.ready_tasks(), vec![peer, loop_node]);
509 assert_eq!(
510 g.ready_heap.len(),
511 2,
512 "stale loop generations must be collected"
513 );
514 }
515
516 #[test]
517 fn reverse_adjacency_tracks_only_outgoing_dependents() {
518 let mut g = TaskGraph::new();
519 let root = g.add(RuntimeTask::new("root"), vec![]);
520 let unrelated = g.add(RuntimeTask::new("unrelated"), vec![]);
521 let child = g.add(RuntimeTask::new("child"), vec![root]);
522 g.add(RuntimeTask::new("grandchild"), vec![child]);
523
524 assert_eq!(g.reverse_adjacency[root], vec![child]);
525 assert!(g.reverse_adjacency[unrelated].is_empty());
526 }
527}