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