Skip to main content

oxios_kernel/tools/kernel/
persona_tool.rs

1//! Persona tool — wraps `PersonaApi` behind the `AgentTool` interface.
2//!
3//! Provides agents with persona management capabilities.
4//! Actions: list, get, set_active.
5//!
6//! ## Example
7//!
8//! ```json
9//! { "action": "list" }
10//! { "action": "get", "id": "persona-id" }
11//! { "action": "set_active", "id": "persona-id" }
12//! ```
13
14use 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::persona_manager::PersonaManager;
23
24/// Agent tool for persona management.
25///
26/// Wraps the `PersonaApi` domain of the `KernelHandle`. Allows agents
27/// to query and switch between active personas.
28///
29/// ## Actions
30///
31/// | Action       | Description              | Required params |
32/// |--------------|--------------------------|-----------------|
33/// | `list`       | List all personas        | —               |
34/// | `get`        | Get persona by ID        | `id`            |
35/// | `set_active` | Set the active persona   | `id`            |
36pub struct PersonaTool {
37    persona_manager: Arc<PersonaManager>,
38}
39
40impl PersonaTool {
41    /// Create a new `PersonaTool` from a `KernelHandle`.
42    ///
43    /// Extracts the `PersonaManager` Arc from the kernel's Persona API.
44    pub fn from_kernel(kernel: &KernelHandle) -> Self {
45        Self {
46            persona_manager: kernel.persona.persona_manager.clone(),
47        }
48    }
49}
50
51impl std::fmt::Debug for PersonaTool {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("PersonaTool").finish()
54    }
55}
56
57#[async_trait]
58impl AgentTool for PersonaTool {
59    fn name(&self) -> &str {
60        "persona"
61    }
62
63    fn label(&self) -> &str {
64        "Persona"
65    }
66
67    fn description(&self) -> &'static str {
68        "Manage personas — list, inspect, or switch the active persona. \
69         Actions: list, get, set_active."
70    }
71
72    fn parameters_schema(&self) -> Value {
73        json!({
74            "type": "object",
75            "properties": {
76                "action": {
77                    "type": "string",
78                    "enum": ["list", "get", "set_active"],
79                    "description": "Persona operation to perform"
80                },
81                "id": {
82                    "type": "string",
83                    "description": "Persona identifier (required for get and set_active)"
84                }
85            },
86            "required": ["action"]
87        })
88    }
89
90    async fn execute(
91        &self,
92        _tool_call_id: &str,
93        params: Value,
94        _signal: Option<oneshot::Receiver<()>>,
95        _ctx: &ToolContext,
96    ) -> Result<AgentToolResult, String> {
97        let action = params
98            .get("action")
99            .and_then(|v| v.as_str())
100            .ok_or_else(|| "Missing required parameter: action".to_string())?;
101
102        // Build a temporary PersonaApi to delegate to.
103        let api = crate::kernel_handle::PersonaApi::new(self.persona_manager.clone());
104
105        match action {
106            "list" => {
107                let personas = api.list();
108                if personas.is_empty() {
109                    return Ok(AgentToolResult::success("No personas defined."));
110                }
111
112                // Get active persona ID for display.
113                let active_id = api.active().map(|p| p.id.clone());
114
115                let mut output = format!("Found {} persona(s):\n\n", personas.len());
116                for p in &personas {
117                    let marker = if active_id.as_deref() == Some(&p.id) {
118                        " ← active"
119                    } else {
120                        ""
121                    };
122                    output.push_str(&format!(
123                        "- {} ({}) enabled={}{}\n",
124                        p.name, p.id, p.enabled, marker,
125                    ));
126                }
127                Ok(AgentToolResult::success(output))
128            }
129
130            "get" => {
131                let id = params
132                    .get("id")
133                    .and_then(|v| v.as_str())
134                    .ok_or_else(|| "get requires 'id' parameter".to_string())?;
135
136                match api.get(id) {
137                    Some(p) => Ok(AgentToolResult::success(
138                        serde_json::to_string_pretty(&json!({
139                            "id": p.id,
140                            "name": p.name,
141                            "description": p.description,
142                            "enabled": p.enabled,
143                            "system_prompt": p.system_prompt,
144                            "traits": p.personality_traits,
145                        }))
146                        .unwrap_or_default(),
147                    )),
148                    None => Ok(AgentToolResult::error(format!(
149                        "Persona '{}' not found",
150                        id
151                    ))),
152                }
153            }
154
155            "set_active" => {
156                let id = params
157                    .get("id")
158                    .and_then(|v| v.as_str())
159                    .ok_or_else(|| "set_active requires 'id' parameter".to_string())?;
160
161                match api.set_active(id) {
162                    Ok(()) => Ok(AgentToolResult::success(format!(
163                        "Active persona set to '{}'.",
164                        id
165                    ))),
166                    Err(e) => Ok(AgentToolResult::error(format!(
167                        "Failed to set active persona: {}",
168                        e
169                    ))),
170                }
171            }
172
173            other => Err(format!(
174                "Unknown persona action '{}'. Valid: list, get, set_active",
175                other
176            )),
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_schema_structure() {
187        let schema = json!({
188            "type": "object",
189            "properties": {
190                "action": {
191                    "type": "string",
192                    "enum": ["list", "get", "set_active"]
193                },
194                "id": { "type": "string" }
195            },
196            "required": ["action"]
197        });
198
199        let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
200        assert_eq!(actions.len(), 3);
201        assert!(actions.iter().any(|a| a == "list"));
202        assert!(actions.iter().any(|a| a == "get"));
203        assert!(actions.iter().any(|a| a == "set_active"));
204    }
205}