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