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(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 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 ¤t.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 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 = ¤t.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(¤t.host.core.clock),
289 );
290 let local_executor = crate::RuntimeEffectLocalExecutor::direct(
291 provider,
292 Arc::clone(¤t.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 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}