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::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}