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#[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#[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#[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#[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#[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#[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#[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#[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#[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}