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}