use crate::{
contracts::{CapabilityToken, TaskIntent},
kernel::{KernelDispatch, LoongKernel},
policy::PolicyEngine,
};
use loong_contracts::{Fault, TaskState};
pub struct TaskSupervisor {
state: TaskState,
}
impl TaskSupervisor {
pub fn new(intent: TaskIntent) -> Self {
Self {
state: TaskState::Runnable(intent),
}
}
pub fn state(&self) -> &TaskState {
&self.state
}
pub fn is_runnable(&self) -> bool {
matches!(self.state, TaskState::Runnable(_))
}
fn take_state(&self) -> TaskState {
self.state.clone()
}
pub async fn execute<P: PolicyEngine>(
&mut self,
kernel: &LoongKernel<P>,
pack_id: &str,
token: &CapabilityToken,
) -> Result<KernelDispatch, Fault> {
let intent = match &self.state {
TaskState::Runnable(intent) => intent.clone(),
TaskState::InSend { .. }
| TaskState::InReply { .. }
| TaskState::Completed(_)
| TaskState::Faulted(_) => {
return Err(Fault::ProtocolViolation {
detail: "task is not in Runnable state".to_owned(),
});
}
};
let taken = self.take_state();
self.state = taken
.transition_to_in_send()
.map_err(|detail| Fault::ProtocolViolation { detail })?;
let taken = self.take_state();
self.state = taken
.transition_to_in_reply()
.map_err(|detail| Fault::ProtocolViolation { detail })?;
match kernel.execute_task(pack_id, token, intent).await {
Ok(dispatch) => {
let taken = self.take_state();
self.state = taken
.transition_to_completed(dispatch.outcome.clone())
.map_err(|detail| Fault::ProtocolViolation { detail })?;
Ok(dispatch)
}
Err(kernel_err) => {
let fault = Fault::from_kernel_error(kernel_err);
let taken = self.take_state();
self.state = taken.transition_to_faulted(fault.clone());
Err(fault)
}
}
}
#[cfg(test)]
pub fn force_state(&mut self, state: TaskState) {
self.state = state;
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use serde_json::json;
use super::TaskSupervisor;
use crate::contracts::{Capability, TaskIntent};
use loong_contracts::{Fault, TaskState};
fn sample_intent() -> TaskIntent {
TaskIntent {
task_id: "supervised-guard".to_owned(),
objective: "exercise guarded transition".to_owned(),
required_capabilities: BTreeSet::from([Capability::InvokeTool]),
payload: json!({}),
}
}
#[test]
fn take_state_does_not_poison_supervisor_when_transition_is_rejected() {
let supervisor = TaskSupervisor::new(sample_intent());
let error = supervisor
.take_state()
.transition_to_in_reply()
.expect_err("Runnable cannot transition directly to InReply");
assert!(error.contains("cannot move to InReply"));
assert!(matches!(
supervisor.state(),
TaskState::Runnable(intent) if intent.task_id == "supervised-guard"
));
assert!(!matches!(
supervisor.state(),
TaskState::Faulted(Fault::ProtocolViolation { .. })
));
}
}