1use 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 host_event_store: Option<Arc<dyn crate::HostEventStore>>,
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 host_event_store: Some(Arc::new(crate::InMemoryHostEventStore::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 if let Some(host) = self.env.plugin_host.take() {
163 let abilities = super::builder::lashlang_abilities_for_process_registry(
164 host.lashlang_abilities(),
165 true,
166 );
167 self.env.plugin_host = Some(Arc::new(
168 host.as_ref().clone().with_lashlang_abilities(abilities),
169 ));
170 }
171 self
172 }
173
174 pub fn with_host_event_store(mut self, store: Arc<dyn crate::HostEventStore>) -> Self {
175 self.env.host_event_store = Some(store);
176 self
177 }
178
179 pub fn with_session_store_factory(
180 mut self,
181 factory: Arc<dyn crate::SessionStoreFactory>,
182 ) -> Self {
183 self.env.session_store_factory = Some(factory);
184 self
185 }
186
187 pub fn with_process_work_poke(mut self, poke: super::ProcessWorkPoke) -> Self {
191 self.env.process_work_poke = Some(poke);
192 self
193 }
194
195 pub fn with_queued_work_poke(mut self, poke: super::QueuedWorkPoke) -> Self {
196 self.env.queued_work_poke = Some(poke);
197 self
198 }
199
200 pub fn with_runtime_host_config(mut self, core: RuntimeHostConfig) -> Self {
201 self.env.core = core;
202 self
203 }
204
205 pub fn with_attachment_store(mut self, store: Arc<dyn crate::AttachmentStore>) -> Self {
206 self.env.core.durability.attachment_store = store;
207 self
208 }
209
210 pub fn with_prompt_template(mut self, template: crate::PromptTemplate) -> Self {
211 self.env.core.prompt.prompt.template = Some(template);
212 self
213 }
214
215 pub fn with_prompt_contribution(mut self, contribution: crate::PromptContribution) -> Self {
216 self.env.core.prompt.prompt.add_contribution(contribution);
217 self
218 }
219
220 pub fn with_replaced_prompt_slot(
221 mut self,
222 slot: crate::PromptSlot,
223 contributions: impl IntoIterator<Item = crate::PromptContribution>,
224 ) -> Self {
225 self.env
226 .core
227 .prompt
228 .prompt
229 .replace_slot(slot, contributions);
230 self
231 }
232
233 pub fn with_cleared_prompt_slot(mut self, slot: crate::PromptSlot) -> Self {
234 self.env.core.prompt.prompt.clear_slot(slot);
235 self
236 }
237
238 pub fn with_prompt_layer(mut self, prompt: crate::PromptLayer) -> Self {
239 self.env.core.prompt.prompt = prompt;
240 self
241 }
242
243 pub fn with_trace_sink(mut self, sink: Option<Arc<dyn TraceSink>>) -> Self {
244 self.env.core.tracing.trace_sink = sink;
245 self
246 }
247
248 pub fn with_lashlang_execution_sink(mut self, sink: Option<Arc<dyn TraceSink>>) -> Self {
249 self.env.core.tracing.lashlang_execution_sink = sink;
250 self
251 }
252
253 pub fn with_lashlang_execution_jsonl_path(mut self, path: Option<std::path::PathBuf>) -> Self {
254 self.env.core.tracing.lashlang_execution_sink =
255 path.map(|path| Arc::new(lash_trace::JsonlTraceSink::new(path)) as Arc<dyn TraceSink>);
256 self
257 }
258
259 pub fn with_trace_level(mut self, level: TraceLevel) -> Self {
260 self.env.core.tracing.trace_level = level;
261 self
262 }
263
264 pub fn with_trace_context(mut self, context: TraceContext) -> Self {
265 self.env.core.tracing.trace_context = context;
266 self
267 }
268
269 pub fn with_termination(mut self, termination: TerminationPolicy) -> Self {
270 self.env.core.control.termination = termination;
271 self
272 }
273
274 pub fn with_effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
275 self.env.core.control.effect_host = effect_host;
276 self
277 }
278
279 pub fn with_provider_resolver(
280 mut self,
281 provider_resolver: Arc<dyn crate::RuntimeProviderResolver>,
282 ) -> Self {
283 self.env.core.providers.provider_resolver = provider_resolver;
284 self
285 }
286
287 pub fn build(self) -> RuntimeEnvironment {
288 self.env
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn builder_methods_configure_runtime_host() {
298 let attachment_store: Arc<dyn crate::AttachmentStore> =
299 Arc::new(crate::InMemoryAttachmentStore::new());
300 let effect_host: Arc<dyn EffectHost> = Arc::new(InlineEffectHost::default());
301 let trace_context = TraceContext::default().for_session("session-1");
302 let termination = TerminationPolicy {
303 treat_missing_done_as_failure: false,
304 };
305
306 let env = RuntimeEnvironment::builder()
307 .with_attachment_store(Arc::clone(&attachment_store))
308 .with_prompt_template(crate::default_prompt_template())
309 .with_trace_sink(Some(Arc::new(lash_trace::JsonlTraceSink::new(
310 std::env::temp_dir().join("lash-runtime-environment-builder-test.jsonl"),
311 ))))
312 .with_trace_level(TraceLevel::Extended)
313 .with_trace_context(trace_context.clone())
314 .with_termination(termination.clone())
315 .with_effect_host(Arc::clone(&effect_host))
316 .build();
317
318 assert!(Arc::ptr_eq(
319 &env.core.durability.attachment_store,
320 &attachment_store
321 ));
322 assert!(env.core.prompt.prompt.template.is_some());
323 assert!(env.core.tracing.trace_sink.is_some());
324 assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
325 assert_eq!(env.core.tracing.trace_context, trace_context);
326 assert_eq!(
327 env.core.control.termination.treat_missing_done_as_failure,
328 termination.treat_missing_done_as_failure
329 );
330 assert!(Arc::ptr_eq(&env.core.control.effect_host, &effect_host));
331 }
332
333 #[test]
334 fn runtime_host_config_replaces_core_config() {
335 let mut core = RuntimeHostConfig::in_memory();
336 core.tracing.trace_level = TraceLevel::Extended;
337 core.control.termination = TerminationPolicy {
338 treat_missing_done_as_failure: false,
339 };
340
341 let env = RuntimeEnvironment::builder()
342 .with_trace_level(TraceLevel::Standard)
343 .with_runtime_host_config(core)
344 .build();
345
346 assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
347 assert!(!env.core.control.termination.treat_missing_done_as_failure);
348 }
349
350 #[test]
351 fn runtime_environment_does_not_mirror_runtime_host_config_fields() {
352 let source = std::fs::read_to_string(
353 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/runtime/environment.rs"),
354 )
355 .expect("read environment source");
356 for field in [
357 ["pub ", "attachment_store:"].concat(),
358 ["pub ", "prompt:"].concat(),
359 ["pub ", "trace_sink:"].concat(),
360 ["pub ", "trace_level:"].concat(),
361 ["pub ", "trace_context:"].concat(),
362 ["pub ", "termination:"].concat(),
363 ["pub ", "effect_host:"].concat(),
364 ["mirror ", "`RuntimeHostConfig`"].concat(),
365 ] {
366 assert!(
367 !source.contains(&field),
368 "found mirrored field/comment: {field}"
369 );
370 }
371 }
372}