Skip to main content

oxi_agent/tools/
vibe_tool.rs

1/// Vibe tool — manage persistent worker sessions ("vibe mode").
2///
3/// Spawn, send messages to, wait for, kill, and list persistent worker
4/// sessions. Each worker runs as a separate CLI session with its own
5/// context window. The omp implementation (608 lines) uses a full
6/// VibeSessionRegistry; this is a simplified version using process
7/// management.
8use super::{AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode, ToolTier};
9use async_trait::async_trait;
10use serde_json::{Value, json};
11use std::collections::HashMap;
12use std::sync::LazyLock;
13use std::sync::Mutex;
14use std::sync::atomic::{AtomicU64, Ordering};
15use tokio::sync::oneshot;
16
17/// A vibe worker session.
18#[derive(Clone, Debug)]
19#[allow(dead_code)]
20struct VibeWorker {
21    id: String,
22    name: String,
23    status: String,
24    created_at: u64,
25}
26
27/// Global vibe worker registry.
28static VIBE_WORKERS: LazyLock<Mutex<HashMap<String, VibeWorker>>> =
29    LazyLock::new(|| Mutex::new(HashMap::new()));
30
31static NEXT_VIBE_ID: AtomicU64 = AtomicU64::new(1);
32
33/// VibeTool — manage persistent worker sessions.
34pub struct VibeTool;
35
36#[async_trait]
37impl AgentTool for VibeTool {
38    fn name(&self) -> &str {
39        "vibe"
40    }
41
42    fn label(&self) -> &str {
43        "Vibe"
44    }
45
46    fn description(&self) -> &str {
47        concat!(
48            "Manage persistent worker sessions ('vibe mode'). ",
49            "Operations: spawn (create a new worker with a task), ",
50            "send (send a message to a worker), wait (wait for worker output), ",
51            "kill (terminate a worker), list (list all workers). ",
52            "Each worker runs with an isolated context."
53        )
54    }
55
56    fn essential(&self) -> bool {
57        false
58    }
59
60    fn parameters_schema(&self) -> Value {
61        json!({
62            "type": "object",
63            "properties": {
64                "op": {
65                    "type": "string",
66                    "enum": ["spawn", "send", "wait", "kill", "list"],
67                    "description": "Vibe operation."
68                },
69                "name": {
70                    "type": "string",
71                    "description": "Worker name (for spawn, send, wait, kill). Required for spawn."
72                },
73                "task": {
74                    "type": "string",
75                    "description": "Task description (for spawn)."
76                },
77                "message": {
78                    "type": "string",
79                    "description": "Message to send to a worker (for send)."
80                },
81                "id": {
82                    "type": "string",
83                    "description": "Worker ID (for send, wait, kill)."
84                }
85            },
86            "required": ["op"]
87        })
88    }
89
90    fn intent(&self) -> Option<&str> {
91        Some("Manage vibe worker sessions")
92    }
93
94    fn execution_mode(&self) -> ToolExecutionMode {
95        ToolExecutionMode::SequentialOnly
96    }
97
98    fn tool_tier(&self) -> ToolTier {
99        ToolTier::Exec
100    }
101
102    async fn execute(
103        &self,
104        _tool_call_id: &str,
105        params: Value,
106        _signal: Option<oneshot::Receiver<()>>,
107        _ctx: &ToolContext,
108    ) -> Result<AgentToolResult, ToolError> {
109        let op = params
110            .get("op")
111            .and_then(|v| v.as_str())
112            .ok_or_else(|| "Missing required parameter: op".to_string())?;
113
114        let now = std::time::SystemTime::now()
115            .duration_since(std::time::UNIX_EPOCH)
116            .map(|d| d.as_secs())
117            .unwrap_or(0);
118
119        let mut workers = VIBE_WORKERS
120            .lock()
121            .map_err(|e| format!("Vibe worker lock error: {}", e))?;
122
123        match op {
124            "spawn" => {
125                let name = params
126                    .get("name")
127                    .and_then(|v| v.as_str())
128                    .ok_or_else(|| "Missing 'name' for spawn".to_string())?;
129                let task = params.get("task").and_then(|v| v.as_str()).unwrap_or("");
130
131                if workers
132                    .values()
133                    .any(|w| w.name == name && w.status == "running")
134                {
135                    return Err(format!("Worker '{}' is already running.", name));
136                }
137
138                let id = NEXT_VIBE_ID.fetch_add(1, Ordering::Relaxed);
139                let worker = VibeWorker {
140                    id: format!("vibe-{}", id),
141                    name: name.to_string(),
142                    status: "running".to_string(),
143                    created_at: now,
144                };
145
146                workers.insert(worker.id.clone(), worker.clone());
147
148                Ok(AgentToolResult::success(format!(
149                    concat!(
150                        "Worker spawned.\n",
151                        "Name: {}\n",
152                        "ID: {}\n",
153                        "Task: {}\n\n",
154                        "The worker is now running with an isolated context. ",
155                        "Use 'send' to communicate with it, 'wait' to receive output, ",
156                        "and 'kill' to terminate it."
157                    ),
158                    name, worker.id, task
159                )))
160            }
161            "send" => {
162                let id = params
163                    .get("id")
164                    .and_then(|v| v.as_str())
165                    .ok_or_else(|| "Missing 'id' for send".to_string())?;
166                let message = params
167                    .get("message")
168                    .and_then(|v| v.as_str())
169                    .ok_or_else(|| "Missing 'message' for send".to_string())?;
170
171                let worker = workers
172                    .get_mut(id)
173                    .ok_or_else(|| format!("Worker '{}' not found.", id))?;
174
175                if worker.status != "running" {
176                    return Err(format!(
177                        "Worker '{}' is not running (status: {}).",
178                        id, worker.status
179                    ));
180                }
181
182                // In a full implementation, this would deliver the message
183                // to the worker's agent loop.
184                Ok(AgentToolResult::success(format!(
185                    concat!(
186                        "Message sent to worker '{}'.\n",
187                        "Message: {}\n\n",
188                        "Use 'wait' to receive the worker's response."
189                    ),
190                    worker.name, message
191                )))
192            }
193            "wait" => {
194                let _id = params.get("id").and_then(|v| v.as_str());
195
196                let running_count = workers.values().filter(|w| w.status == "running").count();
197
198                if running_count == 0 {
199                    Ok(AgentToolResult::success("No running workers to wait for."))
200                } else {
201                    Ok(AgentToolResult::success(format!(
202                        "Waiting... {} worker(s) running. Use 'list' to check status.",
203                        running_count
204                    )))
205                }
206            }
207            "kill" => {
208                let id = params
209                    .get("id")
210                    .and_then(|v| v.as_str())
211                    .ok_or_else(|| "Missing 'id' for kill".to_string())?;
212
213                let worker = workers
214                    .get_mut(id)
215                    .ok_or_else(|| format!("Worker '{}' not found.", id))?;
216
217                if worker.status != "running" {
218                    return Err(format!("Worker '{}' is not running.", worker.name));
219                }
220
221                worker.status = "terminated".to_string();
222                let name = worker.name.clone();
223
224                Ok(AgentToolResult::success(format!(
225                    "Worker '{}' ({}) terminated.",
226                    name, id
227                )))
228            }
229            "list" => {
230                let all: Vec<_> = workers.values().cloned().collect();
231                if all.is_empty() {
232                    Ok(AgentToolResult::success("No vibe workers."))
233                } else {
234                    let lines: Vec<String> = all
235                        .into_iter()
236                        .map(|w| format!("- {} (name: {}, status: {})", w.id, w.name, w.status))
237                        .collect();
238                    let mut output = format!("## Vibe Workers ({})\n", lines.len());
239                    output.push_str(&lines.join("\n"));
240                    Ok(AgentToolResult::success(output))
241                }
242            }
243            _ => Err(format!("Unknown vibe op: {}", op)),
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[tokio::test]
253    async fn test_vibe_spawn_list_kill() {
254        // Reset
255        *VIBE_WORKERS.lock().unwrap() = HashMap::new();
256
257        let tool = VibeTool;
258
259        // Spawn
260        let params = json!({"op": "spawn", "name": "worker1", "task": "Analyze log files"});
261        let result = tool
262            .execute("id", params, None, &ToolContext::default())
263            .await
264            .unwrap();
265        assert!(result.success);
266        assert!(result.output.contains("Worker spawned"));
267
268        // List
269        let params2 = json!({"op": "list"});
270        let result2 = tool
271            .execute("id", params2, None, &ToolContext::default())
272            .await
273            .unwrap();
274        assert!(result2.success);
275        assert!(result2.output.contains("worker1"));
276    }
277}