use std::sync::Arc;
use lash_trace::{TraceContext, TraceLevel, TraceSink};
#[cfg(test)]
use super::InlineEffectHost;
use super::process::ProcessRegistry;
use super::{EffectHost, RuntimeHostConfig, TerminationPolicy};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Residency {
#[default]
KeepAll,
ActivePathOnly,
}
#[derive(Clone)]
pub struct RuntimeEnvironment {
pub plugin_host: Option<Arc<crate::PluginHost>>,
pub residency: Residency,
pub process_registry: Option<Arc<dyn ProcessRegistry>>,
pub host_event_store: Option<Arc<dyn crate::HostEventStore>>,
pub session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
pub process_work_poke: Option<super::ProcessWorkPoke>,
pub queued_work_poke: Option<super::QueuedWorkPoke>,
pub core: RuntimeHostConfig,
}
impl RuntimeEnvironment {
pub fn builder() -> RuntimeEnvironmentBuilder {
RuntimeEnvironmentBuilder::default()
}
}
pub struct ParkedSession {
pub(crate) session_id: String,
pub(crate) store: Arc<dyn crate::store::RuntimePersistence>,
pub(crate) policy: crate::SessionPolicy,
}
impl ParkedSession {
pub fn session_id(&self) -> &str {
&self.session_id
}
}
pub struct RuntimeEnvironmentBuilder {
env: RuntimeEnvironment,
}
impl Default for RuntimeEnvironmentBuilder {
fn default() -> Self {
Self {
env: RuntimeEnvironment {
plugin_host: None,
residency: Residency::default(),
process_registry: None,
host_event_store: Some(Arc::new(crate::InMemoryHostEventStore::default())),
session_store_factory: None,
process_work_poke: None,
queued_work_poke: None,
core: RuntimeHostConfig::in_memory(),
},
}
}
}
impl RuntimeEnvironmentBuilder {
pub fn with_plugin_host(mut self, host: Arc<crate::PluginHost>) -> Self {
self.env.plugin_host = Some(host);
self
}
pub fn with_residency(mut self, residency: Residency) -> Self {
self.env.residency = residency;
self
}
pub fn with_process_registry(mut self, process_registry: Arc<dyn ProcessRegistry>) -> Self {
self.env.process_registry = Some(process_registry);
if let Some(host) = self.env.plugin_host.take() {
let abilities = super::builder::lashlang_abilities_for_process_registry(
host.lashlang_abilities(),
true,
);
self.env.plugin_host = Some(Arc::new(
host.as_ref().clone().with_lashlang_abilities(abilities),
));
}
self
}
pub fn with_host_event_store(mut self, store: Arc<dyn crate::HostEventStore>) -> Self {
self.env.host_event_store = Some(store);
self
}
pub fn with_session_store_factory(
mut self,
factory: Arc<dyn crate::SessionStoreFactory>,
) -> Self {
self.env.session_store_factory = Some(factory);
self
}
pub fn with_process_work_poke(mut self, poke: super::ProcessWorkPoke) -> Self {
self.env.process_work_poke = Some(poke);
self
}
pub fn with_queued_work_poke(mut self, poke: super::QueuedWorkPoke) -> Self {
self.env.queued_work_poke = Some(poke);
self
}
pub fn with_runtime_host_config(mut self, core: RuntimeHostConfig) -> Self {
self.env.core = core;
self
}
pub fn with_attachment_store(mut self, store: Arc<dyn crate::AttachmentStore>) -> Self {
self.env.core.durability.attachment_store = store;
self
}
pub fn with_prompt_template(mut self, template: crate::PromptTemplate) -> Self {
self.env.core.prompt.prompt.template = Some(template);
self
}
pub fn with_prompt_contribution(mut self, contribution: crate::PromptContribution) -> Self {
self.env.core.prompt.prompt.add_contribution(contribution);
self
}
pub fn with_replaced_prompt_slot(
mut self,
slot: crate::PromptSlot,
contributions: impl IntoIterator<Item = crate::PromptContribution>,
) -> Self {
self.env
.core
.prompt
.prompt
.replace_slot(slot, contributions);
self
}
pub fn with_cleared_prompt_slot(mut self, slot: crate::PromptSlot) -> Self {
self.env.core.prompt.prompt.clear_slot(slot);
self
}
pub fn with_prompt_layer(mut self, prompt: crate::PromptLayer) -> Self {
self.env.core.prompt.prompt = prompt;
self
}
pub fn with_trace_sink(mut self, sink: Option<Arc<dyn TraceSink>>) -> Self {
self.env.core.tracing.trace_sink = sink;
self
}
pub fn with_lashlang_execution_sink(mut self, sink: Option<Arc<dyn TraceSink>>) -> Self {
self.env.core.tracing.lashlang_execution_sink = sink;
self
}
pub fn with_lashlang_execution_jsonl_path(mut self, path: Option<std::path::PathBuf>) -> Self {
self.env.core.tracing.lashlang_execution_sink =
path.map(|path| Arc::new(lash_trace::JsonlTraceSink::new(path)) as Arc<dyn TraceSink>);
self
}
pub fn with_trace_level(mut self, level: TraceLevel) -> Self {
self.env.core.tracing.trace_level = level;
self
}
pub fn with_trace_context(mut self, context: TraceContext) -> Self {
self.env.core.tracing.trace_context = context;
self
}
pub fn with_termination(mut self, termination: TerminationPolicy) -> Self {
self.env.core.control.termination = termination;
self
}
pub fn with_effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
self.env.core.control.effect_host = effect_host;
self
}
pub fn with_provider_resolver(
mut self,
provider_resolver: Arc<dyn crate::RuntimeProviderResolver>,
) -> Self {
self.env.core.providers.provider_resolver = provider_resolver;
self
}
pub fn build(self) -> RuntimeEnvironment {
self.env
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_methods_configure_runtime_host() {
let attachment_store: Arc<dyn crate::AttachmentStore> =
Arc::new(crate::InMemoryAttachmentStore::new());
let effect_host: Arc<dyn EffectHost> = Arc::new(InlineEffectHost::default());
let trace_context = TraceContext::default().for_session("session-1");
let termination = TerminationPolicy {
treat_missing_done_as_failure: false,
};
let env = RuntimeEnvironment::builder()
.with_attachment_store(Arc::clone(&attachment_store))
.with_prompt_template(crate::default_prompt_template())
.with_trace_sink(Some(Arc::new(lash_trace::JsonlTraceSink::new(
std::env::temp_dir().join("lash-runtime-environment-builder-test.jsonl"),
))))
.with_trace_level(TraceLevel::Extended)
.with_trace_context(trace_context.clone())
.with_termination(termination.clone())
.with_effect_host(Arc::clone(&effect_host))
.build();
assert!(Arc::ptr_eq(
&env.core.durability.attachment_store,
&attachment_store
));
assert!(env.core.prompt.prompt.template.is_some());
assert!(env.core.tracing.trace_sink.is_some());
assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
assert_eq!(env.core.tracing.trace_context, trace_context);
assert_eq!(
env.core.control.termination.treat_missing_done_as_failure,
termination.treat_missing_done_as_failure
);
assert!(Arc::ptr_eq(&env.core.control.effect_host, &effect_host));
}
#[test]
fn runtime_host_config_replaces_core_config() {
let mut core = RuntimeHostConfig::in_memory();
core.tracing.trace_level = TraceLevel::Extended;
core.control.termination = TerminationPolicy {
treat_missing_done_as_failure: false,
};
let env = RuntimeEnvironment::builder()
.with_trace_level(TraceLevel::Standard)
.with_runtime_host_config(core)
.build();
assert_eq!(env.core.tracing.trace_level, TraceLevel::Extended);
assert!(!env.core.control.termination.treat_missing_done_as_failure);
}
#[test]
fn runtime_environment_does_not_mirror_runtime_host_config_fields() {
let source = std::fs::read_to_string(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/runtime/environment.rs"),
)
.expect("read environment source");
for field in [
["pub ", "attachment_store:"].concat(),
["pub ", "prompt:"].concat(),
["pub ", "trace_sink:"].concat(),
["pub ", "trace_level:"].concat(),
["pub ", "trace_context:"].concat(),
["pub ", "termination:"].concat(),
["pub ", "effect_host:"].concat(),
["mirror ", "`RuntimeHostConfig`"].concat(),
] {
assert!(
!source.contains(&field),
"found mirrored field/comment: {field}"
);
}
}
}