vv_agent/tools/
background_agent_task.rs1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::{Arc, Mutex};
3
4use serde_json::{json, Value};
5
6use crate::agent::Agent;
7use crate::result::RunResult;
8use crate::runner::{NormalizedInput, Runner};
9use crate::tools::{Tool, ToolContext, ToolOutput, ToolSpec};
10use crate::types::{AgentStatus, ToolArguments};
11
12static NEXT_BACKGROUND_AGENT_TASK_ID: AtomicU64 = AtomicU64::new(1);
13
14#[derive(Clone)]
15pub struct BackgroundAgentTask {
16 agent: Agent,
17 name: String,
18 description: String,
19 parameters_schema: Value,
20}
21
22impl BackgroundAgentTask {
23 pub fn start(
24 &self,
25 runner: &Runner,
26 _context: &mut ToolContext,
27 raw_arguments: Value,
28 ) -> Result<BackgroundAgentTaskHandle, String> {
29 let input = self.input_from_arguments(raw_arguments)?;
30 let task_id = format!(
31 "bg_agent_{:012x}",
32 NEXT_BACKGROUND_AGENT_TASK_ID.fetch_add(1, Ordering::Relaxed)
33 );
34 let state = Arc::new(Mutex::new(BackgroundAgentTaskState {
35 status: AgentStatus::Running,
36 result: None,
37 error: None,
38 }));
39 let state_for_worker = state.clone();
40 let runner = runner.clone();
41 let agent = self.agent.clone();
42 let task_id_for_error = task_id.clone();
43 let _ = std::thread::Builder::new()
44 .name(format!("vv-agent-background-{task_id}"))
45 .spawn(move || {
46 let result = runner.run_blocking(
47 &agent,
48 NormalizedInput::from(input),
49 crate::run_config::RunConfig::default(),
50 None,
51 );
52 if let Ok(mut state) = state_for_worker.lock() {
53 match result {
54 Ok(result) => {
55 state.status = result.status();
56 state.result = Some(result);
57 }
58 Err(error) => {
59 state.status = AgentStatus::Failed;
60 state.error = Some(error);
61 }
62 }
63 }
64 })
65 .map_err(|error| {
66 if let Ok(mut state) = state.lock() {
67 state.status = AgentStatus::Failed;
68 state.error = Some(error.to_string());
69 }
70 format!("failed to spawn background agent task {task_id_for_error}: {error}")
71 })?;
72 Ok(BackgroundAgentTaskHandle {
73 task_id,
74 agent_name: self.agent.name().to_string(),
75 state,
76 })
77 }
78
79 fn input_from_arguments(&self, raw_arguments: Value) -> Result<String, String> {
80 let object = raw_arguments
81 .as_object()
82 .ok_or_else(|| "background task arguments must be an object".to_string())?;
83 object
84 .get("task_description")
85 .or_else(|| object.get("task"))
86 .or_else(|| object.get("input"))
87 .and_then(Value::as_str)
88 .map(str::trim)
89 .filter(|value| !value.is_empty())
90 .map(str::to_string)
91 .ok_or_else(|| "background task requires task_description".to_string())
92 }
93}
94
95impl Tool for BackgroundAgentTask {
96 fn name(&self) -> &str {
97 &self.name
98 }
99
100 fn description(&self) -> &str {
101 &self.description
102 }
103
104 fn parameters_schema(&self) -> &Value {
105 &self.parameters_schema
106 }
107
108 fn as_tool_spec(&self) -> ToolSpec {
109 let name = self.name.clone();
110 let description = self.description.clone();
111 let parameters_schema = self.parameters_schema.clone();
112 let task = self.clone();
113 let mut spec = ToolSpec::new(
114 name.clone(),
115 description.clone(),
116 Arc::new(
117 move |_context: &mut ToolContext, arguments: &ToolArguments| {
118 let raw_arguments = Value::Object(arguments.clone().into_iter().collect());
119 match task.input_from_arguments(raw_arguments) {
120 Ok(task_description) => ToolOutput::json(json!({
121 "agent_name": task.agent.name(),
122 "status": "background_task_requested",
123 "task_description": task_description,
124 }))
125 .to_result(""),
126 Err(error) => ToolOutput::error(error)
127 .with_code("invalid_background_task_arguments")
128 .to_result(""),
129 }
130 },
131 ),
132 );
133 spec.schema = json!({
134 "type": "function",
135 "function": {
136 "name": name,
137 "description": description,
138 "parameters": parameters_schema,
139 }
140 });
141 spec
142 }
143}
144
145pub struct BackgroundAgentTaskBuilder {
146 agent: Agent,
147 name: Option<String>,
148 description: Option<String>,
149}
150
151impl BackgroundAgentTaskBuilder {
152 pub fn new(agent: Agent) -> Self {
153 Self {
154 agent,
155 name: None,
156 description: None,
157 }
158 }
159
160 pub fn name(mut self, name: impl Into<String>) -> Self {
161 self.name = Some(name.into());
162 self
163 }
164
165 pub fn description(mut self, description: impl Into<String>) -> Self {
166 self.description = Some(description.into());
167 self
168 }
169
170 pub fn build(self) -> Result<BackgroundAgentTask, String> {
171 let name = self
172 .name
173 .unwrap_or_else(|| format!("{}_background_task", self.agent.name()));
174 if name.trim().is_empty() {
175 return Err("background task tool name cannot be empty".to_string());
176 }
177 let description = self.description.unwrap_or_else(|| {
178 format!(
179 "Start the {} agent as a background task.",
180 self.agent.name()
181 )
182 });
183 Ok(BackgroundAgentTask {
184 agent: self.agent,
185 name,
186 description,
187 parameters_schema: json!({
188 "type": "object",
189 "properties": {
190 "task_description": {
191 "type": "string",
192 "description": "Task for the background agent."
193 }
194 },
195 "required": ["task_description"]
196 }),
197 })
198 }
199}
200
201#[derive(Clone)]
202pub struct BackgroundAgentTaskHandle {
203 task_id: String,
204 agent_name: String,
205 state: Arc<Mutex<BackgroundAgentTaskState>>,
206}
207
208impl BackgroundAgentTaskHandle {
209 pub fn task_id(&self) -> &str {
210 &self.task_id
211 }
212
213 pub fn agent_name(&self) -> &str {
214 &self.agent_name
215 }
216
217 pub fn status(&self) -> AgentStatus {
218 self.state
219 .lock()
220 .map(|state| state.status)
221 .unwrap_or(AgentStatus::Failed)
222 }
223
224 pub fn poll(&self) -> Result<BackgroundAgentTaskSnapshot, String> {
225 let state = self
226 .state
227 .lock()
228 .map_err(|_| "background task lock poisoned".to_string())?;
229 Ok(BackgroundAgentTaskSnapshot {
230 task_id: self.task_id.clone(),
231 agent_name: self.agent_name.clone(),
232 status: state.status,
233 final_output: state
234 .result
235 .as_ref()
236 .and_then(|result| result.final_output().map(str::to_string)),
237 error: state.error.clone(),
238 })
239 }
240
241 pub async fn wait(&self) -> Result<BackgroundAgentTaskSnapshot, String> {
242 loop {
243 let snapshot = self.poll()?;
244 if !matches!(snapshot.status, AgentStatus::Running | AgentStatus::Pending) {
245 return Ok(snapshot);
246 }
247 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
248 }
249 }
250}
251
252struct BackgroundAgentTaskState {
253 status: AgentStatus,
254 result: Option<RunResult>,
255 error: Option<String>,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct BackgroundAgentTaskSnapshot {
260 task_id: String,
261 agent_name: String,
262 status: AgentStatus,
263 final_output: Option<String>,
264 error: Option<String>,
265}
266
267impl BackgroundAgentTaskSnapshot {
268 pub fn task_id(&self) -> &str {
269 &self.task_id
270 }
271
272 pub fn agent_name(&self) -> &str {
273 &self.agent_name
274 }
275
276 pub fn status(&self) -> AgentStatus {
277 self.status
278 }
279
280 pub fn final_output(&self) -> Option<&str> {
281 self.final_output.as_deref()
282 }
283
284 pub fn error(&self) -> Option<&str> {
285 self.error.as_deref()
286 }
287}