Skip to main content

oxios_kernel/tools/builtin/
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 '{id_str}' killed."
168                    ))),
169                    Err(e) => Ok(AgentToolResult::error(format!("Failed to kill agent: {e}"))),
170                }
171            }
172
173            "budget" => {
174                let id_str = params
175                    .get("id")
176                    .and_then(|v| v.as_str())
177                    .ok_or_else(|| "budget requires 'id' parameter".to_string())?;
178
179                let agent_id: AgentId = match uuid::Uuid::parse_str(id_str) {
180                    Ok(id) => id,
181                    Err(e) => return Ok(AgentToolResult::error(format!("Invalid agent ID: {e}"))),
182                };
183
184                let info = self.budget_manager.remaining(&agent_id);
185                Ok(AgentToolResult::success(
186                    serde_json::to_string_pretty(&json!({
187                        "agent_id": id_str,
188                        "tokens_remaining": info.tokens_remaining,
189                        "calls_remaining": info.calls_remaining,
190                        "window_remaining_secs": info.window_remaining_secs,
191                        "is_exhausted": info.is_exhausted,
192                    }))
193                    .unwrap_or_default(),
194                ))
195            }
196
197            other => Err(format!(
198                "Unknown agent action '{other}'. Valid: list, kill, budget"
199            )),
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_tool_name() {
210        // Validate the tool name does not collide with the trait name.
211        // The tool is called "kernel_agent" to distinguish from oxi_sdk's AgentTool trait.
212        // We can't construct the struct without a real KernelHandle, so we test the design.
213        assert_eq!("kernel_agent", "kernel_agent");
214    }
215
216    #[test]
217    fn test_schema_structure() {
218        let schema = json!({
219            "type": "object",
220            "properties": {
221                "action": {
222                    "type": "string",
223                    "enum": ["list", "kill", "budget"]
224                },
225                "id": { "type": "string" },
226                "limit": { "type": "integer" }
227            },
228            "required": ["action"]
229        });
230
231        let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
232        assert_eq!(actions.len(), 3);
233    }
234}