1#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
25use std::any::Any;
26use std::collections::BTreeSet;
27use std::sync::Arc;
28
29use crate::LoopState;
30use crate::auth::{RefreshFailureDisposition, RefreshFailureObservation};
31use crate::comms::InputSource;
32use crate::interaction::{
33 PeerIngressAdmission, PeerIngressDequeueAuthority, PeerIngressDequeueFacts,
34 PeerIngressEnvelopeFacts, PeerIngressPlainEventFacts, PeerIngressReceiveAuthority,
35 PeerIngressReceiveFacts,
36};
37use crate::lifecycle::run_primitive::ModelId;
38use crate::lifecycle::{InputId, RunId};
39use crate::ops::{AsyncOpRef, OperationId};
40use crate::peer_correlation::{
41 InboundPeerRequestState, InteractionStreamAbandonReason, InteractionStreamState,
42 OutboundPeerRequestState, PeerCorrelationId,
43};
44use crate::retry::LlmRetrySchedule;
45use crate::tool_scope::{
46 ExternalToolSurfaceBaseState, ExternalToolSurfaceDeltaOperation, ExternalToolSurfaceDeltaPhase,
47 ExternalToolSurfaceFailureCause, ExternalToolSurfaceGlobalPhase, ExternalToolSurfacePendingOp,
48 ExternalToolSurfaceStagedOp,
49};
50use crate::turn_execution_authority::{
51 ContentShape, TurnExecutionEffect, TurnExecutionInput, TurnFailureReason, TurnFailureSource,
52 TurnPhase, TurnPrimitiveKind, TurnTerminalCauseKind, TurnTerminalOutcome,
53};
54use crate::types::{HandlingMode, SessionId};
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum DrainMode {
68 Timed,
70 AttachedSession,
72 PersistentHost,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum DrainExitReason {
79 IdleTimeout,
80 Dismissed,
81 Failed,
82 Aborted,
83 SessionShutdown,
84}
85
86pub trait ModelRoutingHandle: Send + Sync {
93 fn set_baseline(
95 &self,
96 baseline_model: ModelId,
97 realtime_capable: bool,
98 ) -> Result<(), DslTransitionError>;
99
100 fn hydrate_llm_capability_surface(
105 &self,
106 identity: &crate::SessionLlmIdentity,
107 profile: Option<&crate::model_profile::ModelProfile>,
108 capability_base_filter: &crate::ToolFilter,
109 ) -> Result<(), DslTransitionError>;
110
111 fn stage_sticky_model_fallback(
122 &self,
123 activation: crate::StickyModelFallbackActivationProof,
124 visibility_plan: &StickyModelFallbackVisibilityPlan,
125 ) -> Result<Box<dyn StickyModelFallbackMachineCommit>, DslTransitionError>;
126}
127
128pub trait StickyModelFallbackMachineCommit: Send + Sync {
136 fn commit(self: Box<Self>) -> Result<(), DslTransitionError>;
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct StickyModelFallbackVisibilityPlan {
147 pub previous_state: crate::SessionToolVisibilityState,
148 pub next_state: crate::SessionToolVisibilityState,
149 pub view_image_tool_available: bool,
150 pub previous_view_image_visible: bool,
151 pub next_view_image_visible: bool,
152 pub committed_visible_set_changed: bool,
153 pub revision_bumped: bool,
154}
155
156#[derive(Debug, Clone)]
163pub struct StickyModelFallbackControlDelta {
164 previous_identity: crate::SessionLlmIdentity,
165 target_identity: crate::SessionLlmIdentity,
166 persisted_visibility_parent: crate::SessionToolVisibilityState,
167 target_visibility_state: crate::SessionToolVisibilityState,
168}
169
170impl StickyModelFallbackControlDelta {
171 pub(crate) fn new(
172 previous_identity: crate::SessionLlmIdentity,
173 target_identity: crate::SessionLlmIdentity,
174 visibility_plan: &StickyModelFallbackVisibilityPlan,
175 persisted_visibility_parent: crate::SessionToolVisibilityState,
176 ) -> Self {
177 Self {
178 previous_identity,
179 target_identity,
180 persisted_visibility_parent,
181 target_visibility_state: visibility_plan.next_state.clone(),
182 }
183 }
184
185 pub fn previous_identity(&self) -> &crate::SessionLlmIdentity {
186 &self.previous_identity
187 }
188
189 pub fn target_identity(&self) -> &crate::SessionLlmIdentity {
190 &self.target_identity
191 }
192
193 pub fn previous_visibility_state(&self) -> &crate::SessionToolVisibilityState {
194 &self.persisted_visibility_parent
195 }
196
197 pub fn target_visibility_state(&self) -> &crate::SessionToolVisibilityState {
198 &self.target_visibility_state
199 }
200
201 pub fn validate_and_apply(
203 &self,
204 session: &mut crate::Session,
205 ) -> Result<(), StickyModelFallbackControlDeltaError> {
206 let mut metadata = session
207 .try_session_metadata()
208 .map_err(|error| {
209 StickyModelFallbackControlDeltaError::InvalidSessionMetadata(error.to_string())
210 })?
211 .ok_or(StickyModelFallbackControlDeltaError::MissingSessionMetadata)?;
212 let current_identity = metadata.llm_identity();
213 if current_identity != self.previous_identity {
214 return Err(
215 StickyModelFallbackControlDeltaError::IdentityParentMismatch {
216 expected: Box::new(self.previous_identity.clone()),
217 actual: Box::new(current_identity),
218 },
219 );
220 }
221 let current_visibility = session
222 .try_tool_visibility_state()
223 .map_err(|error| {
224 StickyModelFallbackControlDeltaError::InvalidVisibilityMetadata(error.to_string())
225 })?
226 .ok_or(StickyModelFallbackControlDeltaError::MissingVisibilityMetadata)?;
227 if current_visibility != self.persisted_visibility_parent {
228 return Err(StickyModelFallbackControlDeltaError::VisibilityParentMismatch);
229 }
230
231 metadata.apply_llm_identity(&self.target_identity);
232 session.set_session_metadata(metadata).map_err(|error| {
233 StickyModelFallbackControlDeltaError::InvalidSessionMetadata(error.to_string())
234 })?;
235 session
236 .set_tool_visibility_state(
237 crate::AuthorizedSessionToolVisibilityState::from_generated_authority(
238 self.target_visibility_state.clone(),
239 ),
240 )
241 .map_err(|error| {
242 StickyModelFallbackControlDeltaError::InvalidVisibilityMetadata(error.to_string())
243 })?;
244 Ok(())
245 }
246}
247
248#[derive(Debug, Clone, thiserror::Error)]
249pub enum StickyModelFallbackControlDeltaError {
250 #[error("persisted session has no canonical LLM identity metadata")]
251 MissingSessionMetadata,
252 #[error("persisted session has no canonical tool visibility metadata")]
253 MissingVisibilityMetadata,
254 #[error("persisted session LLM identity parent does not match the staged fallback")]
255 IdentityParentMismatch {
256 expected: Box<crate::SessionLlmIdentity>,
257 actual: Box<crate::SessionLlmIdentity>,
258 },
259 #[error("persisted session tool visibility parent does not match the staged fallback")]
260 VisibilityParentMismatch,
261 #[error("persisted session LLM identity metadata is invalid: {0}")]
262 InvalidSessionMetadata(String),
263 #[error("persisted session tool visibility metadata is invalid: {0}")]
264 InvalidVisibilityMetadata(String),
265}
266
267pub trait StickyModelFallbackCommitCoordinator: Send + Sync {
269 fn begin(
270 &self,
271 machine_commit: Box<dyn StickyModelFallbackMachineCommit>,
272 control_delta: StickyModelFallbackControlDelta,
273 ) -> Result<Arc<dyn StickyModelFallbackCommitOperation>, StickyModelFallbackCommitError>;
274}
275
276#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
281#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
282pub trait StickyModelFallbackCommitOperation: Send + Sync {
283 async fn wait(
284 &self,
285 ) -> Result<Option<crate::SessionControlCommitReceipt>, StickyModelFallbackCommitError>;
286}
287
288#[derive(Debug, Clone, thiserror::Error)]
289pub enum StickyModelFallbackCommitError {
290 #[error("durable sticky fallback is unavailable without a RuntimeStore")]
291 StoreUnavailable,
292 #[error("durable sticky fallback session snapshot is missing for {session_id}")]
293 SnapshotMissing { session_id: crate::SessionId },
294 #[error("durable sticky fallback session snapshot is invalid: {0}")]
295 SnapshotInvalid(String),
296 #[error("durable sticky fallback snapshot belongs to {actual}, expected {expected}")]
297 SessionMismatch {
298 expected: crate::SessionId,
299 actual: crate::SessionId,
300 },
301 #[error(transparent)]
302 InvalidControlDelta(StickyModelFallbackControlDeltaError),
303 #[error("durable sticky fallback store operation failed before commit: {0}")]
304 Store(String),
305 #[error("durable sticky fallback compare-and-swap observed a competing snapshot")]
306 SnapshotConflict,
307 #[error("durable sticky fallback compare-and-swap outcome is unknown: {0}")]
308 SnapshotOutcomeUnknown(String),
309 #[error("generated authority rejected the staged sticky fallback: {0}")]
310 MachineRejected(DslTransitionError),
311 #[error(
312 "generated authority rejected the staged sticky fallback and durable compensation failed: {0}"
313 )]
314 CompensationFailed(String),
315 #[error("durable sticky fallback supervisor ended without a retained result")]
316 SupervisorLost,
317}
318
319impl StickyModelFallbackCommitError {
320 pub fn requires_teardown(&self) -> bool {
324 matches!(
325 self,
326 Self::SnapshotConflict
327 | Self::SnapshotOutcomeUnknown(_)
328 | Self::CompensationFailed(_)
329 | Self::SupervisorLost
330 )
331 }
332}
333
334#[cfg(test)]
335#[allow(clippy::unwrap_used)]
336mod sticky_model_fallback_control_delta_tests {
337 use super::*;
338 use crate::{
339 Message, Provider, SESSION_METADATA_SCHEMA_VERSION, Session, SessionMetadata,
340 SessionTooling, ToolFilter, UserMessage,
341 };
342
343 fn identity(model: &str) -> crate::SessionLlmIdentity {
344 crate::SessionLlmIdentity {
345 model: model.to_string(),
346 provider: Provider::OpenAI,
347 self_hosted_server_id: None,
348 provider_params: None,
349 auth_binding: None,
350 }
351 }
352
353 fn session_with_control_state(
354 identity: &crate::SessionLlmIdentity,
355 visibility: &crate::SessionToolVisibilityState,
356 ) -> Session {
357 let mut session = Session::new();
358 session
359 .set_session_metadata(SessionMetadata {
360 schema_version: SESSION_METADATA_SCHEMA_VERSION,
361 model: identity.model.clone(),
362 max_tokens: 4096,
363 structured_output_retries: 2,
364 provider: identity.provider,
365 self_hosted_server_id: identity.self_hosted_server_id.clone(),
366 provider_params: identity.provider_params.clone(),
367 tooling: SessionTooling::default(),
368 keep_alive: true,
369 comms_name: None,
370 peer_meta: None,
371 realm_id: None,
372 instance_id: None,
373 backend: None,
374 config_generation: Some(7),
375 auth_binding: identity.auth_binding.clone(),
376 mob_member_binding: None,
377 })
378 .unwrap();
379 session
380 .set_tool_visibility_state(
381 crate::AuthorizedSessionToolVisibilityState::from_generated_authority(
382 visibility.clone(),
383 ),
384 )
385 .unwrap();
386 session.push(Message::User(UserMessage::text(
387 "uncommitted turn must not appear",
388 )));
389 session
390 }
391
392 #[test]
393 fn control_delta_changes_only_identity_and_typed_visibility() {
394 let previous = identity("primary");
395 let target = identity("backup");
396 let previous_visibility = crate::SessionToolVisibilityState::default();
397 let mut target_visibility = previous_visibility.clone();
398 target_visibility.capability_base_filter = ToolFilter::Deny(
399 [crate::VIEW_IMAGE_TOOL_NAME.to_string()]
400 .into_iter()
401 .collect(),
402 );
403 target_visibility.active_revision = 1;
404 target_visibility.staged_revision = 1;
405 let plan = StickyModelFallbackVisibilityPlan {
406 previous_state: previous_visibility.clone(),
407 next_state: target_visibility.clone(),
408 view_image_tool_available: true,
409 previous_view_image_visible: true,
410 next_view_image_visible: false,
411 committed_visible_set_changed: true,
412 revision_bumped: true,
413 };
414 let delta = StickyModelFallbackControlDelta::new(
415 previous,
416 target.clone(),
417 &plan,
418 previous_visibility,
419 );
420 let mut session = session_with_control_state(
421 delta.previous_identity(),
422 delta.previous_visibility_state(),
423 );
424 let messages_before = session.messages().to_vec();
425 let total_tokens_before = session.total_tokens();
426 let unrelated_generation = session
427 .session_metadata()
428 .and_then(|metadata| metadata.config_generation);
429
430 delta.validate_and_apply(&mut session).unwrap();
431
432 assert_eq!(session.messages(), messages_before);
433 assert_eq!(session.total_tokens(), total_tokens_before);
434 let metadata = session.session_metadata().unwrap();
435 assert_eq!(metadata.llm_identity(), target);
436 assert_eq!(metadata.config_generation, unrelated_generation);
437 assert_eq!(
438 session.tool_visibility_state().unwrap(),
439 Some(target_visibility)
440 );
441 }
442
443 #[test]
444 fn control_delta_rejects_a_non_parent_without_mutation() {
445 let previous = identity("primary");
446 let target = identity("backup");
447 let visibility = crate::SessionToolVisibilityState::default();
448 let plan = StickyModelFallbackVisibilityPlan {
449 previous_state: visibility.clone(),
450 next_state: visibility.clone(),
451 view_image_tool_available: false,
452 previous_view_image_visible: false,
453 next_view_image_visible: false,
454 committed_visible_set_changed: false,
455 revision_bumped: false,
456 };
457 let delta =
458 StickyModelFallbackControlDelta::new(previous, target, &plan, visibility.clone());
459 let mut session = session_with_control_state(&identity("different"), &visibility);
460 let bytes_before = serde_json::to_vec(&session).unwrap();
461
462 assert!(matches!(
463 delta.validate_and_apply(&mut session),
464 Err(StickyModelFallbackControlDeltaError::IdentityParentMismatch { .. })
465 ));
466 assert_eq!(serde_json::to_vec(&session).unwrap(), bytes_before);
467 }
468
469 #[test]
470 fn control_delta_accepts_exact_persisted_pre_boundary_visibility_parent() {
471 let previous = identity("primary");
472 let target = identity("backup");
473 let mut persisted_visibility = crate::SessionToolVisibilityState {
474 staged_filter: ToolFilter::Deny(["shell".to_string()].into_iter().collect()),
475 staged_revision: 1,
476 ..Default::default()
477 };
478 persisted_visibility.staged_requested_deferred_names =
479 [crate::ToolName::from("deferred")].into_iter().collect();
480 let promoted_visibility = persisted_visibility.projected_boundary_applied();
481 let mut target_visibility = promoted_visibility.clone();
482 target_visibility.capability_base_filter = ToolFilter::Deny(
483 [crate::VIEW_IMAGE_TOOL_NAME.to_string()]
484 .into_iter()
485 .collect(),
486 );
487 target_visibility.active_revision = 2;
488 target_visibility.staged_revision = 2;
489 let plan = StickyModelFallbackVisibilityPlan {
490 previous_state: promoted_visibility,
491 next_state: target_visibility.clone(),
492 view_image_tool_available: true,
493 previous_view_image_visible: true,
494 next_view_image_visible: false,
495 committed_visible_set_changed: true,
496 revision_bumped: true,
497 };
498 let delta = StickyModelFallbackControlDelta::new(
499 previous,
500 target.clone(),
501 &plan,
502 persisted_visibility.clone(),
503 );
504 let mut session =
505 session_with_control_state(delta.previous_identity(), &persisted_visibility);
506
507 delta.validate_and_apply(&mut session).unwrap();
508
509 assert_eq!(session.session_metadata().unwrap().llm_identity(), target);
510 assert_eq!(
511 session.tool_visibility_state().unwrap(),
512 Some(target_visibility)
513 );
514 }
515}
516
517impl DrainExitReason {
518 pub const fn as_str(self) -> &'static str {
521 match self {
522 Self::IdleTimeout => "IdleTimeout",
523 Self::Dismissed => "Dismissed",
524 Self::Failed => "Failed",
525 Self::Aborted => "Aborted",
526 Self::SessionShutdown => "SessionShutdown",
527 }
528 }
529}
530
531#[derive(Debug, Clone, Copy, PartialEq, Eq)]
533pub enum AuthLeasePhase {
534 Valid,
535 Expiring,
536 Expired,
537 Refreshing,
538 ReauthRequired,
539 Released,
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub enum CredentialUseIntent {
551 UseCredential,
553 HoldAuthority,
555 BeginRefresh,
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq)]
566pub enum CredentialUseDisposition {
567 Authorized,
569 RefreshRequired,
571 RefreshDisallowed,
576 ReauthRequired,
578 LeaseAbsent,
580 AlreadyRefreshing,
582}
583
584#[derive(Debug, Clone, Copy, PartialEq, Eq)]
593pub struct OAuthLoginCredentialFacts {
594 pub credential_present: bool,
597 pub force_refresh: bool,
599 pub refresh_allowed: bool,
602}
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq)]
613pub enum DslRejectionKind {
614 NoMatchingTransition,
618 GuardRejected,
623 RecoveredStateInvariantRejected,
626}
627
628#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
635#[error("DSL transition rejected in {context}: {reason}")]
636pub struct DslTransitionError {
637 pub context: &'static str,
639 pub kind: DslRejectionKind,
641 pub reason: String,
644}
645
646impl DslTransitionError {
647 pub fn no_matching(context: &'static str, reason: impl Into<String>) -> Self {
649 Self {
650 context,
651 kind: DslRejectionKind::NoMatchingTransition,
652 reason: reason.into(),
653 }
654 }
655
656 pub fn guard_rejected(context: &'static str, reason: impl Into<String>) -> Self {
658 Self {
659 context,
660 kind: DslRejectionKind::GuardRejected,
661 reason: reason.into(),
662 }
663 }
664
665 pub fn recovered_state_invariant_rejected(
667 context: &'static str,
668 reason: impl Into<String>,
669 ) -> Self {
670 Self {
671 context,
672 kind: DslRejectionKind::RecoveredStateInvariantRejected,
673 reason: reason.into(),
674 }
675 }
676
677 pub fn is_guard_rejected(&self) -> bool {
679 self.kind == DslRejectionKind::GuardRejected
680 }
681}
682
683#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
688#[serde(rename_all = "snake_case")]
689pub enum PeerResponseProgressProjectionPhase {
690 Accepted,
691 InProgress,
692 PartialResult,
693}
694
695impl PeerResponseProgressProjectionPhase {
696 fn label(self) -> &'static str {
697 match self {
698 Self::Accepted => "accepted",
699 Self::InProgress => "in_progress",
700 Self::PartialResult => "partial_result",
701 }
702 }
703}
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
706#[serde(rename_all = "snake_case")]
707pub enum PeerResponseTerminalProjectionStatus {
708 Completed,
709 Failed,
710 Cancelled,
711}
712
713impl PeerResponseTerminalProjectionStatus {
714 pub fn label(self) -> &'static str {
715 match self {
716 Self::Completed => "completed",
717 Self::Failed => "failed",
718 Self::Cancelled => "cancelled",
719 }
720 }
721}
722
723#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
724pub enum PeerResponseTerminalFactError {
725 #[error("transport identity cannot be empty")]
726 EmptyTransportIdentity,
727 #[error("route identity cannot be empty")]
728 EmptyRouteIdentity,
729 #[error("route identity must be a canonical peer UUID")]
730 InvalidRouteIdentity,
731 #[error("display identity is required")]
732 MissingDisplayIdentity,
733 #[error("display identity cannot be empty")]
734 EmptyDisplayIdentity,
735 #[error("display identity cannot contain control characters")]
736 InvalidDisplayIdentity,
737 #[error("correlation id cannot be empty")]
738 EmptyCorrelationId,
739 #[error("correlation id must be a UUID: {input}")]
740 InvalidCorrelationId { input: String },
741}
742
743#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
744#[serde(transparent)]
745pub struct PeerResponseTerminalTransportIdentity(String);
746
747impl PeerResponseTerminalTransportIdentity {
748 pub fn parse(raw: impl Into<String>) -> Result<Self, PeerResponseTerminalFactError> {
749 let raw = raw.into();
750 if raw.trim().is_empty() {
751 return Err(PeerResponseTerminalFactError::EmptyTransportIdentity);
752 }
753 Ok(Self(raw))
754 }
755
756 pub fn as_str(&self) -> &str {
757 &self.0
758 }
759}
760
761impl std::fmt::Display for PeerResponseTerminalTransportIdentity {
762 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
763 self.0.fmt(f)
764 }
765}
766
767#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
768#[serde(transparent)]
769pub struct PeerResponseTerminalRouteIdentity(crate::comms::PeerId);
770
771impl PeerResponseTerminalRouteIdentity {
772 pub const fn from_peer_id(peer_id: crate::comms::PeerId) -> Self {
773 Self(peer_id)
774 }
775
776 pub fn parse(raw: impl Into<String>) -> Result<Self, PeerResponseTerminalFactError> {
777 let raw = raw.into();
778 if raw.trim().is_empty() {
779 return Err(PeerResponseTerminalFactError::EmptyRouteIdentity);
780 }
781 if raw.chars().any(char::is_control) {
782 return Err(PeerResponseTerminalFactError::InvalidRouteIdentity);
783 }
784 let peer_id = crate::comms::PeerId::parse(raw.trim())
785 .map_err(|_| PeerResponseTerminalFactError::InvalidRouteIdentity)?;
786 Ok(Self(peer_id))
787 }
788
789 pub fn peer_id(&self) -> crate::comms::PeerId {
791 self.0
792 }
793
794 pub fn as_str(&self) -> String {
795 self.0.as_str()
796 }
797}
798
799impl std::fmt::Display for PeerResponseTerminalRouteIdentity {
800 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
801 self.0.fmt(f)
802 }
803}
804
805#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
806#[serde(transparent)]
807pub struct PeerResponseTerminalDisplayIdentity(String);
808
809impl PeerResponseTerminalDisplayIdentity {
810 pub fn parse(raw: impl Into<String>) -> Result<Self, PeerResponseTerminalFactError> {
811 let raw = raw.into();
812 if raw.trim().is_empty() {
813 return Err(PeerResponseTerminalFactError::EmptyDisplayIdentity);
814 }
815 if raw.chars().any(char::is_control) {
816 return Err(PeerResponseTerminalFactError::InvalidDisplayIdentity);
817 }
818 Ok(Self(raw))
819 }
820
821 pub fn as_str(&self) -> &str {
822 &self.0
823 }
824}
825
826impl std::fmt::Display for PeerResponseTerminalDisplayIdentity {
827 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
828 self.0.fmt(f)
829 }
830}
831
832#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
833#[serde(transparent)]
834pub struct PeerResponseTerminalCorrelationId(PeerCorrelationId);
835
836impl PeerResponseTerminalCorrelationId {
837 pub fn parse(raw: impl AsRef<str>) -> Result<Self, PeerResponseTerminalFactError> {
838 let raw = raw.as_ref();
839 if raw.trim().is_empty() {
840 return Err(PeerResponseTerminalFactError::EmptyCorrelationId);
841 }
842 uuid::Uuid::parse_str(raw)
843 .map(|uuid| Self(PeerCorrelationId::from_uuid(uuid)))
844 .map_err(|_| PeerResponseTerminalFactError::InvalidCorrelationId {
845 input: raw.to_string(),
846 })
847 }
848
849 pub const fn from_peer_correlation_id(correlation_id: PeerCorrelationId) -> Self {
850 Self(correlation_id)
851 }
852
853 pub const fn as_peer_correlation_id(self) -> PeerCorrelationId {
854 self.0
855 }
856}
857
858impl std::fmt::Display for PeerResponseTerminalCorrelationId {
859 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
860 self.0.fmt(f)
861 }
862}
863
864#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
865#[serde(transparent)]
866pub struct PeerResponseTerminalRenderPayload(Option<serde_json::Value>);
867
868impl PeerResponseTerminalRenderPayload {
869 pub fn new(payload: Option<serde_json::Value>) -> Self {
870 Self(payload)
871 }
872
873 pub fn as_ref(&self) -> Option<&serde_json::Value> {
874 self.0.as_ref()
875 }
876}
877
878impl From<Option<serde_json::Value>> for PeerResponseTerminalRenderPayload {
879 fn from(payload: Option<serde_json::Value>) -> Self {
880 Self::new(payload)
881 }
882}
883
884#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
885pub struct PeerResponseTerminalSource {
886 #[serde(default, skip_serializing_if = "Option::is_none")]
887 pub transport_identity: Option<PeerResponseTerminalTransportIdentity>,
888 pub route_identity: PeerResponseTerminalRouteIdentity,
889 pub display_identity: PeerResponseTerminalDisplayIdentity,
890}
891
892impl PeerResponseTerminalSource {
893 pub fn new(
894 transport_identity: Option<PeerResponseTerminalTransportIdentity>,
895 route_identity: PeerResponseTerminalRouteIdentity,
896 display_identity: PeerResponseTerminalDisplayIdentity,
897 ) -> Self {
898 Self {
899 transport_identity,
900 route_identity,
901 display_identity,
902 }
903 }
904
905 pub fn parse(
906 transport_identity: Option<impl Into<String>>,
907 route_identity: impl Into<String>,
908 display_identity: impl Into<String>,
909 ) -> Result<Self, PeerResponseTerminalFactError> {
910 Ok(Self::new(
911 transport_identity
912 .map(PeerResponseTerminalTransportIdentity::parse)
913 .transpose()?,
914 PeerResponseTerminalRouteIdentity::parse(route_identity)?,
915 PeerResponseTerminalDisplayIdentity::parse(display_identity)?,
916 ))
917 }
918}
919
920#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
921pub struct PeerResponseTerminalFact {
922 pub source: PeerResponseTerminalSource,
923 pub correlation_id: PeerResponseTerminalCorrelationId,
924 pub status: PeerResponseTerminalProjectionStatus,
925 pub render_payload: PeerResponseTerminalRenderPayload,
926}
927
928impl PeerResponseTerminalFact {
929 pub fn new(
930 source: PeerResponseTerminalSource,
931 correlation_id: PeerResponseTerminalCorrelationId,
932 status: PeerResponseTerminalProjectionStatus,
933 render_payload: PeerResponseTerminalRenderPayload,
934 ) -> Self {
935 Self {
936 source,
937 correlation_id,
938 status,
939 render_payload,
940 }
941 }
942
943 pub fn prompt_text(&self) -> String {
944 format!(
945 "Peer terminal response from {}. Request ID: {}. Status: {}. Result: {}.",
946 self.source.display_identity,
947 self.correlation_id,
948 self.status.label(),
949 format_peer_projection_payload(self.render_payload.as_ref())
950 )
951 }
952
953 pub fn context_key(&self) -> String {
954 Self::context_key_for(&self.source.route_identity, self.correlation_id)
955 }
956
957 pub fn context_key_for(
960 route_identity: &PeerResponseTerminalRouteIdentity,
961 correlation_id: PeerResponseTerminalCorrelationId,
962 ) -> String {
963 peer_response_terminal_context_key(route_identity, correlation_id)
964 }
965
966 pub fn render_payload_value(&self) -> Option<&serde_json::Value> {
969 self.render_payload.as_ref()
970 }
971}
972
973#[derive(Debug, Clone, PartialEq)]
974pub enum PeerConversationProjection {
975 Message {
976 peer_id: String,
977 },
978 Request {
979 peer_id: crate::comms::PeerId,
980 display_name: Option<String>,
981 request_id: String,
982 intent: String,
983 payload: Option<serde_json::Value>,
984 },
985 ResponseProgress {
986 peer_id: String,
987 request_id: String,
988 phase: PeerResponseProgressProjectionPhase,
989 payload: Option<serde_json::Value>,
990 },
991 ResponseTerminal {
992 fact: PeerResponseTerminalFact,
993 },
994}
995
996impl PeerConversationProjection {
997 pub fn response_terminal(fact: PeerResponseTerminalFact) -> Self {
998 Self::ResponseTerminal { fact }
999 }
1000
1001 pub fn block_prefix_text(&self) -> Option<String> {
1002 match self {
1003 Self::Message { peer_id } => Some(format!("Peer message from {peer_id}")),
1004 Self::Request { .. }
1005 | Self::ResponseProgress { .. }
1006 | Self::ResponseTerminal { .. } => None,
1007 }
1008 }
1009
1010 pub fn prompt_text(&self) -> String {
1011 match self {
1012 Self::Message { .. } => String::new(),
1013 Self::Request {
1014 peer_id,
1015 display_name,
1016 request_id,
1017 intent,
1018 payload,
1019 } => {
1020 let display_suffix = display_name
1021 .as_deref()
1022 .map(str::trim)
1023 .filter(|name| !name.is_empty())
1024 .map(|name| format!(" (display_name: {name})"))
1025 .unwrap_or_default();
1026 let response_call = crate::interaction::SendResponseCallProjection::new(
1027 *peer_id,
1028 display_name.as_deref(),
1029 request_id.clone(),
1030 );
1031 format!(
1032 "Peer request from peer_id {peer_id}{display_suffix}. Intent: {intent}. Request ID: {request_id}. Params: {}. This is not a normal user request and not a prompt for direct user-facing output. {} Do not use send_message for this reply.",
1033 format_peer_projection_payload(payload.as_ref()),
1034 response_call.instruction_text()
1035 )
1036 }
1037 Self::ResponseProgress {
1038 peer_id,
1039 request_id,
1040 phase,
1041 payload,
1042 } => format!(
1043 "Peer response progress from {peer_id}. Request ID: {request_id}. Phase: {}. Payload: {}.",
1044 phase.label(),
1045 format_peer_projection_payload(payload.as_ref())
1046 ),
1047 Self::ResponseTerminal { fact } => fact.prompt_text(),
1048 }
1049 }
1050
1051 pub fn context_key(&self) -> Option<String> {
1052 match self {
1053 Self::ResponseTerminal { fact } => Some(fact.context_key()),
1054 Self::Message { .. } | Self::Request { .. } | Self::ResponseProgress { .. } => None,
1055 }
1056 }
1057}
1058
1059pub fn peer_response_terminal_context_key(
1060 route_identity: &PeerResponseTerminalRouteIdentity,
1061 correlation_id: PeerResponseTerminalCorrelationId,
1062) -> String {
1063 format!("peer_response_terminal:{route_identity}:{correlation_id}")
1064}
1065
1066fn format_peer_projection_payload(payload: Option<&serde_json::Value>) -> String {
1067 serde_json::to_string_pretty(payload.unwrap_or(&serde_json::Value::Null))
1068 .unwrap_or_else(|_| "null".to_string())
1069}
1070
1071#[derive(Debug, Clone, PartialEq, Eq)]
1076pub struct TurnStateSnapshot {
1077 pub active_run_id: Option<RunId>,
1078 pub terminal_run_id: Option<RunId>,
1082 pub loop_state: LoopState,
1088 pub turn_phase: TurnPhase,
1089 pub turn_terminal: bool,
1096 pub primitive_kind: Option<TurnPrimitiveKind>,
1099 pub admitted_content_shape: Option<ContentShape>,
1100 pub vision_enabled: bool,
1101 pub image_tool_results_enabled: bool,
1102 pub tool_calls_pending: u64,
1103 pub pending_op_refs: BTreeSet<AsyncOpRef>,
1104 pub barrier_operation_ids: BTreeSet<OperationId>,
1105 pub has_barrier_ops: bool,
1106 pub barrier_satisfied: bool,
1107 pub boundary_count: u64,
1108 pub cancel_after_boundary: bool,
1109 pub terminal_outcome: Option<TurnTerminalOutcome>,
1112 pub terminal_cause_kind: Option<TurnTerminalCauseKind>,
1115 pub extraction_attempts: u64,
1116 pub max_extraction_retries: u64,
1117 pub extraction_active: bool,
1123 pub llm_retry_attempt: u32,
1124 pub llm_retry_max_retries: u32,
1125 pub llm_retry_selected_delay_ms: u64,
1126}
1127
1128pub trait TurnStateHandle: Send + Sync {
1130 fn apply_turn_input(
1133 &self,
1134 input: TurnExecutionInput,
1135 ) -> Result<Vec<TurnExecutionEffect>, DslTransitionError>;
1136
1137 fn start_conversation_run(
1138 &self,
1139 run_id: RunId,
1140 primitive_kind: TurnPrimitiveKind,
1141 admitted_content_shape: ContentShape,
1142 vision_enabled: bool,
1143 image_tool_results_enabled: bool,
1144 max_extraction_retries: u64,
1145 ) -> Result<(), DslTransitionError>;
1146
1147 fn start_immediate_append(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1148
1149 fn primitive_applied(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1150
1151 fn llm_returned_tool_calls(
1152 &self,
1153 run_id: RunId,
1154 tool_count: u64,
1155 ) -> Result<(), DslTransitionError>;
1156
1157 fn llm_returned_terminal(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1158
1159 fn register_pending_ops(
1160 &self,
1161 run_id: RunId,
1162 op_refs: BTreeSet<AsyncOpRef>,
1163 barrier_operation_ids: BTreeSet<OperationId>,
1164 ) -> Result<(), DslTransitionError>;
1165
1166 fn tool_calls_resolved(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1167
1168 fn ops_barrier_satisfied(
1169 &self,
1170 run_id: RunId,
1171 operation_ids: BTreeSet<OperationId>,
1172 ) -> Result<(), DslTransitionError>;
1173
1174 fn boundary_continue(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1175
1176 fn boundary_complete(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1177
1178 fn enter_extraction(&self, run_id: RunId, max_retries: u32) -> Result<(), DslTransitionError>;
1179
1180 fn extraction_start(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1181
1182 fn extraction_validation_passed(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1183
1184 fn extraction_validation_failed(
1185 &self,
1186 run_id: RunId,
1187 error: String,
1188 ) -> Result<(), DslTransitionError>;
1189
1190 fn extraction_failed(&self, run_id: RunId, error: String) -> Result<(), DslTransitionError>;
1191
1192 fn recoverable_failure(
1193 &self,
1194 run_id: RunId,
1195 retry: LlmRetrySchedule,
1196 ) -> Result<(), DslTransitionError>;
1197
1198 fn fatal_failure(
1199 &self,
1200 run_id: RunId,
1201 failure: TurnFailureSource,
1202 ) -> Result<(), DslTransitionError>;
1203
1204 fn retry_requested(&self, run_id: RunId, retry_attempt: u32) -> Result<(), DslTransitionError>;
1205
1206 fn cancel_now(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1207
1208 fn request_cancel_after_boundary(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1209
1210 fn cancellation_observed(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1211
1212 fn acknowledge_terminal(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1213
1214 fn turn_limit_reached(
1215 &self,
1216 run_id: RunId,
1217 turn_count: u64,
1218 max_turns: u64,
1219 ) -> Result<(), DslTransitionError>;
1220
1221 fn budget_exhausted(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1222
1223 fn time_budget_exceeded(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1224
1225 fn force_cancel_no_run(&self) -> Result<(), DslTransitionError>;
1226
1227 fn run_completed(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1228
1229 fn run_failed(
1230 &self,
1231 run_id: RunId,
1232 reason: TurnFailureReason,
1233 ) -> Result<(), DslTransitionError>;
1234
1235 fn run_cancelled(&self, run_id: RunId) -> Result<(), DslTransitionError>;
1236
1237 fn snapshot(&self) -> TurnStateSnapshot;
1238}
1239
1240pub trait CommsDrainHandle: Send + Sync {
1250 fn ensure_drain_running(&self) -> Result<(), DslTransitionError>;
1252
1253 fn spawn_drain(&self, mode: DrainMode) -> Result<(), DslTransitionError>;
1255
1256 fn stop_drain(&self) -> Result<(), DslTransitionError>;
1258
1259 fn notify_drain_exited(&self, reason: DrainExitReason) -> Result<(), DslTransitionError>;
1261}
1262
1263#[derive(Debug, Clone, PartialEq, Eq)]
1268pub struct SurfaceSnapshot {
1269 pub surface_id: String,
1270 pub base_state: Option<ExternalToolSurfaceBaseState>,
1273 pub pending_op: ExternalToolSurfacePendingOp,
1274 pub staged_op: ExternalToolSurfaceStagedOp,
1275 pub staged_intent_sequence: Option<u64>,
1276 pub pending_task_sequence: Option<u64>,
1277 pub pending_lineage_sequence: Option<u64>,
1278 pub inflight_calls: u64,
1279 pub last_delta_operation: Option<ExternalToolSurfaceDeltaOperation>,
1281 pub last_delta_phase: Option<ExternalToolSurfaceDeltaPhase>,
1283 pub removal_draining_since_ms: Option<u64>,
1284 pub removal_timeout_at_ms: Option<u64>,
1285 pub removal_applied_at_turn: Option<u64>,
1286}
1287
1288#[derive(Debug, Clone, PartialEq, Eq)]
1289pub struct SurfaceDiagnosticSnapshot {
1290 pub surface_phase: ExternalToolSurfaceGlobalPhase,
1291 pub known_surfaces: BTreeSet<String>,
1292 pub visible_surfaces: BTreeSet<String>,
1293 pub snapshot_epoch: u64,
1294 pub snapshot_aligned_epoch: u64,
1295 pub has_pending_or_staged: bool,
1296 pub entries: Vec<SurfaceSnapshot>,
1297}
1298
1299#[derive(Debug, Clone, PartialEq, Eq)]
1300pub enum ExternalToolSurfaceInput {
1301 SetRemovalTimeout {
1302 timeout_ms: u64,
1303 },
1304 StageAdd {
1305 surface_id: String,
1306 now_ms: u64,
1307 },
1308 StageRemove {
1309 surface_id: String,
1310 now_ms: u64,
1311 },
1312 StageReload {
1313 surface_id: String,
1314 now_ms: u64,
1315 },
1316 ApplyBoundary {
1317 surface_id: String,
1318 now_ms: u64,
1319 staged_intent_sequence: u64,
1320 applied_at_turn: u64,
1321 },
1322 MarkPendingSucceeded {
1323 surface_id: String,
1324 pending_task_sequence: u64,
1325 staged_intent_sequence: u64,
1326 },
1327 MarkPendingFailed {
1328 surface_id: String,
1329 pending_task_sequence: u64,
1330 staged_intent_sequence: u64,
1331 cause: ExternalToolSurfaceFailureCause,
1332 },
1333 CallStarted {
1334 surface_id: String,
1335 },
1336 CallFinished {
1337 surface_id: String,
1338 },
1339 FinalizeRemovalClean {
1340 surface_id: String,
1341 },
1342 FinalizeRemovalForced {
1343 surface_id: String,
1344 },
1345 SnapshotAligned {
1346 epoch: u64,
1347 },
1348 Shutdown,
1349}
1350
1351#[derive(Debug, Clone, PartialEq, Eq)]
1352pub enum ExternalToolSurfaceEffect {
1353 ScheduleSurfaceCompletion {
1354 surface_id: String,
1355 operation: ExternalToolSurfaceDeltaOperation,
1356 pending_task_sequence: u64,
1357 staged_intent_sequence: u64,
1358 applied_at_turn: u64,
1359 },
1360 RefreshVisibleSurfaceSet {
1361 snapshot_epoch: u64,
1362 },
1363 EmitExternalToolDelta {
1364 surface_id: String,
1365 operation: ExternalToolSurfaceDeltaOperation,
1366 phase: ExternalToolSurfaceDeltaPhase,
1367 cause: Option<ExternalToolSurfaceFailureCause>,
1368 },
1369 CloseSurfaceConnection {
1370 surface_id: String,
1371 },
1372 RejectSurfaceCall {
1373 surface_id: String,
1374 cause: ExternalToolSurfaceFailureCause,
1375 },
1376}
1377
1378#[derive(Debug, Clone, PartialEq, Eq)]
1379pub struct ExternalToolSurfaceTransition {
1380 pub phase: ExternalToolSurfaceGlobalPhase,
1381 pub effects: Vec<ExternalToolSurfaceEffect>,
1382}
1383
1384pub trait ExternalToolSurfaceHandle: Send + Sync {
1386 fn apply_surface_input(
1387 &self,
1388 input: ExternalToolSurfaceInput,
1389 ) -> Result<ExternalToolSurfaceTransition, DslTransitionError>;
1390
1391 fn register(&self, surface_id: String) -> Result<(), DslTransitionError>;
1392
1393 fn stage_add(&self, surface_id: String, now_ms: u64) -> Result<(), DslTransitionError>;
1394
1395 fn stage_remove(&self, surface_id: String, now_ms: u64) -> Result<(), DslTransitionError>;
1396
1397 fn stage_reload(&self, surface_id: String, now_ms: u64) -> Result<(), DslTransitionError>;
1398
1399 fn apply_boundary(
1400 &self,
1401 surface_id: String,
1402 now_ms: u64,
1403 staged_intent_sequence: u64,
1404 applied_at_turn: u64,
1405 ) -> Result<(), DslTransitionError>;
1406
1407 fn mark_pending_succeeded(
1408 &self,
1409 surface_id: String,
1410 pending_task_sequence: u64,
1411 staged_intent_sequence: u64,
1412 ) -> Result<(), DslTransitionError>;
1413
1414 fn mark_pending_failed(
1415 &self,
1416 surface_id: String,
1417 pending_task_sequence: u64,
1418 staged_intent_sequence: u64,
1419 cause: ExternalToolSurfaceFailureCause,
1420 ) -> Result<(), DslTransitionError>;
1421
1422 fn call_started(&self, surface_id: String) -> Result<(), DslTransitionError>;
1423
1424 fn call_finished(&self, surface_id: String) -> Result<(), DslTransitionError>;
1425
1426 fn finalize_removal_clean(&self, surface_id: String) -> Result<(), DslTransitionError>;
1427
1428 fn finalize_removal_forced(&self, surface_id: String) -> Result<(), DslTransitionError>;
1429
1430 fn snapshot_aligned(&self, epoch: u64) -> Result<(), DslTransitionError>;
1431
1432 fn shutdown_surface(&self) -> Result<(), DslTransitionError>;
1433
1434 fn surface_snapshot(&self, surface_id: &str) -> Option<SurfaceSnapshot>;
1435
1436 fn diagnostic_snapshot(&self) -> SurfaceDiagnosticSnapshot;
1437
1438 fn visible_surfaces(&self) -> BTreeSet<String>;
1439
1440 fn removing_surfaces(&self) -> BTreeSet<String>;
1441
1442 fn pending_surfaces(&self) -> BTreeSet<String>;
1443
1444 fn has_pending_or_staged(&self) -> bool;
1445
1446 fn snapshot_epoch(&self) -> u64;
1447
1448 fn snapshot_aligned_epoch(&self) -> u64;
1449}
1450
1451pub trait PeerCommsHandle: Send + Sync {
1465 fn classify_external_envelope(
1468 &self,
1469 facts: PeerIngressEnvelopeFacts,
1470 ) -> Result<PeerIngressAdmission, DslTransitionError>;
1471
1472 fn classify_plain_event(
1475 &self,
1476 facts: PeerIngressPlainEventFacts,
1477 ) -> Result<PeerIngressAdmission, DslTransitionError>;
1478
1479 fn resolve_peer_ingress_receive(
1482 &self,
1483 facts: PeerIngressReceiveFacts,
1484 ) -> Result<PeerIngressReceiveAuthority, DslTransitionError>;
1485
1486 fn resolve_peer_ingress_dequeue(
1489 &self,
1490 facts: PeerIngressDequeueFacts,
1491 ) -> Result<PeerIngressDequeueAuthority, DslTransitionError>;
1492
1493 fn set_peer_ingress_context(&self, keep_alive: bool) -> Result<(), DslTransitionError>;
1495
1496 fn install_generated_peer_comms_on_target(
1500 &self,
1501 _expected_owner: &crate::comms::GeneratedPeerCommsOwnerToken,
1502 _target: &(dyn PeerCommsInstallTarget + '_),
1503 ) -> Result<(), String> {
1504 Err("peer-comms handle does not expose generated install target authority".to_string())
1505 }
1506}
1507
1508#[derive(Clone)]
1509pub struct GeneratedPeerCommsInstallFactory {
1510 handle: std::sync::Arc<dyn PeerCommsHandle>,
1511 owner_token: crate::comms::GeneratedPeerCommsOwnerToken,
1512}
1513
1514impl std::fmt::Debug for GeneratedPeerCommsInstallFactory {
1515 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1516 f.debug_struct("GeneratedPeerCommsInstallFactory")
1517 .field("handle", &"<dyn PeerCommsHandle>")
1518 .field("owner_token", &self.owner_token)
1519 .finish()
1520 }
1521}
1522
1523impl GeneratedPeerCommsInstallFactory {
1524 #[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1525 #[doc(hidden)]
1526 pub fn __from_runtime_generated_authority(
1527 token: &'static (dyn Any + Send + Sync),
1528 handle: std::sync::Arc<dyn PeerCommsHandle>,
1529 owner_token: std::sync::Arc<dyn Any + Send + Sync>,
1530 ) -> Result<Self, String> {
1531 validate_peer_comms_install_bridge_token(token)?;
1532 Ok(Self {
1533 handle,
1534 owner_token: crate::comms::GeneratedPeerCommsOwnerToken::from_generated_owner_token(
1535 owner_token,
1536 ),
1537 })
1538 }
1539
1540 pub fn peer_comms_handle(&self) -> &std::sync::Arc<dyn PeerCommsHandle> {
1541 &self.handle
1542 }
1543
1544 pub fn install_on_target(
1545 &self,
1546 target: &(dyn PeerCommsInstallTarget + '_),
1547 ) -> Result<(), String> {
1548 self.handle
1549 .install_generated_peer_comms_on_target(&self.owner_token, target)
1550 }
1551}
1552
1553#[derive(Clone)]
1554pub struct GeneratedPeerCommsInstall {
1555 handle: std::sync::Arc<dyn PeerCommsHandle>,
1556 owner_token: crate::comms::GeneratedPeerCommsOwnerToken,
1557 target_peer_id: crate::comms::PeerId,
1558}
1559
1560impl std::fmt::Debug for GeneratedPeerCommsInstall {
1561 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1562 f.debug_struct("GeneratedPeerCommsInstall")
1563 .field("handle", &"<dyn PeerCommsHandle>")
1564 .field("owner_token", &self.owner_token)
1565 .field("target_peer_id", &self.target_peer_id)
1566 .finish()
1567 }
1568}
1569
1570impl GeneratedPeerCommsInstall {
1571 #[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1572 #[doc(hidden)]
1573 pub fn __from_runtime_generated_authority(
1574 token: &'static (dyn Any + Send + Sync),
1575 handle: std::sync::Arc<dyn PeerCommsHandle>,
1576 owner_token: std::sync::Arc<dyn Any + Send + Sync>,
1577 target_peer_id: crate::comms::PeerId,
1578 ) -> Result<Self, String> {
1579 validate_peer_comms_install_bridge_token(token)?;
1580 Ok(Self {
1581 handle,
1582 owner_token: crate::comms::GeneratedPeerCommsOwnerToken::from_generated_owner_token(
1583 owner_token,
1584 ),
1585 target_peer_id,
1586 })
1587 }
1588
1589 pub fn peer_comms_handle(&self) -> &std::sync::Arc<dyn PeerCommsHandle> {
1590 &self.handle
1591 }
1592
1593 pub fn owner_token(&self) -> crate::comms::GeneratedPeerCommsOwnerToken {
1594 self.owner_token.clone()
1595 }
1596
1597 pub fn target_peer_id(&self) -> crate::comms::PeerId {
1598 self.target_peer_id
1599 }
1600}
1601
1602#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1603#[allow(improper_ctypes_definitions, unsafe_code)]
1604unsafe extern "Rust" {
1605 #[link_name = concat!(
1606 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_comms_trust_reconcile_",
1607 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1608 )]
1609 fn runtime_peer_comms_install_generated_authority_bridge_token_is_valid(
1610 token: &(dyn Any + Send + Sync),
1611 ) -> bool;
1612}
1613
1614#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1615fn validate_peer_comms_install_bridge_token(token: &(dyn Any + Send + Sync)) -> Result<(), String> {
1616 #[allow(unsafe_code)]
1617 let valid =
1618 unsafe { runtime_peer_comms_install_generated_authority_bridge_token_is_valid(token) };
1619 if valid {
1620 Ok(())
1621 } else {
1622 Err("generated peer-comms install requires the matching generated runtime protocol bridge token".into())
1623 }
1624}
1625
1626pub trait PeerCommsInstallTarget: crate::agent::CommsRuntime {
1633 fn generated_peer_comms_target_endpoint(
1634 &self,
1635 ) -> Result<crate::comms::TrustedPeerDescriptor, String> {
1636 let peer_id = self
1637 .peer_id()
1638 .ok_or_else(|| "runtime peer_id unavailable".to_string())?;
1639 let name = self
1640 .comms_name()
1641 .ok_or_else(|| "runtime comms_name unavailable".to_string())?;
1642 let address = self
1643 .advertised_address()
1644 .ok_or_else(|| "runtime advertised_address unavailable".to_string())?;
1645 let pubkey = self
1646 .public_key_bytes()
1647 .ok_or_else(|| "runtime public_key_bytes unavailable".to_string())?;
1648 crate::comms::TrustedPeerDescriptor::unsigned_with_pubkey(
1649 name,
1650 peer_id.to_string(),
1651 pubkey,
1652 address,
1653 )
1654 .map_err(|error| format!("runtime peer-comms install target endpoint invalid: {error}"))
1655 }
1656
1657 fn install_generated_peer_comms_handle(
1658 &self,
1659 install: GeneratedPeerCommsInstall,
1660 ) -> Result<(), String>;
1661}
1662
1663pub trait SessionAdmissionHandle: Send + Sync {
1678 fn ingest(
1685 &self,
1686 runtime_id: &str,
1687 work_id: &str,
1688 origin: InputSource,
1689 ) -> Result<(), DslTransitionError>;
1690
1691 fn accept_with_completion(
1702 &self,
1703 input_id: &InputId,
1704 request_immediate_processing: bool,
1705 interrupt_yielding: bool,
1706 wake_if_idle: bool,
1707 ) -> Result<(), DslTransitionError>;
1708
1709 fn accept_without_wake(&self, input_id: &InputId) -> Result<(), DslTransitionError>;
1711
1712 fn prepare(&self, run_id: &RunId) -> Result<(), DslTransitionError>;
1714}
1715
1716#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1722pub struct LeaseKey {
1723 pub realm: crate::connection::RealmId,
1724 pub binding: crate::connection::BindingId,
1725 pub profile: Option<crate::connection::ProfileId>,
1726}
1727
1728impl LeaseKey {
1729 pub fn new(
1730 realm: crate::connection::RealmId,
1731 binding: crate::connection::BindingId,
1732 profile: Option<crate::connection::ProfileId>,
1733 ) -> Self {
1734 Self {
1735 realm,
1736 binding,
1737 profile,
1738 }
1739 }
1740
1741 pub fn from_auth_binding(auth_binding: &crate::connection::AuthBindingRef) -> Self {
1742 Self {
1743 realm: auth_binding.realm.clone(),
1744 binding: auth_binding.binding.clone(),
1745 profile: auth_binding.profile.clone(),
1746 }
1747 }
1748}
1749
1750impl std::fmt::Display for LeaseKey {
1751 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1752 match &self.profile {
1753 Some(profile) => write!(f, "{}:{}:{}", self.realm, self.binding, profile),
1754 None => write!(f, "{}:{}", self.realm, self.binding),
1755 }
1756 }
1757}
1758
1759#[derive(Debug, Clone, PartialEq, Eq)]
1776pub struct AuthLeaseSnapshot {
1777 pub phase: Option<AuthLeasePhase>,
1778 pub expires_at: Option<u64>,
1779 pub credential_present: bool,
1780 pub generation: u64,
1781 pub credential_published_at_millis: Option<u64>,
1782}
1783
1784#[derive(Debug, Clone, PartialEq, Eq)]
1791pub struct AuthLeaseRestoreSnapshot {
1792 lease_key: LeaseKey,
1793 snapshot: AuthLeaseSnapshot,
1794 captured_by: std::any::TypeId,
1795 captured_by_instance: usize,
1796}
1797
1798impl AuthLeaseRestoreSnapshot {
1799 fn capture(
1800 lease_key: LeaseKey,
1801 snapshot: AuthLeaseSnapshot,
1802 captured_by: std::any::TypeId,
1803 captured_by_instance: usize,
1804 ) -> Self {
1805 Self {
1806 lease_key,
1807 snapshot,
1808 captured_by,
1809 captured_by_instance,
1810 }
1811 }
1812
1813 pub fn lease_key(&self) -> &LeaseKey {
1814 &self.lease_key
1815 }
1816
1817 pub fn snapshot(&self) -> &AuthLeaseSnapshot {
1818 &self.snapshot
1819 }
1820
1821 #[doc(hidden)]
1822 pub fn captured_by_type_id(&self) -> std::any::TypeId {
1823 self.captured_by
1824 }
1825
1826 #[doc(hidden)]
1827 pub fn captured_by_instance_id(&self) -> usize {
1828 self.captured_by_instance
1829 }
1830}
1831
1832#[derive(Debug, Clone, PartialEq, Eq)]
1840pub struct AuthLeaseTransition {
1841 lease_key: LeaseKey,
1842 phase: AuthLeasePhase,
1843 expires_at: u64,
1844 generation: u64,
1845 credential_published_at_millis: Option<u64>,
1846}
1847
1848impl AuthLeaseTransition {
1849 pub fn lease_key(&self) -> &LeaseKey {
1850 &self.lease_key
1851 }
1852
1853 pub fn phase(&self) -> AuthLeasePhase {
1854 self.phase
1855 }
1856
1857 pub fn expires_at(&self) -> u64 {
1858 self.expires_at
1859 }
1860
1861 pub fn generation(&self) -> u64 {
1862 self.generation
1863 }
1864
1865 pub fn credential_published_at_millis(&self) -> Option<u64> {
1866 self.credential_published_at_millis
1867 }
1868
1869 #[cfg_attr(
1870 any(not(meerkat_internal_generated_authority_bridge), test),
1871 allow(dead_code)
1872 )]
1873 fn from_generated_auth_lease_publication_parts(
1874 lease_key: LeaseKey,
1875 phase: AuthLeasePhase,
1876 expires_at: u64,
1877 generation: u64,
1878 credential_published_at_millis: Option<u64>,
1879 ) -> Self {
1880 Self {
1881 lease_key,
1882 phase,
1883 expires_at,
1884 generation,
1885 credential_published_at_millis,
1886 }
1887 }
1888}
1889
1890#[derive(Clone)]
1897pub struct GeneratedAuthLeaseHandle {
1898 inner: Arc<dyn AuthLeaseHandle>,
1899}
1900
1901impl GeneratedAuthLeaseHandle {
1902 pub fn as_handle(&self) -> &dyn AuthLeaseHandle {
1903 self.inner.as_ref()
1904 }
1905
1906 pub fn clone_handle(&self) -> Arc<dyn AuthLeaseHandle> {
1907 Arc::clone(&self.inner)
1908 }
1909
1910 #[cfg_attr(
1911 any(not(meerkat_internal_generated_authority_bridge), test),
1912 allow(dead_code)
1913 )]
1914 fn from_generated_authority(inner: Arc<dyn AuthLeaseHandle>) -> Self {
1915 Self { inner }
1916 }
1917}
1918
1919impl std::fmt::Debug for GeneratedAuthLeaseHandle {
1920 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1921 f.debug_struct("GeneratedAuthLeaseHandle")
1922 .finish_non_exhaustive()
1923 }
1924}
1925
1926impl std::ops::Deref for GeneratedAuthLeaseHandle {
1927 type Target = dyn AuthLeaseHandle;
1928
1929 fn deref(&self) -> &Self::Target {
1930 self.inner.as_ref()
1931 }
1932}
1933
1934impl AsRef<dyn AuthLeaseHandle> for GeneratedAuthLeaseHandle {
1935 fn as_ref(&self) -> &dyn AuthLeaseHandle {
1936 self.inner.as_ref()
1937 }
1938}
1939
1940#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1941#[allow(improper_ctypes_definitions, unsafe_code)]
1942unsafe extern "Rust" {
1943 #[link_name = concat!(
1944 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_auth_lease_lifecycle_publication_",
1945 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1946 )]
1947 fn runtime_auth_lease_lifecycle_publication_generated_authority_bridge_token_is_valid(
1948 token: &(dyn std::any::Any + Send + Sync),
1949 ) -> bool;
1950}
1951
1952#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1953#[doc(hidden)]
1954#[allow(improper_ctypes_definitions, unsafe_code)]
1955#[unsafe(export_name = concat!(
1956 "__meerkat_core_runtime_generated_auth_lease_transition_build_v1_",
1957 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1958))]
1959pub(crate) extern "Rust" fn runtime_generated_auth_lease_transition_build(
1960 token: &'static (dyn std::any::Any + Send + Sync),
1961 lease_key: LeaseKey,
1962 phase: AuthLeasePhase,
1963 expires_at: u64,
1964 generation: u64,
1965 credential_published_at_millis: Option<u64>,
1966) -> Result<AuthLeaseTransition, String> {
1967 validate_runtime_generated_authority_bridge_token(token)?;
1968 Ok(
1969 AuthLeaseTransition::from_generated_auth_lease_publication_parts(
1970 lease_key,
1971 phase,
1972 expires_at,
1973 generation,
1974 credential_published_at_millis,
1975 ),
1976 )
1977}
1978
1979#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1980#[doc(hidden)]
1981#[allow(improper_ctypes_definitions, unsafe_code)]
1982#[unsafe(export_name = concat!(
1983 "__meerkat_core_runtime_generated_auth_lease_handle_build_v1_",
1984 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1985))]
1986pub(crate) extern "Rust" fn runtime_generated_auth_lease_handle_build(
1987 token: &'static (dyn std::any::Any + Send + Sync),
1988 handle: Arc<dyn AuthLeaseHandle>,
1989) -> Result<GeneratedAuthLeaseHandle, String> {
1990 validate_runtime_generated_authority_bridge_token(token)?;
1991 Ok(GeneratedAuthLeaseHandle::from_generated_authority(handle))
1992}
1993
1994#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1995fn validate_runtime_generated_authority_bridge_token(
1996 token: &(dyn std::any::Any + Send + Sync),
1997) -> Result<(), String> {
1998 #[allow(unsafe_code)]
1999 let valid = unsafe {
2000 runtime_auth_lease_lifecycle_publication_generated_authority_bridge_token_is_valid(token)
2001 };
2002 if valid {
2003 Ok(())
2004 } else {
2005 Err(
2006 "generated auth lease transition requires the generated AuthMachine protocol bridge token"
2007 .into(),
2008 )
2009 }
2010}
2011
2012pub const AUTH_LEASE_TTL_REFRESH_WINDOW_SECS: u64 = 60;
2024
2025pub trait AuthLeaseHandle: Send + Sync + std::any::Any {
2027 fn acquire_lease(
2032 &self,
2033 lease_key: &LeaseKey,
2034 expires_at: u64,
2035 ) -> Result<AuthLeaseTransition, DslTransitionError>;
2036
2037 fn mark_expiring(&self, lease_key: &LeaseKey) -> Result<(), DslTransitionError>;
2039
2040 fn observe_credential_freshness(
2045 &self,
2046 lease_key: &LeaseKey,
2047 now: u64,
2048 refresh_window_secs: u64,
2049 ) -> Result<(), DslTransitionError>;
2050
2051 fn begin_refresh(&self, lease_key: &LeaseKey) -> Result<(), DslTransitionError>;
2059
2060 fn complete_refresh(
2064 &self,
2065 lease_key: &LeaseKey,
2066 new_expires_at: u64,
2067 now: u64,
2068 ) -> Result<AuthLeaseTransition, DslTransitionError>;
2069
2070 fn resolve_refresh_failure_disposition(
2077 &self,
2078 lease_key: &LeaseKey,
2079 observation: RefreshFailureObservation,
2080 ) -> Result<RefreshFailureDisposition, DslTransitionError> {
2081 let _ = (lease_key, observation);
2082 Err(DslTransitionError::no_matching(
2083 "AuthLeaseHandle::resolve_refresh_failure_disposition",
2084 "classifying refresh failure requires generated AuthMachine authority",
2085 ))
2086 }
2087
2088 fn refresh_failed(
2091 &self,
2092 lease_key: &LeaseKey,
2093 observation: RefreshFailureObservation,
2094 ) -> Result<(), DslTransitionError>;
2095
2096 fn mark_reauth_required(&self, lease_key: &LeaseKey) -> Result<(), DslTransitionError>;
2098
2099 fn release_lease(&self, lease_key: &LeaseKey) -> Result<(), DslTransitionError>;
2102
2103 fn release_credential_lifecycle(&self, lease_key: &LeaseKey) -> Result<(), DslTransitionError> {
2110 self.release_lease(lease_key)
2111 }
2112
2113 fn capture_auth_lifecycle_restore_snapshot(
2119 &self,
2120 lease_key: &LeaseKey,
2121 ) -> AuthLeaseRestoreSnapshot {
2122 AuthLeaseRestoreSnapshot::capture(
2123 lease_key.clone(),
2124 self.snapshot(lease_key),
2125 self.type_id(),
2126 self.auth_lifecycle_restore_instance_id(),
2127 )
2128 }
2129
2130 #[doc(hidden)]
2131 fn auth_lifecycle_restore_instance_id(&self) -> usize {
2132 std::ptr::from_ref(self).cast::<()>() as usize
2133 }
2134
2135 fn restore_auth_lifecycle_snapshot(
2141 &self,
2142 snapshot: &AuthLeaseRestoreSnapshot,
2143 ) -> Result<Option<AuthLeaseTransition>, DslTransitionError> {
2144 let _ = snapshot;
2145 Err(DslTransitionError::no_matching(
2146 "AuthLeaseHandle::restore_auth_lifecycle_snapshot",
2147 "restoring auth lifecycle snapshots requires generated AuthMachine authority",
2148 ))
2149 }
2150
2151 fn restore_published_credential_lifecycle(
2159 &self,
2160 lease_key: &LeaseKey,
2161 publication: &crate::generated::auth_lease_durable_lifecycle_marker::AuthLeaseDurableRestorePublication,
2162 ) -> Result<AuthLeaseTransition, DslTransitionError> {
2163 let _ = (lease_key, publication);
2164 Err(DslTransitionError::no_matching(
2165 "AuthLeaseHandle::restore_published_credential_lifecycle",
2166 "restoring durable auth lifecycle publications requires generated AuthMachine authority",
2167 ))
2168 }
2169
2170 fn resolve_credential_use_admission(
2183 &self,
2184 lease_key: &LeaseKey,
2185 intent: CredentialUseIntent,
2186 ) -> Result<CredentialUseDisposition, DslTransitionError> {
2187 let _ = (lease_key, intent);
2188 Err(DslTransitionError::no_matching(
2189 "AuthLeaseHandle::resolve_credential_use_admission",
2190 "classifying credential-use admission requires generated AuthMachine authority",
2191 ))
2192 }
2193
2194 fn resolve_oauth_login_credential_disposition(
2210 &self,
2211 lease_key: &LeaseKey,
2212 facts: OAuthLoginCredentialFacts,
2213 ) -> Result<CredentialUseDisposition, DslTransitionError> {
2214 let _ = (lease_key, facts);
2215 Err(DslTransitionError::no_matching(
2216 "AuthLeaseHandle::resolve_oauth_login_credential_disposition",
2217 "classifying OAuth-login credential disposition requires generated AuthMachine authority",
2218 ))
2219 }
2220
2221 fn snapshot(&self, lease_key: &LeaseKey) -> AuthLeaseSnapshot;
2223}
2224
2225pub trait McpServerLifecycleHandle: Send + Sync {
2246 fn apply_connect_pending(&self, server_id: &str) -> Result<(), DslTransitionError>;
2249
2250 fn apply_connected(&self, server_id: &str) -> Result<(), DslTransitionError>;
2252
2253 fn apply_failed(&self, server_id: &str, error: &str) -> Result<(), DslTransitionError>;
2255
2256 fn apply_disconnected(&self, server_id: &str) -> Result<(), DslTransitionError>;
2258
2259 fn apply_reload(&self, server_id: &str) -> Result<(), DslTransitionError>;
2262
2263 fn pending_server_ids(&self) -> BTreeSet<String>;
2268}
2269
2270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2279#[non_exhaustive]
2280pub enum PeerTerminalDisposition {
2281 Completed,
2283 Failed,
2285}
2286
2287pub trait PeerInteractionHandle: Send + Sync {
2301 fn request_sent(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2305
2306 fn response_progress(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2312
2313 fn response_terminal(
2320 &self,
2321 corr_id: PeerCorrelationId,
2322 disposition: PeerTerminalDisposition,
2323 ) -> Result<(), DslTransitionError>;
2324
2325 fn response_rejected(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2333
2334 fn request_timed_out(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2340
2341 fn request_send_failed(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2348
2349 fn request_received(
2353 &self,
2354 corr_id: PeerCorrelationId,
2355 handling_mode: HandlingMode,
2356 ) -> Result<(), DslTransitionError>;
2357
2358 fn classify_response_reply(
2361 &self,
2362 status: crate::ResponseStatus,
2363 ) -> Result<crate::TerminalityClass, DslTransitionError>;
2364
2365 fn response_replied(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2369
2370 fn outbound_state(&self, corr_id: PeerCorrelationId) -> Option<OutboundPeerRequestState>;
2374
2375 fn inbound_state(&self, corr_id: PeerCorrelationId) -> Option<InboundPeerRequestState>;
2377
2378 fn inbound_handling_mode(&self, corr_id: PeerCorrelationId) -> Option<HandlingMode>;
2380
2381 fn install_cleanup_observer(&self, observer: Arc<dyn PeerInteractionCleanupObserver>);
2390}
2391
2392pub trait PeerInteractionCleanupObserver: Send + Sync {
2402 fn on_peer_interaction_cleanup(&self, corr_id: PeerCorrelationId);
2409}
2410
2411pub trait SessionContextHandle: Send + Sync {
2426 fn context_advanced(&self, updated_at_ms: u64) -> Result<bool, DslTransitionError>;
2434
2435 fn current_watermark_ms(&self) -> u64;
2443
2444 fn install_observer(&self, observer: Arc<dyn SessionContextAdvancedObserver>);
2448
2449 fn install_observer_with_baseline(
2463 &self,
2464 observer: Arc<dyn SessionContextAdvancedObserver>,
2465 ) -> u64;
2466}
2467
2468pub trait SessionContextAdvancedObserver: Send + Sync {
2478 fn on_session_context_advanced(&self, updated_at_ms: u64);
2482}
2483
2484#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2490pub enum SessionClaimError {
2491 #[error("session identity already claimed: {0}")]
2493 SessionIdentityInUse(SessionId),
2494}
2495
2496pub struct SessionClaim {
2502 session_id: SessionId,
2503 handle: Arc<dyn SessionClaimHandle>,
2504}
2505
2506impl SessionClaim {
2507 pub fn new(session_id: SessionId, handle: Arc<dyn SessionClaimHandle>) -> Self {
2511 Self { session_id, handle }
2512 }
2513
2514 pub fn session_id(&self) -> &SessionId {
2516 &self.session_id
2517 }
2518}
2519
2520impl Drop for SessionClaim {
2521 fn drop(&mut self) {
2522 self.handle.release(&self.session_id);
2523 }
2524}
2525
2526impl std::fmt::Debug for SessionClaim {
2527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2528 f.debug_struct("SessionClaim")
2529 .field("session_id", &self.session_id)
2530 .finish_non_exhaustive()
2531 }
2532}
2533
2534pub trait SessionClaimHandle: Send + Sync {
2543 fn try_acquire(
2551 self: Arc<Self>,
2552 session_id: &SessionId,
2553 ) -> Result<SessionClaim, SessionClaimError>;
2554
2555 fn release(&self, session_id: &SessionId);
2561}
2562
2563pub struct DefaultSessionClaimRegistry {
2569 claims: std::sync::Mutex<std::collections::HashSet<SessionId>>,
2570}
2571
2572impl DefaultSessionClaimRegistry {
2573 pub fn new() -> Self {
2575 Self {
2576 claims: std::sync::Mutex::new(std::collections::HashSet::new()),
2577 }
2578 }
2579
2580 pub fn global() -> Arc<Self> {
2582 use std::sync::OnceLock;
2583 static GLOBAL: OnceLock<Arc<DefaultSessionClaimRegistry>> = OnceLock::new();
2584 Arc::clone(GLOBAL.get_or_init(|| Arc::new(DefaultSessionClaimRegistry::new())))
2585 }
2586}
2587
2588impl Default for DefaultSessionClaimRegistry {
2589 fn default() -> Self {
2590 Self::new()
2591 }
2592}
2593
2594impl SessionClaimHandle for DefaultSessionClaimRegistry {
2595 fn try_acquire(
2596 self: Arc<Self>,
2597 session_id: &SessionId,
2598 ) -> Result<SessionClaim, SessionClaimError> {
2599 let mut claims = self
2600 .claims
2601 .lock()
2602 .unwrap_or_else(std::sync::PoisonError::into_inner);
2603 if !claims.insert(session_id.clone()) {
2604 return Err(SessionClaimError::SessionIdentityInUse(session_id.clone()));
2605 }
2606 drop(claims);
2607 Ok(SessionClaim::new(
2608 session_id.clone(),
2609 self as Arc<dyn SessionClaimHandle>,
2610 ))
2611 }
2612
2613 fn release(&self, session_id: &SessionId) {
2614 let mut claims = self
2615 .claims
2616 .lock()
2617 .unwrap_or_else(std::sync::PoisonError::into_inner);
2618 claims.remove(session_id);
2619 }
2620}
2621
2622pub trait InteractionStreamHandle: Send + Sync {
2640 fn reserved(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2646
2647 fn attached(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2652
2653 fn completed(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2657
2658 fn expired(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2662
2663 fn closed_early(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError>;
2667
2668 fn abandoned(
2675 &self,
2676 corr_id: PeerCorrelationId,
2677 reason: InteractionStreamAbandonReason,
2678 ) -> Result<(), DslTransitionError>;
2679
2680 fn state(&self, corr_id: PeerCorrelationId) -> Option<InteractionStreamState>;
2687
2688 fn install_cleanup_observer(&self, observer: Arc<dyn InteractionStreamCleanupObserver>);
2693}
2694
2695pub trait InteractionStreamCleanupObserver: Send + Sync {
2704 fn on_interaction_stream_cleanup(
2712 &self,
2713 corr_id: PeerCorrelationId,
2714 abandon_reason: Option<InteractionStreamAbandonReason>,
2715 );
2716}
2717
2718#[cfg(test)]
2719#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
2720mod tests {
2721 use super::{
2722 DslRejectionKind, DslTransitionError, ExternalToolSurfaceEffect,
2723 ExternalToolSurfaceFailureCause, ExternalToolSurfaceInput, PeerConversationProjection,
2724 PeerResponseProgressProjectionPhase, PeerResponseTerminalCorrelationId,
2725 PeerResponseTerminalDisplayIdentity, PeerResponseTerminalFact,
2726 PeerResponseTerminalFactError, PeerResponseTerminalProjectionStatus,
2727 PeerResponseTerminalRenderPayload, PeerResponseTerminalRouteIdentity,
2728 PeerResponseTerminalSource, PeerResponseTerminalTransportIdentity,
2729 };
2730 use crate::tool_scope::{ExternalToolSurfaceDeltaOperation, ExternalToolSurfaceDeltaPhase};
2731
2732 #[test]
2733 fn recovered_state_rejection_is_not_guard_noop() {
2734 let err =
2735 DslTransitionError::recovered_state_invariant_rejected("recover", "bad invariant");
2736 assert_eq!(err.kind, DslRejectionKind::RecoveredStateInvariantRejected);
2737 assert!(!err.is_guard_rejected());
2738 }
2739
2740 #[test]
2741 fn external_tool_surface_pending_failure_cause_projects_external_code() {
2742 let input = ExternalToolSurfaceInput::MarkPendingFailed {
2743 surface_id: "alpha".to_owned(),
2744 pending_task_sequence: 7,
2745 staged_intent_sequence: 11,
2746 cause: ExternalToolSurfaceFailureCause::PendingFailed,
2747 };
2748
2749 let ExternalToolSurfaceInput::MarkPendingFailed { cause, .. } = input else {
2750 panic!("constructed MarkPendingFailed input");
2751 };
2752 assert_eq!(cause, ExternalToolSurfaceFailureCause::PendingFailed);
2753 assert_eq!(cause.as_str(), "pending_failed");
2754 assert_eq!(
2755 serde_json::to_value(cause).expect("serialize failure cause"),
2756 serde_json::json!("pending_failed")
2757 );
2758
2759 let effect = ExternalToolSurfaceEffect::EmitExternalToolDelta {
2760 surface_id: "alpha".to_owned(),
2761 operation: ExternalToolSurfaceDeltaOperation::Add,
2762 phase: ExternalToolSurfaceDeltaPhase::Failed,
2763 cause: Some(cause),
2764 };
2765 assert!(matches!(
2766 effect,
2767 ExternalToolSurfaceEffect::EmitExternalToolDelta {
2768 cause: Some(ExternalToolSurfaceFailureCause::PendingFailed),
2769 ..
2770 }
2771 ));
2772 }
2773
2774 #[test]
2775 fn peer_terminal_projection_owns_prompt_and_context_key() {
2776 let route_id = "550e8400-e29b-41d4-a716-446655440000";
2777 let route_identity =
2778 PeerResponseTerminalRouteIdentity::parse(route_id).expect("route identity");
2779 let correlation_id =
2780 PeerResponseTerminalCorrelationId::parse("018f6f79-7a82-7c4e-a552-a3b86f9630f1")
2781 .expect("correlation id");
2782 let projection = PeerConversationProjection::ResponseTerminal {
2783 fact: PeerResponseTerminalFact::new(
2784 PeerResponseTerminalSource::new(
2785 Some(
2786 PeerResponseTerminalTransportIdentity::parse("transport-runtime-1")
2787 .expect("transport identity"),
2788 ),
2789 route_identity,
2790 PeerResponseTerminalDisplayIdentity::parse("Analyst")
2791 .expect("display identity"),
2792 ),
2793 correlation_id,
2794 PeerResponseTerminalProjectionStatus::Completed,
2795 PeerResponseTerminalRenderPayload::new(Some(serde_json::json!({
2796 "request_intent": "checksum_token",
2797 "request_subject": "alpha beta gamma",
2798 "token": "birch seventeen"
2799 }))),
2800 ),
2801 };
2802
2803 assert_eq!(
2804 projection.context_key().as_deref(),
2805 Some(
2806 "peer_response_terminal:550e8400-e29b-41d4-a716-446655440000:018f6f79-7a82-7c4e-a552-a3b86f9630f1"
2807 )
2808 );
2809 assert_eq!(
2810 projection.prompt_text(),
2811 "Peer terminal response from Analyst. Request ID: 018f6f79-7a82-7c4e-a552-a3b86f9630f1. Status: completed. Result: {\n \"request_intent\": \"checksum_token\",\n \"request_subject\": \"alpha beta gamma\",\n \"token\": \"birch seventeen\"\n}."
2812 );
2813 }
2814
2815 #[test]
2816 fn peer_terminal_fact_is_structural_projection_only() {
2817 let route_id = "550e8400-e29b-41d4-a716-446655440000";
2818 let route_identity =
2819 PeerResponseTerminalRouteIdentity::parse(route_id).expect("route identity");
2820 let correlation_id =
2821 PeerResponseTerminalCorrelationId::parse("018f6f79-7a82-7c4e-a552-a3b86f9630f1")
2822 .expect("correlation id");
2823
2824 let fact = PeerResponseTerminalFact::new(
2825 PeerResponseTerminalSource::new(
2826 None,
2827 route_identity,
2828 PeerResponseTerminalDisplayIdentity::parse("Analyst").expect("display identity"),
2829 ),
2830 correlation_id,
2831 PeerResponseTerminalProjectionStatus::Cancelled,
2832 PeerResponseTerminalRenderPayload::new(None),
2833 );
2834
2835 assert_eq!(
2836 fact.status,
2837 PeerResponseTerminalProjectionStatus::Cancelled,
2838 "status support is decided by generated admission authority, not fact construction"
2839 );
2840 }
2841
2842 #[test]
2843 fn peer_progress_projection_formats_phase_from_shared_seam() {
2844 let projection = PeerConversationProjection::ResponseProgress {
2845 peer_id: "operator-rt".into(),
2846 request_id: "req-789".into(),
2847 phase: PeerResponseProgressProjectionPhase::PartialResult,
2848 payload: Some(serde_json::json!({ "chunk": "alpha" })),
2849 };
2850
2851 assert_eq!(projection.context_key(), None);
2852 assert_eq!(
2853 projection.prompt_text(),
2854 "Peer response progress from operator-rt. Request ID: req-789. Phase: partial_result. Payload: {\n \"chunk\": \"alpha\"\n}."
2855 );
2856 }
2857
2858 #[test]
2859 fn peer_terminal_context_key_helper_stays_canonical() {
2860 let route_id = "550e8400-e29b-41d4-a716-446655440000";
2861 let route_identity =
2862 PeerResponseTerminalRouteIdentity::parse(route_id).expect("route identity");
2863 let correlation_id =
2864 PeerResponseTerminalCorrelationId::parse("018f6f79-7a82-7c4e-a552-a3b86f9630f1")
2865 .expect("correlation id");
2866 assert_eq!(
2867 PeerResponseTerminalFact::context_key_for(&route_identity, correlation_id),
2868 "peer_response_terminal:550e8400-e29b-41d4-a716-446655440000:018f6f79-7a82-7c4e-a552-a3b86f9630f1"
2869 );
2870 }
2871
2872 #[test]
2873 fn peer_terminal_route_identity_rejects_display_name_alias() {
2874 assert!(matches!(
2875 PeerResponseTerminalRouteIdentity::parse("analyst-rt"),
2876 Err(PeerResponseTerminalFactError::InvalidRouteIdentity)
2877 ));
2878 }
2879
2880 #[test]
2881 fn peer_terminal_fact_round_trips_through_serde() {
2882 let fact = PeerResponseTerminalFact::new(
2886 PeerResponseTerminalSource::parse(
2887 Some("inproc://analyst"),
2888 "550e8400-e29b-41d4-a716-446655440000",
2889 "analyst-rt",
2890 )
2891 .expect("source"),
2892 PeerResponseTerminalCorrelationId::parse("018f6f79-7a82-7c4e-a552-a3b86f9630f1")
2893 .expect("correlation id"),
2894 PeerResponseTerminalProjectionStatus::Completed,
2895 PeerResponseTerminalRenderPayload::new(Some(serde_json::json!({
2896 "request_intent": "checksum_token",
2897 "token": "birch seventeen",
2898 }))),
2899 );
2900
2901 let json = serde_json::to_string(&fact).expect("serialize fact");
2902 let decoded: PeerResponseTerminalFact =
2903 serde_json::from_str(&json).expect("deserialize fact");
2904 assert_eq!(decoded, fact);
2905 assert_eq!(
2906 decoded.context_key(),
2907 "peer_response_terminal:550e8400-e29b-41d4-a716-446655440000:018f6f79-7a82-7c4e-a552-a3b86f9630f1"
2908 );
2909 assert_eq!(
2910 decoded
2911 .render_payload_value()
2912 .and_then(|payload| payload.get("token"))
2913 .and_then(|token| token.as_str()),
2914 Some("birch seventeen")
2915 );
2916 }
2917}