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 async_trait::async_trait;
15use std::sync::Arc;
16
17use oxi_sdk::{AgentTool as OxiAgentTool, AgentToolResult, ToolContext};
18use serde_json::{Value, json};
19
20use crate::budget::BudgetManager;
21use crate::kernel_handle::KernelHandle;
22use crate::supervisor::Supervisor;
23use crate::types::AgentId;
24
25/// Agent tool for agent lifecycle management.
26///
27/// Wraps the `AgentApi` domain of the `KernelHandle`. Allows agents
28/// to query peer status, terminate agents, and check budget state.
29///
30/// ## Actions
31///
32/// | Action   | Description               | Required params | Optional params |
33/// |----------|---------------------------|-----------------|-----------------|
34/// | `list`   | List running agents       | —               | `limit`         |
35/// | `kill`   | Kill a running agent      | `id`            | —               |
36/// | `budget` | Check budget for an agent | `id`            | —               |
37///
38/// **Note:** Named `AgentTool` in this module but re-exported as
39/// `KernelAgentTool` to avoid collision with oxi_sdk's `AgentTool` trait.
40pub struct AgentTool {
41    supervisor: Arc<dyn Supervisor>,
42    budget_manager: Arc<BudgetManager>,
43}
44
45impl AgentTool {
46    /// Create a new `AgentTool` from a `KernelHandle`.
47    ///
48    /// Extracts the `Supervisor` and `BudgetManager` Arcs from the
49    /// kernel's Agent API.
50    pub fn from_kernel(kernel: &KernelHandle) -> Self {
51        Self {
52            supervisor: kernel.agents.supervisor.clone(),
53            budget_manager: kernel.agents.budget_manager.clone(),
54        }
55    }
56}
57
58impl std::fmt::Debug for AgentTool {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("AgentTool (kernel)").finish()
61    }
62}
63
64#[async_trait]
65
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<tokio::sync::oneshot::Receiver<()>>,
111        _ctx: &ToolContext,
112    ) -> Result<AgentToolResult, oxi_sdk::ToolError> {
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) => {
163                        return Ok(AgentToolResult::error(format!("Invalid agent ID: {e}")));
164                    }
165                };
166
167                match self.supervisor.kill(agent_id).await {
168                    Ok(()) => Ok(AgentToolResult::success(format!(
169                        "Agent '{id_str}' killed."
170                    ))),
171                    Err(e) => Ok(AgentToolResult::error(format!("Failed to kill agent: {e}"))),
172                }
173            }
174
175            "budget" => {
176                let id_str = params
177                    .get("id")
178                    .and_then(|v| v.as_str())
179                    .ok_or_else(|| "budget requires 'id' parameter".to_string())?;
180
181                let agent_id: AgentId = match uuid::Uuid::parse_str(id_str) {
182                    Ok(id) => id,
183                    Err(e) => {
184                        return Ok(AgentToolResult::error(format!("Invalid agent ID: {e}")));
185                    }
186                };
187
188                let info = self.budget_manager.remaining(&agent_id);
189                Ok(AgentToolResult::success(
190                    serde_json::to_string_pretty(&json!({
191                        "agent_id": id_str,
192                        "tokens_remaining": info.tokens_remaining,
193                        "calls_remaining": info.calls_remaining,
194                        "window_remaining_secs": info.window_remaining_secs,
195                        "is_exhausted": info.is_exhausted,
196                    }))
197                    .unwrap_or_default(),
198                ))
199            }
200
201            other => Err(format!(
202                "Unknown agent action '{other}'. Valid: list, kill, budget"
203            )),
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_tool_name() {
214        // Validate the tool name does not collide with the trait name.
215        // The tool is called "kernel_agent" to distinguish from oxi_sdk's AgentTool trait.
216        // We can't construct the struct without a real KernelHandle, so we test the design.
217        assert_eq!("kernel_agent", "kernel_agent");
218    }
219
220    #[test]
221    fn test_schema_structure() {
222        let schema = json!({
223            "type": "object",
224            "properties": {
225                "action": {
226                    "type": "string",
227                    "enum": ["list", "kill", "budget"]
228                },
229                "id": { "type": "string" },
230                "limit": { "type": "integer" }
231            },
232            "required": ["action"]
233        });
234
235        let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
236        assert_eq!(actions.len(), 3);
237    }
238}