1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
//! Errors produced while authoring or operating scheduled Tasks.
#[cfg(feature = "nexus")]
use crate::nexus::error::NexusError;
use {
crate::{
scheduler::{OccurrenceSnapshot, OccurrenceStatus},
sui,
},
std::error::Error,
thiserror::Error,
};
/// Type erased source retained by a scheduler boundary error.
pub type ErrorSource = Box<dyn Error + Send + Sync + 'static>;
/// An invalid Task or Schedule authoring value.
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScheduleError {
/// A Task cannot reserve zero MIST for an occurrence.
#[error("occurrence budget must be greater than zero")]
ZeroOccurrenceBudget,
/// A Task entry group cannot be empty.
#[error("entry group must not be empty")]
EmptyEntryGroup,
/// An absolute deadline precedes the known absolute start.
#[error("occurrence deadline {deadline_ms}ms precedes its start {start_time_ms}ms")]
DeadlineBeforeStart {
/// Known absolute start timestamp.
start_time_ms: u64,
/// Invalid absolute deadline timestamp.
deadline_ms: u64,
},
/// The priority fee is outside the protocol range.
#[error("priority fee percentage must be in {minimum}..={maximum}, got {percentage}")]
PriorityFeeOutOfRange {
/// Invalid percentage.
percentage: u64,
/// Inclusive protocol minimum.
minimum: u64,
/// Inclusive protocol maximum.
maximum: u64,
},
/// A recurrence interval must advance time.
#[error("recurrence interval must be greater than zero")]
ZeroRecurrenceInterval,
/// A finite recurrence must contain at least one occurrence.
#[error("finite recurrence occurrence count must be greater than zero")]
ZeroRecurrenceCount,
/// The atomic create and schedule shortcut requires work.
#[error("schedule must contain at least one occurrence or a recurrence")]
EmptySchedule,
/// Resolving a relative timestamp exceeded the `u64` timestamp range.
#[error("resolving {field} from {base_ms}ms with offset {offset_ms}ms overflowed")]
TimeOverflow {
/// Name of the timestamp being resolved.
field: &'static str,
/// Timestamp to which the offset was applied.
base_ms: u64,
/// Relative offset that overflowed.
offset_ms: u64,
},
/// A Task operation and funding controller cannot be combined.
#[error("{message}")]
IncompatibleFunding {
/// Explanation of the invalid combination.
message: &'static str,
},
}
/// An error while resolving, submitting, or inspecting scheduler state.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SchedulerError {
/// A Task or Schedule authoring value is invalid.
#[error(transparent)]
Schedule(#[from] ScheduleError),
/// The configured Nexus client could not complete the operation.
#[cfg(feature = "nexus")]
#[error(transparent)]
Client(Box<NexusError>),
/// The requested Task object does not exist.
#[error("Task '{task_id}' was not found")]
TaskNotFound {
/// Missing Task identifier.
task_id: sui::types::Address,
},
/// The requested occurrence record does not exist.
#[error("occurrence {occurrence_id} was not found on Task '{task_id}'")]
OccurrenceNotFound {
/// Owning Task identifier.
task_id: sui::types::Address,
/// Missing occurrence identifier.
occurrence_id: u64,
},
/// The requested operation requires a dispatched occurrence.
#[error("occurrence {occurrence_id} on Task '{task_id}' has not been dispatched")]
OccurrenceNotDispatched {
/// Owning Task identifier.
task_id: sui::types::Address,
/// Occurrence identifier.
occurrence_id: u64,
},
/// Settlement was requested from a lifecycle state other than `Finished`.
#[error(
"occurrence {occurrence_id} on Task '{task_id}' is not ready for settlement; expected \
finished, observed {observed:?}"
)]
OccurrenceNotReadyForSettlement {
/// Owning Task identifier.
task_id: sui::types::Address,
/// Occurrence identifier.
occurrence_id: u64,
/// Lifecycle state observed immediately before transaction construction.
observed: OccurrenceStatus,
},
/// A selected DAG does not contain the requested Task entry group.
#[error(
"DAG '{dag_id}' does not contain Task entry group '{entry_group}'; available groups: \
{available:?}"
)]
TaskEntryGroupNotFound {
/// Selected DAG identifier.
dag_id: sui::types::Address,
/// Requested entry group.
entry_group: String,
/// Entry groups published by the DAG.
available: Vec<String>,
},
/// Task inputs do not exactly match the selected DAG entry group.
#[error(
"Task inputs do not match entry group '{entry_group}' on DAG '{dag_id}'; expected shape \
{expected}, received shape {received}"
)]
TaskInputsMismatch {
/// Selected DAG identifier.
dag_id: sui::types::Address,
/// Selected entry group.
entry_group: String,
/// Required input JSON shape with placeholder values.
expected: String,
/// Supplied input JSON shape without user values.
received: String,
},
/// One provided Task input does not conform to the immutable Tool schema stored on its DAG vertex.
#[error(
"Task input '{vertex}.{port}' on DAG '{dag_id}' does not conform to MetaSchema; expected \
{expected}, received {received}"
)]
TaskInputSchemaMismatch {
/// Effective DAG identifier.
dag_id: sui::types::Address,
/// Entry vertex name.
vertex: String,
/// Entry input port name.
port: String,
/// Required cardinality and value kind.
expected: Box<str>,
/// Received cardinality and value kind.
received: Box<str>,
},
/// A pinned Agent skill forbids a caller-selected DAG, even when it repeats the pinned ID.
#[error(
"Agent '{agent_id}' skill {skill_id} is pinned to DAG '{pinned_dag}' and rejects caller-selected DAG '{selected_dag}'"
)]
PinnedSkillDagSelectionConflict {
/// Selected Agent.
agent_id: sui::types::Address,
/// Selected Agent-local skill.
skill_id: u64,
/// Authoritative registered DAG.
pinned_dag: sui::types::Address,
/// Rejected caller-selected DAG.
selected_dag: sui::types::Address,
},
/// A runtime-selected Agent skill requires the Task author to choose a DAG.
#[error("Agent '{agent_id}' skill {skill_id} requires a caller-selected DAG")]
RuntimeSelectedSkillDagRequired {
/// Selected Agent.
agent_id: sui::types::Address,
/// Selected Agent-local skill.
skill_id: u64,
},
/// The configured signer cannot satisfy the Task controller.
#[error("authority for Task '{task_id}' is unavailable: {message}")]
AuthorityUnavailable {
/// Task whose controller cannot be resolved.
task_id: sui::types::Address,
/// Human readable authority mismatch.
message: String,
},
/// An onchain object did not have the required type or shape.
#[error("object '{object_id}' is invalid: {message}")]
InvalidObject {
/// Invalid object identifier.
object_id: sui::types::Address,
/// Human readable schema mismatch.
message: String,
},
/// Binary Canonical Serialization failed at the Move boundary.
#[error("scheduler BCS conversion failed")]
Bcs(#[source] bcs::Error),
/// RPC or network transport failed.
#[error("scheduler transport failed: {source}")]
Transport {
/// Original transport failure.
#[source]
source: ErrorSource,
},
/// Programmable transaction construction failed.
#[error("scheduler transaction construction failed: {source}")]
Transaction {
/// Original construction failure.
#[source]
source: ErrorSource,
},
/// A submitted transaction was not confirmed as expected.
#[error("scheduler transaction confirmation failed: {message}")]
Confirmation {
/// Confirmation failure details.
message: String,
},
/// Confirmed chain data violates the scheduler's structural invariants.
#[error("inconsistent scheduler chain state: {message}")]
InconsistentChainState {
/// Invariant violation details.
message: String,
},
/// A scheduler query option is invalid.
#[error("invalid scheduler request: {message}")]
InvalidRequest {
/// Invalid request details.
message: String,
},
/// Watching an occurrence reached its timeout.
#[error("timed out while waiting for occurrence lifecycle completion")]
WatchTimedOut {
/// Last object backed snapshot observed before timeout.
last_snapshot: Box<OccurrenceSnapshot>,
},
}
impl SchedulerError {
pub(crate) fn transport<E>(source: E) -> Self
where
E: Into<anyhow::Error>,
{
let source = source.into();
Self::Transport {
source: source.into_boxed_dyn_error(),
}
}
pub(crate) fn transaction<E>(source: E) -> Self
where
E: Into<anyhow::Error>,
{
let source = source.into();
Self::Transaction {
source: source.into_boxed_dyn_error(),
}
}
}
#[cfg(feature = "nexus")]
impl From<NexusError> for SchedulerError {
fn from(error: NexusError) -> Self {
Self::Client(Box::new(error))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "nexus")]
#[test]
fn client_errors_remain_typed() {
let tag = crate::sui::types::StructTag::new(
crate::sui::types::Address::from_static("0xa"),
crate::sui::types::Identifier::from_static("era"),
crate::sui::types::Identifier::from_static("V2"),
vec![],
);
let source: crate::nexus::error::NexusError =
crate::nexus::error::ClientUpgradeRequired::new(
crate::sui::types::Address::from_static("0xb"),
tag,
None,
)
.into();
let error = SchedulerError::from(source);
assert!(matches!(
error,
SchedulerError::Client(source)
if matches!(
*source,
crate::nexus::error::NexusError::ClientUpgradeRequired(_)
)
));
}
#[test]
fn boundary_errors_retain_transport_and_transaction_sources() {
let transport = SchedulerError::transport(anyhow::anyhow!("RPC unavailable"));
assert_eq!(
transport.to_string(),
"scheduler transport failed: RPC unavailable"
);
assert_eq!(
transport.source().map(ToString::to_string).as_deref(),
Some("RPC unavailable")
);
let transaction = SchedulerError::transaction(anyhow::anyhow!("invalid PTB"));
assert_eq!(
transaction.to_string(),
"scheduler transaction construction failed: invalid PTB"
);
assert_eq!(
transaction.source().map(ToString::to_string).as_deref(),
Some("invalid PTB")
);
}
}