use compact_str::CompactString;
use serde::{Deserialize, Serialize};
use crate::types::agent::{AgentIsolation, AgentRole, ContextInheritance};
use crate::types::result::{SubAgentResult, TerminationReason};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProcessState {
Running,
Joined,
Failed,
}
impl ProcessState {
pub fn label(self) -> &'static str {
match self {
Self::Running => "running",
Self::Joined => "joined",
Self::Failed => "failed",
}
}
}
fn process_state_of(state: crate::scheduler::tcb::TaskState) -> ProcessState {
use crate::scheduler::tcb::TaskState;
match state {
TaskState::Done(TerminationReason::Completed) => ProcessState::Joined,
TaskState::Done(_) => ProcessState::Failed,
_ => ProcessState::Running,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProcess {
pub agent_id: CompactString,
pub parent_session_id: CompactString,
pub role: AgentRole,
pub isolation: AgentIsolation,
pub context_inheritance: ContextInheritance,
pub state: ProcessState,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub permitted_capability_ids: Vec<CompactString>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<SubAgentResult>,
}
impl AgentProcess {
pub fn from_tcb(tcb: &crate::scheduler::tcb::Tcb) -> Option<Self> {
let info = tcb.proc.as_ref()?;
Some(Self {
agent_id: tcb.id.clone(),
parent_session_id: info.parent_session_id.clone(),
role: info.role,
isolation: info.isolation,
context_inheritance: info.context_inheritance,
state: process_state_of(tcb.state),
permitted_capability_ids: tcb.caps.clone(),
result: info.result.clone(),
})
}
pub fn result_termination_label(&self) -> Option<&'static str> {
let result = self.result.as_ref()?;
Some(match result.result.termination {
TerminationReason::Completed => "completed",
TerminationReason::MaxTurns => "max_turns",
TerminationReason::TokenBudget => "token_budget",
TerminationReason::Timeout => "timeout",
TerminationReason::UserAbort => "user_abort",
TerminationReason::Error => "error",
TerminationReason::MilestoneExceeded => "milestone_exceeded",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scheduler::policy::SchedulerBudget;
use crate::scheduler::tcb::{Tcb, TaskState};
use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec, IsolationManifest};
use crate::types::capability::CapabilityManifest;
fn child_tcb(id: &str) -> Tcb {
let spec = AgentRunSpec::new(
AgentIdentity::sub_agent(id, &format!("{id}-session")),
AgentRole::Implement,
"do work",
);
let manifest = IsolationManifest::from_spec(&spec, "parent-sess", &CapabilityManifest::new());
Tcb::spawned(&manifest, SchedulerBudget::default())
}
#[test]
fn from_tcb_is_none_for_root_task() {
let root = Tcb::root("root", SchedulerBudget::default());
assert!(AgentProcess::from_tcb(&root).is_none());
}
#[test]
fn from_tcb_reconstructs_running_process() {
let tcb = child_tcb("worker");
let p = AgentProcess::from_tcb(&tcb).expect("child reconstructs a process");
assert_eq!(p.agent_id.as_str(), "worker");
assert_eq!(p.parent_session_id.as_str(), "parent-sess");
assert_eq!(p.role, AgentRole::Implement);
assert_eq!(p.state, ProcessState::Running);
assert!(p.result.is_none());
}
#[test]
fn process_state_of_maps_terminal_task_states() {
assert_eq!(process_state_of(TaskState::Running), ProcessState::Running);
assert_eq!(
process_state_of(TaskState::Done(TerminationReason::Completed)),
ProcessState::Joined
);
assert_eq!(
process_state_of(TaskState::Done(TerminationReason::Error)),
ProcessState::Failed
);
assert_eq!(
process_state_of(TaskState::Done(TerminationReason::Timeout)),
ProcessState::Failed
);
}
}