use std::sync::Arc;
use meerkat_contracts::wire::supervisor_bridge::{
BridgeDeliveryRejectionCause, BridgeHostRuntimeIncarnation, BridgeMemberIncarnation,
BridgeTrackedInputCancelOutcome, BridgeTurnOutcomeAck, BridgeTurnOutcomeRecord,
WireFlowTurnOutcome,
};
use meerkat_core::event::{AgentEvent, EventEnvelope};
use meerkat_core::service::SessionHistoryPage;
use meerkat_core::time_compat::Duration;
use meerkat_core::types::SessionId;
use crate::completion::CompletionHandle;
pub const MAX_TURN_OUTCOME_RECORD_BYTES: usize = 64 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum MemberObservationError {
#[error("stale member observation residency: {reason}")]
StaleIncarnation { reason: String },
#[error("cursor overran the retained window (watermark {watermark}, generation {generation})")]
StaleCursor { watermark: u64, generation: u64 },
#[error("cursor generation {requested} is ahead of current generation {current}")]
FutureGenerationCursor { requested: u64, current: u64 },
#[error("member observation unavailable: {reason}")]
Unavailable { reason: String },
#[error("turn outcome record is {encoded_bytes} bytes (maximum {max_bytes})")]
OutcomeRecordTooLarge {
encoded_bytes: usize,
max_bytes: usize,
},
#[error("member observation internal fault: {reason}")]
Internal { reason: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemberObservationCursor {
Tail,
At { generation: u64, seq: u64 },
}
#[derive(Debug)]
pub struct MemberEventsWindow {
pub runtime_incarnation: BridgeHostRuntimeIncarnation,
pub generation: u64,
pub fence_token: u64,
pub rows: Vec<(u64, EventEnvelope<AgentEvent>)>,
pub from_seq: u64,
pub next_seq: u64,
pub watermark: u64,
pub turn_outcomes: Vec<BridgeTurnOutcomeRecord>,
pub outcomes_complete: bool,
}
#[derive(Debug)]
pub struct MemberHistoryWindow {
pub generation: u64,
pub page: SessionHistoryPage,
}
#[derive(Debug, Clone, Copy)]
pub struct MemberEventsPollRequest<'a> {
pub expected_member: &'a BridgeMemberIncarnation,
pub cursor: MemberObservationCursor,
pub max: u32,
pub wait: Duration,
pub outcome_acks: &'a [BridgeTurnOutcomeAck],
pub max_outcomes: u32,
}
pub struct DirectedTurnWindow {
pub expected_member: meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation,
pub subscription: meerkat_core::comms::EventStream,
pub window_start: u64,
pub generation: u64,
pub fence_token: u64,
pub input_id: String,
pub tracking: DirectedTurnTracking,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectedTurnAdmissionRequest {
pub expected_member: BridgeMemberIncarnation,
pub window_start: u64,
pub generation: u64,
pub fence_token: u64,
pub input_id: String,
}
impl DirectedTurnWindow {
#[must_use]
pub fn admission_request(&self) -> DirectedTurnAdmissionRequest {
DirectedTurnAdmissionRequest {
expected_member: self.expected_member.clone(),
window_start: self.window_start,
generation: self.generation,
fence_token: self.fence_token,
input_id: self.input_id.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectedTurnTracking {
PendingFresh,
PendingReplay,
TerminalReplay,
}
pub struct DirectedTurnAdmissionPermit {
_guard: Box<dyn Send>,
}
impl DirectedTurnAdmissionPermit {
#[doc(hidden)]
pub fn new(guard: impl Send + 'static) -> Self {
Self {
_guard: Box::new(guard),
}
}
}
pub enum DirectedTurnAdmissionDecision {
Admit(DirectedTurnAdmissionPermit),
TerminalReplay,
}
impl std::fmt::Debug for DirectedTurnWindow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DirectedTurnWindow")
.field("expected_member", &self.expected_member)
.field("window_start", &self.window_start)
.field("generation", &self.generation)
.field("fence_token", &self.fence_token)
.field("input_id", &self.input_id)
.field("tracking", &self.tracking)
.finish_non_exhaustive()
}
}
pub struct DirectedTurnAdmission {
pub input_id: String,
pub window: DirectedTurnWindow,
pub completion_handle: Option<CompletionHandle>,
pub journal: Arc<dyn TrackedTurnJournal>,
}
#[derive(Debug, thiserror::Error)]
#[error("tracked turn unsupported: {detail}")]
pub struct DirectedTurnReject {
pub cause: BridgeDeliveryRejectionCause,
pub detail: String,
pub definite_no_effect: bool,
}
impl DirectedTurnReject {
#[must_use]
pub fn unsupported(detail: impl Into<String>) -> Self {
let detail = detail.into();
Self {
cause: BridgeDeliveryRejectionCause::TurnDirectiveUnsupported {
detail: detail.clone(),
},
detail,
definite_no_effect: true,
}
}
#[must_use]
pub fn ambiguous(detail: impl Into<String>) -> Self {
let detail = detail.into();
Self {
cause: BridgeDeliveryRejectionCause::TurnDirectiveUnsupported {
detail: detail.clone(),
},
detail,
definite_no_effect: false,
}
}
#[must_use]
pub fn outcome_journal_full(retained: usize, limit: usize) -> Self {
let retained = u32::try_from(retained).unwrap_or(u32::MAX);
let limit = u32::try_from(limit).unwrap_or(u32::MAX);
Self {
cause: BridgeDeliveryRejectionCause::OutcomeJournalFull { retained, limit },
detail: format!(
"directed-turn outcome journal is full ({retained}/{limit}); consume and acknowledge outcomes before submitting more work"
),
definite_no_effect: true,
}
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait MemberObservationHost: Send + Sync {
async fn member_generation(&self, session: &SessionId) -> Result<u64, MemberObservationError>;
async fn read_history(
&self,
session: &SessionId,
from_index: Option<u64>,
limit: Option<u32>,
) -> Result<MemberHistoryWindow, MemberObservationError>;
async fn poll_events(
&self,
session: &SessionId,
request: MemberEventsPollRequest<'_>,
) -> Result<MemberEventsWindow, MemberObservationError>;
async fn open_directed_turn_window(
&self,
session: &SessionId,
expected_member: &BridgeMemberIncarnation,
input_id: &str,
) -> Result<DirectedTurnWindow, DirectedTurnReject>;
async fn cancel_directed_turn_window(
&self,
session: &SessionId,
window: DirectedTurnWindow,
) -> Result<(), DirectedTurnReject>;
async fn lock_and_revalidate_directed_turn_admission(
&self,
session: &SessionId,
request: DirectedTurnAdmissionRequest,
) -> Result<DirectedTurnAdmissionDecision, DirectedTurnReject> {
let _ = (session, request);
Err(DirectedTurnReject::ambiguous(
"member observation host has no exact-key admission lock".to_string(),
))
}
async fn cancel_tracked_member_input(
&self,
session: &SessionId,
expected_member: &BridgeMemberIncarnation,
input_id: &str,
) -> Result<BridgeTrackedInputCancelOutcome, MemberObservationError> {
let _ = (session, expected_member, input_id);
Err(MemberObservationError::Unavailable {
reason: "member observation host has no tracked-input cancellation authority"
.to_string(),
})
}
async fn admit_directed_turn(
&self,
session: &SessionId,
admission: DirectedTurnAdmission,
) -> Result<(), DirectedTurnReject>;
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait DurableEventLogRead: Send + Sync {
async fn read_from(
&self,
session: &SessionId,
from_seq: u64,
max_rows: usize,
) -> Result<Option<Vec<(u64, EventEnvelope<AgentEvent>)>>, MemberObservationError>;
async fn latest_seq(&self, session: &SessionId) -> Result<Option<u64>, MemberObservationError>;
}
#[derive(Debug, Clone)]
pub struct TrackedTurnOutcomeRecord {
pub input_id: String,
pub generation: u64,
pub fence_token: u64,
pub terminal_seq: u64,
pub outcome: WireFlowTurnOutcome,
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait TrackedTurnJournal: Send + Sync {
fn member_incarnation(
&self,
) -> &meerkat_contracts::wire::supervisor_bridge::BridgeMemberIncarnation;
fn member_generation(&self) -> u64;
fn member_fence_token(&self) -> u64;
async fn record_turn_outcome(
&self,
record: TrackedTurnOutcomeRecord,
) -> Result<(), MemberObservationError>;
}