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 /// The provider: `imap`, `gmail`, or `graph`. Absent from daemons
105 /// before mail actions.
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub provider: Option<String>,
108 /// The kinds of mail action the account may propose, as the provider
109 /// and the settings allow them (`draft`, `send`, `archive`,
110 /// `mark_read`, `trash`, `spam`); empty when it only reads.
111 #[serde(skip_serializing_if = "Vec::is_empty")]
112 pub actions: Vec<String>,
113 /// Mail actions waiting for the owner or for their turn to run.
114 #[serde(skip_serializing_if = "is_zero")]
115 pub actions_open: u64,
116 /// Mail actions being carried out now.
117 #[serde(skip_serializing_if = "is_zero")]
118 pub actions_executing: u64,
119 /// Mail actions SCV lost track of while carrying them out, kept for a
120 /// check of the mailbox.
121 #[serde(skip_serializing_if = "is_zero")]
122 pub actions_unknown: u64,
123 /// Mail actions carried out in the last 24 hours.
124 #[serde(skip_serializing_if = "is_zero")]
125 pub actions_done_24h: u64,
126}
127
128#[allow(
129 clippy::trivially_copy_pass_by_ref,
130 reason = "serde's skip_serializing_if passes a reference"
131)]
132fn is_zero(value: &u64) -> bool {
133 *value == 0
134}
135
136/// One mail action, as `scv mail status` lists it: IDs, kinds, states, and
137/// times only, never a code, a handle, an address, or mail text.
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
139pub struct MailAction {
140 /// The email account, `email:<account>`.
141 pub account: String,
142 /// The action's ID, `a` and 32 hex digits.
143 pub id: String,
144 /// `draft`, `send`, `archive`, `mark_read`, `trash`, or `spam`.
145 pub kind: String,
146 /// `proposed`, `previewing`, `open`, `approved`, `executing`, or
147 /// `unknown`.
148 pub state: String,
149 /// When it was proposed.
150 pub created_unix_seconds: u64,
151 /// When its approval stops counting, while it waits for one.
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub expires_unix_seconds: Option<u64>,
154}
155
156/// Who may use tools through a remote bridge account.
157#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
158#[serde(rename_all = "snake_case")]
159pub enum RemoteTools {
160 /// Every remote session is tool-free (the default).
161 #[default]
162 None,
163 /// The account's authenticated owner gets full, auto-approved tools.
164 Owner,
165}
166
167/// Whose messages a remote bridge account answers.
168#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
169#[serde(rename_all = "snake_case")]
170pub enum Senders {
171 /// Only the account's authenticated owner (the default); everyone else's
172 /// messages are dropped unanswered. Without a known owner ID, nobody.
173 #[default]
174 Owner,
175 /// Anyone who can reach the bot; everyone but the owner stays tool-free.
176 Anyone,
177}
178
179/// What `scv status` shows: the daemon, its components, and its delegations.
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
181pub struct DaemonStatus {
182 /// The daemon's release.
183 pub version: String,
184 /// The daemon's process ID.
185 pub pid: u32,
186 /// Channel accounts.
187 pub components: Vec<ComponentHealth>,
188 /// Delegated agent runs.
189 #[serde(default)]
190 pub delegations: DelegationSummary,
191 /// A restart the daemon has scheduled, if any.
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub restart: Option<RestartInfo>,
194 /// The question a `confirm_ask` or `confirm_status` request is about.
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub confirm: Option<ConfirmInfo>,
197 /// The response to a project ledger command, when one was requested.
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub project: Option<ProjectResponse>,
200 /// The mail actions a `mail_status` or `mail_cancel` request is about.
201 #[serde(default, skip_serializing_if = "Vec::is_empty")]
202 pub mail_actions: Vec<MailAction>,
203 /// What a `mail_cancel` request did, in SCV's words.
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub mail_note: Option<String>,
206}
207
208/// The lifecycle phase observed for a durable project.
209#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
210#[serde(rename_all = "snake_case")]
211pub enum ProjectPhase {
212 /// The project has been created and is being understood.
213 Discovery,
214 /// Tasks and dependencies are being prepared.
215 Planning,
216 /// One or more implementation tasks are running.
217 Implementation,
218 /// Work is being checked or has failed a run.
219 Verification,
220 /// Work is ready for staging.
221 Staging,
222 /// Work has been released to production.
223 Production,
224 /// The project is being reported or closed.
225 Reporting,
226}
227
228/// The reducer's current project status.
229#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
230#[serde(rename_all = "snake_case")]
231pub enum ProjectStatus {
232 /// The orchestrator can make progress.
233 Active,
234 /// No task can currently make progress.
235 Blocked,
236 /// An owner decision is required.
237 WaitingApproval,
238 /// Every task has completed successfully.
239 Completed,
240 /// A task exhausted its retry budget or a run failed permanently.
241 Failed,
242 /// The project was intentionally retired.
243 Archived,
244}
245
246/// A task's observed state.
247#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "snake_case")]
249pub enum ProjectTaskStatus {
250 /// Waiting for dependencies.
251 Pending,
252 /// Eligible to start.
253 Ready,
254 /// Owned by a currently running agent.
255 Running,
256 /// Waiting on an external condition.
257 Blocked,
258 /// Finished successfully.
259 Done,
260 /// Failed permanently.
261 Failed,
262 /// Its owner stopped reporting heartbeats.
263 Stale,
264}
265
266/// A delegated run's observed state.
267#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
268#[serde(rename_all = "snake_case")]
269pub enum ProjectRunStatus {
270 /// The delegated run is still executing.
271 Running,
272 /// The run finished successfully.
273 Succeeded,
274 /// The run finished with an error.
275 Failed,
276 /// The run was intentionally cancelled.
277 Cancelled,
278 /// The run stopped reporting heartbeats.
279 Stale,
280}
281
282/// A compact project record suitable for status output.
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
284pub struct ProjectSummary {
285 /// Stable project identifier.
286 pub id: String,
287 /// Owner-visible project name.
288 pub name: String,
289 /// Absolute workspace directory.
290 pub workspace: String,
291 /// Current lifecycle phase.
292 pub phase: ProjectPhase,
293 /// Current reducer status.
294 pub status: ProjectStatus,
295 /// Creation time in Unix seconds.
296 pub created_unix_seconds: u64,
297 /// Last reducer update in Unix seconds.
298 pub updated_unix_seconds: u64,
299 /// Number of tasks in the project.
300 pub task_count: u64,
301 /// Number of tasks in the done state.
302 pub completed_tasks: u64,
303 /// Last task or run heartbeat, when one exists.
304 pub last_heartbeat_unix_seconds: Option<u64>,
305}
306
307/// A project task and its dependency/progress evidence.
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
309pub struct ProjectTask {
310 /// Stable task identifier.
311 pub id: String,
312 /// Owner-visible task title.
313 pub title: String,
314 /// Current task state.
315 pub status: ProjectTaskStatus,
316 /// Task identifiers that must complete first.
317 #[serde(default)]
318 pub depends_on: Vec<String>,
319 /// Number of failed attempts.
320 pub retries: u32,
321 /// Maximum failed attempts before the task is permanently failed.
322 pub max_retries: u32,
323 /// The current run, when the task is running.
324 pub run_id: Option<String>,
325 /// Latest observed progress text.
326 pub progress: Option<String>,
327 /// Last heartbeat in Unix seconds.
328 pub last_heartbeat_unix_seconds: Option<u64>,
329}
330
331/// A run tracked by the project ledger.
332#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
333pub struct ProjectRun {
334 /// Stable run identifier.
335 pub id: String,
336 /// Task owned by this run.
337 pub task_id: String,
338 /// Agent adapter that owns the run.
339 pub agent: String,
340 /// Current run state.
341 pub status: ProjectRunStatus,
342 /// One-based attempt number.
343 pub attempt: u32,
344 /// Start time in Unix seconds.
345 pub started_unix_seconds: u64,
346 /// Last observed update in Unix seconds.
347 pub updated_unix_seconds: u64,
348 /// Latest observed progress text.
349 pub progress: Option<String>,
350}
351
352/// One durable event, exposed for audit and recovery inspection.
353#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
354pub struct ProjectEvent {
355 /// Monotonic event sequence within the instance ledger.
356 pub sequence: u64,
357 /// Project that owns the event.
358 pub project_id: String,
359 /// Reducer event kind.
360 pub kind: String,
361 /// Event time in Unix seconds.
362 pub timestamp_unix_seconds: u64,
363 /// Event-specific evidence.
364 pub data: serde_json::Value,
365}
366
367/// A report generated from observed ledger state.
368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
369pub struct ProjectReport {
370 /// Current project summary.
371 pub project: ProjectSummary,
372 /// Current task records.
373 pub tasks: Vec<ProjectTask>,
374 /// Current run records.
375 pub runs: Vec<ProjectRun>,
376 /// Report generation time in Unix seconds.
377 pub generated_unix_seconds: u64,
378 /// Human-readable evidence notes.
379 pub evidence: Vec<String>,
380}
381
382/// Payload returned by a project command.
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
384#[serde(tag = "kind", rename_all = "snake_case")]
385pub enum ProjectResponse {
386 /// A project was created.
387 Created {
388 /// Created project summary.
389 project: ProjectSummary,
390 },
391 /// A project status was requested.
392 Status {
393 /// Current project summary.
394 project: ProjectSummary,
395 },
396 /// Project events were requested.
397 Events {
398 /// Project identifier.
399 project_id: String,
400 /// Highest sequence included in a compacted snapshot, when older
401 /// events are no longer retained in the live tail.
402 #[serde(default, skip_serializing_if = "Option::is_none")]
403 compacted_before: Option<u64>,
404 /// Matching events.
405 events: Vec<ProjectEvent>,
406 },
407 /// Project tasks were requested.
408 Tasks {
409 /// Project identifier.
410 project_id: String,
411 /// Current tasks.
412 tasks: Vec<ProjectTask>,
413 },
414 /// A report was generated.
415 Report {
416 /// Generated report.
417 report: ProjectReport,
418 },
419 /// A project changed.
420 Updated {
421 /// Current project summary.
422 project: ProjectSummary,
423 },
424 /// A task was created.
425 TaskAdded {
426 /// Current project summary.
427 project: ProjectSummary,
428 /// New task identifier.
429 task_id: String,
430 },
431 /// A run was started.
432 RunStarted {
433 /// Current project summary.
434 project: ProjectSummary,
435 /// New run identifier.
436 run_id: String,
437 },
438}
439
440/// How long a question to the owner waits for an answer unless the asker
441/// says otherwise.
442pub const DEFAULT_CONFIRM_SECONDS: u64 = 30 * 60;
443/// The longest a question to the owner may wait: the default ceiling of a
444/// delegated agent's own tool call (`tools.max_timeout_seconds`), which is
445/// how a delegated agent asks.
446pub const MAX_CONFIRM_SECONDS: u64 = 4 * 60 * 60;
447
448/// A yes/no question to the owner in chat (`scv confirm`).
449#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
450pub struct ConfirmInfo {
451 /// The question's ID, for `confirm_status`.
452 pub id: String,
453 /// Where it stands.
454 pub state: ConfirmState,
455 /// The account whose owner was asked, as `<channel>:<account>`.
456 pub chat: String,
457 /// When no answer counts as no.
458 pub deadline_unix_seconds: u64,
459}
460
461/// Where a question to the owner stands.
462#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
463#[serde(rename_all = "snake_case")]
464pub enum ConfirmState {
465 /// Sent, or being stored for sending, and waiting for an answer.
466 Pending,
467 /// The owner answered yes.
468 Yes,
469 /// The owner answered no.
470 No,
471 /// No answer came before the deadline, which counts as no.
472 Expired,
473 /// The asker stopped asking about it before an answer came.
474 Withdrawn,
475 /// The question could not be handed to the chat, or its answer was lost.
476 Failed,
477 /// A state this client does not know, from a newer daemon.
478 #[serde(other)]
479 Unknown,
480}
481
482/// A restart into a newly installed release, waiting for owner work to end.
483#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
484pub struct RestartInfo {
485 /// The release it restarts into.
486 pub to_version: String,
487 /// What it still waits for, such as the requesting delegation or an
488 /// owner's message; `None` once it restarts.
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub waiting_for: Option<String>,
491 /// The delegation that asked, whose report goes out first.
492 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub requester: Option<String>,
494 /// The chat the announcement goes to, as `<channel>:<account>`.
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub origin: Option<String>,
497 /// When it restarts even if work is still running.
498 pub deadline_unix_seconds: u64,
499}
500
501/// Delegated agent runs of the daemon's SCV instance.
502#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
503pub struct DelegationSummary {
504 /// Running delegations, whichever SCV process of the instance started them.
505 /// Live agents waiting between turns count too.
506 pub active: u64,
507 /// How many of `active` are live agents (a nested SCV or an ACP agent)
508 /// waiting between turns, apart from a nested SCV whose own background
509 /// jobs still count; `None` from a daemon that does not tell.
510 #[serde(default, skip_serializing_if = "Option::is_none")]
511 pub idle: Option<u64>,
512 /// Orphaned delegations the daemon has stopped since it started.
513 pub reaped: u64,
514 /// Listed delegations, for `delegations` and `delegation_kill`.
515 #[serde(default, skip_serializing_if = "Vec::is_empty")]
516 pub entries: Vec<DelegationInfo>,
517 /// Handles this request stopped.
518 #[serde(default, skip_serializing_if = "Vec::is_empty")]
519 pub killed: Vec<String>,
520}
521
522/// One running delegated agent.
523#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
524pub struct DelegationInfo {
525 /// The delegation to stop.
526 pub handle: String,
527 /// The agent, such as `codex`.
528 pub agent: String,
529 /// The SCV session that started it.
530 pub session: String,
531 /// Delegation depth: 1 for an agent SCV started directly.
532 pub depth: u32,
533 /// The agent's process ID (and process group).
534 pub pid: u32,
535 /// The SCV process that started it.
536 pub owner_pid: u32,
537 /// Live processes in its group plus tagged processes outside it.
538 pub processes: u32,
539 /// Its working directory.
540 pub cwd: String,
541 /// When it started.
542 pub started_unix_seconds: u64,
543 /// The owning SCV process is gone; the daemon will stop it.
544 pub orphaned: bool,
545 /// The conversation this run is a turn of, and which turn.
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub conversation: Option<String>,
548 /// Which turn of that conversation.
549 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub turn: Option<u32>,
551 /// A live agent (a nested SCV or an ACP agent) with no turn running:
552 /// when its last turn ended. Absent while it works, for a per-turn run,
553 /// and from older daemons.
554 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub idle_since_unix_seconds: Option<u64>,
556 /// A nested SCV's own background jobs that still run or wait to be
557 /// reported to it; a planned restart waits for them even between turns.
558 /// Absent when there are none, for other agents, and from older daemons.
559 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub background_jobs: Option<u32>,
561}
562
563/// What a `daemon.control` message asks the daemon to do.
564#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
565#[serde(tag = "action", rename_all = "snake_case")]
566pub enum DaemonCommand {
567 /// Report [`DaemonStatus`].
568 Status,
569 /// Reread channel settings and reconcile the components now.
570 Reload,
571 /// Enable or disable one channel account, optionally changing its
572 /// workspace, remote tool grant, and whose messages it answers.
573 ChannelSet {
574 /// The chat channel, such as `wechat` or `feishu`.
575 channel: String,
576 /// The account name within the channel.
577 account: String,
578 /// Whether the account should run.
579 enabled: bool,
580 /// The account's workspace directory; `None` keeps the saved one.
581 workspace: Option<String>,
582 /// Omitted keeps the saved setting.
583 #[serde(default, skip_serializing_if = "Option::is_none")]
584 remote_tools: Option<RemoteTools>,
585 /// Whose messages the account answers; omitted keeps the saved
586 /// setting.
587 #[serde(default, skip_serializing_if = "Option::is_none")]
588 senders: Option<Senders>,
589 /// What a chat account carries; omitted keeps the saved setting.
590 #[serde(default, skip_serializing_if = "Option::is_none")]
591 purpose: Option<Purpose>,
592 },
593 /// Stop one channel account and remove its credentials and state.
594 ChannelLogout {
595 /// The chat channel, such as `wechat` or `feishu`.
596 channel: String,
597 /// The account name within the channel.
598 account: String,
599 },
600 /// List running delegations; `all` includes orphans awaiting cleanup.
601 Delegations {
602 /// Include orphans awaiting cleanup.
603 #[serde(default)]
604 all: bool,
605 },
606 /// Stop one delegation by handle, or every orphaned one.
607 DelegationKill {
608 /// The delegation to stop.
609 #[serde(default, skip_serializing_if = "Option::is_none")]
610 handle: Option<String>,
611 /// Stop every orphaned delegation instead.
612 #[serde(default)]
613 orphans: bool,
614 },
615 /// Restart into the release installed at the daemon's own path once the
616 /// requesting delegation has finished and its report is stored and no
617 /// owner message is being answered, or at `max_wait_seconds` anyway.
618 RestartWhenIdle {
619 /// The release the caller installed; the daemon checks it.
620 #[serde(default, skip_serializing_if = "Option::is_none")]
621 version: Option<String>,
622 /// The commit it was built from, for the announcement.
623 #[serde(default, skip_serializing_if = "Option::is_none")]
624 commit: Option<String>,
625 /// The caller's `SCV_PARENT` chain, naming the delegation to wait for.
626 #[serde(default, skip_serializing_if = "Option::is_none")]
627 parent: Option<String>,
628 /// Longest wait before restarting anyway.
629 #[serde(default, skip_serializing_if = "Option::is_none")]
630 max_wait_seconds: Option<u64>,
631 },
632 /// Ask the owner a yes/no question in chat: in the chat that started the
633 /// work `parent` names, or else the notify target. The reply's `confirm`
634 /// names the question; `confirm_status` then follows it.
635 ConfirmAsk {
636 /// The question, as the owner reads it.
637 question: String,
638 /// The caller's `SCV_PARENT` chain, naming the delegation whose chat
639 /// is asked.
640 #[serde(default, skip_serializing_if = "Option::is_none")]
641 parent: Option<String>,
642 /// How long no answer waits before it counts as no
643 /// ([`DEFAULT_CONFIRM_SECONDS`], at most [`MAX_CONFIRM_SECONDS`]).
644 #[serde(default, skip_serializing_if = "Option::is_none")]
645 timeout_seconds: Option<u64>,
646 },
647 /// Report where the question `id` stands. Asking also keeps it alive: a
648 /// question nobody asks about for a minute is withdrawn.
649 ConfirmStatus {
650 /// The ID `confirm_ask` returned.
651 id: String,
652 },
653 /// Explicitly create an owner-authorized durable project.
654 ProjectCreate {
655 /// Owner-visible project name.
656 name: String,
657 /// Absolute workspace directory.
658 workspace: String,
659 },
660 /// Read a project's reducer state without contacting an agent.
661 ProjectStatus {
662 /// Project name or identifier.
663 project: String,
664 },
665 /// Read the append-only event history after an optional sequence.
666 ProjectEvents {
667 /// Project name or identifier.
668 project: String,
669 /// Return events after this sequence.
670 after: Option<u64>,
671 },
672 /// Read current tasks and observed progress.
673 ProjectTasks {
674 /// Project name or identifier.
675 project: String,
676 },
677 /// Build a report from ledger evidence.
678 ProjectReport {
679 /// Project name or identifier.
680 project: String,
681 },
682 /// Add a task to an explicitly created project.
683 ProjectTaskAdd {
684 /// Project name or identifier.
685 project: String,
686 /// Owner-visible task title.
687 title: String,
688 /// Task identifiers that must be done first.
689 depends_on: Vec<String>,
690 /// Maximum failed attempts before permanent failure.
691 max_retries: u32,
692 },
693 /// Record an observed task state/progress update.
694 ProjectTaskUpdate {
695 /// Project name or identifier.
696 project: String,
697 /// Task identifier.
698 task: String,
699 /// New task state.
700 status: ProjectTaskStatus,
701 /// Optional progress text.
702 progress: Option<String>,
703 },
704 /// Record a supervised agent run starting.
705 ProjectRunStart {
706 /// Project name or identifier.
707 project: String,
708 /// Task identifier.
709 task: String,
710 /// Agent adapter name.
711 agent: String,
712 },
713 /// Record observed progress from a run heartbeat.
714 ProjectRunProgress {
715 /// Project name or identifier.
716 project: String,
717 /// Run identifier.
718 run: String,
719 /// Progress text.
720 progress: String,
721 },
722 /// Record an observed terminal run state.
723 ProjectRunFinish {
724 /// Project name or identifier.
725 project: String,
726 /// Run identifier.
727 run: String,
728 /// Terminal run state.
729 status: ProjectRunStatus,
730 },
731 /// Record a heartbeat for a project task/run.
732 ProjectHeartbeat {
733 /// Project name or identifier.
734 project: String,
735 /// Optional task identifier.
736 task: Option<String>,
737 /// Optional run identifier.
738 run: Option<String>,
739 },
740 /// List the mail actions of every running email account, or of one;
741 /// the reply's `mail_actions` holds them.
742 MailStatus {
743 /// The email account name; omitted, every account.
744 #[serde(default, skip_serializing_if = "Option::is_none")]
745 account: Option<String>,
746 },
747 /// Withdraw one mail action by its ID, or every action of an account
748 /// that has not started. It can never approve anything.
749 MailCancel {
750 /// The email account name.
751 account: String,
752 /// The action's ID; omitted with `all`.
753 #[serde(default, skip_serializing_if = "Option::is_none")]
754 id: Option<String>,
755 /// Withdraw every action that has not started.
756 #[serde(default)]
757 all: bool,
758 },
759}