oxios_kernel/tools/kernel/
agent_tool.rs1use 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
26pub struct AgentTool {
42 supervisor: Arc<dyn Supervisor>,
43 budget_manager: Arc<BudgetManager>,
44}
45
46impl AgentTool {
47 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 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 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}