1use super::RunId;
8use super::run_primitive::RunPrimitive;
9use super::run_receipt::RunBoundaryReceiptDraft;
10use crate::error::AgentError;
11use crate::service::SessionError;
12use crate::session::PendingSystemContextAppend;
13use crate::turn_execution_authority::{TurnTerminalCauseKind, TurnTerminalOutcome};
14use crate::types::RunResult;
15use crate::{TurnErrorMetadata, event::AgentEvent, interaction::InteractionId};
16use serde_json::Value;
17use sha2::{Digest, Sha256};
18use std::sync::Arc;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum CoreApplyFailureCauseKind {
24 PrimitiveRejected,
25 RuntimeContextApply,
26 RuntimeTurn,
27 HookDenied,
28 HookRuntimeFailure,
29 ExecutorStopped,
30 ExecutorControlFailed,
31 ExecutorInternal,
32 Unknown,
33}
34
35impl CoreApplyFailureCauseKind {
36 pub fn as_str(self) -> &'static str {
37 match self {
38 Self::PrimitiveRejected => "PrimitiveRejected",
39 Self::RuntimeContextApply => "RuntimeContextApply",
40 Self::RuntimeTurn => "RuntimeTurn",
41 Self::HookDenied => "HookDenied",
42 Self::HookRuntimeFailure => "HookRuntimeFailure",
43 Self::ExecutorStopped => "ExecutorStopped",
44 Self::ExecutorControlFailed => "ExecutorControlFailed",
45 Self::ExecutorInternal => "ExecutorInternal",
46 Self::Unknown => "Unknown",
47 }
48 }
49
50 pub fn from_wire_str(value: &str) -> Option<Self> {
51 match value {
52 "PrimitiveRejected" => Some(Self::PrimitiveRejected),
53 "RuntimeContextApply" => Some(Self::RuntimeContextApply),
54 "RuntimeTurn" => Some(Self::RuntimeTurn),
55 "HookDenied" => Some(Self::HookDenied),
56 "HookRuntimeFailure" => Some(Self::HookRuntimeFailure),
57 "ExecutorStopped" => Some(Self::ExecutorStopped),
58 "ExecutorControlFailed" => Some(Self::ExecutorControlFailed),
59 "ExecutorInternal" => Some(Self::ExecutorInternal),
60 "Unknown" => Some(Self::Unknown),
61 _ => None,
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct CoreApplyFailureCause {
69 pub kind: CoreApplyFailureCauseKind,
70 pub message: String,
71}
72
73impl CoreApplyFailureCause {
74 pub fn new(kind: CoreApplyFailureCauseKind, message: impl Into<String>) -> Self {
75 Self {
76 kind,
77 message: message.into(),
78 }
79 }
80
81 pub fn primitive_rejected(message: impl Into<String>) -> Self {
82 Self::new(CoreApplyFailureCauseKind::PrimitiveRejected, message)
83 }
84
85 pub fn runtime_context_apply(message: impl Into<String>) -> Self {
86 Self::new(CoreApplyFailureCauseKind::RuntimeContextApply, message)
87 }
88
89 pub fn runtime_turn(message: impl Into<String>) -> Self {
90 Self::new(CoreApplyFailureCauseKind::RuntimeTurn, message)
91 }
92
93 pub fn hook_denied(message: impl Into<String>) -> Self {
94 Self::new(CoreApplyFailureCauseKind::HookDenied, message)
95 }
96
97 pub fn hook_runtime_failure(message: impl Into<String>) -> Self {
98 Self::new(CoreApplyFailureCauseKind::HookRuntimeFailure, message)
99 }
100
101 pub fn executor_stopped() -> Self {
102 Self::new(
103 CoreApplyFailureCauseKind::ExecutorStopped,
104 "executor is stopped",
105 )
106 }
107
108 pub fn executor_control_failed(message: impl Into<String>) -> Self {
109 Self::new(CoreApplyFailureCauseKind::ExecutorControlFailed, message)
110 }
111
112 pub fn executor_internal(message: impl Into<String>) -> Self {
113 Self::new(CoreApplyFailureCauseKind::ExecutorInternal, message)
114 }
115
116 pub fn unknown(message: impl Into<String>) -> Self {
117 Self::new(CoreApplyFailureCauseKind::Unknown, message)
118 }
119
120 pub fn from_agent_error(error: &AgentError) -> Self {
121 match error {
122 AgentError::HookDenied { .. } => Self::hook_denied(error.to_string()),
123 AgentError::HookTimeout { .. }
124 | AgentError::HookExecutionFailed { .. }
125 | AgentError::HookConfigInvalid { .. } => Self::hook_runtime_failure(error.to_string()),
126 _ => Self::runtime_turn(error.to_string()),
127 }
128 }
129
130 pub fn from_session_error(error: &SessionError) -> Self {
131 match error {
132 SessionError::Agent(agent_error) => Self::from_agent_error(agent_error),
133 _ => Self::runtime_turn(error.to_string()),
134 }
135 }
136
137 pub fn message(&self) -> &str {
138 &self.message
139 }
140}
141
142impl std::fmt::Display for CoreApplyFailureCause {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 f.write_str(&self.message)
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150#[non_exhaustive]
151pub enum CoreControlFailureCauseKind {
152 RuntimeControl,
153 ExecutorInternal,
154 Unknown,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct CoreControlFailureCause {
160 pub kind: CoreControlFailureCauseKind,
161 pub message: String,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171#[non_exhaustive]
172pub enum CoreExecutorTeardownReason {
173 ArchivedSession,
174 SessionUnavailable,
175}
176
177impl CoreExecutorTeardownReason {
178 pub fn as_str(self) -> &'static str {
179 match self {
180 Self::ArchivedSession => "ArchivedSession",
181 Self::SessionUnavailable => "SessionUnavailable",
182 }
183 }
184
185 pub fn from_wire_str(value: &str) -> Option<Self> {
186 match value {
187 "ArchivedSession" => Some(Self::ArchivedSession),
188 "SessionUnavailable" => Some(Self::SessionUnavailable),
189 _ => None,
190 }
191 }
192}
193
194impl CoreControlFailureCause {
195 pub fn new(kind: CoreControlFailureCauseKind, message: impl Into<String>) -> Self {
196 Self {
197 kind,
198 message: message.into(),
199 }
200 }
201
202 pub fn runtime_control(message: impl Into<String>) -> Self {
203 Self::new(CoreControlFailureCauseKind::RuntimeControl, message)
204 }
205
206 pub fn executor_internal(message: impl Into<String>) -> Self {
207 Self::new(CoreControlFailureCauseKind::ExecutorInternal, message)
208 }
209
210 pub fn unknown(message: impl Into<String>) -> Self {
211 Self::new(CoreControlFailureCauseKind::Unknown, message)
212 }
213}
214
215impl std::fmt::Display for CoreControlFailureCause {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 f.write_str(&self.message)
218 }
219}
220
221#[derive(Debug, Clone, thiserror::Error)]
223#[non_exhaustive]
224pub enum CoreExecutorError {
225 #[error("Apply failed: {cause}")]
227 ApplyFailed { cause: CoreApplyFailureCause },
228
229 #[error("Terminal failure: {outcome:?} ({cause_kind:?}): {message}")]
233 TerminalFailure {
234 outcome: TurnTerminalOutcome,
235 cause_kind: TurnTerminalCauseKind,
236 message: String,
237 },
238
239 #[error("Executor requires teardown ({reason:?}): {message}")]
244 TeardownRequired {
245 reason: CoreExecutorTeardownReason,
246 message: String,
247 },
248
249 #[error("Control failed: {cause}")]
251 ControlFailed { cause: CoreControlFailureCause },
252
253 #[error("Executor is stopped")]
255 Stopped,
256
257 #[error("Run was cancelled")]
259 Cancelled,
260
261 #[error("Internal error: {0}")]
263 Internal(String),
264}
265
266impl CoreExecutorError {
267 pub fn apply_failed(cause: CoreApplyFailureCause) -> Self {
268 Self::ApplyFailed { cause }
269 }
270
271 pub fn apply_failed_primitive_rejected(message: impl Into<String>) -> Self {
272 Self::apply_failed(CoreApplyFailureCause::primitive_rejected(message))
273 }
274
275 pub fn apply_failed_runtime_context(message: impl Into<String>) -> Self {
276 Self::apply_failed(CoreApplyFailureCause::runtime_context_apply(message))
277 }
278
279 pub fn apply_failed_runtime_turn(message: impl Into<String>) -> Self {
280 Self::apply_failed(CoreApplyFailureCause::runtime_turn(message))
281 }
282
283 pub fn terminal_failure(
284 outcome: TurnTerminalOutcome,
285 cause_kind: TurnTerminalCauseKind,
286 message: impl Into<String>,
287 ) -> Self {
288 Self::TerminalFailure {
289 outcome,
290 cause_kind,
291 message: message.into(),
292 }
293 }
294
295 pub fn teardown_required(
296 reason: CoreExecutorTeardownReason,
297 message: impl Into<String>,
298 ) -> Self {
299 Self::TeardownRequired {
300 reason,
301 message: message.into(),
302 }
303 }
304
305 pub fn archived_session_requires_teardown(message: impl Into<String>) -> Self {
306 Self::teardown_required(CoreExecutorTeardownReason::ArchivedSession, message)
307 }
308
309 pub fn session_unavailable_requires_teardown(message: impl Into<String>) -> Self {
310 Self::teardown_required(CoreExecutorTeardownReason::SessionUnavailable, message)
311 }
312
313 pub fn apply_failed_from_session_error(error: SessionError) -> Self {
314 if error.requests_runtime_executor_stop() {
315 return Self::Stopped;
316 }
317 match error {
318 SessionError::Agent(AgentError::Cancelled) => Self::Cancelled,
319 SessionError::Agent(AgentError::StickyModelFallbackAuthorityUnknown { message }) => {
320 Self::session_unavailable_requires_teardown(message)
321 }
322 SessionError::Agent(AgentError::TerminalFailure {
323 outcome,
324 cause_kind,
325 message,
326 }) if cause_kind.is_specific_failure_cause() => {
327 Self::terminal_failure(outcome, cause_kind, message)
328 }
329 SessionError::Agent(AgentError::TerminalFailure { cause_kind, .. }) => Self::Internal(
330 format!("runtime turn returned unknown machine terminal cause: {cause_kind:?}"),
331 ),
332 error => Self::apply_failed(CoreApplyFailureCause::from_session_error(&error)),
333 }
334 }
335
336 pub fn apply_failed_unknown(message: impl Into<String>) -> Self {
337 Self::apply_failed(CoreApplyFailureCause::unknown(message))
338 }
339
340 pub fn cancelled() -> Self {
341 Self::Cancelled
342 }
343
344 pub fn is_cancelled(&self) -> bool {
345 matches!(self, Self::Cancelled)
346 }
347
348 pub fn requires_runtime_teardown(&self) -> bool {
349 matches!(self, Self::TeardownRequired { .. })
350 }
351
352 pub fn control_failed(cause: CoreControlFailureCause) -> Self {
353 Self::ControlFailed { cause }
354 }
355
356 pub fn control_failed_runtime(message: impl Into<String>) -> Self {
357 Self::control_failed(CoreControlFailureCause::runtime_control(message))
358 }
359
360 pub fn apply_failure_cause(&self) -> CoreApplyFailureCause {
361 match self {
362 Self::ApplyFailed { cause } => cause.clone(),
363 Self::TerminalFailure { cause_kind, .. } => {
364 CoreApplyFailureCause::executor_internal(format!(
365 "typed machine terminal failure escaped runtime-loop handling: {cause_kind:?}"
366 ))
367 }
368 Self::TeardownRequired { reason, message } => CoreApplyFailureCause::new(
369 CoreApplyFailureCauseKind::ExecutorStopped,
370 format!("executor requested {} teardown: {message}", reason.as_str()),
371 ),
372 Self::ControlFailed { cause } => {
373 CoreApplyFailureCause::executor_control_failed(cause.message.clone())
374 }
375 Self::Stopped => CoreApplyFailureCause::executor_stopped(),
376 Self::Cancelled => CoreApplyFailureCause::runtime_turn("cancelled"),
377 Self::Internal(message) => CoreApplyFailureCause::executor_internal(message.clone()),
378 }
379 }
380}
381
382#[derive(Debug, Clone)]
384pub enum CoreApplyTerminal {
385 RunResult(Box<RunResult>),
387 NoPendingBoundary,
389 MachineTerminalFailure { error: TurnErrorMetadata },
394 CallbackPending {
397 tool_use_id: String,
398 tool_name: String,
399 args: Value,
400 },
401 CallbackBatchPending {
404 pending_tool_calls: Vec<crate::error::PendingCallbackToolCall>,
405 },
406}
407
408#[derive(Debug, Clone)]
409pub struct CoreApplyOutput {
410 pub receipt: RunBoundaryReceiptDraft,
415 pub session_snapshot: Option<Vec<u8>>,
418 pub terminal: Option<CoreApplyTerminal>,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
429pub struct CoreInteractionTerminalPublicationReceipt {
430 interaction_id: InteractionId,
431 terminal_seq: u64,
432 payload_digest: String,
433}
434
435impl CoreInteractionTerminalPublicationReceipt {
436 pub fn try_new(event: &AgentEvent, terminal_seq: u64) -> Result<Self, CoreExecutorError> {
437 if terminal_seq == 0 {
438 return Err(CoreExecutorError::Internal(
439 "interaction terminal durable sequence must be non-zero".to_string(),
440 ));
441 }
442 let interaction_id = match event {
443 AgentEvent::InteractionComplete { interaction_id, .. }
444 | AgentEvent::InteractionCallbackPending { interaction_id, .. }
445 | AgentEvent::InteractionFailed { interaction_id, .. } => *interaction_id,
446 _ => {
447 return Err(CoreExecutorError::Internal(
448 "interaction terminal publication receipt requires an Interaction terminal event"
449 .to_string(),
450 ));
451 }
452 };
453 let encoded = serde_json::to_vec(event).map_err(|error| {
454 CoreExecutorError::Internal(format!(
455 "failed to encode interaction terminal publication receipt: {error}"
456 ))
457 })?;
458 Ok(Self {
459 interaction_id,
460 terminal_seq,
461 payload_digest: format!("{:x}", Sha256::digest(encoded)),
462 })
463 }
464
465 pub fn interaction_id(&self) -> InteractionId {
466 self.interaction_id
467 }
468
469 pub fn terminal_seq(&self) -> u64 {
470 self.terminal_seq
471 }
472
473 pub fn payload_digest(&self) -> &str {
474 &self.payload_digest
475 }
476}
477
478#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
485pub enum CoreBoundaryStageError {
486 #[error("active turn boundary is unavailable: {reason}")]
487 Unavailable { reason: String },
488 #[error("active turn boundary authority is stale: {reason}")]
489 Stale { reason: String },
490 #[error("active turn boundary preparation failed: {reason}")]
491 Fault { reason: String },
492}
493
494impl CoreBoundaryStageError {
495 pub fn unavailable(reason: impl Into<String>) -> Self {
496 Self::Unavailable {
497 reason: reason.into(),
498 }
499 }
500
501 pub fn stale(reason: impl Into<String>) -> Self {
502 Self::Stale {
503 reason: reason.into(),
504 }
505 }
506
507 pub fn fault(reason: impl Into<String>) -> Self {
508 Self::Fault {
509 reason: reason.into(),
510 }
511 }
512
513 #[must_use]
514 pub fn is_unavailable(&self) -> bool {
515 matches!(self, Self::Unavailable { .. })
516 }
517}
518
519pub(crate) trait CoreBoundaryStageCommitAuthority: Send {
520 fn commit(&mut self) -> Result<(), CoreBoundaryStageError>;
521 fn abort(&mut self) -> Result<(), CoreBoundaryStageError>;
522}
523
524#[must_use = "a prepared boundary must be committed or aborted; dropping it aborts"]
536pub struct CoreBoundaryStageOutput {
537 session_snapshot: Option<Vec<u8>>,
540 authority: Option<Box<dyn CoreBoundaryStageCommitAuthority>>,
541}
542
543impl CoreBoundaryStageOutput {
544 pub(crate) fn prepared(
545 session_snapshot: Option<Vec<u8>>,
546 authority: Box<dyn CoreBoundaryStageCommitAuthority>,
547 ) -> Self {
548 Self {
549 session_snapshot,
550 authority: Some(authority),
551 }
552 }
553
554 #[must_use]
555 pub fn session_snapshot(&self) -> Option<&[u8]> {
556 self.session_snapshot.as_deref()
557 }
558
559 pub fn commit(mut self) -> Result<(), CoreBoundaryStageError> {
565 let Some(mut authority) = self.authority.take() else {
566 return Err(CoreBoundaryStageError::stale(
567 "prepared boundary authority was already resolved",
568 ));
569 };
570 authority.commit()
571 }
572
573 pub fn abort(mut self) -> Result<(), CoreBoundaryStageError> {
574 let Some(mut authority) = self.authority.take() else {
575 return Err(CoreBoundaryStageError::stale(
576 "prepared boundary authority was already resolved",
577 ));
578 };
579 authority.abort()
580 }
581}
582
583impl std::fmt::Debug for CoreBoundaryStageOutput {
584 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585 formatter
586 .debug_struct("CoreBoundaryStageOutput")
587 .field(
588 "session_snapshot_len",
589 &self.session_snapshot.as_ref().map(Vec::len),
590 )
591 .field("authority", &self.authority.as_ref().map(|_| "prepared"))
592 .finish()
593 }
594}
595
596impl CoreApplyOutput {
597 pub fn with_run_result(
598 receipt: RunBoundaryReceiptDraft,
599 session_snapshot: Option<Vec<u8>>,
600 run_result: RunResult,
601 ) -> Self {
602 Self {
603 receipt,
604 session_snapshot,
605 terminal: Some(CoreApplyTerminal::RunResult(Box::new(run_result))),
606 }
607 }
608
609 pub fn with_callback_pending(
610 receipt: RunBoundaryReceiptDraft,
611 session_snapshot: Option<Vec<u8>>,
612 tool_use_id: impl Into<String>,
613 tool_name: impl Into<String>,
614 args: Value,
615 ) -> Self {
616 Self {
617 receipt,
618 session_snapshot,
619 terminal: Some(CoreApplyTerminal::CallbackPending {
620 tool_use_id: tool_use_id.into(),
621 tool_name: tool_name.into(),
622 args,
623 }),
624 }
625 }
626
627 pub fn with_callback_batch_pending(
628 receipt: RunBoundaryReceiptDraft,
629 session_snapshot: Option<Vec<u8>>,
630 pending_tool_calls: Vec<crate::error::PendingCallbackToolCall>,
631 ) -> Self {
632 Self {
633 receipt,
634 session_snapshot,
635 terminal: Some(CoreApplyTerminal::CallbackBatchPending { pending_tool_calls }),
636 }
637 }
638
639 pub fn without_terminal(
640 receipt: RunBoundaryReceiptDraft,
641 session_snapshot: Option<Vec<u8>>,
642 ) -> Self {
643 Self {
644 receipt,
645 session_snapshot,
646 terminal: None,
647 }
648 }
649}
650
651#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
664#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
665pub trait CoreExecutorBoundaryHandle: Send + Sync {
666 async fn cancel_after_boundary(
668 &self,
669 expected_run_id: &RunId,
670 reason: String,
671 ) -> Result<(), CoreExecutorError>;
672
673 async fn prepare_system_context_at_boundary(
682 &self,
683 _expected_run_id: &RunId,
684 _appends: Vec<PendingSystemContextAppend>,
685 ) -> Result<CoreBoundaryStageOutput, CoreBoundaryStageError> {
686 Err(CoreBoundaryStageError::unavailable(
687 "live boundary system-context preparation is unsupported by this executor",
688 ))
689 }
690}
691
692#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
694#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
695pub trait CoreExecutorInterruptHandle: Send + Sync {
696 async fn hard_cancel_current_run(&self, reason: String) -> Result<(), CoreExecutorError>;
697}
698
699#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
708#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
709pub trait CoreExecutorPublicationHandle: Send + Sync {
710 async fn publish_interaction_terminals(
711 &self,
712 events: &[AgentEvent],
713 ) -> Result<Vec<CoreInteractionTerminalPublicationReceipt>, CoreExecutorError>;
714}
715
716#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
726#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
727pub trait CoreExecutorPostStopCleanupHandle: Send + Sync {
728 async fn cleanup_after_runtime_stop_terminalized(&self) -> Result<(), CoreExecutorError>;
729
730 async fn cleanup_after_runtime_stop_terminalized_under_turn_finalization_boundary(
734 &self,
735 ) -> Result<(), CoreExecutorError> {
736 self.cleanup_after_runtime_stop_terminalized().await
737 }
738}
739
740pub trait CoreExecutorTurnFinalizationGuard: Send {}
745
746impl<T: Send> CoreExecutorTurnFinalizationGuard for T {}
747
748#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
750#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
751pub trait CoreExecutorTurnFinalizationBoundaryHandle: Send + Sync {
752 async fn acquire(
753 &self,
754 ) -> Result<Box<dyn CoreExecutorTurnFinalizationGuard>, CoreExecutorError>;
755}
756
757#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
766#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
767pub trait CoreExecutor: Send + Sync {
768 fn boundary_handle(&self) -> Option<Arc<dyn CoreExecutorBoundaryHandle>> {
774 None
775 }
776
777 fn interrupt_handle(&self) -> Option<Arc<dyn CoreExecutorInterruptHandle>> {
783 None
784 }
785
786 fn publication_handle(&self) -> Option<Arc<dyn CoreExecutorPublicationHandle>> {
788 None
789 }
790
791 fn machine_managed_post_stop_unregister(&self) -> bool {
801 false
802 }
803
804 fn post_stop_cleanup_handle(&self) -> Option<Arc<dyn CoreExecutorPostStopCleanupHandle>> {
807 None
808 }
809
810 fn turn_finalization_boundary_handle(
812 &self,
813 ) -> Option<Arc<dyn CoreExecutorTurnFinalizationBoundaryHandle>> {
814 None
815 }
816
817 async fn apply(
822 &mut self,
823 run_id: RunId,
824 primitive: RunPrimitive,
825 ) -> Result<CoreApplyOutput, CoreExecutorError>;
826
827 async fn checkpoint_committed_session_snapshot(
837 &mut self,
838 _session_snapshot: &[u8],
839 ) -> Result<(), CoreExecutorError> {
840 Ok(())
841 }
842
843 async fn reconcile_committed_compaction_projections(
848 &mut self,
849 intents: &[crate::memory::CompactionProjectionIntent],
850 ) -> Result<(), CoreExecutorError> {
851 if intents.is_empty() {
852 Ok(())
853 } else {
854 Err(CoreExecutorError::Internal(
855 "executor cannot reconcile committed compaction projections".to_string(),
856 ))
857 }
858 }
859
860 async fn abort_uncommitted_compaction_projections(&mut self) -> Result<(), CoreExecutorError> {
866 Ok(())
867 }
868
869 async fn abort_rejected_run_projections(&mut self) -> Result<(), CoreExecutorError> {
880 self.abort_uncommitted_compaction_projections().await
881 }
882
883 async fn publish_interaction_terminals(
888 &mut self,
889 events: &[AgentEvent],
890 ) -> Result<Vec<CoreInteractionTerminalPublicationReceipt>, CoreExecutorError> {
891 if events.is_empty() {
892 return Ok(Vec::new());
893 }
894 Err(CoreExecutorError::Internal(
895 "exact interaction terminal publication is unsupported by this executor".to_string(),
896 ))
897 }
898
899 async fn cancel_after_boundary(&mut self, reason: String) -> Result<(), CoreExecutorError>;
901
902 async fn stop_runtime_executor(&mut self, reason: String) -> Result<(), CoreExecutorError>;
904
905 async fn cleanup_after_runtime_stop_terminalized(&mut self) -> Result<(), CoreExecutorError> {
914 Ok(())
915 }
916}
917
918#[cfg(test)]
919#[allow(clippy::panic)]
920mod tests {
921 use super::*;
922
923 fn _assert_object_safe(_: &dyn CoreExecutor) {}
925
926 #[test]
927 fn core_executor_error_display() {
928 let err = CoreExecutorError::ApplyFailed {
929 cause: CoreApplyFailureCause::runtime_turn("bad input"),
930 };
931 assert_eq!(err.to_string(), "Apply failed: bad input");
932
933 let err = CoreExecutorError::ControlFailed {
934 cause: CoreControlFailureCause::runtime_control("not running"),
935 };
936 assert_eq!(err.to_string(), "Control failed: not running");
937
938 let err = CoreExecutorError::Stopped;
939 assert_eq!(err.to_string(), "Executor is stopped");
940
941 let err = CoreExecutorError::Cancelled;
942 assert_eq!(err.to_string(), "Run was cancelled");
943
944 let err = CoreExecutorError::Internal("oops".into());
945 assert_eq!(err.to_string(), "Internal error: oops");
946 }
947
948 #[test]
949 fn apply_failed_carries_typed_cause() {
950 let err = CoreExecutorError::ApplyFailed {
951 cause: CoreApplyFailureCause::runtime_context_apply("context write failed"),
952 };
953
954 match err {
955 CoreExecutorError::ApplyFailed { cause } => {
956 assert_eq!(cause.kind, CoreApplyFailureCauseKind::RuntimeContextApply);
957 assert_eq!(cause.message(), "context write failed");
958 }
959 other => panic!("expected typed apply failure, got {other:?}"),
960 }
961 }
962
963 #[test]
964 fn cancelled_session_error_remains_typed_at_runtime_executor_boundary() {
965 let err = CoreExecutorError::apply_failed_from_session_error(SessionError::Agent(
966 AgentError::Cancelled,
967 ));
968
969 assert!(err.is_cancelled());
970 assert_eq!(
971 err.apply_failure_cause().kind,
972 CoreApplyFailureCauseKind::RuntimeTurn
973 );
974 }
975
976 #[test]
977 fn corrupted_live_session_signal_stops_instead_of_retrying_apply() {
978 let err = CoreExecutorError::apply_failed_from_session_error(
979 SessionError::runtime_executor_stopped("terminal witness mismatch"),
980 );
981
982 assert!(matches!(err, CoreExecutorError::Stopped));
983 }
984
985 #[test]
986 fn hook_denial_agent_error_maps_to_typed_apply_failure_cause() {
987 let error = AgentError::HookDenied {
988 hook_id: crate::hooks::HookId::new("guard"),
989 point: crate::hooks::HookPoint::PreToolExecution,
990 reason_code: crate::hooks::HookReasonCode::PolicyViolation,
991 message: "blocked by hook".to_string(),
992 payload: None,
993 };
994
995 let cause = CoreApplyFailureCause::from_agent_error(&error);
996 assert_eq!(cause.kind, CoreApplyFailureCauseKind::HookDenied);
997 assert!(cause.message().contains("blocked by hook"));
998 }
999
1000 #[test]
1001 fn hook_runtime_agent_error_maps_to_typed_apply_failure_cause() {
1002 let error = AgentError::HookExecutionFailed {
1003 hook_id: crate::hooks::HookId::new("guard"),
1004 reason: "missing runtime".to_string(),
1005 };
1006
1007 let cause = CoreApplyFailureCause::from_agent_error(&error);
1008 assert_eq!(cause.kind, CoreApplyFailureCauseKind::HookRuntimeFailure);
1009 assert!(cause.message().contains("missing runtime"));
1010 }
1011}