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}