oxios_kernel/tools/kernel/
space_tool.rs1use std::sync::Arc;
15
16use async_trait::async_trait;
17use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
18use serde_json::{json, Value};
19use tokio::sync::oneshot;
20
21use crate::kernel_handle::KernelHandle;
22use crate::space::SpaceManager;
23
24pub struct SpaceTool {
41 space_manager: Arc<SpaceManager>,
42}
43
44impl SpaceTool {
45 pub fn from_kernel(kernel: &KernelHandle) -> Self {
49 Self {
50 space_manager: kernel.spaces.space_manager.clone(),
51 }
52 }
53}
54
55impl std::fmt::Debug for SpaceTool {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.debug_struct("SpaceTool").finish()
58 }
59}
60
61#[async_trait]
62impl AgentTool for SpaceTool {
63 fn name(&self) -> &str {
64 "space"
65 }
66
67 fn label(&self) -> &str {
68 "Space"
69 }
70
71 fn description(&self) -> &'static str {
72 "Manage Spaces — context partitions that isolate agent knowledge. \
73 Actions: list, get, archive, merge, restore."
74 }
75
76 fn parameters_schema(&self) -> Value {
77 json!({
78 "type": "object",
79 "properties": {
80 "action": {
81 "type": "string",
82 "enum": ["list", "get", "create", "archive", "merge", "restore"],
83 "description": "Space operation to perform"
84 },
85 "id": {
86 "type": "string",
87 "description": "Space UUID (required for get, archive, merge, restore)"
88 },
89 "name": {
90 "type": "string",
91 "description": "Space name (for create, optional)"
92 },
93 "absorbed_id": {
94 "type": "string",
95 "description": "UUID of the Space to absorb (merge action only)"
96 }
97 },
98 "required": ["action"]
99 })
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, String> {
109 let action = params
110 .get("action")
111 .and_then(|v| v.as_str())
112 .ok_or_else(|| "Missing required parameter: action".to_string())?;
113
114 let api = crate::kernel_handle::SpaceApi::new(
118 self.space_manager.clone(),
119 crate::event_bus::EventBus::new(16),
120 );
121
122 match action {
123 "list" => {
124 let spaces = api.list_spaces();
125 if spaces.is_empty() {
126 return Ok(AgentToolResult::success("No Spaces found."));
127 }
128 let mut output = format!("Found {} Space(s):\n\n", spaces.len());
129 for s in &spaces {
130 output.push_str(&format!(
131 "- {} ({}) active={} paths={}\n",
132 s.name,
133 &s.id[..8.min(s.id.len())],
134 s.active,
135 s.paths.join(", "),
136 ));
137 }
138 Ok(AgentToolResult::success(output))
139 }
140
141 "get" => {
142 let id = params
143 .get("id")
144 .and_then(|v| v.as_str())
145 .ok_or_else(|| "get requires 'id' parameter".to_string())?;
146
147 match api.get_space(id).await {
148 Some(info) => Ok(AgentToolResult::success(
149 serde_json::to_string_pretty(&json!({
150 "id": info.id,
151 "name": info.name,
152 "source": info.source,
153 "active": info.active,
154 "paths": info.paths,
155 "interaction_count": info.interaction_count,
156 "knowledge_visible": info.knowledge_visible,
157 "last_active": info.last_active,
158 }))
159 .unwrap_or_default(),
160 )),
161 None => Ok(AgentToolResult::error(format!("Space '{}' not found", id))),
162 }
163 }
164
165 "create" => {
166 Ok(AgentToolResult::error(
169 "Space creation via tool is not supported. Spaces are created through the kernel or gateway API.",
170 ))
171 }
172
173 "archive" => {
174 let id = params
175 .get("id")
176 .and_then(|v| v.as_str())
177 .ok_or_else(|| "archive requires 'id' parameter".to_string())?;
178
179 match api.archive(id).await {
180 Ok(()) => Ok(AgentToolResult::success(format!(
181 "Space '{}' archived.",
182 id
183 ))),
184 Err(e) => Ok(AgentToolResult::error(format!(
185 "Failed to archive Space: {}",
186 e
187 ))),
188 }
189 }
190
191 "merge" => {
192 let survivor_id = params
193 .get("id")
194 .and_then(|v| v.as_str())
195 .ok_or_else(|| "merge requires 'id' (survivor) parameter".to_string())?;
196 let absorbed_id = params
197 .get("absorbed_id")
198 .and_then(|v| v.as_str())
199 .ok_or_else(|| "merge requires 'absorbed_id' parameter".to_string())?;
200
201 match api.merge(survivor_id, absorbed_id).await {
202 Ok(()) => Ok(AgentToolResult::success(format!(
203 "Merged Space '{}' into '{}'.",
204 absorbed_id, survivor_id
205 ))),
206 Err(e) => Ok(AgentToolResult::error(format!(
207 "Failed to merge Spaces: {}",
208 e
209 ))),
210 }
211 }
212
213 "restore" => {
214 let id = params
215 .get("id")
216 .and_then(|v| v.as_str())
217 .ok_or_else(|| "restore requires 'id' parameter".to_string())?;
218
219 match api.restore(id).await {
220 Ok(()) => Ok(AgentToolResult::success(format!(
221 "Space '{}' restored.",
222 id
223 ))),
224 Err(e) => Ok(AgentToolResult::error(format!(
225 "Failed to restore Space: {}",
226 e
227 ))),
228 }
229 }
230
231 other => Err(format!(
232 "Unknown space action '{}'. Valid: list, get, create, archive, merge, restore",
233 other
234 )),
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_name_and_label() {
245 let schema = json!({
248 "type": "object",
249 "properties": {
250 "action": {
251 "type": "string",
252 "enum": ["list", "get", "create", "archive", "merge", "restore"]
253 }
254 },
255 "required": ["action"]
256 });
257
258 let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
259 assert_eq!(actions.len(), 6);
260 assert!(actions.iter().any(|a| a == "list"));
261 assert!(actions.iter().any(|a| a == "get"));
262 assert!(actions.iter().any(|a| a == "archive"));
263 assert!(actions.iter().any(|a| a == "merge"));
264 assert!(actions.iter().any(|a| a == "restore"));
265 }
266
267 #[test]
268 fn test_schema_has_required_action() {
269 let expected_actions = vec!["list", "get", "create", "archive", "merge", "restore"];
271 assert!(!expected_actions.is_empty());
273 }
274}