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