oxios_kernel/tools/builtin/
project_tool.rs1use std::sync::Arc;
10
11use async_trait::async_trait;
12use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
13use serde_json::{json, Value};
14use tokio::sync::oneshot;
15
16use crate::kernel_handle::KernelHandle;
17use crate::project::ProjectManager;
18
19pub struct ProjectTool {
33 project_manager: Option<Arc<ProjectManager>>,
34}
35
36impl ProjectTool {
37 pub fn from_kernel(kernel: &KernelHandle) -> Self {
39 Self {
40 project_manager: kernel.projects.as_ref().map(|p| p.project_manager.clone()),
41 }
42 }
43}
44
45impl std::fmt::Debug for ProjectTool {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("ProjectTool").finish()
48 }
49}
50
51#[async_trait]
52impl AgentTool for ProjectTool {
53 fn name(&self) -> &str {
54 "project"
55 }
56
57 fn label(&self) -> &str {
58 "Project"
59 }
60
61 fn description(&self) -> &'static str {
62 "Query registered Projects — work contexts with paths and memory associations. \
63 Actions: list, get, link_memory, unlink_memory."
64 }
65
66 fn parameters_schema(&self) -> Value {
67 json!({
68 "type": "object",
69 "properties": {
70 "action": {
71 "type": "string",
72 "enum": ["list", "get", "link_memory", "unlink_memory"],
73 "description": "Project operation to perform"
74 },
75 "id": {
76 "type": "string",
77 "description": "Project UUID"
78 },
79 "name": {
80 "type": "string",
81 "description": "Project name (alternative to id for 'get')"
82 },
83 "project_id": {
84 "type": "string",
85 "description": "Project UUID (for link_memory/unlink_memory)"
86 },
87 "memory_id": {
88 "type": "string",
89 "description": "Memory UUID (for link_memory/unlink_memory)"
90 }
91 },
92 "required": ["action"]
93 })
94 }
95
96 async fn execute(
97 &self,
98 _tool_call_id: &str,
99 params: Value,
100 _signal: Option<oneshot::Receiver<()>>,
101 _ctx: &ToolContext,
102 ) -> Result<AgentToolResult, String> {
103 let action = params
104 .get("action")
105 .and_then(|v| v.as_str())
106 .ok_or_else(|| "Missing required parameter: action".to_string())?;
107
108 let pm = self
109 .project_manager
110 .as_ref()
111 .ok_or_else(|| "Project system not available (SQLite not enabled)".to_string())?;
112
113 match action {
114 "list" => {
115 let projects = pm.list_projects();
116 if projects.is_empty() {
117 return Ok(AgentToolResult::success("No Projects registered."));
118 }
119 let mut output = format!("Found {} Project(s):\n\n", projects.len());
120 for p in &projects {
121 let paths_str = if p.paths.is_empty() {
122 "(no paths)".to_string()
123 } else {
124 p.paths
125 .iter()
126 .map(|p| p.to_string_lossy().to_string())
127 .collect::<Vec<_>>()
128 .join(", ")
129 };
130 output.push_str(&format!(
131 "- {} {} ({}) paths={} tags={}\n",
132 p.emoji,
133 p.name,
134 &p.id.to_string()[..8.min(p.id.to_string().len())],
135 paths_str,
136 p.tags.join(", "),
137 ));
138 }
139 Ok(AgentToolResult::success(output))
140 }
141
142 "get" => {
143 let project = if let Some(id_str) = params.get("id").and_then(|v| v.as_str()) {
144 let id = uuid::Uuid::parse_str(id_str)
145 .map_err(|e| format!("Invalid project ID: {e}"))?;
146 pm.get_project(id)
147 } else if let Some(name) = params.get("name").and_then(|v| v.as_str()) {
148 pm.get_project_by_name(name)
149 } else {
150 return Err("'get' requires 'id' or 'name' parameter".to_string());
151 };
152
153 match project {
154 Some(p) => {
155 let memory_ids = pm.get_project_memory_ids(p.id).unwrap_or_default();
157 Ok(AgentToolResult::success(
158 serde_json::to_string_pretty(&json!({
159 "id": p.id.to_string(),
160 "name": p.name,
161 "description": p.description,
162 "emoji": p.emoji,
163 "source": p.source.to_string(),
164 "paths": p.paths.iter().map(|p| p.to_string_lossy().to_string()).collect::<Vec<_>>(),
165 "tags": p.tags,
166 "memory_visible": p.memory_visible,
167 "associated_memory_count": memory_ids.len(),
168 "last_active": p.last_active_at.to_rfc3339(),
169 }))
170 .unwrap_or_default(),
171 ))
172 }
173 None => Ok(AgentToolResult::error("Project not found")),
174 }
175 }
176
177 "link_memory" => {
178 let project_id_str = params
179 .get("project_id")
180 .and_then(|v| v.as_str())
181 .ok_or_else(|| "link_memory requires 'project_id'".to_string())?;
182 let memory_id = params
183 .get("memory_id")
184 .and_then(|v| v.as_str())
185 .ok_or_else(|| "link_memory requires 'memory_id'".to_string())?;
186
187 let project_id = uuid::Uuid::parse_str(project_id_str)
188 .map_err(|e| format!("Invalid project_id: {e}"))?;
189
190 match pm.link_memory(project_id, memory_id) {
191 Ok(()) => Ok(AgentToolResult::success(format!(
192 "Linked memory {} to project {}",
193 &memory_id[..8.min(memory_id.len())],
194 &project_id_str[..8.min(project_id_str.len())],
195 ))),
196 Err(e) => Ok(AgentToolResult::error(format!(
197 "Failed to link memory: {e}"
198 ))),
199 }
200 }
201
202 "unlink_memory" => {
203 let project_id_str = params
204 .get("project_id")
205 .and_then(|v| v.as_str())
206 .ok_or_else(|| "unlink_memory requires 'project_id'".to_string())?;
207 let memory_id = params
208 .get("memory_id")
209 .and_then(|v| v.as_str())
210 .ok_or_else(|| "unlink_memory requires 'memory_id'".to_string())?;
211
212 let project_id = uuid::Uuid::parse_str(project_id_str)
213 .map_err(|e| format!("Invalid project_id: {e}"))?;
214
215 match pm.unlink_memory(project_id, memory_id) {
216 Ok(()) => Ok(AgentToolResult::success(format!(
217 "Unlinked memory {} from project {}",
218 &memory_id[..8.min(memory_id.len())],
219 &project_id_str[..8.min(project_id_str.len())],
220 ))),
221 Err(e) => Ok(AgentToolResult::error(format!(
222 "Failed to unlink memory: {e}"
223 ))),
224 }
225 }
226
227 other => Err(format!(
228 "Unknown project action '{other}'. Valid: list, get, link_memory, unlink_memory"
229 )),
230 }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_schema_actions() {
240 let schema = serde_json::json!({
241 "properties": {
242 "action": {
243 "enum": ["list", "get", "link_memory", "unlink_memory"]
244 }
245 }
246 });
247 let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
248 assert_eq!(actions.len(), 4);
249 assert!(actions.iter().any(|a| a == "list"));
250 assert!(actions.iter().any(|a| a == "get"));
251 assert!(actions.iter().any(|a| a == "link_memory"));
252 assert!(actions.iter().any(|a| a == "unlink_memory"));
253 }
254}