Skip to main content

lash_core/runtime/effect/
executor.rs

1use std::collections::HashMap;
2use std::pin::Pin;
3use std::sync::Arc;
4use std::time::Instant;
5
6use serde::{Deserialize, Serialize};
7use tokio::sync::mpsc;
8use tokio_util::sync::CancellationToken;
9
10use crate::LlmRequest as CoreLlmRequest;
11use crate::ProcessRecord;
12use crate::ProcessRegistry;
13use crate::provider::ProviderHandle;
14use crate::runtime::{RuntimeStreamEvent, RuntimeTurnDriver};
15use crate::sansio::LlmCallError;
16use crate::{PluginError, RuntimeError, RuntimeErrorCode};
17
18use super::envelope::{
19    ProcessCommand, ProcessEffectOutcome, RuntimeDirectLlmOutcome, RuntimeEffectCommand,
20    RuntimeEffectEnvelope, RuntimeEffectKind, RuntimeEffectOutcome,
21};
22use super::outcome::llm_call_error_from_transport;
23
24/// Host controls attached to one external event wait.
25///
26/// Durable effect controllers consume these controls and translate them to
27/// their engine-native cancellation and timer primitives.
28pub struct RuntimeAwaitEventOptions {
29    pub cancellation: CancellationToken,
30    pub deadline: Option<Instant>,
31    pub clock: Arc<dyn crate::Clock>,
32}
33
34use super::await_events::inline_await_events;
35
36// =============================================================================
37// Effect host + controller trait + scope + error
38// =============================================================================
39
40/// Stable semantic identity for one effectful runtime operation.
41///
42/// The scope is chosen by the host boundary before any nondeterministic work is
43/// planned. It is intentionally generic: Restate, an inline test host, or a
44/// future durable effect host all receive the same Lash scope vocabulary.
45#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[serde(tag = "type", rename_all = "snake_case")]
47pub enum ExecutionScope {
48    Turn {
49        session_id: String,
50        turn_id: String,
51    },
52    Process {
53        process_id: String,
54    },
55    QueueDrain {
56        session_id: String,
57        drain_id: String,
58    },
59    SessionDelete {
60        session_id: String,
61    },
62    RuntimeOperation {
63        operation_id: String,
64    },
65}
66
67impl ExecutionScope {
68    pub fn turn(session_id: impl Into<String>, turn_id: impl Into<String>) -> Self {
69        Self::Turn {
70            session_id: session_id.into(),
71            turn_id: turn_id.into(),
72        }
73    }
74
75    pub fn process(process_id: impl Into<String>) -> Self {
76        Self::Process {
77            process_id: process_id.into(),
78        }
79    }
80
81    pub fn queue_drain(session_id: impl Into<String>, drain_id: impl Into<String>) -> Self {
82        Self::QueueDrain {
83            session_id: session_id.into(),
84            drain_id: drain_id.into(),
85        }
86    }
87
88    pub fn session_delete(session_id: impl Into<String>) -> Self {
89        Self::SessionDelete {
90            session_id: session_id.into(),
91        }
92    }
93
94    pub fn runtime_operation(operation_id: impl Into<String>) -> Self {
95        Self::RuntimeOperation {
96            operation_id: operation_id.into(),
97        }
98    }
99
100    pub fn id(&self) -> &str {
101        match self {
102            Self::Turn { turn_id, .. } => turn_id,
103            Self::Process { process_id } => process_id,
104            Self::QueueDrain { drain_id, .. } => drain_id,
105            Self::SessionDelete { session_id } => session_id,
106            Self::RuntimeOperation { operation_id } => operation_id,
107        }
108    }
109
110    pub fn session_id(&self) -> Option<&str> {
111        match self {
112            Self::Turn { session_id, .. }
113            | Self::QueueDrain { session_id, .. }
114            | Self::SessionDelete { session_id } => Some(session_id),
115            Self::Process { .. } | Self::RuntimeOperation { .. } => None,
116        }
117    }
118
119    pub fn turn_id(&self) -> Option<&str> {
120        match self {
121            Self::Turn { turn_id, .. } => Some(turn_id),
122            _ => None,
123        }
124    }
125
126    pub fn validates_turn_trace_id(&self) -> bool {
127        matches!(self, Self::Turn { .. })
128    }
129
130    pub(super) fn validate(&self) -> Result<(), RuntimeError> {
131        let missing = match self {
132            Self::Turn {
133                session_id,
134                turn_id,
135            } => session_id.trim().is_empty() || turn_id.trim().is_empty(),
136            Self::Process { process_id } => process_id.trim().is_empty(),
137            Self::QueueDrain {
138                session_id,
139                drain_id,
140            } => session_id.trim().is_empty() || drain_id.trim().is_empty(),
141            Self::SessionDelete { session_id } => session_id.trim().is_empty(),
142            Self::RuntimeOperation { operation_id } => operation_id.trim().is_empty(),
143        };
144        if missing {
145            return Err(RuntimeError::new(
146                RuntimeErrorCode::MissingExecutionScopeId,
147                "execution scopes require non-empty stable ids",
148            ));
149        }
150        Ok(())
151    }
152}
153
154#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
155#[serde(tag = "type", rename_all = "snake_case")]
156pub enum AwaitEventWaitIdentity {
157    ToolCompletion {
158        tool_call_id: String,
159    },
160    ProcessSignal {
161        process_id: String,
162        signal_name: String,
163        ordinal: u64,
164    },
165    Custom {
166        key: String,
167    },
168}
169
170impl AwaitEventWaitIdentity {
171    pub fn tool_completion(tool_call_id: impl Into<String>) -> Self {
172        Self::ToolCompletion {
173            tool_call_id: tool_call_id.into(),
174        }
175    }
176
177    pub fn process_signal(
178        process_id: impl Into<String>,
179        signal_name: impl Into<String>,
180        ordinal: u64,
181    ) -> Self {
182        Self::ProcessSignal {
183            process_id: process_id.into(),
184            signal_name: signal_name.into(),
185            ordinal,
186        }
187    }
188
189    pub(super) fn validate(&self) -> Result<(), RuntimeError> {
190        let invalid = match self {
191            Self::ToolCompletion { tool_call_id } => tool_call_id.trim().is_empty(),
192            Self::ProcessSignal {
193                process_id,
194                signal_name,
195                ordinal,
196            } => process_id.trim().is_empty() || signal_name.trim().is_empty() || *ordinal == 0,
197            Self::Custom { key } => key.trim().is_empty(),
198        };
199        if invalid {
200            return Err(RuntimeError::new(
201                "invalid_await_event_wait_identity",
202                "await-event wait identity requires non-empty stable ids",
203            ));
204        }
205        Ok(())
206    }
207}
208
209#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
210pub struct AwaitEventKey {
211    pub scope: ExecutionScope,
212    pub wait: AwaitEventWaitIdentity,
213    pub key_id: String,
214    pub signature: String,
215}
216
217impl AwaitEventKey {
218    pub fn promise_key(&self) -> String {
219        format!("lash-await-event:{}", self.key_id)
220    }
221}
222
223#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
224pub struct ExternalCompletionError {
225    pub code: String,
226    pub message: String,
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    pub raw: Option<serde_json::Value>,
229}
230
231impl ExternalCompletionError {
232    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
233        Self {
234            code: code.into(),
235            message: message.into(),
236            raw: None,
237        }
238    }
239}
240
241#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(tag = "status", content = "payload", rename_all = "snake_case")]
243pub enum Resolution {
244    Ok(serde_json::Value),
245    Err(ExternalCompletionError),
246    Timeout,
247    Cancelled,
248}
249
250#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(tag = "status", rename_all = "snake_case")]
252pub enum ResolveOutcome {
253    Accepted,
254    AlreadyResolved { terminal: Resolution },
255    UnknownOrRevoked,
256}
257
258enum ScopedEffectControllerInner<'run> {
259    Borrowed(&'run dyn RuntimeEffectController),
260    Shared(Arc<dyn RuntimeEffectController>),
261}
262
263impl Clone for ScopedEffectControllerInner<'_> {
264    fn clone(&self) -> Self {
265        match self {
266            Self::Borrowed(controller) => Self::Borrowed(*controller),
267            Self::Shared(controller) => Self::Shared(Arc::clone(controller)),
268        }
269    }
270}
271
272/// Scoped low-level controller plus the semantic execution scope it is serving.
273#[derive(Clone)]
274pub struct ScopedEffectController<'run> {
275    controller: ScopedEffectControllerInner<'run>,
276    scope: ExecutionScope,
277}
278
279impl<'run> ScopedEffectController<'run> {
280    pub fn borrowed(
281        controller: &'run dyn RuntimeEffectController,
282        scope: ExecutionScope,
283    ) -> Result<Self, RuntimeError> {
284        scope.validate()?;
285        Ok(Self {
286            controller: ScopedEffectControllerInner::Borrowed(controller),
287            scope,
288        })
289    }
290
291    pub fn shared(
292        controller: Arc<dyn RuntimeEffectController>,
293        scope: ExecutionScope,
294    ) -> Result<Self, RuntimeError> {
295        scope.validate()?;
296        Ok(Self {
297            controller: ScopedEffectControllerInner::Shared(controller),
298            scope,
299        })
300    }
301
302    pub fn controller(&self) -> &dyn RuntimeEffectController {
303        match &self.controller {
304            ScopedEffectControllerInner::Borrowed(controller) => *controller,
305            ScopedEffectControllerInner::Shared(controller) => controller.as_ref(),
306        }
307    }
308
309    pub fn execution_scope(&self) -> &ExecutionScope {
310        &self.scope
311    }
312
313    pub fn scope_id(&self) -> &str {
314        self.scope.id()
315    }
316
317    pub fn turn_id(&self) -> Option<&str> {
318        self.scope.turn_id()
319    }
320}
321
322/// Shared durability and Durable Wait contract for effect boundaries.
323///
324/// Both the deployment-level [`EffectHost`] factory and the per-run
325/// [`RuntimeEffectController`] resolve Durable Waits and describe their
326/// durability; this supertrait is the single declaration of that contract.
327#[async_trait::async_trait]
328pub trait AwaitEventResolver: Send + Sync {
329    fn durability_tier(&self) -> crate::DurabilityTier {
330        crate::DurabilityTier::Inline
331    }
332
333    fn supports_durable_effects(&self) -> bool {
334        false
335    }
336
337    async fn await_event_key(
338        &self,
339        _scope: &ExecutionScope,
340        _wait: AwaitEventWaitIdentity,
341    ) -> Result<AwaitEventKey, RuntimeError> {
342        Err(RuntimeError::new(
343            "await_event_unsupported",
344            "this effect boundary does not support await-event keys",
345        ))
346    }
347
348    async fn resolve_await_event(
349        &self,
350        _key: &AwaitEventKey,
351        _resolution: Resolution,
352    ) -> Result<ResolveOutcome, RuntimeError> {
353        Ok(ResolveOutcome::UnknownOrRevoked)
354    }
355
356    async fn await_await_event(
357        &self,
358        _key: &AwaitEventKey,
359        _cancel: CancellationToken,
360        _deadline: Option<Instant>,
361    ) -> Result<Resolution, RuntimeError> {
362        Err(RuntimeError::new(
363            "await_event_unsupported",
364            "this effect boundary does not support await-event waits",
365        ))
366    }
367
368    async fn revoke_await_events_for_session(&self, _session_id: &str) -> Result<(), RuntimeError> {
369        Ok(())
370    }
371
372    /// Cancel every *outstanding* durable wait for `session_id` without
373    /// deleting the session: each waiter receives a terminal
374    /// [`Resolution::Cancelled`] instead of hanging, late resolves observe
375    /// that terminal, and waits registered afterwards behave normally — in
376    /// contrast to [`revoke_await_events_for_session`](Self::revoke_await_events_for_session),
377    /// which tombstones the session's waits forever.
378    ///
379    /// The default errors loudly: an effect boundary that tracks durable waits
380    /// must implement this to honor the host lever, and one that cannot must
381    /// not silently claim success.
382    async fn cancel_await_events_for_session(&self, _session_id: &str) -> Result<(), RuntimeError> {
383        Err(RuntimeError::new(
384            "await_event_cancel_unsupported",
385            "this effect boundary does not support cancelling durable waits",
386        ))
387    }
388}
389
390/// Deployment-level factory for scoped effect controllers.
391#[async_trait::async_trait]
392pub trait EffectHost: AwaitEventResolver {
393    fn scoped<'run>(
394        &'run self,
395        scope: ExecutionScope,
396    ) -> Result<ScopedEffectController<'run>, RuntimeError>;
397
398    fn scoped_static(
399        &self,
400        _scope: ExecutionScope,
401    ) -> Result<Option<ScopedEffectController<'static>>, RuntimeError> {
402        Ok(None)
403    }
404}
405
406/// Boundary for nondeterministic runtime work.
407#[async_trait::async_trait]
408pub trait RuntimeEffectController: AwaitEventResolver {
409    /// Advises an engine to end the current in-process execution segment at a
410    /// quiescent point. Engines may decline when live state is not capturable,
411    /// but must make progress before returning another decline. In particular,
412    /// an engine must not repeatedly return the same boundary and unchanged
413    /// durable-wait state in one invocation; a host may bound and retry such a
414    /// non-progressing invocation.
415    fn wants_segment_boundary(&self, _progress: &SegmentProgress) -> Option<BoundaryReason> {
416        None
417    }
418
419    /// Whether this controller can safely accept overlapping `execute_effect`
420    /// calls from one runtime coordinator.
421    ///
422    /// Local and store-backed controllers can usually fan out independent
423    /// effects. Some workflow substrates expose a single ordered journal
424    /// context where native operations must be awaited immediately before the
425    /// next context call is issued. Those controllers should return `false` so
426    /// coordinators serialize child effects while still replaying each child by
427    /// its own stable key.
428    fn supports_concurrent_effects(&self) -> bool {
429        true
430    }
431
432    async fn execute_effect(
433        &self,
434        envelope: RuntimeEffectEnvelope,
435        local_executor: RuntimeEffectLocalExecutor<'_>,
436    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError>;
437}
438
439#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
440pub struct SegmentProgress {
441    pub effects_executed: u64,
442    pub journaled_bytes_estimate: Option<u64>,
443}
444
445#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
446#[serde(rename_all = "snake_case")]
447pub enum BoundaryReason {
448    JournalBudget,
449    DurationCap,
450}
451
452/// Runtime-internal handle for effect-controller references carried through
453/// per-turn execution contexts.
454#[derive(Clone)]
455pub(crate) enum RuntimeEffectControllerHandle<'run> {
456    Borrowed(ScopedEffectController<'run>),
457    #[cfg(any(test, feature = "testing"))]
458    Shared {
459        controller: Arc<dyn RuntimeEffectController>,
460        scope: ExecutionScope,
461    },
462}
463
464impl<'run> RuntimeEffectControllerHandle<'run> {
465    pub(crate) fn borrowed(scoped: ScopedEffectController<'run>) -> Self {
466        Self::Borrowed(scoped)
467    }
468
469    #[cfg(any(test, feature = "testing"))]
470    pub(crate) fn shared(controller: Arc<dyn RuntimeEffectController>) -> Self {
471        Self::Shared {
472            controller,
473            scope: ExecutionScope::runtime_operation("test-runtime-effect-controller"),
474        }
475    }
476
477    pub(crate) fn controller(&self) -> &dyn RuntimeEffectController {
478        match self {
479            Self::Borrowed(scoped) => scoped.controller(),
480            #[cfg(any(test, feature = "testing"))]
481            Self::Shared { controller, .. } => controller.as_ref(),
482        }
483    }
484
485    pub(crate) fn scoped(&self) -> ScopedEffectController<'_> {
486        match self {
487            Self::Borrowed(scoped) => scoped.clone(),
488            #[cfg(any(test, feature = "testing"))]
489            Self::Shared { controller, scope } => {
490                ScopedEffectController::shared(Arc::clone(controller), scope.clone())
491                    .expect("runtime effect controller handle carries a valid scope")
492            }
493        }
494    }
495
496    pub(crate) fn clone_scoped(&self) -> RuntimeEffectControllerHandle<'run> {
497        self.clone()
498    }
499}
500
501#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)]
502#[error("{code}: {message}")]
503pub struct RuntimeEffectControllerError {
504    pub code: String,
505    pub message: String,
506}
507
508impl RuntimeEffectControllerError {
509    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
510        Self {
511            code: code.into(),
512            message: message.into(),
513        }
514    }
515
516    pub(super) fn wrong_outcome(expected: RuntimeEffectKind, actual: RuntimeEffectKind) -> Self {
517        Self::new(
518            "runtime_effect_wrong_outcome",
519            format!(
520                "expected {} outcome, got {}",
521                expected.as_str(),
522                actual.as_str()
523            ),
524        )
525    }
526
527    pub(crate) fn into_runtime_error(self) -> RuntimeError {
528        RuntimeError::new(self.code, self.message)
529    }
530}
531
532impl From<RuntimeError> for RuntimeEffectControllerError {
533    fn from(err: RuntimeError) -> Self {
534        Self::new(err.code.as_str(), err.message)
535    }
536}
537
538impl From<PluginError> for RuntimeEffectControllerError {
539    fn from(err: PluginError) -> Self {
540        Self::new("plugin", err.to_string())
541    }
542}
543
544impl From<crate::StoreError> for RuntimeEffectControllerError {
545    fn from(err: crate::StoreError) -> Self {
546        Self::new("runtime_store", err.to_string())
547    }
548}
549
550// =============================================================================
551// Local executor (per-effect borrowed runner state)
552// =============================================================================
553
554#[async_trait::async_trait]
555pub(crate) trait ProcessRunner: Send + Sync {
556    async fn run_process(
557        &self,
558        registration: crate::ProcessRegistration,
559        execution_context: crate::ProcessExecutionContext,
560        registry: Arc<dyn ProcessRegistry>,
561        scoped_effect_controller: crate::ScopedEffectController<'_>,
562        cancellation: CancellationToken,
563        handover: Option<crate::SegmentHandover>,
564    ) -> crate::ProcessRunOutcome;
565}
566
567pub struct ProcessLocalExecution {
568    pub registry: Arc<dyn ProcessRegistry>,
569    pub process_work_driver: Option<crate::ProcessWorkDriver>,
570}
571
572impl ProcessLocalExecution {
573    pub async fn execute(
574        self,
575        command: ProcessCommand,
576    ) -> Result<ProcessEffectOutcome, RuntimeEffectControllerError> {
577        let Self {
578            registry,
579            process_work_driver,
580        } = self;
581        match command {
582            ProcessCommand::Start {
583                registration,
584                grant,
585                execution_context: _,
586            } => {
587                let record =
588                    InlineRuntimeEffectController::start_process(registry, registration, grant)
589                        .await?;
590                if let Some(driver) = process_work_driver.as_ref() {
591                    driver.claim_and_run_pending("process_start").await?;
592                }
593                Ok(ProcessEffectOutcome::Start {
594                    record: Box::new(record),
595                })
596            }
597            ProcessCommand::List {
598                session_scope,
599                mode,
600            } => {
601                let entries = match mode {
602                    crate::ProcessListMode::Live => {
603                        registry.list_live_handle_grants(&session_scope).await?
604                    }
605                    crate::ProcessListMode::All => {
606                        registry.list_handle_grants(&session_scope).await?
607                    }
608                };
609                Ok(ProcessEffectOutcome::List { entries })
610            }
611            ProcessCommand::Transfer {
612                from_scope,
613                to_scope,
614                process_ids,
615            } => {
616                registry
617                    .transfer_handle_grants(&from_scope, &to_scope, &process_ids)
618                    .await?;
619                Ok(ProcessEffectOutcome::Transfer)
620            }
621            ProcessCommand::DeleteSession { session_id } => {
622                let report = registry.delete_session_process_state(&session_id).await?;
623                Ok(ProcessEffectOutcome::DeleteSession { report })
624            }
625            ProcessCommand::Await { process_id } => {
626                let output = if let Some(driver) = process_work_driver.as_ref() {
627                    driver.await_terminal(&process_id).await?
628                } else {
629                    crate::ProcessAwaiter::polling(registry)
630                        .await_terminal(&process_id)
631                        .await?
632                };
633                Ok(ProcessEffectOutcome::Await { output })
634            }
635            ProcessCommand::Cancel { process_id, reason } => {
636                let record = InlineRuntimeEffectController
637                    .request_process_cancel(registry, &process_id, reason)
638                    .await?;
639                Ok(ProcessEffectOutcome::Cancel {
640                    record: Box::new(record),
641                })
642            }
643            ProcessCommand::Signal {
644                process_id,
645                request,
646                ..
647            } => {
648                let result = registry.append_event(&process_id, request).await?;
649                Ok(ProcessEffectOutcome::Signal {
650                    event: Box::new(result.event),
651                })
652            }
653        }
654    }
655}
656
657pub(super) struct LocalTurnEffectRunner<'a, 'run> {
658    driver: &'a mut RuntimeTurnDriver<'run>,
659    machine: &'a mut crate::TurnMachine,
660    event_tx: mpsc::Sender<RuntimeStreamEvent>,
661    cancellation: CancellationToken,
662}
663
664pub(super) struct LocalDirectEffectRunner {
665    provider: ProviderHandle,
666    attachment_store: Arc<crate::SessionAttachmentStore>,
667}
668
669struct LocalToolBatchEffectRunner<'run> {
670    context: crate::RuntimeExecutionContext<'run>,
671    child_trace_hooks: HashMap<String, crate::ToolChildExecutionTraceHook>,
672}
673
674#[async_trait::async_trait]
675trait RuntimeEffectLocalRunner: Send {
676    async fn execute(
677        self: Box<Self>,
678        envelope: RuntimeEffectEnvelope,
679    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError>;
680}
681
682#[cfg(any(test, feature = "testing"))]
683type TestingRuntimeEffectLocalRunnerFn<'run> = dyn FnOnce(
684        RuntimeEffectEnvelope,
685    ) -> Pin<
686        Box<
687            dyn Future<Output = Result<RuntimeEffectOutcome, RuntimeEffectControllerError>>
688                + Send
689                + 'run,
690        >,
691    > + Send
692    + 'run;
693
694#[cfg(any(test, feature = "testing"))]
695struct TestingRuntimeEffectLocalRunner<'run> {
696    run: Box<TestingRuntimeEffectLocalRunnerFn<'run>>,
697}
698
699type DurableStepLocalRunnerFn<'run> = dyn FnOnce(
700        serde_json::Value,
701    ) -> Pin<
702        Box<
703            dyn Future<Output = Result<serde_json::Value, RuntimeEffectControllerError>>
704                + Send
705                + 'run,
706        >,
707    > + Send
708    + 'run;
709
710struct DurableStepLocalRunner<'run> {
711    run: Box<DurableStepLocalRunnerFn<'run>>,
712}
713
714enum RuntimeEffectLocalExecutorState<'run> {
715    Unavailable,
716    SleepOnly {
717        cancellation: CancellationToken,
718        clock: Arc<dyn crate::Clock>,
719    },
720    ExternalWaitOptions {
721        cancellation: CancellationToken,
722        deadline: Option<Instant>,
723        clock: Arc<dyn crate::Clock>,
724    },
725    Process(ProcessLocalExecution),
726    Runner(Box<dyn RuntimeEffectLocalRunner + Send + 'run>),
727}
728
729/// Scoped local executor provided to a [`RuntimeEffectController`] for one effect.
730///
731/// Durable controllers may ignore it and replay their own recorded result. The
732/// default inline controller delegates to it, so local provider/tool/checkpoint
733/// work still crosses the same `execute_effect` boundary as durable controllers.
734pub struct RuntimeEffectLocalExecutor<'run> {
735    state: RuntimeEffectLocalExecutorState<'run>,
736}
737
738impl<'run> RuntimeEffectLocalExecutor<'run> {
739    pub fn unavailable() -> Self {
740        Self {
741            state: RuntimeEffectLocalExecutorState::Unavailable,
742        }
743    }
744
745    pub fn sleep(cancellation: CancellationToken) -> Self {
746        Self::sleep_with_clock(cancellation, Arc::new(crate::SystemClock))
747    }
748
749    pub fn sleep_with_clock(cancellation: CancellationToken, clock: Arc<dyn crate::Clock>) -> Self {
750        Self {
751            state: RuntimeEffectLocalExecutorState::SleepOnly {
752                cancellation,
753                clock,
754            },
755        }
756    }
757
758    pub fn await_event(cancellation: CancellationToken, deadline: Option<Instant>) -> Self {
759        Self::await_event_with_clock(cancellation, deadline, Arc::new(crate::SystemClock))
760    }
761
762    pub fn await_event_with_clock(
763        cancellation: CancellationToken,
764        deadline: Option<Instant>,
765        clock: Arc<dyn crate::Clock>,
766    ) -> Self {
767        Self {
768            state: RuntimeEffectLocalExecutorState::ExternalWaitOptions {
769                cancellation,
770                deadline,
771                clock,
772            },
773        }
774    }
775
776    pub fn processes(
777        registry: Arc<dyn ProcessRegistry>,
778        process_work_driver: Option<crate::ProcessWorkDriver>,
779    ) -> Self {
780        Self {
781            state: RuntimeEffectLocalExecutorState::Process(ProcessLocalExecution {
782                registry,
783                process_work_driver,
784            }),
785        }
786    }
787
788    pub fn durable_step<F, Fut>(run: F) -> Self
789    where
790        F: FnOnce(serde_json::Value) -> Fut + Send + 'run,
791        Fut: Future<Output = Result<serde_json::Value, RuntimeError>> + Send + 'run,
792    {
793        Self {
794            state: RuntimeEffectLocalExecutorState::Runner(Box::new(DurableStepLocalRunner {
795                run: Box::new(move |input| {
796                    Box::pin(async move { run(input).await.map_err(Into::into) })
797                }),
798            })),
799        }
800    }
801
802    #[cfg(any(test, feature = "testing"))]
803    pub fn testing<F, Fut>(run: F) -> Self
804    where
805        F: FnOnce(RuntimeEffectEnvelope) -> Fut + Send + 'run,
806        Fut: Future<Output = Result<RuntimeEffectOutcome, RuntimeEffectControllerError>>
807            + Send
808            + 'run,
809    {
810        Self {
811            state: RuntimeEffectLocalExecutorState::Runner(Box::new(
812                TestingRuntimeEffectLocalRunner {
813                    run: Box::new(move |envelope| Box::pin(run(envelope))),
814                },
815            )),
816        }
817    }
818
819    pub(in crate::runtime) fn turn<'scope>(
820        driver: &'run mut RuntimeTurnDriver<'scope>,
821        machine: &'run mut crate::TurnMachine,
822        event_tx: mpsc::Sender<RuntimeStreamEvent>,
823        cancellation: CancellationToken,
824    ) -> Self
825    where
826        'scope: 'run,
827    {
828        Self {
829            state: RuntimeEffectLocalExecutorState::Runner(Box::new(LocalTurnEffectRunner {
830                driver,
831                machine,
832                event_tx,
833                cancellation,
834            })),
835        }
836    }
837
838    pub(in crate::runtime) fn direct(
839        provider: ProviderHandle,
840        attachment_store: Arc<crate::SessionAttachmentStore>,
841    ) -> Self {
842        Self {
843            state: RuntimeEffectLocalExecutorState::Runner(Box::new(LocalDirectEffectRunner {
844                provider,
845                attachment_store,
846            })),
847        }
848    }
849
850    pub(crate) fn tool_batch(
851        context: crate::RuntimeExecutionContext<'run>,
852        child_trace_hooks: HashMap<String, crate::ToolChildExecutionTraceHook>,
853    ) -> Self {
854        Self {
855            state: RuntimeEffectLocalExecutorState::Runner(Box::new(LocalToolBatchEffectRunner {
856                context,
857                child_trace_hooks,
858            })),
859        }
860    }
861
862    pub async fn execute(
863        self,
864        envelope: RuntimeEffectEnvelope,
865    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
866        match self.state {
867            RuntimeEffectLocalExecutorState::Runner(runner) => runner.execute(envelope).await,
868            RuntimeEffectLocalExecutorState::SleepOnly {
869                cancellation,
870                clock,
871            } => execute_local_sleep(envelope, cancellation, clock.as_ref()).await,
872            RuntimeEffectLocalExecutorState::ExternalWaitOptions { .. } => {
873                Err(RuntimeEffectControllerError::new(
874                    "runtime_effect_local_executor_mismatch",
875                    format!(
876                        "local await-event options cannot execute {} command directly",
877                        envelope.command.kind().as_str()
878                    ),
879                ))
880            }
881            RuntimeEffectLocalExecutorState::Unavailable => Err(RuntimeEffectControllerError::new(
882                "runtime_effect_local_executor_unavailable",
883                format!(
884                    "no local executor is available for {}",
885                    envelope.command.kind().as_str()
886                ),
887            )),
888            RuntimeEffectLocalExecutorState::Process(_) => Err(RuntimeEffectControllerError::new(
889                "runtime_effect_local_executor_mismatch",
890                format!(
891                    "process executor cannot execute {} command directly",
892                    envelope.command.kind().as_str()
893                ),
894            )),
895        }
896    }
897
898    pub fn into_process(self) -> Result<ProcessLocalExecution, RuntimeEffectControllerError> {
899        match self.state {
900            RuntimeEffectLocalExecutorState::Process(execution) => Ok(execution),
901            _ => Err(RuntimeEffectControllerError::new(
902                "runtime_effect_local_executor_unavailable",
903                "no process executor is available for process command",
904            )),
905        }
906    }
907
908    pub fn into_await_event_options(
909        self,
910    ) -> Result<RuntimeAwaitEventOptions, RuntimeEffectControllerError> {
911        match self.state {
912            RuntimeEffectLocalExecutorState::ExternalWaitOptions {
913                cancellation,
914                deadline,
915                clock,
916            } => Ok(RuntimeAwaitEventOptions {
917                cancellation,
918                deadline,
919                clock,
920            }),
921            _ => Ok(RuntimeAwaitEventOptions {
922                cancellation: CancellationToken::new(),
923                deadline: None,
924                clock: Arc::new(crate::SystemClock),
925            }),
926        }
927    }
928}
929
930#[cfg(any(test, feature = "testing"))]
931#[async_trait::async_trait]
932impl RuntimeEffectLocalRunner for TestingRuntimeEffectLocalRunner<'_> {
933    async fn execute(
934        self: Box<Self>,
935        envelope: RuntimeEffectEnvelope,
936    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
937        (self.run)(envelope).await
938    }
939}
940
941#[async_trait::async_trait]
942impl RuntimeEffectLocalRunner for DurableStepLocalRunner<'_> {
943    async fn execute(
944        self: Box<Self>,
945        envelope: RuntimeEffectEnvelope,
946    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
947        match envelope.command {
948            RuntimeEffectCommand::DurableStep { input, .. } => {
949                let value = (self.run)(input).await?;
950                Ok(RuntimeEffectOutcome::DurableStep { value })
951            }
952            command => Err(RuntimeEffectControllerError::new(
953                "runtime_effect_local_executor_mismatch",
954                format!(
955                    "local durable step executor cannot execute {} command",
956                    command.kind().as_str()
957                ),
958            )),
959        }
960    }
961}
962
963#[async_trait::async_trait]
964impl RuntimeEffectLocalRunner for LocalToolBatchEffectRunner<'_> {
965    async fn execute(
966        self: Box<Self>,
967        envelope: RuntimeEffectEnvelope,
968    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
969        match envelope.command {
970            RuntimeEffectCommand::ToolBatch { batch } => {
971                let outcome = self
972                    .context
973                    .execute_prepared_tool_batch_launches(
974                        batch,
975                        envelope.invocation,
976                        self.child_trace_hooks,
977                    )
978                    .await?;
979                Ok(RuntimeEffectOutcome::ToolBatch {
980                    launches: outcome.launches,
981                    triggers: outcome.triggers,
982                })
983            }
984            RuntimeEffectCommand::ToolAttempt {
985                call,
986                execution_grant,
987                attempt,
988                max_attempts,
989            } => {
990                let child_execution_trace_hook = self.child_trace_hooks.get(&call.call_id).cloned();
991                let outcome = self
992                    .context
993                    .execute_prepared_tool_attempt_effect(
994                        call,
995                        execution_grant,
996                        attempt,
997                        max_attempts,
998                        envelope.invocation,
999                        child_execution_trace_hook,
1000                    )
1001                    .await?;
1002                Ok(RuntimeEffectOutcome::ToolAttempt {
1003                    launch: outcome.launch,
1004                    triggers: outcome.triggers,
1005                })
1006            }
1007            command => Err(RuntimeEffectControllerError::new(
1008                "runtime_effect_local_executor_mismatch",
1009                format!(
1010                    "local tool executor cannot execute {} command",
1011                    command.kind().as_str()
1012                ),
1013            )),
1014        }
1015    }
1016}
1017
1018#[async_trait::async_trait]
1019impl RuntimeEffectLocalRunner for LocalTurnEffectRunner<'_, '_> {
1020    async fn execute(
1021        self: Box<Self>,
1022        envelope: RuntimeEffectEnvelope,
1023    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1024        let runner = *self;
1025        match envelope.command {
1026            RuntimeEffectCommand::LlmCall { request } => {
1027                let protocol_iteration = runner.machine.protocol_iteration();
1028                let (result, text_streamed, call_record) = runner
1029                    .driver
1030                    .run_llm_call(
1031                        Arc::new((*request).into_request(None, None)),
1032                        protocol_iteration,
1033                        envelope.invocation,
1034                        &runner.event_tx,
1035                        &runner.cancellation,
1036                    )
1037                    .await;
1038                Ok(RuntimeEffectOutcome::LlmCall {
1039                    result,
1040                    text_streamed,
1041                    call_record,
1042                })
1043            }
1044            RuntimeEffectCommand::ToolBatch { batch } => {
1045                let outcome = runner
1046                    .driver
1047                    .run_tool_batch(
1048                        batch,
1049                        envelope.invocation,
1050                        &runner.event_tx,
1051                        &runner.cancellation,
1052                    )
1053                    .await?;
1054                Ok(RuntimeEffectOutcome::ToolBatch {
1055                    launches: outcome.launches,
1056                    triggers: outcome.triggers,
1057                })
1058            }
1059            RuntimeEffectCommand::ExecCode { language, code } => {
1060                let protocol_iteration = runner.machine.protocol_iteration();
1061                let messages = runner.machine.message_sequence();
1062                Ok(RuntimeEffectOutcome::ExecCode {
1063                    result: runner
1064                        .driver
1065                        .run_exec_code(
1066                            language,
1067                            &code,
1068                            messages,
1069                            protocol_iteration,
1070                            envelope.invocation,
1071                            &runner.event_tx,
1072                        )
1073                        .await,
1074                })
1075            }
1076            RuntimeEffectCommand::Checkpoint { checkpoint } => {
1077                Ok(RuntimeEffectOutcome::Checkpoint {
1078                    result: runner
1079                        .driver
1080                        .run_checkpoint(runner.machine, checkpoint, &runner.event_tx)
1081                        .await
1082                        .map_err(RuntimeEffectControllerError::from),
1083                })
1084            }
1085            RuntimeEffectCommand::SyncExecutionEnvironment {
1086                update_machine_config,
1087            } => Ok(RuntimeEffectOutcome::SyncExecutionEnvironment {
1088                result: runner
1089                    .driver
1090                    .refresh_execution_environment(runner.machine, update_machine_config)
1091                    .await
1092                    .map_err(|err| err.to_string()),
1093            }),
1094            RuntimeEffectCommand::Sleep { duration_ms } => {
1095                sleep_with_cancellation(
1096                    duration_ms,
1097                    &runner.cancellation,
1098                    runner.driver.host.core.clock.as_ref(),
1099                )
1100                .await?;
1101                Ok(RuntimeEffectOutcome::Sleep)
1102            }
1103            command => Err(RuntimeEffectControllerError::new(
1104                "runtime_effect_local_executor_mismatch",
1105                format!(
1106                    "local turn executor cannot execute {} command",
1107                    command.kind().as_str()
1108                ),
1109            )),
1110        }
1111    }
1112}
1113
1114#[async_trait::async_trait]
1115impl RuntimeEffectLocalRunner for LocalDirectEffectRunner {
1116    async fn execute(
1117        mut self: Box<Self>,
1118        envelope: RuntimeEffectEnvelope,
1119    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1120        match envelope.command {
1121            RuntimeEffectCommand::Direct { request, .. } => {
1122                let (result, call_record) = self
1123                    .run_direct_llm_request((*request).into_request(
1124                        crate::session_model::transport_stream_events(&self.provider, None),
1125                        None,
1126                    ))
1127                    .await;
1128                Ok(RuntimeEffectOutcome::Direct {
1129                    result,
1130                    call_record,
1131                })
1132            }
1133            RuntimeEffectCommand::Sleep { duration_ms } => {
1134                sleep_with_cancellation(
1135                    duration_ms,
1136                    &CancellationToken::new(),
1137                    &crate::SystemClock,
1138                )
1139                .await?;
1140                Ok(RuntimeEffectOutcome::Sleep)
1141            }
1142            command => Err(RuntimeEffectControllerError::new(
1143                "runtime_effect_local_executor_mismatch",
1144                format!(
1145                    "local direct executor cannot execute {} command",
1146                    command.kind().as_str()
1147                ),
1148            )),
1149        }
1150    }
1151}
1152
1153impl LocalDirectEffectRunner {
1154    async fn run_direct_llm_request(&mut self, request: CoreLlmRequest) -> RuntimeDirectLlmOutcome {
1155        let request = match crate::attachments::resolve_llm_request_attachments(
1156            request,
1157            self.attachment_store.as_ref(),
1158        )
1159        .await
1160        {
1161            Ok(request) => request,
1162            Err(err) => {
1163                return (
1164                    Err(LlmCallError {
1165                        message: err.to_string(),
1166                        retryable: false,
1167                        kind: crate::ProviderFailureKind::Unknown,
1168                        raw: None,
1169                        code: Some("attachment_resolution_failed".to_string()),
1170                        terminal_reason: crate::LlmTerminalReason::ProviderError,
1171                        request_body: None,
1172                    }),
1173                    None,
1174                );
1175            }
1176        };
1177        match self.provider.complete(request).await {
1178            Ok(completion) => (Ok(completion.response), Some(completion.call_record)),
1179            Err(failure) => (
1180                Err(llm_call_error_from_transport(failure.error)),
1181                Some(failure.call_record),
1182            ),
1183        }
1184    }
1185}
1186
1187async fn execute_local_sleep(
1188    envelope: RuntimeEffectEnvelope,
1189    cancellation: CancellationToken,
1190    clock: &dyn crate::Clock,
1191) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1192    match envelope.command {
1193        RuntimeEffectCommand::Sleep { duration_ms } => {
1194            sleep_with_cancellation(duration_ms, &cancellation, clock).await?;
1195            Ok(RuntimeEffectOutcome::Sleep)
1196        }
1197        command => Err(RuntimeEffectControllerError::new(
1198            "runtime_effect_local_executor_mismatch",
1199            format!(
1200                "local sleep executor cannot execute {} command",
1201                command.kind().as_str()
1202            ),
1203        )),
1204    }
1205}
1206
1207async fn sleep_with_cancellation(
1208    duration_ms: u64,
1209    cancellation: &CancellationToken,
1210    clock: &dyn crate::Clock,
1211) -> Result<(), RuntimeEffectControllerError> {
1212    let sleep = clock.sleep(std::time::Duration::from_millis(duration_ms));
1213    tokio::pin!(sleep);
1214    tokio::select! {
1215        _ = cancellation.cancelled() => Err(RuntimeEffectControllerError::new(
1216            "runtime_effect_sleep_cancelled",
1217            "runtime effect sleep was cancelled",
1218        )),
1219        _ = &mut sleep => Ok(()),
1220    }
1221}
1222
1223// =============================================================================
1224// Default in-process effect controller
1225// =============================================================================
1226
1227/// Default in-process effect controller.
1228///
1229/// The inline controller executes local runners in process, provides in-memory
1230/// await-event resolution, and exposes durable-tool-effect semantics for local
1231/// runs. It does not make in-flight effects crash durable; workflow adapters
1232/// provide that by recording outcomes in their own history.
1233#[derive(Clone, Default)]
1234pub struct InlineRuntimeEffectController;
1235
1236#[async_trait::async_trait]
1237impl AwaitEventResolver for InlineRuntimeEffectController {
1238    fn supports_durable_effects(&self) -> bool {
1239        true
1240    }
1241
1242    async fn await_event_key(
1243        &self,
1244        scope: &ExecutionScope,
1245        wait: AwaitEventWaitIdentity,
1246    ) -> Result<AwaitEventKey, RuntimeError> {
1247        inline_await_events().key_for(scope, wait)
1248    }
1249
1250    async fn resolve_await_event(
1251        &self,
1252        key: &AwaitEventKey,
1253        resolution: Resolution,
1254    ) -> Result<ResolveOutcome, RuntimeError> {
1255        inline_await_events().resolve(key, resolution)
1256    }
1257
1258    async fn await_await_event(
1259        &self,
1260        key: &AwaitEventKey,
1261        cancel: CancellationToken,
1262        deadline: Option<Instant>,
1263    ) -> Result<Resolution, RuntimeError> {
1264        inline_await_events()
1265            .await_resolution(key, cancel, deadline, &crate::SystemClock)
1266            .await
1267    }
1268
1269    async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1270        inline_await_events().revoke_session(session_id)
1271    }
1272
1273    async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1274        inline_await_events().cancel_session(session_id)
1275    }
1276}
1277
1278#[async_trait::async_trait]
1279impl RuntimeEffectController for InlineRuntimeEffectController {
1280    async fn execute_effect(
1281        &self,
1282        envelope: RuntimeEffectEnvelope,
1283        local_executor: RuntimeEffectLocalExecutor<'_>,
1284    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1285        match envelope.command {
1286            RuntimeEffectCommand::AwaitEvent { key } => {
1287                let RuntimeAwaitEventOptions {
1288                    cancellation,
1289                    deadline,
1290                    clock,
1291                } = local_executor.into_await_event_options()?;
1292                let resolution = inline_await_events()
1293                    .await_resolution(&key, cancellation, deadline, clock.as_ref())
1294                    .await
1295                    .map_err(RuntimeEffectControllerError::from)?;
1296                Ok(RuntimeEffectOutcome::AwaitEvent { resolution })
1297            }
1298            RuntimeEffectCommand::Process { command } => {
1299                let execution = local_executor.into_process()?;
1300                let result = tokio::task::spawn(async move { execution.execute(*command).await })
1301                    .await
1302                    .map_err(|err| {
1303                        RuntimeEffectControllerError::new(
1304                            "runtime_effect_process_task_join",
1305                            format!("inline process effect task failed: {err}"),
1306                        )
1307                    })??;
1308                Ok(RuntimeEffectOutcome::Process { result })
1309            }
1310            _ => local_executor.execute(envelope).await,
1311        }
1312    }
1313}
1314
1315/// In-process deployment effect host.
1316#[derive(Clone)]
1317pub struct InlineEffectHost {
1318    controller: Arc<dyn RuntimeEffectController>,
1319}
1320
1321impl InlineEffectHost {
1322    pub fn new(controller: Arc<dyn RuntimeEffectController>) -> Self {
1323        Self { controller }
1324    }
1325}
1326
1327impl Default for InlineEffectHost {
1328    fn default() -> Self {
1329        Self::new(Arc::new(InlineRuntimeEffectController))
1330    }
1331}
1332
1333#[async_trait::async_trait]
1334impl AwaitEventResolver for InlineEffectHost {
1335    fn durability_tier(&self) -> crate::DurabilityTier {
1336        self.controller.durability_tier()
1337    }
1338
1339    fn supports_durable_effects(&self) -> bool {
1340        self.controller.supports_durable_effects()
1341    }
1342
1343    async fn await_event_key(
1344        &self,
1345        scope: &ExecutionScope,
1346        wait: AwaitEventWaitIdentity,
1347    ) -> Result<AwaitEventKey, RuntimeError> {
1348        self.controller.await_event_key(scope, wait).await
1349    }
1350
1351    async fn resolve_await_event(
1352        &self,
1353        key: &AwaitEventKey,
1354        resolution: Resolution,
1355    ) -> Result<ResolveOutcome, RuntimeError> {
1356        self.controller.resolve_await_event(key, resolution).await
1357    }
1358
1359    async fn await_await_event(
1360        &self,
1361        key: &AwaitEventKey,
1362        cancel: CancellationToken,
1363        deadline: Option<Instant>,
1364    ) -> Result<Resolution, RuntimeError> {
1365        self.controller
1366            .await_await_event(key, cancel, deadline)
1367            .await
1368    }
1369
1370    async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1371        self.controller
1372            .revoke_await_events_for_session(session_id)
1373            .await
1374    }
1375
1376    async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1377        self.controller
1378            .cancel_await_events_for_session(session_id)
1379            .await
1380    }
1381}
1382
1383#[async_trait::async_trait]
1384impl EffectHost for InlineEffectHost {
1385    fn scoped<'run>(
1386        &'run self,
1387        scope: ExecutionScope,
1388    ) -> Result<ScopedEffectController<'run>, RuntimeError> {
1389        ScopedEffectController::shared(Arc::clone(&self.controller), scope)
1390    }
1391
1392    fn scoped_static(
1393        &self,
1394        scope: ExecutionScope,
1395    ) -> Result<Option<ScopedEffectController<'static>>, RuntimeError> {
1396        Ok(Some(ScopedEffectController::shared(
1397            Arc::clone(&self.controller),
1398            scope,
1399        )?))
1400    }
1401}
1402
1403impl InlineRuntimeEffectController {
1404    /// Register the process (and any handle grant) into the durable registry.
1405    ///
1406    /// The inline controller no longer runs the process here: the registry's
1407    /// non-terminal row *is* the durable work queue, and the host-owned
1408    /// [`ProcessWorkDriver`](crate::ProcessWorkDriver) is the sole executor.
1409    /// Registering the row is all this path does; the control seam drives the
1410    /// host driver after a successful start.
1411    pub(crate) async fn start_process(
1412        registry: Arc<dyn crate::ProcessRegistry>,
1413        registration: crate::ProcessRegistration,
1414        grant: Option<crate::ProcessStartGrant>,
1415    ) -> Result<ProcessRecord, PluginError> {
1416        let registration_for_record = registration.clone();
1417        let record = registry.register_process(registration_for_record).await?;
1418        if let Some(grant) = grant {
1419            registry
1420                .grant_handle(&grant.session_scope, &registration.id, grant.descriptor)
1421                .await?;
1422        }
1423        Ok(record)
1424    }
1425
1426    pub(crate) async fn request_process_cancel(
1427        &self,
1428        registry: Arc<dyn crate::ProcessRegistry>,
1429        process_id: &str,
1430        reason: Option<String>,
1431    ) -> Result<ProcessRecord, PluginError> {
1432        // Cancellation is a durable signal: the cancel event is what the
1433        // runner-run process observes, so the inline controller appends it and
1434        // no longer tracks an in-process cancellation token.
1435        registry
1436            .append_event(
1437                process_id,
1438                crate::ProcessEventAppendRequest::cancel_requested(process_id, reason.clone()),
1439            )
1440            .await?;
1441        registry
1442            .get_process(process_id)
1443            .await
1444            .ok_or_else(|| PluginError::Session(format!("unknown process `{process_id}`")))
1445    }
1446}
1447
1448impl std::fmt::Debug for InlineRuntimeEffectController {
1449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1450        f.debug_struct("InlineRuntimeEffectController").finish()
1451    }
1452}