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