vv_agent/tools/
agent_tool.rs1use 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}