返回 CodeWhale
error.rs
根目录 / crates / workflow-js / src / error.rs
1 //! Error types for the dynamic Workflow runtime.
2
3 use thiserror::Error;
4
5 /// Errors surfaced by [`crate::WorkflowVm::run_script`].
6 ///
7 /// Script-visible failures (thrown JS exceptions, rejected promises, host
8 /// function errors that were not caught inside the script) all collapse into
9 /// [`WorkflowJsError::Script`] with the exception message and stack. The
10 /// remaining variants describe runtime-level failures that never reached the
11 /// script.
12 #[derive(Debug, Error)]
13 pub enum WorkflowJsError {
14 /// The QuickJS runtime or context could not be created.
15 #[error("failed to initialize the Workflow JS VM: {0}")]
16 VmInit(String),
17 /// The script threw (or a promise rejected) and nothing caught it.
18 /// Carries the exception message plus stack when available.
19 #[error("script error: {0}")]
20 Script(String),
21 /// The run was cancelled — either the caller dropped the run future or
22 /// the cooperative cancel signal fired mid-script.
23 #[error("workflow run cancelled")]
24 Cancelled,
25 /// The script completed but its return value could not be encoded as
26 /// JSON (e.g. it returned a function or a cyclic object).
27 #[error("script result is not JSON-encodable: {0}")]
28 ResultEncoding(String),
29 /// The invocation arguments could not be injected into the VM.
30 #[error("invalid workflow arguments: {0}")]
31 InvalidArgs(String),
32 /// The dedicated VM thread exited without reporting a result (panic or
33 /// spawn failure). Outstanding driver tasks are cancelled when this is
34 /// observed.
35 #[error("Workflow VM thread terminated unexpectedly: {0}")]
36 VmTerminated(String),
37 }
38
39 /// Errors a [`crate::WorkflowDriver`] can return from `spawn_task`.
40 ///
41 /// Both variants surface inside the script as a thrown exception on the
42 /// corresponding `task()` call, so a script can `try`/`catch` an individual
43 /// rejection (admission, depth, budget) without the whole run failing.
44 #[derive(Debug, Clone, Error)]
45 pub enum DriverError {
46 /// The driver refused to spawn this task (admission cap, depth ceiling,
47 /// budget reservation failure, invalid subagent type, ...).
48 #[error("spawn rejected: {0}")]
49 Rejected(String),
50 /// The driver is gone or its channel closed; no more spawns will work.
51 #[error("driver unavailable: {0}")]
52 Unavailable(String),
53 }
54
55 /// Why a `task()` call failed, as a stable machine kind (R9).
56 ///
57 /// The host assigns the kind at the point the failure actually happens and
58 /// ships it on the task envelope as `error_kind`; the JS prelude copies it
59 /// onto the thrown `Error` as `.kind`, and `parallel()` / `pipeline()`
60 /// classify slots from that field alone.
61 ///
62 /// This exists because the classification used to be a substring match on the
63 /// operator-facing message. A child whose own reply text contained the words
64 /// "budget exhausted" — or a script that threw `new Error("run cancelled")` —
65 /// could forge a fatal classification and abort a healthy run, and a genuine
66 /// subagent failure was indistinguishable from a plain script throw. The kind
67 /// is now data, not prose.
68 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
69 pub enum TaskErrorKind {
70 /// The call never became a child: malformed options, an invalid
71 /// `responseSchema`, the workflow lifetime cap, or a driver admission
72 /// refusal. Nothing ran, so nothing was spent.
73 Admission,
74 /// The run's shared token pool is exhausted — either the pre-spawn gate
75 /// or a child that reported [`crate::TaskCompletion::BudgetExhausted`].
76 Budget,
77 /// The run or the child was cancelled. Always fatal to the fan-out:
78 /// cancellation is the run's own deadline, never a per-slot outcome.
79 Cancelled,
80 /// The child ran and failed (error result, timeout, tool refusal, ...).
81 Agent,
82 /// The reply did not satisfy `responseSchema` and no bounded repair
83 /// remained.
84 Schema,
85 /// The driver seam broke: unavailable, or the completion channel dropped
86 /// before a terminal outcome arrived.
87 Driver,
88 }
89
90 impl TaskErrorKind {
91 /// The wire spelling carried on the envelope and on JS `Error.kind`.
92 pub fn as_str(self) -> &'static str {
93 match self {
94 Self::Admission => "admission",
95 Self::Budget => "budget",
96 Self::Cancelled => "cancelled",
97 Self::Agent => "agent",
98 Self::Schema => "schema",
99 Self::Driver => "driver",
100 }
101 }
102 }
103
104 /// One `task()` failure: the operator-facing message plus its typed kind.
105 #[derive(Debug, Clone, PartialEq, Eq)]
106 pub(crate) struct TaskError {
107 pub(crate) kind: TaskErrorKind,
108 pub(crate) message: String,
109 }
110
111 impl TaskError {
112 pub(crate) fn new(kind: TaskErrorKind, message: impl Into<String>) -> Self {
113 Self {
114 kind,
115 message: message.into(),
116 }
117 }
118 }
119
120 impl From<&DriverError> for TaskErrorKind {
121 fn from(err: &DriverError) -> Self {
122 match err {
123 // A refused spawn never produced a child; it is admission, not a
124 // failure of work that ran.
125 DriverError::Rejected(_) => Self::Admission,
126 DriverError::Unavailable(_) => Self::Driver,
127 }
128 }
129 }
130
130 lines RUST