1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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 { .. }))
})
}
}
|