//! Tool lifecycle management within the FSM. //! //! Each tool call goes through an independent lifecycle. The ToolManager //! tracks all tools in the current turn and provides the "all resolved" //! check that gates turn completion. use crate::diff::{EditPreview, WritePreview}; use crate::tools::ClientToolCall; /// Per-tool lifecycle state. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum ToolState { /// Permission resolver is running asynchronously. CheckingPermission, /// Waiting for user to grant/deny via the permission dialog. AwaitingPermission, /// Actively executing. Executing, /// Execution completed (result injected into conversation). Completed, /// User denied permission (error result injected into conversation). Denied, } /// Cached preview data for rendering tool output. #[derive(Debug, Clone)] pub(crate) enum ToolPreviewData { /// Shell command VT100 output lines. Shell { lines: Vec, exit_code: Option, interrupted: bool, }, /// File edit diff preview. Edit(EditPreview), /// File write content preview. Write(WritePreview), } /// A tracked tool call with its current lifecycle state. #[derive(Debug, Clone)] pub(crate) struct TrackedTool { pub id: String, pub tool: ClientToolCall, pub state: ToolState, /// Cached preview data for rendering (populated during/after execution). pub preview: Option, } impl TrackedTool { /// Whether this tool has reached a terminal state. pub fn is_resolved(&self) -> bool { matches!(self.state, ToolState::Completed | ToolState::Denied) } /// Extract shell preview data (for TurnBuilder compatibility). pub fn shell_preview(&self) -> Option { match &self.preview { Some(ToolPreviewData::Shell { lines, exit_code, interrupted, }) => Some(crate::tools::ToolPreview { lines: lines.clone(), exit_code: *exit_code, interrupted: *interrupted, }), _ => None, } } /// Extract edit diff preview (for TurnBuilder compatibility). pub fn edit_preview(&self) -> Option<&EditPreview> { match &self.preview { Some(ToolPreviewData::Edit(p)) => Some(p), _ => None, } } /// Extract write content preview (for TurnBuilder compatibility). pub fn write_preview(&self) -> Option<&WritePreview> { match &self.preview { Some(ToolPreviewData::Write(p)) => Some(p), _ => None, } } } /// Manages tool call lifecycles for a single turn. /// /// Tools are inserted when received from the stream and progress through /// their lifecycle independently. The manager provides aggregate queries /// (all resolved, any awaiting permission, etc.) that the FSM uses for /// state transitions. #[derive(Debug, Clone, Default)] pub(crate) struct ToolManager { tools: Vec, } impl ToolManager { pub fn new() -> Self { Self { tools: Vec::new() } } /// Insert a new tool in CheckingPermission state. pub fn insert(&mut self, id: String, tool: ClientToolCall) { self.tools.push(TrackedTool { id, tool, state: ToolState::CheckingPermission, preview: None, }); } /// Look up a tool by ID. pub fn get(&self, id: &str) -> Option<&TrackedTool> { self.tools.iter().find(|t| t.id == id) } /// Look up a tool mutably by ID. pub fn get_mut(&mut self, id: &str) -> Option<&mut TrackedTool> { self.tools.iter_mut().find(|t| t.id == id) } /// True if all tools from the given set of IDs have reached a terminal state. /// Returns true for an empty set (vacuously — no tools to wait for). pub fn all_resolved(&self, tool_ids: &[String]) -> bool { tool_ids .iter() .all(|id| self.get(id).is_some_and(|t| t.is_resolved())) } /// Find the first tool awaiting user permission. pub fn awaiting_permission(&self) -> Option<&TrackedTool> { self.tools .iter() .find(|t| t.state == ToolState::AwaitingPermission) } /// Get IDs of all non-resolved tools (for cancel). pub fn pending_ids(&self) -> Vec { self.tools .iter() .filter(|t| !t.is_resolved()) .map(|t| t.id.clone()) .collect() } /// Get IDs of all currently executing tools (for interrupt/abort). pub fn executing_ids(&self) -> Vec { self.tools .iter() .filter(|t| t.state == ToolState::Executing) .map(|t| t.id.clone()) .collect() } /// True if any tool has a shell preview with live output. pub fn has_executing_preview(&self) -> bool { self.tools.iter().any(|t| { t.state == ToolState::Executing && matches!(t.preview, Some(ToolPreviewData::Shell { .. })) }) } }