Skip to main content

lash_core/runtime/session_manager/
direct.rs

1use super::*;
2
3/// Runtime-backed direct completion source.
4///
5/// Carries everything needed to plan and journal a direct LLM effect against
6/// the owning session manager.
7#[derive(Clone)]
8struct RuntimeDirectSource<'run> {
9    manager: Arc<RuntimeSessionServices>,
10    effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
11    turn_id: Option<String>,
12}
13
14#[cfg(any(test, feature = "testing"))]
15type TestDirectFn = Arc<
16    dyn Fn(crate::DirectRequest, String) -> Result<crate::DirectCompletion, crate::PluginError>
17        + Send
18        + Sync,
19>;
20
21/// Source of direct (single-shot) LLM completions for plugins and tools.
22///
23/// In production this is always backed by the runtime session manager; the
24/// test/testing variants exist only so that out-of-runtime test harnesses can
25/// inject a canned completion without standing up a full runtime.
26#[derive(Clone)]
27enum DirectCompletionSource<'run> {
28    Runtime(RuntimeDirectSource<'run>),
29    #[cfg(any(test, feature = "testing"))]
30    Unavailable(String),
31    #[cfg(any(test, feature = "testing"))]
32    TestFn(TestDirectFn),
33}
34
35#[derive(Clone)]
36pub struct DirectCompletionClient<'run> {
37    source: DirectCompletionSource<'run>,
38}
39
40impl<'run> DirectCompletionClient<'run> {
41    pub(super) fn runtime(
42        manager: Arc<RuntimeSessionServices>,
43        effect_controller: crate::runtime::RuntimeEffectControllerHandle<'run>,
44        turn_id: Option<String>,
45    ) -> Self {
46        Self {
47            source: DirectCompletionSource::Runtime(RuntimeDirectSource {
48                manager,
49                effect_controller,
50                turn_id,
51            }),
52        }
53    }
54
55    pub async fn direct_completion(
56        &self,
57        request: crate::DirectRequest,
58        usage_source: &str,
59    ) -> Result<crate::DirectCompletion, crate::PluginError> {
60        self.direct_completion_at(request, usage_source, DirectExecutionPosition::Independent)
61            .await
62    }
63
64    pub(crate) async fn direct_completion_for_tool(
65        &self,
66        request: crate::DirectRequest,
67        usage_source: &str,
68        parent_invocation: Option<&crate::RuntimeInvocation>,
69    ) -> Result<crate::DirectCompletion, crate::PluginError> {
70        let position = if parent_invocation.is_some_and(|invocation| {
71            invocation.effect_kind() == Some(crate::RuntimeEffectKind::ToolAttempt)
72        }) {
73            DirectExecutionPosition::ToolAttempt
74        } else {
75            DirectExecutionPosition::Independent
76        };
77        self.direct_completion_at(request, usage_source, position)
78            .await
79    }
80
81    async fn direct_completion_at(
82        &self,
83        request: crate::DirectRequest,
84        usage_source: &str,
85        position: DirectExecutionPosition,
86    ) -> Result<crate::DirectCompletion, crate::PluginError> {
87        match &self.source {
88            DirectCompletionSource::Runtime(source) => {
89                source
90                    .manager
91                    .direct
92                    .invoke_direct_completion(
93                        source.invocation_context(position),
94                        request,
95                        usage_source,
96                    )
97                    .await
98            }
99            #[cfg(any(test, feature = "testing"))]
100            DirectCompletionSource::Unavailable(message) => {
101                Err(crate::PluginError::Session(message.clone()))
102            }
103            #[cfg(any(test, feature = "testing"))]
104            DirectCompletionSource::TestFn(invoke) => invoke(request, usage_source.to_string()),
105        }
106    }
107
108    pub async fn direct_llm_completion(
109        &self,
110        request: crate::LlmRequest,
111        usage_source: &str,
112    ) -> Result<crate::DirectLlmCompletion, crate::PluginError> {
113        match &self.source {
114            DirectCompletionSource::Runtime(source) => {
115                source
116                    .manager
117                    .direct
118                    .invoke_direct_llm_completion(
119                        source.invocation_context(DirectExecutionPosition::Independent),
120                        request,
121                        usage_source,
122                    )
123                    .await
124            }
125            #[cfg(any(test, feature = "testing"))]
126            DirectCompletionSource::Unavailable(message) => {
127                Err(crate::PluginError::Session(message.clone()))
128            }
129            #[cfg(any(test, feature = "testing"))]
130            DirectCompletionSource::TestFn(_) => Err(crate::PluginError::Session(
131                "direct LLM completions are unavailable in this test context".to_string(),
132            )),
133        }
134    }
135
136    #[cfg(any(test, feature = "testing"))]
137    pub(crate) fn unavailable(message: impl Into<String>) -> Self {
138        Self {
139            source: DirectCompletionSource::Unavailable(message.into()),
140        }
141    }
142
143    #[cfg(any(test, feature = "testing"))]
144    pub fn from_fn<F>(invoke: F) -> Self
145    where
146        F: Fn(crate::DirectRequest, String) -> Result<crate::DirectCompletion, crate::PluginError>
147            + Send
148            + Sync
149            + 'static,
150    {
151        Self {
152            source: DirectCompletionSource::TestFn(Arc::new(invoke)),
153        }
154    }
155}
156
157impl<'run> RuntimeDirectSource<'run> {
158    fn invocation_context(&self, position: DirectExecutionPosition) -> DirectInvocationContext<'_> {
159        DirectInvocationContext {
160            current: &self.manager.current,
161            usage_capability: &self.manager.usage,
162            effect_controller: self.effect_controller.controller(),
163            turn_id: self.turn_id.as_deref(),
164            position,
165        }
166    }
167}
168
169pub(in crate::runtime::session_manager) struct DirectInvocationContext<'a> {
170    current: &'a CurrentSessionCapability,
171    usage_capability: &'a UsageCapability,
172    effect_controller: &'a dyn crate::RuntimeEffectController,
173    turn_id: Option<&'a str>,
174    position: DirectExecutionPosition,
175}
176
177struct DirectEffectPlan {
178    provider: crate::ProviderHandle,
179    envelope: crate::RuntimeEffectEnvelope,
180    request: Box<crate::LlmRequest>,
181    usage_source: String,
182}
183
184#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
185enum DirectExecutionPosition {
186    #[default]
187    Independent,
188    ToolAttempt,
189}
190
191impl DirectCompletionCapability {
192    /// Plans a single direct LLM effect from a normalized [`crate::LlmRequest`].
193    ///
194    /// Both the text-only (`DirectRequest`) and full-output entry points feed
195    /// the same effect lane; they differ only in how the caller projects the
196    /// resulting [`crate::LlmResponse`].
197    async fn plan_direct_effect(
198        &self,
199        context: &DirectInvocationContext<'_>,
200        provider: crate::ProviderHandle,
201        request: crate::LlmRequest,
202        usage_source: &str,
203        replay: Option<&crate::RuntimeReplay>,
204        caused_by: Option<&crate::CausalRef>,
205    ) -> Result<DirectEffectPlan, crate::PluginError> {
206        let current = context.current;
207        let usage_source = usage_source.to_string();
208        let request_spec = crate::LlmRequestSpec::from_request(
209            &request,
210            current.host.core.durability.attachment_store.as_ref(),
211        )
212        .await?;
213        let discriminator =
214            crate::runtime::causal::direct_request_discriminator(&request_spec, replay, caused_by)?;
215        let invocation = crate::runtime::causal::direct_effect_invocation(
216            &current.session_id,
217            &usage_source,
218            discriminator,
219            context.turn_id,
220            caused_by.cloned(),
221        );
222        let envelope = crate::RuntimeEffectEnvelope::new(
223            invocation,
224            crate::RuntimeEffectCommand::Direct {
225                request: Box::new(request_spec),
226                usage_source: usage_source.clone(),
227            },
228        );
229        Ok(DirectEffectPlan {
230            provider,
231            envelope,
232            request: Box::new(request),
233            usage_source,
234        })
235    }
236
237    /// Runs a planned direct effect across the journal/controller boundary and
238    /// applies usage/trace bookkeeping, yielding the raw provider response.
239    async fn run_direct_effect(
240        &self,
241        context: DirectInvocationContext<'_>,
242        plan: DirectEffectPlan,
243        caused_by: Option<crate::CausalRef>,
244    ) -> Result<(crate::LlmResponse, crate::TokenUsage, crate::LlmCallRecord), crate::PluginError>
245    {
246        let current = context.current;
247        let DirectEffectPlan {
248            provider,
249            envelope,
250            request,
251            usage_source,
252        } = plan;
253        let local_executor = crate::RuntimeEffectLocalExecutor::direct(
254            provider,
255            Arc::clone(&current.host.core.durability.attachment_store),
256        );
257        let outcome = match context.position {
258            DirectExecutionPosition::Independent => {
259                context
260                    .effect_controller
261                    .execute_effect(envelope, local_executor)
262                    .await?
263            }
264            DirectExecutionPosition::ToolAttempt => local_executor.execute(envelope).await?,
265        };
266        crate::runtime::effect::apply_direct_outcome(
267            current,
268            context.usage_capability,
269            &request,
270            &usage_source,
271            caused_by.as_ref(),
272            outcome,
273        )
274        .await
275    }
276
277    pub(in crate::runtime::session_manager) async fn invoke_direct_completion(
278        &self,
279        context: DirectInvocationContext<'_>,
280        request: crate::DirectRequest,
281        usage_source: &str,
282    ) -> Result<crate::DirectCompletion, crate::PluginError> {
283        let resolved = context.current.resolve_policy()?;
284        let provider = resolved.provider().clone();
285        let mut request = request;
286        let model = request.model.clone();
287        // Validate against the capability carried by the request and write the
288        // resolved (alias-normalized) effort back before the provider sees it.
289        request.model_variant = request
290            .model_capability
291            .validate_selection(&model, provider.kind(), &request.model_variant)
292            .map_err(|error| crate::PluginError::Session(error.message))?;
293        let replay = request.replay.clone();
294        let caused_by = request.caused_by.clone();
295        let normalized = crate::direct::build_llm_request(&provider, request, model);
296        let plan = self
297            .plan_direct_effect(
298                &context,
299                provider,
300                normalized,
301                usage_source,
302                replay.as_ref(),
303                caused_by.as_ref(),
304            )
305            .await?;
306        let (response, usage, llm_call) = self.run_direct_effect(context, plan, caused_by).await?;
307        Ok(crate::DirectCompletion {
308            text: response.full_text,
309            usage,
310            llm_call,
311        })
312    }
313
314    pub(in crate::runtime::session_manager) async fn invoke_direct_llm_completion(
315        &self,
316        context: DirectInvocationContext<'_>,
317        request: crate::LlmRequest,
318        usage_source: &str,
319    ) -> Result<crate::DirectLlmCompletion, crate::PluginError> {
320        let resolved = context.current.resolve_policy()?;
321        let plan = self
322            .plan_direct_effect(
323                &context,
324                resolved.binding.provider,
325                request,
326                usage_source,
327                None,
328                None,
329            )
330            .await?;
331        let (response, usage, llm_call) = self.run_direct_effect(context, plan, None).await?;
332        Ok(crate::DirectLlmCompletion {
333            response,
334            usage,
335            llm_call,
336        })
337    }
338}