basis_tasks/error.rs
1//! One error type for the whole crate.
2//!
3//! Every fallible operation in the moved implementation — file I/O, decode
4//! failures, policy refusals — already carries a human-readable message; that
5//! was true when it answered only to `basis-cli`'s own `ClientError`, and
6//! nothing about becoming a library changes it. What changes is the type: a
7//! `Result<_, String>` is an implementation detail a caller should not have
8//! to `.to_string()` their way around, so the public surface returns this
9//! instead. `basis-cli`'s exit-code and hint *text* — which of these are a
10//! timeout, which are a usage error, what a `next:` line reads — stays
11//! exactly where ADR-0015 puts it, in the CLI's own `ClientError`; this type
12//! carries no opinion about either. What it does carry, for the handful of
13//! errors that name one unambiguously, is [`hint`](Error::hint) — a fact a
14//! host builds its own hint text from, not the text itself.
15
16use std::fmt;
17
18use crate::handle::TaskHandle;
19
20/// A next step this error names unambiguously enough for a host to build its
21/// own hint text from, without parsing this error's message. Not every
22/// error carries one: an ordinary operational failure has no next step this
23/// crate can name from here, and stays [`None`](Error::hint).
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Hint {
26 /// The caller is itself a task in a different workspace; spawning here
27 /// needs the detached escape — `basis spawn --detached <PROMPT>`, in
28 /// `basis-cli`'s words.
29 SpawnDetached,
30 /// No task in the target workspace has a conversation to continue yet —
31 /// `basis spawn <PROMPT>`, in `basis-cli`'s words.
32 SpawnFresh,
33 /// The conversation named exists but cannot be continued right now —
34 /// still running (one executor per conversation), or never attached —
35 /// so the next step is the task itself: `basis wait <task>`.
36 Wait(TaskHandle),
37}
38
39/// An error from a `basis-tasks` operation: what went wrong, in one line.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Error {
42 message: String,
43 invalid_reference: bool,
44 task_limit: bool,
45 hint: Option<Hint>,
46}
47
48impl Error {
49 pub(crate) fn new(message: impl Into<String>) -> Self {
50 Self {
51 message: message.into(),
52 invalid_reference: false,
53 task_limit: false,
54 hint: None,
55 }
56 }
57
58 /// `spawn` refusing because the workspace already holds
59 /// [`MAX_TASKS`](crate::MAX_TASKS) tasks.
60 pub(crate) fn task_limit(message: impl Into<String>) -> Self {
61 Self {
62 task_limit: true,
63 ..Self::new(message)
64 }
65 }
66
67 /// An error over a handle or reference the caller gave that could never
68 /// have resolved — not a task in a state that might still change (a
69 /// running task, an expired wait), but one that was never going to name
70 /// anything valid, the way a malformed handle or a handle from a
71 /// different workspace does not.
72 pub(crate) fn invalid_reference(message: impl Into<String>) -> Self {
73 Self {
74 message: message.into(),
75 invalid_reference: true,
76 task_limit: false,
77 hint: None,
78 }
79 }
80
81 /// Attaches the next step this error names, for a host that wants to
82 /// build a hint from it.
83 #[must_use]
84 pub(crate) fn with_hint(self, hint: Hint) -> Self {
85 Self {
86 hint: Some(hint),
87 ..self
88 }
89 }
90
91 /// Whether this is exactly that: a bad argument no amount of waiting
92 /// fixes, as opposed to an ordinary operational failure. A host mapping
93 /// this crate's errors onto its own vocabulary — `basis-cli`'s "usage"
94 /// exit code (ADR-0015), for one — reads this rather than the message
95 /// text.
96 pub fn is_invalid_reference(&self) -> bool {
97 self.invalid_reference
98 }
99
100 /// Whether `spawn` refused because the workspace is full — a condition
101 /// a host serving other callers reports as such, not as its own fault,
102 /// and that only archiving settled tasks clears.
103 pub fn is_task_limit(&self) -> bool {
104 self.task_limit
105 }
106
107 /// The next step this error names, when it names one unambiguously —
108 /// see [`Hint`].
109 pub fn hint(&self) -> Option<&Hint> {
110 self.hint.as_ref()
111 }
112}
113
114impl fmt::Display for Error {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 f.write_str(&self.message)
117 }
118}
119
120impl std::error::Error for Error {}
121
122impl From<String> for Error {
123 fn from(message: String) -> Self {
124 Self::new(message)
125 }
126}
127
128impl From<&str> for Error {
129 fn from(message: &str) -> Self {
130 Self::new(message)
131 }
132}