Skip to main content

bobby_browser_client/
outcomes.rs

1//! Command outcomes, evidence, and accessibility snapshot nodes.
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6use crate::{AttemptId, CommandId, PageId};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10#[serde(rename_all = "camelCase", deny_unknown_fields)]
11pub struct ControlActionEvidence {
12    pub operation: crate::FormControlOperation,
13    pub target: crate::FormControlTarget,
14    pub state: crate::FormControlState,
15    pub validity: crate::FormControlValidity,
16    pub node_replaced: bool,
17    /// Controls that appeared because of this action (conditional fields,
18    /// e.g. a billing-cycle select revealed by a plan choice). Read this
19    /// before re-snapshotting: the targets are passable to `control_action`
20    /// verbatim.
21    #[serde(default, skip_serializing_if = "Vec::is_empty")]
22    pub revealed_controls: Vec<crate::forms::RevealedControl>,
23}
24
25/// Semantic target for accessibility-based commands (role + accessible name).
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct AccessibilityTarget {
30    pub role: String,
31    pub accessible_name: String,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub ordinal: Option<usize>,
34    /// Frame hops from the main frame to the document holding this node;
35    /// empty for main-frame nodes. Segments are the same role/name/ordinal
36    /// shape `FormControlTarget.frame_path` resolves, so the target can be
37    /// passed verbatim to `control_action`.
38    #[serde(default, skip_serializing_if = "Vec::is_empty")]
39    pub frame_path: Vec<crate::forms::SemanticTargetSegment>,
40}
41
42/// Where the retained page context says a described control is.
43///
44/// Must stay in `types`: the crate-boundary guard requires every wire-advertised
45/// shape to be a `types::` one so the schema parity guard covers it.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48#[serde(rename_all = "camelCase")]
49pub struct ContextAnswer {
50    pub target: AccessibilityTarget,
51    pub confidence: f32,
52    /// Whether the answer was observed live this session (stamped with the
53    /// page's generation) or remembered from a prior session's persisted
54    /// context. Additive; absent means a live generation-0 answer.
55    #[serde(default)]
56    pub observed_at: ContextObservedAt,
57    /// How the underlying record entered the graph. Absent for live answers
58    /// (always direct observation).
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub source: Option<ContextAnswerSource>,
61}
62
63impl Default for ContextObservedAt {
64    fn default() -> Self {
65        Self::Generation { generation: 0 }
66    }
67}
68
69/// Provenance of a [`ContextAnswer`]: live-observed under a page generation,
70/// or remembered from the persisted per-profile context store.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[serde(tag = "kind", rename_all = "camelCase")]
74pub enum ContextObservedAt {
75    Generation { generation: u64 },
76    Persisted,
77}
78
79/// How a remembered control record entered the graph.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82#[serde(rename_all = "kebab-case")]
83pub enum ContextAnswerSource {
84    Observed,
85    VisionPromoted,
86}
87
88/// The remembered form structure around a located control (`context_neighbors`).
89/// Structure only: roles, names, ordinals, and per-intent counters — never
90/// values, page text, or timestamps finer than a day.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
93#[serde(rename_all = "camelCase")]
94pub struct ContextNeighbors {
95    pub answer: ContextAnswer,
96    /// Key of the enclosing form within the remembered page.
97    pub form: String,
98    /// Pattern of the remembered page the form belongs to.
99    pub page_pattern: String,
100    pub controls: Vec<ContextNeighborControl>,
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[serde(rename_all = "camelCase")]
106pub struct ContextNeighborControl {
107    pub role: String,
108    pub accessible_name: String,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub ordinal: Option<usize>,
111    /// Per-intent-kind counters, keyed by intent kind.
112    pub intents: std::collections::BTreeMap<String, ContextNeighborStats>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
117#[serde(rename_all = "camelCase")]
118pub struct ContextNeighborStats {
119    pub success_count: u64,
120    pub failure_count: u64,
121    /// Days since the Unix epoch of the last verified success.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub last_verified_day: Option<u32>,
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub source: Option<ContextAnswerSource>,
126}
127
128/// One remembered site's structure: page pattern → form key → controls.
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
131#[serde(rename_all = "camelCase")]
132pub struct ContextSiteView {
133    pub site_key: String,
134    pub pages: std::collections::BTreeMap<
135        String,
136        std::collections::BTreeMap<String, Vec<ContextNeighborControl>>,
137    >,
138}
139
140/// One node in an `accessibilitySnapshot` result tree.
141#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
142#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
143#[serde(rename_all = "camelCase", deny_unknown_fields)]
144pub struct AccessibilityNode {
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub role: Option<String>,
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub name: Option<String>,
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub target: Option<AccessibilityTarget>,
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub value: Option<String>,
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub description: Option<String>,
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub required: Option<bool>,
157    #[serde(default, skip_serializing_if = "Option::is_none")]
158    pub disabled: Option<bool>,
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub read_only: Option<bool>,
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub invalid: Option<bool>,
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub checked: Option<bool>,
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub autocomplete: Option<String>,
167    /// Link destination, when the engine reports one for this node. Lets an
168    /// agent feed `download_url` (or navigate) without an HTML round-trip.
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub url: Option<String>,
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub value_min: Option<String>,
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub value_max: Option<String>,
175    #[serde(default, skip_serializing_if = "Vec::is_empty")]
176    pub children: Vec<AccessibilityNode>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
180#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
181#[serde(
182    tag = "status",
183    rename_all = "camelCase",
184    rename_all_fields = "camelCase"
185)]
186pub enum CommandOutcome {
187    Completed {
188        command_id: CommandId,
189        evidence: Vec<Evidence>,
190    },
191    RetryableFailure {
192        command_id: CommandId,
193        error: CommandError,
194    },
195    NeedsReconciliation {
196        command_id: CommandId,
197        error: CommandError,
198        evidence: Vec<Evidence>,
199    },
200    PolicyDenied {
201        command_id: CommandId,
202        error: CommandError,
203    },
204    ResourceExhausted {
205        command_id: CommandId,
206        error: CommandError,
207        retry_after_ms: u64,
208    },
209    Restarted {
210        command_id: CommandId,
211        prior_attempt_id: AttemptId,
212        attempt_id: AttemptId,
213        reason: String,
214        #[serde(default)]
215        evidence: Vec<Evidence>,
216    },
217    Failed {
218        command_id: CommandId,
219        error: CommandError,
220        #[serde(default)]
221        evidence: Vec<Evidence>,
222    },
223}
224
225/// Upper bound on [`Evidence::Wait`]'s `observed` value, in characters.
226///
227/// A verification value -- a matched label, a URL, a ready state -- not page
228/// text. Truncation is on a character boundary; a byte index can land inside a
229/// multi-byte codepoint and panic, which is exactly how extraction used to die
230/// on non-ASCII pages.
231pub const MAX_WAIT_OBSERVED_CHARS: usize = 512;
232
233#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235#[serde(rename_all = "camelCase")]
236pub enum SubmitSettlementOutcome {
237    Settled,
238    ValidationRejected,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243#[serde(
244    tag = "kind",
245    rename_all = "camelCase",
246    rename_all_fields = "camelCase"
247)]
248pub enum Evidence {
249    ExecutionPath {
250        path: ExecutionPath,
251        reason: ExecutionReason,
252        state_version: u64,
253        elapsed_ms: u64,
254        bytes: Option<u64>,
255        sha256: Option<String>,
256        #[serde(default, skip_serializing_if = "Option::is_none")]
257        final_url: Option<String>,
258        #[serde(default, skip_serializing_if = "Option::is_none")]
259        content_type: Option<String>,
260        #[serde(default, skip_serializing_if = "Option::is_none")]
261        status: Option<u16>,
262        #[serde(default, skip_serializing_if = "Vec::is_empty")]
263        redirect_chain: Vec<String>,
264    },
265    Navigation {
266        url: String,
267        title: String,
268    },
269    Inspection {
270        selector: Option<String>,
271        url: String,
272        title: String,
273        text: String,
274        html: Option<String>,
275    },
276    /// Result of a boundary submit that used `networkQuiet` as its
277    /// postcondition. This classifies the settled page without requiring the
278    /// caller to predict confirmation copy or risk a second submit.
279    SubmitSettlement {
280        outcome: SubmitSettlementOutcome,
281    },
282    Element {
283        selector: String,
284        text: Option<String>,
285    },
286    Upload {
287        selector: String,
288        paths: Vec<String>,
289    },
290    Page {
291        page_id: PageId,
292        url: String,
293        title: String,
294    },
295    Pages {
296        pages: Vec<PageEvidence>,
297    },
298    Popup {
299        opener_page_id: PageId,
300        page_id: PageId,
301        url: String,
302        title: String,
303    },
304    Download {
305        filename: String,
306        path: String,
307        bytes: u64,
308        sha256: String,
309        /// The exact caller-supplied destination after it passed the configured
310        /// downloads-root policy. Absolute input is echoed as absolute; relative
311        /// input remains relative. The runtime never discovers or exposes a path
312        /// the caller did not already provide.
313        #[serde(default, skip_serializing_if = "Option::is_none")]
314        saved_to: Option<String>,
315    },
316    Configuration {
317        name: String,
318        value: String,
319    },
320    Resolution {
321        target: Box<crate::TargetSpec>,
322        fingerprint: Box<TargetFingerprint>,
323        candidates: Vec<CandidateEvidence>,
324        best_match_authorized: bool,
325    },
326    Wait {
327        condition: crate::WaitCondition,
328        elapsed_ms: u64,
329        observations: u64,
330        #[serde(default, skip_serializing_if = "Vec::is_empty")]
331        excluded_classes: Vec<String>,
332        /// What the satisfying poll actually read: the element text or value,
333        /// the URL, or the document ready state, depending on the condition.
334        ///
335        /// The wait already reads this to decide whether it is satisfied. It
336        /// used to be discarded, so an agent that verified a submit had to
337        /// spend a second round trip snapshotting the page to learn what it
338        /// had just confirmed. Bounded by [`MAX_WAIT_OBSERVED_CHARS`] -- this
339        /// is a verification value, never a page-text dump.
340        #[serde(default, skip_serializing_if = "Option::is_none")]
341        observed: Option<String>,
342    },
343    Screenshot {
344        artifact_id: String,
345        media_type: String,
346        width: u32,
347        height: u32,
348        bytes: u64,
349        sha256: String,
350    },
351    BrowserExecution {
352        engine: String,
353        browser_version: String,
354        profile_id: String,
355        interaction_path: String,
356    },
357    JavaScriptResult {
358        value: serde_json::Value,
359        truncated: bool,
360    },
361    AccessibilitySnapshot {
362        page_id: PageId,
363        nodes: Vec<AccessibilityNode>,
364        truncated: bool,
365    },
366    FormSnapshot {
367        snapshot: crate::FormSnapshot,
368    },
369    FormValidation {
370        issues: Vec<crate::FormValidationIssue>,
371    },
372    ControlAction {
373        action: ControlActionEvidence,
374    },
375    StructuredExtraction {
376        page_id: PageId,
377        value: serde_json::Value,
378        truncated: bool,
379    },
380    /// A challenge classification from `detectChallenge`: `Some` is a
381    /// detected challenge, `None` is a provably clean page. `confidence` is
382    /// the model's confidence in its answer either way — a caller gating a
383    /// solve spend on a clean answer needs it. `prior_kind` records the site
384    /// prior that enriched the prompt when one existed — transparency, never
385    /// a blended answer.
386    ChallengeDetection {
387        confidence: f32,
388        detection: Option<crate::challenges::ChallengeDetection>,
389        #[serde(default, skip_serializing_if = "Option::is_none")]
390        prior_kind: Option<String>,
391    },
392    CookieState {
393        page_id: Option<PageId>,
394        cookies: Vec<crate::CookieRecord>,
395    },
396    PdfArtifact {
397        artifact_id: String,
398        media_type: String,
399        bytes: u64,
400        sha256: String,
401    },
402    Dialog {
403        dialog_type: String,
404        message: String,
405        action: String,
406    },
407    Emulation {
408        viewport: Option<crate::ViewportSize>,
409        geolocation: Option<crate::GeolocationCoordinates>,
410    },
411    HarArtifact {
412        artifact_id: String,
413        media_type: String,
414        bytes: u64,
415        sha256: String,
416        entries: u32,
417    },
418    IntentExecution {
419        record: ExecutionRecord,
420    },
421    /// Input timing the runtime synthesized rather than observed, emitted when
422    /// the session opted into `executionPolicy.humanize`. Lets `intent-engine`
423    /// verify an effect against the timing actually issued. Carries no typed
424    /// text: action counts and durations only.
425    Humanization {
426        engine: String,
427        actions: u32,
428        synthesized_ms: u64,
429    },
430    /// Result of resolving one named field of an `ExtractIntent`, emitted once
431    /// per field in field order. `value: None` means unresolved, with the
432    /// reason in `errorCode`; the rest of the extraction still runs.
433    Extraction {
434        field: String,
435        #[serde(default, skip_serializing_if = "Option::is_none")]
436        value: Option<String>,
437        resolution_path: IntentResolutionPath,
438        #[serde(default, skip_serializing_if = "Option::is_none")]
439        error_code: Option<ErrorCode>,
440    },
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
444#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
445#[serde(rename_all = "camelCase")]
446pub enum IntentResolutionPath {
447    Deterministic,
448    VisionFallback,
449    /// Resolved from a cached vision proposal (lazy batch prefill) rather
450    /// than a live stuck-rescue escalation.
451    VisionPrefill,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
455#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
456#[serde(rename_all = "camelCase")]
457pub struct ExecutionRecord {
458    pub intent_kind: String,
459    pub purpose: Option<String>,
460    pub resolution_path: IntentResolutionPath,
461    pub plan_summary: String,
462    pub candidates: Vec<CandidateEvidence>,
463    pub wait_elapsed_ms: Option<u64>,
464    pub verification: String,
465    pub artifact_ids: Vec<String>,
466    pub vision_proposal_sha256: Option<String>,
467}
468
469impl Evidence {
470    pub fn journal_safe(&self) -> Self {
471        fn safe_url(value: &str) -> String {
472            let Ok(mut url) = url::Url::parse(value) else {
473                return "[redacted-invalid-url]".into();
474            };
475            let _ = url.set_username("");
476            let _ = url.set_password(None);
477            url.set_query(None);
478            url.set_fragment(None);
479            url.to_string()
480        }
481        let mut safe = self.clone();
482        match &mut safe {
483            Self::ExecutionPath {
484                final_url,
485                redirect_chain,
486                ..
487            } => {
488                if let Some(url) = final_url {
489                    *url = safe_url(url);
490                }
491                for url in redirect_chain {
492                    *url = safe_url(url);
493                }
494            }
495            Self::Navigation { url, .. }
496            | Self::Inspection { url, .. }
497            | Self::Page { url, .. }
498            | Self::Popup { url, .. } => *url = safe_url(url),
499            Self::Pages { pages } => {
500                for page in pages {
501                    page.url = safe_url(&page.url);
502                }
503            }
504            Self::Upload { paths, .. } => {
505                for (index, path) in paths.iter_mut().enumerate() {
506                    *path = format!("upload://evidence/{index}");
507                }
508            }
509            Self::Download {
510                path,
511                sha256,
512                saved_to,
513                ..
514            } => {
515                *path = format!("artifact://sha256/{sha256}");
516                *saved_to = None;
517            }
518            Self::BrowserExecution { .. } => {}
519            Self::IntentExecution { .. } => {}
520            Self::Extraction { .. } => {}
521            _ => {}
522        }
523        safe
524    }
525}
526
527impl CommandOutcome {
528    pub fn journal_safe(&self) -> Self {
529        let mut safe = self.clone();
530        match &mut safe {
531            Self::Completed { evidence, .. }
532            | Self::NeedsReconciliation { evidence, .. }
533            | Self::Restarted { evidence, .. }
534            | Self::Failed { evidence, .. } => {
535                *evidence = evidence.iter().map(Evidence::journal_safe).collect();
536            }
537            _ => {}
538        }
539        match &mut safe {
540            Self::RetryableFailure { error, .. }
541            | Self::NeedsReconciliation { error, .. }
542            | Self::PolicyDenied { error, .. }
543            | Self::ResourceExhausted { error, .. }
544            | Self::Failed { error, .. } => error.message = "redacted durable diagnostic".into(),
545            _ => {}
546        }
547        safe
548    }
549}
550
551#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
553#[serde(rename_all = "camelCase")]
554/// Which strategy served a command: the direct-HTTP reader, or the live
555/// browser. The browser variants name the *strategy*, never an engine — a
556/// Firefox-companion run reports `browser`, and reporting `chromium` there
557/// contradicted the `browserExecution` evidence sitting beside it. The old
558/// wire names still deserialize so recorded journals keep replaying.
559pub enum ExecutionPath {
560    DirectHttp,
561    #[serde(alias = "chromium")]
562    Browser,
563    #[serde(alias = "chromiumFallback")]
564    BrowserFallback,
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
568#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
569#[serde(rename_all = "camelCase")]
570pub enum ExecutionReason {
571    EligibleStaticDocument,
572    EligibleExplicitDownload,
573    IneligibleCommand,
574    SemanticTargetRequired,
575    JavascriptRequired,
576    UnsupportedContentType,
577    StateConflict,
578    PolicyRequired,
579    /// The page has mutated since load (a non-read-only command ran against
580    /// it), so a whole-page read must come from the live DOM, not a refetch.
581    PageMutated,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
585#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
586#[serde(rename_all = "camelCase")]
587pub struct CandidateEvidence {
588    pub role: Option<String>,
589    pub name: Option<String>,
590    pub score: i32,
591    pub reasons: Vec<String>,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
595#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
596#[serde(rename_all = "camelCase")]
597pub struct TargetFingerprint {
598    pub page_id: PageId,
599    pub frame: Option<String>,
600    pub role: Option<String>,
601    pub name: Option<String>,
602    pub stable_attributes: std::collections::BTreeMap<String, String>,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
606#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
607#[serde(rename_all = "camelCase")]
608pub struct PageEvidence {
609    pub page_id: PageId,
610    pub url: String,
611    pub title: String,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
615#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
616#[serde(rename_all = "camelCase")]
617pub struct CommandError {
618    pub code: ErrorCode,
619    pub message: String,
620    pub layer: ErrorLayer,
621    pub retryable: bool,
622}
623
624#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
625#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
626#[serde(rename_all = "camelCase")]
627pub enum ErrorCode {
628    InvalidRequest,
629    NotFound,
630    DeadlineExceeded,
631    BrowserLaunchFailed,
632    BrowserCommandFailed,
633    VerificationFailed,
634    JournalFailed,
635    ResourceExhausted,
636    PolicyDenied,
637    Internal,
638    TargetNotFound,
639    TargetAmbiguous,
640    FrameNotFound,
641    ShadowRootUnavailable,
642    TargetDetached,
643    TargetObscured,
644    TargetOutOfBounds,
645    WaitConditionTimedOut,
646    ScreenshotCaptureFailed,
647    NetworkPolicyDenied,
648    HttpResponseTooLarge,
649    HttpTransferFailed,
650    HttpStateConflict,
651    HttpEquivalenceUnproven,
652    IntentCompileFailed,
653    IntentActionMismatch,
654    ObstructionSuspected,
655    VisionAssistDenied,
656    VisionAssistFailed,
657    // `submitAndVerify`'s expected state already held before the submit ran,
658    // so a post-act pass would prove nothing (static-copy matcher).
659    ExpectedStatePreSatisfied,
660    // A Boundary submit already completed within this workflow; running
661    // another would double-apply the effect unless the caller explicitly
662    // acknowledges the prior one (`reSubmit`).
663    BoundaryAlreadyExecuted,
664}
665
666#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
667#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
668#[serde(rename_all = "camelCase")]
669pub enum ErrorLayer {
670    Interface,
671    Broker,
672    Workflow,
673    Page,
674    Driver,
675    Browser,
676    Network,
677    Site,
678    Journal,
679}
680
681#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
682#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
683#[serde(rename_all = "camelCase")]
684pub enum CommandPhase {
685    Accepted,
686    Prepared,
687    Executing,
688    ResultPrepared,
689    Verifying,
690    Recovering,
691    Completed,
692    Failed,
693}
694
695#[derive(Debug, Error, Serialize, Deserialize)]
696#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
697pub enum RuntimeError {
698    #[error("not found: {0}")]
699    NotFound(String),
700    #[error("invalid request: {0}")]
701    InvalidRequest(String),
702    /// The configured browser engine could not be reached or started. An
703    /// environment fault, not a bad call: the caller's arguments were fine and
704    /// resubmitting them unchanged is the right move once the engine is up.
705    #[error("{0}")]
706    EngineUnreachable(String),
707    #[error("internal error: {0}")]
708    Internal(String),
709}