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