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