use std::sync::Arc;
use async_trait::async_trait;
use oxi_sdk::{AgentTool as OxiAgentTool, AgentToolResult, ToolContext};
use serde_json::{json, Value};
use tokio::sync::oneshot;
use crate::budget::BudgetManager;
use crate::kernel_handle::KernelHandle;
use crate::supervisor::Supervisor;
use crate::types::AgentId;
pub struct AgentTool {
supervisor: Arc<dyn Supervisor>,
budget_manager: Arc<BudgetManager>,
}
impl AgentTool {
pub fn from_kernel(kernel: &KernelHandle) -> Self {
Self {
supervisor: kernel.agents.supervisor.clone(),
budget_manager: kernel.agents.budget_manager.clone(),
}
}
}
impl std::fmt::Debug for AgentTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentTool (kernel)").finish()
}
}
#[async_trait]
impl OxiAgentTool for AgentTool {
fn name(&self) -> &str {
"kernel_agent"
}
fn label(&self) -> &str {
"Agent Management"
}
fn description(&self) -> &'static str {
"Manage agents — list running agents, kill an agent, or check an agent's budget. \
Actions: list, kill, budget."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "kill", "budget"],
"description": "Agent operation to perform"
},
"id": {
"type": "string",
"description": "Agent UUID (required for kill and budget)"
},
"limit": {
"type": "integer",
"description": "Maximum number of agents to return (list action, default 50)"
}
},
"required": ["action"]
})
}
async fn execute(
&self,
_tool_call_id: &str,
params: Value,
_signal: Option<oneshot::Receiver<()>>,
_ctx: &ToolContext,
) -> Result<AgentToolResult, String> {
let action = params
.get("action")
.and_then(|v| v.as_str())
.ok_or_else(|| "Missing required parameter: action".to_string())?;
match action {
"list" => {
let limit = params["limit"].as_u64().unwrap_or(50) as usize;
let agents = match self.supervisor.list().await {
Ok(a) => a,
Err(e) => {
return Ok(AgentToolResult::error(format!(
"Failed to list agents: {e}"
)))
}
};
if agents.is_empty() {
return Ok(AgentToolResult::success("No agents currently running."));
}
let display: Vec<Value> = agents
.into_iter()
.take(limit)
.map(|info| {
json!({
"id": info.id.to_string(),
"name": info.name,
"status": format!("{:?}", info.status),
})
})
.collect();
let count = display.len();
Ok(AgentToolResult::success(
serde_json::to_string_pretty(&json!({ "agents": display, "count": count }))
.unwrap_or_default(),
))
}
"kill" => {
let id_str = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| "kill requires 'id' parameter".to_string())?;
let agent_id: AgentId = match uuid::Uuid::parse_str(id_str) {
Ok(id) => id,
Err(e) => return Ok(AgentToolResult::error(format!("Invalid agent ID: {e}"))),
};
match self.supervisor.kill(agent_id).await {
Ok(()) => Ok(AgentToolResult::success(format!(
"Agent '{}' killed.",
id_str
))),
Err(e) => Ok(AgentToolResult::error(format!("Failed to kill agent: {e}"))),
}
}
"budget" => {
let id_str = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| "budget requires 'id' parameter".to_string())?;
let agent_id: AgentId = match uuid::Uuid::parse_str(id_str) {
Ok(id) => id,
Err(e) => return Ok(AgentToolResult::error(format!("Invalid agent ID: {e}"))),
};
let info = self.budget_manager.remaining(&agent_id);
Ok(AgentToolResult::success(
serde_json::to_string_pretty(&json!({
"agent_id": id_str,
"tokens_remaining": info.tokens_remaining,
"calls_remaining": info.calls_remaining,
"window_remaining_secs": info.window_remaining_secs,
"is_exhausted": info.is_exhausted,
}))
.unwrap_or_default(),
))
}
other => Err(format!(
"Unknown agent action '{}'. Valid: list, kill, budget",
other
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_name() {
assert_eq!("kernel_agent", "kernel_agent");
}
#[test]
fn test_schema_structure() {
let schema = json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "kill", "budget"]
},
"id": { "type": "string" },
"limit": { "type": "integer" }
},
"required": ["action"]
});
let actions = schema["properties"]["action"]["enum"].as_array().unwrap();
assert_eq!(actions.len(), 3);
}
}