use std::sync::Arc;
use async_trait::async_trait;
use oxi_sdk::{AgentTool, AgentToolResult, ToolContext};
use serde_json::{json, Value};
use tokio::sync::oneshot;
use crate::kernel_handle::KernelHandle;
use crate::space::SpaceManager;
pub struct SpaceTool {
space_manager: Arc<SpaceManager>,
}
impl SpaceTool {
pub fn from_kernel(kernel: &KernelHandle) -> Self {
Self {
space_manager: kernel.spaces.space_manager.clone(),
}
}
}
impl std::fmt::Debug for SpaceTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SpaceTool").finish()
}
}
#[async_trait]
impl AgentTool for SpaceTool {
fn name(&self) -> &str {
"space"
}
fn label(&self) -> &str {
"Space"
}
fn description(&self) -> &'static str {
"Manage Spaces — context partitions that isolate agent knowledge. \
Actions: list, get, archive, merge, restore."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "get", "create", "archive", "merge", "restore"],
"description": "Space operation to perform"
},
"id": {
"type": "string",
"description": "Space UUID (required for get, archive, merge, restore)"
},
"name": {
"type": "string",
"description": "Space name (for create, optional)"
},
"absorbed_id": {
"type": "string",
"description": "UUID of the Space to absorb (merge action only)"
}
},
"required": ["action"]
})
}
async fn execute(
&self,
_tool_call_id: &str,
params: Value,
_signal: Option<oneshot::Receiver<()>>,
_ctx: &ToolContext,
) -> Result<AgentToolResult, String> {
let action = params
.get("action")
.and_then(|v| v.as_str())
.ok_or_else(|| "Missing required parameter: action".to_string())?;
let api = crate::kernel_handle::SpaceApi::new(
self.space_manager.clone(),
crate::event_bus::EventBus::new(16),
);
match action {
"list" => {
let spaces = api.list_spaces();
if spaces.is_empty() {
return Ok(AgentToolResult::success("No Spaces found."));
}
let mut output = format!("Found {} Space(s):\n\n", spaces.len());
for s in &spaces {
output.push_str(&format!(
"- {} ({}) active={} paths={}\n",
s.name,
&s.id[..8.min(s.id.len())],
s.active,
s.paths.join(", "),
));
}
Ok(AgentToolResult::success(output))
}
"get" => {
let id = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| "get requires 'id' parameter".to_string())?;
match api.get_space(id).await {
Some(info) => Ok(AgentToolResult::success(
serde_json::to_string_pretty(&json!({
"id": info.id,
"name": info.name,
"source": info.source,
"active": info.active,
"paths": info.paths,
"interaction_count": info.interaction_count,
"knowledge_visible": info.knowledge_visible,
"last_active": info.last_active,
}))
.unwrap_or_default(),
)),
None => Ok(AgentToolResult::error(format!("Space '{}' not found", id))),
}
}
"create" => {
Ok(AgentToolResult::error(
"Space creation via tool is not supported. Spaces are created through the kernel or gateway API.",
))
}
"archive" => {
let id = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| "archive requires 'id' parameter".to_string())?;
match api.archive(id).await {
Ok(()) => Ok(AgentToolResult::success(format!(
"Space '{}' archived.",
id
))),
Err(e) => Ok(AgentToolResult::error(format!(
"Failed to archive Space: {}",
e
))),
}
}
"merge" => {
let survivor_id = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| "merge requires 'id' (survivor) parameter".to_string())?;
let absorbed_id = params
.get("absorbed_id")
.and_then(|v| v.as_str())
.ok_or_else(|| "merge requires 'absorbed_id' parameter".to_string())?;
match api.merge(survivor_id, absorbed_id).await {
Ok(()) => Ok(AgentToolResult::success(format!(
"Merged Space '{}' into '{}'.",
absorbed_id, survivor_id
))),
Err(e) => Ok(AgentToolResult::error(format!(
"Failed to merge Spaces: {}",
e
))),
}
}
"restore" => {
let id = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| "restore requires 'id' parameter".to_string())?;
match api.restore(id).await {
Ok(()) => Ok(AgentToolResult::success(format!(
"Space '{}' restored.",
id
))),
Err(e) => Ok(AgentToolResult::error(format!(
"Failed to restore Space: {}",
e
))),
}
}
other => Err(format!(
"Unknown space action '{}'. Valid: list, get, create, archive, merge, restore",
other
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_name_and_label() {
let schema = json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "get", "create", "archive", "merge", "restore"]
}
},
"required": ["action"]
});
let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
assert_eq!(actions.len(), 6);
assert!(actions.iter().any(|a| a == "list"));
assert!(actions.iter().any(|a| a == "get"));
assert!(actions.iter().any(|a| a == "archive"));
assert!(actions.iter().any(|a| a == "merge"));
assert!(actions.iter().any(|a| a == "restore"));
}
#[test]
fn test_schema_has_required_action() {
let expected_actions = vec!["list", "get", "create", "archive", "merge", "restore"];
assert!(!expected_actions.is_empty());
}
}