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, CausalRef, CheckpointDelivery, ExecResponse,
15 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 ToolAttempt,
29 ToolBatch,
30 Process,
31 ExecCode,
32 Checkpoint,
33 SyncExecutionEnvironment,
34 Sleep,
35 AwaitEvent,
36 DurableStep,
37}
38
39impl RuntimeEffectKind {
40 pub fn as_str(self) -> &'static str {
41 match self {
42 Self::LlmCall => "llm_call",
43 Self::Direct => "direct",
44 Self::ToolAttempt => "tool_attempt",
45 Self::ToolBatch => "tool_batch",
46 Self::Process => "process",
47 Self::ExecCode => "exec_code",
48 Self::Checkpoint => "checkpoint",
49 Self::SyncExecutionEnvironment => "sync_execution_environment",
50 Self::Sleep => "sleep",
51 Self::AwaitEvent => "await_event",
52 Self::DurableStep => "durable_step",
53 }
54 }
55}
56
57#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub struct RuntimeInvocation {
60 pub scope: RuntimeScope,
61 pub subject: RuntimeSubject,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub caused_by: Option<CausalRef>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub replay: Option<RuntimeReplay>,
66}
67
68impl RuntimeInvocation {
69 pub fn effect(
70 scope: RuntimeScope,
71 effect_id: impl Into<String>,
72 kind: RuntimeEffectKind,
73 replay_key: impl Into<String>,
74 ) -> Self {
75 Self {
76 scope,
77 subject: RuntimeSubject::Effect {
78 effect_id: effect_id.into(),
79 kind,
80 },
81 caused_by: None,
82 replay: Some(RuntimeReplay {
83 key: replay_key.into(),
84 }),
85 }
86 }
87
88 pub fn with_caused_by(mut self, caused_by: Option<CausalRef>) -> Self {
89 self.caused_by = caused_by;
90 self
91 }
92
93 pub fn effect_id(&self) -> Option<&str> {
94 match &self.subject {
95 RuntimeSubject::Effect { effect_id, .. } => Some(effect_id),
96 _ => None,
97 }
98 }
99
100 pub fn effect_kind(&self) -> Option<RuntimeEffectKind> {
101 match &self.subject {
102 RuntimeSubject::Effect { kind, .. } => Some(*kind),
103 _ => None,
104 }
105 }
106
107 pub fn replay_key(&self) -> Option<&str> {
108 self.replay.as_ref().map(|replay| replay.key.as_str())
109 }
110
111 pub fn causal_ref(&self) -> Option<CausalRef> {
112 match &self.subject {
113 RuntimeSubject::Effect { effect_id, .. } => Some(CausalRef::Effect {
114 session_id: self.scope.session_id.clone(),
115 turn_id: self.scope.turn_id.clone(),
116 effect_id: effect_id.clone(),
117 }),
118 RuntimeSubject::Process { process_id } => Some(CausalRef::Process {
119 process_id: process_id.clone(),
120 }),
121 RuntimeSubject::ProcessEvent {
122 process_id,
123 sequence,
124 ..
125 } => Some(CausalRef::ProcessEvent {
126 process_id: process_id.clone(),
127 sequence: *sequence,
128 }),
129 RuntimeSubject::TriggerOccurrence { occurrence_id } => {
130 Some(CausalRef::TriggerOccurrence {
131 occurrence_id: occurrence_id.clone(),
132 subscription_id: None,
133 })
134 }
135 RuntimeSubject::SessionNode { node_id } => Some(CausalRef::SessionNode {
136 session_id: self.scope.session_id.clone(),
137 node_id: node_id.clone(),
138 }),
139 }
140 }
141}
142
143#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
144pub struct RuntimeScope {
145 pub session_id: String,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub turn_id: Option<String>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub turn_index: Option<usize>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub protocol_iteration: Option<usize>,
152}
153
154impl RuntimeScope {
155 pub fn new(session_id: impl Into<String>) -> Self {
156 Self {
157 session_id: session_id.into(),
158 turn_id: None,
159 turn_index: None,
160 protocol_iteration: None,
161 }
162 }
163
164 pub fn for_turn(
165 session_id: impl Into<String>,
166 turn_id: impl Into<String>,
167 turn_index: usize,
168 protocol_iteration: usize,
169 ) -> Self {
170 Self {
171 session_id: session_id.into(),
172 turn_id: Some(turn_id.into()),
173 turn_index: Some(turn_index),
174 protocol_iteration: Some(protocol_iteration),
175 }
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
180pub struct RuntimeReplay {
181 pub key: String,
182}
183
184#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(tag = "type", rename_all = "snake_case")]
186pub enum RuntimeSubject {
187 Effect {
188 effect_id: String,
189 kind: RuntimeEffectKind,
190 },
191 Process {
192 process_id: String,
193 },
194 ProcessEvent {
195 process_id: String,
196 sequence: u64,
197 event_type: String,
198 },
199 TriggerOccurrence {
200 occurrence_id: String,
201 },
202 SessionNode {
203 node_id: String,
204 },
205}
206
207#[derive(Clone, Debug, Serialize, Deserialize)]
209pub struct RuntimeEffectEnvelope {
210 pub invocation: RuntimeInvocation,
211 pub command: RuntimeEffectCommand,
212}
213
214impl RuntimeEffectEnvelope {
215 pub fn new(invocation: RuntimeInvocation, command: RuntimeEffectCommand) -> Self {
216 Self::try_new(invocation, command).expect("valid runtime effect invocation")
217 }
218
219 pub fn try_new(
220 invocation: RuntimeInvocation,
221 command: RuntimeEffectCommand,
222 ) -> Result<Self, RuntimeEffectControllerError> {
223 validate_effect_invocation(&invocation, command.kind())?;
224 validate_effect_command(&command)?;
225 Ok(Self {
226 invocation,
227 command,
228 })
229 }
230
231 pub fn stable_hash(&self) -> Result<String, RuntimeEffectControllerError> {
232 crate::stable_hash::stable_json_sha256_hex(self).map_err(|err| {
233 RuntimeEffectControllerError::new(
234 "runtime_effect_envelope_hash",
235 format!("failed to serialize runtime effect envelope: {err}"),
236 )
237 })
238 }
239}
240
241fn validate_effect_invocation(
242 invocation: &RuntimeInvocation,
243 command_kind: RuntimeEffectKind,
244) -> Result<(), RuntimeEffectControllerError> {
245 let RuntimeSubject::Effect { effect_id, kind } = &invocation.subject else {
246 return Err(RuntimeEffectControllerError::new(
247 "runtime_effect_invocation_subject",
248 "runtime effect envelope subject must be an effect",
249 ));
250 };
251 if effect_id.trim().is_empty() {
252 return Err(RuntimeEffectControllerError::new(
253 "runtime_effect_invocation_subject",
254 "runtime effect envelope effect id must be non-empty",
255 ));
256 }
257 if *kind != command_kind {
258 return Err(RuntimeEffectControllerError::new(
259 "runtime_effect_invocation_kind",
260 format!(
261 "runtime effect invocation kind {} does not match command kind {}",
262 kind.as_str(),
263 command_kind.as_str()
264 ),
265 ));
266 }
267 if invocation
268 .replay
269 .as_ref()
270 .is_none_or(|replay| replay.key.is_empty())
271 {
272 return Err(RuntimeEffectControllerError::new(
273 "runtime_effect_replay_required",
274 "runtime effect envelope requires replay.key",
275 ));
276 }
277 Ok(())
278}
279
280fn validate_effect_command(
281 command: &RuntimeEffectCommand,
282) -> Result<(), RuntimeEffectControllerError> {
283 if let RuntimeEffectCommand::DurableStep { step_id, .. } = command
284 && step_id.trim().is_empty()
285 {
286 return Err(RuntimeEffectControllerError::new(
287 "runtime_effect_durable_step_id",
288 "runtime effect durable step id must be non-empty",
289 ));
290 }
291 if let RuntimeEffectCommand::ToolAttempt {
292 call,
293 execution_grant: _,
294 attempt,
295 max_attempts,
296 } = command
297 {
298 if call.call_id.trim().is_empty() {
299 return Err(RuntimeEffectControllerError::new(
300 "runtime_effect_tool_attempt_call_id",
301 "runtime effect tool attempt requires a non-empty call id",
302 ));
303 }
304 if *attempt == 0 || *max_attempts == 0 || *attempt > *max_attempts {
305 return Err(RuntimeEffectControllerError::new(
306 "runtime_effect_tool_attempt_index",
307 format!(
308 "runtime effect tool attempt must satisfy 1 <= attempt <= max_attempts, got {attempt}/{max_attempts}"
309 ),
310 ));
311 }
312 }
313 if let RuntimeEffectCommand::ToolBatch { batch } = command {
314 if batch.batch_id.trim().is_empty() {
315 return Err(RuntimeEffectControllerError::new(
316 "runtime_effect_tool_batch_id",
317 "runtime effect tool batch id must be non-empty",
318 ));
319 }
320 if batch.calls.is_empty() {
321 return Err(RuntimeEffectControllerError::new(
322 "runtime_effect_tool_batch_empty",
323 "runtime effect tool batch must contain at least one prepared call",
324 ));
325 }
326 for (index, call) in batch.calls.iter().enumerate() {
327 if call.call.call_id.trim().is_empty() {
328 return Err(RuntimeEffectControllerError::new(
329 "runtime_effect_tool_batch_call_id",
330 format!("runtime effect tool batch call {index} has an empty call id"),
331 ));
332 }
333 if call.replay_suffix.trim().is_empty() {
334 return Err(RuntimeEffectControllerError::new(
335 "runtime_effect_tool_batch_call_replay",
336 format!("runtime effect tool batch call {index} has an empty replay suffix"),
337 ));
338 }
339 }
340 }
341 Ok(())
342}
343
344#[derive(Clone, Debug, Serialize, Deserialize)]
346#[serde(tag = "type", rename_all = "snake_case")]
347pub enum RuntimeEffectCommand {
348 LlmCall {
349 request: Box<LlmRequestSpec>,
350 },
351 Direct {
352 request: Box<LlmRequestSpec>,
353 usage_source: String,
354 },
355 ToolAttempt {
356 call: crate::PreparedToolCall,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 execution_grant: Option<Box<crate::ToolExecutionGrant>>,
359 attempt: u32,
360 max_attempts: u32,
361 },
362 ToolBatch {
363 batch: crate::PreparedToolBatch,
364 },
365 Process {
366 command: Box<ProcessCommand>,
367 },
368 ExecCode {
369 language: String,
370 code: String,
371 },
372 Checkpoint {
373 checkpoint: CheckpointKind,
374 },
375 SyncExecutionEnvironment {
376 update_machine_config: bool,
377 },
378 Sleep {
379 duration_ms: u64,
380 },
381 AwaitEvent {
382 key: crate::AwaitEventKey,
383 },
384 DurableStep {
385 step_id: String,
386 input: serde_json::Value,
387 },
388}
389
390impl RuntimeEffectCommand {
391 pub fn process(command: ProcessCommand) -> Self {
392 Self::Process {
393 command: Box::new(command),
394 }
395 }
396
397 pub fn kind(&self) -> RuntimeEffectKind {
398 match self {
399 Self::LlmCall { .. } => RuntimeEffectKind::LlmCall,
400 Self::Direct { .. } => RuntimeEffectKind::Direct,
401 Self::ToolAttempt { .. } => RuntimeEffectKind::ToolAttempt,
402 Self::ToolBatch { .. } => RuntimeEffectKind::ToolBatch,
403 Self::Process { .. } => RuntimeEffectKind::Process,
404 Self::ExecCode { .. } => RuntimeEffectKind::ExecCode,
405 Self::Checkpoint { .. } => RuntimeEffectKind::Checkpoint,
406 Self::SyncExecutionEnvironment { .. } => RuntimeEffectKind::SyncExecutionEnvironment,
407 Self::Sleep { .. } => RuntimeEffectKind::Sleep,
408 Self::AwaitEvent { .. } => RuntimeEffectKind::AwaitEvent,
409 Self::DurableStep { .. } => RuntimeEffectKind::DurableStep,
410 }
411 }
412}
413
414#[derive(Clone, Debug, Serialize, Deserialize)]
416#[serde(tag = "op", rename_all = "snake_case")]
417#[allow(clippy::large_enum_variant)]
418pub enum ProcessCommand {
419 Start {
420 registration: ProcessRegistration,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 grant: Option<ProcessStartGrant>,
423 #[serde(
424 default,
425 skip_serializing_if = "boxed_process_execution_context_is_empty"
426 )]
427 execution_context: Box<ProcessExecutionContext>,
428 },
429 List {
430 session_scope: SessionScope,
431 #[serde(default)]
432 mode: ProcessListMode,
433 },
434 Transfer {
435 from_scope: SessionScope,
436 to_scope: SessionScope,
437 process_ids: Vec<String>,
438 },
439 DeleteSession {
440 session_id: String,
441 },
442 Await {
443 process_id: String,
444 },
445 Cancel {
446 process_id: String,
447 reason: Option<String>,
448 },
449 Signal {
450 process_id: String,
451 signal_name: String,
452 signal_id: String,
453 request: crate::ProcessEventAppendRequest,
454 },
455}
456
457fn boxed_process_execution_context_is_empty(context: &ProcessExecutionContext) -> bool {
458 context.is_empty()
459}
460
461type CheckpointOutcome = Result<CheckpointDelivery, RuntimeEffectControllerError>;
462
463impl ProcessCommand {
464 pub fn effect_id(&self) -> String {
465 match self {
466 Self::Start { registration, .. } => format!("process:start:{}", registration.id),
467 Self::List {
468 session_scope,
469 mode,
470 } => {
471 format!("process:list:{}:{}", session_scope.id(), mode.as_str())
472 }
473 Self::Transfer {
474 from_scope,
475 to_scope,
476 process_ids,
477 } => {
478 let digest = crate::stable_hash::stable_json_sha256_hex(process_ids)
479 .unwrap_or_else(|_| "unhashable".to_string());
480 format!(
481 "process:transfer:{}:{}:{digest}",
482 from_scope.id(),
483 to_scope.id()
484 )
485 }
486 Self::DeleteSession { session_id } => format!("process:delete-session:{session_id}"),
487 Self::Await { process_id } => format!("process:await:{process_id}"),
488 Self::Cancel { process_id, .. } => format!("process:cancel:{process_id}"),
489 Self::Signal {
490 process_id,
491 signal_name,
492 signal_id,
493 ..
494 } => {
495 format!("process:signal:{process_id}:signal.{signal_name}:{signal_id}")
496 }
497 }
498 }
499}
500
501#[derive(Clone, Debug, Serialize, Deserialize)]
503#[serde(tag = "op", rename_all = "snake_case")]
504pub enum ProcessEffectOutcome {
505 Start {
506 record: Box<ProcessRecord>,
510 },
511 List {
512 entries: Vec<ProcessHandleGrantEntry>,
513 },
514 Transfer,
515 DeleteSession {
516 report: crate::ProcessSessionDeleteReport,
517 },
518 Await {
519 output: ProcessAwaitOutput,
520 },
521 Cancel {
522 record: Box<ProcessRecord>,
523 },
524 Signal {
525 event: Box<crate::ProcessEvent>,
528 },
529}
530
531#[derive(Clone, Debug, Serialize, Deserialize)]
532pub struct ToolAttemptEffectOutcome {
533 pub launch: ToolAttemptLaunch,
534 #[serde(default, skip_serializing_if = "Vec::is_empty")]
535 pub triggers: Vec<ToolTriggerEffectOutcome>,
536}
537
538#[derive(Clone, Debug, Serialize, Deserialize)]
539pub struct ToolBatchEffectOutcome {
540 pub launches: Vec<ToolCallLaunch>,
541 #[serde(default, skip_serializing_if = "Vec::is_empty")]
542 pub triggers: Vec<ToolTriggerEffectOutcome>,
543}
544
545#[derive(Clone, Debug, Serialize, Deserialize)]
546#[serde(tag = "status", rename_all = "snake_case")]
547#[allow(clippy::large_enum_variant)]
548pub enum ToolCallLaunch {
549 Done {
550 result: CompletedToolCall,
551 },
552 Pending {
553 key: crate::AwaitEventKey,
554 pending: crate::PendingCompletion,
555 duration_ms: u64,
556 },
557}
558
559#[derive(Clone, Debug, Serialize, Deserialize)]
560#[serde(tag = "status", rename_all = "snake_case")]
561pub enum ToolAttemptLaunch {
562 Done {
563 record: crate::ToolCallRecord,
564 },
565 Pending {
566 key: crate::AwaitEventKey,
567 pending: crate::PendingCompletion,
568 duration_ms: u64,
569 },
570}
571
572#[derive(Clone, Debug, Serialize, Deserialize)]
574#[serde(tag = "type", rename_all = "snake_case")]
575#[allow(clippy::large_enum_variant)]
576pub enum RuntimeEffectOutcome {
577 LlmCall {
578 result: Result<LlmResponse, LlmCallError>,
579 text_streamed: bool,
580 },
581 Direct {
582 result: Result<LlmResponse, LlmCallError>,
583 },
584 ToolAttempt {
585 launch: ToolAttemptLaunch,
586 #[serde(default, skip_serializing_if = "Vec::is_empty")]
587 triggers: Vec<ToolTriggerEffectOutcome>,
588 },
589 ToolBatch {
590 launches: Vec<ToolCallLaunch>,
591 #[serde(default, skip_serializing_if = "Vec::is_empty")]
592 triggers: Vec<ToolTriggerEffectOutcome>,
593 },
594 Process {
595 result: ProcessEffectOutcome,
596 },
597 ExecCode {
598 result: Result<ExecResponse, String>,
599 },
600 Checkpoint {
601 result: CheckpointOutcome,
602 },
603 SyncExecutionEnvironment {
604 result: Result<Option<ExecutionEnvironmentSync>, String>,
605 },
606 Sleep,
607 AwaitEvent {
608 resolution: crate::Resolution,
609 },
610 DurableStep {
611 value: serde_json::Value,
612 },
613}
614
615#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
625pub struct LlmAttachmentSpec {
626 pub reference: AttachmentRef,
627}
628
629impl LlmAttachmentSpec {
630 fn into_attachment(self) -> LlmAttachment {
631 LlmAttachment::reference(self.reference)
632 }
633}
634
635#[derive(Clone, Debug, Serialize, Deserialize)]
639pub struct LlmRequestSpec {
640 pub model: String,
641 pub messages: Vec<LlmMessage>,
642 pub attachments: Vec<LlmAttachmentSpec>,
643 pub tools: Arc<Vec<LlmToolSpec>>,
644 pub tool_choice: LlmToolChoice,
645 pub model_variant: Option<String>,
646 #[serde(default)]
647 pub model_capability: crate::ModelCapability,
648 #[serde(default)]
649 pub generation: crate::GenerationOptions,
650 pub scope: crate::LlmRequestScope,
651 pub output_spec: Option<LlmOutputSpec>,
652}
653
654impl LlmRequestSpec {
655 pub async fn from_request(
656 request: &CoreLlmRequest,
657 attachment_store: &crate::SessionAttachmentStore,
658 ) -> Result<Self, RuntimeEffectControllerError> {
659 Ok(Self {
660 model: request.model.clone(),
661 messages: request.messages.clone(),
662 attachments: attachment_specs_from_attachments(&request.attachments, attachment_store)
663 .await?,
664 tools: Arc::clone(&request.tools),
665 tool_choice: request.tool_choice.clone(),
666 model_variant: request.model_variant.clone(),
667 model_capability: request.model_capability.clone(),
668 generation: request.generation.clone(),
669 scope: request.scope.clone(),
670 output_spec: request.output_spec.clone(),
671 })
672 }
673
674 pub fn into_request(
675 self,
676 stream_events: Option<LlmEventSender>,
677 provider_trace: Option<LlmProviderTraceSender>,
678 ) -> CoreLlmRequest {
679 CoreLlmRequest {
680 model: self.model,
681 messages: self.messages,
682 attachments: self
683 .attachments
684 .into_iter()
685 .map(LlmAttachmentSpec::into_attachment)
686 .collect(),
687 tools: self.tools,
688 tool_choice: self.tool_choice,
689 model_variant: self.model_variant,
690 model_capability: self.model_capability,
691 generation: self.generation,
692 scope: self.scope,
693 output_spec: self.output_spec,
694 stream_events,
695 provider_trace,
696 }
697 }
698}
699
700async fn attachment_specs_from_attachments(
701 attachments: &[LlmAttachment],
702 attachment_store: &crate::SessionAttachmentStore,
703) -> Result<Vec<LlmAttachmentSpec>, RuntimeEffectControllerError> {
704 let mut specs = Vec::with_capacity(attachments.len());
705 for attachment in attachments {
706 specs.push(attachment_spec_from_attachment(attachment, attachment_store).await?);
707 }
708 Ok(specs)
709}
710
711async fn attachment_spec_from_attachment(
712 attachment: &LlmAttachment,
713 attachment_store: &crate::SessionAttachmentStore,
714) -> Result<LlmAttachmentSpec, RuntimeEffectControllerError> {
715 if let Some(reference) = attachment.reference.as_ref() {
716 return Ok(LlmAttachmentSpec {
717 reference: reference.clone(),
718 });
719 }
720 if attachment.data.is_empty() {
721 return Err(RuntimeEffectControllerError::new(
722 "runtime_effect_attachment_missing_reference",
723 "runtime effect attachment has neither a durable reference nor inline bytes",
724 ));
725 }
726 let media_type = MediaType::from_mime(&attachment.mime).ok_or_else(|| {
727 RuntimeEffectControllerError::new(
728 "runtime_effect_attachment_media_type",
729 format!(
730 "attachment media type `{}` cannot be represented durably",
731 attachment.mime
732 ),
733 )
734 })?;
735 let reference = attachment_store
736 .put(
737 attachment.data.clone(),
738 AttachmentCreateMeta::new(media_type, None, None, None),
739 )
740 .await
741 .map_err(|err| {
742 RuntimeEffectControllerError::new(
743 "runtime_effect_attachment_store",
744 format!("failed to store attachment before runtime effect invocation: {err}"),
745 )
746 })?;
747 Ok(LlmAttachmentSpec { reference })
748}
749
750impl RuntimeEffectOutcome {
751 pub fn into_llm_call(
752 self,
753 ) -> Result<(Result<LlmResponse, LlmCallError>, bool), RuntimeEffectControllerError> {
754 match self {
755 Self::LlmCall {
756 result,
757 text_streamed,
758 } => Ok((result, text_streamed)),
759 other => Err(RuntimeEffectControllerError::wrong_outcome(
760 RuntimeEffectKind::LlmCall,
761 other.kind(),
762 )),
763 }
764 }
765
766 pub fn into_direct_response(
767 self,
768 ) -> Result<Result<LlmResponse, LlmCallError>, RuntimeEffectControllerError> {
769 match self {
770 Self::Direct { result } => Ok(result),
771 other => Err(RuntimeEffectControllerError::wrong_outcome(
772 RuntimeEffectKind::Direct,
773 other.kind(),
774 )),
775 }
776 }
777
778 pub fn into_tool_attempt_effect(
779 self,
780 ) -> Result<ToolAttemptEffectOutcome, RuntimeEffectControllerError> {
781 match self {
782 Self::ToolAttempt { launch, triggers } => {
783 Ok(ToolAttemptEffectOutcome { launch, triggers })
784 }
785 other => Err(RuntimeEffectControllerError::wrong_outcome(
786 RuntimeEffectKind::ToolAttempt,
787 other.kind(),
788 )),
789 }
790 }
791
792 pub fn into_tool_batch_effect(
793 self,
794 ) -> Result<ToolBatchEffectOutcome, RuntimeEffectControllerError> {
795 match self {
796 Self::ToolBatch { launches, triggers } => {
797 Ok(ToolBatchEffectOutcome { launches, triggers })
798 }
799 other => Err(RuntimeEffectControllerError::wrong_outcome(
800 RuntimeEffectKind::ToolBatch,
801 other.kind(),
802 )),
803 }
804 }
805
806 pub fn into_process(self) -> Result<ProcessEffectOutcome, RuntimeEffectControllerError> {
807 match self {
808 Self::Process { result } => Ok(result),
809 other => Err(RuntimeEffectControllerError::wrong_outcome(
810 RuntimeEffectKind::Process,
811 other.kind(),
812 )),
813 }
814 }
815
816 pub fn into_exec_code(
817 self,
818 ) -> Result<Result<ExecResponse, String>, RuntimeEffectControllerError> {
819 match self {
820 Self::ExecCode { result } => Ok(result),
821 other => Err(RuntimeEffectControllerError::wrong_outcome(
822 RuntimeEffectKind::ExecCode,
823 other.kind(),
824 )),
825 }
826 }
827
828 pub(crate) fn into_checkpoint(self) -> Result<CheckpointOutcome, RuntimeEffectControllerError> {
829 match self {
830 Self::Checkpoint { result } => Ok(result),
831 other => Err(RuntimeEffectControllerError::wrong_outcome(
832 RuntimeEffectKind::Checkpoint,
833 other.kind(),
834 )),
835 }
836 }
837
838 pub fn into_sync_execution_environment(
839 self,
840 ) -> Result<Result<Option<ExecutionEnvironmentSync>, String>, RuntimeEffectControllerError>
841 {
842 match self {
843 Self::SyncExecutionEnvironment { result } => Ok(result),
844 other => Err(RuntimeEffectControllerError::wrong_outcome(
845 RuntimeEffectKind::SyncExecutionEnvironment,
846 other.kind(),
847 )),
848 }
849 }
850
851 pub fn into_await_event(self) -> Result<crate::Resolution, RuntimeEffectControllerError> {
852 match self {
853 Self::AwaitEvent { resolution } => Ok(resolution),
854 other => Err(RuntimeEffectControllerError::wrong_outcome(
855 RuntimeEffectKind::AwaitEvent,
856 other.kind(),
857 )),
858 }
859 }
860
861 pub fn into_durable_step(self) -> Result<serde_json::Value, RuntimeEffectControllerError> {
862 match self {
863 Self::DurableStep { value } => Ok(value),
864 other => Err(RuntimeEffectControllerError::wrong_outcome(
865 RuntimeEffectKind::DurableStep,
866 other.kind(),
867 )),
868 }
869 }
870
871 pub fn kind(&self) -> RuntimeEffectKind {
872 match self {
873 Self::LlmCall { .. } => RuntimeEffectKind::LlmCall,
874 Self::Direct { .. } => RuntimeEffectKind::Direct,
875 Self::ToolAttempt { .. } => RuntimeEffectKind::ToolAttempt,
876 Self::ToolBatch { .. } => RuntimeEffectKind::ToolBatch,
877 Self::Process { .. } => RuntimeEffectKind::Process,
878 Self::ExecCode { .. } => RuntimeEffectKind::ExecCode,
879 Self::Checkpoint { .. } => RuntimeEffectKind::Checkpoint,
880 Self::SyncExecutionEnvironment { .. } => RuntimeEffectKind::SyncExecutionEnvironment,
881 Self::Sleep => RuntimeEffectKind::Sleep,
882 Self::AwaitEvent { .. } => RuntimeEffectKind::AwaitEvent,
883 Self::DurableStep { .. } => RuntimeEffectKind::DurableStep,
884 }
885 }
886}