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