//! Turn state machine algebra and typed event separation.
//!
//! Lucid Code Posture:
//! - Provider reasoning, assistant scratchpad prose, and tool proposals stay distinct typed events.
//! - Diagnostic prints and results are cell outputs; cell prints/results are NEVER speech.
//! - Explicit speech ONLY occurs via `local.send()` passing `delivery.review`.
//! - Single-tool contract: the model has exactly one tool: Python code execution in workbench.
//! - Terminal wait: `await runtime.wait(...)` is a hard host yield and state commit,
//! halting inference cleanly until external wake.
//! - Compositional event fold: transitions form a deterministic algebra.
use serde::{Deserialize, Serialize};
use std::fmt;
/// Status of deliberate speech evaluated through `delivery.review`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SpeechStatus {
Confirmed,
Held,
}
/// Explicit speech receipt emitted ONLY via `local.send()`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpeechReceipt {
pub effect_id: String,
pub status: SpeechStatus,
pub event_id: Option<i64>,
pub reason: Option<String>,
pub text: String,
}
impl SpeechReceipt {
pub fn confirmed(
effect_id: impl Into<String>,
text: impl Into<String>,
event_id: Option<i64>,
) -> Self {
Self {
effect_id: effect_id.into(),
status: SpeechStatus::Confirmed,
event_id,
reason: None,
text: text.into(),
}
}
pub fn held(
effect_id: impl Into<String>,
text: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self {
effect_id: effect_id.into(),
status: SpeechStatus::Held,
event_id: None,
reason: Some(reason.into()),
text: text.into(),
}
}
pub fn is_confirmed(&self) -> bool {
self.status == SpeechStatus::Confirmed
}
pub fn is_held(&self) -> bool {
self.status == SpeechStatus::Held
}
}
/// Diagnostic streams for cell evaluation. Never confused with speech.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CellOutputStream {
Stdout,
Stderr,
Result,
}
/// Diagnostic output record from workbench execution.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CellOutputRecord {
pub stream: CellOutputStream,
pub text: String,
}
impl CellOutputRecord {
pub fn stdout(text: impl Into<String>) -> Self {
Self {
stream: CellOutputStream::Stdout,
text: text.into(),
}
}
pub fn stderr(text: impl Into<String>) -> Self {
Self {
stream: CellOutputStream::Stderr,
text: text.into(),
}
}
pub fn result(text: impl Into<String>) -> Self {
Self {
stream: CellOutputStream::Result,
text: text.into(),
}
}
}
impl From<klbr_runtime::protocol::OutputStream> for CellOutputStream {
fn from(s: klbr_runtime::protocol::OutputStream) -> Self {
match s {
klbr_runtime::protocol::OutputStream::Stdout => Self::Stdout,
klbr_runtime::protocol::OutputStream::Stderr => Self::Stderr,
klbr_runtime::protocol::OutputStream::Result => Self::Result,
}
}
}
impl From<CellOutputStream> for klbr_runtime::protocol::OutputStream {
fn from(s: CellOutputStream) -> Self {
match s {
CellOutputStream::Stdout => Self::Stdout,
CellOutputStream::Stderr => Self::Stderr,
CellOutputStream::Result => Self::Result,
}
}
}
impl From<klbr_runtime::protocol::Output> for CellOutputRecord {
fn from(o: klbr_runtime::protocol::Output) -> Self {
Self {
stream: o.stream.into(),
text: o.text,
}
}
}
/// Hard host yield specification from `await runtime.wait(...)`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct YieldRecord {
pub seconds: Option<f64>,
pub reason: String,
}
impl YieldRecord {
pub fn new(seconds: Option<f64>, reason: impl Into<String>) -> Self {
Self {
seconds,
reason: reason.into(),
}
}
pub fn duration(seconds: f64, reason: impl Into<String>) -> Self {
Self {
seconds: Some(seconds),
reason: reason.into(),
}
}
pub fn until_wake(reason: impl Into<String>) -> Self {
Self {
seconds: None,
reason: reason.into(),
}
}
}
/// Proposed tool call from scratchpad stream (single-tool contract: workbench execution).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolCallProposal {
pub id: String,
pub name: String,
pub arguments: String,
}
impl ToolCallProposal {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
arguments: arguments.into(),
}
}
}
/// Strictly typed scratchpad events streamed directly from model inference.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScratchpadEvent {
ThoughtDelta(String),
ProseDelta(String),
ToolCallStarted,
ToolCallProposed {
id: String,
name: String,
arguments: String,
},
}
impl ScratchpadEvent {
pub fn thought(delta: impl Into<String>) -> Self {
Self::ThoughtDelta(delta.into())
}
pub fn prose(delta: impl Into<String>) -> Self {
Self::ProseDelta(delta.into())
}
pub fn tool_proposal(
id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
Self::ToolCallProposed {
id: id.into(),
name: name.into(),
arguments: arguments.into(),
}
}
}
/// Sealed phase enum capturing the complete lifecycle of a turn.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TurnPhase {
Idle,
Streaming {
step: usize,
},
ExecutingTool {
step: usize,
call_id: String,
},
ToolCompleted {
step: usize,
call_id: String,
},
Yielded(YieldRecord),
Completed,
/// The slice's step allowance ran out. The task is incomplete and the host
/// continues it; this is neither success nor failure.
Exhausted {
step: usize,
max_steps: usize,
},
Failed(String),
Interrupted,
Cancelled,
Timeout,
}
impl TurnPhase {
pub fn name(&self) -> &'static str {
match self {
Self::Idle => "Idle",
Self::Streaming { .. } => "Streaming",
Self::ExecutingTool { .. } => "ExecutingTool",
Self::ToolCompleted { .. } => "ToolCompleted",
Self::Yielded(_) => "Yielded",
Self::Completed => "Completed",
Self::Exhausted { .. } => "Exhausted",
Self::Failed(_) => "Failed",
Self::Interrupted => "Interrupted",
Self::Cancelled => "Cancelled",
Self::Timeout => "Timeout",
}
}
pub fn is_terminal(&self) -> bool {
matches!(
self,
Self::Yielded(_)
| Self::Completed
| Self::Exhausted { .. }
| Self::Failed(_)
| Self::Interrupted
| Self::Cancelled
| Self::Timeout
)
}
pub fn is_active(&self) -> bool {
!self.is_terminal()
}
pub fn is_idle(&self) -> bool {
matches!(self, Self::Idle)
}
pub fn is_streaming(&self) -> bool {
matches!(self, Self::Streaming { .. })
}
pub fn is_executing_tool(&self) -> bool {
matches!(self, Self::ExecutingTool { .. })
}
pub fn is_tool_completed(&self) -> bool {
matches!(self, Self::ToolCompleted { .. })
}
pub fn is_yielded(&self) -> bool {
matches!(self, Self::Yielded(_))
}
pub fn is_exhausted(&self) -> bool {
matches!(self, Self::Exhausted { .. })
}
pub fn is_cancelled(&self) -> bool {
matches!(self, Self::Cancelled)
}
pub fn is_timeout(&self) -> bool {
matches!(self, Self::Timeout)
}
pub fn step(&self) -> Option<usize> {
match self {
Self::Streaming { step }
| Self::ExecutingTool { step, .. }
| Self::ToolCompleted { step, .. } => Some(*step),
_ => None,
}
}
}
/// Typed terminal outcomes for model provider attempts and turns.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderOutcome {
NormalEnd,
ProviderError(String),
Cancelled,
Timeout,
}
pub type TerminalOutcome = ProviderOutcome;
/// State transition and invariant errors.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StateError {
InvalidPhaseTransition {
current: &'static str,
attempted: &'static str,
reason: String,
},
MaxStepsExceeded {
current_step: usize,
max_steps: usize,
},
TerminalStateFrozen {
phase: &'static str,
},
IllegalScratchpadInPhase {
phase: &'static str,
},
IllegalCellOutputInPhase {
phase: &'static str,
},
StepMismatch {
expected: usize,
actual: usize,
},
InvariantViolation(String),
}
impl fmt::Display for StateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidPhaseTransition {
current,
attempted,
reason,
} => {
write!(
f,
"invalid transition from '{current}' to '{attempted}': {reason}"
)
}
Self::MaxStepsExceeded {
current_step,
max_steps,
} => {
write!(f, "maximum steps exceeded ({current_step}/{max_steps})")
}
Self::TerminalStateFrozen { phase } => {
write!(
f,
"state machine is terminal in phase '{phase}' and cannot accept events"
)
}
Self::IllegalScratchpadInPhase { phase } => {
write!(
f,
"cannot record scratchpad events in non-streaming phase '{phase}'"
)
}
Self::IllegalCellOutputInPhase { phase } => {
write!(
f,
"cannot record cell outputs in phase '{phase}'; workbench tool must be executing"
)
}
Self::StepMismatch { expected, actual } => {
write!(f, "step index mismatch: expected {expected}, got {actual}")
}
Self::InvariantViolation(msg) => {
write!(f, "invariant violation: {msg}")
}
}
}
}
impl std::error::Error for StateError {}
/// Atomic event driving state machine transitions.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TurnEvent {
BeginStream,
Scratchpad(ScratchpadEvent),
BeginToolExecution {
call_id: String,
},
ToolExecutionCompleted {
call_id: String,
},
CellOutput(CellOutputRecord),
Speech(SpeechReceipt),
Yielded(YieldRecord),
StepCompleted {
step: usize,
},
/// The slice ended with its allowance spent, not with a finished response.
Exhausted {
step: usize,
max_steps: usize,
},
TurnCompleted,
Failed(String),
Interrupted,
Cancelled,
Timeout,
}
impl TurnEvent {
pub fn begin_stream() -> Self {
Self::BeginStream
}
pub fn thought(delta: impl Into<String>) -> Self {
Self::Scratchpad(ScratchpadEvent::ThoughtDelta(delta.into()))
}
pub fn prose(delta: impl Into<String>) -> Self {
Self::Scratchpad(ScratchpadEvent::ProseDelta(delta.into()))
}
pub fn tool_proposal(
id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
Self::Scratchpad(ScratchpadEvent::ToolCallProposed {
id: id.into(),
name: name.into(),
arguments: arguments.into(),
})
}
pub fn begin_tool(call_id: impl Into<String>) -> Self {
Self::BeginToolExecution {
call_id: call_id.into(),
}
}
pub fn stdout(text: impl Into<String>) -> Self {
Self::CellOutput(CellOutputRecord::stdout(text))
}
pub fn stderr(text: impl Into<String>) -> Self {
Self::CellOutput(CellOutputRecord::stderr(text))
}
pub fn cell_result(text: impl Into<String>) -> Self {
Self::CellOutput(CellOutputRecord::result(text))
}
pub fn speech(receipt: SpeechReceipt) -> Self {
Self::Speech(receipt)
}
pub fn yielded(record: YieldRecord) -> Self {
Self::Yielded(record)
}
pub fn step_completed(step: usize) -> Self {
Self::StepCompleted { step }
}
pub fn completed() -> Self {
Self::TurnCompleted
}
pub fn exhausted(step: usize, max_steps: usize) -> Self {
Self::Exhausted { step, max_steps }
}
pub fn failed(reason: impl Into<String>) -> Self {
Self::Failed(reason.into())
}
pub fn interrupted() -> Self {
Self::Interrupted
}
pub fn tool_completed(call_id: impl Into<String>) -> Self {
Self::ToolExecutionCompleted {
call_id: call_id.into(),
}
}
pub fn cancelled() -> Self {
Self::Cancelled
}
pub fn timeout() -> Self {
Self::Timeout
}
}
/// Invariant-preserving turn state machine.
///
/// Ensures strict separation of:
/// - Scratchpad thoughts and prose (model stream)
/// - Cell outputs (diagnostic stdout/stderr/result)
/// - Speech receipts (deliberate speech via `local.send()`)
/// - Terminal yield (`await runtime.wait(...)`)
#[derive(Debug, Clone)]
pub struct TurnStateMachine {
phase: TurnPhase,
current_step: usize,
max_steps: usize,
}
impl TurnStateMachine {
pub fn new(max_steps: usize) -> Self {
Self {
phase: TurnPhase::Idle,
current_step: 0,
max_steps,
}
}
/// Index of the step this state is on. The request that is in flight is the
/// one being measured, so this is the identity a per-request usage record
/// needs; it advances only when a step actually completes.
pub fn step_index(&self) -> usize {
self.current_step
}
/// Construct and fold a state machine from a stream/iterator of events.
pub fn from_events<I: IntoIterator<Item = TurnEvent>>(
max_steps: usize,
events: I,
) -> Result<Self, StateError> {
let mut sm = Self::new(max_steps);
sm.apply_events(events)?;
Ok(sm)
}
/// Fold multiple events in sequence into the state machine.
pub fn apply_events<I: IntoIterator<Item = TurnEvent>>(
&mut self,
events: I,
) -> Result<(), StateError> {
for event in events {
self.apply_event(event)?;
}
Ok(())
}
/// Primary state machine transition fold: `(State, Event) -> Result<State, StateError>`.
pub fn apply_event(&mut self, event: TurnEvent) -> Result<(), StateError> {
if self.is_terminal() {
return Err(StateError::TerminalStateFrozen {
phase: self.phase.name(),
});
}
match event {
TurnEvent::BeginStream => match &self.phase {
TurnPhase::Idle => {
if self.current_step >= self.max_steps {
self.phase = TurnPhase::Exhausted {
step: self.current_step,
max_steps: self.max_steps,
};
return Err(StateError::MaxStepsExceeded {
current_step: self.current_step,
max_steps: self.max_steps,
});
}
self.phase = TurnPhase::Streaming {
step: self.current_step,
};
}
TurnPhase::ExecutingTool { .. } => {
self.current_step += 1;
if self.current_step >= self.max_steps {
self.phase = TurnPhase::Exhausted {
step: self.current_step,
max_steps: self.max_steps,
};
return Err(StateError::MaxStepsExceeded {
current_step: self.current_step,
max_steps: self.max_steps,
});
}
self.phase = TurnPhase::Streaming {
step: self.current_step,
};
}
phase => {
return Err(StateError::InvalidPhaseTransition {
current: phase.name(),
attempted: "Streaming",
reason: "can only begin stream from Idle or completed tool execution"
.into(),
});
}
},
TurnEvent::Scratchpad(event) => {
// Streamed text is delivered as events; the state machine's job here is the phase
// law and the identifiers, not a second copy of what the caller already has.
if !matches!(self.phase, TurnPhase::Streaming { .. }) {
return Err(StateError::IllegalScratchpadInPhase {
phase: self.phase.name(),
});
}
if let ScratchpadEvent::ToolCallProposed { id, .. } = &event {
if id.trim().is_empty() {
return Err(StateError::InvariantViolation(
"proposed tool call has empty id".into(),
));
}
}
}
TurnEvent::BeginToolExecution { call_id } => {
match &self.phase {
TurnPhase::Streaming { step } | TurnPhase::ToolCompleted { step, .. } => {
self.phase = TurnPhase::ExecutingTool {
step: *step,
call_id,
};
}
phase => {
return Err(StateError::InvalidPhaseTransition {
current: phase.name(),
attempted: "ExecutingTool",
reason: "tool execution can only begin from Streaming or ToolCompleted phase".into(),
});
}
}
}
TurnEvent::ToolExecutionCompleted { call_id } => match &self.phase {
TurnPhase::ExecutingTool {
step,
call_id: active_id,
} => {
if active_id != &call_id {
return Err(StateError::InvalidPhaseTransition {
current: "ExecutingTool",
attempted: "ToolCompleted",
reason: format!(
"cannot complete call_id '{call_id}' while executing '{active_id}'"
),
});
}
self.phase = TurnPhase::ToolCompleted {
step: *step,
call_id,
};
}
phase => {
return Err(StateError::InvalidPhaseTransition {
current: phase.name(),
attempted: "ToolCompleted",
reason: "can only complete tool execution from ExecutingTool phase".into(),
});
}
},
TurnEvent::CellOutput(_) => {
if !matches!(self.phase, TurnPhase::ExecutingTool { .. }) {
return Err(StateError::IllegalCellOutputInPhase {
phase: self.phase.name(),
});
}
}
TurnEvent::Speech(receipt) => {
if !matches!(self.phase, TurnPhase::ExecutingTool { .. }) {
return Err(StateError::InvalidPhaseTransition {
current: self.phase.name(),
attempted: "Speech",
reason: "speech can only occur during tool execution via local.send()"
.into(),
});
}
if receipt.effect_id.trim().is_empty() {
return Err(StateError::InvariantViolation(
"speech receipt has empty effect_id".into(),
));
}
}
TurnEvent::Yielded(record) => {
self.phase = TurnPhase::Yielded(record);
}
TurnEvent::StepCompleted { step } => {
if !matches!(
self.phase,
TurnPhase::Streaming { .. }
| TurnPhase::ExecutingTool { .. }
| TurnPhase::ToolCompleted { .. }
) {
return Err(StateError::InvalidPhaseTransition {
current: self.phase.name(),
attempted: "StepCompleted",
reason: "step can only be completed from Streaming, ExecutingTool, or ToolCompleted phase"
.into(),
});
}
if step != self.current_step {
return Err(StateError::StepMismatch {
expected: self.current_step,
actual: step,
});
}
self.current_step += 1;
if self.current_step >= self.max_steps {
self.phase = TurnPhase::Exhausted {
step: self.current_step,
max_steps: self.max_steps,
};
} else {
self.phase = TurnPhase::Idle;
}
}
TurnEvent::TurnCompleted => {
self.phase = TurnPhase::Completed;
}
TurnEvent::Exhausted { step, max_steps } => {
self.phase = TurnPhase::Exhausted { step, max_steps };
}
TurnEvent::Failed(err) => {
self.phase = TurnPhase::Failed(err);
}
TurnEvent::Interrupted => {
self.phase = TurnPhase::Interrupted;
}
TurnEvent::Cancelled => {
self.phase = TurnPhase::Cancelled;
}
TurnEvent::Timeout => {
self.phase = TurnPhase::Timeout;
}
}
self.check_invariants()
}
/// Verifies core algebraic invariants of the turn state.
pub fn check_invariants(&self) -> Result<(), StateError> {
if self.current_step > self.max_steps {
return Err(StateError::InvariantViolation(format!(
"current_step ({}) exceeds max_steps ({})",
self.current_step, self.max_steps
)));
}
if self.current_step == self.max_steps && !self.is_terminal() {
return Err(StateError::InvariantViolation(format!(
"reached max_steps ({}) but phase is non-terminal: {}",
self.max_steps,
self.phase.name()
)));
}
match &self.phase {
TurnPhase::Streaming { step } => {
if *step != self.current_step {
return Err(StateError::InvariantViolation(format!(
"Streaming phase step ({step}) does not match current_step ({})",
self.current_step
)));
}
}
TurnPhase::ExecutingTool { step, call_id } => {
if *step != self.current_step {
return Err(StateError::InvariantViolation(format!(
"ExecutingTool phase step ({step}) does not match current_step ({})",
self.current_step
)));
}
if call_id.trim().is_empty() {
return Err(StateError::InvariantViolation(
"ExecutingTool has empty call_id".into(),
));
}
}
TurnPhase::ToolCompleted { step, call_id } => {
if *step != self.current_step {
return Err(StateError::InvariantViolation(format!(
"ToolCompleted phase step ({step}) does not match current_step ({})",
self.current_step
)));
}
if call_id.trim().is_empty() {
return Err(StateError::InvariantViolation(
"ToolCompleted has empty call_id".into(),
));
}
}
_ => {}
}
Ok(())
}
// --- Accessors ---
pub fn phase(&self) -> &TurnPhase {
&self.phase
}
pub fn current_step(&self) -> usize {
self.current_step
}
pub fn max_steps(&self) -> usize {
self.max_steps
}
pub fn is_terminal(&self) -> bool {
self.phase.is_terminal()
}
pub fn is_idle(&self) -> bool {
self.phase.is_idle()
}
pub fn is_streaming(&self) -> bool {
self.phase.is_streaming()
}
pub fn is_executing_tool(&self) -> bool {
self.phase.is_executing_tool()
}
pub fn is_yielded(&self) -> bool {
self.phase.is_yielded()
}
pub fn yield_record(&self) -> Option<&YieldRecord> {
match &self.phase {
TurnPhase::Yielded(rec) => Some(rec),
_ => None,
}
}
// --- Transition Combinators ---
pub fn begin_stream(&mut self) -> Result<(), StateError> {
self.apply_event(TurnEvent::BeginStream)
}
pub fn record_scratchpad(&mut self, event: ScratchpadEvent) -> Result<(), StateError> {
self.apply_event(TurnEvent::Scratchpad(event))
}
pub fn propose_tool_call(
&mut self,
id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Result<(), StateError> {
self.record_scratchpad(ScratchpadEvent::ToolCallProposed {
id: id.into(),
name: name.into(),
arguments: arguments.into(),
})
}
pub fn begin_tool_execution(&mut self, call_id: impl Into<String>) -> Result<(), StateError> {
self.apply_event(TurnEvent::BeginToolExecution {
call_id: call_id.into(),
})
}
pub fn record_cell_output(&mut self, output: CellOutputRecord) -> Result<(), StateError> {
self.apply_event(TurnEvent::CellOutput(output))
}
pub fn record_stdout(&mut self, text: impl Into<String>) -> Result<(), StateError> {
self.record_cell_output(CellOutputRecord::stdout(text))
}
pub fn record_stderr(&mut self, text: impl Into<String>) -> Result<(), StateError> {
self.record_cell_output(CellOutputRecord::stderr(text))
}
pub fn record_cell_result(&mut self, text: impl Into<String>) -> Result<(), StateError> {
self.record_cell_output(CellOutputRecord::result(text))
}
pub fn record_speech(&mut self, receipt: SpeechReceipt) -> Result<(), StateError> {
self.apply_event(TurnEvent::Speech(receipt))
}
pub fn transition_yield(&mut self, record: YieldRecord) -> Result<(), StateError> {
self.apply_event(TurnEvent::Yielded(record))
}
pub fn complete_tool_execution(
&mut self,
call_id: impl Into<String>,
) -> Result<(), StateError> {
self.apply_event(TurnEvent::ToolExecutionCompleted {
call_id: call_id.into(),
})
}
pub fn cancel(&mut self) -> Result<(), StateError> {
self.apply_event(TurnEvent::Cancelled)
}
pub fn timeout(&mut self) -> Result<(), StateError> {
self.apply_event(TurnEvent::Timeout)
}
/// True when the turn ended because its step allowance ran out.
pub fn is_exhausted(&self) -> bool {
self.phase.is_exhausted()
}
pub fn step_completed(&mut self) -> Result<bool, StateError> {
self.apply_event(TurnEvent::StepCompleted {
step: self.current_step,
})?;
Ok(!self.is_terminal())
}
pub fn complete_turn(&mut self) -> Result<(), StateError> {
self.apply_event(TurnEvent::TurnCompleted)
}
pub fn fail_turn(&mut self, error: impl Into<String>) -> Result<(), StateError> {
self.apply_event(TurnEvent::Failed(error.into()))
}
pub fn interrupt(&mut self) -> Result<(), StateError> {
self.apply_event(TurnEvent::Interrupted)
}
}