1use super::RunId;
8use super::run_primitive::RunPrimitive;
9use super::run_receipt::RunBoundaryReceiptDraft;
10use crate::error::AgentError;
11use crate::lifecycle::run_primitive::TurnRequestContext;
12use crate::service::SessionError;
13use crate::turn_execution_authority::{TurnTerminalCauseKind, TurnTerminalOutcome};
14use crate::types::{RunResult, SessionId};
15use crate::{TurnErrorMetadata, event::AgentEvent, interaction::InteractionId};
16use serde_json::Value;
17use sha2::{Digest, Sha256};
18use std::sync::Arc;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum CommittedSessionBoundaryAuthority {
29 WholeBlob {
30 session_id: SessionId,
31 committed_store_revision: u64,
32 committed_blob_sha256: String,
33 },
34 HeadCanonical {
35 session_id: SessionId,
36 committed_head_token: String,
37 },
38 Provisional {
39 session_id: SessionId,
40 committed_store_revision: u64,
41 committed_authority_token: String,
42 },
43}
44
45impl CommittedSessionBoundaryAuthority {
46 #[must_use]
47 pub fn session_id(&self) -> &SessionId {
48 match self {
49 Self::WholeBlob { session_id, .. }
50 | Self::HeadCanonical { session_id, .. }
51 | Self::Provisional { session_id, .. } => session_id,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum CoreApplyFailureCauseKind {
60 PrimitiveRejected,
61 RuntimeContextApply,
62 RuntimeTurn,
63 HookDenied,
64 HookRuntimeFailure,
65 ExecutorStopped,
66 ExecutorControlFailed,
67 ExecutorInternal,
68 Unknown,
69}
70
71impl CoreApplyFailureCauseKind {
72 pub fn as_str(self) -> &'static str {
73 match self {
74 Self::PrimitiveRejected => "PrimitiveRejected",
75 Self::RuntimeContextApply => "RuntimeContextApply",
76 Self::RuntimeTurn => "RuntimeTurn",
77 Self::HookDenied => "HookDenied",
78 Self::HookRuntimeFailure => "HookRuntimeFailure",
79 Self::ExecutorStopped => "ExecutorStopped",
80 Self::ExecutorControlFailed => "ExecutorControlFailed",
81 Self::ExecutorInternal => "ExecutorInternal",
82 Self::Unknown => "Unknown",
83 }
84 }
85
86 pub fn from_wire_str(value: &str) -> Option<Self> {
87 match value {
88 "PrimitiveRejected" => Some(Self::PrimitiveRejected),
89 "RuntimeContextApply" => Some(Self::RuntimeContextApply),
90 "RuntimeTurn" => Some(Self::RuntimeTurn),
91 "HookDenied" => Some(Self::HookDenied),
92 "HookRuntimeFailure" => Some(Self::HookRuntimeFailure),
93 "ExecutorStopped" => Some(Self::ExecutorStopped),
94 "ExecutorControlFailed" => Some(Self::ExecutorControlFailed),
95 "ExecutorInternal" => Some(Self::ExecutorInternal),
96 "Unknown" => Some(Self::Unknown),
97 _ => None,
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct CoreApplyFailureCause {
105 pub kind: CoreApplyFailureCauseKind,
106 pub message: String,
107}
108
109impl CoreApplyFailureCause {
110 pub fn new(kind: CoreApplyFailureCauseKind, message: impl Into<String>) -> Self {
111 Self {
112 kind,
113 message: message.into(),
114 }
115 }
116
117 pub fn primitive_rejected(message: impl Into<String>) -> Self {
118 Self::new(CoreApplyFailureCauseKind::PrimitiveRejected, message)
119 }
120
121 pub fn runtime_context_apply(message: impl Into<String>) -> Self {
122 Self::new(CoreApplyFailureCauseKind::RuntimeContextApply, message)
123 }
124
125 pub fn runtime_turn(message: impl Into<String>) -> Self {
126 Self::new(CoreApplyFailureCauseKind::RuntimeTurn, message)
127 }
128
129 pub fn hook_denied(message: impl Into<String>) -> Self {
130 Self::new(CoreApplyFailureCauseKind::HookDenied, message)
131 }
132
133 pub fn hook_runtime_failure(message: impl Into<String>) -> Self {
134 Self::new(CoreApplyFailureCauseKind::HookRuntimeFailure, message)
135 }
136
137 pub fn executor_stopped() -> Self {
138 Self::new(
139 CoreApplyFailureCauseKind::ExecutorStopped,
140 "executor is stopped",
141 )
142 }
143
144 pub fn executor_control_failed(message: impl Into<String>) -> Self {
145 Self::new(CoreApplyFailureCauseKind::ExecutorControlFailed, message)
146 }
147
148 pub fn executor_internal(message: impl Into<String>) -> Self {
149 Self::new(CoreApplyFailureCauseKind::ExecutorInternal, message)
150 }
151
152 pub fn unknown(message: impl Into<String>) -> Self {
153 Self::new(CoreApplyFailureCauseKind::Unknown, message)
154 }
155
156 pub fn from_agent_error(error: &AgentError) -> Self {
157 match error {
158 AgentError::HookDenied { .. } => Self::hook_denied(error.to_string()),
159 AgentError::HookTimeout { .. }
160 | AgentError::HookExecutionFailed { .. }
161 | AgentError::HookConfigInvalid { .. } => Self::hook_runtime_failure(error.to_string()),
162 _ => Self::runtime_turn(error.to_string()),
163 }
164 }
165
166 pub fn from_session_error(error: &SessionError) -> Self {
167 match error {
168 SessionError::Agent(agent_error) => Self::from_agent_error(agent_error),
169 _ => Self::runtime_turn(error.to_string()),
170 }
171 }
172
173 pub fn message(&self) -> &str {
174 &self.message
175 }
176}
177
178impl std::fmt::Display for CoreApplyFailureCause {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 f.write_str(&self.message)
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186#[non_exhaustive]
187pub enum CoreControlFailureCauseKind {
188 RuntimeControl,
189 ExecutorInternal,
190 Unknown,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct CoreControlFailureCause {
196 pub kind: CoreControlFailureCauseKind,
197 pub message: String,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207#[non_exhaustive]
208pub enum CoreExecutorTeardownReason {
209 ArchivedSession,
210 SessionUnavailable,
211 DurableProjectionAuthorityUnknown,
212}
213
214impl CoreExecutorTeardownReason {
215 pub fn as_str(self) -> &'static str {
216 match self {
217 Self::ArchivedSession => "ArchivedSession",
218 Self::SessionUnavailable => "SessionUnavailable",
219 Self::DurableProjectionAuthorityUnknown => "DurableProjectionAuthorityUnknown",
220 }
221 }
222
223 pub fn from_wire_str(value: &str) -> Option<Self> {
224 match value {
225 "ArchivedSession" => Some(Self::ArchivedSession),
226 "SessionUnavailable" => Some(Self::SessionUnavailable),
227 "DurableProjectionAuthorityUnknown" => Some(Self::DurableProjectionAuthorityUnknown),
228 _ => None,
229 }
230 }
231}
232
233impl CoreControlFailureCause {
234 pub fn new(kind: CoreControlFailureCauseKind, message: impl Into<String>) -> Self {
235 Self {
236 kind,
237 message: message.into(),
238 }
239 }
240
241 pub fn runtime_control(message: impl Into<String>) -> Self {
242 Self::new(CoreControlFailureCauseKind::RuntimeControl, message)
243 }
244
245 pub fn executor_internal(message: impl Into<String>) -> Self {
246 Self::new(CoreControlFailureCauseKind::ExecutorInternal, message)
247 }
248
249 pub fn unknown(message: impl Into<String>) -> Self {
250 Self::new(CoreControlFailureCauseKind::Unknown, message)
251 }
252}
253
254impl std::fmt::Display for CoreControlFailureCause {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 f.write_str(&self.message)
257 }
258}
259
260#[derive(Debug, Clone, thiserror::Error)]
262#[non_exhaustive]
263pub enum CoreExecutorError {
264 #[error("Apply failed: {cause}")]
266 ApplyFailed { cause: CoreApplyFailureCause },
267
268 #[error("Terminal failure: {outcome:?} ({cause_kind:?}): {message}")]
272 TerminalFailure {
273 outcome: TurnTerminalOutcome,
274 cause_kind: TurnTerminalCauseKind,
275 message: String,
276 },
277
278 #[error("Executor requires teardown ({reason:?}): {message}")]
283 TeardownRequired {
284 reason: CoreExecutorTeardownReason,
285 message: String,
286 },
287
288 #[error("Control failed: {cause}")]
290 ControlFailed { cause: CoreControlFailureCause },
291
292 #[error("Executor is stopped")]
294 Stopped,
295
296 #[error("Run was cancelled")]
298 Cancelled,
299
300 #[error("Internal error: {0}")]
302 Internal(String),
303}
304
305impl CoreExecutorError {
306 pub fn apply_failed(cause: CoreApplyFailureCause) -> Self {
307 Self::ApplyFailed { cause }
308 }
309
310 pub fn apply_failed_primitive_rejected(message: impl Into<String>) -> Self {
311 Self::apply_failed(CoreApplyFailureCause::primitive_rejected(message))
312 }
313
314 pub fn apply_failed_runtime_context(message: impl Into<String>) -> Self {
315 Self::apply_failed(CoreApplyFailureCause::runtime_context_apply(message))
316 }
317
318 pub fn apply_failed_runtime_turn(message: impl Into<String>) -> Self {
319 Self::apply_failed(CoreApplyFailureCause::runtime_turn(message))
320 }
321
322 pub fn terminal_failure(
323 outcome: TurnTerminalOutcome,
324 cause_kind: TurnTerminalCauseKind,
325 message: impl Into<String>,
326 ) -> Self {
327 Self::TerminalFailure {
328 outcome,
329 cause_kind,
330 message: message.into(),
331 }
332 }
333
334 pub fn teardown_required(
335 reason: CoreExecutorTeardownReason,
336 message: impl Into<String>,
337 ) -> Self {
338 Self::TeardownRequired {
339 reason,
340 message: message.into(),
341 }
342 }
343
344 pub fn archived_session_requires_teardown(message: impl Into<String>) -> Self {
345 Self::teardown_required(CoreExecutorTeardownReason::ArchivedSession, message)
346 }
347
348 pub fn session_unavailable_requires_teardown(message: impl Into<String>) -> Self {
349 Self::teardown_required(CoreExecutorTeardownReason::SessionUnavailable, message)
350 }
351
352 pub fn durable_projection_authority_unknown_requires_teardown(
353 message: impl Into<String>,
354 ) -> Self {
355 Self::teardown_required(
356 CoreExecutorTeardownReason::DurableProjectionAuthorityUnknown,
357 message,
358 )
359 }
360
361 pub fn apply_failed_from_session_error(error: SessionError) -> Self {
362 if error.requests_runtime_executor_stop() {
363 return Self::Stopped;
364 }
365 match error {
366 SessionError::Agent(AgentError::Cancelled) => Self::Cancelled,
367 SessionError::Agent(AgentError::StickyModelFallbackAuthorityUnknown { message }) => {
368 Self::session_unavailable_requires_teardown(message)
369 }
370 SessionError::Agent(AgentError::SessionDurableProjectionAuthorityUnknown {
371 message,
372 }) => Self::durable_projection_authority_unknown_requires_teardown(message),
373 SessionError::Agent(AgentError::TerminalFailure {
374 outcome,
375 cause_kind,
376 message,
377 }) if cause_kind.is_specific_failure_cause() => {
378 Self::terminal_failure(outcome, cause_kind, message)
379 }
380 SessionError::Agent(AgentError::TerminalFailure { cause_kind, .. }) => Self::Internal(
381 format!("runtime turn returned unknown machine terminal cause: {cause_kind:?}"),
382 ),
383 error => Self::apply_failed(CoreApplyFailureCause::from_session_error(&error)),
384 }
385 }
386
387 pub fn apply_failed_unknown(message: impl Into<String>) -> Self {
388 Self::apply_failed(CoreApplyFailureCause::unknown(message))
389 }
390
391 pub fn cancelled() -> Self {
392 Self::Cancelled
393 }
394
395 pub fn is_cancelled(&self) -> bool {
396 matches!(self, Self::Cancelled)
397 }
398
399 pub fn requires_runtime_teardown(&self) -> bool {
400 matches!(self, Self::TeardownRequired { .. })
401 }
402
403 pub fn control_failed(cause: CoreControlFailureCause) -> Self {
404 Self::ControlFailed { cause }
405 }
406
407 pub fn control_failed_runtime(message: impl Into<String>) -> Self {
408 Self::control_failed(CoreControlFailureCause::runtime_control(message))
409 }
410
411 pub fn apply_failure_cause(&self) -> CoreApplyFailureCause {
412 match self {
413 Self::ApplyFailed { cause } => cause.clone(),
414 Self::TerminalFailure { cause_kind, .. } => {
415 CoreApplyFailureCause::executor_internal(format!(
416 "typed machine terminal failure escaped runtime-loop handling: {cause_kind:?}"
417 ))
418 }
419 Self::TeardownRequired { reason, message } => CoreApplyFailureCause::new(
420 CoreApplyFailureCauseKind::ExecutorStopped,
421 format!("executor requested {} teardown: {message}", reason.as_str()),
422 ),
423 Self::ControlFailed { cause } => {
424 CoreApplyFailureCause::executor_control_failed(cause.message.clone())
425 }
426 Self::Stopped => CoreApplyFailureCause::executor_stopped(),
427 Self::Cancelled => CoreApplyFailureCause::runtime_turn("cancelled"),
428 Self::Internal(message) => CoreApplyFailureCause::executor_internal(message.clone()),
429 }
430 }
431}
432
433#[derive(Debug, Clone)]
435pub enum CoreApplyTerminal {
436 RunResult(Box<RunResult>),
438 NoPendingBoundary,
440 MachineTerminalFailure { error: TurnErrorMetadata },
445 CallbackPending {
448 tool_use_id: String,
449 tool_name: String,
450 args: Value,
451 },
452 CallbackBatchPending {
455 pending_tool_calls: Vec<crate::error::PendingCallbackToolCall>,
456 },
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
467#[error("failed to encode prepared session boundary: {message}")]
468pub struct SessionBoundaryEncodeError {
469 message: std::sync::Arc<str>,
470}
471
472impl SessionBoundaryEncodeError {
473 fn from_serde(error: serde_json::Error) -> Self {
474 Self {
475 message: std::sync::Arc::from(error.to_string()),
476 }
477 }
478
479 #[must_use]
481 pub fn message(&self) -> &str {
482 &self.message
483 }
484}
485
486#[derive(Debug, Clone)]
492pub enum PreparedHeadCanonicalPhysicalMutation {
493 Ordinary(crate::session_store::PreparedHeadCanonicalMutation),
494 Rewrite(crate::session_store::PreparedHeadCanonicalRewriteMutation),
495}
496
497impl PreparedHeadCanonicalPhysicalMutation {
498 #[must_use]
499 pub fn session_id(&self) -> &crate::types::SessionId {
500 match self {
501 Self::Ordinary(mutation) => mutation.session_id(),
502 Self::Rewrite(mutation) => mutation.session_id(),
503 }
504 }
505
506 #[must_use]
507 pub fn predecessor_head(&self) -> Option<&crate::session_store::SessionHead> {
508 match self {
509 Self::Ordinary(mutation) => mutation.predecessor_head(),
510 Self::Rewrite(mutation) => Some(mutation.predecessor_head()),
511 }
512 }
513
514 #[must_use]
515 pub fn predecessor_head_token(&self) -> Option<&str> {
516 match self {
517 Self::Ordinary(mutation) => mutation.predecessor_head_token(),
518 Self::Rewrite(mutation) => Some(mutation.predecessor_head_token()),
519 }
520 }
521
522 #[must_use]
523 pub fn successor_head(&self) -> &crate::session_store::SessionHead {
524 match self {
525 Self::Ordinary(mutation) => mutation.successor_head(),
526 Self::Rewrite(mutation) => mutation.successor_head(),
527 }
528 }
529
530 #[must_use]
531 pub fn successor_head_token(&self) -> &str {
532 match self {
533 Self::Ordinary(mutation) => mutation.successor_head_token(),
534 Self::Rewrite(mutation) => mutation.successor_head_token(),
535 }
536 }
537
538 #[must_use]
539 pub fn ordinary(&self) -> Option<&crate::session_store::PreparedHeadCanonicalMutation> {
540 match self {
541 Self::Ordinary(mutation) => Some(mutation),
542 Self::Rewrite(_) => None,
543 }
544 }
545
546 #[must_use]
547 pub fn rewrite(&self) -> Option<&crate::session_store::PreparedHeadCanonicalRewriteMutation> {
548 match self {
549 Self::Ordinary(_) => None,
550 Self::Rewrite(mutation) => Some(mutation),
551 }
552 }
553
554 pub(crate) fn validate_live_successor(
555 &self,
556 session: &crate::Session,
557 ) -> Result<(), crate::SessionStoreError> {
558 match self {
559 Self::Ordinary(mutation) => mutation.validate_live_successor(session),
560 Self::Rewrite(mutation) => mutation.validate_live_successor(session),
561 }
562 }
563
564 pub fn acknowledge_session(
565 &self,
566 session: &mut crate::Session,
567 committed_head_token: &str,
568 ) -> Result<(), crate::SessionStoreError> {
569 match self {
570 Self::Ordinary(mutation) => mutation.acknowledge_session(session, committed_head_token),
571 Self::Rewrite(mutation) => mutation.acknowledge_session(session, committed_head_token),
572 }
573 }
574}
575
576impl From<crate::session_store::PreparedHeadCanonicalMutation>
577 for PreparedHeadCanonicalPhysicalMutation
578{
579 fn from(mutation: crate::session_store::PreparedHeadCanonicalMutation) -> Self {
580 Self::Ordinary(mutation)
581 }
582}
583
584impl From<crate::session_store::PreparedHeadCanonicalRewriteMutation>
585 for PreparedHeadCanonicalPhysicalMutation
586{
587 fn from(mutation: crate::session_store::PreparedHeadCanonicalRewriteMutation) -> Self {
588 Self::Rewrite(mutation)
589 }
590}
591
592#[derive(Debug, Clone)]
598pub struct PreparedHeadCanonicalBoundary {
599 mutation: PreparedHeadCanonicalPhysicalMutation,
600 compaction_projection_intents: std::sync::Arc<[crate::CompactionProjectionIntent]>,
601 catalog_labels: std::collections::BTreeMap<String, String>,
602 catalog_lifecycle_terminal: Option<crate::SessionLifecycleTerminal>,
603}
604
605impl PreparedHeadCanonicalBoundary {
606 #[must_use]
607 pub fn mutation(&self) -> &PreparedHeadCanonicalPhysicalMutation {
608 &self.mutation
609 }
610
611 #[must_use]
613 pub fn compaction_projection_intents(&self) -> &[crate::CompactionProjectionIntent] {
614 self.compaction_projection_intents.as_ref()
615 }
616
617 #[must_use]
619 pub fn catalog_labels(&self) -> &std::collections::BTreeMap<String, String> {
620 &self.catalog_labels
621 }
622
623 #[must_use]
625 pub const fn catalog_lifecycle_terminal(&self) -> Option<crate::SessionLifecycleTerminal> {
626 self.catalog_lifecycle_terminal
627 }
628}
629
630#[derive(Debug, Clone)]
640enum BoundSessionCommitKind {
641 WholeBlobTyped {
642 session: std::sync::Arc<crate::Session>,
643 whole_blob: std::sync::Arc<
644 std::sync::OnceLock<
645 Result<
646 std::sync::Arc<crate::SerializedSessionArtifact>,
647 SessionBoundaryEncodeError,
648 >,
649 >,
650 >,
651 },
652 WholeBlobUntyped {
653 whole_blob: std::sync::Arc<
654 std::sync::OnceLock<
655 Result<
656 std::sync::Arc<crate::SerializedSessionArtifact>,
657 SessionBoundaryEncodeError,
658 >,
659 >,
660 >,
661 },
662 HeadCanonical {
663 boundary: std::sync::Arc<PreparedHeadCanonicalBoundary>,
664 },
665 ProvisionalPromotion {
669 receipt: crate::RunCheckpointReceipt,
670 },
671}
672
673#[derive(Debug, Clone)]
674pub struct BoundSessionCommit {
675 kind: BoundSessionCommitKind,
676 #[cfg(test)]
677 whole_blob_encode_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
678}
679
680impl BoundSessionCommit {
681 pub fn sealed(session: std::sync::Arc<crate::Session>) -> Result<Self, serde_json::Error> {
692 Ok(Self {
693 kind: BoundSessionCommitKind::WholeBlobTyped {
694 session,
695 whole_blob: std::sync::Arc::new(std::sync::OnceLock::new()),
696 },
697 #[cfg(test)]
698 whole_blob_encode_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
699 })
700 }
701
702 #[must_use]
705 pub fn untyped(snapshot: Vec<u8>) -> Self {
706 Self {
707 kind: BoundSessionCommitKind::WholeBlobUntyped {
708 whole_blob: std::sync::Arc::new(std::sync::OnceLock::from(Ok(
709 std::sync::Arc::new(crate::SerializedSessionArtifact::from_raw_bytes(snapshot)),
710 ))),
711 },
712 #[cfg(test)]
713 whole_blob_encode_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
714 }
715 }
716
717 #[must_use]
725 pub fn from_serialized_artifact(
726 artifact: std::sync::Arc<crate::SerializedSessionArtifact>,
727 ) -> Self {
728 Self {
729 kind: BoundSessionCommitKind::WholeBlobUntyped {
730 whole_blob: std::sync::Arc::new(std::sync::OnceLock::from(Ok(artifact))),
731 },
732 #[cfg(test)]
733 whole_blob_encode_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
734 }
735 }
736
737 #[must_use]
740 pub fn provisional_promotion(receipt: crate::RunCheckpointReceipt) -> Self {
741 Self {
742 kind: BoundSessionCommitKind::ProvisionalPromotion { receipt },
743 #[cfg(test)]
744 whole_blob_encode_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
745 }
746 }
747
748 pub fn with_head_canonical_mutation(
755 self,
756 mutation: crate::session_store::PreparedHeadCanonicalMutation,
757 ) -> Result<Self, crate::SessionStoreError> {
758 let mutation_session_id = mutation.session_id().clone();
759 let invalid = |reason: String| crate::SessionStoreError::InvalidTranscriptRewrite {
760 id: mutation_session_id.clone(),
761 reason,
762 };
763 let session = match &self.kind {
764 BoundSessionCommitKind::WholeBlobTyped { session, .. } => {
765 std::sync::Arc::clone(session)
766 }
767 BoundSessionCommitKind::WholeBlobUntyped { .. } => {
768 return Err(invalid(
769 "head-canonical persistence requires a typed session boundary".to_string(),
770 ));
771 }
772 BoundSessionCommitKind::HeadCanonical { .. } => {
773 return Err(invalid(
774 "head-canonical mutation was already attached to this boundary".to_string(),
775 ));
776 }
777 BoundSessionCommitKind::ProvisionalPromotion { .. } => {
778 return Err(invalid(
779 "provisional promotion cannot be converted into a head-canonical mutation"
780 .to_string(),
781 ));
782 }
783 };
784 Self::head_canonical_from_session(session.as_ref(), mutation)
785 }
786
787 pub fn head_canonical_from_session(
795 session: &crate::Session,
796 mutation: crate::session_store::PreparedHeadCanonicalMutation,
797 ) -> Result<Self, crate::SessionStoreError> {
798 Self::head_canonical_physical_from_session(session, mutation.into())
799 }
800
801 pub fn head_canonical_physical_from_session(
804 session: &crate::Session,
805 mutation: PreparedHeadCanonicalPhysicalMutation,
806 ) -> Result<Self, crate::SessionStoreError> {
807 let boundary = Self::prepare_head_canonical_boundary(session, mutation)?;
808 Ok(Self {
809 kind: BoundSessionCommitKind::HeadCanonical {
810 boundary: std::sync::Arc::new(boundary),
811 },
812 #[cfg(test)]
813 whole_blob_encode_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
814 })
815 }
816
817 pub fn head_canonical_rewrite_from_session(
820 session: &crate::Session,
821 mutation: crate::session_store::PreparedHeadCanonicalRewriteMutation,
822 ) -> Result<Self, crate::SessionStoreError> {
823 Self::head_canonical_physical_from_session(session, mutation.into())
824 }
825
826 fn prepare_head_canonical_boundary(
827 session: &crate::Session,
828 mutation: PreparedHeadCanonicalPhysicalMutation,
829 ) -> Result<PreparedHeadCanonicalBoundary, crate::SessionStoreError> {
830 let mutation_session_id = mutation.session_id().clone();
831 let invalid = |reason: String| crate::SessionStoreError::InvalidTranscriptRewrite {
832 id: mutation_session_id.clone(),
833 reason,
834 };
835 if session.id() != mutation.session_id() {
836 return Err(invalid(format!(
837 "prepared mutation belongs to session {}, not sealed session {}",
838 mutation.session_id(),
839 session.id()
840 )));
841 }
842
843 mutation.validate_live_successor(session)?;
844
845 let compaction_projection_intents = session
846 .validated_compaction_projection_intents()
847 .map_err(|error| {
848 invalid(format!(
849 "head-canonical successor carries invalid compaction projection intents: {error}"
850 ))
851 })?
852 .into();
853 let catalog_labels = session
854 .metadata()
855 .get("session_labels")
856 .map(|value| {
857 serde_json::from_value::<std::collections::BTreeMap<String, String>>(value.clone())
858 .map_err(|error| {
859 invalid(format!(
860 "head-canonical successor carries malformed catalog labels: {error}"
861 ))
862 })
863 })
864 .transpose()?
865 .unwrap_or_default();
866 let catalog_lifecycle_terminal = session.try_lifecycle_terminal().map_err(|error| {
867 invalid(format!(
868 "head-canonical successor carries malformed lifecycle-terminal metadata: {error}"
869 ))
870 })?;
871
872 Ok(PreparedHeadCanonicalBoundary {
873 mutation,
874 compaction_projection_intents,
875 catalog_labels,
876 catalog_lifecycle_terminal,
877 })
878 }
879
880 #[must_use]
883 pub fn head_canonical(&self) -> Option<&PreparedHeadCanonicalBoundary> {
884 match &self.kind {
885 BoundSessionCommitKind::HeadCanonical { boundary } => Some(boundary.as_ref()),
886 BoundSessionCommitKind::WholeBlobTyped { .. }
887 | BoundSessionCommitKind::WholeBlobUntyped { .. }
888 | BoundSessionCommitKind::ProvisionalPromotion { .. } => None,
889 }
890 }
891
892 #[must_use]
895 pub fn provisional_promotion_receipt(&self) -> Option<&crate::RunCheckpointReceipt> {
896 match &self.kind {
897 BoundSessionCommitKind::ProvisionalPromotion { receipt } => Some(receipt),
898 BoundSessionCommitKind::WholeBlobTyped { .. }
899 | BoundSessionCommitKind::WholeBlobUntyped { .. }
900 | BoundSessionCommitKind::HeadCanonical { .. } => None,
901 }
902 }
903
904 pub fn acknowledge_head_canonical_commit(
910 &self,
911 committed_head_cas_token: &str,
912 ) -> Result<(), crate::SessionStoreError> {
913 let boundary = self.head_canonical().ok_or_else(|| {
914 crate::SessionStoreError::Internal(
915 "session boundary has no head-canonical mutation to acknowledge".to_string(),
916 )
917 })?;
918 if boundary.mutation().successor_head_token() != committed_head_cas_token {
919 return Err(crate::SessionStoreError::TranscriptRevisionConflict {
920 id: boundary.mutation().session_id().clone(),
921 expected: boundary.mutation().successor_head_token().to_string(),
922 actual: committed_head_cas_token.to_string(),
923 });
924 }
925 Ok(())
926 }
927
928 pub fn whole_blob_bytes(&self) -> Result<&[u8], SessionBoundaryEncodeError> {
936 let (whole_blob, session) = match &self.kind {
937 BoundSessionCommitKind::WholeBlobTyped {
938 session,
939 whole_blob,
940 } => (whole_blob, Some(session)),
941 BoundSessionCommitKind::WholeBlobUntyped { whole_blob } => (whole_blob, None),
942 BoundSessionCommitKind::HeadCanonical { .. } => {
943 return Err(SessionBoundaryEncodeError {
944 message: std::sync::Arc::from(
945 "head-canonical boundary has no whole-blob representation",
946 ),
947 });
948 }
949 BoundSessionCommitKind::ProvisionalPromotion { .. } => {
950 return Err(SessionBoundaryEncodeError {
951 message: std::sync::Arc::from(
952 "provisional promotion boundary has no whole-blob representation",
953 ),
954 });
955 }
956 };
957 whole_blob
958 .get_or_init(|| {
959 let Some(session) = session else {
960 return Err(SessionBoundaryEncodeError {
961 message: std::sync::Arc::from(
962 "untyped whole-blob carrier lost its compatibility bytes",
963 ),
964 });
965 };
966 let snapshot = session
967 .to_persisted_artifact()
968 .map_err(SessionBoundaryEncodeError::from_serde)?;
969 #[cfg(test)]
970 self.whole_blob_encode_count
971 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
972 Ok(std::sync::Arc::new(snapshot))
973 })
974 .as_ref()
975 .map(|snapshot| snapshot.bytes())
976 .map_err(Clone::clone)
977 }
978
979 pub fn whole_blob_artifact(
986 &self,
987 ) -> Result<&crate::SerializedSessionArtifact, SessionBoundaryEncodeError> {
988 let _ = self.whole_blob_bytes()?;
989 let whole_blob = match &self.kind {
990 BoundSessionCommitKind::WholeBlobTyped { whole_blob, .. }
991 | BoundSessionCommitKind::WholeBlobUntyped { whole_blob } => whole_blob,
992 BoundSessionCommitKind::HeadCanonical { .. } => {
993 return Err(SessionBoundaryEncodeError {
994 message: std::sync::Arc::from(
995 "head-canonical boundary has no whole-blob representation",
996 ),
997 });
998 }
999 BoundSessionCommitKind::ProvisionalPromotion { .. } => {
1000 return Err(SessionBoundaryEncodeError {
1001 message: std::sync::Arc::from(
1002 "provisional promotion boundary has no whole-blob representation",
1003 ),
1004 });
1005 }
1006 };
1007 match whole_blob.get() {
1008 Some(Ok(artifact)) => Ok(artifact.as_ref()),
1009 Some(Err(error)) => Err(error.clone()),
1010 None => Err(SessionBoundaryEncodeError {
1011 message: std::sync::Arc::from(
1012 "whole-blob cell remained empty after successful materialization",
1013 ),
1014 }),
1015 }
1016 }
1017
1018 pub fn into_whole_blob_bytes(
1024 self,
1025 ) -> Result<std::sync::Arc<Vec<u8>>, SessionBoundaryEncodeError> {
1026 let _ = self.whole_blob_bytes()?;
1027 let whole_blob = match &self.kind {
1028 BoundSessionCommitKind::WholeBlobTyped { whole_blob, .. }
1029 | BoundSessionCommitKind::WholeBlobUntyped { whole_blob } => whole_blob,
1030 BoundSessionCommitKind::HeadCanonical { .. } => {
1031 return Err(SessionBoundaryEncodeError {
1032 message: std::sync::Arc::from(
1033 "head-canonical boundary has no whole-blob representation",
1034 ),
1035 });
1036 }
1037 BoundSessionCommitKind::ProvisionalPromotion { .. } => {
1038 return Err(SessionBoundaryEncodeError {
1039 message: std::sync::Arc::from(
1040 "provisional promotion boundary has no whole-blob representation",
1041 ),
1042 });
1043 }
1044 };
1045 match whole_blob.get() {
1046 Some(Ok(snapshot)) => Ok(snapshot.bytes_arc()),
1047 Some(Err(error)) => Err(error.clone()),
1048 None => Err(SessionBoundaryEncodeError {
1049 message: std::sync::Arc::from(
1050 "whole-blob cell remained empty after successful materialization",
1051 ),
1052 }),
1053 }
1054 }
1055
1056 #[cfg(test)]
1057 fn whole_blob_encode_count(&self) -> usize {
1058 self.whole_blob_encode_count
1059 .load(std::sync::atomic::Ordering::Relaxed)
1060 }
1061
1062 #[must_use]
1065 pub fn session(&self) -> Option<&crate::Session> {
1066 match &self.kind {
1067 BoundSessionCommitKind::WholeBlobTyped { session, .. } => Some(session.as_ref()),
1068 BoundSessionCommitKind::WholeBlobUntyped { .. }
1069 | BoundSessionCommitKind::HeadCanonical { .. }
1070 | BoundSessionCommitKind::ProvisionalPromotion { .. } => None,
1071 }
1072 }
1073
1074 #[must_use]
1076 pub fn session_arc(&self) -> Option<&std::sync::Arc<crate::Session>> {
1077 match &self.kind {
1078 BoundSessionCommitKind::WholeBlobTyped { session, .. } => Some(session),
1079 BoundSessionCommitKind::WholeBlobUntyped { .. }
1080 | BoundSessionCommitKind::HeadCanonical { .. }
1081 | BoundSessionCommitKind::ProvisionalPromotion { .. } => None,
1082 }
1083 }
1084
1085 #[must_use]
1088 pub fn session_arc_cloned(&self) -> Option<std::sync::Arc<crate::Session>> {
1089 self.session_arc().cloned()
1090 }
1091
1092 #[must_use]
1094 pub fn into_session_arc(self) -> Option<std::sync::Arc<crate::Session>> {
1095 match self.kind {
1096 BoundSessionCommitKind::WholeBlobTyped { session, .. } => Some(session),
1097 BoundSessionCommitKind::WholeBlobUntyped { .. }
1098 | BoundSessionCommitKind::HeadCanonical { .. }
1099 | BoundSessionCommitKind::ProvisionalPromotion { .. } => None,
1100 }
1101 }
1102}
1103
1104#[derive(Debug, Clone)]
1105pub struct CoreApplyOutput {
1106 pub receipt: RunBoundaryReceiptDraft,
1111 committed: Option<BoundSessionCommit>,
1129 pub terminal: Option<CoreApplyTerminal>,
1136}
1137
1138#[derive(Debug, Clone, PartialEq, Eq)]
1140pub struct CoreInteractionTerminalPublicationReceipt {
1141 interaction_id: InteractionId,
1142 terminal_seq: u64,
1143 payload_digest: String,
1144}
1145
1146impl CoreInteractionTerminalPublicationReceipt {
1147 pub fn try_new(event: &AgentEvent, terminal_seq: u64) -> Result<Self, CoreExecutorError> {
1148 if terminal_seq == 0 {
1149 return Err(CoreExecutorError::Internal(
1150 "interaction terminal durable sequence must be non-zero".to_string(),
1151 ));
1152 }
1153 let interaction_id = match event {
1154 AgentEvent::InteractionComplete { interaction_id, .. }
1155 | AgentEvent::InteractionCallbackPending { interaction_id, .. }
1156 | AgentEvent::InteractionFailed { interaction_id, .. } => *interaction_id,
1157 _ => {
1158 return Err(CoreExecutorError::Internal(
1159 "interaction terminal publication receipt requires an Interaction terminal event"
1160 .to_string(),
1161 ));
1162 }
1163 };
1164 let encoded = serde_json::to_vec(event).map_err(|error| {
1165 CoreExecutorError::Internal(format!(
1166 "failed to encode interaction terminal publication receipt: {error}"
1167 ))
1168 })?;
1169 Ok(Self {
1170 interaction_id,
1171 terminal_seq,
1172 payload_digest: format!("{:x}", Sha256::digest(encoded)),
1173 })
1174 }
1175
1176 pub fn interaction_id(&self) -> InteractionId {
1177 self.interaction_id
1178 }
1179
1180 pub fn terminal_seq(&self) -> u64 {
1181 self.terminal_seq
1182 }
1183
1184 pub fn payload_digest(&self) -> &str {
1185 &self.payload_digest
1186 }
1187}
1188
1189#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1197pub enum CoreBoundaryStageError {
1198 #[error("active turn boundary is unavailable: {reason}")]
1199 Unavailable { reason: String },
1200 #[error("active turn boundary authority is stale: {reason}")]
1201 Stale { reason: String },
1202 #[error("active turn boundary preparation failed: {reason}")]
1203 Fault { reason: String },
1204}
1205
1206impl CoreBoundaryStageError {
1207 pub fn unavailable(reason: impl Into<String>) -> Self {
1208 Self::Unavailable {
1209 reason: reason.into(),
1210 }
1211 }
1212
1213 pub fn stale(reason: impl Into<String>) -> Self {
1214 Self::Stale {
1215 reason: reason.into(),
1216 }
1217 }
1218
1219 pub fn fault(reason: impl Into<String>) -> Self {
1220 Self::Fault {
1221 reason: reason.into(),
1222 }
1223 }
1224
1225 #[must_use]
1226 pub fn is_unavailable(&self) -> bool {
1227 matches!(self, Self::Unavailable { .. })
1228 }
1229}
1230
1231pub(crate) trait CoreBoundaryStageCommitAuthority: Send {
1232 fn commit(&mut self) -> Result<(), CoreBoundaryStageError>;
1233 fn abort(&mut self) -> Result<(), CoreBoundaryStageError>;
1234}
1235
1236#[must_use = "a prepared boundary must be committed or aborted; dropping it aborts"]
1248pub struct CoreBoundaryStageOutput {
1249 session_snapshot: Option<Vec<u8>>,
1252 authority: Option<Box<dyn CoreBoundaryStageCommitAuthority>>,
1253}
1254
1255impl CoreBoundaryStageOutput {
1256 pub(crate) fn prepared(
1257 session_snapshot: Option<Vec<u8>>,
1258 authority: Box<dyn CoreBoundaryStageCommitAuthority>,
1259 ) -> Self {
1260 Self {
1261 session_snapshot,
1262 authority: Some(authority),
1263 }
1264 }
1265
1266 #[must_use]
1267 pub fn session_snapshot(&self) -> Option<&[u8]> {
1268 self.session_snapshot.as_deref()
1269 }
1270
1271 pub fn commit(mut self) -> Result<(), CoreBoundaryStageError> {
1277 let Some(mut authority) = self.authority.take() else {
1278 return Err(CoreBoundaryStageError::stale(
1279 "prepared boundary authority was already resolved",
1280 ));
1281 };
1282 authority.commit()
1283 }
1284
1285 pub fn abort(mut self) -> Result<(), CoreBoundaryStageError> {
1286 let Some(mut authority) = self.authority.take() else {
1287 return Err(CoreBoundaryStageError::stale(
1288 "prepared boundary authority was already resolved",
1289 ));
1290 };
1291 authority.abort()
1292 }
1293}
1294
1295impl std::fmt::Debug for CoreBoundaryStageOutput {
1296 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1297 formatter
1298 .debug_struct("CoreBoundaryStageOutput")
1299 .field(
1300 "session_snapshot_len",
1301 &self.session_snapshot.as_ref().map(Vec::len),
1302 )
1303 .field("authority", &self.authority.as_ref().map(|_| "prepared"))
1304 .finish()
1305 }
1306}
1307
1308impl CoreApplyOutput {
1309 pub fn new(receipt: RunBoundaryReceiptDraft, terminal: Option<CoreApplyTerminal>) -> Self {
1311 Self {
1312 receipt,
1313 committed: None,
1314 terminal,
1315 }
1316 }
1317
1318 pub fn with_untyped_snapshot(
1326 receipt: RunBoundaryReceiptDraft,
1327 untyped_snapshot: Option<Vec<u8>>,
1328 terminal: Option<CoreApplyTerminal>,
1329 ) -> Self {
1330 Self {
1331 receipt,
1332 committed: untyped_snapshot.map(BoundSessionCommit::untyped),
1333 terminal,
1334 }
1335 }
1336
1337 pub fn with_run_result(
1338 receipt: RunBoundaryReceiptDraft,
1339 untyped_snapshot: Option<Vec<u8>>,
1340 run_result: RunResult,
1341 ) -> Self {
1342 Self::with_untyped_snapshot(
1343 receipt,
1344 untyped_snapshot,
1345 Some(CoreApplyTerminal::RunResult(Box::new(run_result))),
1346 )
1347 }
1348
1349 pub fn with_callback_pending(
1350 receipt: RunBoundaryReceiptDraft,
1351 untyped_snapshot: Option<Vec<u8>>,
1352 tool_use_id: impl Into<String>,
1353 tool_name: impl Into<String>,
1354 args: Value,
1355 ) -> Self {
1356 Self::with_untyped_snapshot(
1357 receipt,
1358 untyped_snapshot,
1359 Some(CoreApplyTerminal::CallbackPending {
1360 tool_use_id: tool_use_id.into(),
1361 tool_name: tool_name.into(),
1362 args,
1363 }),
1364 )
1365 }
1366
1367 pub fn with_callback_batch_pending(
1368 receipt: RunBoundaryReceiptDraft,
1369 untyped_snapshot: Option<Vec<u8>>,
1370 pending_tool_calls: Vec<crate::error::PendingCallbackToolCall>,
1371 ) -> Self {
1372 Self::with_untyped_snapshot(
1373 receipt,
1374 untyped_snapshot,
1375 Some(CoreApplyTerminal::CallbackBatchPending { pending_tool_calls }),
1376 )
1377 }
1378
1379 pub fn without_terminal(
1380 receipt: RunBoundaryReceiptDraft,
1381 untyped_snapshot: Option<Vec<u8>>,
1382 ) -> Self {
1383 Self::with_untyped_snapshot(receipt, untyped_snapshot, None)
1384 }
1385
1386 pub fn with_session(
1394 mut self,
1395 session: std::sync::Arc<crate::Session>,
1396 ) -> Result<Self, serde_json::Error> {
1397 self.committed = Some(BoundSessionCommit::sealed(session)?);
1398 Ok(self)
1399 }
1400
1401 #[must_use]
1409 pub fn with_bound_session(mut self, committed: BoundSessionCommit) -> Self {
1410 self.committed = Some(committed);
1411 self
1412 }
1413
1414 #[must_use]
1416 pub fn committed(&self) -> Option<&BoundSessionCommit> {
1417 self.committed.as_ref()
1418 }
1419
1420 pub fn whole_blob_bytes(&self) -> Result<Option<&[u8]>, SessionBoundaryEncodeError> {
1422 self.committed
1423 .as_ref()
1424 .map(BoundSessionCommit::whole_blob_bytes)
1425 .transpose()
1426 }
1427
1428 #[must_use]
1431 pub fn session(&self) -> Option<&crate::Session> {
1432 self.committed
1433 .as_ref()
1434 .and_then(BoundSessionCommit::session)
1435 }
1436
1437 #[must_use]
1440 pub fn into_committed(self) -> Option<BoundSessionCommit> {
1441 self.committed
1442 }
1443
1444 #[must_use]
1447 pub fn into_parts(
1448 self,
1449 ) -> (
1450 RunBoundaryReceiptDraft,
1451 Option<BoundSessionCommit>,
1452 Option<CoreApplyTerminal>,
1453 ) {
1454 (self.receipt, self.committed, self.terminal)
1455 }
1456}
1457
1458#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1471#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1472pub trait CoreExecutorBoundaryHandle: Send + Sync {
1473 async fn cancel_after_boundary(
1475 &self,
1476 expected_run_id: &RunId,
1477 reason: String,
1478 ) -> Result<(), CoreExecutorError>;
1479
1480 async fn prepare_transient_turn_context_at_boundary(
1487 &self,
1488 _expected_run_id: &RunId,
1489 _contexts: Vec<TurnRequestContext>,
1490 ) -> Result<CoreBoundaryStageOutput, CoreBoundaryStageError> {
1491 Err(CoreBoundaryStageError::unavailable(
1492 "live transient turn-context preparation is unsupported by this executor",
1493 ))
1494 }
1495}
1496
1497#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1499#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1500pub trait CoreExecutorInterruptHandle: Send + Sync {
1501 async fn hard_cancel_current_run(&self, reason: String) -> Result<(), CoreExecutorError>;
1502}
1503
1504#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1513#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1514pub trait CoreExecutorPublicationHandle: Send + Sync {
1515 async fn publish_interaction_terminals(
1516 &self,
1517 events: &[AgentEvent],
1518 ) -> Result<Vec<CoreInteractionTerminalPublicationReceipt>, CoreExecutorError>;
1519}
1520
1521#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1531#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1532pub trait CoreExecutorPostStopCleanupHandle: Send + Sync {
1533 async fn cleanup_after_runtime_stop_terminalized(&self) -> Result<(), CoreExecutorError>;
1534
1535 async fn cleanup_after_runtime_stop_terminalized_under_turn_finalization_boundary(
1539 &self,
1540 ) -> Result<(), CoreExecutorError> {
1541 self.cleanup_after_runtime_stop_terminalized().await
1542 }
1543}
1544
1545pub trait CoreExecutorTurnFinalizationGuard: Send {}
1550
1551impl<T: Send> CoreExecutorTurnFinalizationGuard for T {}
1552
1553#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1555#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1556pub trait CoreExecutorTurnFinalizationBoundaryHandle: Send + Sync {
1557 async fn acquire(
1558 &self,
1559 ) -> Result<Box<dyn CoreExecutorTurnFinalizationGuard>, CoreExecutorError>;
1560}
1561
1562#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1571#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1572pub trait CoreExecutor: Send + Sync {
1573 fn boundary_handle(&self) -> Option<Arc<dyn CoreExecutorBoundaryHandle>> {
1579 None
1580 }
1581
1582 fn interrupt_handle(&self) -> Option<Arc<dyn CoreExecutorInterruptHandle>> {
1588 None
1589 }
1590
1591 fn publication_handle(&self) -> Option<Arc<dyn CoreExecutorPublicationHandle>> {
1593 None
1594 }
1595
1596 fn machine_managed_post_stop_unregister(&self) -> bool {
1606 false
1607 }
1608
1609 fn post_stop_cleanup_handle(&self) -> Option<Arc<dyn CoreExecutorPostStopCleanupHandle>> {
1612 None
1613 }
1614
1615 fn turn_finalization_boundary_handle(
1617 &self,
1618 ) -> Option<Arc<dyn CoreExecutorTurnFinalizationBoundaryHandle>> {
1619 None
1620 }
1621
1622 async fn apply(
1627 &mut self,
1628 run_id: RunId,
1629 primitive: RunPrimitive,
1630 ) -> Result<CoreApplyOutput, CoreExecutorError>;
1631
1632 async fn checkpoint_committed_session_snapshot(
1642 &mut self,
1643 _session_snapshot: std::sync::Arc<Vec<u8>>,
1644 ) -> Result<(), CoreExecutorError> {
1645 Ok(())
1646 }
1647
1648 async fn acknowledge_committed_session_boundary(
1662 &mut self,
1663 _authority: &CommittedSessionBoundaryAuthority,
1664 ) -> Result<(), CoreExecutorError> {
1665 Err(CoreExecutorError::Internal(
1666 "executor cannot acknowledge a store-owned session boundary".to_string(),
1667 ))
1668 }
1669
1670 async fn reconcile_committed_compaction_projections(
1675 &mut self,
1676 intents: &[crate::memory::CompactionProjectionIntent],
1677 ) -> Result<(), CoreExecutorError> {
1678 if intents.is_empty() {
1679 Ok(())
1680 } else {
1681 Err(CoreExecutorError::Internal(
1682 "executor cannot reconcile committed compaction projections".to_string(),
1683 ))
1684 }
1685 }
1686
1687 async fn abort_uncommitted_compaction_projections(&mut self) -> Result<(), CoreExecutorError> {
1693 Ok(())
1694 }
1695
1696 async fn abort_rejected_run_projections(&mut self) -> Result<(), CoreExecutorError> {
1707 self.abort_uncommitted_compaction_projections().await
1708 }
1709
1710 async fn publish_interaction_terminals(
1715 &mut self,
1716 events: &[AgentEvent],
1717 ) -> Result<Vec<CoreInteractionTerminalPublicationReceipt>, CoreExecutorError> {
1718 if events.is_empty() {
1719 return Ok(Vec::new());
1720 }
1721 Err(CoreExecutorError::Internal(
1722 "exact interaction terminal publication is unsupported by this executor".to_string(),
1723 ))
1724 }
1725
1726 async fn cancel_after_boundary(&mut self, reason: String) -> Result<(), CoreExecutorError>;
1728
1729 async fn stop_runtime_executor(&mut self, reason: String) -> Result<(), CoreExecutorError>;
1731
1732 async fn cleanup_after_runtime_stop_terminalized(&mut self) -> Result<(), CoreExecutorError> {
1741 Ok(())
1742 }
1743}
1744
1745#[cfg(test)]
1746#[allow(clippy::panic)]
1747mod tests {
1748 use super::*;
1749
1750 fn _assert_object_safe(_: &dyn CoreExecutor) {}
1752
1753 #[test]
1754 fn prepared_session_boundary_serializes_exactly_once_across_clones() {
1755 let Ok(commit) = BoundSessionCommit::sealed(std::sync::Arc::new(crate::Session::new()))
1756 else {
1757 panic!("sealing a typed boundary no longer serializes and cannot fail");
1758 };
1759 let cloned = commit.clone();
1760
1761 assert_eq!(commit.whole_blob_encode_count(), 0);
1762 assert!(commit.whole_blob_bytes().is_ok());
1763 assert!(cloned.whole_blob_bytes().is_ok());
1764 assert_eq!(commit.whole_blob_encode_count(), 1);
1765 assert_eq!(cloned.whole_blob_encode_count(), 1);
1766 }
1767
1768 #[test]
1769 #[allow(clippy::expect_used)]
1770 fn prepared_serialized_artifact_is_retained_without_copy_or_rehash() {
1771 let artifact = std::sync::Arc::new(crate::SerializedSessionArtifact::from_raw_bytes(
1772 br#"{"exact":"artifact"}"#.to_vec(),
1773 ));
1774 let commit = BoundSessionCommit::from_serialized_artifact(std::sync::Arc::clone(&artifact));
1775 let retained = commit
1776 .whole_blob_artifact()
1777 .expect("pre-serialized artifact remains immediately available");
1778
1779 assert!(std::ptr::eq(retained, artifact.as_ref()));
1780 assert_eq!(commit.whole_blob_encode_count(), 0);
1781 assert!(commit.session().is_none());
1782 }
1783
1784 #[test]
1785 fn core_executor_error_display() {
1786 let err = CoreExecutorError::ApplyFailed {
1787 cause: CoreApplyFailureCause::runtime_turn("bad input"),
1788 };
1789 assert_eq!(err.to_string(), "Apply failed: bad input");
1790
1791 let err = CoreExecutorError::ControlFailed {
1792 cause: CoreControlFailureCause::runtime_control("not running"),
1793 };
1794 assert_eq!(err.to_string(), "Control failed: not running");
1795
1796 let err = CoreExecutorError::Stopped;
1797 assert_eq!(err.to_string(), "Executor is stopped");
1798
1799 let err = CoreExecutorError::Cancelled;
1800 assert_eq!(err.to_string(), "Run was cancelled");
1801
1802 let err = CoreExecutorError::Internal("oops".into());
1803 assert_eq!(err.to_string(), "Internal error: oops");
1804 }
1805
1806 #[test]
1807 fn apply_failed_carries_typed_cause() {
1808 let err = CoreExecutorError::ApplyFailed {
1809 cause: CoreApplyFailureCause::runtime_context_apply("context write failed"),
1810 };
1811
1812 match err {
1813 CoreExecutorError::ApplyFailed { cause } => {
1814 assert_eq!(cause.kind, CoreApplyFailureCauseKind::RuntimeContextApply);
1815 assert_eq!(cause.message(), "context write failed");
1816 }
1817 other => panic!("expected typed apply failure, got {other:?}"),
1818 }
1819 }
1820
1821 #[test]
1822 fn cancelled_session_error_remains_typed_at_runtime_executor_boundary() {
1823 let err = CoreExecutorError::apply_failed_from_session_error(SessionError::Agent(
1824 AgentError::Cancelled,
1825 ));
1826
1827 assert!(err.is_cancelled());
1828 assert_eq!(
1829 err.apply_failure_cause().kind,
1830 CoreApplyFailureCauseKind::RuntimeTurn
1831 );
1832 }
1833
1834 #[test]
1835 fn corrupted_live_session_signal_stops_instead_of_retrying_apply() {
1836 let err = CoreExecutorError::apply_failed_from_session_error(
1837 SessionError::runtime_executor_stopped("terminal witness mismatch"),
1838 );
1839
1840 assert!(matches!(err, CoreExecutorError::Stopped));
1841 }
1842
1843 #[test]
1844 fn durable_projection_authority_unknown_requests_canonical_runtime_teardown() {
1845 let err = CoreExecutorError::apply_failed_from_session_error(SessionError::Agent(
1846 AgentError::session_durable_projection_authority_unknown(
1847 "durable transcript projection split",
1848 ),
1849 ));
1850
1851 assert!(err.requires_runtime_teardown());
1852 assert_eq!(
1853 CoreExecutorTeardownReason::DurableProjectionAuthorityUnknown.as_str(),
1854 "DurableProjectionAuthorityUnknown"
1855 );
1856 assert_eq!(
1857 CoreExecutorTeardownReason::from_wire_str("DurableProjectionAuthorityUnknown"),
1858 Some(CoreExecutorTeardownReason::DurableProjectionAuthorityUnknown)
1859 );
1860 assert!(matches!(
1861 err,
1862 CoreExecutorError::TeardownRequired {
1863 reason: CoreExecutorTeardownReason::DurableProjectionAuthorityUnknown,
1864 ..
1865 }
1866 ));
1867 }
1868
1869 #[test]
1870 fn hook_denial_agent_error_maps_to_typed_apply_failure_cause() {
1871 let error = AgentError::HookDenied {
1872 hook_id: crate::hooks::HookId::new("guard"),
1873 point: crate::hooks::HookPoint::PreToolExecution,
1874 reason_code: crate::hooks::HookReasonCode::PolicyViolation,
1875 message: "blocked by hook".to_string(),
1876 payload: None,
1877 };
1878
1879 let cause = CoreApplyFailureCause::from_agent_error(&error);
1880 assert_eq!(cause.kind, CoreApplyFailureCauseKind::HookDenied);
1881 assert!(cause.message().contains("blocked by hook"));
1882 }
1883
1884 #[test]
1885 fn hook_runtime_agent_error_maps_to_typed_apply_failure_cause() {
1886 let error = AgentError::HookExecutionFailed {
1887 hook_id: crate::hooks::HookId::new("guard"),
1888 reason: "missing runtime".to_string(),
1889 };
1890
1891 let cause = CoreApplyFailureCause::from_agent_error(&error);
1892 assert_eq!(cause.kind, CoreApplyFailureCauseKind::HookRuntimeFailure);
1893 assert!(cause.message().contains("missing runtime"));
1894 }
1895}