Skip to main content

lash_standard_plugins/
lib.rs

1pub mod rolling_history;
2
3use std::sync::Arc;
4
5use lash_core::plugin::{PluginSpec, StaticPluginFactory};
6use lash_core::{PluginStack, ToolProvider};
7pub use lash_plugin_observational_memory::ObservationalMemoryConfig;
8use lash_plugin_observational_memory::ObservationalMemoryPluginFactory;
9use lash_plugin_process_controls::SessionProcessAdminPluginFactory;
10use lash_plugin_tool_output_budget::{ToolOutputBudgetPluginFactory, tool_output_budget_stack};
11use lash_tools::apply_patch::apply_patch_provider;
12use lash_tools::files::{glob_provider, read_file_provider};
13use lash_tools::shell::StandardShellPluginFactory;
14use lash_tools::web::{fetch_url_provider, web_search_provider};
15pub use rolling_history::RollingHistoryConfig;
16use rolling_history::RollingHistoryPluginFactory;
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(ToolOutputBudgetPluginFactory::default()));
80}
81
82fn push_standard_context_tools(
83    stack: &mut PluginStack,
84    standard_context_approach: Option<&StandardContextApproach>,
85) {
86    match standard_context_approach {
87        Some(StandardContextApproach::RollingHistory(config)) => {
88            stack.push(Arc::new(RollingHistoryPluginFactory::new(config.clone())));
89        }
90        Some(StandardContextApproach::ObservationalMemory(config)) => {
91            stack.push(Arc::new(ObservationalMemoryPluginFactory::new(
92                config.clone(),
93            )));
94        }
95        None => {}
96    }
97}
98
99fn push_local_runtime_tools(stack: &mut PluginStack, include_cancel_process: bool) {
100    let processess = if include_cancel_process {
101        SessionProcessAdminPluginFactory::new()
102    } else {
103        SessionProcessAdminPluginFactory::without_cancel_process()
104    };
105    stack.push(Arc::new(processess));
106    stack.push(Arc::new(StandardShellPluginFactory::new()));
107    stack.push(Arc::new(StaticPluginFactory::new(
108        "apply_patch",
109        PluginSpec::new()
110            .with_tool_provider(Arc::new(apply_patch_provider()) as Arc<dyn ToolProvider>),
111    )));
112    stack.push(Arc::new(StaticPluginFactory::new(
113        "read_file",
114        PluginSpec::new()
115            .with_tool_provider(Arc::new(read_file_provider()) as Arc<dyn ToolProvider>),
116    )));
117    stack.push(Arc::new(StaticPluginFactory::new(
118        "glob",
119        PluginSpec::new().with_tool_provider(Arc::new(glob_provider()) as Arc<dyn ToolProvider>),
120    )));
121}
122
123fn push_web_tools(stack: &mut PluginStack, tavily_api_key: String) {
124    let search_key = tavily_api_key.clone();
125    stack.push(Arc::new(StaticPluginFactory::new(
126        "search_web",
127        PluginSpec::new()
128            .with_tool_provider(Arc::new(web_search_provider(search_key)) as Arc<dyn ToolProvider>),
129    )));
130    stack.push(Arc::new(StaticPluginFactory::new(
131        "fetch_url",
132        PluginSpec::new().with_tool_provider(
133            Arc::new(fetch_url_provider(tavily_api_key)) as Arc<dyn ToolProvider>
134        ),
135    )));
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    fn stack_ids(stack: &PluginStack) -> Vec<&'static str> {
143        stack
144            .factories()
145            .iter()
146            .map(|factory| factory.id())
147            .collect()
148    }
149
150    fn tool_names_for_stack(
151        protocol_factories: Vec<Arc<dyn lash_core::plugin::PluginFactory>>,
152        standard_context_approach: Option<StandardContextApproach>,
153        include_cancel_process: bool,
154    ) -> Vec<String> {
155        let mut factories = standard_tool_stack(StandardToolStackOptions {
156            standard_context_approach: standard_context_approach.clone(),
157            include_cancel_process,
158            ..Default::default()
159        })
160        .into_factories();
161        factories.extend(protocol_factories);
162        let host = lash_core::PluginHost::new(factories);
163        let session_id = "test".to_string();
164        let session = host
165            .build_session(session_id.clone(), None)
166            .expect("session");
167        session
168            .resolved_tool_catalog(&session_id)
169            .expect("tool catalog")
170            .tool_names()
171            .as_ref()
172            .clone()
173    }
174
175    #[test]
176    fn rolling_history_context_installs_rolling_history_only() {
177        let stack = standard_tool_stack(StandardToolStackOptions {
178            standard_context_approach: Some(StandardContextApproach::RollingHistory(
179                Default::default(),
180            )),
181            tavily_api_key: None,
182            include_cancel_process: true,
183        });
184        let ids = stack_ids(&stack);
185
186        assert!(ids.contains(&"rolling_history"));
187        assert!(!ids.contains(&"observational_memory"));
188    }
189
190    #[test]
191    fn observational_memory_context_installs_om_support() {
192        let stack = standard_tool_stack(StandardToolStackOptions {
193            standard_context_approach: Some(StandardContextApproach::ObservationalMemory(
194                Default::default(),
195            )),
196            tavily_api_key: None,
197            include_cancel_process: true,
198        });
199        let ids = stack_ids(&stack);
200        assert!(ids.contains(&"observational_memory"));
201    }
202
203    #[test]
204    fn web_tools_are_explicitly_keyed() {
205        let without_web = stack_ids(&standard_tool_stack(StandardToolStackOptions::default()));
206        let with_web = stack_ids(&standard_tool_stack(StandardToolStackOptions {
207            tavily_api_key: Some("key".to_string()),
208            ..Default::default()
209        }));
210
211        assert!(!without_web.contains(&"search_web"));
212        assert!(with_web.contains(&"search_web"));
213        assert!(with_web.contains(&"fetch_url"));
214    }
215
216    #[test]
217    fn standard_stack_does_not_install_cli_local_grep() {
218        let ids = stack_ids(&standard_tool_stack(StandardToolStackOptions::default()));
219
220        assert!(!ids.contains(&"grep"));
221    }
222
223    #[test]
224    fn standard_stack_exposes_glob_and_read_without_ls() {
225        let names = tool_names_for_stack(
226            lash_core::testing::test_standard_protocol_factories(),
227            None,
228            true,
229        );
230
231        assert!(names.contains(&"glob".to_string()));
232        assert!(names.contains(&"read_file".to_string()));
233        assert!(!names.contains(&"ls".to_string()));
234    }
235
236    #[test]
237    fn shared_stack_exposes_process_list_in_rlm_without_cancel_tool() {
238        let standard_names = tool_names_for_stack(
239            lash_core::testing::test_standard_protocol_factories(),
240            Some(StandardContextApproach::default()),
241            true,
242        );
243        let rlm_names = tool_names_for_stack(
244            lash_core::testing::test_code_protocol_factories(),
245            None,
246            false,
247        );
248
249        assert!(standard_names.contains(&"list_process_handles".to_string()));
250        assert!(standard_names.contains(&"cancel_process".to_string()));
251        assert!(rlm_names.contains(&"list_process_handles".to_string()));
252        assert!(!rlm_names.contains(&"cancel_process".to_string()));
253    }
254}