Skip to main content

winx_code_agent/
tool_registry.rs

1//! Typed source of truth for MCP tool identity and cross-cutting behavior.
2
3use std::fmt;
4
5/// Stable tool order used by discovery, policy bits, and dispatch.
6#[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
151/// Backwards-compatible name array derived from the typed stable order.
152pub 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}