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