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