lash_core/runtime/
environment.rs1use std::sync::Arc;
28
29use lash_trace::{TraceContext, TraceLevel, TraceSink};
30
31#[cfg(test)]
32use super::InlineEffectHost;
33use super::process::ProcessRegistry;
34use super::{EffectHost, RuntimeHostConfig, TerminationPolicy};
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub enum Residency {
46 #[default]
49 KeepAll,
50 ActivePathOnly,
56}
57
58#[derive(Clone)]
66pub struct RuntimeEnvironment {
67 pub plugin_host: Option<Arc<crate::PluginHost>>,
70
71 pub residency: Residency,
77
78 pub process_registry: Option<Arc<dyn ProcessRegistry>>,
80
81 pub trigger_store: Option<Arc<dyn crate::TriggerStore>>,
83
84 pub session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
87
88 pub process_work_poke: Option<super::ProcessWorkPoke>,
93
94 pub queued_work_poke: Option<super::QueuedWorkPoke>,
97
98 pub core: RuntimeHostConfig,
99}
100
101impl RuntimeEnvironment {
102 pub fn builder() -> RuntimeEnvironmentBuilder {
103 RuntimeEnvironmentBuilder::default()
104 }
105}
106
107pub struct ParkedSession {
112 pub(crate) session_id: String,
113 pub(crate) store: Arc<dyn crate::store::RuntimePersistence>,
114 pub(crate) policy: crate::SessionPolicy,
115}
116
117impl ParkedSession {
118 pub fn session_id(&self) -> &str {
119 &self.session_id
120 }
121}
122
123pub struct RuntimeEnvironmentBuilder {
125 env: RuntimeEnvironment,
126}
127
128impl Default for RuntimeEnvironmentBuilder {
129 fn default() -> Self {
130 Self {
135 env: RuntimeEnvironment {
136 plugin_host: None,
137 residency: Residency::default(),
138 process_registry: None,
139 trigger_store: Some(Arc::new(crate::InMemoryTriggerStore::default())),
140 session_store_factory: None,
141 process_work_poke: None,
142 queued_work_poke: None,
143 core: RuntimeHostConfig::in_memory(),
144 },
145 }
146 }
147}
148
149impl RuntimeEnvironmentBuilder {
150 pub fn with_plugin_host(mut self, host: Arc<crate::PluginHost>) -> Self {
151 self.env.plugin_host = Some(host);
152 self
153 }
154
155 pub fn with_residency(mut self, residency: Residency) -> Self {
156 self.env.residency = residency;
157 self
158 }
159
160 pub fn with_process_registry(mut self, process_registry: Arc<dyn ProcessRegistry>) -> Self {
161 self.env.process_registry = Some(process_registry);
162 self
163 }
164
165 pub fn with_trigger_store(mut self, store: Arc<dyn crate::TriggerStore>) -> Self {
166 self.env.trigger_store = Some(store);
167 self
168 }
169
170 pub fn with_session_store_factory(
171 mut self,
172 factory: Arc<dyn crate::SessionStoreFactory>,
173 ) -> Self {
174 self.env.session_store_factory = Some(factory);
175 self
176 }
177
178 pub fn with_process_work_poke(mut self, poke: super::ProcessWorkPoke) -> Self {
182 self.env.process_work_poke = Some(poke);
183 self
184 }
185
186 pub fn with_queued_work_poke(mut self, poke: super::QueuedWorkPoke) -> Self {
187 self.env.queued_work_poke = Some(poke);
188 self
189 }
190
191 pub fn with_runtime_host_config(mut self, core: RuntimeHostConfig) -> Self {
192 self.env.core = core;
193 self
194 }
195
196 pub fn with_attachment_store(mut self, store: Arc<dyn crate::AttachmentStore>) -> Self {
197 self.env.core.durability.attachment_store = store;
198 self
199 }
200
201 pub fn with_prompt_template(mut self, template: crate::PromptTemplate) -> Self {
202 self.env.core.prompt.prompt.template = Some(template);
203 self
204 }
205
206 pub fn with_prompt_contribution(mut self, contribution: crate::PromptContribution) -> Self {
207 self.env.core.prompt.prompt.add_contribution(contribution);
208 self
209 }
210
211 pub fn with_replaced_prompt_slot(
212 mut self,
213 slot: crate::PromptSlot,
214 contributions: impl IntoIterator<Item = crate::PromptContribution>,
215 ) -> Self {
216 self.env
217 .core
218 .prompt
219 .prompt
220 .replace_slot(slot, contributions);
221 self
222 }
223
224 pub fn with_cleared_prompt_slot(mut self, slot: crate::PromptSlot) -> Self {
225 self.env.core.prompt.prompt.clear_slot(slot);
226 self
227 }
228
229 pub fn with_prompt_layer(mut self, prompt: crate::PromptLayer) -> Self {
230 self.env.core.prompt.prompt = prompt;
231 self
232 }
233
234 pub fn with_trace_sink(mut self, sink: Option<Arc<dyn TraceSink>>) -> Self {
235 self.env.core.tracing.trace_sink = sink;
236 self
237 }
238
239 pub fn with_trace_level(mut self, level: TraceLevel) -> Self {
240 self.env.core.tracing.trace_level = level;
241 self
242 }
243
244 pub fn with_trace_context(mut self, context: TraceContext) -> Self {
245 self.env.core.tracing.trace_context = context;
246 self
247 }
248
249 pub fn with_termination(mut self, termination: TerminationPolicy) -> Self {
250 self.env.core.control.termination = termination;
251 self
252 }
253
254 pub fn with_effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
255 self.env.core.control.effect_host = effect_host;
256 self
257 }
258
259 pub fn with_provider_resolver(
260 mut self,
261 provider_resolver: Arc<dyn crate::RuntimeProviderResolver>,
262 ) -> Self {
263 self.env.core.providers.provider_resolver = provider_resolver;
264 self
265 }
266
267 pub fn build(self) -> RuntimeEnvironment {
268 self.env
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn builder_methods_configure_runtime_host() {
278 let attachment_store: Arc<dyn crate::AttachmentStore> =
279 Arc::new(crate::InMemoryAttachmentStore::new());
280 let effect_host: Arc<dyn EffectHost> = Arc::new(InlineEffectHost::default());
281 let trace_context = TraceContext::default().for_session("session-1");
282 let termination = TerminationPolicy {
283 treat_missing_done_as_failure: false,
284 };
285
286 let env = RuntimeEnvironment::builder()
287 .with_attachment_store(Arc::clone(&attachment_store))
288 .with_prompt_template(crate::default_prompt_template())
289 .with_trace_sink(Some(Arc::new(lash_trace::JsonlTraceSink::new(
290 std::env::temp_dir().join("lash-runtime-environment-builder-test.jsonl"),
291 ))))
292 .with_trace_level(TraceLevel::Extended)
293 .with_trace_context(trace_context.clone())
294 .with_termination(termination.clone())
295 .with_effect_host(Arc::clone(&effect_host))
296 .build();
297
298 assert!(Arc::ptr_eq(
299 &env.core.durability.attachment_store,
300 &attachment_store
301 ));
302 assert!(env.core.prompt.prompt.template.is_some());
303 assert!(env.core.tracing.trace_sink.is_some());
304 assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
305 assert_eq!(env.core.tracing.trace_context, trace_context);
306 assert_eq!(
307 env.core.control.termination.treat_missing_done_as_failure,
308 termination.treat_missing_done_as_failure
309 );
310 assert!(Arc::ptr_eq(&env.core.control.effect_host, &effect_host));
311 }
312
313 #[test]
314 fn runtime_host_config_replaces_core_config() {
315 let mut core = RuntimeHostConfig::in_memory();
316 core.tracing.trace_level = TraceLevel::Extended;
317 core.control.termination = TerminationPolicy {
318 treat_missing_done_as_failure: false,
319 };
320
321 let env = RuntimeEnvironment::builder()
322 .with_trace_level(TraceLevel::Standard)
323 .with_runtime_host_config(core)
324 .build();
325
326 assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
327 assert!(!env.core.control.termination.treat_missing_done_as_failure);
328 }
329
330 #[test]
331 fn runtime_environment_does_not_mirror_runtime_host_config_fields() {
332 let source = std::fs::read_to_string(
333 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/runtime/environment.rs"),
334 )
335 .expect("read environment source");
336 for field in [
337 ["pub ", "attachment_store:"].concat(),
338 ["pub ", "prompt:"].concat(),
339 ["pub ", "trace_sink:"].concat(),
340 ["pub ", "trace_level:"].concat(),
341 ["pub ", "trace_context:"].concat(),
342 ["pub ", "termination:"].concat(),
343 ["pub ", "effect_host:"].concat(),
344 ["mirror ", "`RuntimeHostConfig`"].concat(),
345 ] {
346 assert!(
347 !source.contains(&field),
348 "found mirrored field/comment: {field}"
349 );
350 }
351 }
352}