oxios-kernel 0.1.1

Oxios kernel: supervisor, event bus, state store
Documentation
//! Space tool — wraps `SpaceApi` behind the `AgentTool` interface.
//!
//! Provides agents with Space management capabilities through an action-based
//! parameter schema. Actions: list, get, create, archive, merge, restore.
//!
//! ## Example
//!
//! ```json
//! { "action": "list" }
//! { "action": "get", "id": "uuid-of-space" }
//! { "action": "merge", "id": "survivor-uuid", "absorbed_id": "absorbed-uuid" }
//! ```

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;

/// Agent tool for Space management.
///
/// Wraps the `SpaceApi` domain of the `KernelHandle` behind a single
/// `AgentTool` implementation. The tool uses an `action` parameter to
/// dispatch to the appropriate Space operation.
///
/// ## Actions
///
/// | Action     | Description                     | Required params          | Optional params |
/// |------------|---------------------------------|--------------------------|-----------------|
/// | `list`     | List all Spaces                 | —                        | —               |
/// | `get`      | Get Space details               | `id`                     | —               |
/// | `create`   | (reserved)                      | `name`                   | —               |
/// | `archive`  | Archive a Space                 | `id`                     | —               |
/// | `merge`    | Merge two Spaces                | `id`, `absorbed_id`      | —               |
/// | `restore`  | Restore an archived Space       | `id`                     | —               |
pub struct SpaceTool {
    space_manager: Arc<SpaceManager>,
}

impl SpaceTool {
    /// Create a new `SpaceTool` from a `KernelHandle`.
    ///
    /// Extracts the `SpaceManager` Arc from the kernel's Space API.
    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())?;

        // Build a temporary SpaceApi to delegate to.
        // We use the event_bus from the kernel — but SpaceApi only needs
        // the space_manager for our operations, so we create a minimal instance.
        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" => {
                // Create is reserved — Space creation is typically handled by
                // the kernel or gateway, not by agents directly.
                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() {
        // We can't create a full KernelHandle in unit tests, but we can
        // verify tool metadata by constructing from Arc directly.
        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() {
        // Verify the parameter schema structure matches expectations.
        let expected_actions = vec!["list", "get", "create", "archive", "merge", "restore"];
        // This validates the design: action is always required.
        assert!(!expected_actions.is_empty());
    }
}