Skip to main content

lash_core/runtime/effect/
executor.rs

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