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