Skip to main content

agent_exec/
schema.rs

1//! Shared JSON output schema types for agent-exec v0.1.
2//!
3//! All stdout output is JSON only. Tracing logs go to stderr.
4//! Schema version is fixed at "0.1".
5
6use serde::{Deserialize, Serialize};
7
8pub const SCHEMA_VERSION: &str = "0.1";
9
10/// Serialize `value` to a JSON string and print it as a single line to stdout.
11///
12/// This is the single place where stdout JSON output is written, ensuring the
13/// stdout-is-JSON-only contract is enforced uniformly across all response types.
14fn print_json_to_stdout(value: &impl Serialize) {
15    println!(
16        "{}",
17        serde_json::to_string(value).expect("JSON serialization failed")
18    );
19}
20
21/// Top-level envelope used for every successful response.
22#[derive(Debug, Serialize, Deserialize)]
23pub struct Response<T: Serialize> {
24    pub schema_version: &'static str,
25    pub ok: bool,
26    #[serde(rename = "type")]
27    pub kind: &'static str,
28    #[serde(flatten)]
29    pub data: T,
30}
31
32impl<T: Serialize> Response<T> {
33    pub fn new(kind: &'static str, data: T) -> Self {
34        Response {
35            schema_version: SCHEMA_VERSION,
36            ok: true,
37            kind,
38            data,
39        }
40    }
41
42    /// Serialize to a JSON string and print to stdout.
43    pub fn print(&self) {
44        print_json_to_stdout(self);
45    }
46}
47
48/// Top-level envelope for error responses.
49#[derive(Debug, Serialize, Deserialize)]
50pub struct ErrorResponse {
51    pub schema_version: &'static str,
52    pub ok: bool,
53    #[serde(rename = "type")]
54    pub kind: &'static str,
55    pub error: ErrorDetail,
56}
57
58#[derive(Debug, Serialize, Deserialize)]
59pub struct ErrorDetail {
60    pub code: String,
61    pub message: String,
62    /// Whether the caller may retry the same request and expect a different outcome.
63    pub retryable: bool,
64}
65
66impl ErrorResponse {
67    /// Create an error response.
68    ///
69    /// `retryable` should be `true` only when a transient condition (e.g. I/O
70    /// contention, temporary unavailability) caused the failure and the caller
71    /// is expected to succeed on a subsequent attempt without changing the
72    /// request.  Use `false` for permanent failures such as "job not found" or
73    /// internal logic errors.
74    pub fn new(code: impl Into<String>, message: impl Into<String>, retryable: bool) -> Self {
75        ErrorResponse {
76            schema_version: SCHEMA_VERSION,
77            ok: false,
78            kind: "error",
79            error: ErrorDetail {
80                code: code.into(),
81                message: message.into(),
82                retryable,
83            },
84        }
85    }
86
87    pub fn print(&self) {
88        print_json_to_stdout(self);
89    }
90}
91
92// ---------- Command-specific response payloads ----------
93
94/// Response for `create` command.
95#[derive(Debug, Serialize, Deserialize)]
96pub struct CreateData {
97    pub job_id: String,
98    /// Always "created".
99    pub state: String,
100    /// Absolute path to stdout.log for this job.
101    pub stdout_log_path: String,
102    /// Absolute path to stderr.log for this job.
103    pub stderr_log_path: String,
104}
105
106/// Response for `run` command.
107#[derive(Debug, Serialize, Deserialize)]
108pub struct RunData {
109    pub job_id: String,
110    pub state: String,
111    /// Tags assigned to this job (always present; empty array when none).
112    #[serde(default)]
113    pub tags: Vec<String>,
114    /// Environment variables passed to the job, with masked values replaced by "***".
115    /// Omitted from JSON when empty.
116    #[serde(skip_serializing_if = "Vec::is_empty", default)]
117    pub env_vars: Vec<String>,
118    /// Present when `snapshot_after` elapsed before `run` returned.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub snapshot: Option<Snapshot>,
121    /// Absolute path to stdout.log for this job.
122    pub stdout_log_path: String,
123    /// Absolute path to stderr.log for this job.
124    pub stderr_log_path: String,
125    /// Milliseconds actually waited for snapshot (0 when snapshot_after=0).
126    pub waited_ms: u64,
127    /// Wall-clock milliseconds from run invocation start to JSON output.
128    pub elapsed_ms: u64,
129    /// Exit code of the process; present only when `--wait` is used and job has terminated.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub exit_code: Option<i32>,
132    /// RFC 3339 timestamp when the job finished; present only when `--wait` is used.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub finished_at: Option<String>,
135    /// Final log tail snapshot taken after job completion; present only when `--wait` is used.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub final_snapshot: Option<Snapshot>,
138}
139
140/// Response for `status` command.
141#[derive(Debug, Serialize, Deserialize)]
142pub struct StatusData {
143    pub job_id: String,
144    pub state: String,
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub exit_code: Option<i32>,
147    /// RFC 3339 timestamp when the job was created (always present).
148    pub created_at: String,
149    /// RFC 3339 timestamp when the job started executing; absent for `created` state.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub started_at: Option<String>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub finished_at: Option<String>,
154}
155
156/// Response for `tail` command.
157#[derive(Debug, Serialize, Deserialize)]
158pub struct TailData {
159    pub job_id: String,
160    pub stdout_tail: String,
161    pub stderr_tail: String,
162    /// True when the output was truncated by tail_lines or max_bytes constraints.
163    pub truncated: bool,
164    pub encoding: String,
165    /// Absolute path to stdout.log for this job.
166    pub stdout_log_path: String,
167    /// Absolute path to stderr.log for this job.
168    pub stderr_log_path: String,
169    /// Size of stdout.log in bytes at the time of the tail read (0 if file absent).
170    pub stdout_observed_bytes: u64,
171    /// Size of stderr.log in bytes at the time of the tail read (0 if file absent).
172    pub stderr_observed_bytes: u64,
173    /// UTF-8 byte length of the stdout_tail string included in this response.
174    pub stdout_included_bytes: u64,
175    /// UTF-8 byte length of the stderr_tail string included in this response.
176    pub stderr_included_bytes: u64,
177}
178
179/// Response for `wait` command.
180#[derive(Debug, Serialize, Deserialize)]
181pub struct WaitData {
182    pub job_id: String,
183    pub state: String,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub exit_code: Option<i32>,
186}
187
188/// Response for `kill` command.
189#[derive(Debug, Serialize, Deserialize)]
190pub struct KillData {
191    pub job_id: String,
192    pub signal: String,
193}
194
195/// Response for `schema` command.
196#[derive(Debug, Serialize, Deserialize)]
197pub struct SchemaData {
198    /// The JSON Schema format identifier (e.g. "json-schema-draft-07").
199    pub schema_format: String,
200    /// The JSON Schema document describing all CLI response types.
201    pub schema: serde_json::Value,
202    /// Timestamp when the schema file was last updated (RFC 3339).
203    pub generated_at: String,
204}
205
206/// Summary of a single job, included in `list` responses.
207#[derive(Debug, Serialize, Deserialize)]
208pub struct JobSummary {
209    pub job_id: String,
210    /// Job state: created | running | exited | killed | failed | unknown
211    pub state: String,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub exit_code: Option<i32>,
214    /// Creation timestamp from meta.json (RFC 3339).
215    pub created_at: String,
216    /// Execution start timestamp; absent for `created` state.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub started_at: Option<String>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub finished_at: Option<String>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub updated_at: Option<String>,
223    /// Tags assigned to this job (always present; empty array when none).
224    #[serde(default)]
225    pub tags: Vec<String>,
226}
227
228/// Response for `tag set` command.
229#[derive(Debug, Serialize, Deserialize)]
230pub struct TagSetData {
231    pub job_id: String,
232    /// The new deduplicated tag list as persisted to meta.json.
233    pub tags: Vec<String>,
234}
235
236/// Response for `list` command.
237#[derive(Debug, Serialize, Deserialize)]
238pub struct ListData {
239    /// Resolved root directory path.
240    pub root: String,
241    /// Array of job summaries, sorted by started_at descending.
242    pub jobs: Vec<JobSummary>,
243    /// True when the result was truncated by --limit.
244    pub truncated: bool,
245    /// Number of directories skipped because they could not be read as jobs.
246    pub skipped: u64,
247}
248
249/// Per-job result entry in a `gc` response.
250#[derive(Debug, Serialize, Deserialize)]
251pub struct GcJobResult {
252    pub job_id: String,
253    /// Job state as reported from state.json: running | exited | killed | failed | unknown
254    pub state: String,
255    /// What GC did: "deleted" | "would_delete" | "skipped"
256    pub action: String,
257    /// Human-readable explanation for the action.
258    pub reason: String,
259    /// Byte size of the job directory (0 for skipped jobs where size is not computed).
260    pub bytes: u64,
261}
262
263/// Response for the `gc` command.
264#[derive(Debug, Serialize, Deserialize)]
265pub struct GcData {
266    /// Resolved root directory path.
267    pub root: String,
268    /// Whether this was a dry-run (no deletions performed).
269    pub dry_run: bool,
270    /// The effective retention window (e.g. "30d").
271    pub older_than: String,
272    /// How the retention window was determined: "default" or "flag".
273    pub older_than_source: String,
274    /// Number of job directories actually deleted (0 when dry_run=true).
275    pub deleted: u64,
276    /// Number of job directories skipped (running, unreadable, or too recent).
277    pub skipped: u64,
278    /// Total bytes freed (or would be freed in dry-run mode).
279    pub freed_bytes: u64,
280    /// Per-job details.
281    pub jobs: Vec<GcJobResult>,
282}
283
284// ---------- install-skills response payload ----------
285
286/// Summary of a single installed skill, included in `install_skills` responses.
287#[derive(Debug, Serialize, Deserialize)]
288pub struct InstalledSkillSummary {
289    /// Skill name (directory name under `.agents/skills/`).
290    pub name: String,
291    /// Source type string used when the skill was installed (e.g. "self", "local").
292    pub source_type: String,
293    /// Absolute path to the installed skill directory.
294    pub path: String,
295}
296
297/// Response for `notify set` command.
298#[derive(Debug, Serialize, Deserialize)]
299pub struct NotifySetData {
300    pub job_id: String,
301    /// Updated notification configuration saved to meta.json.
302    pub notification: NotificationConfig,
303}
304
305/// Response for `install-skills` command.
306#[derive(Debug, Serialize, Deserialize)]
307pub struct InstallSkillsData {
308    /// List of installed skills.
309    pub skills: Vec<InstalledSkillSummary>,
310    /// Whether skills were installed globally (`~/.agents/`) or locally (`./.agents/`).
311    pub global: bool,
312    /// Absolute path to the `.skill-lock.json` file that was updated.
313    pub lock_file_path: String,
314}
315
316/// Snapshot of stdout/stderr tail at a point in time.
317#[derive(Debug, Serialize, Deserialize)]
318pub struct Snapshot {
319    pub stdout_tail: String,
320    pub stderr_tail: String,
321    /// True when the output was truncated by tail_lines or max_bytes constraints.
322    pub truncated: bool,
323    pub encoding: String,
324    /// Size of stdout.log in bytes at the time of the snapshot (0 if file absent).
325    pub stdout_observed_bytes: u64,
326    /// Size of stderr.log in bytes at the time of the snapshot (0 if file absent).
327    pub stderr_observed_bytes: u64,
328    /// UTF-8 byte length of the stdout_tail string included in this snapshot.
329    pub stdout_included_bytes: u64,
330    /// UTF-8 byte length of the stderr_tail string included in this snapshot.
331    pub stderr_included_bytes: u64,
332}
333
334// ---------- Notification / completion event models ----------
335
336/// Match type for output-match notification.
337#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
338#[serde(rename_all = "lowercase")]
339pub enum OutputMatchType {
340    #[default]
341    Contains,
342    Regex,
343}
344
345/// Stream selector for output-match notification.
346#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
347#[serde(rename_all = "lowercase")]
348pub enum OutputMatchStream {
349    Stdout,
350    Stderr,
351    #[default]
352    Either,
353}
354
355/// Configuration for output-match notifications.
356#[derive(Debug, Serialize, Deserialize, Clone)]
357pub struct OutputMatchConfig {
358    /// Pattern to match against output lines.
359    pub pattern: String,
360    /// Match type: contains (substring) or regex.
361    #[serde(default)]
362    pub match_type: OutputMatchType,
363    /// Which stream to match: stdout, stderr, or either.
364    #[serde(default)]
365    pub stream: OutputMatchStream,
366    /// Shell command string for command sink; executed via platform shell on match.
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub command: Option<String>,
369    /// File path for NDJSON append sink.
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub file: Option<String>,
372}
373
374/// Notification configuration persisted in meta.json.
375#[derive(Debug, Serialize, Deserialize, Clone)]
376pub struct NotificationConfig {
377    /// Shell command string for command sink; executed via platform shell on completion.
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub notify_command: Option<String>,
380    /// File path for NDJSON append sink.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub notify_file: Option<String>,
383    /// Output-match notification configuration.
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub on_output_match: Option<OutputMatchConfig>,
386}
387
388/// The `job.finished` event payload.
389#[derive(Debug, Serialize, Deserialize, Clone)]
390pub struct CompletionEvent {
391    pub schema_version: String,
392    pub event_type: String,
393    pub job_id: String,
394    pub state: String,
395    pub command: Vec<String>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub cwd: Option<String>,
398    pub started_at: String,
399    pub finished_at: String,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub duration_ms: Option<u64>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub exit_code: Option<i32>,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub signal: Option<String>,
406    pub stdout_log_path: String,
407    pub stderr_log_path: String,
408}
409
410/// Delivery result for a single notification sink.
411#[derive(Debug, Serialize, Deserialize, Clone)]
412pub struct SinkDeliveryResult {
413    pub sink_type: String,
414    pub target: String,
415    pub success: bool,
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub error: Option<String>,
418    pub attempted_at: String,
419}
420
421/// Persisted in `completion_event.json` after terminal state is reached.
422#[derive(Debug, Serialize, Deserialize, Clone)]
423pub struct CompletionEventRecord {
424    #[serde(flatten)]
425    pub event: CompletionEvent,
426    pub delivery_results: Vec<SinkDeliveryResult>,
427}
428
429/// The `job.output.matched` event payload.
430#[derive(Debug, Serialize, Deserialize, Clone)]
431pub struct OutputMatchEvent {
432    pub schema_version: String,
433    pub event_type: String,
434    pub job_id: String,
435    pub pattern: String,
436    pub match_type: String,
437    pub stream: String,
438    pub line: String,
439    pub stdout_log_path: String,
440    pub stderr_log_path: String,
441}
442
443/// Delivery record for a single output-match event; appended to `notification_events.ndjson`.
444#[derive(Debug, Serialize, Deserialize, Clone)]
445pub struct OutputMatchEventRecord {
446    #[serde(flatten)]
447    pub event: OutputMatchEvent,
448    pub delivery_results: Vec<SinkDeliveryResult>,
449}
450
451// ---------- Persisted job metadata / state ----------
452
453/// Nested `job` block within `meta.json`.
454#[derive(Debug, Serialize, Deserialize, Clone)]
455pub struct JobMetaJob {
456    pub id: String,
457}
458
459/// Persisted in `meta.json` at job creation time.
460///
461/// Structure:
462/// ```json
463/// {
464///   "job": { "id": "..." },
465///   "schema_version": "0.1",
466///   "command": [...],
467///   "created_at": "...",
468///   "root": "...",
469///   "env_keys": [...],
470///   "env_vars": [...],
471///   "mask": [...]
472/// }
473/// ```
474///
475/// `env_keys` stores only the names (keys) of environment variables passed via `--env`.
476/// `env_vars` stores KEY=VALUE strings with masked values replaced by "***" (display only).
477/// `env_vars_runtime` stores the actual (unmasked) KEY=VALUE strings used at `start` time.
478///   For the `run` command, this field is empty (env vars are passed directly to the supervisor).
479///   For the `create`/`start` lifecycle, this field persists the real KEY=VALUE pairs so
480///   `start` can apply them without re-specifying CLI arguments.
481/// `mask` stores the list of keys whose values are masked in output/metadata views.
482/// `cwd` stores the effective working directory at job creation time (canonicalized).
483///
484/// For the `create`/`start` lifecycle, additional execution-definition fields are
485/// persisted so that `start` can launch the job without re-specifying CLI arguments.
486#[derive(Debug, Serialize, Deserialize, Clone)]
487pub struct JobMeta {
488    pub job: JobMetaJob,
489    pub schema_version: String,
490    pub command: Vec<String>,
491    pub created_at: String,
492    pub root: String,
493    /// Keys of environment variables provided at job creation time.
494    pub env_keys: Vec<String>,
495    /// Environment variables as KEY=VALUE strings, with masked values replaced by "***".
496    /// Used for display in JSON responses and metadata views only.
497    #[serde(skip_serializing_if = "Vec::is_empty", default)]
498    pub env_vars: Vec<String>,
499    /// Actual (unmasked) KEY=VALUE env var pairs persisted for `start` runtime use.
500    /// Only populated in the `create`/`start` lifecycle. For `run`, this is empty
501    /// because env vars are passed directly to the supervisor.
502    /// `--env` in the create/start lifecycle is treated as durable, non-secret configuration;
503    /// use `--env-file` for values that should never be written to disk.
504    #[serde(skip_serializing_if = "Vec::is_empty", default)]
505    pub env_vars_runtime: Vec<String>,
506    /// Keys whose values are masked in output.
507    #[serde(skip_serializing_if = "Vec::is_empty", default)]
508    pub mask: Vec<String>,
509    /// Effective working directory at job creation time (canonicalized absolute path).
510    /// Used by `list` to filter jobs by cwd. Absent for jobs created before this feature.
511    #[serde(skip_serializing_if = "Option::is_none", default)]
512    pub cwd: Option<String>,
513    /// Notification configuration (present only when --notify-command or --notify-file was used).
514    #[serde(skip_serializing_if = "Option::is_none", default)]
515    pub notification: Option<NotificationConfig>,
516    /// User-defined tags for grouping and filtering. Empty array when none.
517    #[serde(default)]
518    pub tags: Vec<String>,
519
520    // --- Execution-definition fields (persisted for create/start lifecycle) ---
521    /// Whether to inherit the current process environment at start time. Default: true.
522    #[serde(default = "default_inherit_env")]
523    pub inherit_env: bool,
524    /// Env-file paths to apply in order at start time (real values read from file on start).
525    #[serde(skip_serializing_if = "Vec::is_empty", default)]
526    pub env_files: Vec<String>,
527    /// Timeout in milliseconds; 0 = no timeout.
528    #[serde(default)]
529    pub timeout_ms: u64,
530    /// Milliseconds after SIGTERM before SIGKILL; 0 = immediate SIGKILL.
531    #[serde(default)]
532    pub kill_after_ms: u64,
533    /// Interval (ms) for state.json updated_at refresh; 0 = disabled.
534    #[serde(default)]
535    pub progress_every_ms: u64,
536    /// Resolved shell wrapper argv (e.g. ["sh", "-lc"]). None = resolved from config at start time.
537    #[serde(skip_serializing_if = "Option::is_none", default)]
538    pub shell_wrapper: Option<Vec<String>>,
539}
540
541fn default_inherit_env() -> bool {
542    true
543}
544
545impl JobMeta {
546    /// Convenience accessor: returns the job ID.
547    pub fn job_id(&self) -> &str {
548        &self.job.id
549    }
550}
551
552/// Nested `job` block within `state.json`.
553#[derive(Debug, Serialize, Deserialize, Clone)]
554pub struct JobStateJob {
555    pub id: String,
556    pub status: JobStatus,
557    /// RFC 3339 execution start timestamp; absent for jobs in `created` state.
558    #[serde(skip_serializing_if = "Option::is_none", default)]
559    pub started_at: Option<String>,
560}
561
562/// Nested `result` block within `state.json`.
563///
564/// Option fields are serialized as `null` (not omitted) so callers always
565/// see consistent keys regardless of job lifecycle stage.
566#[derive(Debug, Serialize, Deserialize, Clone)]
567pub struct JobStateResult {
568    /// `null` while running; set to exit code when process ends.
569    pub exit_code: Option<i32>,
570    /// `null` unless the process was killed by a signal.
571    pub signal: Option<String>,
572    /// `null` while running; set to elapsed milliseconds when process ends.
573    pub duration_ms: Option<u64>,
574}
575
576/// Persisted in `state.json`, updated as the job progresses.
577///
578/// Structure:
579/// ```json
580/// {
581///   "job": { "id": "...", "status": "running", "started_at": "..." },
582///   "result": { "exit_code": null, "signal": null, "duration_ms": null },
583///   "updated_at": "..."
584/// }
585/// ```
586///
587/// Required fields per spec: `job.id`, `job.status`, `job.started_at`,
588/// `result.exit_code`, `result.signal`, `result.duration_ms`, `updated_at`.
589/// Option fields MUST be serialized as `null` (not omitted) so callers always
590/// see consistent keys regardless of job lifecycle stage.
591#[derive(Debug, Serialize, Deserialize, Clone)]
592pub struct JobState {
593    pub job: JobStateJob,
594    pub result: JobStateResult,
595    /// Process ID (not part of the public spec; omitted when not available).
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub pid: Option<u32>,
598    /// Finish time (not part of the nested result block; kept for internal use).
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub finished_at: Option<String>,
601    /// Last time this state was written to disk (RFC 3339).
602    pub updated_at: String,
603    /// Windows-only: name of the Job Object used to manage the process tree.
604    /// Present only when the supervisor successfully created and assigned a
605    /// named Job Object; absent on non-Windows platforms and when creation
606    /// fails (in which case tree management falls back to snapshot enumeration).
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub windows_job_name: Option<String>,
609}
610
611impl JobState {
612    /// Convenience accessor: returns the job ID.
613    pub fn job_id(&self) -> &str {
614        &self.job.id
615    }
616
617    /// Convenience accessor: returns the job status.
618    pub fn status(&self) -> &JobStatus {
619        &self.job.status
620    }
621
622    /// Convenience accessor: returns the started_at timestamp, if present.
623    pub fn started_at(&self) -> Option<&str> {
624        self.job.started_at.as_deref()
625    }
626
627    /// Convenience accessor: returns the exit code.
628    pub fn exit_code(&self) -> Option<i32> {
629        self.result.exit_code
630    }
631
632    /// Convenience accessor: returns the signal name.
633    pub fn signal(&self) -> Option<&str> {
634        self.result.signal.as_deref()
635    }
636
637    /// Convenience accessor: returns the duration in milliseconds.
638    pub fn duration_ms(&self) -> Option<u64> {
639        self.result.duration_ms
640    }
641}
642
643#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
644#[serde(rename_all = "lowercase")]
645pub enum JobStatus {
646    Created,
647    Running,
648    Exited,
649    Killed,
650    Failed,
651}
652
653impl JobStatus {
654    pub fn as_str(&self) -> &'static str {
655        match self {
656            JobStatus::Created => "created",
657            JobStatus::Running => "running",
658            JobStatus::Exited => "exited",
659            JobStatus::Killed => "killed",
660            JobStatus::Failed => "failed",
661        }
662    }
663
664    /// Returns true when the status is a non-terminal state (created or running).
665    pub fn is_non_terminal(&self) -> bool {
666        matches!(self, JobStatus::Created | JobStatus::Running)
667    }
668}