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