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    /// Read a keyed promise without waiting for or resolving it.
380    ///
381    /// Turn owners use this as a synchronous start gate before beginning a
382    /// new effect. Durable owners must perform that read through their
383    /// handler-scoped, replay-aware controller: its result affects subsequent
384    /// command order and therefore must replay identically after an owner
385    /// crash. An unresolved promise returns `None` and remains open.
386    async fn peek_await_event(
387        &self,
388        key: &AwaitEventKey,
389    ) -> Result<Option<Resolution>, RuntimeError> {
390        if key.wait.is_turn_control() {
391            return super::await_events::inline_await_events().peek_resolution(key);
392        }
393        Err(RuntimeError::new(
394            "await_event_unsupported",
395            "this effect boundary does not support await-event reads",
396        ))
397    }
398
399    async fn await_await_event(
400        &self,
401        key: &AwaitEventKey,
402        cancel: CancellationToken,
403        deadline: Option<Instant>,
404    ) -> Result<Resolution, RuntimeError> {
405        if key.wait.is_turn_control() {
406            return super::await_events::inline_await_events()
407                .await_resolution(key, cancel, deadline, &crate::SystemClock)
408                .await;
409        }
410        Err(RuntimeError::new(
411            "await_event_unsupported",
412            "this effect boundary does not support await-event waits",
413        ))
414    }
415
416    async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
417        super::await_events::inline_await_events().revoke_session(session_id)
418    }
419
420    /// Cancel every *outstanding* durable wait for `session_id` without
421    /// deleting the session: each waiter receives a terminal
422    /// [`Resolution::Cancelled`] instead of hanging, late resolves observe
423    /// that terminal, and waits registered afterwards behave normally — in
424    /// contrast to [`revoke_await_events_for_session`](Self::revoke_await_events_for_session),
425    /// which tombstones the session's waits forever.
426    ///
427    /// The default errors loudly: an effect boundary that tracks durable waits
428    /// must implement this to honor the host lever, and one that cannot must
429    /// not silently claim success.
430    async fn cancel_await_events_for_session(&self, _session_id: &str) -> Result<(), RuntimeError> {
431        Err(RuntimeError::new(
432            "await_event_cancel_unsupported",
433            "this effect boundary does not support cancelling durable waits",
434        ))
435    }
436}
437
438/// Deployment-level factory for scoped effect controllers.
439#[async_trait::async_trait]
440pub trait EffectHost: AwaitEventResolver {
441    fn scoped<'run>(
442        &'run self,
443        scope: ExecutionScope,
444    ) -> Result<ScopedEffectController<'run>, RuntimeError>;
445
446    fn scoped_static(
447        &self,
448        _scope: ExecutionScope,
449    ) -> Result<Option<ScopedEffectController<'static>>, RuntimeError> {
450        Ok(None)
451    }
452}
453
454/// Boundary for nondeterministic runtime work.
455#[async_trait::async_trait]
456pub trait RuntimeEffectController: AwaitEventResolver {
457    /// Advises an engine to end the current in-process execution segment at a
458    /// quiescent point. Engines may decline when live state is not capturable,
459    /// but must make progress before returning another decline. In particular,
460    /// an engine must not repeatedly return the same boundary and unchanged
461    /// durable-wait state in one invocation; a host may bound and retry such a
462    /// non-progressing invocation.
463    fn wants_segment_boundary(&self, _progress: &SegmentProgress) -> Option<BoundaryReason> {
464        None
465    }
466
467    /// Whether this controller can safely accept overlapping `execute_effect`
468    /// calls from one runtime coordinator.
469    ///
470    /// Local and store-backed controllers can usually fan out independent
471    /// effects. Some workflow substrates expose a single ordered journal
472    /// context where native operations must be awaited immediately before the
473    /// next context call is issued. Those controllers should return `false` so
474    /// coordinators serialize child effects while still replaying each child by
475    /// its own stable key.
476    fn supports_concurrent_effects(&self) -> bool {
477        true
478    }
479
480    async fn execute_effect(
481        &self,
482        envelope: RuntimeEffectEnvelope,
483        local_executor: RuntimeEffectLocalExecutor<'_>,
484    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError>;
485}
486
487#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
488pub struct SegmentProgress {
489    pub effects_executed: u64,
490    pub journaled_bytes_estimate: Option<u64>,
491}
492
493#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
494#[serde(rename_all = "snake_case")]
495pub enum BoundaryReason {
496    JournalBudget,
497    DurationCap,
498}
499
500/// Runtime-internal handle for effect-controller references carried through
501/// per-turn execution contexts.
502#[derive(Clone)]
503pub(crate) enum RuntimeEffectControllerHandle<'run> {
504    Borrowed(ScopedEffectController<'run>),
505    #[cfg(any(test, feature = "testing"))]
506    Shared {
507        controller: Arc<dyn RuntimeEffectController>,
508        scope: ExecutionScope,
509    },
510}
511
512impl<'run> RuntimeEffectControllerHandle<'run> {
513    pub(crate) fn borrowed(scoped: ScopedEffectController<'run>) -> Self {
514        Self::Borrowed(scoped)
515    }
516
517    #[cfg(any(test, feature = "testing"))]
518    pub(crate) fn shared(controller: Arc<dyn RuntimeEffectController>) -> Self {
519        Self::Shared {
520            controller,
521            scope: ExecutionScope::runtime_operation("test-runtime-effect-controller"),
522        }
523    }
524
525    pub(crate) fn controller(&self) -> &dyn RuntimeEffectController {
526        match self {
527            Self::Borrowed(scoped) => scoped.controller(),
528            #[cfg(any(test, feature = "testing"))]
529            Self::Shared { controller, .. } => controller.as_ref(),
530        }
531    }
532
533    pub(crate) fn scoped(&self) -> ScopedEffectController<'_> {
534        match self {
535            Self::Borrowed(scoped) => scoped.clone(),
536            #[cfg(any(test, feature = "testing"))]
537            Self::Shared { controller, scope } => {
538                ScopedEffectController::shared(Arc::clone(controller), scope.clone())
539                    .expect("runtime effect controller handle carries a valid scope")
540            }
541        }
542    }
543
544    pub(crate) fn clone_scoped(&self) -> RuntimeEffectControllerHandle<'run> {
545        self.clone()
546    }
547}
548
549#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)]
550#[error("{code}: {message}")]
551pub struct RuntimeEffectControllerError {
552    pub code: String,
553    pub message: String,
554}
555
556impl RuntimeEffectControllerError {
557    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
558        Self {
559            code: code.into(),
560            message: message.into(),
561        }
562    }
563
564    pub(super) fn wrong_outcome(expected: RuntimeEffectKind, actual: RuntimeEffectKind) -> Self {
565        Self::new(
566            "runtime_effect_wrong_outcome",
567            format!(
568                "expected {} outcome, got {}",
569                expected.as_str(),
570                actual.as_str()
571            ),
572        )
573    }
574
575    pub(crate) fn into_runtime_error(self) -> RuntimeError {
576        RuntimeError::new(self.code, self.message)
577    }
578}
579
580impl From<RuntimeError> for RuntimeEffectControllerError {
581    fn from(err: RuntimeError) -> Self {
582        Self::new(err.code.as_str(), err.message)
583    }
584}
585
586impl From<PluginError> for RuntimeEffectControllerError {
587    fn from(err: PluginError) -> Self {
588        Self::new("plugin", err.to_string())
589    }
590}
591
592impl From<crate::StoreError> for RuntimeEffectControllerError {
593    fn from(err: crate::StoreError) -> Self {
594        Self::new("runtime_store", err.to_string())
595    }
596}
597
598// =============================================================================
599// Local executor (per-effect borrowed runner state)
600// =============================================================================
601
602#[async_trait::async_trait]
603pub(crate) trait ProcessRunner: Send + Sync {
604    async fn run_process(
605        &self,
606        registration: crate::ProcessRegistration,
607        execution_context: crate::ProcessExecutionContext,
608        registry: Arc<dyn ProcessRegistry>,
609        scoped_effect_controller: crate::ScopedEffectController<'_>,
610        cancellation: CancellationToken,
611        handover: Option<crate::SegmentHandover>,
612    ) -> crate::ProcessRunOutcome;
613}
614
615pub struct ProcessLocalExecution {
616    pub registry: Arc<dyn ProcessRegistry>,
617    pub process_work_driver: Option<crate::ProcessWorkDriver>,
618}
619
620impl ProcessLocalExecution {
621    pub async fn execute(
622        self,
623        command: ProcessCommand,
624    ) -> Result<ProcessEffectOutcome, RuntimeEffectControllerError> {
625        let Self {
626            registry,
627            process_work_driver,
628        } = self;
629        match command {
630            ProcessCommand::Start {
631                registration,
632                grant,
633                execution_context: _,
634            } => {
635                let record =
636                    InlineRuntimeEffectController::start_process(registry, registration, grant)
637                        .await?;
638                if let Some(driver) = process_work_driver.as_ref() {
639                    driver.claim_and_run_pending("process_start").await?;
640                }
641                Ok(ProcessEffectOutcome::Start {
642                    record: Box::new(record),
643                })
644            }
645            ProcessCommand::List {
646                session_scope,
647                mode,
648            } => {
649                let entries = match mode {
650                    crate::ProcessListMode::Live => {
651                        registry.list_live_handle_grants(&session_scope).await?
652                    }
653                    crate::ProcessListMode::All => {
654                        registry.list_handle_grants(&session_scope).await?
655                    }
656                };
657                Ok(ProcessEffectOutcome::List { entries })
658            }
659            ProcessCommand::Transfer {
660                from_scope,
661                to_scope,
662                process_ids,
663            } => {
664                registry
665                    .transfer_handle_grants(&from_scope, &to_scope, &process_ids)
666                    .await?;
667                Ok(ProcessEffectOutcome::Transfer)
668            }
669            ProcessCommand::DeleteSession { session_id } => {
670                let report = registry.delete_session_process_state(&session_id).await?;
671                Ok(ProcessEffectOutcome::DeleteSession { report })
672            }
673            ProcessCommand::Await { process_id } => {
674                let output = if let Some(driver) = process_work_driver.as_ref() {
675                    driver.await_terminal(&process_id).await?
676                } else {
677                    crate::ProcessAwaiter::polling(registry)
678                        .await_terminal(&process_id)
679                        .await?
680                };
681                Ok(ProcessEffectOutcome::Await { output })
682            }
683            ProcessCommand::Cancel { process_id, reason } => {
684                let record = InlineRuntimeEffectController
685                    .request_process_cancel(registry, &process_id, reason)
686                    .await?;
687                Ok(ProcessEffectOutcome::Cancel {
688                    record: Box::new(record),
689                })
690            }
691            ProcessCommand::Signal {
692                process_id,
693                request,
694                ..
695            } => {
696                let result = registry.append_event(&process_id, request).await?;
697                Ok(ProcessEffectOutcome::Signal {
698                    event: Box::new(result.event),
699                })
700            }
701        }
702    }
703}
704
705pub(super) struct LocalTurnEffectRunner<'a, 'run> {
706    driver: &'a mut RuntimeTurnDriver<'run>,
707    machine: &'a mut crate::TurnMachine,
708    event_tx: mpsc::Sender<RuntimeStreamEvent>,
709    cancellation: CancellationToken,
710}
711
712pub(super) struct LocalDirectEffectRunner {
713    provider: ProviderHandle,
714    attachment_store: Arc<crate::SessionAttachmentStore>,
715}
716
717struct LocalToolBatchEffectRunner<'run> {
718    context: crate::RuntimeExecutionContext<'run>,
719    child_trace_hooks: HashMap<String, crate::ToolChildExecutionTraceHook>,
720}
721
722#[async_trait::async_trait]
723trait RuntimeEffectLocalRunner: Send {
724    async fn execute(
725        self: Box<Self>,
726        envelope: RuntimeEffectEnvelope,
727    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError>;
728}
729
730#[cfg(any(test, feature = "testing"))]
731type TestingRuntimeEffectLocalRunnerFn<'run> = dyn FnOnce(
732        RuntimeEffectEnvelope,
733    ) -> Pin<
734        Box<
735            dyn Future<Output = Result<RuntimeEffectOutcome, RuntimeEffectControllerError>>
736                + Send
737                + 'run,
738        >,
739    > + Send
740    + 'run;
741
742#[cfg(any(test, feature = "testing"))]
743struct TestingRuntimeEffectLocalRunner<'run> {
744    run: Box<TestingRuntimeEffectLocalRunnerFn<'run>>,
745}
746
747type DurableStepLocalRunnerFn<'run> = dyn FnOnce(
748        serde_json::Value,
749    ) -> Pin<
750        Box<
751            dyn Future<Output = Result<serde_json::Value, RuntimeEffectControllerError>>
752                + Send
753                + 'run,
754        >,
755    > + Send
756    + 'run;
757
758struct DurableStepLocalRunner<'run> {
759    run: Box<DurableStepLocalRunnerFn<'run>>,
760}
761
762enum RuntimeEffectLocalExecutorState<'run> {
763    Unavailable,
764    SleepOnly {
765        cancellation: CancellationToken,
766        clock: Arc<dyn crate::Clock>,
767    },
768    ExternalWaitOptions {
769        cancellation: CancellationToken,
770        deadline: Option<Instant>,
771        clock: Arc<dyn crate::Clock>,
772    },
773    Process(ProcessLocalExecution),
774    Runner(Box<dyn RuntimeEffectLocalRunner + Send + 'run>),
775}
776
777/// Scoped local executor provided to a [`RuntimeEffectController`] for one effect.
778///
779/// Durable controllers may ignore it and replay their own recorded result. The
780/// default inline controller delegates to it, so local provider/tool/checkpoint
781/// work still crosses the same `execute_effect` boundary as durable controllers.
782pub struct RuntimeEffectLocalExecutor<'run> {
783    state: RuntimeEffectLocalExecutorState<'run>,
784}
785
786impl<'run> RuntimeEffectLocalExecutor<'run> {
787    pub fn unavailable() -> Self {
788        Self {
789            state: RuntimeEffectLocalExecutorState::Unavailable,
790        }
791    }
792
793    pub fn sleep(cancellation: CancellationToken) -> Self {
794        Self::sleep_with_clock(cancellation, Arc::new(crate::SystemClock))
795    }
796
797    pub fn sleep_with_clock(cancellation: CancellationToken, clock: Arc<dyn crate::Clock>) -> Self {
798        Self {
799            state: RuntimeEffectLocalExecutorState::SleepOnly {
800                cancellation,
801                clock,
802            },
803        }
804    }
805
806    pub fn await_event(cancellation: CancellationToken, deadline: Option<Instant>) -> Self {
807        Self::await_event_with_clock(cancellation, deadline, Arc::new(crate::SystemClock))
808    }
809
810    pub fn await_event_with_clock(
811        cancellation: CancellationToken,
812        deadline: Option<Instant>,
813        clock: Arc<dyn crate::Clock>,
814    ) -> Self {
815        Self {
816            state: RuntimeEffectLocalExecutorState::ExternalWaitOptions {
817                cancellation,
818                deadline,
819                clock,
820            },
821        }
822    }
823
824    pub fn processes(
825        registry: Arc<dyn ProcessRegistry>,
826        process_work_driver: Option<crate::ProcessWorkDriver>,
827    ) -> Self {
828        Self {
829            state: RuntimeEffectLocalExecutorState::Process(ProcessLocalExecution {
830                registry,
831                process_work_driver,
832            }),
833        }
834    }
835
836    pub fn durable_step<F, Fut>(run: F) -> Self
837    where
838        F: FnOnce(serde_json::Value) -> Fut + Send + 'run,
839        Fut: Future<Output = Result<serde_json::Value, RuntimeError>> + Send + 'run,
840    {
841        Self {
842            state: RuntimeEffectLocalExecutorState::Runner(Box::new(DurableStepLocalRunner {
843                run: Box::new(move |input| {
844                    Box::pin(async move { run(input).await.map_err(Into::into) })
845                }),
846            })),
847        }
848    }
849
850    #[cfg(any(test, feature = "testing"))]
851    pub fn testing<F, Fut>(run: F) -> Self
852    where
853        F: FnOnce(RuntimeEffectEnvelope) -> Fut + Send + 'run,
854        Fut: Future<Output = Result<RuntimeEffectOutcome, RuntimeEffectControllerError>>
855            + Send
856            + 'run,
857    {
858        Self {
859            state: RuntimeEffectLocalExecutorState::Runner(Box::new(
860                TestingRuntimeEffectLocalRunner {
861                    run: Box::new(move |envelope| Box::pin(run(envelope))),
862                },
863            )),
864        }
865    }
866
867    pub(in crate::runtime) fn turn<'scope>(
868        driver: &'run mut RuntimeTurnDriver<'scope>,
869        machine: &'run mut crate::TurnMachine,
870        event_tx: mpsc::Sender<RuntimeStreamEvent>,
871        cancellation: CancellationToken,
872    ) -> Self
873    where
874        'scope: 'run,
875    {
876        Self {
877            state: RuntimeEffectLocalExecutorState::Runner(Box::new(LocalTurnEffectRunner {
878                driver,
879                machine,
880                event_tx,
881                cancellation,
882            })),
883        }
884    }
885
886    pub(in crate::runtime) fn direct(
887        provider: ProviderHandle,
888        attachment_store: Arc<crate::SessionAttachmentStore>,
889    ) -> Self {
890        Self {
891            state: RuntimeEffectLocalExecutorState::Runner(Box::new(LocalDirectEffectRunner {
892                provider,
893                attachment_store,
894            })),
895        }
896    }
897
898    pub(crate) fn tool_batch(
899        context: crate::RuntimeExecutionContext<'run>,
900        child_trace_hooks: HashMap<String, crate::ToolChildExecutionTraceHook>,
901    ) -> Self {
902        Self {
903            state: RuntimeEffectLocalExecutorState::Runner(Box::new(LocalToolBatchEffectRunner {
904                context,
905                child_trace_hooks,
906            })),
907        }
908    }
909
910    pub async fn execute(
911        self,
912        envelope: RuntimeEffectEnvelope,
913    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
914        match self.state {
915            RuntimeEffectLocalExecutorState::Runner(runner) => runner.execute(envelope).await,
916            RuntimeEffectLocalExecutorState::SleepOnly {
917                cancellation,
918                clock,
919            } => execute_local_sleep(envelope, cancellation, clock.as_ref()).await,
920            RuntimeEffectLocalExecutorState::ExternalWaitOptions { .. } => {
921                Err(RuntimeEffectControllerError::new(
922                    "runtime_effect_local_executor_mismatch",
923                    format!(
924                        "local await-event options cannot execute {} command directly",
925                        envelope.command.kind().as_str()
926                    ),
927                ))
928            }
929            RuntimeEffectLocalExecutorState::Unavailable => Err(RuntimeEffectControllerError::new(
930                "runtime_effect_local_executor_unavailable",
931                format!(
932                    "no local executor is available for {}",
933                    envelope.command.kind().as_str()
934                ),
935            )),
936            RuntimeEffectLocalExecutorState::Process(_) => Err(RuntimeEffectControllerError::new(
937                "runtime_effect_local_executor_mismatch",
938                format!(
939                    "process executor cannot execute {} command directly",
940                    envelope.command.kind().as_str()
941                ),
942            )),
943        }
944    }
945
946    pub fn into_process(self) -> Result<ProcessLocalExecution, RuntimeEffectControllerError> {
947        match self.state {
948            RuntimeEffectLocalExecutorState::Process(execution) => Ok(execution),
949            _ => Err(RuntimeEffectControllerError::new(
950                "runtime_effect_local_executor_unavailable",
951                "no process executor is available for process command",
952            )),
953        }
954    }
955
956    pub fn into_await_event_options(
957        self,
958    ) -> Result<RuntimeAwaitEventOptions, RuntimeEffectControllerError> {
959        match self.state {
960            RuntimeEffectLocalExecutorState::ExternalWaitOptions {
961                cancellation,
962                deadline,
963                clock,
964            } => Ok(RuntimeAwaitEventOptions {
965                cancellation,
966                deadline,
967                clock,
968            }),
969            _ => Ok(RuntimeAwaitEventOptions {
970                cancellation: CancellationToken::new(),
971                deadline: None,
972                clock: Arc::new(crate::SystemClock),
973            }),
974        }
975    }
976
977    pub fn into_sleep_cancellation(self) -> CancellationToken {
978        match self.state {
979            RuntimeEffectLocalExecutorState::SleepOnly { cancellation, .. } => cancellation,
980            _ => CancellationToken::new(),
981        }
982    }
983}
984
985#[cfg(any(test, feature = "testing"))]
986#[async_trait::async_trait]
987impl RuntimeEffectLocalRunner for TestingRuntimeEffectLocalRunner<'_> {
988    async fn execute(
989        self: Box<Self>,
990        envelope: RuntimeEffectEnvelope,
991    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
992        (self.run)(envelope).await
993    }
994}
995
996#[async_trait::async_trait]
997impl RuntimeEffectLocalRunner for DurableStepLocalRunner<'_> {
998    async fn execute(
999        self: Box<Self>,
1000        envelope: RuntimeEffectEnvelope,
1001    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1002        match envelope.command {
1003            RuntimeEffectCommand::DurableStep { input, .. } => {
1004                let value = (self.run)(input).await?;
1005                Ok(RuntimeEffectOutcome::DurableStep { value })
1006            }
1007            command => Err(RuntimeEffectControllerError::new(
1008                "runtime_effect_local_executor_mismatch",
1009                format!(
1010                    "local durable step executor cannot execute {} command",
1011                    command.kind().as_str()
1012                ),
1013            )),
1014        }
1015    }
1016}
1017
1018#[async_trait::async_trait]
1019impl RuntimeEffectLocalRunner for LocalToolBatchEffectRunner<'_> {
1020    async fn execute(
1021        self: Box<Self>,
1022        envelope: RuntimeEffectEnvelope,
1023    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1024        match envelope.command {
1025            RuntimeEffectCommand::ToolBatch { batch } => {
1026                let outcome = self
1027                    .context
1028                    .execute_prepared_tool_batch_launches(
1029                        batch,
1030                        envelope.invocation,
1031                        self.child_trace_hooks,
1032                    )
1033                    .await?;
1034                Ok(RuntimeEffectOutcome::ToolBatch {
1035                    launches: outcome.launches,
1036                    triggers: outcome.triggers,
1037                })
1038            }
1039            RuntimeEffectCommand::ToolAttempt {
1040                call,
1041                execution_grant,
1042                attempt,
1043                max_attempts,
1044            } => {
1045                let child_execution_trace_hook = self.child_trace_hooks.get(&call.call_id).cloned();
1046                let outcome = self
1047                    .context
1048                    .execute_prepared_tool_attempt_effect(
1049                        call,
1050                        execution_grant,
1051                        attempt,
1052                        max_attempts,
1053                        envelope.invocation,
1054                        child_execution_trace_hook,
1055                    )
1056                    .await?;
1057                Ok(RuntimeEffectOutcome::ToolAttempt {
1058                    launch: outcome.launch,
1059                    triggers: outcome.triggers,
1060                })
1061            }
1062            command => Err(RuntimeEffectControllerError::new(
1063                "runtime_effect_local_executor_mismatch",
1064                format!(
1065                    "local tool executor cannot execute {} command",
1066                    command.kind().as_str()
1067                ),
1068            )),
1069        }
1070    }
1071}
1072
1073#[async_trait::async_trait]
1074impl RuntimeEffectLocalRunner for LocalTurnEffectRunner<'_, '_> {
1075    async fn execute(
1076        self: Box<Self>,
1077        envelope: RuntimeEffectEnvelope,
1078    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1079        let runner = *self;
1080        match envelope.command {
1081            RuntimeEffectCommand::LlmCall { request } => {
1082                let protocol_iteration = runner.machine.protocol_iteration();
1083                let (result, text_streamed, call_record) = runner
1084                    .driver
1085                    .run_llm_call(
1086                        Arc::new((*request).into_request(None, None)),
1087                        protocol_iteration,
1088                        envelope.invocation,
1089                        &runner.event_tx,
1090                        &runner.cancellation,
1091                    )
1092                    .await;
1093                Ok(RuntimeEffectOutcome::LlmCall {
1094                    result,
1095                    text_streamed,
1096                    call_record,
1097                })
1098            }
1099            RuntimeEffectCommand::ToolBatch { batch } => {
1100                let outcome = runner
1101                    .driver
1102                    .run_tool_batch(
1103                        batch,
1104                        envelope.invocation,
1105                        &runner.event_tx,
1106                        &runner.cancellation,
1107                    )
1108                    .await?;
1109                Ok(RuntimeEffectOutcome::ToolBatch {
1110                    launches: outcome.launches,
1111                    triggers: outcome.triggers,
1112                })
1113            }
1114            RuntimeEffectCommand::ExecCode { language, code } => {
1115                let protocol_iteration = runner.machine.protocol_iteration();
1116                let messages = runner.machine.message_sequence();
1117                Ok(RuntimeEffectOutcome::ExecCode {
1118                    result: runner
1119                        .driver
1120                        .run_exec_code(
1121                            language,
1122                            &code,
1123                            messages,
1124                            protocol_iteration,
1125                            envelope.invocation,
1126                            &runner.event_tx,
1127                            &runner.cancellation,
1128                        )
1129                        .await,
1130                })
1131            }
1132            RuntimeEffectCommand::Checkpoint { checkpoint } => {
1133                Ok(RuntimeEffectOutcome::Checkpoint {
1134                    result: runner
1135                        .driver
1136                        .run_checkpoint(runner.machine, checkpoint, &runner.event_tx)
1137                        .await
1138                        .map_err(RuntimeEffectControllerError::from),
1139                })
1140            }
1141            RuntimeEffectCommand::SyncExecutionEnvironment {
1142                update_machine_config,
1143            } => Ok(RuntimeEffectOutcome::SyncExecutionEnvironment {
1144                result: runner
1145                    .driver
1146                    .refresh_execution_environment(runner.machine, update_machine_config)
1147                    .await
1148                    .map_err(|err| err.to_string()),
1149            }),
1150            RuntimeEffectCommand::Sleep { duration_ms } => {
1151                sleep_with_cancellation(
1152                    duration_ms,
1153                    &runner.cancellation,
1154                    runner.driver.host.core.clock.as_ref(),
1155                )
1156                .await?;
1157                Ok(RuntimeEffectOutcome::Sleep)
1158            }
1159            command => Err(RuntimeEffectControllerError::new(
1160                "runtime_effect_local_executor_mismatch",
1161                format!(
1162                    "local turn executor cannot execute {} command",
1163                    command.kind().as_str()
1164                ),
1165            )),
1166        }
1167    }
1168}
1169
1170#[async_trait::async_trait]
1171impl RuntimeEffectLocalRunner for LocalDirectEffectRunner {
1172    async fn execute(
1173        mut self: Box<Self>,
1174        envelope: RuntimeEffectEnvelope,
1175    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1176        match envelope.command {
1177            RuntimeEffectCommand::Direct { request, .. } => {
1178                let (result, call_record) = self
1179                    .run_direct_llm_request((*request).into_request(
1180                        crate::session_model::transport_stream_events(&self.provider, None),
1181                        None,
1182                    ))
1183                    .await;
1184                Ok(RuntimeEffectOutcome::Direct {
1185                    result,
1186                    call_record,
1187                })
1188            }
1189            RuntimeEffectCommand::Sleep { duration_ms } => {
1190                sleep_with_cancellation(
1191                    duration_ms,
1192                    &CancellationToken::new(),
1193                    &crate::SystemClock,
1194                )
1195                .await?;
1196                Ok(RuntimeEffectOutcome::Sleep)
1197            }
1198            command => Err(RuntimeEffectControllerError::new(
1199                "runtime_effect_local_executor_mismatch",
1200                format!(
1201                    "local direct executor cannot execute {} command",
1202                    command.kind().as_str()
1203                ),
1204            )),
1205        }
1206    }
1207}
1208
1209impl LocalDirectEffectRunner {
1210    async fn run_direct_llm_request(&mut self, request: CoreLlmRequest) -> RuntimeDirectLlmOutcome {
1211        let request = match crate::attachments::resolve_llm_request_attachments(
1212            request,
1213            self.attachment_store.as_ref(),
1214        )
1215        .await
1216        {
1217            Ok(request) => request,
1218            Err(err) => {
1219                return (
1220                    Err(LlmCallError {
1221                        message: err.to_string(),
1222                        retryable: false,
1223                        kind: crate::ProviderFailureKind::Unknown,
1224                        raw: None,
1225                        code: Some("attachment_resolution_failed".to_string()),
1226                        terminal_reason: crate::LlmTerminalReason::ProviderError,
1227                        request_body: None,
1228                        partial_response: None,
1229                    }),
1230                    None,
1231                );
1232            }
1233        };
1234        match self.provider.complete(request).await {
1235            Ok(completion) => (Ok(completion.response), Some(completion.call_record)),
1236            Err(failure) => (
1237                Err(llm_call_error_from_transport(failure.error)),
1238                Some(failure.call_record),
1239            ),
1240        }
1241    }
1242}
1243
1244async fn execute_local_sleep(
1245    envelope: RuntimeEffectEnvelope,
1246    cancellation: CancellationToken,
1247    clock: &dyn crate::Clock,
1248) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1249    match envelope.command {
1250        RuntimeEffectCommand::Sleep { duration_ms } => {
1251            sleep_with_cancellation(duration_ms, &cancellation, clock).await?;
1252            Ok(RuntimeEffectOutcome::Sleep)
1253        }
1254        command => Err(RuntimeEffectControllerError::new(
1255            "runtime_effect_local_executor_mismatch",
1256            format!(
1257                "local sleep executor cannot execute {} command",
1258                command.kind().as_str()
1259            ),
1260        )),
1261    }
1262}
1263
1264async fn sleep_with_cancellation(
1265    duration_ms: u64,
1266    cancellation: &CancellationToken,
1267    clock: &dyn crate::Clock,
1268) -> Result<(), RuntimeEffectControllerError> {
1269    let sleep = clock.sleep(std::time::Duration::from_millis(duration_ms));
1270    tokio::pin!(sleep);
1271    tokio::select! {
1272        _ = cancellation.cancelled() => Err(RuntimeEffectControllerError::new(
1273            "runtime_effect_sleep_cancelled",
1274            "runtime effect sleep was cancelled",
1275        )),
1276        _ = &mut sleep => Ok(()),
1277    }
1278}
1279
1280// =============================================================================
1281// Default in-process effect controller
1282// =============================================================================
1283
1284/// Default in-process effect controller.
1285///
1286/// The inline controller executes local runners in process, provides in-memory
1287/// await-event resolution, and exposes durable-tool-effect semantics for local
1288/// runs. It does not make in-flight effects crash durable; workflow adapters
1289/// provide that by recording outcomes in their own history.
1290#[derive(Clone, Default)]
1291pub struct InlineRuntimeEffectController;
1292
1293#[async_trait::async_trait]
1294impl AwaitEventResolver for InlineRuntimeEffectController {
1295    fn supports_durable_effects(&self) -> bool {
1296        true
1297    }
1298
1299    async fn await_event_key(
1300        &self,
1301        scope: &ExecutionScope,
1302        wait: AwaitEventWaitIdentity,
1303    ) -> Result<AwaitEventKey, RuntimeError> {
1304        inline_await_events().key_for(scope, wait)
1305    }
1306
1307    async fn resolve_await_event(
1308        &self,
1309        key: &AwaitEventKey,
1310        resolution: Resolution,
1311    ) -> Result<ResolveOutcome, RuntimeError> {
1312        inline_await_events().resolve(key, resolution)
1313    }
1314
1315    async fn peek_await_event(
1316        &self,
1317        key: &AwaitEventKey,
1318    ) -> Result<Option<Resolution>, RuntimeError> {
1319        inline_await_events().peek_resolution(key)
1320    }
1321
1322    async fn await_await_event(
1323        &self,
1324        key: &AwaitEventKey,
1325        cancel: CancellationToken,
1326        deadline: Option<Instant>,
1327    ) -> Result<Resolution, RuntimeError> {
1328        inline_await_events()
1329            .await_resolution(key, cancel, deadline, &crate::SystemClock)
1330            .await
1331    }
1332
1333    async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1334        inline_await_events().revoke_session(session_id)
1335    }
1336
1337    async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1338        inline_await_events().cancel_session(session_id)
1339    }
1340}
1341
1342#[async_trait::async_trait]
1343impl RuntimeEffectController for InlineRuntimeEffectController {
1344    async fn execute_effect(
1345        &self,
1346        envelope: RuntimeEffectEnvelope,
1347        local_executor: RuntimeEffectLocalExecutor<'_>,
1348    ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1349        match envelope.command {
1350            RuntimeEffectCommand::AwaitEvent { key } => {
1351                let RuntimeAwaitEventOptions {
1352                    cancellation,
1353                    deadline,
1354                    clock,
1355                } = local_executor.into_await_event_options()?;
1356                let resolution = inline_await_events()
1357                    .await_resolution(&key, cancellation, deadline, clock.as_ref())
1358                    .await
1359                    .map_err(RuntimeEffectControllerError::from)?;
1360                Ok(RuntimeEffectOutcome::AwaitEvent { resolution })
1361            }
1362            RuntimeEffectCommand::Process { command } => {
1363                let execution = local_executor.into_process()?;
1364                let result = tokio::task::spawn(async move { execution.execute(*command).await })
1365                    .await
1366                    .map_err(|err| {
1367                        RuntimeEffectControllerError::new(
1368                            "runtime_effect_process_task_join",
1369                            format!("inline process effect task failed: {err}"),
1370                        )
1371                    })??;
1372                Ok(RuntimeEffectOutcome::Process { result })
1373            }
1374            _ => local_executor.execute(envelope).await,
1375        }
1376    }
1377}
1378
1379/// In-process deployment effect host.
1380#[derive(Clone)]
1381pub struct InlineEffectHost {
1382    controller: Arc<dyn RuntimeEffectController>,
1383}
1384
1385impl InlineEffectHost {
1386    pub fn new(controller: Arc<dyn RuntimeEffectController>) -> Self {
1387        Self { controller }
1388    }
1389}
1390
1391impl Default for InlineEffectHost {
1392    fn default() -> Self {
1393        Self::new(Arc::new(InlineRuntimeEffectController))
1394    }
1395}
1396
1397#[async_trait::async_trait]
1398impl AwaitEventResolver for InlineEffectHost {
1399    fn durability_tier(&self) -> crate::DurabilityTier {
1400        self.controller.durability_tier()
1401    }
1402
1403    fn supports_durable_effects(&self) -> bool {
1404        self.controller.supports_durable_effects()
1405    }
1406
1407    async fn await_event_key(
1408        &self,
1409        scope: &ExecutionScope,
1410        wait: AwaitEventWaitIdentity,
1411    ) -> Result<AwaitEventKey, RuntimeError> {
1412        self.controller.await_event_key(scope, wait).await
1413    }
1414
1415    async fn resolve_await_event(
1416        &self,
1417        key: &AwaitEventKey,
1418        resolution: Resolution,
1419    ) -> Result<ResolveOutcome, RuntimeError> {
1420        self.controller.resolve_await_event(key, resolution).await
1421    }
1422
1423    async fn peek_await_event(
1424        &self,
1425        key: &AwaitEventKey,
1426    ) -> Result<Option<Resolution>, RuntimeError> {
1427        self.controller.peek_await_event(key).await
1428    }
1429
1430    async fn await_await_event(
1431        &self,
1432        key: &AwaitEventKey,
1433        cancel: CancellationToken,
1434        deadline: Option<Instant>,
1435    ) -> Result<Resolution, RuntimeError> {
1436        self.controller
1437            .await_await_event(key, cancel, deadline)
1438            .await
1439    }
1440
1441    async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1442        self.controller
1443            .revoke_await_events_for_session(session_id)
1444            .await
1445    }
1446
1447    async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1448        self.controller
1449            .cancel_await_events_for_session(session_id)
1450            .await
1451    }
1452}
1453
1454#[async_trait::async_trait]
1455impl EffectHost for InlineEffectHost {
1456    fn scoped<'run>(
1457        &'run self,
1458        scope: ExecutionScope,
1459    ) -> Result<ScopedEffectController<'run>, RuntimeError> {
1460        ScopedEffectController::shared(Arc::clone(&self.controller), scope)
1461    }
1462
1463    fn scoped_static(
1464        &self,
1465        scope: ExecutionScope,
1466    ) -> Result<Option<ScopedEffectController<'static>>, RuntimeError> {
1467        Ok(Some(ScopedEffectController::shared(
1468            Arc::clone(&self.controller),
1469            scope,
1470        )?))
1471    }
1472}
1473
1474impl InlineRuntimeEffectController {
1475    /// Register the process (and any handle grant) into the durable registry.
1476    ///
1477    /// The inline controller no longer runs the process here: the registry's
1478    /// non-terminal row *is* the durable work queue, and the host-owned
1479    /// [`ProcessWorkDriver`](crate::ProcessWorkDriver) is the sole executor.
1480    /// Registering the row is all this path does; the control seam drives the
1481    /// host driver after a successful start.
1482    pub(crate) async fn start_process(
1483        registry: Arc<dyn crate::ProcessRegistry>,
1484        registration: crate::ProcessRegistration,
1485        grant: Option<crate::ProcessStartGrant>,
1486    ) -> Result<ProcessRecord, PluginError> {
1487        let registration_for_record = registration.clone();
1488        let record = registry.register_process(registration_for_record).await?;
1489        if let Some(grant) = grant {
1490            registry
1491                .grant_handle(&grant.session_scope, &registration.id, grant.descriptor)
1492                .await?;
1493        }
1494        Ok(record)
1495    }
1496
1497    pub(crate) async fn request_process_cancel(
1498        &self,
1499        registry: Arc<dyn crate::ProcessRegistry>,
1500        process_id: &str,
1501        reason: Option<String>,
1502    ) -> Result<ProcessRecord, PluginError> {
1503        // Cancellation is a durable signal: the cancel event is what the
1504        // runner-run process observes, so the inline controller appends it and
1505        // no longer tracks an in-process cancellation token.
1506        registry
1507            .append_event(
1508                process_id,
1509                crate::ProcessEventAppendRequest::cancel_requested(process_id, reason.clone()),
1510            )
1511            .await?;
1512        registry
1513            .get_process(process_id)
1514            .await
1515            .ok_or_else(|| PluginError::Session(format!("unknown process `{process_id}`")))
1516    }
1517}
1518
1519impl std::fmt::Debug for InlineRuntimeEffectController {
1520    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1521        f.debug_struct("InlineRuntimeEffectController").finish()
1522    }
1523}