//! Linearization utilities for task messages. //! //! This module provides pure functions for linearizing task messages in a conversation, //! following a DFS traversal that interleaves subtask messages at subagent tool calls. use std::collections::{HashMap, HashSet}; use warp_multi_agent_api as api; use crate::ai::agent::task::helper::TaskExt as _; /// Computes the set of "active" task IDs in a task tree. /// /// An active task is one that is still in progress. The algorithm: /// 1. Start with a queue containing the root task ID /// 2. For each task in the queue, walk through its messages: /// - When encountering a subagent ToolCall, add the subtask to the queue /// - When encountering a ToolCallResult matching a subagent call, remove from queue /// 3. After processing all messages, add the task to the active set /// 4. Repeat until the queue is empty pub fn compute_active_task_ids<'a>( root_task_id: &str, tasks: &HashMap<&str, &'a api::Task>, ) -> HashSet<&'a str> { let mut active_tasks = HashSet::new(); let mut visited = HashSet::new(); let mut queue = vec![root_task_id]; while let Some(task_id) = queue.pop() { // Cycle protection: skip tasks we've already processed. if !visited.insert(task_id) { log::error!("Cycle detected in active task computation at task {task_id}"); continue; } let Some(task) = tasks.get(task_id) else { // Task not found - skip it. continue; }; // Track subagent tool calls: tool_call_id -> subtask_id. let mut pending_subagents: HashMap<&str, &str> = HashMap::new(); for message in &task.messages { match &message.message { Some(api::message::Message::ToolCall(tool_call)) => { // Check if this is a subagent call. if let Some(api::message::tool_call::Tool::Subagent(subagent)) = &tool_call.tool { if !subagent.task_id.is_empty() { // Add subtask to the queue. queue.push(subagent.task_id.as_str()); // Track this subagent call so we can remove it when we see the result. pending_subagents .insert(tool_call.tool_call_id.as_str(), subagent.task_id.as_str()); } } } Some(api::message::Message::ToolCallResult(result)) => { // If this result matches a pending subagent call, remove from queue. if let Some(subtask_id) = pending_subagents.remove(result.tool_call_id.as_str()) { queue.retain(|id| *id != subtask_id); } } _ => {} } } // After processing all messages, add this task to the active set. active_tasks.insert(task.id.as_str()); } active_tasks } /// Computes the depth (distance from root) for each task in the map. /// /// Tasks with no parent have depth 0. Tasks whose parent chain contains a cycle or leads to a /// missing task are assigned depth 0. pub fn compute_task_depths(tasks: &HashMap) -> HashMap<&str, usize> { let mut depths = HashMap::new(); for (task_id, _) in tasks.iter() { let mut depth = 0; let mut current_id: &str = task_id; let mut visited = HashSet::new(); while let Some(task) = tasks.get(current_id) { if !visited.insert(current_id) { // Cycle detected; treat as depth 0. log::error!("Cycle detected in task parent chain starting from task {task_id}"); depth = 0; break; } if let Some(parent_id) = task.parent_id() { depth += 1; current_id = parent_id; } else { break; } } depths.insert(task_id.as_str(), depth); } depths } #[cfg(test)] #[path = "linearization_tests.rs"] mod tests;