diff options
Diffstat (limited to 'crates/atuin-ai/src/fsm/tools.rs')
| -rw-r--r-- | crates/atuin-ai/src/fsm/tools.rs | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/crates/atuin-ai/src/fsm/tools.rs b/crates/atuin-ai/src/fsm/tools.rs new file mode 100644 index 00000000..a6b2e9ae --- /dev/null +++ b/crates/atuin-ai/src/fsm/tools.rs @@ -0,0 +1,165 @@ +//! 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<String>, + exit_code: Option<i32>, + 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<ToolPreviewData>, +} + +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<crate::tools::ToolPreview> { + 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<TrackedTool>, +} + +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<String> { + 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<String> { + 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 { .. })) + }) + } +} |
