Skip to main content

lash_core/runtime/effect/
executor.rs

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