Skip to main content

steer_core/tools/
capability.rs

1//! Tool capability system for static tools.
2//!
3//! Capabilities define what runtime services a tool needs. The runtime
4//! advertises its available capabilities, and only tools whose requirements
5//! are satisfied get exposed to the model.
6
7use bitflags::bitflags;
8use serde::{Deserialize, Serialize};
9
10bitflags! {
11    /// Capabilities that the runtime can provide to tools.
12    ///
13    /// Tools declare which capabilities they require, and the runtime
14    /// filters tool availability based on what it can provide.
15    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16    pub struct Capabilities: u64 {
17        /// Access to the workspace (filesystem, environment, working directory).
18        /// Almost all tools require this.
19        const WORKSPACE = 1 << 0;
20
21        /// Ability to spawn sub-agents that run tool loops.
22        /// Implies access to EventStore, ApiClient, ToolExecutor.
23        const AGENT_SPAWNER = 1 << 1;
24
25        /// Ability to call LLM models directly (outside of agent loop).
26        /// Used by tools like `fetch` that need summarization.
27        const MODEL_CALLER = 1 << 2;
28
29        /// Network access for external HTTP requests.
30        const NETWORK = 1 << 3;
31
32        /// Convenience: basic workspace tools (grep, view, glob, ls)
33        const BASIC = Self::WORKSPACE.bits();
34
35        /// Convenience: tools that need to spawn agents
36        const AGENT = Self::WORKSPACE.bits() | Self::AGENT_SPAWNER.bits();
37
38        /// Convenience: tools that need model access
39        const MODEL = Self::WORKSPACE.bits() | Self::MODEL_CALLER.bits();
40    }
41}
42
43impl Capabilities {
44    /// Check if these capabilities satisfy a tool's requirements.
45    pub fn satisfies(&self, required: Capabilities) -> bool {
46        self.contains(required)
47    }
48
49    /// Return human-readable names of the capabilities in this set.
50    pub fn names(&self) -> Vec<&'static str> {
51        let mut names = Vec::new();
52        if self.contains(Capabilities::WORKSPACE) {
53            names.push("workspace");
54        }
55        if self.contains(Capabilities::AGENT_SPAWNER) {
56            names.push("agent_spawner");
57        }
58        if self.contains(Capabilities::MODEL_CALLER) {
59            names.push("model_caller");
60        }
61        if self.contains(Capabilities::NETWORK) {
62            names.push("network");
63        }
64        names
65    }
66
67    /// Return the capabilities that are missing to satisfy requirements.
68    pub fn missing(&self, required: Capabilities) -> Capabilities {
69        required - *self
70    }
71}
72
73impl Default for Capabilities {
74    fn default() -> Self {
75        Capabilities::empty()
76    }
77}
78
79impl std::fmt::Display for Capabilities {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        let names = self.names();
82        if names.is_empty() {
83            write!(f, "(none)")
84        } else {
85            write!(f, "{}", names.join(", "))
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_satisfies_exact() {
96        let available = Capabilities::WORKSPACE;
97        assert!(available.satisfies(Capabilities::WORKSPACE));
98    }
99
100    #[test]
101    fn test_satisfies_superset() {
102        let available = Capabilities::WORKSPACE | Capabilities::AGENT_SPAWNER;
103        assert!(available.satisfies(Capabilities::WORKSPACE));
104        assert!(available.satisfies(Capabilities::AGENT_SPAWNER));
105        assert!(available.satisfies(Capabilities::WORKSPACE | Capabilities::AGENT_SPAWNER));
106    }
107
108    #[test]
109    fn test_satisfies_missing() {
110        let available = Capabilities::WORKSPACE;
111        assert!(!available.satisfies(Capabilities::AGENT_SPAWNER));
112        assert!(!available.satisfies(Capabilities::WORKSPACE | Capabilities::AGENT_SPAWNER));
113    }
114
115    #[test]
116    fn test_missing_capabilities() {
117        let available = Capabilities::WORKSPACE;
118        let required = Capabilities::WORKSPACE | Capabilities::AGENT_SPAWNER;
119        let missing = available.missing(required);
120        assert_eq!(missing, Capabilities::AGENT_SPAWNER);
121    }
122
123    #[test]
124    fn test_convenience_flags() {
125        assert!(Capabilities::BASIC.contains(Capabilities::WORKSPACE));
126        assert!(Capabilities::AGENT.contains(Capabilities::WORKSPACE));
127        assert!(Capabilities::AGENT.contains(Capabilities::AGENT_SPAWNER));
128    }
129
130    #[test]
131    fn test_display() {
132        let caps = Capabilities::WORKSPACE | Capabilities::NETWORK;
133        let s = caps.to_string();
134        assert!(s.contains("workspace"));
135        assert!(s.contains("network"));
136    }
137}