Skip to main content

vv_agent/tools/
agent_tool.rs

1use std::sync::Arc;
2
3use serde_json::{json, Value};
4
5use crate::agent::Agent;
6use crate::run_config::RunConfig;
7use crate::runner::Runner;
8use crate::tools::background_agent_task::inherited_run_config;
9use crate::tools::{Tool, ToolContext, ToolOutput, ToolSpec, ToolSpecKind};
10use crate::types::{SubTaskRequest, ToolArguments};
11
12#[derive(Clone)]
13pub struct AgentTool {
14    agent: Agent,
15    name: String,
16    description: String,
17    parameters_schema: Value,
18}
19
20impl AgentTool {
21    pub fn request_from_arguments(&self, raw_arguments: Value) -> Result<SubTaskRequest, String> {
22        let object = raw_arguments
23            .as_object()
24            .ok_or_else(|| "agent tool arguments must be an object".to_string())?;
25        let task_description = object
26            .get("task_description")
27            .or_else(|| object.get("task"))
28            .or_else(|| object.get("input"))
29            .and_then(Value::as_str)
30            .map(str::trim)
31            .filter(|value| !value.is_empty())
32            .ok_or_else(|| "agent tool requires task_description".to_string())?;
33        let mut request = SubTaskRequest::new(self.agent.name(), task_description);
34        request.output_requirements = object
35            .get("output_requirements")
36            .and_then(Value::as_str)
37            .unwrap_or_default()
38            .to_string();
39        request.include_main_summary = object
40            .get("include_main_summary")
41            .and_then(Value::as_bool)
42            .unwrap_or(false);
43        Ok(request)
44    }
45}
46
47impl Tool for AgentTool {
48    fn name(&self) -> &str {
49        &self.name
50    }
51
52    fn description(&self) -> &str {
53        &self.description
54    }
55
56    fn parameters_schema(&self) -> &Value {
57        &self.parameters_schema
58    }
59
60    fn as_tool_spec(&self) -> ToolSpec {
61        let agent_tool = self.clone();
62        let mut spec = ToolSpec::new(
63            self.name.clone(),
64            self.description.clone(),
65            Arc::new(
66                move |context: &mut ToolContext, arguments: &ToolArguments| {
67                    let raw_arguments = Value::Object(arguments.clone().into_iter().collect());
68                    let request = match agent_tool.request_from_arguments(raw_arguments) {
69                        Ok(request) => request,
70                        Err(error) => return ToolOutput::error(error).to_result(""),
71                    };
72                    let prompt = prompt_from_request(&request, context);
73                    if let Some(provider) = context.model_provider.clone() {
74                        let runner = Runner::builder()
75                            .model_provider_arc(provider)
76                            .workspace(context.workspace.clone())
77                            .build();
78                        let runner = match runner {
79                            Ok(runner) => runner,
80                            Err(error) => return ToolOutput::error(error).to_result(""),
81                        };
82                        let result = runner.run_blocking(
83                            &agent_tool.agent,
84                            crate::runner::NormalizedInput::from(prompt),
85                            inherited_run_config(context, Some(RunConfig::default())),
86                            None,
87                        );
88                        return match result {
89                            Ok(result) => {
90                                let output = result.final_output().unwrap_or_default();
91                                ToolOutput::text(output).to_result("")
92                            }
93                            Err(error) => ToolOutput::error(error).to_result(""),
94                        };
95                    }
96                    let Some(runner) = context.sub_task_runner.clone() else {
97                        return ToolOutput::error("sub-agent runtime is not available")
98                            .with_code("sub_agents_not_enabled")
99                            .to_result("");
100                    };
101                    let outcome = runner(request);
102                    ToolOutput::json(outcome.to_value()).to_result("")
103                },
104            ),
105        );
106        spec.kind = ToolSpecKind::Agent;
107        spec.schema = json!({
108            "type": "function",
109            "function": {
110                "name": self.name,
111                "description": self.description,
112                "parameters": self.parameters_schema,
113            }
114        });
115        spec
116    }
117}
118
119pub struct AgentToolBuilder {
120    agent: Agent,
121    name: Option<String>,
122    description: Option<String>,
123}
124
125impl AgentToolBuilder {
126    pub fn new(agent: Agent) -> Self {
127        Self {
128            agent,
129            name: None,
130            description: None,
131        }
132    }
133
134    pub fn name(mut self, name: impl Into<String>) -> Self {
135        self.name = Some(name.into());
136        self
137    }
138
139    pub fn description(mut self, description: impl Into<String>) -> Self {
140        self.description = Some(description.into());
141        self
142    }
143
144    pub fn build(self) -> Result<AgentTool, String> {
145        let name = self.name.unwrap_or_else(|| self.agent.name().to_string());
146        if name.trim().is_empty() {
147            return Err("agent tool name cannot be empty".to_string());
148        }
149        let description = self
150            .description
151            .unwrap_or_else(|| format!("Run the {} agent as a delegated task.", self.agent.name()));
152        Ok(AgentTool {
153            agent: self.agent,
154            name,
155            description,
156            parameters_schema: json!({
157                "type": "object",
158                "properties": {
159                    "task_description": {
160                        "type": "string",
161                        "description": "Task for the delegated agent."
162                    },
163                    "output_requirements": {
164                        "type": "string",
165                        "description": "Optional output requirements for the delegated agent."
166                    },
167                    "include_main_summary": {
168                        "type": "boolean",
169                        "description": "Whether to include parent task summary."
170                    }
171                },
172                "required": ["task_description"],
173                "additionalProperties": false
174            }),
175        })
176    }
177}
178
179fn prompt_from_request(request: &SubTaskRequest, context: &ToolContext) -> String {
180    let mut prompt = request.task_description.clone();
181    if !request.output_requirements.is_empty() {
182        prompt.push_str("\n\n<Output Requirements>\n");
183        prompt.push_str(&request.output_requirements);
184        prompt.push_str("\n</Output Requirements>");
185    }
186    if request.include_main_summary {
187        let parent_summary = context
188            .shared_state
189            .get("main_task_summary")
190            .and_then(Value::as_str)
191            .or_else(|| {
192                context
193                    .metadata
194                    .get("_vv_agent_input")
195                    .and_then(Value::as_str)
196            })
197            .map(str::trim)
198            .filter(|summary| !summary.is_empty());
199        if let Some(parent_summary) = parent_summary {
200            prompt.push_str("\n\n<Main Task Summary>\n");
201            prompt.push_str(parent_summary);
202            prompt.push_str("\n</Main Task Summary>");
203        }
204    }
205    prompt
206}