Skip to main content

navi_core/tool/
background.rs

1use serde::{Deserialize, Serialize};
2
3/// Status of a background bash command.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum BackgroundTaskStatus {
7    Running,
8    Completed,
9    Failed,
10    TimedOut,
11    Cancelled,
12}
13
14impl BackgroundTaskStatus {
15    pub fn is_final(self) -> bool {
16        self != Self::Running
17    }
18}
19
20impl std::fmt::Display for BackgroundTaskStatus {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            Self::Running => f.write_str("running"),
24            Self::Completed => f.write_str("completed"),
25            Self::Failed => f.write_str("failed"),
26            Self::TimedOut => f.write_str("timed_out"),
27            Self::Cancelled => f.write_str("cancelled"),
28        }
29    }
30}
31
32/// Snapshot of a background bash command's current state.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct BackgroundCommandSnapshot {
35    pub task_id: String,
36    pub command: String,
37    pub description: Option<String>,
38    pub status: BackgroundTaskStatus,
39    pub elapsed_ms: u64,
40    pub timeout_ms: u64,
41    pub exit_code: Option<i32>,
42    pub stdout: String,
43    pub stderr: String,
44    pub stdout_truncated: bool,
45    pub stderr_truncated: bool,
46    pub error: Option<String>,
47}
48
49impl BackgroundCommandSnapshot {
50    /// Parse from the JSON output of a `bash` tool invocation with
51    /// `action=list`, `task_id=...`, or `background=true` spawn result.
52    pub fn from_json(value: &serde_json::Value) -> Option<Self> {
53        let obj = value.as_object()?;
54        let task_id = obj.get("task_id")?.as_str()?.to_string();
55        let command = obj.get("command")?.as_str()?.to_string();
56        let description = obj
57            .get("description")
58            .and_then(|v| v.as_str())
59            .map(String::from);
60        let status_str = obj.get("status")?.as_str()?;
61        let status = match status_str {
62            "running" => BackgroundTaskStatus::Running,
63            "completed" => BackgroundTaskStatus::Completed,
64            "failed" => BackgroundTaskStatus::Failed,
65            "timed_out" => BackgroundTaskStatus::TimedOut,
66            "cancelled" => BackgroundTaskStatus::Cancelled,
67            _ => return None,
68        };
69        let elapsed_ms = obj.get("elapsed_ms")?.as_u64()?;
70        let timeout_ms = obj.get("timeout_ms")?.as_u64()?;
71        let exit_code = obj
72            .get("exit_code")
73            .and_then(|v| v.as_i64())
74            .map(|v| v as i32);
75        let stdout = obj
76            .get("stdout")
77            .and_then(|v| v.as_str())
78            .unwrap_or("")
79            .to_string();
80        let stderr = obj
81            .get("stderr")
82            .and_then(|v| v.as_str())
83            .unwrap_or("")
84            .to_string();
85        let stdout_truncated = obj
86            .get("stdout_truncated")
87            .and_then(|v| v.as_bool())
88            .unwrap_or(false);
89        let stderr_truncated = obj
90            .get("stderr_truncated")
91            .and_then(|v| v.as_bool())
92            .unwrap_or(false);
93        let error = obj.get("error").and_then(|v| v.as_str()).map(String::from);
94
95        Some(Self {
96            task_id,
97            command,
98            description,
99            status,
100            elapsed_ms,
101            timeout_ms,
102            exit_code,
103            stdout,
104            stderr,
105            stdout_truncated,
106            stderr_truncated,
107            error,
108        })
109    }
110
111    /// True if the task is still running.
112    pub fn is_running(&self) -> bool {
113        self.status == BackgroundTaskStatus::Running
114    }
115}