Skip to main content

ferrin_core/agent/
mod.rs

1//! Agents: reusable configurations of the generation loop.
2//!
3//! [`Agent`] is the abstraction; [`ToolLoopAgent`] is the built-in
4//! implementation that delegates to `generate_text` and `stream_text`.
5//! Applications may implement [`Agent`] themselves to combine models or
6//! add planning logic; an agent can also serve as the executor of a tool
7//! (sub-agent pattern), typically returning `GenerateTextResult::text()`
8//! as the tool output.
9
10mod tool_loop_agent;
11
12use std::fmt;
13use std::future::Future;
14use std::sync::Arc;
15
16use ferrin_message::Message;
17use ferrin_tool::ToolSet;
18use tokio_util::sync::CancellationToken;
19
20pub use tool_loop_agent::AGENT_USER_AGENT;
21pub use tool_loop_agent::PrepareCall;
22pub use tool_loop_agent::PrepareCallInput;
23pub use tool_loop_agent::PreparedCall;
24pub use tool_loop_agent::ToolLoopAgent;
25pub use tool_loop_agent::ToolLoopAgentBuilder;
26
27use crate::error::Error;
28use crate::generate_text::GenerateTextResult;
29use crate::generate_text::StepResult;
30use crate::hooks::HookFn;
31use crate::hooks::Hooks;
32use crate::stream_text::OnErrorFn;
33use crate::stream_text::StreamConfig;
34use crate::stream_text::StreamEvent;
35use crate::stream_text::StreamTextResult;
36use crate::stream_text::StreamTransform;
37use crate::telemetry::AbortEvent;
38use crate::telemetry::EndEvent;
39use crate::timeout::Timeout;
40
41/// An agent: a model plus configuration that answers prompts, possibly over
42/// several tool-calling steps.
43pub trait Agent: Send + Sync {
44    /// Per-call options (`()` when the agent takes none).
45    type Options: Send + 'static;
46    /// Structured output type (`()` when there is none).
47    type Output: Send + 'static;
48
49    /// Optional identifier used in telemetry.
50    fn id(&self) -> Option<&str>;
51
52    /// Tools available to the agent.
53    fn tools(&self) -> &ToolSet;
54
55    /// Runs the agent to completion.
56    fn generate(
57        &self,
58        call: AgentCall<Self::Options>,
59    ) -> impl Future<Output = Result<GenerateTextResult<Self::Output>, Error>> + Send;
60
61    /// Runs the agent as a stream.
62    fn stream(
63        &self,
64        call: AgentStreamCall<Self::Options>,
65    ) -> impl Future<Output = Result<StreamTextResult<Self::Output>, Error>> + Send;
66}
67
68/// What the agent is asked about.
69#[derive(Debug, Clone, PartialEq)]
70pub enum AgentInput {
71    /// A single user prompt.
72    Prompt(String),
73    /// A conversation.
74    Messages(Vec<Message>),
75}
76
77impl From<&str> for AgentInput {
78    fn from(text: &str) -> Self {
79        Self::Prompt(text.to_owned())
80    }
81}
82
83impl From<String> for AgentInput {
84    fn from(text: String) -> Self {
85        Self::Prompt(text)
86    }
87}
88
89impl From<Vec<Message>> for AgentInput {
90    fn from(messages: Vec<Message>) -> Self {
91        Self::Messages(messages)
92    }
93}
94
95/// Parameters of one agent call.
96pub struct AgentCall<O> {
97    /// The input.
98    pub input: AgentInput,
99    /// Per-call options.
100    pub options: O,
101    /// Cancels the call.
102    pub cancellation: CancellationToken,
103    /// Overrides the agent's timeouts when set.
104    pub timeout: Option<Timeout>,
105    /// Hooks run after the agent's own hooks.
106    pub hooks: Hooks,
107    /// Sandbox passed to tools.
108    #[cfg(feature = "sandbox")]
109    pub sandbox: Option<Arc<dyn ferrin_tool::Sandbox>>,
110}
111
112impl<O: fmt::Debug> fmt::Debug for AgentCall<O> {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.debug_struct("AgentCall")
115            .field("input", &self.input)
116            .field("options", &self.options)
117            .field("timeout", &self.timeout)
118            .finish_non_exhaustive()
119    }
120}
121
122impl AgentCall<()> {
123    /// A call without options.
124    #[must_use]
125    pub fn new(input: impl Into<AgentInput>) -> Self {
126        Self {
127            input: input.into(),
128            options: (),
129            cancellation: CancellationToken::new(),
130            timeout: None,
131            hooks: Hooks::default(),
132            #[cfg(feature = "sandbox")]
133            sandbox: None,
134        }
135    }
136
137    /// A call with a single user prompt.
138    #[must_use]
139    pub fn prompt(text: impl Into<String>) -> Self {
140        Self::new(AgentInput::Prompt(text.into()))
141    }
142
143    /// A call with a conversation.
144    #[must_use]
145    pub fn messages(messages: impl IntoIterator<Item = Message>) -> Self {
146        Self::new(AgentInput::Messages(messages.into_iter().collect()))
147    }
148}
149
150impl<O> AgentCall<O> {
151    /// Sets the per-call options.
152    #[must_use]
153    pub fn options<P>(self, options: P) -> AgentCall<P> {
154        AgentCall {
155            input: self.input,
156            options,
157            cancellation: self.cancellation,
158            timeout: self.timeout,
159            hooks: self.hooks,
160            #[cfg(feature = "sandbox")]
161            sandbox: self.sandbox,
162        }
163    }
164
165    /// Sets the cancellation token.
166    #[must_use]
167    pub fn cancellation(mut self, token: CancellationToken) -> Self {
168        self.cancellation = token;
169        self
170    }
171
172    /// Overrides the agent's timeouts.
173    #[must_use]
174    pub fn timeout(mut self, timeout: impl Into<Timeout>) -> Self {
175        self.timeout = Some(timeout.into());
176        self
177    }
178
179    /// Adds hooks (run after the agent's hooks).
180    #[must_use]
181    pub fn hooks(mut self, hooks: Hooks) -> Self {
182        self.hooks = self.hooks.merged(hooks);
183        self
184    }
185
186    /// Adds a step-end hook.
187    #[must_use]
188    pub fn on_step_end(mut self, f: impl HookFn<StepResult>) -> Self {
189        self.hooks.on_step_end.push(Arc::new(f));
190        self
191    }
192
193    /// Adds an end hook.
194    #[must_use]
195    pub fn on_end(mut self, f: impl HookFn<EndEvent>) -> Self {
196        self.hooks.on_end.push(Arc::new(f));
197        self
198    }
199
200    /// Sets the sandbox passed to tools.
201    #[cfg(feature = "sandbox")]
202    #[must_use]
203    pub fn sandbox(mut self, sandbox: Arc<dyn ferrin_tool::Sandbox>) -> Self {
204        self.sandbox = Some(sandbox);
205        self
206    }
207
208    /// Converts into a streaming call.
209    #[must_use]
210    pub fn streaming(self) -> AgentStreamCall<O> {
211        AgentStreamCall::new(self)
212    }
213}
214
215/// Parameters of one streaming agent call.
216pub struct AgentStreamCall<O> {
217    /// The shared call parameters.
218    pub call: AgentCall<O>,
219    pub(crate) stream: StreamConfig,
220}
221
222impl<O: fmt::Debug> fmt::Debug for AgentStreamCall<O> {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        f.debug_struct("AgentStreamCall")
225            .field("call", &self.call)
226            .field("stream", &self.stream)
227            .finish()
228    }
229}
230
231impl<O> From<AgentCall<O>> for AgentStreamCall<O> {
232    fn from(call: AgentCall<O>) -> Self {
233        Self::new(call)
234    }
235}
236
237impl<O> AgentStreamCall<O> {
238    /// Wraps a call.
239    #[must_use]
240    pub fn new(call: AgentCall<O>) -> Self {
241        Self {
242            call,
243            stream: StreamConfig::default(),
244        }
245    }
246
247    /// Adds a stream transform (applied in order).
248    #[must_use]
249    pub fn transform(mut self, transform: impl StreamTransform + 'static) -> Self {
250        self.stream.transforms.push(Arc::new(transform));
251        self
252    }
253
254    /// Forwards raw provider chunks.
255    #[must_use]
256    pub fn include_raw_chunks(mut self) -> Self {
257        self.stream.include_raw_chunks = true;
258        self
259    }
260
261    /// Enables automatic retries of failed model streams.
262    #[must_use]
263    pub fn stream_retries(mut self, retries: u32) -> Self {
264        self.stream.stream_retries = Some(retries);
265        self
266    }
267
268    /// Sets the stream error callback.
269    #[must_use]
270    pub fn on_error(mut self, f: impl OnErrorFn) -> Self {
271        self.stream.on_error = Some(Arc::new(f));
272        self
273    }
274
275    /// Adds a chunk hook.
276    #[must_use]
277    pub fn on_chunk(mut self, f: impl HookFn<StreamEvent>) -> Self {
278        self.call.hooks.on_chunk.push(Arc::new(f));
279        self
280    }
281
282    /// Adds an abort hook.
283    #[must_use]
284    pub fn on_abort(mut self, f: impl HookFn<AbortEvent>) -> Self {
285        self.call.hooks.on_abort.push(Arc::new(f));
286        self
287    }
288}