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