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 pub fn into_sleep_cancellation(self) -> CancellationToken {
958 match self.state {
959 RuntimeEffectLocalExecutorState::SleepOnly { cancellation, .. } => cancellation,
960 _ => CancellationToken::new(),
961 }
962 }
963}
964
965#[cfg(any(test, feature = "testing"))]
966#[async_trait::async_trait]
967impl RuntimeEffectLocalRunner for TestingRuntimeEffectLocalRunner<'_> {
968 async fn execute(
969 self: Box<Self>,
970 envelope: RuntimeEffectEnvelope,
971 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
972 (self.run)(envelope).await
973 }
974}
975
976#[async_trait::async_trait]
977impl RuntimeEffectLocalRunner for DurableStepLocalRunner<'_> {
978 async fn execute(
979 self: Box<Self>,
980 envelope: RuntimeEffectEnvelope,
981 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
982 match envelope.command {
983 RuntimeEffectCommand::DurableStep { input, .. } => {
984 let value = (self.run)(input).await?;
985 Ok(RuntimeEffectOutcome::DurableStep { value })
986 }
987 command => Err(RuntimeEffectControllerError::new(
988 "runtime_effect_local_executor_mismatch",
989 format!(
990 "local durable step executor cannot execute {} command",
991 command.kind().as_str()
992 ),
993 )),
994 }
995 }
996}
997
998#[async_trait::async_trait]
999impl RuntimeEffectLocalRunner for LocalToolBatchEffectRunner<'_> {
1000 async fn execute(
1001 self: Box<Self>,
1002 envelope: RuntimeEffectEnvelope,
1003 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1004 match envelope.command {
1005 RuntimeEffectCommand::ToolBatch { batch } => {
1006 let outcome = self
1007 .context
1008 .execute_prepared_tool_batch_launches(
1009 batch,
1010 envelope.invocation,
1011 self.child_trace_hooks,
1012 )
1013 .await?;
1014 Ok(RuntimeEffectOutcome::ToolBatch {
1015 launches: outcome.launches,
1016 triggers: outcome.triggers,
1017 })
1018 }
1019 RuntimeEffectCommand::ToolAttempt {
1020 call,
1021 execution_grant,
1022 attempt,
1023 max_attempts,
1024 } => {
1025 let child_execution_trace_hook = self.child_trace_hooks.get(&call.call_id).cloned();
1026 let outcome = self
1027 .context
1028 .execute_prepared_tool_attempt_effect(
1029 call,
1030 execution_grant,
1031 attempt,
1032 max_attempts,
1033 envelope.invocation,
1034 child_execution_trace_hook,
1035 )
1036 .await?;
1037 Ok(RuntimeEffectOutcome::ToolAttempt {
1038 launch: outcome.launch,
1039 triggers: outcome.triggers,
1040 })
1041 }
1042 command => Err(RuntimeEffectControllerError::new(
1043 "runtime_effect_local_executor_mismatch",
1044 format!(
1045 "local tool executor cannot execute {} command",
1046 command.kind().as_str()
1047 ),
1048 )),
1049 }
1050 }
1051}
1052
1053#[async_trait::async_trait]
1054impl RuntimeEffectLocalRunner for LocalTurnEffectRunner<'_, '_> {
1055 async fn execute(
1056 self: Box<Self>,
1057 envelope: RuntimeEffectEnvelope,
1058 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1059 let runner = *self;
1060 match envelope.command {
1061 RuntimeEffectCommand::LlmCall { request } => {
1062 let protocol_iteration = runner.machine.protocol_iteration();
1063 let (result, text_streamed, call_record) = runner
1064 .driver
1065 .run_llm_call(
1066 Arc::new((*request).into_request(None, None)),
1067 protocol_iteration,
1068 envelope.invocation,
1069 &runner.event_tx,
1070 &runner.cancellation,
1071 )
1072 .await;
1073 Ok(RuntimeEffectOutcome::LlmCall {
1074 result,
1075 text_streamed,
1076 call_record,
1077 })
1078 }
1079 RuntimeEffectCommand::ToolBatch { batch } => {
1080 let outcome = runner
1081 .driver
1082 .run_tool_batch(
1083 batch,
1084 envelope.invocation,
1085 &runner.event_tx,
1086 &runner.cancellation,
1087 )
1088 .await?;
1089 Ok(RuntimeEffectOutcome::ToolBatch {
1090 launches: outcome.launches,
1091 triggers: outcome.triggers,
1092 })
1093 }
1094 RuntimeEffectCommand::ExecCode { language, code } => {
1095 let protocol_iteration = runner.machine.protocol_iteration();
1096 let messages = runner.machine.message_sequence();
1097 Ok(RuntimeEffectOutcome::ExecCode {
1098 result: runner
1099 .driver
1100 .run_exec_code(
1101 language,
1102 &code,
1103 messages,
1104 protocol_iteration,
1105 envelope.invocation,
1106 &runner.event_tx,
1107 &runner.cancellation,
1108 )
1109 .await,
1110 })
1111 }
1112 RuntimeEffectCommand::Checkpoint { checkpoint } => {
1113 Ok(RuntimeEffectOutcome::Checkpoint {
1114 result: runner
1115 .driver
1116 .run_checkpoint(runner.machine, checkpoint, &runner.event_tx)
1117 .await
1118 .map_err(RuntimeEffectControllerError::from),
1119 })
1120 }
1121 RuntimeEffectCommand::SyncExecutionEnvironment {
1122 update_machine_config,
1123 } => Ok(RuntimeEffectOutcome::SyncExecutionEnvironment {
1124 result: runner
1125 .driver
1126 .refresh_execution_environment(runner.machine, update_machine_config)
1127 .await
1128 .map_err(|err| err.to_string()),
1129 }),
1130 RuntimeEffectCommand::Sleep { duration_ms } => {
1131 sleep_with_cancellation(
1132 duration_ms,
1133 &runner.cancellation,
1134 runner.driver.host.core.clock.as_ref(),
1135 )
1136 .await?;
1137 Ok(RuntimeEffectOutcome::Sleep)
1138 }
1139 command => Err(RuntimeEffectControllerError::new(
1140 "runtime_effect_local_executor_mismatch",
1141 format!(
1142 "local turn executor cannot execute {} command",
1143 command.kind().as_str()
1144 ),
1145 )),
1146 }
1147 }
1148}
1149
1150#[async_trait::async_trait]
1151impl RuntimeEffectLocalRunner for LocalDirectEffectRunner {
1152 async fn execute(
1153 mut self: Box<Self>,
1154 envelope: RuntimeEffectEnvelope,
1155 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1156 match envelope.command {
1157 RuntimeEffectCommand::Direct { request, .. } => {
1158 let (result, call_record) = self
1159 .run_direct_llm_request((*request).into_request(
1160 crate::session_model::transport_stream_events(&self.provider, None),
1161 None,
1162 ))
1163 .await;
1164 Ok(RuntimeEffectOutcome::Direct {
1165 result,
1166 call_record,
1167 })
1168 }
1169 RuntimeEffectCommand::Sleep { duration_ms } => {
1170 sleep_with_cancellation(
1171 duration_ms,
1172 &CancellationToken::new(),
1173 &crate::SystemClock,
1174 )
1175 .await?;
1176 Ok(RuntimeEffectOutcome::Sleep)
1177 }
1178 command => Err(RuntimeEffectControllerError::new(
1179 "runtime_effect_local_executor_mismatch",
1180 format!(
1181 "local direct executor cannot execute {} command",
1182 command.kind().as_str()
1183 ),
1184 )),
1185 }
1186 }
1187}
1188
1189impl LocalDirectEffectRunner {
1190 async fn run_direct_llm_request(&mut self, request: CoreLlmRequest) -> RuntimeDirectLlmOutcome {
1191 let request = match crate::attachments::resolve_llm_request_attachments(
1192 request,
1193 self.attachment_store.as_ref(),
1194 )
1195 .await
1196 {
1197 Ok(request) => request,
1198 Err(err) => {
1199 return (
1200 Err(LlmCallError {
1201 message: err.to_string(),
1202 retryable: false,
1203 kind: crate::ProviderFailureKind::Unknown,
1204 raw: None,
1205 code: Some("attachment_resolution_failed".to_string()),
1206 terminal_reason: crate::LlmTerminalReason::ProviderError,
1207 request_body: None,
1208 partial_response: None,
1209 }),
1210 None,
1211 );
1212 }
1213 };
1214 match self.provider.complete(request).await {
1215 Ok(completion) => (Ok(completion.response), Some(completion.call_record)),
1216 Err(failure) => (
1217 Err(llm_call_error_from_transport(failure.error)),
1218 Some(failure.call_record),
1219 ),
1220 }
1221 }
1222}
1223
1224async fn execute_local_sleep(
1225 envelope: RuntimeEffectEnvelope,
1226 cancellation: CancellationToken,
1227 clock: &dyn crate::Clock,
1228) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1229 match envelope.command {
1230 RuntimeEffectCommand::Sleep { duration_ms } => {
1231 sleep_with_cancellation(duration_ms, &cancellation, clock).await?;
1232 Ok(RuntimeEffectOutcome::Sleep)
1233 }
1234 command => Err(RuntimeEffectControllerError::new(
1235 "runtime_effect_local_executor_mismatch",
1236 format!(
1237 "local sleep executor cannot execute {} command",
1238 command.kind().as_str()
1239 ),
1240 )),
1241 }
1242}
1243
1244async fn sleep_with_cancellation(
1245 duration_ms: u64,
1246 cancellation: &CancellationToken,
1247 clock: &dyn crate::Clock,
1248) -> Result<(), RuntimeEffectControllerError> {
1249 let sleep = clock.sleep(std::time::Duration::from_millis(duration_ms));
1250 tokio::pin!(sleep);
1251 tokio::select! {
1252 _ = cancellation.cancelled() => Err(RuntimeEffectControllerError::new(
1253 "runtime_effect_sleep_cancelled",
1254 "runtime effect sleep was cancelled",
1255 )),
1256 _ = &mut sleep => Ok(()),
1257 }
1258}
1259
1260#[derive(Clone, Default)]
1271pub struct InlineRuntimeEffectController;
1272
1273#[async_trait::async_trait]
1274impl AwaitEventResolver for InlineRuntimeEffectController {
1275 fn supports_durable_effects(&self) -> bool {
1276 true
1277 }
1278
1279 async fn await_event_key(
1280 &self,
1281 scope: &ExecutionScope,
1282 wait: AwaitEventWaitIdentity,
1283 ) -> Result<AwaitEventKey, RuntimeError> {
1284 inline_await_events().key_for(scope, wait)
1285 }
1286
1287 async fn resolve_await_event(
1288 &self,
1289 key: &AwaitEventKey,
1290 resolution: Resolution,
1291 ) -> Result<ResolveOutcome, RuntimeError> {
1292 inline_await_events().resolve(key, resolution)
1293 }
1294
1295 async fn await_await_event(
1296 &self,
1297 key: &AwaitEventKey,
1298 cancel: CancellationToken,
1299 deadline: Option<Instant>,
1300 ) -> Result<Resolution, RuntimeError> {
1301 inline_await_events()
1302 .await_resolution(key, cancel, deadline, &crate::SystemClock)
1303 .await
1304 }
1305
1306 async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1307 inline_await_events().revoke_session(session_id)
1308 }
1309
1310 async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1311 inline_await_events().cancel_session(session_id)
1312 }
1313}
1314
1315#[async_trait::async_trait]
1316impl RuntimeEffectController for InlineRuntimeEffectController {
1317 async fn execute_effect(
1318 &self,
1319 envelope: RuntimeEffectEnvelope,
1320 local_executor: RuntimeEffectLocalExecutor<'_>,
1321 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1322 match envelope.command {
1323 RuntimeEffectCommand::AwaitEvent { key } => {
1324 let RuntimeAwaitEventOptions {
1325 cancellation,
1326 deadline,
1327 clock,
1328 } = local_executor.into_await_event_options()?;
1329 let resolution = inline_await_events()
1330 .await_resolution(&key, cancellation, deadline, clock.as_ref())
1331 .await
1332 .map_err(RuntimeEffectControllerError::from)?;
1333 Ok(RuntimeEffectOutcome::AwaitEvent { resolution })
1334 }
1335 RuntimeEffectCommand::Process { command } => {
1336 let execution = local_executor.into_process()?;
1337 let result = tokio::task::spawn(async move { execution.execute(*command).await })
1338 .await
1339 .map_err(|err| {
1340 RuntimeEffectControllerError::new(
1341 "runtime_effect_process_task_join",
1342 format!("inline process effect task failed: {err}"),
1343 )
1344 })??;
1345 Ok(RuntimeEffectOutcome::Process { result })
1346 }
1347 _ => local_executor.execute(envelope).await,
1348 }
1349 }
1350}
1351
1352#[derive(Clone)]
1354pub struct InlineEffectHost {
1355 controller: Arc<dyn RuntimeEffectController>,
1356}
1357
1358impl InlineEffectHost {
1359 pub fn new(controller: Arc<dyn RuntimeEffectController>) -> Self {
1360 Self { controller }
1361 }
1362}
1363
1364impl Default for InlineEffectHost {
1365 fn default() -> Self {
1366 Self::new(Arc::new(InlineRuntimeEffectController))
1367 }
1368}
1369
1370#[async_trait::async_trait]
1371impl AwaitEventResolver for InlineEffectHost {
1372 fn durability_tier(&self) -> crate::DurabilityTier {
1373 self.controller.durability_tier()
1374 }
1375
1376 fn supports_durable_effects(&self) -> bool {
1377 self.controller.supports_durable_effects()
1378 }
1379
1380 async fn await_event_key(
1381 &self,
1382 scope: &ExecutionScope,
1383 wait: AwaitEventWaitIdentity,
1384 ) -> Result<AwaitEventKey, RuntimeError> {
1385 self.controller.await_event_key(scope, wait).await
1386 }
1387
1388 async fn resolve_await_event(
1389 &self,
1390 key: &AwaitEventKey,
1391 resolution: Resolution,
1392 ) -> Result<ResolveOutcome, RuntimeError> {
1393 self.controller.resolve_await_event(key, resolution).await
1394 }
1395
1396 async fn await_await_event(
1397 &self,
1398 key: &AwaitEventKey,
1399 cancel: CancellationToken,
1400 deadline: Option<Instant>,
1401 ) -> Result<Resolution, RuntimeError> {
1402 self.controller
1403 .await_await_event(key, cancel, deadline)
1404 .await
1405 }
1406
1407 async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1408 self.controller
1409 .revoke_await_events_for_session(session_id)
1410 .await
1411 }
1412
1413 async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1414 self.controller
1415 .cancel_await_events_for_session(session_id)
1416 .await
1417 }
1418}
1419
1420#[async_trait::async_trait]
1421impl EffectHost for InlineEffectHost {
1422 fn scoped<'run>(
1423 &'run self,
1424 scope: ExecutionScope,
1425 ) -> Result<ScopedEffectController<'run>, RuntimeError> {
1426 ScopedEffectController::shared(Arc::clone(&self.controller), scope)
1427 }
1428
1429 fn scoped_static(
1430 &self,
1431 scope: ExecutionScope,
1432 ) -> Result<Option<ScopedEffectController<'static>>, RuntimeError> {
1433 Ok(Some(ScopedEffectController::shared(
1434 Arc::clone(&self.controller),
1435 scope,
1436 )?))
1437 }
1438}
1439
1440impl InlineRuntimeEffectController {
1441 pub(crate) async fn start_process(
1449 registry: Arc<dyn crate::ProcessRegistry>,
1450 registration: crate::ProcessRegistration,
1451 grant: Option<crate::ProcessStartGrant>,
1452 ) -> Result<ProcessRecord, PluginError> {
1453 let registration_for_record = registration.clone();
1454 let record = registry.register_process(registration_for_record).await?;
1455 if let Some(grant) = grant {
1456 registry
1457 .grant_handle(&grant.session_scope, ®istration.id, grant.descriptor)
1458 .await?;
1459 }
1460 Ok(record)
1461 }
1462
1463 pub(crate) async fn request_process_cancel(
1464 &self,
1465 registry: Arc<dyn crate::ProcessRegistry>,
1466 process_id: &str,
1467 reason: Option<String>,
1468 ) -> Result<ProcessRecord, PluginError> {
1469 registry
1473 .append_event(
1474 process_id,
1475 crate::ProcessEventAppendRequest::cancel_requested(process_id, reason.clone()),
1476 )
1477 .await?;
1478 registry
1479 .get_process(process_id)
1480 .await
1481 .ok_or_else(|| PluginError::Session(format!("unknown process `{process_id}`")))
1482 }
1483}
1484
1485impl std::fmt::Debug for InlineRuntimeEffectController {
1486 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1487 f.debug_struct("InlineRuntimeEffectController").finish()
1488 }
1489}