Skip to main content

scv_protocol/
daemon.rs

1//! Daemon control: health, delegations, and the commands `scv` sends over
2//! `daemon.control`.
3
4use serde::{Deserialize, Serialize};
5
6/// Where a daemon component (a channel account) is in its lifecycle.
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9pub enum ComponentState {
10    /// Turned off in configuration.
11    Disabled,
12    /// Starting up.
13    Starting,
14    /// Running and connected to its platform.
15    Connected,
16    /// Running but not connected.
17    Disconnected,
18    /// Waiting before a restart after a failure.
19    Backoff,
20    /// Shutting down.
21    Stopping,
22    /// Not running.
23    Stopped,
24    /// Stopped after an error it cannot recover from, such as expired credentials.
25    Failed,
26}
27
28/// The health of one channel account.
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct ComponentHealth {
31    /// Component ID, `<channel>:<account>`.
32    pub id: String,
33    /// The chat channel this account belongs to, such as `wechat`.
34    #[serde(default)]
35    pub channel: String,
36    /// The account name within the channel.
37    pub account: String,
38    /// The platform's ID of the bot account, when known.
39    pub bot_id: Option<String>,
40    /// The platform's ID of the account's owner, when known.
41    pub user_id: Option<String>,
42    /// Whether the account should run.
43    pub enabled: bool,
44    /// Lifecycle state.
45    pub state: ComponentState,
46    /// When the platform last answered successfully.
47    pub last_success_unix_seconds: Option<u64>,
48    /// The latest error, if any.
49    pub error: Option<String>,
50    /// Restarts since the daemon started.
51    pub restarts: u64,
52    /// Effective remote tool authority; `owner` only when the owner ID is known.
53    #[serde(default)]
54    pub remote_tools: RemoteTools,
55    /// Who the account answers, as set; `owner` with no known owner ID
56    /// answers nobody. `None` from daemons before 0.3.0, which answer anyone.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub senders: Option<Senders>,
59    /// What a chat account carries, when it is not an ordinary chat:
60    /// `mail` for a mail chat. Absent for ordinary chats, email accounts,
61    /// and daemons before mail.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub purpose: Option<Purpose>,
64    /// An email account's counts: never addresses, subjects, or any other
65    /// mail text. Absent for chat accounts.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub mail: Option<MailCounts>,
68}
69
70/// What a chat account carries.
71#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(rename_all = "snake_case")]
73pub enum Purpose {
74    /// Conversations with a model (the default).
75    #[default]
76    Chat,
77    /// Only the email accounts' reports to the owner: no model ever answers
78    /// in it, it has no tools, and SCV never logs it.
79    Mail,
80}
81
82/// An email account's activity, as counts only.
83#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
84#[serde(default)]
85pub struct MailCounts {
86    /// Mail claimed and not yet decided.
87    pub claimed: u64,
88    /// Report items waiting to be sent to the mail chat.
89    pub queued: u64,
90    /// Mail decided today (the account's local day).
91    pub seen_today: u64,
92    /// Of those, mail a model triaged.
93    pub triaged_today: u64,
94    /// Of those, mail reported to the owner.
95    pub reported_today: u64,
96    /// Model tokens spent today.
97    pub tokens_today: u64,
98    /// The daily token budget.
99    pub token_budget: u64,
100    /// Digest messages sent to the mail chat in the last 24 hours.
101    pub messages_24h: u64,
102    /// When the mailbox was last checked, in Unix seconds.
103    pub last_check_unix_seconds: Option<u64>,
104}
105
106/// Who may use tools through a remote bridge account.
107#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(rename_all = "snake_case")]
109pub enum RemoteTools {
110    /// Every remote session is tool-free (the default).
111    #[default]
112    None,
113    /// The account's authenticated owner gets full, auto-approved tools.
114    Owner,
115}
116
117/// Whose messages a remote bridge account answers.
118#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
119#[serde(rename_all = "snake_case")]
120pub enum Senders {
121    /// Only the account's authenticated owner (the default); everyone else's
122    /// messages are dropped unanswered. Without a known owner ID, nobody.
123    #[default]
124    Owner,
125    /// Anyone who can reach the bot; everyone but the owner stays tool-free.
126    Anyone,
127}
128
129/// What `scv status` shows: the daemon, its components, and its delegations.
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131pub struct DaemonStatus {
132    /// The daemon's release.
133    pub version: String,
134    /// The daemon's process ID.
135    pub pid: u32,
136    /// Channel accounts.
137    pub components: Vec<ComponentHealth>,
138    /// Delegated agent runs.
139    #[serde(default)]
140    pub delegations: DelegationSummary,
141    /// A restart the daemon has scheduled, if any.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub restart: Option<RestartInfo>,
144    /// The question a `confirm_ask` or `confirm_status` request is about.
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub confirm: Option<ConfirmInfo>,
147    /// The response to a project ledger command, when one was requested.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub project: Option<ProjectResponse>,
150}
151
152/// The lifecycle phase observed for a durable project.
153#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
154#[serde(rename_all = "snake_case")]
155pub enum ProjectPhase {
156    /// The project has been created and is being understood.
157    Discovery,
158    /// Tasks and dependencies are being prepared.
159    Planning,
160    /// One or more implementation tasks are running.
161    Implementation,
162    /// Work is being checked or has failed a run.
163    Verification,
164    /// Work is ready for staging.
165    Staging,
166    /// Work has been released to production.
167    Production,
168    /// The project is being reported or closed.
169    Reporting,
170}
171
172/// The reducer's current project status.
173#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
174#[serde(rename_all = "snake_case")]
175pub enum ProjectStatus {
176    /// The orchestrator can make progress.
177    Active,
178    /// No task can currently make progress.
179    Blocked,
180    /// An owner decision is required.
181    WaitingApproval,
182    /// Every task has completed successfully.
183    Completed,
184    /// A task exhausted its retry budget or a run failed permanently.
185    Failed,
186    /// The project was intentionally retired.
187    Archived,
188}
189
190/// A task's observed state.
191#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
192#[serde(rename_all = "snake_case")]
193pub enum ProjectTaskStatus {
194    /// Waiting for dependencies.
195    Pending,
196    /// Eligible to start.
197    Ready,
198    /// Owned by a currently running agent.
199    Running,
200    /// Waiting on an external condition.
201    Blocked,
202    /// Finished successfully.
203    Done,
204    /// Failed permanently.
205    Failed,
206    /// Its owner stopped reporting heartbeats.
207    Stale,
208}
209
210/// A delegated run's observed state.
211#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
212#[serde(rename_all = "snake_case")]
213pub enum ProjectRunStatus {
214    /// The delegated run is still executing.
215    Running,
216    /// The run finished successfully.
217    Succeeded,
218    /// The run finished with an error.
219    Failed,
220    /// The run was intentionally cancelled.
221    Cancelled,
222    /// The run stopped reporting heartbeats.
223    Stale,
224}
225
226/// A compact project record suitable for status output.
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
228pub struct ProjectSummary {
229    /// Stable project identifier.
230    pub id: String,
231    /// Owner-visible project name.
232    pub name: String,
233    /// Absolute workspace directory.
234    pub workspace: String,
235    /// Current lifecycle phase.
236    pub phase: ProjectPhase,
237    /// Current reducer status.
238    pub status: ProjectStatus,
239    /// Creation time in Unix seconds.
240    pub created_unix_seconds: u64,
241    /// Last reducer update in Unix seconds.
242    pub updated_unix_seconds: u64,
243    /// Number of tasks in the project.
244    pub task_count: u64,
245    /// Number of tasks in the done state.
246    pub completed_tasks: u64,
247    /// Last task or run heartbeat, when one exists.
248    pub last_heartbeat_unix_seconds: Option<u64>,
249}
250
251/// A project task and its dependency/progress evidence.
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
253pub struct ProjectTask {
254    /// Stable task identifier.
255    pub id: String,
256    /// Owner-visible task title.
257    pub title: String,
258    /// Current task state.
259    pub status: ProjectTaskStatus,
260    /// Task identifiers that must complete first.
261    #[serde(default)]
262    pub depends_on: Vec<String>,
263    /// Number of failed attempts.
264    pub retries: u32,
265    /// Maximum failed attempts before the task is permanently failed.
266    pub max_retries: u32,
267    /// The current run, when the task is running.
268    pub run_id: Option<String>,
269    /// Latest observed progress text.
270    pub progress: Option<String>,
271    /// Last heartbeat in Unix seconds.
272    pub last_heartbeat_unix_seconds: Option<u64>,
273}
274
275/// A run tracked by the project ledger.
276#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
277pub struct ProjectRun {
278    /// Stable run identifier.
279    pub id: String,
280    /// Task owned by this run.
281    pub task_id: String,
282    /// Agent adapter that owns the run.
283    pub agent: String,
284    /// Current run state.
285    pub status: ProjectRunStatus,
286    /// One-based attempt number.
287    pub attempt: u32,
288    /// Start time in Unix seconds.
289    pub started_unix_seconds: u64,
290    /// Last observed update in Unix seconds.
291    pub updated_unix_seconds: u64,
292    /// Latest observed progress text.
293    pub progress: Option<String>,
294}
295
296/// One durable event, exposed for audit and recovery inspection.
297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
298pub struct ProjectEvent {
299    /// Monotonic event sequence within the instance ledger.
300    pub sequence: u64,
301    /// Project that owns the event.
302    pub project_id: String,
303    /// Reducer event kind.
304    pub kind: String,
305    /// Event time in Unix seconds.
306    pub timestamp_unix_seconds: u64,
307    /// Event-specific evidence.
308    pub data: serde_json::Value,
309}
310
311/// A report generated from observed ledger state.
312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
313pub struct ProjectReport {
314    /// Current project summary.
315    pub project: ProjectSummary,
316    /// Current task records.
317    pub tasks: Vec<ProjectTask>,
318    /// Current run records.
319    pub runs: Vec<ProjectRun>,
320    /// Report generation time in Unix seconds.
321    pub generated_unix_seconds: u64,
322    /// Human-readable evidence notes.
323    pub evidence: Vec<String>,
324}
325
326/// Payload returned by a project command.
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328#[serde(tag = "kind", rename_all = "snake_case")]
329pub enum ProjectResponse {
330    /// A project was created.
331    Created {
332        /// Created project summary.
333        project: ProjectSummary,
334    },
335    /// A project status was requested.
336    Status {
337        /// Current project summary.
338        project: ProjectSummary,
339    },
340    /// Project events were requested.
341    Events {
342        /// Project identifier.
343        project_id: String,
344        /// Highest sequence included in a compacted snapshot, when older
345        /// events are no longer retained in the live tail.
346        #[serde(default, skip_serializing_if = "Option::is_none")]
347        compacted_before: Option<u64>,
348        /// Matching events.
349        events: Vec<ProjectEvent>,
350    },
351    /// Project tasks were requested.
352    Tasks {
353        /// Project identifier.
354        project_id: String,
355        /// Current tasks.
356        tasks: Vec<ProjectTask>,
357    },
358    /// A report was generated.
359    Report {
360        /// Generated report.
361        report: ProjectReport,
362    },
363    /// A project changed.
364    Updated {
365        /// Current project summary.
366        project: ProjectSummary,
367    },
368    /// A task was created.
369    TaskAdded {
370        /// Current project summary.
371        project: ProjectSummary,
372        /// New task identifier.
373        task_id: String,
374    },
375    /// A run was started.
376    RunStarted {
377        /// Current project summary.
378        project: ProjectSummary,
379        /// New run identifier.
380        run_id: String,
381    },
382}
383
384/// How long a question to the owner waits for an answer unless the asker
385/// says otherwise.
386pub const DEFAULT_CONFIRM_SECONDS: u64 = 30 * 60;
387/// The longest a question to the owner may wait: the default ceiling of a
388/// delegated agent's own tool call (`tools.max_timeout_seconds`), which is
389/// how a delegated agent asks.
390pub const MAX_CONFIRM_SECONDS: u64 = 4 * 60 * 60;
391
392/// A yes/no question to the owner in chat (`scv confirm`).
393#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
394pub struct ConfirmInfo {
395    /// The question's ID, for `confirm_status`.
396    pub id: String,
397    /// Where it stands.
398    pub state: ConfirmState,
399    /// The account whose owner was asked, as `<channel>:<account>`.
400    pub chat: String,
401    /// When no answer counts as no.
402    pub deadline_unix_seconds: u64,
403}
404
405/// Where a question to the owner stands.
406#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
407#[serde(rename_all = "snake_case")]
408pub enum ConfirmState {
409    /// Sent, or being stored for sending, and waiting for an answer.
410    Pending,
411    /// The owner answered yes.
412    Yes,
413    /// The owner answered no.
414    No,
415    /// No answer came before the deadline, which counts as no.
416    Expired,
417    /// The asker stopped asking about it before an answer came.
418    Withdrawn,
419    /// The question could not be handed to the chat, or its answer was lost.
420    Failed,
421    /// A state this client does not know, from a newer daemon.
422    #[serde(other)]
423    Unknown,
424}
425
426/// A restart into a newly installed release, waiting for owner work to end.
427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
428pub struct RestartInfo {
429    /// The release it restarts into.
430    pub to_version: String,
431    /// What it still waits for, such as the requesting delegation or an
432    /// owner's message; `None` once it restarts.
433    #[serde(default, skip_serializing_if = "Option::is_none")]
434    pub waiting_for: Option<String>,
435    /// The delegation that asked, whose report goes out first.
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub requester: Option<String>,
438    /// The chat the announcement goes to, as `<channel>:<account>`.
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub origin: Option<String>,
441    /// When it restarts even if work is still running.
442    pub deadline_unix_seconds: u64,
443}
444
445/// Delegated agent runs of the daemon's SCV instance.
446#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
447pub struct DelegationSummary {
448    /// Running delegations, whichever SCV process of the instance started them.
449    /// Live agents waiting between turns count too.
450    pub active: u64,
451    /// How many of `active` are live agents (a nested SCV or an ACP agent)
452    /// waiting between turns, apart from a nested SCV whose own background
453    /// jobs still count; `None` from a daemon that does not tell.
454    #[serde(default, skip_serializing_if = "Option::is_none")]
455    pub idle: Option<u64>,
456    /// Orphaned delegations the daemon has stopped since it started.
457    pub reaped: u64,
458    /// Listed delegations, for `delegations` and `delegation_kill`.
459    #[serde(default, skip_serializing_if = "Vec::is_empty")]
460    pub entries: Vec<DelegationInfo>,
461    /// Handles this request stopped.
462    #[serde(default, skip_serializing_if = "Vec::is_empty")]
463    pub killed: Vec<String>,
464}
465
466/// One running delegated agent.
467#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
468pub struct DelegationInfo {
469    /// The delegation to stop.
470    pub handle: String,
471    /// The agent, such as `codex`.
472    pub agent: String,
473    /// The SCV session that started it.
474    pub session: String,
475    /// Delegation depth: 1 for an agent SCV started directly.
476    pub depth: u32,
477    /// The agent's process ID (and process group).
478    pub pid: u32,
479    /// The SCV process that started it.
480    pub owner_pid: u32,
481    /// Live processes in its group plus tagged processes outside it.
482    pub processes: u32,
483    /// Its working directory.
484    pub cwd: String,
485    /// When it started.
486    pub started_unix_seconds: u64,
487    /// The owning SCV process is gone; the daemon will stop it.
488    pub orphaned: bool,
489    /// The conversation this run is a turn of, and which turn.
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    pub conversation: Option<String>,
492    /// Which turn of that conversation.
493    #[serde(default, skip_serializing_if = "Option::is_none")]
494    pub turn: Option<u32>,
495    /// A live agent (a nested SCV or an ACP agent) with no turn running:
496    /// when its last turn ended. Absent while it works, for a per-turn run,
497    /// and from older daemons.
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub idle_since_unix_seconds: Option<u64>,
500    /// A nested SCV's own background jobs that still run or wait to be
501    /// reported to it; a planned restart waits for them even between turns.
502    /// Absent when there are none, for other agents, and from older daemons.
503    #[serde(default, skip_serializing_if = "Option::is_none")]
504    pub background_jobs: Option<u32>,
505}
506
507/// What a `daemon.control` message asks the daemon to do.
508#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
509#[serde(tag = "action", rename_all = "snake_case")]
510pub enum DaemonCommand {
511    /// Report [`DaemonStatus`].
512    Status,
513    /// Reread channel settings and reconcile the components now.
514    Reload,
515    /// Enable or disable one channel account, optionally changing its
516    /// workspace, remote tool grant, and whose messages it answers.
517    ChannelSet {
518        /// The chat channel, such as `wechat` or `feishu`.
519        channel: String,
520        /// The account name within the channel.
521        account: String,
522        /// Whether the account should run.
523        enabled: bool,
524        /// The account's workspace directory; `None` keeps the saved one.
525        workspace: Option<String>,
526        /// Omitted keeps the saved setting.
527        #[serde(default, skip_serializing_if = "Option::is_none")]
528        remote_tools: Option<RemoteTools>,
529        /// Whose messages the account answers; omitted keeps the saved
530        /// setting.
531        #[serde(default, skip_serializing_if = "Option::is_none")]
532        senders: Option<Senders>,
533        /// What a chat account carries; omitted keeps the saved setting.
534        #[serde(default, skip_serializing_if = "Option::is_none")]
535        purpose: Option<Purpose>,
536    },
537    /// Stop one channel account and remove its credentials and state.
538    ChannelLogout {
539        /// The chat channel, such as `wechat` or `feishu`.
540        channel: String,
541        /// The account name within the channel.
542        account: String,
543    },
544    /// List running delegations; `all` includes orphans awaiting cleanup.
545    Delegations {
546        /// Include orphans awaiting cleanup.
547        #[serde(default)]
548        all: bool,
549    },
550    /// Stop one delegation by handle, or every orphaned one.
551    DelegationKill {
552        /// The delegation to stop.
553        #[serde(default, skip_serializing_if = "Option::is_none")]
554        handle: Option<String>,
555        /// Stop every orphaned delegation instead.
556        #[serde(default)]
557        orphans: bool,
558    },
559    /// Restart into the release installed at the daemon's own path once the
560    /// requesting delegation has finished and its report is stored and no
561    /// owner message is being answered, or at `max_wait_seconds` anyway.
562    RestartWhenIdle {
563        /// The release the caller installed; the daemon checks it.
564        #[serde(default, skip_serializing_if = "Option::is_none")]
565        version: Option<String>,
566        /// The commit it was built from, for the announcement.
567        #[serde(default, skip_serializing_if = "Option::is_none")]
568        commit: Option<String>,
569        /// The caller's `SCV_PARENT` chain, naming the delegation to wait for.
570        #[serde(default, skip_serializing_if = "Option::is_none")]
571        parent: Option<String>,
572        /// Longest wait before restarting anyway.
573        #[serde(default, skip_serializing_if = "Option::is_none")]
574        max_wait_seconds: Option<u64>,
575    },
576    /// Ask the owner a yes/no question in chat: in the chat that started the
577    /// work `parent` names, or else the notify target. The reply's `confirm`
578    /// names the question; `confirm_status` then follows it.
579    ConfirmAsk {
580        /// The question, as the owner reads it.
581        question: String,
582        /// The caller's `SCV_PARENT` chain, naming the delegation whose chat
583        /// is asked.
584        #[serde(default, skip_serializing_if = "Option::is_none")]
585        parent: Option<String>,
586        /// How long no answer waits before it counts as no
587        /// ([`DEFAULT_CONFIRM_SECONDS`], at most [`MAX_CONFIRM_SECONDS`]).
588        #[serde(default, skip_serializing_if = "Option::is_none")]
589        timeout_seconds: Option<u64>,
590    },
591    /// Report where the question `id` stands. Asking also keeps it alive: a
592    /// question nobody asks about for a minute is withdrawn.
593    ConfirmStatus {
594        /// The ID `confirm_ask` returned.
595        id: String,
596    },
597    /// Explicitly create an owner-authorized durable project.
598    ProjectCreate {
599        /// Owner-visible project name.
600        name: String,
601        /// Absolute workspace directory.
602        workspace: String,
603    },
604    /// Read a project's reducer state without contacting an agent.
605    ProjectStatus {
606        /// Project name or identifier.
607        project: String,
608    },
609    /// Read the append-only event history after an optional sequence.
610    ProjectEvents {
611        /// Project name or identifier.
612        project: String,
613        /// Return events after this sequence.
614        after: Option<u64>,
615    },
616    /// Read current tasks and observed progress.
617    ProjectTasks {
618        /// Project name or identifier.
619        project: String,
620    },
621    /// Build a report from ledger evidence.
622    ProjectReport {
623        /// Project name or identifier.
624        project: String,
625    },
626    /// Add a task to an explicitly created project.
627    ProjectTaskAdd {
628        /// Project name or identifier.
629        project: String,
630        /// Owner-visible task title.
631        title: String,
632        /// Task identifiers that must be done first.
633        depends_on: Vec<String>,
634        /// Maximum failed attempts before permanent failure.
635        max_retries: u32,
636    },
637    /// Record an observed task state/progress update.
638    ProjectTaskUpdate {
639        /// Project name or identifier.
640        project: String,
641        /// Task identifier.
642        task: String,
643        /// New task state.
644        status: ProjectTaskStatus,
645        /// Optional progress text.
646        progress: Option<String>,
647    },
648    /// Record a supervised agent run starting.
649    ProjectRunStart {
650        /// Project name or identifier.
651        project: String,
652        /// Task identifier.
653        task: String,
654        /// Agent adapter name.
655        agent: String,
656    },
657    /// Record observed progress from a run heartbeat.
658    ProjectRunProgress {
659        /// Project name or identifier.
660        project: String,
661        /// Run identifier.
662        run: String,
663        /// Progress text.
664        progress: String,
665    },
666    /// Record an observed terminal run state.
667    ProjectRunFinish {
668        /// Project name or identifier.
669        project: String,
670        /// Run identifier.
671        run: String,
672        /// Terminal run state.
673        status: ProjectRunStatus,
674    },
675    /// Record a heartbeat for a project task/run.
676    ProjectHeartbeat {
677        /// Project name or identifier.
678        project: String,
679        /// Optional task identifier.
680        task: Option<String>,
681        /// Optional run identifier.
682        run: Option<String>,
683    },
684}