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