Skip to main content

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}