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