Skip to main content

lash_core/runtime/
config_ops.rs

1//! `LashRuntime` configuration mutators: provider, model spec, session id,
2//! and tool-catalog refresh.
3//!
4//! Extracted from `runtime/mod.rs`. This file re-opens `impl LashRuntime`;
5//! no types live here and no public API is changed.
6
7use crate::SessionError;
8use crate::provider::ProviderHandle;
9
10use super::LashRuntime;
11
12impl LashRuntime {
13    /// Update model spec on the runtime config.
14    pub fn set_model(&mut self, model: crate::ModelSpec) {
15        self.policy.model = model;
16        self.state.policy.model = self.policy.model.clone();
17        if let Some(frame) = self.state.current_agent_frame_mut() {
18            frame.assignment.policy.model = self.policy.model.clone();
19        }
20    }
21
22    /// Update provider on the runtime config.
23    pub fn set_provider(&mut self, provider: ProviderHandle) {
24        self.host.core.providers.provider_resolver =
25            std::sync::Arc::new(crate::SingleProviderResolver::new(provider.clone()));
26        self.policy.provider_id = provider.kind().to_string();
27        self.state.policy.provider_id = self.policy.provider_id.clone();
28        if let Some(frame) = self.state.current_agent_frame_mut() {
29            frame.assignment.policy.provider_id = self.policy.provider_id.clone();
30        }
31    }
32
33    /// Update session ID metadata on the runtime config.
34    pub fn set_session_id(&mut self, session_id: Option<String>) {
35        self.policy.session_id = session_id;
36        self.state.policy.session_id = self.policy.session_id.clone();
37        if let Some(frame) = self.state.current_agent_frame_mut() {
38            frame.assignment.policy.session_id = self.policy.session_id.clone();
39        }
40    }
41
42    pub async fn update_session_config(
43        &mut self,
44        provider: Option<ProviderHandle>,
45        model: Option<crate::ModelSpec>,
46        prompt: Option<crate::PromptLayer>,
47    ) {
48        let previous = self.session_policy();
49        if let Some(provider) = provider {
50            self.set_provider(provider);
51        }
52        if let Some(model) = model {
53            self.policy.model = model;
54        }
55        if let Some(prompt) = prompt {
56            self.policy.prompt = prompt;
57        }
58        self.state.policy = self.policy.clone();
59        if let Some(frame) = self.state.current_agent_frame_mut() {
60            frame.assignment.policy = self.policy.clone();
61        }
62        self.apply_session_config_mutations(previous.clone()).await;
63        self.notify_session_config_changed(previous).await;
64    }
65
66    pub async fn set_prompt_template(&mut self, template: crate::PromptTemplate) {
67        let mut prompt = self.policy.prompt.clone();
68        prompt.template = Some(template);
69        self.update_session_config(None, None, Some(prompt)).await;
70    }
71
72    pub async fn clear_prompt_template(&mut self) {
73        let mut prompt = self.policy.prompt.clone();
74        prompt.template = None;
75        self.update_session_config(None, None, Some(prompt)).await;
76    }
77
78    pub async fn add_prompt_contribution(&mut self, contribution: crate::PromptContribution) {
79        let mut prompt = self.policy.prompt.clone();
80        prompt.add_contribution(contribution);
81        self.update_session_config(None, None, Some(prompt)).await;
82    }
83
84    pub async fn replace_prompt_slot(
85        &mut self,
86        slot: crate::PromptSlot,
87        contributions: impl IntoIterator<Item = crate::PromptContribution>,
88    ) {
89        let mut prompt = self.policy.prompt.clone();
90        prompt.replace_slot(slot, contributions);
91        self.update_session_config(None, None, Some(prompt)).await;
92    }
93
94    pub async fn clear_prompt_slot(&mut self, slot: crate::PromptSlot) {
95        let mut prompt = self.policy.prompt.clone();
96        prompt.clear_slot(slot);
97        self.update_session_config(None, None, Some(prompt)).await;
98    }
99
100    /// Re-register the current tool catalog in the live protocol session.
101    pub async fn refresh_session_tool_catalog(&mut self) -> Result<(), SessionError> {
102        let Some(session) = self.session.as_mut() else {
103            return Err(SessionError::Protocol(
104                "runtime session not available".to_string(),
105            ));
106        };
107        session
108            .plugins()
109            .tool_registry()
110            .refresh_sources()
111            .map_err(|err| SessionError::Protocol(format!("tool refresh failed: {err}")))?;
112        session.refresh_tool_catalog().await?;
113        self.stamp_live_plugin_state();
114        Ok(())
115    }
116
117    pub async fn apply_tool_state(
118        &mut self,
119        snapshot: crate::ToolState,
120    ) -> Result<u64, SessionError> {
121        let Some(session) = self.session.as_mut() else {
122            return Err(SessionError::Protocol(
123                "runtime session not available".to_string(),
124            ));
125        };
126        let generation = session
127            .plugins()
128            .tool_registry()
129            .apply_state(snapshot)
130            .map_err(|err| SessionError::Protocol(format!("tool reconfigure failed: {err}")))?;
131        session.refresh_tool_catalog().await?;
132        self.stamp_live_plugin_state();
133        Ok(generation)
134    }
135
136    /// Restore a persisted tool-state snapshot over the live source surface.
137    ///
138    /// Unlike [`apply_tool_state`](Self::apply_tool_state) — a generation-checked
139    /// delta that requires the snapshot to match the current generation and
140    /// bumps it — this adopts the persisted generation when the reconciled
141    /// surface is unchanged. A live-surface change bumps once, marking the
142    /// snapshot dirty for the next commit. A cold resume whose surface reached
143    /// generation ≥ 2 still succeeds because this is not a delta apply onto the
144    /// fresh base-1 registry.
145    ///
146    /// Persisted tools that no registered source resolves become orphans
147    /// (kept as non-members, rebound when their source returns) and are listed
148    /// in the returned [`crate::ToolRestoreReport`].
149    pub async fn restore_tool_state(
150        &mut self,
151        snapshot: crate::ToolState,
152    ) -> Result<crate::ToolRestoreReport, SessionError> {
153        let Some(session) = self.session.as_mut() else {
154            return Err(SessionError::Protocol(
155                "runtime session not available".to_string(),
156            ));
157        };
158        let report = session
159            .plugins()
160            .tool_registry()
161            .restore_state(snapshot)
162            .map_err(|err| SessionError::Protocol(format!("tool restore failed: {err}")))?;
163        if !report.orphaned.is_empty() {
164            tracing::warn!(
165                orphaned = ?report.orphaned,
166                "tool state restored with orphaned tools: no registered source \
167                 resolves them; they remain non-members until their source returns"
168            );
169        }
170        session.refresh_tool_catalog().await?;
171        self.stamp_live_plugin_state();
172        Ok(report)
173    }
174}