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