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