use crate::types::{
AuditEvent, BriefRecord, ClosureDecision, ClosureOutcome, RuntimePosture, TurnTerminalKind,
WaitingReason, WorkItemSchedulingState, WorkReactivationSignal,
};
#[derive(Debug, Clone, Default)]
pub(super) struct ClosureFacts {
pub(super) runtime_error: bool,
pub(super) awaiting_operator_input: bool,
pub(super) active_blocking_tasks: usize,
pub(super) active_waiting_intents: usize,
pub(super) active_agent_waiting_intents: usize,
pub(super) active_timers: usize,
pub(super) current_work_item_scheduling_state: Option<WorkItemSchedulingState>,
pub(super) work_signal: Option<WorkReactivationSignal>,
pub(super) turn_started: bool,
pub(super) turn_in_progress: bool,
pub(super) turn_terminal_kind: Option<TurnTerminalKind>,
pub(super) runtime_posture: Option<RuntimePosture>,
}
pub(super) fn derive_closure_decision(facts: &ClosureFacts) -> ClosureDecision {
let runtime_posture = facts.runtime_posture.unwrap_or(RuntimePosture::Awake);
let mut evidence = Vec::new();
if facts.runtime_error {
evidence.push("runtime_error".to_string());
}
if facts.awaiting_operator_input {
evidence.push("awaiting_operator_input_signal".to_string());
}
if facts.active_blocking_tasks > 0 {
evidence.push(format!(
"active_blocking_tasks={}",
facts.active_blocking_tasks
));
}
if facts.active_waiting_intents > 0 {
evidence.push(format!(
"active_waiting_intents={}",
facts.active_waiting_intents
));
}
if facts.active_agent_waiting_intents > 0 {
evidence.push(format!(
"active_agent_waiting_intents={}",
facts.active_agent_waiting_intents
));
}
if facts.active_timers > 0 {
evidence.push(format!("active_timers={}", facts.active_timers));
}
if let Some(state) = facts.current_work_item_scheduling_state {
evidence.push(format!("current_work_item_scheduling_state={state:?}"));
}
if let Some(work_signal) = facts.work_signal.as_ref() {
evidence.push(format!(
"work_reactivation_mode={:?}",
work_signal.reactivation_mode
));
evidence.push(format!("work_item_state={:?}", work_signal.state));
evidence.push(format!("work_item_id={}", work_signal.work_item_id));
}
if runtime_posture == RuntimePosture::Sleeping {
evidence.push("runtime_sleeping".to_string());
}
if facts.runtime_error {
return ClosureDecision {
outcome: ClosureOutcome::Failed,
waiting_reason: None,
work_signal: None,
runtime_posture,
evidence,
};
}
if facts.turn_in_progress {
evidence.push("turn_in_progress".to_string());
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: Some(WaitingReason::AwaitingExternalChange),
work_signal: None,
runtime_posture,
evidence,
};
}
if let Some(kind) = facts.turn_terminal_kind {
if kind.is_failure() {
let marker = match kind {
TurnTerminalKind::Aborted => "turn_terminal_aborted",
TurnTerminalKind::BaselineOverBudget => "turn_terminal_baseline_over_budget",
TurnTerminalKind::DeferredToFallback => "turn_terminal_deferred_to_fallback",
TurnTerminalKind::ProviderFailedNeedsRecovery => {
"turn_terminal_provider_failed_needs_recovery"
}
TurnTerminalKind::Completed => unreachable!("completed is not a failure"),
};
evidence.push(marker.to_string());
return ClosureDecision {
outcome: ClosureOutcome::Failed,
waiting_reason: None,
work_signal: None,
runtime_posture,
evidence,
};
}
}
if facts.turn_started && facts.turn_terminal_kind.is_none() {
evidence.push("turn_terminal_missing".to_string());
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: Some(WaitingReason::AwaitingExternalChange),
work_signal: None,
runtime_posture,
evidence,
};
}
if let Some(work_signal) = facts.work_signal.clone() {
evidence.push("runnable_work_present".to_string());
return ClosureDecision {
outcome: ClosureOutcome::Continuable,
waiting_reason: None,
work_signal: Some(work_signal),
runtime_posture,
evidence,
};
}
if facts.awaiting_operator_input {
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: Some(WaitingReason::AwaitingOperatorInput),
work_signal: None,
runtime_posture,
evidence,
};
}
if facts.active_agent_waiting_intents > 0 {
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: Some(WaitingReason::AwaitingExternalChange),
work_signal: None,
runtime_posture,
evidence,
};
}
if facts.active_timers > 0 {
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: Some(WaitingReason::AwaitingTimer),
work_signal: None,
runtime_posture,
evidence,
};
}
match facts.current_work_item_scheduling_state {
Some(WorkItemSchedulingState::WaitingTask) => {
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: Some(WaitingReason::AwaitingTaskResult),
work_signal: None,
runtime_posture,
evidence,
};
}
Some(WorkItemSchedulingState::WaitingExternal) => {
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: Some(WaitingReason::AwaitingExternalChange),
work_signal: None,
runtime_posture,
evidence,
};
}
Some(WorkItemSchedulingState::WaitingTimer) => {
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: Some(WaitingReason::AwaitingTimer),
work_signal: None,
runtime_posture,
evidence,
};
}
Some(WorkItemSchedulingState::WaitingOperator) => {
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: Some(WaitingReason::AwaitingOperatorInput),
work_signal: None,
runtime_posture,
evidence,
};
}
Some(WorkItemSchedulingState::WaitingSystem) => {
return ClosureDecision {
outcome: ClosureOutcome::Waiting,
waiting_reason: None,
work_signal: None,
runtime_posture,
evidence,
};
}
_ => {}
}
if matches!(facts.turn_terminal_kind, Some(TurnTerminalKind::Completed)) {
evidence.push("turn_terminal_completed".to_string());
}
evidence.push("no_waiting_condition".to_string());
ClosureDecision {
outcome: ClosureOutcome::Completed,
waiting_reason: None,
work_signal: None,
runtime_posture,
evidence,
}
}
pub(crate) fn runtime_error_active(events: &[AuditEvent], briefs: &[BriefRecord]) -> bool {
let latest_runtime_error = events
.iter()
.filter(|event| event.kind == "runtime_error")
.map(|event| event.created_at)
.max();
let latest_success_result_brief = briefs
.iter()
.filter(|brief| brief.kind.is_success())
.map(|brief| brief.created_at)
.max();
match (latest_runtime_error, latest_success_result_brief) {
(Some(error_at), Some(result_at)) => error_at > result_at,
(Some(_), None) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{WorkItemState, WorkReactivationMode};
use chrono::{Duration, Utc};
fn facts() -> ClosureFacts {
ClosureFacts::default()
}
#[test]
fn runtime_error_overrides_every_waiting_condition() {
let decision = derive_closure_decision(&ClosureFacts {
runtime_error: true,
awaiting_operator_input: true,
active_blocking_tasks: 1,
active_waiting_intents: 1,
active_timers: 1,
turn_in_progress: true,
runtime_posture: Some(RuntimePosture::Sleeping),
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Failed);
assert_eq!(decision.waiting_reason, None);
assert_eq!(decision.runtime_posture, RuntimePosture::Sleeping);
}
#[test]
fn awaiting_operator_input_wins_over_other_waiting_conditions() {
let decision = derive_closure_decision(&ClosureFacts {
awaiting_operator_input: true,
active_blocking_tasks: 1,
active_waiting_intents: 1,
active_timers: 1,
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Waiting);
assert_eq!(
decision.waiting_reason,
Some(WaitingReason::AwaitingOperatorInput)
);
}
#[test]
fn blocking_tasks_are_metadata_not_waiting_reason() {
let decision = derive_closure_decision(&ClosureFacts {
active_blocking_tasks: 2,
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Completed);
assert_eq!(decision.waiting_reason, None);
assert!(decision
.evidence
.iter()
.any(|item| item == "active_blocking_tasks=2"));
}
#[test]
fn waiting_intents_map_to_awaiting_external_change() {
let decision = derive_closure_decision(&ClosureFacts {
active_waiting_intents: 1,
active_agent_waiting_intents: 1,
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Waiting);
assert_eq!(
decision.waiting_reason,
Some(WaitingReason::AwaitingExternalChange)
);
}
#[test]
fn timers_map_to_awaiting_timer() {
let decision = derive_closure_decision(&ClosureFacts {
active_timers: 1,
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Waiting);
assert_eq!(decision.waiting_reason, Some(WaitingReason::AwaitingTimer));
}
#[test]
fn sleeping_posture_is_preserved_independently_from_outcome() {
let waiting = derive_closure_decision(&ClosureFacts {
active_timers: 1,
runtime_posture: Some(RuntimePosture::Sleeping),
..facts()
});
let completed = derive_closure_decision(&ClosureFacts {
turn_terminal_kind: Some(TurnTerminalKind::Completed),
runtime_posture: Some(RuntimePosture::Sleeping),
..facts()
});
assert_eq!(waiting.runtime_posture, RuntimePosture::Sleeping);
assert_eq!(completed.runtime_posture, RuntimePosture::Sleeping);
}
#[test]
fn aborted_turn_maps_to_failed_closure() {
let decision = derive_closure_decision(&ClosureFacts {
turn_terminal_kind: Some(TurnTerminalKind::Aborted),
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Failed);
assert_eq!(decision.waiting_reason, None);
}
#[test]
fn baseline_over_budget_turn_maps_to_failed_closure() {
let decision = derive_closure_decision(&ClosureFacts {
turn_terminal_kind: Some(TurnTerminalKind::BaselineOverBudget),
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Failed);
assert_eq!(decision.waiting_reason, None);
assert!(decision
.evidence
.iter()
.any(|item| item == "turn_terminal_baseline_over_budget"));
}
#[test]
fn completed_turn_with_no_waiting_conditions_maps_to_completed_closure() {
let decision = derive_closure_decision(&ClosureFacts {
turn_terminal_kind: Some(TurnTerminalKind::Completed),
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Completed);
assert_eq!(decision.waiting_reason, None);
assert!(decision
.evidence
.iter()
.any(|item| item == "turn_terminal_completed"));
}
#[test]
fn runnable_active_work_maps_to_continuable_closure() {
let decision = derive_closure_decision(&ClosureFacts {
work_signal: Some(WorkReactivationSignal {
work_item_id: "work-1".into(),
state: WorkItemState::Open,
reactivation_mode: WorkReactivationMode::ContinueActive,
}),
turn_terminal_kind: Some(TurnTerminalKind::Completed),
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Continuable);
assert_eq!(decision.waiting_reason, None);
assert_eq!(
decision.work_signal,
Some(WorkReactivationSignal {
work_item_id: "work-1".into(),
state: WorkItemState::Open,
reactivation_mode: WorkReactivationMode::ContinueActive,
})
);
}
#[test]
fn runnable_work_overrides_unrelated_agent_waiting_intent() {
let decision = derive_closure_decision(&ClosureFacts {
active_waiting_intents: 1,
active_agent_waiting_intents: 1,
work_signal: Some(WorkReactivationSignal {
work_item_id: "work-1".into(),
state: WorkItemState::Open,
reactivation_mode: WorkReactivationMode::ContinueActive,
}),
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Continuable);
assert_eq!(decision.waiting_reason, None);
assert_eq!(
decision
.work_signal
.as_ref()
.map(|signal| signal.work_item_id.as_str()),
Some("work-1")
);
}
#[test]
fn blocking_tasks_do_not_override_runnable_work() {
let decision = derive_closure_decision(&ClosureFacts {
active_blocking_tasks: 1,
work_signal: Some(WorkReactivationSignal {
work_item_id: "work-1".into(),
state: WorkItemState::Open,
reactivation_mode: WorkReactivationMode::ContinueActive,
}),
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Continuable);
assert_eq!(decision.waiting_reason, None);
assert!(decision
.evidence
.iter()
.any(|item| item == "active_blocking_tasks=1"));
}
#[test]
fn started_turn_without_terminal_record_maps_to_waiting() {
let decision = derive_closure_decision(&ClosureFacts {
turn_started: true,
turn_terminal_kind: None,
..facts()
});
assert_eq!(decision.outcome, ClosureOutcome::Waiting);
assert_eq!(
decision.waiting_reason,
Some(WaitingReason::AwaitingExternalChange)
);
}
#[test]
fn runtime_error_active_clears_when_later_success_brief_exists() {
let base = Utc::now();
let older_error = AuditEvent {
created_at: base,
..AuditEvent::new("runtime_error", serde_json::json!({}))
};
let newer_success = BriefRecord {
created_at: base + Duration::milliseconds(1),
work_item_id: None,
..BriefRecord::new(
"default",
crate::types::BriefKind::Result,
"done",
None,
None,
)
};
assert!(!runtime_error_active(&[older_error], &[newer_success]));
}
#[test]
fn runtime_error_active_stays_true_without_later_success_brief() {
let error = AuditEvent::new("runtime_error", serde_json::json!({}));
assert!(runtime_error_active(&[error], &[]));
}
}