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    /// Unified mutation surface. Appended so every persisted legacy policy bit
21    /// keeps its original meaning during the catalog migration.
22    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
161/// Backwards-compatible name array derived from the typed stable order.
162pub 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}