1use std::fmt;
4use std::future::Future;
5use std::marker::PhantomData;
6use std::sync::Arc;
7
8use ferrin_spec::BoxFuture;
9use ferrin_spec::JsonValue;
10use ferrin_spec::LanguageModelRef;
11use ferrin_spec::ToolChoice;
12use ferrin_spec::ToolName;
13use ferrin_tool::ToolSet;
14
15use super::Agent;
16use super::AgentCall;
17use super::AgentInput;
18use super::AgentStreamCall;
19use crate::error::Error;
20use crate::generate_text::GenerateText;
21use crate::generate_text::GenerateTextResult;
22use crate::generate_text::Include;
23use crate::generate_text::StopCondition;
24use crate::generate_text::config::CallConfig;
25use crate::generate_text::step_count;
26use crate::output::NoOutput;
27use crate::output::Output;
28use crate::output::OutputHandler;
29use crate::prompt::CallSettings;
30use crate::prompt::Instructions;
31use crate::retry::RetryPolicy;
32use crate::stream_text::StreamText;
33use crate::stream_text::StreamTextResult;
34use crate::telemetry::TelemetryOptions;
35use crate::timeout::Timeout;
36
37pub const AGENT_USER_AGENT: &str = "ferrin-agent/tool-loop";
39
40const DEFAULT_MAX_STEPS: u32 = 20;
42
43#[derive(Clone)]
49pub struct PreparedCall {
50 pub input: AgentInput,
52 pub instructions: Option<Instructions>,
54 pub allow_system_in_messages: bool,
56 pub model: LanguageModelRef,
58 pub tools: ToolSet,
60 pub tool_choice: Option<ToolChoice>,
62 pub active_tools: Option<Vec<ToolName>>,
64 pub tool_order: Vec<ToolName>,
66 pub tools_context: Option<JsonValue>,
68 pub settings: CallSettings,
70 pub stop_conditions: Vec<Arc<dyn StopCondition>>,
72 pub timeout: Timeout,
74 pub retry_policy: RetryPolicy,
76 pub include: Include,
78 pub max_tool_concurrency: Option<usize>,
80 pub telemetry: TelemetryOptions,
82}
83
84impl fmt::Debug for PreparedCall {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.debug_struct("PreparedCall")
87 .field("input", &self.input)
88 .field("instructions", &self.instructions)
89 .field("allow_system_in_messages", &self.allow_system_in_messages)
90 .field("model", &self.model)
91 .field("tools", &self.tools.names().collect::<Vec<_>>())
92 .field("tool_choice", &self.tool_choice)
93 .field("active_tools", &self.active_tools)
94 .field("tool_order", &self.tool_order)
95 .field("tools_context", &self.tools_context)
96 .field("settings", &self.settings)
97 .field("stop_conditions", &self.stop_conditions.len())
98 .field("timeout", &self.timeout)
99 .field("retry_policy", &self.retry_policy)
100 .field("include", &self.include)
101 .field("max_tool_concurrency", &self.max_tool_concurrency)
102 .finish_non_exhaustive()
103 }
104}
105
106#[derive(Debug)]
108pub struct PrepareCallInput<Opt> {
109 pub options: Opt,
111 pub defaults: PreparedCall,
114}
115
116pub trait PrepareCall<Opt>: Send + Sync {
119 fn prepare_call(
121 &self,
122 input: PrepareCallInput<Opt>,
123 ) -> BoxFuture<'_, Result<PreparedCall, Error>>;
124}
125
126impl<Opt, F, Fut> PrepareCall<Opt> for F
127where
128 F: Fn(PrepareCallInput<Opt>) -> Fut + Send + Sync,
129 Fut: Future<Output = Result<PreparedCall, Error>> + Send + 'static,
130{
131 fn prepare_call(
132 &self,
133 input: PrepareCallInput<Opt>,
134 ) -> BoxFuture<'_, Result<PreparedCall, Error>> {
135 Box::pin(self(input))
136 }
137}
138
139struct Settings<Opt, Out> {
140 id: Option<String>,
141 config: CallConfig,
142 output: Arc<dyn OutputHandler<Out>>,
143 prepare_call: Option<Arc<dyn PrepareCall<Opt>>>,
144}
145
146pub struct ToolLoopAgent<Opt = (), Out = ()> {
149 settings: Arc<Settings<Opt, Out>>,
150}
151
152impl<Opt, Out> Clone for ToolLoopAgent<Opt, Out> {
153 fn clone(&self) -> Self {
154 Self {
155 settings: Arc::clone(&self.settings),
156 }
157 }
158}
159
160impl<Opt, Out> fmt::Debug for ToolLoopAgent<Opt, Out> {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 f.debug_struct("ToolLoopAgent")
163 .field("id", &self.settings.id)
164 .field("config", &self.settings.config)
165 .field("has_prepare_call", &self.settings.prepare_call.is_some())
166 .finish()
167 }
168}
169
170impl ToolLoopAgent {
171 #[must_use]
173 pub fn builder(model: impl Into<LanguageModelRef>) -> ToolLoopAgentBuilder<(), ()> {
174 ToolLoopAgentBuilder {
175 id: None,
176 config: CallConfig::new(model.into()),
177 output: Arc::new(NoOutput),
178 prepare_call: None,
179 _options: PhantomData,
180 }
181 }
182}
183
184impl<Opt: Send + 'static, Out: Send + 'static> ToolLoopAgent<Opt, Out> {
185 fn effective(&self, call: &AgentCall<Opt>) -> PreparedCall {
187 let config = &self.settings.config;
188 PreparedCall {
189 input: call.input.clone(),
190 instructions: config.system.clone(),
191 allow_system_in_messages: config.allow_system_in_messages,
192 model: config.model.clone(),
193 tools: config.tools.clone(),
194 tool_choice: config.tool_choice.clone(),
195 active_tools: config.active_tools.clone(),
196 tool_order: config.tool_order.clone(),
197 tools_context: config.tools_context.clone(),
198 settings: config.settings.clone(),
199 stop_conditions: if config.stop_conditions.is_empty() {
200 vec![Arc::new(step_count(DEFAULT_MAX_STEPS))]
201 } else {
202 config.stop_conditions.clone()
203 },
204 timeout: call
205 .timeout
206 .clone()
207 .unwrap_or_else(|| config.timeout.clone()),
208 retry_policy: config.retry_policy.clone(),
209 include: config.include,
210 max_tool_concurrency: config.max_tool_concurrency,
211 telemetry: config.telemetry.clone(),
212 }
213 }
214
215 async fn prepare(&self, call: AgentCall<Opt>) -> Result<CallConfig, Error> {
217 let mut prepared = self.effective(&call);
218 let AgentCall {
219 options,
220 cancellation,
221 hooks,
222 #[cfg(feature = "sandbox")]
223 sandbox,
224 ..
225 } = call;
226 if let Some(prepare_call) = &self.settings.prepare_call {
227 prepared = prepare_call
228 .prepare_call(PrepareCallInput {
229 options,
230 defaults: prepared,
231 })
232 .await?;
233 }
234 let mut config = self.settings.config.clone();
235 match prepared.input {
236 AgentInput::Prompt(prompt) => {
237 config.prompt = Some(prompt);
238 config.messages = None;
239 }
240 AgentInput::Messages(messages) => {
241 config.prompt = None;
242 config.messages = Some(messages);
243 }
244 }
245 config.system = prepared.instructions;
246 config.allow_system_in_messages = prepared.allow_system_in_messages;
247 config.model = prepared.model;
248 config.tools = prepared.tools;
249 config.tool_choice = prepared.tool_choice;
250 config.active_tools = prepared.active_tools;
251 config.tool_order = prepared.tool_order;
252 config.tools_context = prepared.tools_context;
253 config.settings = prepared.settings;
254 config.settings.headers = config
255 .settings
256 .headers
257 .with_user_agent_suffix([AGENT_USER_AGENT]);
258 config.stop_conditions = prepared.stop_conditions;
259 config.timeout = prepared.timeout;
260 config.retry_policy = prepared.retry_policy;
261 config.include = prepared.include;
262 config.max_tool_concurrency = prepared.max_tool_concurrency;
263 config.telemetry = prepared.telemetry;
264 if config.telemetry.function_id.is_none() {
265 config.telemetry.function_id = self.settings.id.clone();
266 }
267 config.cancellation = cancellation;
268 config.hooks = self.settings.config.hooks.clone().merged(hooks);
270 #[cfg(feature = "sandbox")]
271 if let Some(sandbox) = sandbox {
272 config.sandbox = Some(sandbox);
273 }
274 Ok(config)
275 }
276}
277
278impl<Opt: Send + 'static, Out: Send + 'static> Agent for ToolLoopAgent<Opt, Out> {
279 type Options = Opt;
280 type Output = Out;
281
282 fn id(&self) -> Option<&str> {
283 self.settings.id.as_deref()
284 }
285
286 fn tools(&self) -> &ToolSet {
287 &self.settings.config.tools
288 }
289
290 async fn generate(&self, call: AgentCall<Opt>) -> Result<GenerateTextResult<Out>, Error> {
291 let config = self.prepare(call).await?;
292 GenerateText {
293 config,
294 output: Arc::clone(&self.settings.output),
295 }
296 .await
297 }
298
299 async fn stream(&self, call: AgentStreamCall<Opt>) -> Result<StreamTextResult<Out>, Error> {
300 let AgentStreamCall { call, stream } = call;
301 let config = self.prepare(call).await?;
302 StreamText {
303 config,
304 output: Arc::clone(&self.settings.output),
305 stream,
306 }
307 .await
308 }
309}
310
311pub struct ToolLoopAgentBuilder<Opt, Out> {
314 id: Option<String>,
315 pub(crate) config: CallConfig,
316 output: Arc<dyn OutputHandler<Out>>,
317 prepare_call: Option<Arc<dyn PrepareCall<Opt>>>,
318 _options: PhantomData<fn() -> Opt>,
319}
320
321impl<Opt, Out> fmt::Debug for ToolLoopAgentBuilder<Opt, Out> {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 f.debug_struct("ToolLoopAgentBuilder")
324 .field("id", &self.id)
325 .field("config", &self.config)
326 .field("has_prepare_call", &self.prepare_call.is_some())
327 .finish()
328 }
329}
330
331crate::builder::impl_call_builder!(ToolLoopAgentBuilder<Opt, Out>);
332
333impl<Opt, Out> ToolLoopAgentBuilder<Opt, Out> {
334 #[must_use]
336 pub fn id(mut self, id: impl Into<String>) -> Self {
337 self.id = Some(id.into());
338 self
339 }
340
341 #[must_use]
343 pub fn instructions(mut self, instructions: impl Into<Instructions>) -> Self {
344 self.config.system = Some(instructions.into());
345 self
346 }
347
348 #[must_use]
350 pub fn output<T>(self, output: Output<T>) -> ToolLoopAgentBuilder<Opt, T> {
351 ToolLoopAgentBuilder {
352 id: self.id,
353 config: self.config,
354 output: output.handler(),
355 prepare_call: self.prepare_call,
356 _options: PhantomData,
357 }
358 }
359
360 #[must_use]
363 pub fn call_options<O>(self) -> ToolLoopAgentBuilder<O, Out> {
364 ToolLoopAgentBuilder {
365 id: self.id,
366 config: self.config,
367 output: self.output,
368 prepare_call: None,
369 _options: PhantomData,
370 }
371 }
372
373 #[must_use]
375 pub fn prepare_call(mut self, prepare: impl PrepareCall<Opt> + 'static) -> Self {
376 self.prepare_call = Some(Arc::new(prepare));
377 self
378 }
379
380 #[must_use]
382 pub fn build(self) -> ToolLoopAgent<Opt, Out> {
383 ToolLoopAgent {
384 settings: Arc::new(Settings {
385 id: self.id,
386 config: self.config,
387 output: self.output,
388 prepare_call: self.prepare_call,
389 }),
390 }
391 }
392}