1use super::*;
2
3#[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#[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 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 ¤t.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 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(¤t.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 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}