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 }),
1173 None,
1174 );
1175 }
1176 };
1177 match self.provider.complete(request).await {
1178 Ok(completion) => (Ok(completion.response), Some(completion.call_record)),
1179 Err(failure) => (
1180 Err(llm_call_error_from_transport(failure.error)),
1181 Some(failure.call_record),
1182 ),
1183 }
1184 }
1185}
1186
1187async fn execute_local_sleep(
1188 envelope: RuntimeEffectEnvelope,
1189 cancellation: CancellationToken,
1190 clock: &dyn crate::Clock,
1191) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1192 match envelope.command {
1193 RuntimeEffectCommand::Sleep { duration_ms } => {
1194 sleep_with_cancellation(duration_ms, &cancellation, clock).await?;
1195 Ok(RuntimeEffectOutcome::Sleep)
1196 }
1197 command => Err(RuntimeEffectControllerError::new(
1198 "runtime_effect_local_executor_mismatch",
1199 format!(
1200 "local sleep executor cannot execute {} command",
1201 command.kind().as_str()
1202 ),
1203 )),
1204 }
1205}
1206
1207async fn sleep_with_cancellation(
1208 duration_ms: u64,
1209 cancellation: &CancellationToken,
1210 clock: &dyn crate::Clock,
1211) -> Result<(), RuntimeEffectControllerError> {
1212 let sleep = clock.sleep(std::time::Duration::from_millis(duration_ms));
1213 tokio::pin!(sleep);
1214 tokio::select! {
1215 _ = cancellation.cancelled() => Err(RuntimeEffectControllerError::new(
1216 "runtime_effect_sleep_cancelled",
1217 "runtime effect sleep was cancelled",
1218 )),
1219 _ = &mut sleep => Ok(()),
1220 }
1221}
1222
1223#[derive(Clone, Default)]
1234pub struct InlineRuntimeEffectController;
1235
1236#[async_trait::async_trait]
1237impl AwaitEventResolver for InlineRuntimeEffectController {
1238 fn supports_durable_effects(&self) -> bool {
1239 true
1240 }
1241
1242 async fn await_event_key(
1243 &self,
1244 scope: &ExecutionScope,
1245 wait: AwaitEventWaitIdentity,
1246 ) -> Result<AwaitEventKey, RuntimeError> {
1247 inline_await_events().key_for(scope, wait)
1248 }
1249
1250 async fn resolve_await_event(
1251 &self,
1252 key: &AwaitEventKey,
1253 resolution: Resolution,
1254 ) -> Result<ResolveOutcome, RuntimeError> {
1255 inline_await_events().resolve(key, resolution)
1256 }
1257
1258 async fn await_await_event(
1259 &self,
1260 key: &AwaitEventKey,
1261 cancel: CancellationToken,
1262 deadline: Option<Instant>,
1263 ) -> Result<Resolution, RuntimeError> {
1264 inline_await_events()
1265 .await_resolution(key, cancel, deadline, &crate::SystemClock)
1266 .await
1267 }
1268
1269 async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1270 inline_await_events().revoke_session(session_id)
1271 }
1272
1273 async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1274 inline_await_events().cancel_session(session_id)
1275 }
1276}
1277
1278#[async_trait::async_trait]
1279impl RuntimeEffectController for InlineRuntimeEffectController {
1280 async fn execute_effect(
1281 &self,
1282 envelope: RuntimeEffectEnvelope,
1283 local_executor: RuntimeEffectLocalExecutor<'_>,
1284 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1285 match envelope.command {
1286 RuntimeEffectCommand::AwaitEvent { key } => {
1287 let RuntimeAwaitEventOptions {
1288 cancellation,
1289 deadline,
1290 clock,
1291 } = local_executor.into_await_event_options()?;
1292 let resolution = inline_await_events()
1293 .await_resolution(&key, cancellation, deadline, clock.as_ref())
1294 .await
1295 .map_err(RuntimeEffectControllerError::from)?;
1296 Ok(RuntimeEffectOutcome::AwaitEvent { resolution })
1297 }
1298 RuntimeEffectCommand::Process { command } => {
1299 let execution = local_executor.into_process()?;
1300 let result = tokio::task::spawn(async move { execution.execute(*command).await })
1301 .await
1302 .map_err(|err| {
1303 RuntimeEffectControllerError::new(
1304 "runtime_effect_process_task_join",
1305 format!("inline process effect task failed: {err}"),
1306 )
1307 })??;
1308 Ok(RuntimeEffectOutcome::Process { result })
1309 }
1310 _ => local_executor.execute(envelope).await,
1311 }
1312 }
1313}
1314
1315#[derive(Clone)]
1317pub struct InlineEffectHost {
1318 controller: Arc<dyn RuntimeEffectController>,
1319}
1320
1321impl InlineEffectHost {
1322 pub fn new(controller: Arc<dyn RuntimeEffectController>) -> Self {
1323 Self { controller }
1324 }
1325}
1326
1327impl Default for InlineEffectHost {
1328 fn default() -> Self {
1329 Self::new(Arc::new(InlineRuntimeEffectController))
1330 }
1331}
1332
1333#[async_trait::async_trait]
1334impl AwaitEventResolver for InlineEffectHost {
1335 fn durability_tier(&self) -> crate::DurabilityTier {
1336 self.controller.durability_tier()
1337 }
1338
1339 fn supports_durable_effects(&self) -> bool {
1340 self.controller.supports_durable_effects()
1341 }
1342
1343 async fn await_event_key(
1344 &self,
1345 scope: &ExecutionScope,
1346 wait: AwaitEventWaitIdentity,
1347 ) -> Result<AwaitEventKey, RuntimeError> {
1348 self.controller.await_event_key(scope, wait).await
1349 }
1350
1351 async fn resolve_await_event(
1352 &self,
1353 key: &AwaitEventKey,
1354 resolution: Resolution,
1355 ) -> Result<ResolveOutcome, RuntimeError> {
1356 self.controller.resolve_await_event(key, resolution).await
1357 }
1358
1359 async fn await_await_event(
1360 &self,
1361 key: &AwaitEventKey,
1362 cancel: CancellationToken,
1363 deadline: Option<Instant>,
1364 ) -> Result<Resolution, RuntimeError> {
1365 self.controller
1366 .await_await_event(key, cancel, deadline)
1367 .await
1368 }
1369
1370 async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1371 self.controller
1372 .revoke_await_events_for_session(session_id)
1373 .await
1374 }
1375
1376 async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1377 self.controller
1378 .cancel_await_events_for_session(session_id)
1379 .await
1380 }
1381}
1382
1383#[async_trait::async_trait]
1384impl EffectHost for InlineEffectHost {
1385 fn scoped<'run>(
1386 &'run self,
1387 scope: ExecutionScope,
1388 ) -> Result<ScopedEffectController<'run>, RuntimeError> {
1389 ScopedEffectController::shared(Arc::clone(&self.controller), scope)
1390 }
1391
1392 fn scoped_static(
1393 &self,
1394 scope: ExecutionScope,
1395 ) -> Result<Option<ScopedEffectController<'static>>, RuntimeError> {
1396 Ok(Some(ScopedEffectController::shared(
1397 Arc::clone(&self.controller),
1398 scope,
1399 )?))
1400 }
1401}
1402
1403impl InlineRuntimeEffectController {
1404 pub(crate) async fn start_process(
1412 registry: Arc<dyn crate::ProcessRegistry>,
1413 registration: crate::ProcessRegistration,
1414 grant: Option<crate::ProcessStartGrant>,
1415 ) -> Result<ProcessRecord, PluginError> {
1416 let registration_for_record = registration.clone();
1417 let record = registry.register_process(registration_for_record).await?;
1418 if let Some(grant) = grant {
1419 registry
1420 .grant_handle(&grant.session_scope, ®istration.id, grant.descriptor)
1421 .await?;
1422 }
1423 Ok(record)
1424 }
1425
1426 pub(crate) async fn request_process_cancel(
1427 &self,
1428 registry: Arc<dyn crate::ProcessRegistry>,
1429 process_id: &str,
1430 reason: Option<String>,
1431 ) -> Result<ProcessRecord, PluginError> {
1432 registry
1436 .append_event(
1437 process_id,
1438 crate::ProcessEventAppendRequest::cancel_requested(process_id, reason.clone()),
1439 )
1440 .await?;
1441 registry
1442 .get_process(process_id)
1443 .await
1444 .ok_or_else(|| PluginError::Session(format!("unknown process `{process_id}`")))
1445 }
1446}
1447
1448impl std::fmt::Debug for InlineRuntimeEffectController {
1449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1450 f.debug_struct("InlineRuntimeEffectController").finish()
1451 }
1452}