Skip to main content

deepstrike_core/proc/
mod.rs

1use compact_str::CompactString;
2use serde::{Deserialize, Serialize};
3
4use crate::types::agent::{AgentIsolation, AgentRole, ContextInheritance};
5use crate::types::result::{SubAgentResult, TerminationReason};
6
7/// Kernel-owned lifecycle state for a spawned agent process.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ProcessState {
11    Running,
12    Joined,
13    Failed,
14}
15
16impl ProcessState {
17    pub fn label(self) -> &'static str {
18        match self {
19            Self::Running => "running",
20            Self::Joined => "joined",
21            Self::Failed => "failed",
22        }
23    }
24}
25
26/// Project a task's schedulability onto the coarser process lifecycle exposed in the
27/// `AgentProcess` view. Inverse of `impl From<ProcessState> for TaskState`: a child task is
28/// `Joined` once it completed successfully, `Failed` on any other terminal reason, else `Running`.
29fn process_state_of(state: crate::scheduler::tcb::TaskState) -> ProcessState {
30    use crate::scheduler::tcb::TaskState;
31    match state {
32        TaskState::Done(TerminationReason::Completed) => ProcessState::Joined,
33        TaskState::Done(_) => ProcessState::Failed,
34        _ => ProcessState::Running,
35    }
36}
37
38/// A sub-agent process registered by the kernel.
39///
40/// The kernel owns only declarative lifecycle state. Host execution,
41/// worktree/remote isolation, I/O, and concurrency remain SDK concerns.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct AgentProcess {
44    pub agent_id: CompactString,
45    pub parent_session_id: CompactString,
46    pub role: AgentRole,
47    pub isolation: AgentIsolation,
48    pub context_inheritance: ContextInheritance,
49    pub state: ProcessState,
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub permitted_capability_ids: Vec<CompactString>,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub result: Option<SubAgentResult>,
54}
55
56impl AgentProcess {
57    /// Reconstruct an `AgentProcess` from a child [`crate::scheduler::tcb::Tcb`] (M1 收口).
58    ///
59    /// Returns `None` for the root task (no `proc`). This is the bridge that makes the
60    /// `AgentProcess` records a *derived view* over the kernel's `TaskTable`: the sub-agent's
61    /// declarative identity lives on the TCB, and the `AgentProcess` shape — the SDK ABI /
62    /// session-log contract — is rebuilt on demand without a second source of truth.
63    pub fn from_tcb(tcb: &crate::scheduler::tcb::Tcb) -> Option<Self> {
64        let info = tcb.proc.as_ref()?;
65        Some(Self {
66            agent_id: tcb.id.clone(),
67            parent_session_id: info.parent_session_id.clone(),
68            role: info.role,
69            isolation: info.isolation,
70            context_inheritance: info.context_inheritance,
71            state: process_state_of(tcb.state),
72            permitted_capability_ids: tcb.caps.clone(),
73            result: info.result.clone(),
74        })
75    }
76
77    pub fn result_termination_label(&self) -> Option<&'static str> {
78        let result = self.result.as_ref()?;
79        Some(match result.result.termination {
80            TerminationReason::Completed => "completed",
81            TerminationReason::MaxTurns => "max_turns",
82            TerminationReason::TokenBudget => "token_budget",
83            TerminationReason::Timeout => "timeout",
84            TerminationReason::UserAbort => "user_abort",
85            TerminationReason::Error => "error",
86            TerminationReason::MilestoneExceeded => "milestone_exceeded",
87        })
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::scheduler::policy::SchedulerBudget;
95    use crate::scheduler::tcb::{Tcb, TaskState};
96    use crate::types::agent::{AgentIdentity, AgentRole, AgentRunSpec, IsolationManifest};
97    use crate::types::capability::CapabilityManifest;
98
99    fn child_tcb(id: &str) -> Tcb {
100        let spec = AgentRunSpec::new(
101            AgentIdentity::sub_agent(id, &format!("{id}-session")),
102            AgentRole::Implement,
103            "do work",
104        );
105        let manifest = IsolationManifest::from_spec(&spec, "parent-sess", &CapabilityManifest::new());
106        Tcb::spawned(&manifest, SchedulerBudget::default())
107    }
108
109    #[test]
110    fn from_tcb_is_none_for_root_task() {
111        let root = Tcb::root("root", SchedulerBudget::default());
112        assert!(AgentProcess::from_tcb(&root).is_none());
113    }
114
115    #[test]
116    fn from_tcb_reconstructs_running_process() {
117        let tcb = child_tcb("worker");
118        let p = AgentProcess::from_tcb(&tcb).expect("child reconstructs a process");
119        assert_eq!(p.agent_id.as_str(), "worker");
120        assert_eq!(p.parent_session_id.as_str(), "parent-sess");
121        assert_eq!(p.role, AgentRole::Implement);
122        assert_eq!(p.state, ProcessState::Running);
123        assert!(p.result.is_none());
124    }
125
126    #[test]
127    fn process_state_of_maps_terminal_task_states() {
128        assert_eq!(process_state_of(TaskState::Running), ProcessState::Running);
129        assert_eq!(
130            process_state_of(TaskState::Done(TerminationReason::Completed)),
131            ProcessState::Joined
132        );
133        assert_eq!(
134            process_state_of(TaskState::Done(TerminationReason::Error)),
135            ProcessState::Failed
136        );
137        assert_eq!(
138            process_state_of(TaskState::Done(TerminationReason::Timeout)),
139            ProcessState::Failed
140        );
141    }
142}