lash_standard_plugins/
lib.rs1pub 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}