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