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