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