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 session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
84
85 pub process_work_poke: Option<super::ProcessWorkPoke>,
90
91 pub queued_work_poke: Option<super::QueuedWorkPoke>,
94
95 pub core: RuntimeHostConfig,
96}
97
98impl RuntimeEnvironment {
99 pub fn builder() -> RuntimeEnvironmentBuilder {
100 RuntimeEnvironmentBuilder::default()
101 }
102}
103
104pub struct ParkedSession {
109 pub(crate) session_id: String,
110 pub(crate) store: Arc<dyn crate::store::RuntimePersistence>,
111 pub(crate) policy: crate::SessionPolicy,
112}
113
114impl ParkedSession {
115 pub fn session_id(&self) -> &str {
116 &self.session_id
117 }
118}
119
120pub struct RuntimeEnvironmentBuilder {
122 env: RuntimeEnvironment,
123}
124
125impl Default for RuntimeEnvironmentBuilder {
126 fn default() -> Self {
127 Self {
132 env: RuntimeEnvironment {
133 plugin_host: None,
134 residency: Residency::default(),
135 process_registry: None,
136 session_store_factory: None,
137 process_work_poke: None,
138 queued_work_poke: None,
139 core: RuntimeHostConfig::in_memory(),
140 },
141 }
142 }
143}
144
145impl RuntimeEnvironmentBuilder {
146 pub fn with_plugin_host(mut self, host: Arc<crate::PluginHost>) -> Self {
147 self.env.plugin_host = Some(host);
148 self
149 }
150
151 pub fn with_residency(mut self, residency: Residency) -> Self {
152 self.env.residency = residency;
153 self
154 }
155
156 pub fn with_process_registry(mut self, process_registry: Arc<dyn ProcessRegistry>) -> Self {
157 self.env.process_registry = Some(process_registry);
158 if let Some(host) = self.env.plugin_host.take() {
159 let abilities = super::builder::lashlang_abilities_for_process_registry(
160 host.lashlang_abilities(),
161 true,
162 );
163 self.env.plugin_host = Some(Arc::new(
164 host.as_ref().clone().with_lashlang_abilities(abilities),
165 ));
166 }
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_lashlang_execution_sink(mut self, sink: Option<Arc<dyn TraceSink>>) -> Self {
240 self.env.core.tracing.lashlang_execution_sink = sink;
241 self
242 }
243
244 pub fn with_lashlang_execution_jsonl_path(mut self, path: Option<std::path::PathBuf>) -> Self {
245 self.env.core.tracing.lashlang_execution_sink =
246 path.map(|path| Arc::new(lash_trace::JsonlTraceSink::new(path)) as Arc<dyn TraceSink>);
247 self
248 }
249
250 pub fn with_trace_level(mut self, level: TraceLevel) -> Self {
251 self.env.core.tracing.trace_level = level;
252 self
253 }
254
255 pub fn with_trace_context(mut self, context: TraceContext) -> Self {
256 self.env.core.tracing.trace_context = context;
257 self
258 }
259
260 pub fn with_termination(mut self, termination: TerminationPolicy) -> Self {
261 self.env.core.control.termination = termination;
262 self
263 }
264
265 pub fn with_effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
266 self.env.core.control.effect_host = effect_host;
267 self
268 }
269
270 pub fn with_provider_resolver(
271 mut self,
272 provider_resolver: Arc<dyn crate::RuntimeProviderResolver>,
273 ) -> Self {
274 self.env.core.providers.provider_resolver = provider_resolver;
275 self
276 }
277
278 pub fn build(self) -> RuntimeEnvironment {
279 self.env
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn builder_methods_configure_runtime_host() {
289 let attachment_store: Arc<dyn crate::AttachmentStore> =
290 Arc::new(crate::InMemoryAttachmentStore::new());
291 let effect_host: Arc<dyn EffectHost> = Arc::new(InlineEffectHost::default());
292 let trace_context = TraceContext::default().for_session("session-1");
293 let termination = TerminationPolicy {
294 treat_missing_done_as_failure: false,
295 };
296
297 let env = RuntimeEnvironment::builder()
298 .with_attachment_store(Arc::clone(&attachment_store))
299 .with_prompt_template(crate::default_prompt_template())
300 .with_trace_sink(Some(Arc::new(lash_trace::JsonlTraceSink::new(
301 std::env::temp_dir().join("lash-runtime-environment-builder-test.jsonl"),
302 ))))
303 .with_trace_level(TraceLevel::Extended)
304 .with_trace_context(trace_context.clone())
305 .with_termination(termination.clone())
306 .with_effect_host(Arc::clone(&effect_host))
307 .build();
308
309 assert!(Arc::ptr_eq(
310 &env.core.durability.attachment_store,
311 &attachment_store
312 ));
313 assert!(env.core.prompt.prompt.template.is_some());
314 assert!(env.core.tracing.trace_sink.is_some());
315 assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
316 assert_eq!(env.core.tracing.trace_context, trace_context);
317 assert_eq!(
318 env.core.control.termination.treat_missing_done_as_failure,
319 termination.treat_missing_done_as_failure
320 );
321 assert!(Arc::ptr_eq(&env.core.control.effect_host, &effect_host));
322 }
323
324 #[test]
325 fn runtime_host_config_replaces_core_config() {
326 let mut core = RuntimeHostConfig::in_memory();
327 core.tracing.trace_level = TraceLevel::Extended;
328 core.control.termination = TerminationPolicy {
329 treat_missing_done_as_failure: false,
330 };
331
332 let env = RuntimeEnvironment::builder()
333 .with_trace_level(TraceLevel::Standard)
334 .with_runtime_host_config(core)
335 .build();
336
337 assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
338 assert!(!env.core.control.termination.treat_missing_done_as_failure);
339 }
340
341 #[test]
342 fn runtime_environment_does_not_mirror_runtime_host_config_fields() {
343 let source = std::fs::read_to_string(
344 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/runtime/environment.rs"),
345 )
346 .expect("read environment source");
347 for field in [
348 ["pub ", "attachment_store:"].concat(),
349 ["pub ", "prompt:"].concat(),
350 ["pub ", "trace_sink:"].concat(),
351 ["pub ", "trace_level:"].concat(),
352 ["pub ", "trace_context:"].concat(),
353 ["pub ", "termination:"].concat(),
354 ["pub ", "effect_host:"].concat(),
355 ["mirror ", "`RuntimeHostConfig`"].concat(),
356 ] {
357 assert!(
358 !source.contains(&field),
359 "found mirrored field/comment: {field}"
360 );
361 }
362 }
363}