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