Skip to main content

yapi_core/tools/
registry.rs

1//! The session's tools and which of them are active, as pi's `AgentSession`
2//! keeps them (`_refreshToolRegistry`, `_applyToolLoadout` in pi `v1.0.0`).
3//!
4//! Active tools are declared to the model. Registering a `direct` or
5//! `model-only` tool activates it unless it opts out. Re-registering a name
6//! replaces the tool in place, and a tool that becomes hidden is deactivated.
7//! Names restored from the transcript before their tool is registered wait as
8//! pending and activate on registration.
9//!
10//! `--tools`, `--no-tools` and `--exclude-tools` restrict which tools exist
11//! at all, as pi's `_isAllowedTool`: tools outside them are never registered
12//! or activated, whoever registers them.
13
14use indexmap::IndexMap;
15
16use super::{Exposure, RegisteredTool};
17
18/// Every registered tool, in registration order, and the active set.
19#[derive(Clone, Default)]
20pub struct ToolRegistry {
21    tools: IndexMap<String, RegisteredTool>,
22    active: Vec<String>,
23    pending: Vec<String>,
24    /// The only tool names allowed, when restricted.
25    allowed: Option<Vec<String>>,
26    /// Tool names never allowed.
27    excluded: Vec<String>,
28}
29
30impl ToolRegistry {
31    /// `tools` registered, with `active` the active names among them.
32    pub fn new(tools: Vec<RegisteredTool>, active: Vec<String>) -> ToolRegistry {
33        let mut registry = ToolRegistry {
34            tools: tools
35                .into_iter()
36                .map(|tool| (tool.name().to_owned(), tool))
37                .collect(),
38            active: Vec::new(),
39            pending: Vec::new(),
40            allowed: None,
41            excluded: Vec::new(),
42        };
43        registry.apply(active);
44        registry
45    }
46
47    /// Allows only `allowed` (every name when `None`) minus `excluded`, and
48    /// removes registered tools outside them.
49    pub fn restrict(&mut self, allowed: Option<Vec<String>>, excluded: Vec<String>) {
50        self.allowed = allowed;
51        self.excluded = excluded;
52        let removed: Vec<String> = self
53            .tools
54            .keys()
55            .filter(|name| !self.is_allowed(name))
56            .cloned()
57            .collect();
58        for name in removed {
59            self.tools.shift_remove(&name);
60        }
61        self.apply(self.active.clone());
62    }
63
64    /// pi's `_isAllowedTool`.
65    pub fn is_allowed(&self, name: &str) -> bool {
66        self.allowed
67            .as_ref()
68            .is_none_or(|allowed| allowed.iter().any(|allowed| allowed == name))
69            && !self.excluded.iter().any(|excluded| excluded == name)
70    }
71
72    fn activated_on_registration(tool: &RegisteredTool) -> bool {
73        tool.exposure.declarable() && tool.default_active
74    }
75
76    /// Keeps the names of registered, non-hidden tools, once each.
77    fn apply(&mut self, names: Vec<String>) {
78        let mut active = Vec::new();
79        for name in names {
80            let usable = self
81                .tools
82                .get(&name)
83                .is_some_and(|tool| tool.exposure != Exposure::Hidden);
84            if usable && !active.contains(&name) {
85                active.push(name);
86            }
87        }
88        self.pending.retain(|name| !active.contains(name));
89        self.active = active;
90    }
91
92    /// Registers or replaces a tool, then updates the active set.
93    pub fn register(&mut self, tool: RegisteredTool) {
94        let name = tool.name().to_owned();
95        if !self.is_allowed(&name) {
96            return;
97        }
98        let was_activated = self
99            .tools
100            .get(&name)
101            .is_some_and(Self::activated_on_registration);
102        // Naming a tool in `--tools` activates it even when it is not active
103        // by default, as in pi.
104        let named = self.allowed.is_some() && tool.exposure.declarable();
105        let activate = !was_activated && (named || Self::activated_on_registration(&tool));
106        self.tools.insert(name.clone(), tool);
107        let mut next = self.active.clone();
108        if activate {
109            next.push(name);
110        }
111        next.extend(
112            self.pending
113                .iter()
114                .filter(|pending| self.tools.contains_key(*pending))
115                .cloned(),
116        );
117        self.apply(next);
118    }
119
120    /// Activates exactly `names`, ignoring unknown and hidden ones. Pending
121    /// names are dropped when a previously active tool is deactivated.
122    pub fn set_active(&mut self, names: Vec<String>) {
123        let previous = self.active.clone();
124        self.apply(names);
125        if previous.iter().any(|name| !self.active.contains(name)) {
126            self.pending.clear();
127        }
128    }
129
130    /// Activates `names` as a transcript recorded them. Names of tools not
131    /// registered yet activate when they are.
132    pub fn restore(&mut self, names: Vec<String>) {
133        self.pending = names
134            .iter()
135            .filter(|name| !self.tools.contains_key(*name))
136            .cloned()
137            .collect();
138        self.apply(names);
139    }
140
141    /// Names of the active tools, in activation order.
142    pub fn active(&self) -> Vec<String> {
143        self.active.clone()
144    }
145
146    /// The active tools, as declared to the model.
147    pub fn declared(&self) -> Vec<RegisteredTool> {
148        self.active
149            .iter()
150            .filter_map(|name| self.tools.get(name).cloned())
151            .collect()
152    }
153
154    /// Every registered tool, in registration order.
155    pub fn all(&self) -> Vec<RegisteredTool> {
156        self.tools.values().cloned().collect()
157    }
158
159    /// The tool named `name`.
160    pub fn get(&self, name: &str) -> Option<&RegisteredTool> {
161        self.tools.get(name)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use std::sync::Arc;
168
169    use futures_util::future::BoxFuture;
170    use serde_json::Value;
171    use tokio_util::sync::CancellationToken;
172    use yapi_agent::{Tool, UpdateSink};
173    use yapi_types::event::ToolResult;
174    use yapi_types::message::ToolDeclaration;
175
176    use super::*;
177
178    struct Named(ToolDeclaration);
179
180    impl Tool for Named {
181        fn declaration(&self) -> &ToolDeclaration {
182            &self.0
183        }
184
185        fn execute(
186            &self,
187            _call_id: String,
188            _args: Value,
189            _cancel: CancellationToken,
190            _updates: UpdateSink,
191        ) -> BoxFuture<'_, Result<ToolResult, String>> {
192            Box::pin(async { Err("unused".into()) })
193        }
194    }
195
196    fn tool(name: &str, exposure: Exposure) -> RegisteredTool {
197        RegisteredTool {
198            exposure,
199            ..RegisteredTool::direct(
200                Arc::new(Named(ToolDeclaration {
201                    name: name.into(),
202                    description: String::new(),
203                    parameters: serde_json::json!({}),
204                    constrained_sampling: None,
205                })),
206                None,
207                Vec::new(),
208            )
209        }
210    }
211
212    #[test]
213    fn activates_like_pi() {
214        let mut registry = ToolRegistry::new(
215            vec![
216                tool("read", Exposure::Direct),
217                tool("bash", Exposure::Direct),
218            ],
219            vec!["read".into(), "nope".into()],
220        );
221        assert_eq!(registry.active(), ["read"]);
222        registry.register(tool("mcp__a", Exposure::Direct));
223        registry.register(tool("mcp__b", Exposure::Deferred));
224        assert_eq!(registry.active(), ["read", "mcp__a"]);
225        // Re-registering a direct tool does not reactivate it.
226        registry.set_active(vec!["read".into()]);
227        registry.register(tool("mcp__a", Exposure::Direct));
228        assert_eq!(registry.active(), ["read"]);
229        // Hidden tools leave the active set, and becoming direct again
230        // activates them.
231        registry.set_active(vec!["read".into(), "mcp__a".into(), "mcp__b".into()]);
232        registry.register(tool("mcp__a", Exposure::Hidden));
233        assert_eq!(registry.active(), ["read", "mcp__b"]);
234        registry.register(tool("mcp__a", Exposure::Direct));
235        assert_eq!(registry.active(), ["read", "mcp__b", "mcp__a"]);
236    }
237
238    #[test]
239    fn tool_flags_restrict_registrations() {
240        let mut registry = ToolRegistry::new(
241            vec![
242                tool("read", Exposure::Direct),
243                tool("bash", Exposure::Direct),
244            ],
245            vec!["read".into(), "bash".into()],
246        );
247        // `--tools read,mcp__named`: nothing else exists, and a named tool
248        // activates on registration.
249        registry.restrict(Some(vec!["read".into(), "mcp__named".into()]), Vec::new());
250        assert_eq!(registry.active(), ["read"]);
251        registry.register(tool("codemode", Exposure::Direct));
252        registry.register(tool("mcp__other", Exposure::Direct));
253        registry.register(tool("mcp__named", Exposure::Deferred));
254        assert!(registry.get("codemode").is_none());
255        assert!(registry.get("mcp__other").is_none());
256        registry.set_active(vec!["read".into(), "codemode".into()]);
257        assert_eq!(registry.active(), ["read"]);
258        // `--no-tools` allows none, and `--exclude-tools` removes names.
259        registry.restrict(Some(Vec::new()), Vec::new());
260        registry.register(tool("mcp__a", Exposure::Direct));
261        assert!(registry.active().is_empty());
262        registry.restrict(None, vec!["bash".into()]);
263        registry.register(tool("bash", Exposure::Direct));
264        registry.register(tool("mcp__a", Exposure::Direct));
265        assert_eq!(registry.active(), ["mcp__a"]);
266    }
267
268    #[test]
269    fn restored_names_await_registration() {
270        let mut registry = ToolRegistry::new(vec![tool("read", Exposure::Direct)], Vec::new());
271        registry.restore(vec!["read".into(), "mcp__late".into()]);
272        assert_eq!(registry.active(), ["read"]);
273        registry.register(tool("mcp__late", Exposure::Deferred));
274        assert_eq!(registry.active(), ["read", "mcp__late"]);
275    }
276}