1use std::fmt;
4use std::marker::PhantomData;
5use std::sync::Arc;
6
7use ferrin_spec::LanguageModelRef;
8use ferrin_tool::ToolSet;
9
10use super::Agent;
11use super::AgentCall;
12use super::AgentInput;
13use super::AgentStreamCall;
14use super::PrepareCall;
15use super::PrepareCallInput;
16use super::PreparedCall;
17use super::options::CallOptionsValidator;
18use crate::error::Error;
19use crate::generate_text::GenerateText;
20use crate::generate_text::GenerateTextResult;
21use crate::generate_text::config::CallConfig;
22use crate::generate_text::step_count;
23use crate::output::NoOutput;
24use crate::output::Output;
25use crate::output::OutputHandler;
26use crate::prompt::Instructions;
27use crate::stream_text::StreamText;
28use crate::stream_text::StreamTextResult;
29
30pub const AGENT_USER_AGENT: &str = "ferrin-agent/tool-loop";
32
33const DEFAULT_MAX_STEPS: u32 = 20;
35
36struct Settings<Opt, Out> {
37 id: Option<String>,
38 config: CallConfig,
39 output: Arc<dyn OutputHandler<Out>>,
40 prepare_call: Option<Arc<dyn PrepareCall<Opt>>>,
41 call_options_validator: Option<CallOptionsValidator<Opt>>,
42}
43
44pub struct ToolLoopAgent<Opt = (), Out = ()> {
47 settings: Arc<Settings<Opt, Out>>,
48}
49
50impl<Opt, Out> Clone for ToolLoopAgent<Opt, Out> {
51 fn clone(&self) -> Self {
52 Self {
53 settings: Arc::clone(&self.settings),
54 }
55 }
56}
57
58impl<Opt, Out> fmt::Debug for ToolLoopAgent<Opt, Out> {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.debug_struct("ToolLoopAgent")
61 .field("id", &self.settings.id)
62 .field("config", &self.settings.config)
63 .field("has_prepare_call", &self.settings.prepare_call.is_some())
64 .field(
65 "has_call_options_schema",
66 &self.settings.call_options_validator.is_some(),
67 )
68 .finish()
69 }
70}
71
72impl ToolLoopAgent {
73 #[must_use]
75 pub fn builder(model: impl Into<LanguageModelRef>) -> ToolLoopAgentBuilder<(), ()> {
76 ToolLoopAgentBuilder {
77 id: None,
78 config: CallConfig::new(model.into()),
79 output: Arc::new(NoOutput),
80 prepare_call: None,
81 call_options_validator: None,
82 _options: PhantomData,
83 }
84 }
85}
86
87impl<Opt: Send + 'static, Out: Send + 'static> ToolLoopAgent<Opt, Out> {
88 fn effective(&self, call: &AgentCall<Opt>) -> PreparedCall {
90 let config = &self.settings.config;
91 PreparedCall {
92 input: call.input.clone(),
93 instructions: config.system.clone(),
94 allow_system_in_messages: config.allow_system_in_messages,
95 model: config.model.clone(),
96 tools: config.tools.clone(),
97 tool_choice: config.tool_choice.clone(),
98 active_tools: config.active_tools.clone(),
99 tool_order: config.tool_order.clone(),
100 tools_context: config.tools_context.clone(),
101 runtime_context: config.runtime_context.clone(),
102 tool_approval: config.tool_approval.clone(),
103 tool_approval_secret: config.tool_approval_secret.clone(),
104 tool_callers: config.tool_callers.clone(),
105 prepare_step: config.prepare_step.clone(),
106 repair_tool_call: config.repair_tool_call.clone(),
107 refine_tool_inputs: config.refine_tool_inputs.clone(),
108 download: config.download.clone(),
109 settings: config.settings.clone(),
110 stop_conditions: if config.stop_conditions.is_empty() {
111 vec![Arc::new(step_count(DEFAULT_MAX_STEPS))]
112 } else {
113 config.stop_conditions.clone()
114 },
115 timeout: config.timeout.clone(),
116 retry_policy: config.retry_policy.clone(),
117 include: config.include,
118 max_tool_concurrency: config.max_tool_concurrency,
119 telemetry: config.telemetry.clone(),
120 }
121 }
122
123 async fn prepare(&self, call: AgentCall<Opt>) -> Result<CallConfig, Error> {
125 let mut prepared = self.effective(&call);
126 let AgentCall {
127 options,
128 cancellation,
129 timeout,
130 hooks,
131 #[cfg(feature = "sandbox")]
132 sandbox,
133 ..
134 } = call;
135 let options = match &self.settings.call_options_validator {
136 Some(validate) => validate(options)?,
137 None => options,
138 };
139 if let Some(prepare_call) = &self.settings.prepare_call {
140 prepared = prepare_call
141 .prepare_call(PrepareCallInput {
142 options,
143 defaults: prepared,
144 #[cfg(feature = "sandbox")]
145 sandbox: sandbox.clone(),
146 })
147 .await?;
148 }
149 let mut config = self.settings.config.clone();
150 match prepared.input {
151 AgentInput::Prompt(prompt) => {
152 config.prompt = Some(prompt);
153 config.messages = None;
154 }
155 AgentInput::Messages(messages) => {
156 config.prompt = None;
157 config.messages = Some(messages);
158 }
159 }
160 config.system = prepared.instructions;
161 config.allow_system_in_messages = prepared.allow_system_in_messages;
162 config.model = prepared.model;
163 config.tools = prepared.tools;
164 config.tool_choice = prepared.tool_choice;
165 config.active_tools = prepared.active_tools;
166 config.tool_order = prepared.tool_order;
167 config.tools_context = prepared.tools_context;
168 config.runtime_context = prepared.runtime_context;
169 config.tool_approval = prepared.tool_approval;
170 config.tool_approval_secret = prepared.tool_approval_secret;
171 config.tool_callers = prepared.tool_callers;
172 config.prepare_step = prepared.prepare_step;
173 config.repair_tool_call = prepared.repair_tool_call;
174 config.refine_tool_inputs = prepared.refine_tool_inputs;
175 config.download = prepared.download;
176 config.settings = prepared.settings;
177 config.settings.headers = config
178 .settings
179 .headers
180 .with_user_agent_suffix([AGENT_USER_AGENT]);
181 config.stop_conditions = prepared.stop_conditions;
182 config.timeout = timeout.unwrap_or(prepared.timeout);
183 config.retry_policy = prepared.retry_policy;
184 config.include = prepared.include;
185 config.max_tool_concurrency = prepared.max_tool_concurrency;
186 config.telemetry = prepared.telemetry;
187 if config.telemetry.function_id.is_none() {
188 config.telemetry.function_id = self.settings.id.clone();
189 }
190 config.cancellation = cancellation;
191 config.hooks = self.settings.config.hooks.clone().merged(hooks);
193 #[cfg(feature = "sandbox")]
194 if let Some(sandbox) = sandbox {
195 config.sandbox = Some(sandbox);
196 }
197 Ok(config)
198 }
199}
200
201impl<Opt: Send + 'static, Out: Send + 'static> Agent for ToolLoopAgent<Opt, Out> {
202 type Options = Opt;
203 type Output = Out;
204
205 fn id(&self) -> Option<&str> {
206 self.settings.id.as_deref()
207 }
208
209 fn tools(&self) -> &ToolSet {
210 &self.settings.config.tools
211 }
212
213 async fn generate(&self, call: AgentCall<Opt>) -> Result<GenerateTextResult<Out>, Error> {
214 let config = self.prepare(call).await?;
215 GenerateText {
216 config,
217 output: Arc::clone(&self.settings.output),
218 }
219 .await
220 }
221
222 async fn stream(&self, call: AgentStreamCall<Opt>) -> Result<StreamTextResult<Out>, Error> {
223 let AgentStreamCall { call, stream } = call;
224 let config = self.prepare(call).await?;
225 StreamText {
226 config,
227 output: Arc::clone(&self.settings.output),
228 stream,
229 }
230 .await
231 }
232}
233
234pub struct ToolLoopAgentBuilder<Opt, Out> {
237 id: Option<String>,
238 pub(crate) config: CallConfig,
239 output: Arc<dyn OutputHandler<Out>>,
240 prepare_call: Option<Arc<dyn PrepareCall<Opt>>>,
241 call_options_validator: Option<CallOptionsValidator<Opt>>,
242 _options: PhantomData<fn() -> Opt>,
243}
244
245impl<Opt, Out> fmt::Debug for ToolLoopAgentBuilder<Opt, Out> {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 f.debug_struct("ToolLoopAgentBuilder")
248 .field("id", &self.id)
249 .field("config", &self.config)
250 .field("has_prepare_call", &self.prepare_call.is_some())
251 .field(
252 "has_call_options_schema",
253 &self.call_options_validator.is_some(),
254 )
255 .finish()
256 }
257}
258
259crate::builder::impl_call_builder!(ToolLoopAgentBuilder<Opt, Out>);
260
261impl<Opt, Out> ToolLoopAgentBuilder<Opt, Out> {
262 #[must_use]
264 pub fn id(mut self, id: impl Into<String>) -> Self {
265 self.id = Some(id.into());
266 self
267 }
268
269 #[must_use]
271 pub fn instructions(mut self, instructions: impl Into<Instructions>) -> Self {
272 self.config.system = Some(instructions.into());
273 self
274 }
275
276 #[must_use]
278 pub fn output<T>(self, output: Output<T>) -> ToolLoopAgentBuilder<Opt, T> {
279 ToolLoopAgentBuilder {
280 id: self.id,
281 config: self.config,
282 output: output.handler(),
283 prepare_call: self.prepare_call,
284 call_options_validator: self.call_options_validator,
285 _options: PhantomData,
286 }
287 }
288
289 #[must_use]
292 pub fn call_options<O>(self) -> ToolLoopAgentBuilder<O, Out> {
293 ToolLoopAgentBuilder {
294 id: self.id,
295 config: self.config,
296 output: self.output,
297 prepare_call: None,
298 call_options_validator: None,
299 _options: PhantomData,
300 }
301 }
302
303 #[must_use]
305 pub fn prepare_call(mut self, prepare: impl PrepareCall<Opt> + 'static) -> Self {
306 self.prepare_call = Some(Arc::new(prepare));
307 self
308 }
309
310 #[must_use]
333 pub fn call_options_schema(mut self, schema: ferrin_schema::Schema<Opt>) -> Self
334 where
335 Opt: serde::Serialize + 'static,
336 {
337 self.call_options_validator = Some(super::options::validator(schema));
338 self
339 }
340
341 #[must_use]
343 pub fn build(self) -> ToolLoopAgent<Opt, Out> {
344 ToolLoopAgent {
345 settings: Arc::new(Settings {
346 id: self.id,
347 config: self.config,
348 output: self.output,
349 prepare_call: self.prepare_call,
350 call_options_validator: self.call_options_validator,
351 }),
352 }
353 }
354}