Skip to main content

agent_line/
agent.rs

1use crate::ctx::Ctx;
2use std::fmt;
3
4/// The result of running a step: a new state plus what to do next.
5pub type StepResult<S> = Result<(S, Outcome), StepError>;
6
7/// A sync agent that transforms state one step at a time.
8///
9/// Implement this trait on your own structs and register them into a
10/// [`crate::Workflow`] to build a pipeline.
11pub trait Agent<S>: Send + 'static {
12    /// A unique name for this agent, used for routing with [`Outcome::Next`].
13    fn name(&self) -> &'static str;
14
15    /// Run one step. Returns the updated state and an [`Outcome`] that tells
16    /// the runner what to do next.
17    fn run(&mut self, state: S, ctx: &mut Ctx) -> StepResult<S>;
18}
19
20/// Control flow for the runner.
21#[derive(Debug, Clone)]
22pub enum Outcome {
23    /// Follow the workflow’s default next step (set via `.then()`).
24    Continue,
25
26    /// Workflow complete, return the final state.
27    Done,
28    /// Jump to a specific agent by name.
29    Next(&'static str),
30    /// Re-run the current agent (counted against `max_retries`).
31    Retry(RetryHint),
32    /// Sleep for the given duration, then re-run (counted against `max_retries`).
33    Wait(std::time::Duration),
34    /// Stop the workflow with an error.
35    Fail(String),
36}
37
38/// Metadata attached to an [`Outcome::Retry`] to explain why the agent
39/// wants to retry.
40#[derive(Debug, Clone)]
41pub struct RetryHint {
42    /// Human-readable reason for the retry.
43    pub reason: String,
44}
45
46impl RetryHint {
47    /// Create a new hint with the given reason.
48    pub fn new(reason: impl Into<String>) -> Self {
49        Self {
50            reason: reason.into(),
51        }
52    }
53}
54
55/// Error type for agent steps, with variants designed around what the caller
56/// can do about them.
57#[derive(Debug)]
58pub enum StepError {
59    /// Bad input or agent logic error. Don't retry, fix the code.
60    Invalid(String),
61    /// Transient failure (network, rate limit). Retrying might help.
62    Transient(String),
63    /// Agent decided to fail explicitly via Outcome::Fail.
64    Failed(String),
65    /// Everything else. Inspect the message for details.
66    Other(String),
67}
68
69impl From<ureq::Error> for StepError {
70    fn from(e: ureq::Error) -> Self {
71        StepError::Transient(e.to_string())
72    }
73}
74
75impl From<std::io::Error> for StepError {
76    fn from(e: std::io::Error) -> Self {
77        StepError::Other(e.to_string())
78    }
79}
80
81impl StepError {
82    /// Create an [`Invalid`](StepError::Invalid) error.
83    pub fn invalid(msg: impl Into<String>) -> Self {
84        StepError::Invalid(msg.into())
85    }
86
87    /// Create an [`Other`](StepError::Other) error.
88    pub fn other(msg: impl Into<String>) -> Self {
89        StepError::Other(msg.into())
90    }
91
92    /// Create a [`Transient`](StepError::Transient) error.
93    pub fn transient(msg: impl Into<String>) -> Self {
94        StepError::Transient(msg.into())
95    }
96}
97
98impl fmt::Display for StepError {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        match self {
101            Self::Invalid(msg) => write!(f, "invalid: {msg}"),
102            Self::Other(msg) => write!(f, "{msg}"),
103            Self::Transient(msg) => write!(f, "transient: {msg}"),
104            Self::Failed(msg) => write!(f, "failed: {msg}"),
105        }
106    }
107}
108
109impl std::error::Error for StepError {}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    // --- StepError constructors ---
116
117    #[test]
118    fn invalid_constructor() {
119        let err = StepError::invalid("bad input");
120        assert!(matches!(err, StepError::Invalid(msg) if msg == "bad input"));
121    }
122
123    #[test]
124    fn other_constructor() {
125        let err = StepError::other("something");
126        assert!(matches!(err, StepError::Other(msg) if msg == "something"));
127    }
128
129    #[test]
130    fn transient_constructor() {
131        let err = StepError::transient("timeout");
132        assert!(matches!(err, StepError::Transient(msg) if msg == "timeout"));
133    }
134
135    // --- StepError Display ---
136
137    #[test]
138    fn display_invalid() {
139        let err = StepError::Invalid("bad input".into());
140        assert_eq!(err.to_string(), "invalid: bad input");
141    }
142
143    #[test]
144    fn display_other() {
145        let err = StepError::Other("something".into());
146        assert_eq!(err.to_string(), "something");
147    }
148
149    #[test]
150    fn display_transient() {
151        let err = StepError::Transient("timeout".into());
152        assert_eq!(err.to_string(), "transient: timeout");
153    }
154
155    #[test]
156    fn display_failed() {
157        let err = StepError::Failed("nope".into());
158        assert_eq!(err.to_string(), "failed: nope");
159    }
160
161    // --- From conversions ---
162
163    #[test]
164    fn from_io_error() {
165        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
166        let step_err: StepError = io_err.into();
167        assert!(matches!(step_err, StepError::Other(msg) if msg.contains("file missing")));
168    }
169
170    // --- RetryHint ---
171
172    #[test]
173    fn retry_hint_new() {
174        let hint = RetryHint::new("reason");
175        assert_eq!(hint.reason, "reason");
176    }
177}