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