1use std::fmt;
4
5#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
7#[repr(u8)]
8pub enum ToolKind {
9 Initialize = 0,
10 BashCommand = 1,
11 ReadFiles = 2,
12 FileWriteOrEdit = 3,
13 MultiFileEdit = 4,
14 VerifyEdit = 5,
15 UndoEdit = 6,
16 ContextSave = 7,
17 ReadImage = 8,
18 CodeMap = 9,
19 ApplyPatch = 10,
20 EditFiles = 11,
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum ToolAccess {
27 ReadOnly,
28 Neutral,
29 Destructive,
30}
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum ToolOutputContract {
34 Shared,
35 CodeMap,
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum ToolWorld {
40 Closed,
41 Open,
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum ToolSessionContract {
46 Initializes,
47 RequiresInitialized,
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct ToolDescriptor {
52 pub access: ToolAccess,
53 pub world: ToolWorld,
54 pub session: ToolSessionContract,
55 pub output_contract: ToolOutputContract,
56}
57
58impl ToolKind {
59 pub const ALL: [Self; 12] = [
60 Self::Initialize,
61 Self::BashCommand,
62 Self::ReadFiles,
63 Self::FileWriteOrEdit,
64 Self::MultiFileEdit,
65 Self::VerifyEdit,
66 Self::UndoEdit,
67 Self::ContextSave,
68 Self::ReadImage,
69 Self::CodeMap,
70 Self::ApplyPatch,
71 Self::EditFiles,
72 ];
73
74 pub const fn as_str(self) -> &'static str {
75 match self {
76 Self::Initialize => "Initialize",
77 Self::BashCommand => "BashCommand",
78 Self::ReadFiles => "ReadFiles",
79 Self::FileWriteOrEdit => "FileWriteOrEdit",
80 Self::MultiFileEdit => "MultiFileEdit",
81 Self::VerifyEdit => "VerifyEdit",
82 Self::UndoEdit => "UndoEdit",
83 Self::ContextSave => "ContextSave",
84 Self::ReadImage => "ReadImage",
85 Self::CodeMap => "CodeMap",
86 Self::ApplyPatch => "ApplyPatch",
87 Self::EditFiles => "EditFiles",
88 }
89 }
90
91 pub fn parse(name: &str) -> Option<Self> {
92 match name {
93 "Initialize" => Some(Self::Initialize),
94 "BashCommand" => Some(Self::BashCommand),
95 "ReadFiles" => Some(Self::ReadFiles),
96 "FileWriteOrEdit" => Some(Self::FileWriteOrEdit),
97 "MultiFileEdit" => Some(Self::MultiFileEdit),
98 "VerifyEdit" => Some(Self::VerifyEdit),
99 "UndoEdit" => Some(Self::UndoEdit),
100 "ContextSave" => Some(Self::ContextSave),
101 "ReadImage" => Some(Self::ReadImage),
102 "CodeMap" => Some(Self::CodeMap),
103 "ApplyPatch" => Some(Self::ApplyPatch),
104 "EditFiles" => Some(Self::EditFiles),
105 _ => None,
106 }
107 }
108
109 pub const fn bit(self) -> u16 {
110 1_u16 << self as u8
111 }
112
113 pub const fn descriptor(self) -> ToolDescriptor {
114 let (access, world) = match self {
115 Self::Initialize | Self::ReadFiles | Self::ReadImage | Self::CodeMap => {
116 (ToolAccess::ReadOnly, ToolWorld::Closed)
117 }
118 Self::ContextSave => (ToolAccess::Neutral, ToolWorld::Closed),
119 Self::UndoEdit => (ToolAccess::Destructive, ToolWorld::Closed),
120 Self::BashCommand
121 | Self::FileWriteOrEdit
122 | Self::MultiFileEdit
123 | Self::ApplyPatch
124 | Self::VerifyEdit
125 | Self::EditFiles => (ToolAccess::Destructive, ToolWorld::Open),
126 };
127 ToolDescriptor {
128 access,
129 world,
130 session: if matches!(self, Self::Initialize) {
131 ToolSessionContract::Initializes
132 } else {
133 ToolSessionContract::RequiresInitialized
134 },
135 output_contract: if matches!(self, Self::CodeMap) {
136 ToolOutputContract::CodeMap
137 } else {
138 ToolOutputContract::Shared
139 },
140 }
141 }
142
143 pub const fn is_file_mutation(self) -> bool {
144 matches!(
145 self,
146 Self::FileWriteOrEdit | Self::MultiFileEdit | Self::ApplyPatch | Self::EditFiles
147 )
148 }
149
150 pub const fn requires_bash_companion(self) -> bool {
151 matches!(self, Self::VerifyEdit)
152 }
153}
154
155impl fmt::Display for ToolKind {
156 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157 formatter.write_str(self.as_str())
158 }
159}
160
161pub const ALL_TOOL_NAMES: [&str; 12] = [
163 ToolKind::Initialize.as_str(),
164 ToolKind::BashCommand.as_str(),
165 ToolKind::ReadFiles.as_str(),
166 ToolKind::FileWriteOrEdit.as_str(),
167 ToolKind::MultiFileEdit.as_str(),
168 ToolKind::VerifyEdit.as_str(),
169 ToolKind::UndoEdit.as_str(),
170 ToolKind::ContextSave.as_str(),
171 ToolKind::ReadImage.as_str(),
172 ToolKind::CodeMap.as_str(),
173 ToolKind::ApplyPatch.as_str(),
174 ToolKind::EditFiles.as_str(),
175];
176
177#[cfg(test)]
178mod tests {
179 use std::collections::HashSet;
180
181 use super::*;
182
183 #[test]
184 fn names_bits_and_parser_are_one_to_one() {
185 assert_eq!(ALL_TOOL_NAMES, ToolKind::ALL.map(ToolKind::as_str));
186 let names = ToolKind::ALL.into_iter().map(ToolKind::as_str).collect::<HashSet<_>>();
187 let bits = ToolKind::ALL.into_iter().map(ToolKind::bit).collect::<HashSet<_>>();
188 assert_eq!(names.len(), ToolKind::ALL.len());
189 assert_eq!(bits.len(), ToolKind::ALL.len());
190 for kind in ToolKind::ALL {
191 assert_eq!(ToolKind::parse(kind.as_str()), Some(kind));
192 }
193 assert_eq!(ToolKind::parse("readfiles"), None);
194 }
195
196 #[test]
197 fn every_current_tool_is_principal_and_workspace_session_aware() {
198 assert_eq!(ToolKind::Initialize.descriptor().session, ToolSessionContract::Initializes);
199 assert!(ToolKind::ALL
200 .into_iter()
201 .filter(|kind| *kind != ToolKind::Initialize)
202 .all(|kind| { kind.descriptor().session == ToolSessionContract::RequiresInitialized }));
203 }
204}