Skip to main content

vv_agent/tools/
background_agent_task.rs

1use 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}