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(crate) fn to_static(&self) -> Option<DirectCompletionClient<'static>> {
56        let source = match &self.source {
57            DirectCompletionSource::Runtime(source) => {
58                DirectCompletionSource::Runtime(RuntimeDirectSource {
59                    manager: Arc::clone(&source.manager),
60                    effect_controller: source.effect_controller.to_static()?,
61                    turn_id: source.turn_id.clone(),
62                })
63            }
64            #[cfg(any(test, feature = "testing"))]
65            DirectCompletionSource::Unavailable(message) => {
66                DirectCompletionSource::Unavailable(message.clone())
67            }
68            #[cfg(any(test, feature = "testing"))]
69            DirectCompletionSource::TestFn(invoke) => {
70                DirectCompletionSource::TestFn(Arc::clone(invoke))
71            }
72        };
73        Some(DirectCompletionClient { source })
74    }
75
76    pub async fn direct_completion(
77        &self,
78        request: crate::DirectRequest,
79        usage_source: &str,
80    ) -> Result<crate::DirectCompletion, crate::PluginError> {
81        self.direct_completion_at(request, usage_source, DirectExecutionPosition::Independent)
82            .await
83    }
84
85    pub(crate) async fn direct_completion_for_tool(
86        &self,
87        request: crate::DirectRequest,
88        usage_source: &str,
89        parent_invocation: Option<&crate::RuntimeInvocation>,
90    ) -> Result<crate::DirectCompletion, crate::PluginError> {
91        let position = if parent_invocation.is_some_and(|invocation| {
92            invocation.effect_kind() == Some(crate::RuntimeEffectKind::ToolAttempt)
93        }) {
94            DirectExecutionPosition::ToolAttempt
95        } else {
96            DirectExecutionPosition::Independent
97        };
98        self.direct_completion_at(request, usage_source, position)
99            .await
100    }
101
102    async fn direct_completion_at(
103        &self,
104        request: crate::DirectRequest,
105        usage_source: &str,
106        position: DirectExecutionPosition,
107    ) -> Result<crate::DirectCompletion, crate::PluginError> {
108        match &self.source {
109            DirectCompletionSource::Runtime(source) => {
110                source
111                    .manager
112                    .direct
113                    .invoke_direct_completion(
114                        source.invocation_context(position),
115                        request,
116                        usage_source,
117                    )
118                    .await
119            }
120            #[cfg(any(test, feature = "testing"))]
121            DirectCompletionSource::Unavailable(message) => {
122                Err(crate::PluginError::Session(message.clone()))
123            }
124            #[cfg(any(test, feature = "testing"))]
125            DirectCompletionSource::TestFn(invoke) => invoke(request, usage_source.to_string()),
126        }
127    }
128
129    pub async fn direct_llm_completion(
130        &self,
131        request: crate::LlmRequest,
132        usage_source: &str,
133    ) -> Result<crate::DirectLlmCompletion, crate::PluginError> {
134        match &self.source {
135            DirectCompletionSource::Runtime(source) => {
136                source
137                    .manager
138                    .direct
139                    .invoke_direct_llm_completion(
140                        source.invocation_context(DirectExecutionPosition::Independent),
141                        request,
142                        usage_source,
143                    )
144                    .await
145            }
146            #[cfg(any(test, feature = "testing"))]
147            DirectCompletionSource::Unavailable(message) => {
148                Err(crate::PluginError::Session(message.clone()))
149            }
150            #[cfg(any(test, feature = "testing"))]
151            DirectCompletionSource::TestFn(_) => Err(crate::PluginError::Session(
152                "direct LLM completions are unavailable in this test context".to_string(),
153            )),
154        }
155    }
156
157    #[cfg(any(test, feature = "testing"))]
158    pub(crate) fn unavailable(message: impl Into<String>) -> Self {
159        Self {
160            source: DirectCompletionSource::Unavailable(message.into()),
161        }
162    }
163
164    #[cfg(any(test, feature = "testing"))]
165    pub fn from_fn<F>(invoke: F) -> Self
166    where
167        F: Fn(crate::DirectRequest, String) -> Result<crate::DirectCompletion, crate::PluginError>
168            + Send
169            + Sync
170            + 'static,
171    {
172        Self {
173            source: DirectCompletionSource::TestFn(Arc::new(invoke)),
174        }
175    }
176}
177
178impl<'run> RuntimeDirectSource<'run> {
179    fn invocation_context(&self, position: DirectExecutionPosition) -> DirectInvocationContext<'_> {
180        DirectInvocationContext {
181            current: &self.manager.current,
182            usage_capability: &self.manager.usage,
183            effect_controller: self.effect_controller.controller(),
184            turn_id: self.turn_id.as_deref(),
185            position,
186        }
187    }
188}
189
190pub(in crate::runtime::session_manager) struct DirectInvocationContext<'a> {
191    current: &'a CurrentSessionCapability,
192    usage_capability: &'a UsageCapability,
193    effect_controller: &'a dyn crate::RuntimeEffectController,
194    turn_id: Option<&'a str>,
195    position: DirectExecutionPosition,
196}
197
198struct DirectEffectPlan {
199    provider: crate::ProviderHandle,
200    envelope: crate::RuntimeEffectEnvelope,
201    request: Box<crate::LlmRequest>,
202    usage_source: String,
203}
204
205#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
206enum DirectExecutionPosition {
207    #[default]
208    Independent,
209    ToolAttempt,
210}
211
212impl DirectCompletionCapability {
213    /// Plans a single direct LLM effect from a normalized [`crate::LlmRequest`].
214    ///
215    /// Both the text-only (`DirectRequest`) and full-output entry points feed
216    /// the same effect lane; they differ only in how the caller projects the
217    /// resulting [`crate::LlmResponse`].
218    async fn plan_direct_effect(
219        &self,
220        context: &DirectInvocationContext<'_>,
221        provider: crate::ProviderHandle,
222        request: crate::LlmRequest,
223        usage_source: &str,
224        replay: Option<&crate::RuntimeReplay>,
225        caused_by: Option<&crate::CausalRef>,
226    ) -> Result<DirectEffectPlan, crate::PluginError> {
227        let current = context.current;
228        let usage_source = usage_source.to_string();
229        for source in &request.attachments {
230            current
231                .host
232                .core
233                .attachment_source_policy
234                .authorize(&crate::AttachmentProducer::Host, source)
235                .map_err(|err| crate::PluginError::Session(err.to_string()))?;
236        }
237        let request_spec = crate::LlmRequestSpec::from_request(
238            &request,
239            current.host.core.durability.attachment_store.as_ref(),
240        )
241        .await?;
242        let discriminator =
243            crate::runtime::causal::direct_request_discriminator(&request_spec, replay, caused_by)?;
244        let invocation = crate::runtime::causal::direct_effect_invocation(
245            &current.session_id,
246            &usage_source,
247            discriminator,
248            context.turn_id,
249            caused_by.cloned(),
250        );
251        let envelope = crate::RuntimeEffectEnvelope::new(
252            invocation,
253            crate::RuntimeEffectCommand::Direct {
254                request: Box::new(request_spec),
255                usage_source: usage_source.clone(),
256            },
257        );
258        Ok(DirectEffectPlan {
259            provider,
260            envelope,
261            request: Box::new(request),
262            usage_source,
263        })
264    }
265
266    /// Runs a planned direct effect across the journal/controller boundary and
267    /// applies usage/trace bookkeeping, yielding the raw provider response.
268    async fn run_direct_effect(
269        &self,
270        context: DirectInvocationContext<'_>,
271        plan: DirectEffectPlan,
272        caused_by: Option<crate::CausalRef>,
273    ) -> Result<(crate::LlmResponse, crate::TokenUsage, crate::LlmCallRecord), crate::PluginError>
274    {
275        let current = context.current;
276        let DirectEffectPlan {
277            provider,
278            envelope,
279            request,
280            usage_source,
281        } = plan;
282        let tracing = &current.host.core.tracing;
283        let replay_trace = crate::RuntimeEffectReplayTrace::gated(
284            tracing.trace_level,
285            tracing.trace_sink.as_ref(),
286            tracing.trace_context.clone(),
287            crate::trace::trace_context_from_invocation(&envelope.invocation),
288            Arc::clone(&current.host.core.clock),
289        );
290        let local_executor = crate::RuntimeEffectLocalExecutor::direct(
291            provider,
292            Arc::clone(&current.host.core.durability.attachment_store),
293            replay_trace,
294        );
295        let outcome = match context.position {
296            DirectExecutionPosition::Independent => {
297                context
298                    .effect_controller
299                    .execute_effect(envelope, local_executor)
300                    .await?
301            }
302            DirectExecutionPosition::ToolAttempt => local_executor.execute(envelope).await?,
303        };
304        crate::runtime::effect::apply_direct_outcome(
305            current,
306            context.usage_capability,
307            &request,
308            &usage_source,
309            caused_by.as_ref(),
310            outcome,
311        )
312        .await
313    }
314
315    pub(in crate::runtime::session_manager) async fn invoke_direct_completion(
316        &self,
317        context: DirectInvocationContext<'_>,
318        request: crate::DirectRequest,
319        usage_source: &str,
320    ) -> Result<crate::DirectCompletion, crate::PluginError> {
321        let resolved = context.current.resolve_policy()?;
322        let provider = resolved.provider().clone();
323        let mut request = request;
324        let model = request.model.clone();
325        // Validate against the capability carried by the request and write the
326        // resolved (alias-normalized) effort back before the provider sees it.
327        request.model_variant = request
328            .model_capability
329            .validate_selection(&model, provider.kind(), &request.model_variant)
330            .map_err(|error| crate::PluginError::Session(error.message))?;
331        let replay = request.replay.clone();
332        let caused_by = request.caused_by.clone();
333        let normalized = crate::direct::build_llm_request(&provider, request, model);
334        let plan = self
335            .plan_direct_effect(
336                &context,
337                provider,
338                normalized,
339                usage_source,
340                replay.as_ref(),
341                caused_by.as_ref(),
342            )
343            .await?;
344        let (response, usage, llm_call) = self.run_direct_effect(context, plan, caused_by).await?;
345        Ok(crate::DirectCompletion {
346            text: response.full_text,
347            usage,
348            llm_call,
349        })
350    }
351
352    pub(in crate::runtime::session_manager) async fn invoke_direct_llm_completion(
353        &self,
354        context: DirectInvocationContext<'_>,
355        request: crate::LlmRequest,
356        usage_source: &str,
357    ) -> Result<crate::DirectLlmCompletion, crate::PluginError> {
358        let resolved = context.current.resolve_policy()?;
359        let plan = self
360            .plan_direct_effect(
361                &context,
362                resolved.binding.provider,
363                request,
364                usage_source,
365                None,
366                None,
367            )
368            .await?;
369        let (response, usage, llm_call) = self.run_direct_effect(context, plan, None).await?;
370        Ok(crate::DirectLlmCompletion {
371            response,
372            usage,
373            llm_call,
374        })
375    }
376}