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