Skip to main content

lash_core/runtime/effect/
executor.rs

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