Skip to main content

lash_core/runtime/effect/
executor.rs

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