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