1use std::{collections::HashMap, sync::Arc};
2
3use schemars::{JsonSchema, Schema, schema_for};
4
5use crate::{
6 agent::hook::{AgentHook, HookStack},
7 completion::{CompletionModel, Document},
8 memory::ConversationMemory,
9 message::ToolChoice,
10 tool::{
11 Tool, ToolDyn, ToolSet,
12 server::{ToolServer, ToolServerHandle},
13 },
14 vector_store::VectorStoreIndexDyn,
15};
16
17#[cfg(feature = "rmcp")]
18#[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
19use crate::tool::rmcp::McpTool as RmcpTool;
20
21use super::{Agent, OutputMode};
22
23#[cfg(feature = "rmcp")]
27fn build_rmcp_tools(
28 tools: Vec<rmcp::model::Tool>,
29 client: rmcp::service::ServerSink,
30 timeout: Option<std::time::Duration>,
31) -> Vec<(String, RmcpTool)> {
32 tools
33 .into_iter()
34 .map(|tool| {
35 let name = tool.name.to_string();
36 let rmcp_tool = RmcpTool::from_mcp_server(tool, client.clone()).with_timeout(timeout);
37 (name, rmcp_tool)
38 })
39 .collect()
40}
41
42#[derive(Default)]
50pub struct NoToolConfig;
51
52pub struct WithToolServerHandle {
57 handle: ToolServerHandle,
58}
59
60pub struct WithBuilderTools {
66 static_tools: Vec<String>,
67 tools: ToolSet,
68 dynamic_tools: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
69}
70
71pub struct AgentBuilder<M, ToolState = NoToolConfig>
97where
98 M: CompletionModel,
99{
100 name: Option<String>,
102 description: Option<String>,
104 model: M,
106 preamble: Option<String>,
108 static_context: Vec<Document>,
110 additional_params: Option<serde_json::Value>,
112 max_tokens: Option<u64>,
114 dynamic_context: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
116 temperature: Option<f64>,
118 tool_choice: Option<ToolChoice>,
120 default_max_turns: Option<usize>,
122 tool_state: ToolState,
124 hooks: HookStack<M>,
126 output_schema: Option<schemars::Schema>,
128 output_mode: OutputMode,
130 memory: Option<Arc<dyn ConversationMemory>>,
132 default_conversation_id: Option<String>,
134}
135
136impl<M, ToolState> AgentBuilder<M, ToolState>
137where
138 M: CompletionModel,
139{
140 pub fn name(mut self, name: &str) -> Self {
142 self.name = Some(name.into());
143 self
144 }
145
146 pub fn description(mut self, description: &str) -> Self {
148 self.description = Some(description.into());
149 self
150 }
151
152 pub fn preamble(mut self, preamble: &str) -> Self {
154 self.preamble = Some(preamble.into());
155 self
156 }
157
158 pub fn without_preamble(mut self) -> Self {
160 self.preamble = None;
161 self
162 }
163
164 pub fn append_preamble(mut self, doc: &str) -> Self {
166 self.preamble = Some(format!("{}\n{}", self.preamble.unwrap_or_default(), doc));
167 self
168 }
169
170 pub fn context(mut self, doc: &str) -> Self {
172 self.static_context.push(Document {
173 id: format!("static_doc_{}", self.static_context.len()),
174 text: doc.into(),
175 additional_props: HashMap::new(),
176 });
177 self
178 }
179
180 pub fn dynamic_context(
183 mut self,
184 sample: usize,
185 dynamic_context: impl VectorStoreIndexDyn + Send + Sync + 'static,
186 ) -> Self {
187 self.dynamic_context
188 .push((sample, Arc::new(dynamic_context)));
189 self
190 }
191
192 pub fn tool_choice(mut self, tool_choice: ToolChoice) -> Self {
194 self.tool_choice = Some(tool_choice);
195 self
196 }
197
198 pub fn default_max_turns(mut self, default_max_turns: usize) -> Self {
201 self.default_max_turns = Some(default_max_turns);
202 self
203 }
204
205 pub fn temperature(mut self, temperature: f64) -> Self {
207 self.temperature = Some(temperature);
208 self
209 }
210
211 pub fn max_tokens(mut self, max_tokens: u64) -> Self {
213 self.max_tokens = Some(max_tokens);
214 self
215 }
216
217 pub fn additional_params(mut self, params: serde_json::Value) -> Self {
219 self.additional_params = Some(params);
220 self
221 }
222
223 pub fn output_schema<T>(mut self) -> Self
226 where
227 T: JsonSchema,
228 {
229 self.output_schema = Some(schema_for!(T));
230 self
231 }
232
233 pub fn output_schema_raw(mut self, schema: Schema) -> Self {
235 self.output_schema = Some(schema);
236 self
237 }
238
239 pub fn output_mode(mut self, mode: OutputMode) -> Self {
244 self.output_mode = mode;
245 self
246 }
247
248 pub fn memory<B>(mut self, memory: B) -> Self
256 where
257 B: ConversationMemory + 'static,
258 {
259 self.memory = Some(Arc::new(memory));
260 self
261 }
262
263 pub fn conversation(mut self, id: impl Into<String>) -> Self {
268 self.default_conversation_id = Some(id.into());
269 self
270 }
271
272 pub fn add_hook<H>(mut self, hook: H) -> Self
280 where
281 H: AgentHook<M> + 'static,
282 {
283 self.hooks.push(hook);
284 self
285 }
286}
287
288impl<M> AgentBuilder<M, NoToolConfig>
289where
290 M: CompletionModel,
291{
292 pub fn new(model: M) -> Self {
294 Self {
295 name: None,
296 description: None,
297 model,
298 preamble: None,
299 static_context: vec![],
300 temperature: None,
301 max_tokens: None,
302 additional_params: None,
303 dynamic_context: vec![],
304 tool_choice: None,
305 default_max_turns: None,
306 tool_state: NoToolConfig,
307 hooks: HookStack::new(),
308 output_schema: None,
309 output_mode: OutputMode::default(),
310 memory: None,
311 default_conversation_id: None,
312 }
313 }
314}
315
316impl<M> AgentBuilder<M, NoToolConfig>
317where
318 M: CompletionModel,
319{
320 pub fn tool_server_handle(
326 self,
327 handle: ToolServerHandle,
328 ) -> AgentBuilder<M, WithToolServerHandle> {
329 AgentBuilder {
330 name: self.name,
331 description: self.description,
332 model: self.model,
333 preamble: self.preamble,
334 static_context: self.static_context,
335 additional_params: self.additional_params,
336 max_tokens: self.max_tokens,
337 dynamic_context: self.dynamic_context,
338 temperature: self.temperature,
339 tool_choice: self.tool_choice,
340 default_max_turns: self.default_max_turns,
341 tool_state: WithToolServerHandle { handle },
342 hooks: self.hooks,
343 output_schema: self.output_schema,
344 output_mode: self.output_mode,
345 memory: self.memory,
346 default_conversation_id: self.default_conversation_id,
347 }
348 }
349
350 pub fn tool(self, tool: impl Tool + 'static) -> AgentBuilder<M, WithBuilderTools> {
355 let toolname = tool.name();
356 AgentBuilder {
357 name: self.name,
358 description: self.description,
359 model: self.model,
360 preamble: self.preamble,
361 static_context: self.static_context,
362 additional_params: self.additional_params,
363 max_tokens: self.max_tokens,
364 dynamic_context: self.dynamic_context,
365 temperature: self.temperature,
366 tool_choice: self.tool_choice,
367 default_max_turns: self.default_max_turns,
368 tool_state: WithBuilderTools {
369 static_tools: vec![toolname],
370 tools: ToolSet::from_tools(vec![tool]),
371 dynamic_tools: vec![],
372 },
373 hooks: self.hooks,
374 output_schema: self.output_schema,
375 output_mode: self.output_mode,
376 memory: self.memory,
377 default_conversation_id: self.default_conversation_id,
378 }
379 }
380
381 pub fn tools(self, tools: Vec<Box<dyn ToolDyn>>) -> AgentBuilder<M, WithBuilderTools> {
386 let static_tools = tools.iter().map(|tool| tool.name()).collect();
387 let tools = ToolSet::from_tools_boxed(tools);
388
389 AgentBuilder {
390 name: self.name,
391 description: self.description,
392 model: self.model,
393 preamble: self.preamble,
394 static_context: self.static_context,
395 additional_params: self.additional_params,
396 max_tokens: self.max_tokens,
397 dynamic_context: self.dynamic_context,
398 temperature: self.temperature,
399 tool_choice: self.tool_choice,
400 default_max_turns: self.default_max_turns,
401 hooks: self.hooks,
402 output_schema: self.output_schema,
403 output_mode: self.output_mode,
404 memory: self.memory,
405 default_conversation_id: self.default_conversation_id,
406 tool_state: WithBuilderTools {
407 static_tools,
408 tools,
409 dynamic_tools: vec![],
410 },
411 }
412 }
413
414 #[cfg(feature = "rmcp")]
421 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
422 pub fn rmcp_tool(
423 self,
424 tool: rmcp::model::Tool,
425 client: rmcp::service::ServerSink,
426 ) -> AgentBuilder<M, WithBuilderTools> {
427 self.rmcp_tool_with_timeout(tool, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
428 }
429
430 #[cfg(feature = "rmcp")]
437 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
438 pub fn rmcp_tool_with_timeout(
439 self,
440 tool: rmcp::model::Tool,
441 client: rmcp::service::ServerSink,
442 timeout: impl Into<Option<std::time::Duration>>,
443 ) -> AgentBuilder<M, WithBuilderTools> {
444 self.with_rmcp_toolset(build_rmcp_tools(vec![tool], client, timeout.into()))
445 }
446
447 #[cfg(feature = "rmcp")]
454 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
455 pub fn rmcp_tools(
456 self,
457 tools: Vec<rmcp::model::Tool>,
458 client: rmcp::service::ServerSink,
459 ) -> AgentBuilder<M, WithBuilderTools> {
460 self.rmcp_tools_with_timeout(tools, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
461 }
462
463 #[cfg(feature = "rmcp")]
471 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
472 pub fn rmcp_tools_with_timeout(
473 self,
474 tools: Vec<rmcp::model::Tool>,
475 client: rmcp::service::ServerSink,
476 timeout: impl Into<Option<std::time::Duration>>,
477 ) -> AgentBuilder<M, WithBuilderTools> {
478 self.with_rmcp_toolset(build_rmcp_tools(tools, client, timeout.into()))
479 }
480
481 #[cfg(feature = "rmcp")]
484 fn with_rmcp_toolset(
485 self,
486 built: Vec<(String, RmcpTool)>,
487 ) -> AgentBuilder<M, WithBuilderTools> {
488 let (static_tools, toolset): (Vec<String>, Vec<RmcpTool>) = built.into_iter().unzip();
489
490 AgentBuilder {
491 name: self.name,
492 description: self.description,
493 model: self.model,
494 preamble: self.preamble,
495 static_context: self.static_context,
496 additional_params: self.additional_params,
497 max_tokens: self.max_tokens,
498 dynamic_context: self.dynamic_context,
499 temperature: self.temperature,
500 tool_choice: self.tool_choice,
501 default_max_turns: self.default_max_turns,
502 hooks: self.hooks,
503 output_schema: self.output_schema,
504 output_mode: self.output_mode,
505 memory: self.memory,
506 default_conversation_id: self.default_conversation_id,
507 tool_state: WithBuilderTools {
508 static_tools,
509 tools: ToolSet::from_tools(toolset),
510 dynamic_tools: vec![],
511 },
512 }
513 }
514
515 pub fn dynamic_tools(
520 self,
521 sample: usize,
522 dynamic_tools: impl VectorStoreIndexDyn + Send + Sync + 'static,
523 toolset: ToolSet,
524 ) -> AgentBuilder<M, WithBuilderTools> {
525 AgentBuilder {
526 name: self.name,
527 description: self.description,
528 model: self.model,
529 preamble: self.preamble,
530 static_context: self.static_context,
531 additional_params: self.additional_params,
532 max_tokens: self.max_tokens,
533 dynamic_context: self.dynamic_context,
534 temperature: self.temperature,
535 tool_choice: self.tool_choice,
536 default_max_turns: self.default_max_turns,
537 hooks: self.hooks,
538 output_schema: self.output_schema,
539 output_mode: self.output_mode,
540 memory: self.memory,
541 default_conversation_id: self.default_conversation_id,
542 tool_state: WithBuilderTools {
543 static_tools: vec![],
544 tools: toolset,
545 dynamic_tools: vec![(sample, Arc::new(dynamic_tools))],
546 },
547 }
548 }
549
550 pub fn build(self) -> Agent<M> {
554 let tool_server_handle = ToolServer::new().run();
555
556 Agent {
557 name: self.name,
558 description: self.description,
559 model: Arc::new(self.model),
560 preamble: self.preamble,
561 static_context: self.static_context,
562 temperature: self.temperature,
563 max_tokens: self.max_tokens,
564 additional_params: self.additional_params,
565 tool_choice: self.tool_choice,
566 dynamic_context: Arc::new(self.dynamic_context),
567 tool_server_handle,
568 default_max_turns: self.default_max_turns,
569 hooks: self.hooks,
570 output_schema: self.output_schema,
571 output_mode: self.output_mode,
572 memory: self.memory,
573 default_conversation_id: self.default_conversation_id,
574 }
575 }
576}
577
578impl<M> AgentBuilder<M, WithToolServerHandle>
579where
580 M: CompletionModel,
581{
582 pub fn build(self) -> Agent<M> {
584 Agent {
585 name: self.name,
586 description: self.description,
587 model: Arc::new(self.model),
588 preamble: self.preamble,
589 static_context: self.static_context,
590 temperature: self.temperature,
591 max_tokens: self.max_tokens,
592 additional_params: self.additional_params,
593 tool_choice: self.tool_choice,
594 dynamic_context: Arc::new(self.dynamic_context),
595 tool_server_handle: self.tool_state.handle,
596 default_max_turns: self.default_max_turns,
597 hooks: self.hooks,
598 output_schema: self.output_schema,
599 output_mode: self.output_mode,
600 memory: self.memory,
601 default_conversation_id: self.default_conversation_id,
602 }
603 }
604}
605
606impl<M> AgentBuilder<M, WithBuilderTools>
607where
608 M: CompletionModel,
609{
610 pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
612 let toolname = tool.name();
613 self.tool_state.tools.add_tool(tool);
614 self.tool_state.static_tools.push(toolname);
615 self
616 }
617
618 pub fn tools(mut self, tools: Vec<Box<dyn ToolDyn>>) -> Self {
620 let toolnames: Vec<String> = tools.iter().map(|tool| tool.name()).collect();
621 let tools = ToolSet::from_tools_boxed(tools);
622 self.tool_state.tools.add_tools(tools);
623 self.tool_state.static_tools.extend(toolnames);
624 self
625 }
626
627 #[cfg(feature = "rmcp")]
632 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
633 pub fn rmcp_tools(
634 self,
635 tools: Vec<rmcp::model::Tool>,
636 client: rmcp::service::ServerSink,
637 ) -> Self {
638 self.rmcp_tools_with_timeout(tools, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
639 }
640
641 #[cfg(feature = "rmcp")]
648 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
649 pub fn rmcp_tools_with_timeout(
650 self,
651 tools: Vec<rmcp::model::Tool>,
652 client: rmcp::service::ServerSink,
653 timeout: impl Into<Option<std::time::Duration>>,
654 ) -> Self {
655 self.add_rmcp_tools(build_rmcp_tools(tools, client, timeout.into()))
656 }
657
658 #[cfg(feature = "rmcp")]
659 fn add_rmcp_tools(mut self, built: Vec<(String, RmcpTool)>) -> Self {
660 for (name, tool) in built {
661 self.tool_state.static_tools.push(name);
662 self.tool_state.tools.add_tool(tool);
663 }
664
665 self
666 }
667
668 pub fn dynamic_tools(
671 mut self,
672 sample: usize,
673 dynamic_tools: impl VectorStoreIndexDyn + Send + Sync + 'static,
674 toolset: ToolSet,
675 ) -> Self {
676 self.tool_state
677 .dynamic_tools
678 .push((sample, Arc::new(dynamic_tools)));
679 self.tool_state.tools.add_tools(toolset);
680 self
681 }
682
683 pub fn build(self) -> Agent<M> {
688 let tool_server_handle = ToolServer::new()
689 .static_tool_names(self.tool_state.static_tools)
690 .add_tools(self.tool_state.tools)
691 .add_dynamic_tools(self.tool_state.dynamic_tools)
692 .run();
693
694 Agent {
695 name: self.name,
696 description: self.description,
697 model: Arc::new(self.model),
698 preamble: self.preamble,
699 static_context: self.static_context,
700 temperature: self.temperature,
701 max_tokens: self.max_tokens,
702 additional_params: self.additional_params,
703 tool_choice: self.tool_choice,
704 dynamic_context: Arc::new(self.dynamic_context),
705 tool_server_handle,
706 default_max_turns: self.default_max_turns,
707 hooks: self.hooks,
708 output_schema: self.output_schema,
709 output_mode: self.output_mode,
710 memory: self.memory,
711 default_conversation_id: self.default_conversation_id,
712 }
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719 use crate::test_utils::{MockAddTool, MockCompletionModel};
720
721 #[derive(Clone)]
722 struct BuilderHook;
723
724 impl AgentHook<MockCompletionModel> for BuilderHook {}
725
726 #[test]
727 fn hook_can_be_set_after_tool_configuration() {
728 let _agent = AgentBuilder::new(MockCompletionModel::text("ok"))
729 .tool(MockAddTool)
730 .add_hook(BuilderHook)
731 .build();
732 }
733
734 #[cfg(feature = "rmcp")]
739 #[tokio::test]
740 async fn build_rmcp_tools_threads_timeout_into_built_tools() {
741 use crate::tool::ToolDyn;
742 use crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT;
743 use rmcp::model::{
744 CallToolRequestParams, CallToolResult, ClientInfo, ErrorData, Implementation,
745 ProtocolVersion, ServerCapabilities, ServerInfo, Tool,
746 };
747 use rmcp::service::RequestContext;
748 use rmcp::{RoleServer, ServerHandler, ServiceExt};
749 use std::sync::Arc;
750 use std::time::Duration;
751
752 #[derive(Clone)]
753 struct HangingServer;
754 impl ServerHandler for HangingServer {
755 fn get_info(&self) -> ServerInfo {
756 ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
757 .with_protocol_version(ProtocolVersion::LATEST)
758 .with_server_info(Implementation::new("builder-timeout-test", "0.1.0"))
759 }
760 async fn call_tool(
761 &self,
762 _request: CallToolRequestParams,
763 _context: RequestContext<RoleServer>,
764 ) -> Result<CallToolResult, ErrorData> {
765 std::future::pending::<Result<CallToolResult, ErrorData>>().await
766 }
767 }
768
769 fn tool(name: &str) -> Tool {
770 Tool::new(
771 name.to_string(),
772 String::new(),
773 Arc::new(serde_json::Map::new()),
774 )
775 }
776
777 let (c2s, sfc) = tokio::io::duplex(8192);
778 let (s2c, cfs) = tokio::io::duplex(8192);
779 let server_task = tokio::spawn(async move {
780 let running = HangingServer.serve((sfc, s2c)).await.expect("server start");
781 running.waiting().await.expect("server error");
782 });
783 let client = ClientInfo::default()
784 .serve((cfs, c2s))
785 .await
786 .expect("client connect");
787 let peer = client.peer().clone();
788
789 let built_default = build_rmcp_tools(
792 vec![tool("a")],
793 peer.clone(),
794 Some(DEFAULT_MCP_TOOL_TIMEOUT),
795 );
796 assert_eq!(built_default[0].1.timeout(), Some(DEFAULT_MCP_TOOL_TIMEOUT));
797 let built_none = build_rmcp_tools(vec![tool("b")], peer.clone(), None);
798 assert_eq!(built_none[0].1.timeout(), None);
799
800 let built = build_rmcp_tools(
802 vec![tool("hang_forever")],
803 peer,
804 Some(Duration::from_millis(200)),
805 );
806 assert_eq!(built.len(), 1);
807 assert_eq!(built[0].0, "hang_forever");
808 let timed =
809 tokio::time::timeout(Duration::from_secs(5), built[0].1.call("{}".to_string())).await;
810 let err = timed
811 .expect("built tool hung past the safety timeout")
812 .expect_err("call should time out");
813 assert!(err.to_string().contains("timed out"), "got: {err}");
814
815 drop(client);
816 server_task.abort();
817 }
818}