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