Skip to main content

lash_core/runtime/effect/
envelope.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5use crate::CheckpointKind;
6use crate::llm::types::{
7    LlmAttachment, LlmEventSender, LlmMessage, LlmOutputSpec, LlmProviderTraceSender,
8    LlmToolChoice, LlmToolSpec,
9};
10use crate::runtime::ProcessHandleGrantEntry;
11use crate::sansio::{CompletedToolCall, ExecutionEnvironmentSync, LlmCallError};
12use crate::tool_dispatch::ToolTriggerEffectOutcome;
13use crate::{
14    AttachmentCreateMeta, AttachmentRef, AttachmentStore, CausalRef, CheckpointDelivery,
15    ExecResponse, LlmRequest as CoreLlmRequest, LlmResponse, MediaType, ProcessAwaitOutput,
16    ProcessExecutionContext, ProcessListMode, ProcessRecord, ProcessRegistration,
17    ProcessStartGrant, SessionScope,
18};
19
20use super::executor::RuntimeEffectControllerError;
21
22/// Durable category for a runtime effect.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum RuntimeEffectKind {
26    LlmCall,
27    Direct,
28    ToolCall,
29    Process,
30    ExecCode,
31    Checkpoint,
32    SyncExecutionEnvironment,
33    Sleep,
34    AwaitEvent,
35    DurableStep,
36}
37
38impl RuntimeEffectKind {
39    pub fn as_str(self) -> &'static str {
40        match self {
41            Self::LlmCall => "llm_call",
42            Self::Direct => "direct",
43            Self::ToolCall => "tool_call",
44            Self::Process => "process",
45            Self::ExecCode => "exec_code",
46            Self::Checkpoint => "checkpoint",
47            Self::SyncExecutionEnvironment => "sync_execution_environment",
48            Self::Sleep => "sleep",
49            Self::AwaitEvent => "await_event",
50            Self::DurableStep => "durable_step",
51        }
52    }
53}
54
55/// Canonical lineage for a runtime-side invocation.
56#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
57pub struct RuntimeInvocation {
58    pub scope: RuntimeScope,
59    pub subject: RuntimeSubject,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub caused_by: Option<CausalRef>,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub replay: Option<RuntimeReplay>,
64}
65
66impl RuntimeInvocation {
67    pub fn effect(
68        scope: RuntimeScope,
69        effect_id: impl Into<String>,
70        kind: RuntimeEffectKind,
71        replay_key: impl Into<String>,
72    ) -> Self {
73        Self {
74            scope,
75            subject: RuntimeSubject::Effect {
76                effect_id: effect_id.into(),
77                kind,
78            },
79            caused_by: None,
80            replay: Some(RuntimeReplay {
81                key: replay_key.into(),
82            }),
83        }
84    }
85
86    pub fn with_caused_by(mut self, caused_by: Option<CausalRef>) -> Self {
87        self.caused_by = caused_by;
88        self
89    }
90
91    pub fn effect_id(&self) -> Option<&str> {
92        match &self.subject {
93            RuntimeSubject::Effect { effect_id, .. } => Some(effect_id),
94            _ => None,
95        }
96    }
97
98    pub fn effect_kind(&self) -> Option<RuntimeEffectKind> {
99        match &self.subject {
100            RuntimeSubject::Effect { kind, .. } => Some(*kind),
101            _ => None,
102        }
103    }
104
105    pub fn replay_key(&self) -> Option<&str> {
106        self.replay.as_ref().map(|replay| replay.key.as_str())
107    }
108
109    pub fn causal_ref(&self) -> Option<CausalRef> {
110        match &self.subject {
111            RuntimeSubject::Effect { effect_id, .. } => Some(CausalRef::Effect {
112                session_id: self.scope.session_id.clone(),
113                turn_id: self.scope.turn_id.clone(),
114                effect_id: effect_id.clone(),
115            }),
116            RuntimeSubject::Process { process_id } => Some(CausalRef::Process {
117                process_id: process_id.clone(),
118            }),
119            RuntimeSubject::ProcessEvent {
120                process_id,
121                sequence,
122                ..
123            } => Some(CausalRef::ProcessEvent {
124                process_id: process_id.clone(),
125                sequence: *sequence,
126            }),
127            RuntimeSubject::TriggerOccurrence { occurrence_id } => {
128                Some(CausalRef::TriggerOccurrence {
129                    occurrence_id: occurrence_id.clone(),
130                })
131            }
132            RuntimeSubject::SessionNode { node_id } => Some(CausalRef::SessionNode {
133                session_id: self.scope.session_id.clone(),
134                node_id: node_id.clone(),
135            }),
136        }
137    }
138}
139
140#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
141pub struct RuntimeScope {
142    pub session_id: String,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub turn_id: Option<String>,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub turn_index: Option<usize>,
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub protocol_iteration: Option<usize>,
149}
150
151impl RuntimeScope {
152    pub fn new(session_id: impl Into<String>) -> Self {
153        Self {
154            session_id: session_id.into(),
155            turn_id: None,
156            turn_index: None,
157            protocol_iteration: None,
158        }
159    }
160
161    pub fn for_turn(
162        session_id: impl Into<String>,
163        turn_id: impl Into<String>,
164        turn_index: usize,
165        protocol_iteration: usize,
166    ) -> Self {
167        Self {
168            session_id: session_id.into(),
169            turn_id: Some(turn_id.into()),
170            turn_index: Some(turn_index),
171            protocol_iteration: Some(protocol_iteration),
172        }
173    }
174}
175
176#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
177pub struct RuntimeReplay {
178    pub key: String,
179}
180
181#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(tag = "type", rename_all = "snake_case")]
183pub enum RuntimeSubject {
184    Effect {
185        effect_id: String,
186        kind: RuntimeEffectKind,
187    },
188    Process {
189        process_id: String,
190    },
191    ProcessEvent {
192        process_id: String,
193        sequence: u64,
194        event_type: String,
195    },
196    TriggerOccurrence {
197        occurrence_id: String,
198    },
199    SessionNode {
200        node_id: String,
201    },
202}
203
204/// Fully serializable envelope emitted at Lash's nondeterministic boundary.
205#[derive(Clone, Debug, Serialize, Deserialize)]
206pub struct RuntimeEffectEnvelope {
207    pub invocation: RuntimeInvocation,
208    pub command: RuntimeEffectCommand,
209}
210
211impl RuntimeEffectEnvelope {
212    pub fn new(invocation: RuntimeInvocation, command: RuntimeEffectCommand) -> Self {
213        Self::try_new(invocation, command).expect("valid runtime effect invocation")
214    }
215
216    pub fn try_new(
217        invocation: RuntimeInvocation,
218        command: RuntimeEffectCommand,
219    ) -> Result<Self, RuntimeEffectControllerError> {
220        validate_effect_invocation(&invocation, command.kind())?;
221        validate_effect_command(&command)?;
222        Ok(Self {
223            invocation,
224            command,
225        })
226    }
227
228    pub fn stable_hash(&self) -> Result<String, RuntimeEffectControllerError> {
229        crate::stable_hash::stable_json_sha256_hex(self).map_err(|err| {
230            RuntimeEffectControllerError::new(
231                "runtime_effect_envelope_hash",
232                format!("failed to serialize runtime effect envelope: {err}"),
233            )
234        })
235    }
236}
237
238fn validate_effect_invocation(
239    invocation: &RuntimeInvocation,
240    command_kind: RuntimeEffectKind,
241) -> Result<(), RuntimeEffectControllerError> {
242    let RuntimeSubject::Effect { effect_id, kind } = &invocation.subject else {
243        return Err(RuntimeEffectControllerError::new(
244            "runtime_effect_invocation_subject",
245            "runtime effect envelope subject must be an effect",
246        ));
247    };
248    if effect_id.trim().is_empty() {
249        return Err(RuntimeEffectControllerError::new(
250            "runtime_effect_invocation_subject",
251            "runtime effect envelope effect id must be non-empty",
252        ));
253    }
254    if *kind != command_kind {
255        return Err(RuntimeEffectControllerError::new(
256            "runtime_effect_invocation_kind",
257            format!(
258                "runtime effect invocation kind {} does not match command kind {}",
259                kind.as_str(),
260                command_kind.as_str()
261            ),
262        ));
263    }
264    if invocation
265        .replay
266        .as_ref()
267        .is_none_or(|replay| replay.key.is_empty())
268    {
269        return Err(RuntimeEffectControllerError::new(
270            "runtime_effect_replay_required",
271            "runtime effect envelope requires replay.key",
272        ));
273    }
274    Ok(())
275}
276
277fn validate_effect_command(
278    command: &RuntimeEffectCommand,
279) -> Result<(), RuntimeEffectControllerError> {
280    if let RuntimeEffectCommand::DurableStep { step_id, .. } = command
281        && step_id.trim().is_empty()
282    {
283        return Err(RuntimeEffectControllerError::new(
284            "runtime_effect_durable_step_id",
285            "runtime effect durable step id must be non-empty",
286        ));
287    }
288    Ok(())
289}
290
291/// Serializable command emitted at Lash's nondeterministic runtime boundary.
292#[derive(Clone, Debug, Serialize, Deserialize)]
293#[serde(tag = "type", rename_all = "snake_case")]
294pub enum RuntimeEffectCommand {
295    LlmCall {
296        request: Box<LlmRequestSpec>,
297    },
298    Direct {
299        request: Box<LlmRequestSpec>,
300        usage_source: String,
301    },
302    ToolCall {
303        call: crate::PreparedToolCall,
304    },
305    Process {
306        command: Box<ProcessCommand>,
307    },
308    ExecCode {
309        code: String,
310    },
311    Checkpoint {
312        checkpoint: CheckpointKind,
313    },
314    SyncExecutionEnvironment {
315        update_machine_config: bool,
316    },
317    Sleep {
318        duration_ms: u64,
319    },
320    AwaitEvent {
321        key: crate::AwaitEventKey,
322    },
323    DurableStep {
324        step_id: String,
325        input: serde_json::Value,
326    },
327}
328
329impl RuntimeEffectCommand {
330    pub fn process(command: ProcessCommand) -> Self {
331        Self::Process {
332            command: Box::new(command),
333        }
334    }
335
336    pub fn kind(&self) -> RuntimeEffectKind {
337        match self {
338            Self::LlmCall { .. } => RuntimeEffectKind::LlmCall,
339            Self::Direct { .. } => RuntimeEffectKind::Direct,
340            Self::ToolCall { .. } => RuntimeEffectKind::ToolCall,
341            Self::Process { .. } => RuntimeEffectKind::Process,
342            Self::ExecCode { .. } => RuntimeEffectKind::ExecCode,
343            Self::Checkpoint { .. } => RuntimeEffectKind::Checkpoint,
344            Self::SyncExecutionEnvironment { .. } => RuntimeEffectKind::SyncExecutionEnvironment,
345            Self::Sleep { .. } => RuntimeEffectKind::Sleep,
346            Self::AwaitEvent { .. } => RuntimeEffectKind::AwaitEvent,
347            Self::DurableStep { .. } => RuntimeEffectKind::DurableStep,
348        }
349    }
350}
351
352/// Serializable operation against the process admin plane.
353#[derive(Clone, Debug, Serialize, Deserialize)]
354#[serde(tag = "op", rename_all = "snake_case")]
355pub enum ProcessCommand {
356    Start {
357        registration: ProcessRegistration,
358        #[serde(default, skip_serializing_if = "Option::is_none")]
359        grant: Option<ProcessStartGrant>,
360        #[serde(
361            default,
362            skip_serializing_if = "boxed_process_execution_context_is_empty"
363        )]
364        execution_context: Box<ProcessExecutionContext>,
365    },
366    List {
367        session_scope: SessionScope,
368        #[serde(default)]
369        mode: ProcessListMode,
370    },
371    Transfer {
372        from_scope: SessionScope,
373        to_scope: SessionScope,
374        process_ids: Vec<String>,
375    },
376    DeleteSession {
377        session_id: String,
378    },
379    Await {
380        process_id: String,
381    },
382    Cancel {
383        process_id: String,
384        reason: Option<String>,
385    },
386    Signal {
387        process_id: String,
388        signal_name: String,
389        signal_id: String,
390        request: crate::ProcessEventAppendRequest,
391    },
392}
393
394fn boxed_process_execution_context_is_empty(context: &ProcessExecutionContext) -> bool {
395    context.is_empty()
396}
397
398type CheckpointOutcome = Result<CheckpointDelivery, RuntimeEffectControllerError>;
399
400impl ProcessCommand {
401    pub fn effect_id(&self) -> String {
402        match self {
403            Self::Start { registration, .. } => format!("process:start:{}", registration.id),
404            Self::List {
405                session_scope,
406                mode,
407            } => {
408                format!("process:list:{}:{}", session_scope.id(), mode.as_str())
409            }
410            Self::Transfer {
411                from_scope,
412                to_scope,
413                process_ids,
414            } => {
415                let digest = crate::stable_hash::stable_json_sha256_hex(process_ids)
416                    .unwrap_or_else(|_| "unhashable".to_string());
417                format!(
418                    "process:transfer:{}:{}:{digest}",
419                    from_scope.id(),
420                    to_scope.id()
421                )
422            }
423            Self::DeleteSession { session_id } => format!("process:delete-session:{session_id}"),
424            Self::Await { process_id } => format!("process:await:{process_id}"),
425            Self::Cancel { process_id, .. } => format!("process:cancel:{process_id}"),
426            Self::Signal {
427                process_id,
428                signal_name,
429                signal_id,
430                ..
431            } => {
432                format!("process:signal:{process_id}:signal.{signal_name}:{signal_id}")
433            }
434        }
435    }
436}
437
438/// Serializable result of a process operation.
439#[derive(Clone, Debug, Serialize, Deserialize)]
440#[serde(tag = "op", rename_all = "snake_case")]
441pub enum ProcessEffectOutcome {
442    Start {
443        record: ProcessRecord,
444    },
445    List {
446        entries: Vec<ProcessHandleGrantEntry>,
447    },
448    Transfer,
449    DeleteSession {
450        report: crate::ProcessSessionDeleteReport,
451    },
452    Await {
453        output: ProcessAwaitOutput,
454    },
455    Cancel {
456        record: ProcessRecord,
457    },
458    Signal {
459        event: crate::ProcessEvent,
460    },
461}
462
463#[derive(Clone, Debug, Serialize, Deserialize)]
464pub struct ToolCallEffectOutcome {
465    pub launch: ToolCallLaunch,
466    #[serde(default, skip_serializing_if = "Vec::is_empty")]
467    pub triggers: Vec<ToolTriggerEffectOutcome>,
468}
469
470#[derive(Clone, Debug, Serialize, Deserialize)]
471#[serde(tag = "status", rename_all = "snake_case")]
472pub enum ToolCallLaunch {
473    Done {
474        result: CompletedToolCall,
475    },
476    Pending {
477        key: crate::AwaitEventKey,
478        pending: crate::PendingCompletion,
479        duration_ms: u64,
480    },
481}
482
483/// Serializable result of a runtime effect command.
484#[derive(Clone, Debug, Serialize, Deserialize)]
485#[serde(tag = "type", rename_all = "snake_case")]
486pub enum RuntimeEffectOutcome {
487    LlmCall {
488        result: Result<LlmResponse, LlmCallError>,
489        text_streamed: bool,
490    },
491    Direct {
492        result: Result<LlmResponse, LlmCallError>,
493    },
494    ToolCall {
495        launch: ToolCallLaunch,
496        #[serde(default, skip_serializing_if = "Vec::is_empty")]
497        triggers: Vec<ToolTriggerEffectOutcome>,
498    },
499    Process {
500        result: ProcessEffectOutcome,
501    },
502    ExecCode {
503        result: Result<ExecResponse, String>,
504    },
505    Checkpoint {
506        result: CheckpointOutcome,
507    },
508    SyncExecutionEnvironment {
509        result: Result<Option<ExecutionEnvironmentSync>, String>,
510    },
511    Sleep,
512    AwaitEvent {
513        resolution: crate::Resolution,
514    },
515    DurableStep {
516        value: serde_json::Value,
517    },
518}
519
520// =============================================================================
521// Request specs (serializable forms of LLM/Direct requests)
522// =============================================================================
523
524/// Serializable attachment data for runtime effect envelopes.
525///
526/// Effect envelopes carry attachment references only. Local executors resolve
527/// bytes from the configured attachment store when a provider request is
528/// actually executed.
529#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
530pub struct LlmAttachmentSpec {
531    pub reference: AttachmentRef,
532}
533
534impl LlmAttachmentSpec {
535    fn into_attachment(self) -> LlmAttachment {
536        LlmAttachment::reference(self.reference)
537    }
538}
539
540/// Serializable LLM request data. Live stream and provider-trace callbacks are
541/// attached by the local executor, and attachment bytes are resolved locally
542/// from refs rather than persisted in the effect envelope.
543#[derive(Clone, Debug, Serialize, Deserialize)]
544pub struct LlmRequestSpec {
545    pub model: String,
546    pub messages: Vec<LlmMessage>,
547    pub attachments: Vec<LlmAttachmentSpec>,
548    pub tools: Arc<Vec<LlmToolSpec>>,
549    pub tool_choice: LlmToolChoice,
550    pub model_variant: Option<String>,
551    #[serde(default)]
552    pub generation: crate::GenerationOptions,
553    pub session_id: Option<String>,
554    pub output_spec: Option<LlmOutputSpec>,
555}
556
557impl LlmRequestSpec {
558    pub async fn from_request(
559        request: &CoreLlmRequest,
560        attachment_store: &dyn AttachmentStore,
561    ) -> Result<Self, RuntimeEffectControllerError> {
562        Ok(Self {
563            model: request.model.clone(),
564            messages: request.messages.clone(),
565            attachments: attachment_specs_from_attachments(&request.attachments, attachment_store)
566                .await?,
567            tools: Arc::clone(&request.tools),
568            tool_choice: request.tool_choice.clone(),
569            model_variant: request.model_variant.clone(),
570            generation: request.generation.clone(),
571            session_id: request.session_id.clone(),
572            output_spec: request.output_spec.clone(),
573        })
574    }
575
576    pub fn into_request(
577        self,
578        stream_events: Option<LlmEventSender>,
579        provider_trace: Option<LlmProviderTraceSender>,
580    ) -> CoreLlmRequest {
581        CoreLlmRequest {
582            model: self.model,
583            messages: self.messages,
584            attachments: self
585                .attachments
586                .into_iter()
587                .map(LlmAttachmentSpec::into_attachment)
588                .collect(),
589            tools: self.tools,
590            tool_choice: self.tool_choice,
591            model_variant: self.model_variant,
592            generation: self.generation,
593            session_id: self.session_id,
594            output_spec: self.output_spec,
595            stream_events,
596            provider_trace,
597        }
598    }
599}
600
601async fn attachment_specs_from_attachments(
602    attachments: &[LlmAttachment],
603    attachment_store: &dyn AttachmentStore,
604) -> Result<Vec<LlmAttachmentSpec>, RuntimeEffectControllerError> {
605    let mut specs = Vec::with_capacity(attachments.len());
606    for attachment in attachments {
607        specs.push(attachment_spec_from_attachment(attachment, attachment_store).await?);
608    }
609    Ok(specs)
610}
611
612async fn attachment_spec_from_attachment(
613    attachment: &LlmAttachment,
614    attachment_store: &dyn AttachmentStore,
615) -> Result<LlmAttachmentSpec, RuntimeEffectControllerError> {
616    if let Some(reference) = attachment.reference.as_ref() {
617        return Ok(LlmAttachmentSpec {
618            reference: reference.clone(),
619        });
620    }
621    if attachment.data.is_empty() {
622        return Err(RuntimeEffectControllerError::new(
623            "runtime_effect_attachment_missing_reference",
624            "runtime effect attachment has neither a durable reference nor inline bytes",
625        ));
626    }
627    let media_type = MediaType::from_mime(&attachment.mime).ok_or_else(|| {
628        RuntimeEffectControllerError::new(
629            "runtime_effect_attachment_media_type",
630            format!(
631                "attachment media type `{}` cannot be represented durably",
632                attachment.mime
633            ),
634        )
635    })?;
636    let reference = attachment_store
637        .put(
638            attachment.data.clone(),
639            AttachmentCreateMeta::new(media_type, None, None, None),
640        )
641        .await
642        .map_err(|err| {
643            RuntimeEffectControllerError::new(
644                "runtime_effect_attachment_store",
645                format!("failed to store attachment before runtime effect invocation: {err}"),
646            )
647        })?;
648    Ok(LlmAttachmentSpec { reference })
649}
650
651impl RuntimeEffectOutcome {
652    pub fn into_llm_call(
653        self,
654    ) -> Result<(Result<LlmResponse, LlmCallError>, bool), RuntimeEffectControllerError> {
655        match self {
656            Self::LlmCall {
657                result,
658                text_streamed,
659            } => Ok((result, text_streamed)),
660            other => Err(RuntimeEffectControllerError::wrong_outcome(
661                RuntimeEffectKind::LlmCall,
662                other.kind(),
663            )),
664        }
665    }
666
667    pub fn into_direct_response(
668        self,
669    ) -> Result<Result<LlmResponse, LlmCallError>, RuntimeEffectControllerError> {
670        match self {
671            Self::Direct { result } => Ok(result),
672            other => Err(RuntimeEffectControllerError::wrong_outcome(
673                RuntimeEffectKind::Direct,
674                other.kind(),
675            )),
676        }
677    }
678
679    pub fn into_tool_call(self) -> Result<CompletedToolCall, RuntimeEffectControllerError> {
680        match self {
681            Self::ToolCall {
682                launch: ToolCallLaunch::Done { result },
683                ..
684            } => Ok(result),
685            Self::ToolCall {
686                launch: ToolCallLaunch::Pending { .. },
687                ..
688            } => Err(RuntimeEffectControllerError::new(
689                "runtime_effect_tool_call_pending",
690                "tool call launch is pending and has no completed output yet",
691            )),
692            other => Err(RuntimeEffectControllerError::wrong_outcome(
693                RuntimeEffectKind::ToolCall,
694                other.kind(),
695            )),
696        }
697    }
698
699    pub fn into_tool_call_effect(
700        self,
701    ) -> Result<ToolCallEffectOutcome, RuntimeEffectControllerError> {
702        match self {
703            Self::ToolCall { launch, triggers } => Ok(ToolCallEffectOutcome { launch, triggers }),
704            other => Err(RuntimeEffectControllerError::wrong_outcome(
705                RuntimeEffectKind::ToolCall,
706                other.kind(),
707            )),
708        }
709    }
710
711    pub fn into_process(self) -> Result<ProcessEffectOutcome, RuntimeEffectControllerError> {
712        match self {
713            Self::Process { result } => Ok(result),
714            other => Err(RuntimeEffectControllerError::wrong_outcome(
715                RuntimeEffectKind::Process,
716                other.kind(),
717            )),
718        }
719    }
720
721    pub fn into_exec_code(
722        self,
723    ) -> Result<Result<ExecResponse, String>, RuntimeEffectControllerError> {
724        match self {
725            Self::ExecCode { result } => Ok(result),
726            other => Err(RuntimeEffectControllerError::wrong_outcome(
727                RuntimeEffectKind::ExecCode,
728                other.kind(),
729            )),
730        }
731    }
732
733    pub(crate) fn into_checkpoint(self) -> Result<CheckpointOutcome, RuntimeEffectControllerError> {
734        match self {
735            Self::Checkpoint { result } => Ok(result),
736            other => Err(RuntimeEffectControllerError::wrong_outcome(
737                RuntimeEffectKind::Checkpoint,
738                other.kind(),
739            )),
740        }
741    }
742
743    pub fn into_sync_execution_environment(
744        self,
745    ) -> Result<Result<Option<ExecutionEnvironmentSync>, String>, RuntimeEffectControllerError>
746    {
747        match self {
748            Self::SyncExecutionEnvironment { result } => Ok(result),
749            other => Err(RuntimeEffectControllerError::wrong_outcome(
750                RuntimeEffectKind::SyncExecutionEnvironment,
751                other.kind(),
752            )),
753        }
754    }
755
756    pub fn into_await_event(self) -> Result<crate::Resolution, RuntimeEffectControllerError> {
757        match self {
758            Self::AwaitEvent { resolution } => Ok(resolution),
759            other => Err(RuntimeEffectControllerError::wrong_outcome(
760                RuntimeEffectKind::AwaitEvent,
761                other.kind(),
762            )),
763        }
764    }
765
766    pub fn into_durable_step(self) -> Result<serde_json::Value, RuntimeEffectControllerError> {
767        match self {
768            Self::DurableStep { value } => Ok(value),
769            other => Err(RuntimeEffectControllerError::wrong_outcome(
770                RuntimeEffectKind::DurableStep,
771                other.kind(),
772            )),
773        }
774    }
775
776    pub fn kind(&self) -> RuntimeEffectKind {
777        match self {
778            Self::LlmCall { .. } => RuntimeEffectKind::LlmCall,
779            Self::Direct { .. } => RuntimeEffectKind::Direct,
780            Self::ToolCall { .. } => RuntimeEffectKind::ToolCall,
781            Self::Process { .. } => RuntimeEffectKind::Process,
782            Self::ExecCode { .. } => RuntimeEffectKind::ExecCode,
783            Self::Checkpoint { .. } => RuntimeEffectKind::Checkpoint,
784            Self::SyncExecutionEnvironment { .. } => RuntimeEffectKind::SyncExecutionEnvironment,
785            Self::Sleep => RuntimeEffectKind::Sleep,
786            Self::AwaitEvent { .. } => RuntimeEffectKind::AwaitEvent,
787            Self::DurableStep { .. } => RuntimeEffectKind::DurableStep,
788        }
789    }
790}