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
728#[async_trait::async_trait]
729trait RuntimeEffectLocalRunner: Send {
730 async fn execute(
731 self: Box<Self>,
732 envelope: RuntimeEffectEnvelope,
733 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError>;
734}
735
736#[cfg(any(test, feature = "testing"))]
737type TestingRuntimeEffectLocalRunnerFn<'run> = dyn FnOnce(
738 RuntimeEffectEnvelope,
739 ) -> Pin<
740 Box<
741 dyn Future<Output = Result<RuntimeEffectOutcome, RuntimeEffectControllerError>>
742 + Send
743 + 'run,
744 >,
745 > + Send
746 + 'run;
747
748#[cfg(any(test, feature = "testing"))]
749struct TestingRuntimeEffectLocalRunner<'run> {
750 run: Box<TestingRuntimeEffectLocalRunnerFn<'run>>,
751}
752
753enum RuntimeEffectLocalExecutorState<'run> {
754 Unavailable,
755 SleepOnly {
756 cancellation: CancellationToken,
757 clock: Arc<dyn crate::Clock>,
758 observe_turn_cancel: bool,
759 },
760 ExternalWaitOptions {
761 cancellation: CancellationToken,
762 deadline: Option<Instant>,
763 clock: Arc<dyn crate::Clock>,
764 observe_turn_cancel: bool,
765 },
766 Process(ProcessLocalExecution),
767 Runner(Box<dyn RuntimeEffectLocalRunner + Send + 'run>),
768}
769
770pub struct RuntimeEffectLocalExecutor<'run> {
776 state: RuntimeEffectLocalExecutorState<'run>,
777}
778
779impl<'run> RuntimeEffectLocalExecutor<'run> {
780 pub fn unavailable() -> Self {
781 Self {
782 state: RuntimeEffectLocalExecutorState::Unavailable,
783 }
784 }
785
786 pub fn sleep(cancellation: CancellationToken) -> Self {
787 Self::sleep_with_clock(cancellation, Arc::new(crate::SystemClock))
788 }
789
790 pub fn sleep_with_clock(cancellation: CancellationToken, clock: Arc<dyn crate::Clock>) -> Self {
791 Self {
792 state: RuntimeEffectLocalExecutorState::SleepOnly {
793 cancellation,
794 clock,
795 observe_turn_cancel: true,
796 },
797 }
798 }
799
800 pub fn await_event(cancellation: CancellationToken, deadline: Option<Instant>) -> Self {
801 Self::await_event_with_clock(cancellation, deadline, Arc::new(crate::SystemClock))
802 }
803
804 pub fn await_event_with_clock(
805 cancellation: CancellationToken,
806 deadline: Option<Instant>,
807 clock: Arc<dyn crate::Clock>,
808 ) -> Self {
809 Self {
810 state: RuntimeEffectLocalExecutorState::ExternalWaitOptions {
811 cancellation,
812 deadline,
813 clock,
814 observe_turn_cancel: true,
815 },
816 }
817 }
818
819 #[doc(hidden)]
820 pub fn with_turn_cancel_observation(mut self, observe_turn_cancel: bool) -> Self {
821 match &mut self.state {
822 RuntimeEffectLocalExecutorState::SleepOnly {
823 observe_turn_cancel: current,
824 ..
825 }
826 | RuntimeEffectLocalExecutorState::ExternalWaitOptions {
827 observe_turn_cancel: current,
828 ..
829 } => *current = observe_turn_cancel,
830 _ => {}
831 }
832 self
833 }
834
835 pub fn processes(
836 registry: Arc<dyn ProcessRegistry>,
837 process_work_driver: Option<crate::ProcessWorkDriver>,
838 ) -> Self {
839 Self {
840 state: RuntimeEffectLocalExecutorState::Process(ProcessLocalExecution {
841 registry,
842 process_work_driver,
843 }),
844 }
845 }
846
847 #[cfg(any(test, feature = "testing"))]
848 pub fn testing<F, Fut>(run: F) -> Self
849 where
850 F: FnOnce(RuntimeEffectEnvelope) -> Fut + Send + 'run,
851 Fut: Future<Output = Result<RuntimeEffectOutcome, RuntimeEffectControllerError>>
852 + Send
853 + 'run,
854 {
855 Self {
856 state: RuntimeEffectLocalExecutorState::Runner(Box::new(
857 TestingRuntimeEffectLocalRunner {
858 run: Box::new(move |envelope| Box::pin(run(envelope))),
859 },
860 )),
861 }
862 }
863
864 pub(in crate::runtime) fn turn<'scope>(
865 driver: &'run mut RuntimeTurnDriver<'scope>,
866 machine: &'run mut crate::TurnMachine,
867 event_tx: mpsc::Sender<RuntimeStreamEvent>,
868 cancellation: CancellationToken,
869 ) -> Self
870 where
871 'scope: 'run,
872 {
873 Self {
874 state: RuntimeEffectLocalExecutorState::Runner(Box::new(LocalTurnEffectRunner {
875 driver,
876 machine,
877 event_tx,
878 cancellation,
879 })),
880 }
881 }
882
883 pub(in crate::runtime) fn direct(
884 provider: ProviderHandle,
885 attachment_store: Arc<crate::SessionAttachmentStore>,
886 ) -> Self {
887 Self {
888 state: RuntimeEffectLocalExecutorState::Runner(Box::new(LocalDirectEffectRunner {
889 provider,
890 attachment_store,
891 })),
892 }
893 }
894
895 pub(crate) fn tool_batch(
896 context: crate::RuntimeExecutionContext<'run>,
897 child_trace_hooks: HashMap<String, crate::ToolChildExecutionTraceHook>,
898 ) -> Self {
899 Self {
900 state: RuntimeEffectLocalExecutorState::Runner(Box::new(LocalToolBatchEffectRunner {
901 context,
902 child_trace_hooks,
903 })),
904 }
905 }
906
907 pub async fn execute(
908 self,
909 envelope: RuntimeEffectEnvelope,
910 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
911 match self.state {
912 RuntimeEffectLocalExecutorState::Runner(runner) => runner.execute(envelope).await,
913 RuntimeEffectLocalExecutorState::SleepOnly {
914 cancellation,
915 clock,
916 ..
917 } => execute_local_sleep(envelope, cancellation, clock.as_ref()).await,
918 RuntimeEffectLocalExecutorState::ExternalWaitOptions { .. } => {
919 Err(RuntimeEffectControllerError::new(
920 "runtime_effect_local_executor_mismatch",
921 format!(
922 "local await-event options cannot execute {} command directly",
923 envelope.command.kind().as_str()
924 ),
925 ))
926 }
927 RuntimeEffectLocalExecutorState::Unavailable => Err(RuntimeEffectControllerError::new(
928 "runtime_effect_local_executor_unavailable",
929 format!(
930 "no local executor is available for {}",
931 envelope.command.kind().as_str()
932 ),
933 )),
934 RuntimeEffectLocalExecutorState::Process(_) => Err(RuntimeEffectControllerError::new(
935 "runtime_effect_local_executor_mismatch",
936 format!(
937 "process executor cannot execute {} command directly",
938 envelope.command.kind().as_str()
939 ),
940 )),
941 }
942 }
943
944 pub fn into_process(self) -> Result<ProcessLocalExecution, RuntimeEffectControllerError> {
945 match self.state {
946 RuntimeEffectLocalExecutorState::Process(execution) => Ok(execution),
947 _ => Err(RuntimeEffectControllerError::new(
948 "runtime_effect_local_executor_unavailable",
949 "no process executor is available for process command",
950 )),
951 }
952 }
953
954 pub fn into_await_event_options(
955 self,
956 ) -> Result<RuntimeAwaitEventOptions, RuntimeEffectControllerError> {
957 match self.state {
958 RuntimeEffectLocalExecutorState::ExternalWaitOptions {
959 cancellation,
960 deadline,
961 clock,
962 observe_turn_cancel,
963 } => Ok(RuntimeAwaitEventOptions {
964 cancellation,
965 deadline,
966 clock,
967 observe_turn_cancel,
968 }),
969 _ => Ok(RuntimeAwaitEventOptions {
970 cancellation: CancellationToken::new(),
971 deadline: None,
972 clock: Arc::new(crate::SystemClock),
973 observe_turn_cancel: false,
974 }),
975 }
976 }
977
978 pub fn into_sleep_options(self) -> RuntimeSleepOptions {
979 match self.state {
980 RuntimeEffectLocalExecutorState::SleepOnly {
981 cancellation,
982 observe_turn_cancel,
983 ..
984 } => RuntimeSleepOptions {
985 cancellation,
986 observe_turn_cancel,
987 },
988 _ => RuntimeSleepOptions {
989 cancellation: CancellationToken::new(),
990 observe_turn_cancel: false,
991 },
992 }
993 }
994}
995
996#[cfg(any(test, feature = "testing"))]
997#[async_trait::async_trait]
998impl RuntimeEffectLocalRunner for TestingRuntimeEffectLocalRunner<'_> {
999 async fn execute(
1000 self: Box<Self>,
1001 envelope: RuntimeEffectEnvelope,
1002 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1003 (self.run)(envelope).await
1004 }
1005}
1006
1007#[async_trait::async_trait]
1008impl RuntimeEffectLocalRunner for LocalToolBatchEffectRunner<'_> {
1009 async fn execute(
1010 self: Box<Self>,
1011 envelope: RuntimeEffectEnvelope,
1012 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1013 match envelope.command {
1014 RuntimeEffectCommand::ToolBatch { batch } => {
1015 let outcome = self
1016 .context
1017 .execute_prepared_tool_batch_launches(
1018 batch,
1019 envelope.invocation,
1020 self.child_trace_hooks,
1021 )
1022 .await?;
1023 Ok(RuntimeEffectOutcome::ToolBatch {
1024 launches: outcome.launches,
1025 triggers: outcome.triggers,
1026 })
1027 }
1028 RuntimeEffectCommand::ToolAttempt {
1029 call,
1030 execution_grant,
1031 attempt,
1032 max_attempts,
1033 } => {
1034 let child_execution_trace_hook = self.child_trace_hooks.get(&call.call_id).cloned();
1035 let outcome = self
1036 .context
1037 .execute_prepared_tool_attempt_effect(
1038 call,
1039 execution_grant,
1040 attempt,
1041 max_attempts,
1042 envelope.invocation,
1043 child_execution_trace_hook,
1044 )
1045 .await?;
1046 Ok(RuntimeEffectOutcome::ToolAttempt {
1047 launch: outcome.launch,
1048 triggers: outcome.triggers,
1049 })
1050 }
1051 command => Err(RuntimeEffectControllerError::new(
1052 "runtime_effect_local_executor_mismatch",
1053 format!(
1054 "local tool executor cannot execute {} command",
1055 command.kind().as_str()
1056 ),
1057 )),
1058 }
1059 }
1060}
1061
1062#[async_trait::async_trait]
1063impl RuntimeEffectLocalRunner for LocalTurnEffectRunner<'_, '_> {
1064 async fn execute(
1065 self: Box<Self>,
1066 envelope: RuntimeEffectEnvelope,
1067 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1068 let runner = *self;
1069 match envelope.command {
1070 RuntimeEffectCommand::LlmCall { request } => {
1071 let protocol_iteration = runner.machine.protocol_iteration();
1072 let (result, text_streamed, call_record) = runner
1073 .driver
1074 .run_llm_call(
1075 Arc::new((*request).into_request(None, None)),
1076 protocol_iteration,
1077 envelope.invocation,
1078 &runner.event_tx,
1079 &runner.cancellation,
1080 )
1081 .await;
1082 Ok(RuntimeEffectOutcome::LlmCall {
1083 result,
1084 text_streamed,
1085 call_record,
1086 })
1087 }
1088 RuntimeEffectCommand::ToolBatch { batch } => {
1089 let outcome = runner
1090 .driver
1091 .run_tool_batch(
1092 batch,
1093 envelope.invocation,
1094 &runner.event_tx,
1095 &runner.cancellation,
1096 )
1097 .await?;
1098 Ok(RuntimeEffectOutcome::ToolBatch {
1099 launches: outcome.launches,
1100 triggers: outcome.triggers,
1101 })
1102 }
1103 RuntimeEffectCommand::ExecCode { language, code } => {
1104 let protocol_iteration = runner.machine.protocol_iteration();
1105 let messages = runner.machine.message_sequence();
1106 Ok(RuntimeEffectOutcome::ExecCode {
1107 result: runner
1108 .driver
1109 .run_exec_code(
1110 language,
1111 &code,
1112 messages,
1113 protocol_iteration,
1114 envelope.invocation,
1115 &runner.event_tx,
1116 &runner.cancellation,
1117 )
1118 .await,
1119 })
1120 }
1121 RuntimeEffectCommand::Checkpoint { checkpoint } => {
1122 Ok(RuntimeEffectOutcome::Checkpoint {
1123 result: runner
1124 .driver
1125 .run_checkpoint(runner.machine, checkpoint, &runner.event_tx)
1126 .await
1127 .map_err(RuntimeEffectControllerError::from),
1128 })
1129 }
1130 RuntimeEffectCommand::SyncExecutionEnvironment {
1131 update_machine_config,
1132 } => Ok(RuntimeEffectOutcome::SyncExecutionEnvironment {
1133 result: runner
1134 .driver
1135 .refresh_execution_environment(runner.machine, update_machine_config)
1136 .await
1137 .map_err(|err| err.to_string()),
1138 }),
1139 RuntimeEffectCommand::Sleep { duration_ms } => {
1140 sleep_with_cancellation(
1141 duration_ms,
1142 &runner.cancellation,
1143 runner.driver.host.core.clock.as_ref(),
1144 )
1145 .await?;
1146 Ok(RuntimeEffectOutcome::Sleep)
1147 }
1148 command => Err(RuntimeEffectControllerError::new(
1149 "runtime_effect_local_executor_mismatch",
1150 format!(
1151 "local turn executor cannot execute {} command",
1152 command.kind().as_str()
1153 ),
1154 )),
1155 }
1156 }
1157}
1158
1159#[async_trait::async_trait]
1160impl RuntimeEffectLocalRunner for LocalDirectEffectRunner {
1161 async fn execute(
1162 mut self: Box<Self>,
1163 envelope: RuntimeEffectEnvelope,
1164 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1165 match envelope.command {
1166 RuntimeEffectCommand::Direct { request, .. } => {
1167 let (result, call_record) = self
1168 .run_direct_llm_request((*request).into_request(
1169 crate::session_model::transport_stream_events(&self.provider, None),
1170 None,
1171 ))
1172 .await;
1173 Ok(RuntimeEffectOutcome::Direct {
1174 result,
1175 call_record,
1176 })
1177 }
1178 RuntimeEffectCommand::Sleep { duration_ms } => {
1179 sleep_with_cancellation(
1180 duration_ms,
1181 &CancellationToken::new(),
1182 &crate::SystemClock,
1183 )
1184 .await?;
1185 Ok(RuntimeEffectOutcome::Sleep)
1186 }
1187 command => Err(RuntimeEffectControllerError::new(
1188 "runtime_effect_local_executor_mismatch",
1189 format!(
1190 "local direct executor cannot execute {} command",
1191 command.kind().as_str()
1192 ),
1193 )),
1194 }
1195 }
1196}
1197
1198impl LocalDirectEffectRunner {
1199 async fn run_direct_llm_request(&mut self, request: CoreLlmRequest) -> RuntimeDirectLlmOutcome {
1200 let request = match crate::attachments::resolve_llm_request_attachments(
1201 request,
1202 self.attachment_store.as_ref(),
1203 )
1204 .await
1205 {
1206 Ok(request) => request,
1207 Err(err) => {
1208 return (
1209 Err(LlmCallError {
1210 message: err.to_string(),
1211 retryable: false,
1212 kind: crate::ProviderFailureKind::Unknown,
1213 raw: None,
1214 code: Some("attachment_resolution_failed".to_string()),
1215 terminal_reason: crate::LlmTerminalReason::ProviderError,
1216 request_body: None,
1217 partial_response: None,
1218 }),
1219 None,
1220 );
1221 }
1222 };
1223 match self.provider.complete(request).await {
1224 Ok(completion) => (Ok(completion.response), Some(completion.call_record)),
1225 Err(failure) => (
1226 Err(llm_call_error_from_transport(failure.error)),
1227 Some(failure.call_record),
1228 ),
1229 }
1230 }
1231}
1232
1233async fn execute_local_sleep(
1234 envelope: RuntimeEffectEnvelope,
1235 cancellation: CancellationToken,
1236 clock: &dyn crate::Clock,
1237) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1238 match envelope.command {
1239 RuntimeEffectCommand::Sleep { duration_ms } => {
1240 sleep_with_cancellation(duration_ms, &cancellation, clock).await?;
1241 Ok(RuntimeEffectOutcome::Sleep)
1242 }
1243 command => Err(RuntimeEffectControllerError::new(
1244 "runtime_effect_local_executor_mismatch",
1245 format!(
1246 "local sleep executor cannot execute {} command",
1247 command.kind().as_str()
1248 ),
1249 )),
1250 }
1251}
1252
1253async fn sleep_with_cancellation(
1254 duration_ms: u64,
1255 cancellation: &CancellationToken,
1256 clock: &dyn crate::Clock,
1257) -> Result<(), RuntimeEffectControllerError> {
1258 let sleep = clock.sleep(std::time::Duration::from_millis(duration_ms));
1259 tokio::pin!(sleep);
1260 tokio::select! {
1261 _ = cancellation.cancelled() => Err(RuntimeEffectControllerError::new(
1262 "runtime_effect_sleep_cancelled",
1263 "runtime effect sleep was cancelled",
1264 )),
1265 _ = &mut sleep => Ok(()),
1266 }
1267}
1268
1269#[derive(Clone, Default)]
1279pub struct InlineRuntimeEffectController;
1280
1281#[async_trait::async_trait]
1282impl AwaitEventResolver for InlineRuntimeEffectController {
1283 async fn await_event_key(
1284 &self,
1285 scope: &ExecutionScope,
1286 wait: AwaitEventWaitIdentity,
1287 ) -> Result<AwaitEventKey, RuntimeError> {
1288 inline_await_events().key_for(scope, wait)
1289 }
1290
1291 async fn resolve_await_event(
1292 &self,
1293 key: &AwaitEventKey,
1294 resolution: Resolution,
1295 ) -> Result<ResolveOutcome, RuntimeError> {
1296 inline_await_events().resolve(key, resolution)
1297 }
1298
1299 async fn peek_await_event(
1300 &self,
1301 key: &AwaitEventKey,
1302 ) -> Result<Option<Resolution>, RuntimeError> {
1303 inline_await_events().peek_resolution(key)
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 inline_await_events()
1313 .await_resolution(key, cancel, deadline, &crate::SystemClock)
1314 .await
1315 }
1316
1317 async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1318 inline_await_events().revoke_session(session_id)
1319 }
1320
1321 async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1322 inline_await_events().cancel_session(session_id)
1323 }
1324}
1325
1326#[async_trait::async_trait]
1327impl RuntimeEffectController for InlineRuntimeEffectController {
1328 async fn execute_effect(
1329 &self,
1330 envelope: RuntimeEffectEnvelope,
1331 local_executor: RuntimeEffectLocalExecutor<'_>,
1332 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
1333 match envelope.command {
1334 RuntimeEffectCommand::AwaitEvent { key } => {
1335 let RuntimeAwaitEventOptions {
1336 cancellation,
1337 deadline,
1338 clock,
1339 ..
1340 } = local_executor.into_await_event_options()?;
1341 let resolution = inline_await_events()
1342 .await_resolution(&key, cancellation, deadline, clock.as_ref())
1343 .await
1344 .map_err(RuntimeEffectControllerError::from)?;
1345 Ok(RuntimeEffectOutcome::AwaitEvent { resolution })
1346 }
1347 RuntimeEffectCommand::Process { command } => {
1348 let execution = local_executor.into_process()?;
1349 let result = tokio::task::spawn(async move { execution.execute(*command).await })
1350 .await
1351 .map_err(|err| {
1352 RuntimeEffectControllerError::new(
1353 "runtime_effect_process_task_join",
1354 format!("inline process effect task failed: {err}"),
1355 )
1356 })??;
1357 Ok(RuntimeEffectOutcome::Process { result })
1358 }
1359 _ => local_executor.execute(envelope).await,
1360 }
1361 }
1362}
1363
1364#[derive(Clone)]
1366pub struct InlineEffectHost {
1367 controller: Arc<dyn RuntimeEffectController>,
1368}
1369
1370impl InlineEffectHost {
1371 pub fn new(controller: Arc<dyn RuntimeEffectController>) -> Self {
1372 Self { controller }
1373 }
1374}
1375
1376impl Default for InlineEffectHost {
1377 fn default() -> Self {
1378 Self::new(Arc::new(InlineRuntimeEffectController))
1379 }
1380}
1381
1382#[async_trait::async_trait]
1383impl AwaitEventResolver for InlineEffectHost {
1384 fn durability_tier(&self) -> crate::DurabilityTier {
1385 self.controller.durability_tier()
1386 }
1387
1388 async fn await_event_key(
1389 &self,
1390 scope: &ExecutionScope,
1391 wait: AwaitEventWaitIdentity,
1392 ) -> Result<AwaitEventKey, RuntimeError> {
1393 self.controller.await_event_key(scope, wait).await
1394 }
1395
1396 async fn resolve_await_event(
1397 &self,
1398 key: &AwaitEventKey,
1399 resolution: Resolution,
1400 ) -> Result<ResolveOutcome, RuntimeError> {
1401 self.controller.resolve_await_event(key, resolution).await
1402 }
1403
1404 async fn peek_await_event(
1405 &self,
1406 key: &AwaitEventKey,
1407 ) -> Result<Option<Resolution>, RuntimeError> {
1408 self.controller.peek_await_event(key).await
1409 }
1410
1411 async fn await_await_event(
1412 &self,
1413 key: &AwaitEventKey,
1414 cancel: CancellationToken,
1415 deadline: Option<Instant>,
1416 ) -> Result<Resolution, RuntimeError> {
1417 self.controller
1418 .await_await_event(key, cancel, deadline)
1419 .await
1420 }
1421
1422 async fn revoke_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1423 self.controller
1424 .revoke_await_events_for_session(session_id)
1425 .await
1426 }
1427
1428 async fn cancel_await_events_for_session(&self, session_id: &str) -> Result<(), RuntimeError> {
1429 self.controller
1430 .cancel_await_events_for_session(session_id)
1431 .await
1432 }
1433}
1434
1435#[async_trait::async_trait]
1436impl EffectHost for InlineEffectHost {
1437 fn scoped<'run>(
1438 &'run self,
1439 scope: ExecutionScope,
1440 ) -> Result<ScopedEffectController<'run>, RuntimeError> {
1441 ScopedEffectController::shared(Arc::clone(&self.controller), scope)
1442 }
1443
1444 fn scoped_static(
1445 &self,
1446 scope: ExecutionScope,
1447 ) -> Result<Option<ScopedEffectController<'static>>, RuntimeError> {
1448 Ok(Some(ScopedEffectController::shared(
1449 Arc::clone(&self.controller),
1450 scope,
1451 )?))
1452 }
1453}
1454
1455impl InlineRuntimeEffectController {
1456 pub(crate) async fn start_process(
1464 registry: Arc<dyn crate::ProcessRegistry>,
1465 registration: crate::ProcessRegistration,
1466 grant: Option<crate::ProcessStartGrant>,
1467 ) -> Result<ProcessRecord, PluginError> {
1468 let registration_for_record = registration.clone();
1469 let record = registry.register_process(registration_for_record).await?;
1470 if let Some(grant) = grant {
1471 registry
1472 .grant_handle(&grant.session_scope, ®istration.id, grant.descriptor)
1473 .await?;
1474 }
1475 Ok(record)
1476 }
1477
1478 pub(crate) async fn request_process_cancel(
1479 &self,
1480 registry: Arc<dyn crate::ProcessRegistry>,
1481 process_id: &str,
1482 reason: Option<String>,
1483 ) -> Result<ProcessRecord, PluginError> {
1484 registry
1488 .append_event(
1489 process_id,
1490 crate::ProcessEventAppendRequest::cancel_requested(process_id, reason.clone()),
1491 )
1492 .await?;
1493 registry
1494 .get_process(process_id)
1495 .await
1496 .ok_or_else(|| PluginError::Session(format!("unknown process `{process_id}`")))
1497 }
1498}
1499
1500impl std::fmt::Debug for InlineRuntimeEffectController {
1501 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1502 f.debug_struct("InlineRuntimeEffectController").finish()
1503 }
1504}