Skip to main content

lash_standard_plugins/
lib.rs

1use std::sync::Arc;
2
3use lash_core::plugin::{PluginSpec, StaticPluginFactory};
4use lash_core::{PluginStack, ToolProvider};
5pub use lash_plugin_observational_memory::ObservationalMemoryConfig;
6use lash_plugin_observational_memory::ObservationalMemoryPluginFactory;
7use lash_plugin_process_controls::ProcessControlsPluginFactory;
8pub use lash_plugin_rolling_history::RollingHistoryConfig;
9use lash_plugin_rolling_history::RollingHistoryPluginFactory;
10use lash_plugin_tool_discovery::ToolDiscoveryPluginFactory;
11use lash_plugin_tool_output_budget::{ToolOutputBudgetPluginFactory, tool_output_budget_stack};
12use lash_tool_apply_patch::apply_patch_provider;
13use lash_tool_files::{glob_provider, ls_provider, read_file_provider};
14use lash_tool_search::grep_provider;
15use lash_tool_shell::StandardShellPluginFactory;
16use lash_tool_web::{fetch_url_provider, web_search_provider};
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub enum StandardContextApproachKind {
20    RollingHistory,
21    ObservationalMemory,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25#[serde(tag = "kind", rename_all = "snake_case")]
26pub enum StandardContextApproach {
27    RollingHistory(RollingHistoryConfig),
28    ObservationalMemory(ObservationalMemoryConfig),
29}
30
31impl Default for StandardContextApproach {
32    fn default() -> Self {
33        Self::RollingHistory(RollingHistoryConfig)
34    }
35}
36
37impl StandardContextApproach {
38    pub fn kind(&self) -> StandardContextApproachKind {
39        match self {
40            Self::RollingHistory(_) => StandardContextApproachKind::RollingHistory,
41            Self::ObservationalMemory(_) => StandardContextApproachKind::ObservationalMemory,
42        }
43    }
44}
45
46#[derive(Clone, Debug)]
47pub struct StandardToolStackOptions {
48    pub standard_context_approach: Option<StandardContextApproach>,
49    pub tavily_api_key: Option<String>,
50    pub include_cancel_process: bool,
51}
52
53impl Default for StandardToolStackOptions {
54    fn default() -> Self {
55        Self {
56            standard_context_approach: None,
57            tavily_api_key: None,
58            include_cancel_process: true,
59        }
60    }
61}
62
63pub fn standard_tool_stack(options: StandardToolStackOptions) -> PluginStack {
64    let mut stack = PluginStack::new();
65    push_core_runtime_tools(&mut stack);
66    push_standard_context_tools(&mut stack, options.standard_context_approach.as_ref());
67    push_local_runtime_tools(&mut stack, options.include_cancel_process);
68    if let Some(key) = options.tavily_api_key {
69        push_web_tools(&mut stack, key);
70    }
71    stack
72}
73
74pub fn locked_down_rlm_plugin_stack() -> PluginStack {
75    tool_output_budget_stack()
76}
77
78fn push_core_runtime_tools(stack: &mut PluginStack) {
79    stack.push(Arc::new(ToolDiscoveryPluginFactory::new()));
80    stack.push(Arc::new(ToolOutputBudgetPluginFactory::default()));
81}
82
83fn push_standard_context_tools(
84    stack: &mut PluginStack,
85    standard_context_approach: Option<&StandardContextApproach>,
86) {
87    match standard_context_approach {
88        Some(StandardContextApproach::RollingHistory(config)) => {
89            stack.push(Arc::new(RollingHistoryPluginFactory::new(config.clone())));
90        }
91        Some(StandardContextApproach::ObservationalMemory(config)) => {
92            stack.push(Arc::new(ObservationalMemoryPluginFactory::new(
93                config.clone(),
94            )));
95        }
96        None => {}
97    }
98}
99
100fn push_local_runtime_tools(stack: &mut PluginStack, include_cancel_process: bool) {
101    let process_controls = if include_cancel_process {
102        ProcessControlsPluginFactory::new()
103    } else {
104        ProcessControlsPluginFactory::without_cancel_process()
105    };
106    stack.push(Arc::new(process_controls));
107    stack.push(Arc::new(StandardShellPluginFactory::new()));
108    stack.push(Arc::new(StaticPluginFactory::new(
109        "apply_patch",
110        PluginSpec::new()
111            .with_tool_provider(Arc::new(apply_patch_provider()) as Arc<dyn ToolProvider>),
112    )));
113    stack.push(Arc::new(StaticPluginFactory::new(
114        "read_file",
115        PluginSpec::new()
116            .with_tool_provider(Arc::new(read_file_provider()) as Arc<dyn ToolProvider>),
117    )));
118    stack.push(Arc::new(StaticPluginFactory::new(
119        "glob",
120        PluginSpec::new().with_tool_provider(Arc::new(glob_provider()) as Arc<dyn ToolProvider>),
121    )));
122    stack.push(Arc::new(StaticPluginFactory::new(
123        "ls",
124        PluginSpec::new().with_tool_provider(Arc::new(ls_provider()) as Arc<dyn ToolProvider>),
125    )));
126    stack.push(Arc::new(StaticPluginFactory::new(
127        "grep",
128        PluginSpec::new().with_tool_provider(Arc::new(grep_provider()) as Arc<dyn ToolProvider>),
129    )));
130}
131
132fn push_web_tools(stack: &mut PluginStack, tavily_api_key: String) {
133    let search_key = tavily_api_key.clone();
134    stack.push(Arc::new(StaticPluginFactory::new(
135        "search_web",
136        PluginSpec::new()
137            .with_tool_provider(Arc::new(web_search_provider(search_key)) as Arc<dyn ToolProvider>),
138    )));
139    stack.push(Arc::new(StaticPluginFactory::new(
140        "fetch_url",
141        PluginSpec::new().with_tool_provider(
142            Arc::new(fetch_url_provider(tavily_api_key)) as Arc<dyn ToolProvider>
143        ),
144    )));
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    fn stack_ids(stack: &PluginStack) -> Vec<&'static str> {
152        stack
153            .factories()
154            .iter()
155            .map(|factory| factory.id())
156            .collect()
157    }
158
159    fn tool_names_for_stack(
160        protocol_factories: Vec<Arc<dyn lash_core::plugin::PluginFactory>>,
161        standard_context_approach: Option<StandardContextApproach>,
162        include_cancel_process: bool,
163    ) -> Vec<String> {
164        let mut factories = standard_tool_stack(StandardToolStackOptions {
165            standard_context_approach: standard_context_approach.clone(),
166            include_cancel_process,
167            ..Default::default()
168        })
169        .into_factories();
170        factories.extend(protocol_factories);
171        let host = lash_core::PluginHost::new(factories);
172        let session_id = "test".to_string();
173        let session = host
174            .build_session(session_id.clone(), None)
175            .expect("session");
176        session
177            .tool_surface(&session_id)
178            .expect("tool surface")
179            .tool_names()
180            .as_ref()
181            .clone()
182    }
183
184    #[test]
185    fn rolling_history_context_installs_rolling_history_only() {
186        let stack = standard_tool_stack(StandardToolStackOptions {
187            standard_context_approach: Some(StandardContextApproach::RollingHistory(
188                Default::default(),
189            )),
190            tavily_api_key: None,
191            include_cancel_process: true,
192        });
193        let ids = stack_ids(&stack);
194
195        assert!(ids.contains(&"rolling_history"));
196        assert!(!ids.contains(&"observational_memory"));
197    }
198
199    #[test]
200    fn observational_memory_context_installs_om_support() {
201        let stack = standard_tool_stack(StandardToolStackOptions {
202            standard_context_approach: Some(StandardContextApproach::ObservationalMemory(
203                Default::default(),
204            )),
205            tavily_api_key: None,
206            include_cancel_process: true,
207        });
208        let ids = stack_ids(&stack);
209        assert!(ids.contains(&"observational_memory"));
210    }
211
212    #[test]
213    fn web_tools_are_explicitly_keyed() {
214        let without_web = stack_ids(&standard_tool_stack(StandardToolStackOptions::default()));
215        let with_web = stack_ids(&standard_tool_stack(StandardToolStackOptions {
216            tavily_api_key: Some("key".to_string()),
217            ..Default::default()
218        }));
219
220        assert!(!without_web.contains(&"search_web"));
221        assert!(with_web.contains(&"search_web"));
222        assert!(with_web.contains(&"fetch_url"));
223    }
224
225    #[test]
226    fn shared_stack_exposes_process_list_in_rlm_without_cancel_tool() {
227        let standard_names = tool_names_for_stack(
228            lash_core::testing::test_standard_protocol_factories(),
229            Some(StandardContextApproach::default()),
230            true,
231        );
232        let rlm_names = tool_names_for_stack(
233            lash_core::testing::test_rlm_protocol_factories(),
234            None,
235            false,
236        );
237
238        assert!(standard_names.contains(&"list_process_handles".to_string()));
239        assert!(standard_names.contains(&"cancel_process".to_string()));
240        assert!(rlm_names.contains(&"list_process_handles".to_string()));
241        assert!(!rlm_names.contains(&"cancel_process".to_string()));
242    }
243}