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 .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}