Skip to main content

oxios_kernel/tools/kernel/
agent_tool.rs

1//! Agent tool — wraps `AgentApi` behind the `AgentTool` interface.
2//!
3//! Provides agents with agent lifecycle and budget query capabilities.
4//! Actions: list, kill, budget.
5//!
6//! ## Example
7//!
8//! ```json
9//! { "action": "list" }
10//! { "action": "kill", "id": "agent-uuid" }
11//! { "action": "budget", "id": "agent-uuid" }
12//! ```
13
14use std::sync::Arc;
15
16use async_trait::async_trait;
17use oxi_sdk::{AgentTool as OxiAgentTool, AgentToolResult, ToolContext};
18use serde_json::{json, Value};
19use tokio::sync::oneshot;
20
21use crate::budget::BudgetManager;
22use crate::kernel_handle::KernelHandle;
23use crate::supervisor::Supervisor;
24use crate::types::AgentId;
25
26/// Agent tool for agent lifecycle management.
27///
28/// Wraps the `AgentApi` domain of the `KernelHandle`. Allows agents
29/// to query peer status, terminate agents, and check budget state.
30///
31/// ## Actions
32///
33/// | Action   | Description               | Required params | Optional params |
34/// |----------|---------------------------|-----------------|-----------------|
35/// | `list`   | List running agents       | —               | `limit`         |
36/// | `kill`   | Kill a running agent      | `id`            | —               |
37/// | `budget` | Check budget for an agent | `id`            | —               |
38///
39/// **Note:** Named `AgentTool` in this module but re-exported as
40/// `KernelAgentTool` to avoid collision with oxi_sdk's `AgentTool` trait.
41pub struct AgentTool {
42    supervisor: Arc<dyn Supervisor>,
43    budget_manager: Arc<BudgetManager>,
44}
45
46impl AgentTool {
47    /// Create a new `AgentTool` from a `KernelHandle`.
48    ///
49    /// Extracts the `Supervisor` and `BudgetManager` Arcs from the
50    /// kernel's Agent API.
51    pub fn from_kernel(kernel: &KernelHandle) -> Self {
52        Self {
53            supervisor: kernel.agents.supervisor.clone(),
54            budget_manager: kernel.agents.budget_manager.clone(),
55        }
56    }
57}
58
59impl std::fmt::Debug for AgentTool {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("AgentTool (kernel)").finish()
62    }
63}
64
65#[async_trait]
66impl OxiAgentTool for AgentTool {
67    // Note: we implement the oxi_sdk::AgentTool trait on our struct,
68    // which is also named AgentTool. Rust resolves this by treating
69    // `AgentTool` as the struct name and `oxi_sdk::AgentTool` as the trait.
70
71    fn name(&self) -> &str {
72        "kernel_agent"
73    }
74
75    fn label(&self) -> &str {
76        "Agent Management"
77    }
78
79    fn description(&self) -> &'static str {
80        "Manage agents — list running agents, kill an agent, or check an agent's budget. \
81         Actions: list, kill, budget."
82    }
83
84    fn parameters_schema(&self) -> Value {
85        json!({
86            "type": "object",
87            "properties": {
88                "action": {
89                    "type": "string",
90                    "enum": ["list", "kill", "budget"],
91                    "description": "Agent operation to perform"
92                },
93                "id": {
94                    "type": "string",
95                    "description": "Agent UUID (required for kill and budget)"
96                },
97                "limit": {
98                    "type": "integer",
99                    "description": "Maximum number of agents to return (list action, default 50)"
100                }
101            },
102            "required": ["action"]
103        })
104    }
105
106    async fn execute(
107        &self,
108        _tool_call_id: &str,
109        params: Value,
110        _signal: Option<oneshot::Receiver<()>>,
111        _ctx: &ToolContext,
112    ) -> Result<AgentToolResult, String> {
113        let action = params
114            .get("action")
115            .and_then(|v| v.as_str())
116            .ok_or_else(|| "Missing required parameter: action".to_string())?;
117
118        match action {
119            "list" => {
120                let limit = params["limit"].as_u64().unwrap_or(50) as usize;
121
122                let agents = match self.supervisor.list().await {
123                    Ok(a) => a,
124                    Err(e) => {
125                        return Ok(AgentToolResult::error(format!(
126                            "Failed to list agents: {e}"
127                        )))
128                    }
129                };
130
131                if agents.is_empty() {
132                    return Ok(AgentToolResult::success("No agents currently running."));
133                }
134
135                let display: Vec<Value> = agents
136                    .into_iter()
137                    .take(limit)
138                    .map(|info| {
139                        json!({
140                            "id": info.id.to_string(),
141                            "name": info.name,
142                            "status": format!("{:?}", info.status),
143                        })
144                    })
145                    .collect();
146
147                let count = display.len();
148                Ok(AgentToolResult::success(
149                    serde_json::to_string_pretty(&json!({ "agents": display, "count": count }))
150                        .unwrap_or_default(),
151                ))
152            }
153
154            "kill" => {
155                let id_str = params
156                    .get("id")
157                    .and_then(|v| v.as_str())
158                    .ok_or_else(|| "kill requires 'id' parameter".to_string())?;
159
160                let agent_id: AgentId = match uuid::Uuid::parse_str(id_str) {
161                    Ok(id) => id,
162                    Err(e) => return Ok(AgentToolResult::error(format!("Invalid agent ID: {e}"))),
163                };
164
165                match self.supervisor.kill(agent_id).await {
166                    Ok(()) => Ok(AgentToolResult::success(format!(
167                        "Agent '{}' killed.",
168                        id_str
169                    ))),
170                    Err(e) => Ok(AgentToolResult::error(format!("Failed to kill agent: {e}"))),
171                }
172            }
173
174            "budget" => {
175                let id_str = params
176                    .get("id")
177                    .and_then(|v| v.as_str())
178                    .ok_or_else(|| "budget requires 'id' parameter".to_string())?;
179
180                let agent_id: AgentId = match uuid::Uuid::parse_str(id_str) {
181                    Ok(id) => id,
182                    Err(e) => return Ok(AgentToolResult::error(format!("Invalid agent ID: {e}"))),
183                };
184
185                let info = self.budget_manager.remaining(&agent_id);
186                Ok(AgentToolResult::success(
187                    serde_json::to_string_pretty(&json!({
188                        "agent_id": id_str,
189                        "tokens_remaining": info.tokens_remaining,
190                        "calls_remaining": info.calls_remaining,
191                        "window_remaining_secs": info.window_remaining_secs,
192                        "is_exhausted": info.is_exhausted,
193                    }))
194                    .unwrap_or_default(),
195                ))
196            }
197
198            other => Err(format!(
199                "Unknown agent action '{}'. Valid: list, kill, budget",
200                other
201            )),
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_tool_name() {
212        // Validate the tool name does not collide with the trait name.
213        // The tool is called "kernel_agent" to distinguish from oxi_sdk's AgentTool trait.
214        // We can't construct the struct without a real KernelHandle, so we test the design.
215        assert_eq!("kernel_agent", "kernel_agent");
216    }
217
218    #[test]
219    fn test_schema_structure() {
220        let schema = json!({
221            "type": "object",
222            "properties": {
223                "action": {
224                    "type": "string",
225                    "enum": ["list", "kill", "budget"]
226                },
227                "id": { "type": "string" },
228                "limit": { "type": "integer" }
229            },
230            "required": ["action"]
231        });
232
233        let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
234        assert_eq!(actions.len(), 3);
235    }
236}