1use std::sync::Arc;
2
3use crate::{
4 agent::Agent,
5 completion::{CompletionModel, Prompt},
6 tool::{DynamicTool, ToolExecutionError, ToolOutput},
7};
8use schemars::{JsonSchema, schema_for};
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11
12#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
13struct AgentToolArgs {
14 prompt: String,
16}
17
18const DEFAULT_AGENT_TOOL_NAME: &str = "agent_tool";
19
20impl<M: CompletionModel + 'static> Agent<M> {
21 pub fn into_tool(self) -> DynamicTool {
27 let name = self
28 .name
29 .clone()
30 .unwrap_or_else(|| DEFAULT_AGENT_TOOL_NAME.to_string());
31 let description = format!(
32 "
33 Prompt a sub-agent to do a task for you.
34
35 Agent name: {name}
36 Agent description: {description}
37 Agent system prompt: {sysprompt}
38 ",
39 name = name,
40 description = self.description.clone().unwrap_or_default(),
41 sysprompt = self.preamble.clone().unwrap_or_default()
42 );
43 let parameters = json!(schema_for!(AgentToolArgs));
44 let agent = Arc::new(self);
45
46 DynamicTool::new(name, description, parameters, move |context, args| {
47 let agent = Arc::clone(&agent);
48 let inherited_context = context.inbound_only();
49 Box::pin(async move {
50 let args: AgentToolArgs = serde_json::from_value(args).map_err(|error| {
51 ToolExecutionError::invalid_args(format!(
52 "failed to parse agent tool arguments: {error}"
53 ))
54 .with_source(error)
55 })?;
56 agent
57 .prompt(args.prompt)
58 .tool_context(inherited_context)
59 .await
60 .map(ToolOutput::text)
61 .map_err(ToolExecutionError::from_error)
62 })
63 })
64 }
65}
66
67impl<M: CompletionModel + 'static> From<Agent<M>> for DynamicTool {
68 fn from(agent: Agent<M>) -> Self {
69 agent.into_tool()
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::agent::AgentBuilder;
77 use crate::test_utils::{MockCompletionModel, MockContextProbeTool, MockTurn, SessionId};
78 use crate::tool::ToolContext;
79
80 #[tokio::test]
83 async fn context_propagates_into_sub_agent() {
84 let probe = MockContextProbeTool::default();
86 let inner_model = MockCompletionModel::new([
87 MockTurn::tool_call("c1", "context_probe", json!({})),
88 MockTurn::text("inner done"),
89 ]);
90 let inner = AgentBuilder::new(inner_model)
91 .name("researcher")
92 .tool(probe.clone())
93 .build();
94
95 let outer_model = MockCompletionModel::new([
98 MockTurn::tool_call("c2", "researcher", json!({"prompt": "do research"})),
99 MockTurn::text("outer done"),
100 ]);
101 let outer = AgentBuilder::new(outer_model)
102 .dynamic_tool(inner.into_tool())
103 .build();
104
105 let mut context = ToolContext::new();
106 context.insert(SessionId("abc-123".to_string()));
107
108 let out = outer
109 .prompt("start")
110 .tool_context(context)
111 .max_turns(5)
112 .await
113 .expect("run succeeds");
114
115 assert_eq!(out, "outer done");
116 assert_eq!(probe.observed().as_deref(), Some("session:abc-123"));
117 }
118}